Compare commits

..

No commits in common. "main" and "codex/back-to-766b5af" have entirely different histories.

122 changed files with 2876 additions and 14716 deletions

View File

@ -16,9 +16,7 @@ description: Navisworks API 开发助手,用于开发 Navisworks 插件。功
| NET API | `doc/navisworks_api/NET/documentation/NET API.chm` | CHM 帮助文件 |
| NET API HTML | `doc/navisworks_api/NET/documentation/NetAPIHtml/` | HTML 文档 |
**推荐导航入口**: `doc/navisworks_api/NET/documentation/NetAPIHtml/index.html`
**原始 HTML 文档入口**: `doc/navisworks_api/NET/documentation/NetAPIHtml/html/index.html`
**HTML 文档入口**: `doc/navisworks_api/NET/documentation/NetAPIHtml/html/index.html`
### API 文档搜索方法

View File

@ -1,8 +1,3 @@
---
name: project-tools
description: Use when writing code that needs tool utilities (DialogHelper, UnitsConverter, LogManager, GeometryHelper, PathHelper, ModelHighlightHelper) in the NavisworksTransport project. Check this skill before writing new utility code to avoid reinventing existing tools.
---
# NavisworksTransport 项目工具类使用指南
## 概述

2
.gitignore vendored
View File

@ -15,5 +15,3 @@ navisworks_api/
*.exe
*.db
.codex-temp

445
AGENTS.md
View File

@ -1,148 +1,441 @@
# AGENTS.md
面向后续 AI 编码助手。让新会话快速理解项目现状、稳定架构、不可破坏的原则、排查入口。
本文件面向后续会话中的 AI 编码助手。目标不是写一份“大而全”的历史说明,而是让新会话能快速理解:
- 项目现在在做什么
- 哪些架构已经稳定
- 哪些开发原则不能再破坏
- 遇到问题时优先看哪里
如果本文件与更细的专项文档冲突,优先参考:
1. `doc/working/current-engineering-state.md`
2. `doc/design/2026/coordinate-system-canonical-space-design.md`
3. `doc/design/2026/NavisworksAPI使用方法.md`
---
## 1. 项目现状
**NavisworksTransport** — Autodesk Navisworks Manage 2026 物流路径规划与动画仿真插件。
**NavisworksTransport** 是一个面向 **Autodesk Navisworks Manage 2026**物流路径规划与动画仿真插件。
核心能力:物流属性分类 · Ground/Hoisting/Rail 三类路径 · 虚拟/真实物体 · 终端安装仿真 · ClashDetective 碰撞检测 · 路径/检测数据存储。
当前已经不只是“自动寻路”项目,而是一套同时覆盖以下能力的工程:
- 物流属性与对象分类
- 地面路径、吊装路径、Rail 路径编辑与可视化
- 终端安装仿真
- 动画播放、起点落位、终点诊断
- ClashDetective 碰撞检测与恢复
- 路径、检测记录、批处理相关数据存储
### 当前重点功能
- `Ground / Hoisting / Rail` 三类路径
- 真实物体与虚拟物体的起点摆放、动画姿态、通行空间
- `YUp / ZUp` 两类宿主模型坐标系支持
- 终端安装仿真中的:
- 端面三点分析
- 光轴辅助线
- 安装面双点确定
- 安装点与 Rail 法向联动
---
## 2. 技术栈与构建
- .NET Framework 4.8 · C# 7.3 · x64 · WPF + DockPane · MSTest
- 平台Navisworks Manage 2026
- 框架:.NET Framework 4.8
- 语言C# 7.3
- 架构x64
- UIWPF + Navisworks DockPane
- 测试MSTest
### 构建流水线(必须串行,不可并行)
### 关键脚本
```bash
./compile.bat # 1. 编译
./deploy-plugin.bat # 2. 等待编译确认成功后部署
```
- `compile.bat`
- `run-unit-tests.bat`
- `deploy-plugin.bat`
**严禁**:加 `cmd.exe /c` 前缀、直接调 MSBuild、拆 PowerShell 逻辑、改 `powershell``pwsh`
### 极重要的执行顺序
### 并行边界
构建和部署必须严格按顺序执行:
| 允许并行(纯读取) | 禁止并行(产出/锁文件) |
|---|---|
| rg, ls, Get-Content, 读日志 | compile, deploy, run-unit-tests, 启动 NW |
1. `./run-unit-tests.bat`(需要时)
2. `./compile.bat`
3. **等待编译完整结束并确认成功**
4. `./deploy-plugin.bat`
`run-unit-tests → compile → deploy` 是单通道流水线,不可拆分并行
不要并行执行编译和部署。否则很容易把旧 DLL 部署到插件目录
### 路径
### 插件部署目录
- 插件部署:`C:\ProgramData\Autodesk\Navisworks Manage 2026\plugins\TransportPlugin\`
- 日志:`...\plugins\TransportPlugin\logs\debug.log`
- `C:\ProgramData\Autodesk\Navisworks Manage 2026\plugins\TransportPlugin\`
日志目录:
- `C:\ProgramData\Autodesk\Navisworks Manage 2026\plugins\TransportPlugin\logs\debug.log`
---
## 3. 目录与关键文件
## 3. 目录与职责
| 目录 | 职责 |
|---|---|
| `src/Core/` | 插件入口、路径管理、动画、碰撞、渲染、配置 |
| `src/UI/WPF/` | 视图、ViewModel |
| `src/Utils/` | 单位、几何、坐标、变换、日志 |
| `src/PathPlanning/` | 网格、A*、路径几何 |
| `UnitTests/` | 回归测试 |
### 核心目录
**高风险区域**(改动需先补测试):`src/Core/Animation` · `src/Utils/CoordinateSystem` · `src/UI/WPF/ViewModels`
- `src/Core/`
- 插件主入口、路径管理、动画、碰撞、渲染、配置
- `src/UI/WPF/`
- 视图、ViewModel、交互命令
- `src/Utils/`
- 单位、几何、坐标、变换、日志等公共工具
- `src/PathPlanning/`
- 网格、A*、路径几何与优化
- `UnitTests/`
- 数学层、工具层、姿态层的回归测试
### 当前最关键的文件
- `src/Core/Animation/PathAnimationManager.cs`
- `src/Core/VirtualObjectManager.cs`
- `src/Core/PathPointRenderPlugin.cs`
- `src/UI/WPF/ViewModels/AnimationControlViewModel.cs`
- `src/UI/WPF/ViewModels/PathEditingViewModel.cs`
- `src/Utils/CoordinateSystem/HostCoordinateAdapter.cs`
- `src/Utils/CoordinateSystem/CanonicalPlanarPoseBuilder.cs`
- `src/Utils/CoordinateSystem/CanonicalRailPoseBuilder.cs`
- `src/Utils/CoordinateSystem/CanonicalTrackedPositionResolver.cs`
- `src/Utils/CoordinateSystem/RotatedObjectExtentHelper.cs`
- `src/Utils/RailPathPoseHelper.cs`
- `src/Utils/ModelItemTransformHelper.cs`
---
## 4. 核心架构规则
## 4. 当前稳定架构
### 4.1 坐标系三层语义
只用三种说法,禁止「本地坐标系」:
以后统一使三种说法:
- **宿主坐标系** — NW 文档坐标系YUp 或 ZUpUI/日志/拾取均以此为基准
- **内部坐标系 (Canonical Space)** — 固定 ZUp纯数学计算在此完成
- **资产坐标系** — 插件资源专属unit_cube.nwc, unit_cylinder.nwc
- **宿主坐标系**
- Navisworks 文档坐标系
- `YUp``ZUp`
- UI 输入输出、日志、拾取结果都按这一层解释
### 4.2 命名规则
- **内部坐标系**
- 项目内部统一使用的 `Canonical Space`
- 固定 `ZUp`
- 纯数学姿态和几何计算优先在这里完成
变量/字段/日志中的方向语义必须带坐标系前缀:
- **资产坐标系**
- 只属于插件自带资源
- 当前主要是:
- 虚拟物体 `unit_cube.nwc`
- 参考杆 `unit_cylinder.nwc`
- 前缀:`Host` / `Canonical` / `Asset` / `Local`(仅对象自身局部轴)
- 禁止无前缀的 `PositiveX` / `upAxis` / `forwardAxis`
禁止再使用“本地坐标系”这种含糊说法。
### 4.3 对象局部轴业务映射
### 4.1.1 Quaternion / Rotation3D 固定定义
不是第四套坐标系,而是业务解释层:把对象哪根局部轴解释为 forward/up/side。
这是项目级硬约束,不允许在任何修复中重新猜测、重新验证或临时改口:
- 真实物体:**不要依赖 `ModelItem.Transform`**Revit 导入件多为单位旋转),优先通过 fragment 代表姿态 + Fragment默认Up 解释
- 虚拟物体:对象局部轴业务映射来自资产轴约定
### 4.4 Quaternion 固定定义(不可再质疑)
- Navisworks `Rotation3D(double, double, double, double)` 的参数顺序固定为:
- `x, y, z, w`
- `Rotation3D.A/B/C/D` 与 quaternion 分量的对应关系固定为:
- `A = x`
- `B = y`
- `C = z`
- `D = w`
- 当代码里已经拿到 quaternion `(qx, qy, qz, qw)` 时,唯一允许的构造方式是:
```csharp
// Rotation3D 参数顺序固定为 x, y, z, w
var rotation = new Rotation3D(qx, qy, qz, qw);
// A=x, B=y, C=z, D=w
```
禁止写成 `new Rotation3D(qw, qx, qy, qz)`。姿态问题不要再归因到「四元数顺序可能不对」。
- 严禁再写成:
### 4.5 真实物体 vs 虚拟物体
```csharp
var rotation = new Rotation3D(qw, qx, qy, qz); // 错误
```
| | 真实物体 | 虚拟物体 |
|---|---|---|
| 坐标系 | 无独立资产坐标系,生活在宿主坐标系 | 有资产坐标系 |
| 角度调整 X/Y/Z | 按宿主坐标系 | 按资产轴约定 |
| 资源要求 | — | unit_cube.nwc 几何中心在原点 |
| 姿态来源 | fragment 代表姿态 | 资产轴约定 |
- 以后遇到姿态问题,禁止再把故障归因到 “Navisworks 四元数分量顺序可能又不一样了”。
- 如果问题涉及真实物体或 fragment 参考姿态的轴语义,优先检查:
- fragment 三轴的业务解释是否正确
- 参考姿态是否应该用显式三轴而不是只传 quaternion
- 宿主坐标系 / 内部坐标系 / 资产坐标系 是否被混用
### 4.6 路径姿态链
### 4.2 真实物体 vs 虚拟物体
- **Ground/Hoisting**:走平面姿态链,已禁止退回旧 yaw。起点/逐帧/终点/通行空间共享同一套尺寸语义。业务跟踪点 = 原始包围盒中心。
- **Rail**:并入 `canonical → rail pose`不能在宿主空间随意补旋转。Rail 0° 基线不可被角度修正污染。
这是当前架构里最容易被改坏的地方。
### 4.7 通行空间
#### 真实物体
必须和起点落位/逐帧位置/终点诊断共用同一套尺寸语义。典型症状「通行空间正确但物体陷入地面」「YUp 下 Y/Z 互换」。
- 没有独立资产坐标系
- 可以视为直接生活在宿主坐标系里
- 角度调整对话框里的 `X/Y/Z`,对真实物体应按**宿主坐标系**消费
#### 真实物体参考姿态
- 真实物体不要再直接依赖 `ModelItem.Transform.Factor().Rotation`
- 对很多 Revit 导入件,这个值是单位旋转,不代表真实显示姿态
- 真实物体参考姿态应优先来自 fragment 代表姿态
- `Fragment默认Up` 的用途是:
- 把 fragment 参考框架解释成当前宿主坐标语义下的真实姿态
- 不是仅仅挑一个“竖直候选轴”
- 在真实物体链路里必须先完成“fragment 姿态解释”,再谈:
- 前进方向
- 路径对齐
- 角度调整
- `_trackedRotation` 对真实物体必须优先跟踪“解释后的真实参考姿态”,不能回退成 `Transform` 的单位旋转
#### 虚拟物体
- 有明确资产坐标系
- 当前依赖 `unit_cube.nwc` 资源
- 资源必须满足:
- 几何中心在原点
- 原点处 `BoundingBox.Center == (0,0,0)`
- 如果生产环境里虚拟物体固定偏移,先检查部署的 `unit_cube.nwc` 是否为最新资源,不要先怀疑代码
### 4.3 路径姿态链
#### Ground / Hoisting
- 走平面姿态链
- 已禁止偷偷退回旧 `yaw` 方案
- 起点、逐帧、终点、通行空间,必须共享同一套尺寸语义
#### Rail
- 不能像平面路径那样在宿主空间随意补旋转
- 必须并入 `canonical -> rail pose`
- `Rail 0°` 基线必须稳定,不能被角度修正逻辑污染
- `Rail` 真实物体不应再退回默认 `PositiveX / PositiveY`
- 三类路径都必须先复用同一个“对象姿态解释层”:
- `Ground / Hoisting`
- `forward = 路径方向`
- `up = 宿主 up`
- `Rail`
- `forward = rail 切向`
- `up = rail 法向 / preferred normal`
- 统一的是“对象参考姿态来源与解释方式”,不是把三类路径都强行改成同一个 `up`
### 4.4 通行空间与物体姿态的关系
通行空间、起点落位、逐帧位置、终点诊断,必须尽量共用同一套尺寸/法线语义。
典型错误信号:
- 通行空间正确,但物体陷入地面
- 物体姿态正确,但通行空间轴搞反
- `YUp``Y/Z` 表现互换
遇到这类问题,优先检查:
- 是否一条链用了“原始高度”
- 另一条链用了“旋转后法线尺寸”
- `Rail` 真实物体尤其要检查:
- 起点/逐帧中心偏移是否仍在用原始 `objectHeight`
- 通行空间是否仍在用旧 `RailAssetConvention`
- 通行空间、路径偏移、最终姿态是否共享同一套“最终姿态尺寸语义”
### 4.5 终端安装仿真
当前稳定链路是:
1. 捕获终点箱体
2. 分析端面3点
3. 选择安装点(当前已是 2 点确定安装面)
4. 生成辅助线 / 安装面 / 安装点
5. 取起点并生成路径
当前 UI 上:
- 终端安装仿真是独立区块
- 不再夹在路径编辑中间
- 安装方式选择只保留一处
---
## 5. 开发原则
1. **彻底禁止 fallback**(第一原则)— 新链失败时暴露错误,不许静默退回旧链/yaw/hardcoded Z-up/默认值
2. **不向后兼容** — 只针对 NW 2026
3. **临时补丁必须清理** — 定位问题的临时代码,确认不是根因后必须删除
4. **优先复用现有工具**`UnitsConverter`, `GeometryHelper`, `LogManager`, `HostCoordinateAdapter`, `Canonical*`, `ModelItemTransformHelper`, `RailPathPoseHelper`
5. **测试优先于猜测** — 几何/旋转/坐标问题按:看日志 → 补日志 → 补单测 → 改代码
### 5.1 不向后兼容
项目只针对 Navisworks 2026。不要写旧版本兼容代码。
### 5.2 不要随意加 fallback
不要为了“先跑起来”就:
- 偷偷退回旧 `yaw`
- 偷偷用硬编码 `Z-up`
- 偷偷在错误时给默认值掩盖问题
如果完整姿态链失败,应优先暴露问题并修根因。
### 5.3 临时补丁不是正式实现
为定位问题临时加入的:
- 强制刷新
- 额外同步
- 再调一次方法
- UI 和稀泥补丁
如果最后证明它不是真正根因,修完后必须删掉,不能残留在正式代码里。
### 5.4 优先复用现有工具
尤其优先看:
- `UnitsConverter`
- `GeometryHelper`
- `LogManager`
- `HostCoordinateAdapter`
- `Canonical*` 姿态工具
- `ModelItemTransformHelper`
- `RailPathPoseHelper`
不要在业务层手搓一套新的矩阵、坐标变换或尺寸投影公式。
### 5.5 测试优先于猜测
对几何/旋转/坐标问题,优先顺序应是:
1. 看日志
2. 日志不够就补日志
3. 先补单元测试
4. 再改代码
不要在没有锁住语义之前反复试错改实现。
---
## 6. 单位原则
内部一律用**模型单位**UI和外部交互使用**米单位**。米单位变量以 `InMeters` 结尾,模型单位不加后缀。
所有路径计算、网格计算、包络尺寸、位移偏移,内部一律使用**模型单位**。
优先用 `UnitsConverter.GetMetersToUnitsConversionFactor()` / `ConvertToMeters()` / `ConvertFromMeters()`
命名规则:
- 米单位:变量名以 `InMeters` 结尾
- 模型单位:变量名不加后缀
不要混用。
优先使用:
- `UnitsConverter.GetMetersToUnitsConversionFactor()`
- `UnitsConverter.GetUnitsToMetersConversionFactor()`
- `UnitsConverter.ConvertToMeters(...)`
- `UnitsConverter.ConvertFromMeters(...)`
---
## 7. 排查指引
## 7. 常见问题的排查入口
| 问题 | 优先检查 |
|---|---|
| 虚拟物体固定偏差 | 部署目录 unit_cube.nwc → BoundingBox.Center 是否为 (0,0,0) → 起点代码 |
| 真实物体旋转轴不对 | 目标姿态算错 or NW 应用姿态错,看 `[动画姿态入口]` / `[模型增量姿态]` |
| 吊装路径不显示 | 渲染链退化段/零长度段 |
| 路径越走越偏 | 是否把实时 BoundingBox.Center 误当业务跟踪点 |
| 设为终点后列表空 | UIStateManager 队列消费 |
### 7.1 虚拟物体固定偏差
先查:
1. 部署目录下的 `resources\\unit_cube.nwc`
2. 原点处 `BoundingBox.Center` 是否是 `(0,0,0)`
3. 再查起点/归位代码
### 7.2 真实物体旋转轴不对
先区分:
- 目标姿态算错
- 还是 Navisworks 应用姿态错
优先看:
- `[动画姿态入口]`
- `[模型增量姿态]`
### 7.3 吊装路径不显示
优先检查渲染链里是否有退化段/零长度段导致整条渲染失败。
### 7.4 “设为终点直接结束”后列表还是空
优先看 `UIStateManager` 队列消费是否吃掉了后续 UI 事件,不要先怀疑坐标系。
---
## 8. 推荐阅读
## 8. 资源与部署注意事项
1. 本文件
### 8.1 虚拟物体资源
当前部署脚本会部署:
- `resources\\unit_cube.nwc`
- `resources\\unit_cylinder.nwc`
虚拟物体和参考杆问题,必须同时检查:
- 仓库里的资源
- `bin\\x64\\Release\\resources`
- 插件部署目录下的 `resources`
### 8.2 deploy-plugin.bat 当前规则
- 走白名单部署
- 不部署测试 DLL
- 不部署 Navisworks 自带 API DLL
不要把它改回“复制所有 dll”。
---
## 9. 推荐阅读顺序
新会话接手本项目时,推荐顺序:
1. 本文件 `AGENTS.md`
2. `doc/working/current-engineering-state.md`
3. `doc/design/2026/coordinate-system-canonical-space-design.md`
4. `doc/design/2026/NavisworksAPI使用方法.md`
5. `.agents/skills/geometry-transform/SKILL.md`(几何/变换/姿态/坐标)
6. `.agents/skills/nw-api/SKILL.md`
5. 相关专项 skill
- `.agents/skills/nw-api/SKILL.md`
- `.agents/skills/geometry-transform/SKILL.md`
---
## 10. 当前对子代理和 skill 的约定
仓库中已经有几何/变换专项 skill
- `.agents/skills/geometry-transform/SKILL.md`
它负责沉淀以下内容:
- 坐标系变换
- 物体姿态
- 物体位移与归位
- 虚拟物体资源定位
- 通行空间几何
- Navisworks 变换 API 使用规则
后续凡是几何/旋转/位移问题,优先沿这套 skill 与工具链继续维护,不要重新发明一套术语和流程。
---
## 11. 最后的硬约束
1. 不要混淆宿主坐标系、内部坐标系、资产坐标系
2. 不要在宿主空间随意补旋转,先判断是否应并入现有姿态链
3. 不要让虚拟物体和真实物体共享含糊的状态分支
4. 不要让通行空间和真实物体使用两套不同的尺寸语义
5. 不要把临时补丁留在正式实现里
6. 不要绕过测试直接改几何核心逻辑
如果你要改动:
- `PathAnimationManager`
- `VirtualObjectManager`
- `PathPointRenderPlugin`
- `AnimationControlViewModel`
- `HostCoordinateAdapter`
- `Canonical*PoseBuilder`
- `ModelItemTransformHelper`
请默认这是高风险改动,先补测试,再动实现。

View File

@ -1,22 +1,5 @@
# NavisworksTransport 变更日志
## [0.15.1] - 2026-05-26
### 🐛 Bug修复
- **角度输入框清空后崩溃**:修复调整物体角度窗口清空输入框后反复点确认导致的崩溃。根因是 `UpdateSourceTrigger=LostFocus` 在空输入时触发 WPF 绑定验证异常,阻止焦点转移到按钮。改为 `Explicit` 模式,由代码显式控制绑定更新。
- **Y轴角度修正动画时丢失**:修复调整物体 Y 轴角度YUp 宿主上轴)后,动画播放时物体转回原角度的问题。根因是 `ApplyPlanarTrackedPose``ResolveAnimationFramePlaybackYawRadians` 已叠加修正值的基础上再次叠加,导致上轴修正被双重应用。
- **先打开文档再加载插件时数据库未连接**:修复用户先打开 Navisworks 模型文件再加载插件时,路径列表为空的问题。根因是数据库连接逻辑只在 `Models.CollectionChanged` 事件(文档刚打开)中触发,文档已打开再加载插件时错过了该事件。在 `LogisticsControlPanel.InitializePathPlanningManager` 中新增数据库连接,覆盖此场景。
### 🔧 重构
- 在 `EditRotationWindow``TryUpdateNumberTextBox` 中将 `IsIntermediateText``TryParseNumber` 拆分独立判断,并增加 `UpdateSource` 异常捕获。
- 将 `MainPlugin` 中两处数据库连接逻辑抽取为 `TryConnectPathDatabase` 辅助方法,保留 `createIfMissing: false` 的懒加载模式。
---
## [0.15.0] - 2026-02-20
### 🎯 物流运输路径规划系统全面完善 - 路径曲线化、虚拟动画与批处理系统

View File

@ -42,9 +42,6 @@
<Reference Include="Microsoft.VisualStudio.TestPlatform.TestFramework.Extensions">
<HintPath>packages\MSTest.TestFramework.3.0.4\lib\net462\Microsoft.VisualStudio.TestPlatform.TestFramework.Extensions.dll</HintPath>
</Reference>
<Reference Include="Newtonsoft.Json">
<HintPath>packages\Newtonsoft.Json.13.0.3\lib\net45\Newtonsoft.Json.dll</HintPath>
</Reference>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Numerics" />
@ -57,24 +54,15 @@
</ItemGroup>
<ItemGroup>
<Compile Include="UnitTests\Core\PathHelperTests.cs" />
<Compile Include="UnitTests\Core\SelectionClipBoxLockStateTests.cs" />
<Compile Include="UnitTests\Core\PathPlanningManagerHoistingCompletionTests.cs" />
<Compile Include="UnitTests\Core\PathPersistenceTests.cs" />
<Compile Include="UnitTests\Core\PathRouteCloneTests.cs" />
<Compile Include="UnitTests\CoordinateSystem\AutoPathPlanningCoordinateSemanticsTests.cs" />
<Compile Include="UnitTests\Integration\AutoPathGridGenerationAutomationTests.cs" />
<Compile Include="UnitTests\Integration\NavisworksTestAutomationClient.cs" />
<Compile Include="UnitTests\Integration\VirtualCollisionAutomationTests.cs" />
<Compile Include="UnitTests\CoordinateSystem\HostCoordinateAdapterTests.cs" />
<Compile Include="UnitTests\CoordinateSystem\CanonicalRailPoseBuilderTests.cs" />
<Compile Include="UnitTests\CoordinateSystem\CanonicalPlanarPoseBuilderTests.cs" />
<Compile Include="UnitTests\CoordinateSystem\ObjectSpaceOrientationHelperTests.cs" />
<Compile Include="UnitTests\CoordinateSystem\ObjectStartPlacementRequestTests.cs" />
<Compile Include="UnitTests\CoordinateSystem\ObjectPassageProjectionOptimizerTests.cs" />
<Compile Include="UnitTests\CoordinateSystem\CanonicalRailOffsetResolverTests.cs" />
<Compile Include="UnitTests\CoordinateSystem\CanonicalTrackedPositionResolverTests.cs" />
<Compile Include="UnitTests\CoordinateSystem\GroundPathObjectLiftOffsetTests.cs" />
<Compile Include="UnitTests\CoordinateSystem\GroundPassageSpaceOffsetTests.cs" />
<Compile Include="UnitTests\CoordinateSystem\FragmentRepresentativePoseHelperTests.cs" />
<Compile Include="UnitTests\CoordinateSystem\HoistingCoordinateHelperTests.cs" />
<Compile Include="UnitTests\CoordinateSystem\HoistingRealObjectPoseHelperTests.cs" />
@ -85,12 +73,9 @@
<Compile Include="UnitTests\CoordinateSystem\RealObjectReferencePoseResolverTests.cs" />
<Compile Include="UnitTests\CoordinateSystem\RealObjectRailAxisConventionResolverTests.cs" />
<Compile Include="UnitTests\CoordinateSystem\RealObjectRailExtentResolverTests.cs" />
<Compile Include="UnitTests\CoordinateSystem\RotatedObjectExtentHelperTests.cs" />
<Compile Include="UnitTests\CoordinateSystem\PathTargetFrameResolverTests.cs" />
<Compile Include="UnitTests\CoordinateSystem\AssemblyEndFaceAnalyzerTests.cs" />
<Compile Include="UnitTests\CoordinateSystem\AssemblyInstallationReferenceBuilderTests.cs" />
<Compile Include="UnitTests\CoordinateSystem\RailAssemblyWorkflowContextTests.cs" />
<Compile Include="UnitTests\CoordinateSystem\ViewpointHelperTests.cs" />
<Compile Include="UnitTests\CoordinateSystem\PathPointVisualizationTests.cs" />
<Compile Include="UnitTests\CoordinateSystem\FragmentDefaultUpContextTests.cs" />
<Compile Include="UnitTests\CoordinateSystem\VirtualGroundPoseCharacterizationTests.cs" />

View File

@ -150,7 +150,6 @@
<Compile Include="src\Core\Services\TimeTagService.cs" />
<Compile Include="src\Core\Services\TimeTagExporter.cs" />
<Compile Include="src\Core\Services\TimeTagTimeLinerIntegration.cs" />
<Compile Include="src\Core\Services\TestAutomationHttpService.cs" />
<!-- Commands - Command Pattern Framework (for testing) -->
<Compile Include="src\Commands\IPathPlanningCommand.cs" />
<Compile Include="src\Commands\CommandBase.cs" />
@ -166,7 +165,6 @@
<Compile Include="src\Commands\SetLogisticsAttributeCommand.cs" />
<Compile Include="src\Commands\StartAnimationCommand.cs" />
<Compile Include="src\Commands\GenerateCollisionReportCommand.cs" />
<Compile Include="src\Commands\TransportRibbonHandler.cs" />
<Compile Include="src\Commands\VoxelGridSDFTestCommand.cs" />
<Compile Include="src\Commands\VoxelPathFindingTestCommand.cs" />
<!-- Core - Animation System -->
@ -286,7 +284,6 @@
<Compile Include="src\UI\WPF\ViewModels\ModelSettingsViewModel.cs" />
<Compile Include="src\UI\WPF\ViewModels\AnimationControlViewModel.cs" />
<Compile Include="src\UI\WPF\ViewModels\PathEditingViewModel.cs" />
<Compile Include="src\UI\WPF\ViewModels\RailAssemblyWorkflowContext.cs" />
<Compile Include="src\UI\WPF\ViewModels\SystemManagementViewModel.cs" />
<Compile Include="src\UI\WPF\ViewModels\CollisionReportViewModel.cs" />
<Compile Include="src\UI\WPF\ViewModels\PathAnalysisViewModel.cs" />
@ -309,7 +306,6 @@
<Compile Include="src\UI\WPF\Converters\BoolToOpacityConverter.cs" />
<Compile Include="src\UI\WPF\Converters\IndexConverter.cs" />
<Compile Include="src\UI\WPF\Converters\CountToVisibilityConverter.cs" />
<Compile Include="src\UI\WPF\Converters\EditableNumberConverter.cs" />
<Compile Include="src\UI\WPF\Converters\PathTypeConverter.cs" />
<Compile Include="src\UI\WPF\Converters\BatchQueueStatusConverter.cs" />
<Compile Include="src\UI\WPF\Converters\BatchQueueStatusHelper.cs" />
@ -340,7 +336,6 @@
<Compile Include="src\Utils\CoordinateSystem\CanonicalBounds3.cs" />
<Compile Include="src\Utils\CoordinateSystem\LocalAxisPoseBuilder.cs" />
<Compile Include="src\Utils\CoordinateSystem\LocalEulerRotationCorrection.cs" />
<Compile Include="src\Utils\CoordinateSystem\ObjectPassageProjectionOptimizer.cs" />
<Compile Include="src\Utils\CoordinateSystem\ObjectStartPlacementRequest.cs" />
<Compile Include="src\Utils\CoordinateSystem\CanonicalPlanarPoseBuilder.cs" />
<Compile Include="src\Utils\CoordinateSystem\CanonicalRailOffsetResolver.cs" />
@ -360,7 +355,6 @@
<Compile Include="src\Utils\CoordinateSystem\ObjectSpaceOrientationHelper.cs" />
<Compile Include="src\Utils\CoordinateSystem\RotatedObjectExtentHelper.cs" />
<Compile Include="src\Utils\CoordinateSystem\HostCoordinateAdapter.cs" />
<Compile Include="src\Utils\CoordinateSystem\HostPlanarGridHelper.cs" />
<Compile Include="src\Utils\CoordinateSystem\HoistingCoordinateHelper.cs" />
<Compile Include="src\Utils\CoordinateSystem\LocalAxisDirection.cs" />
<Compile Include="src\Utils\CoordinateSystem\ModelAxisConvention.cs" />
@ -373,7 +367,6 @@
<Compile Include="src\Utils\VisibilityHelper.cs" />
<Compile Include="src\Utils\NwdExportHelper.cs" />
<Compile Include="src\Utils\ModelItemTransformHelper.cs" />
<Compile Include="src\Utils\SelectionClipBoxLockState.cs" />
<Compile Include="src\Utils\CachedTriangle3D.cs" />
<Compile Include="src\Utils\ClipboardHelper.cs" />
<Compile Include="src\Utils\PathHelper.cs" />
@ -527,18 +520,6 @@
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<Link>resources\TransportPlugin.name.txt</Link>
</None>
<None Include="resources\TransportRibbon.xaml">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<Link>TransportRibbon.xaml</Link>
</None>
<None Include="resources\TransportRibbon_16.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<Link>TransportRibbon_16.png</Link>
</None>
<None Include="resources\TransportRibbon_32.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<Link>TransportRibbon_32.png</Link>
</None>
<!-- Unit Cube NWC Model File -->
<None Include="resources\unit_cube.nwc">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>

View File

@ -63,28 +63,6 @@ namespace NavisworksTransport.UnitTests.CoordinateSystem
Assert.IsFalse(result.IsReliable);
}
[TestMethod]
public void OrientNormalTowardTarget_ShouldFlip_WhenNormalOpposesTargetDirection()
{
Vector3 orientedNormal = AssemblyEndFaceAnalyzer.OrientNormalTowardTarget(
new Vector3(-1f, 0f, 0f),
Vector3.Zero,
new Vector3(10f, 0f, 0f));
AssertPoint(orientedNormal, 1.0, 0.0, 0.0);
}
[TestMethod]
public void OrientNormalTowardTarget_ShouldKeepDirection_WhenNormalAlreadyFacesTarget()
{
Vector3 orientedNormal = AssemblyEndFaceAnalyzer.OrientNormalTowardTarget(
new Vector3(0f, 1f, 0f),
new Vector3(1f, 2f, 3f),
new Vector3(1f, 5f, 3f));
AssertPoint(orientedNormal, 0.0, 1.0, 0.0);
}
private static IEnumerable<AnalysisTriangle3> CreateRectangleFace(double minX, double maxX, double minY, double maxY, double z)
{
yield return new AnalysisTriangle3(

View File

@ -1,405 +0,0 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using NavisworksTransport.Core;
using NavisworksTransport.PathPlanning;
using NavisworksTransport.Utils.CoordinateSystem;
using System.Numerics;
using System.Reflection;
using System.Collections.Generic;
using Autodesk.Navisworks.Api;
namespace NavisworksTransport.UnitTests.CoordinateSystem
{
[TestClass]
public class AutoPathPlanningCoordinateSemanticsTests
{
[TestMethod]
public void YUp_HostPlanarGridHelper_CreateHostPoint_ShouldApplyElevationOnHostY()
{
var adapter = new HostCoordinateAdapter(CoordinateSystemType.YUp);
Vector3 point = HostPlanarGridHelper.CreateHostPoint3(2.0, 3.0, 14.83, adapter);
AssertPoint(point, 2.0, 14.83, 3.0);
}
[TestMethod]
public void ZUp_HostPlanarGridHelper_CreateHostPoint_ShouldApplyElevationOnHostZ()
{
var adapter = new HostCoordinateAdapter(CoordinateSystemType.ZUp);
Vector3 point = HostPlanarGridHelper.CreateHostPoint3(2.0, 3.0, 6.25, adapter);
AssertPoint(point, 2.0, 3.0, 6.25);
}
[TestMethod]
public void YUp_HostPlanarGridHelper_GetAndSetElevation_ShouldUseHostY()
{
var adapter = new HostCoordinateAdapter(CoordinateSystemType.YUp);
var hostPoint = new Vector3(4.0f, 15.0f, 6.0f);
double elevation = HostPlanarGridHelper.GetElevation3(hostPoint, adapter);
Vector3 adjusted = HostPlanarGridHelper.SetElevation3(hostPoint, 20.0, adapter);
Assert.AreEqual(15.0, elevation, 1e-6);
AssertPoint(adjusted, 4.0, 20.0, 6.0);
}
[TestMethod]
public void YUp_HostPlanarGridHelper_GetHorizontalCoords_ShouldProjectToHostXZPlane()
{
var adapter = new HostCoordinateAdapter(CoordinateSystemType.YUp);
var hostPoint = new Vector3(8.0f, 14.83f, -5.35f);
(double h1, double h2) = HostPlanarGridHelper.GetHorizontalCoords3(hostPoint, adapter);
Assert.AreEqual(8.0, h1, 1e-6);
Assert.AreEqual(-5.35, h2, 1e-6);
}
[TestMethod]
public void YUp_HostPlanarGridHelper_HorizontalRange_ShouldMatchHostXZPlane()
{
var adapter = new HostCoordinateAdapter(CoordinateSystemType.YUp);
var hostMin = new Vector3(-10.0f, 2.0f, -30.0f);
var hostMax = new Vector3(40.0f, 12.0f, 5.0f);
(double min1, double max1, double min2, double max2) =
HostPlanarGridHelper.GetHorizontalRange3(hostMin, hostMax, adapter);
Assert.AreEqual(-10.0, min1, 1e-6);
Assert.AreEqual(40.0, max1, 1e-6);
Assert.AreEqual(-30.0, min2, 1e-6);
Assert.AreEqual(5.0, max2, 1e-6);
}
[TestMethod]
public void YUp_GridLikeWorldPointConstruction_ShouldWriteElevationToHostY()
{
var adapter = new HostCoordinateAdapter(CoordinateSystemType.YUp);
double originH1 = -210.0;
double originH2 = -10.0;
double cellSize = 1.0;
Vector3 worldPoint = HostPlanarGridHelper.CreateHostPoint3(
originH1 + 4 * cellSize,
originH2 + 6 * cellSize,
14.83,
adapter);
Assert.AreEqual(-206.0, worldPoint.X, 1e-6);
Assert.AreEqual(14.83, worldPoint.Y, 1e-6);
Assert.AreEqual(-4.0, worldPoint.Z, 1e-6);
}
[TestMethod]
public void YUp_GridLikeWorldToGrid_ShouldReadHostXZAsPlanarAxes()
{
var adapter = new HostCoordinateAdapter(CoordinateSystemType.YUp);
var origin = new Vector3(-210.0f, 14.83f, -10.0f);
var endPoint = new Vector3(-169.20f, 14.83f, -5.35f);
const double cellSize = 1.0;
(double pointH1, double pointH2) = HostPlanarGridHelper.GetHorizontalCoords3(endPoint, adapter);
(double originH1, double originH2) = HostPlanarGridHelper.GetHorizontalCoords3(origin, adapter);
int gridX = (int)System.Math.Round((pointH1 - originH1) / cellSize);
int gridY = (int)System.Math.Round((pointH2 - originH2) / cellSize);
Assert.AreEqual(41, gridX);
Assert.AreEqual(5, gridY);
}
[TestMethod]
public void ZUp_GridMap_CellPlanarBounds_ShouldMatchNearestNodeRoundSemantics()
{
var bounds = GridMap.CalculateNearestNodePlanarBounds(2.0, 3.0, 1.0);
Assert.AreEqual(1.5, bounds.minH1, 1e-9);
Assert.AreEqual(2.5, bounds.maxH1, 1e-9);
Assert.AreEqual(2.5, bounds.minH2, 1e-9);
Assert.AreEqual(3.5, bounds.maxH2, 1e-9);
}
[TestMethod]
public void ZUp_GridMap_SegmentIntersection_ShouldDetectInteriorNotBoundaryTouch()
{
var bounds = GridMap.CalculateNearestNodePlanarBounds(2.0, 3.0, 1.0);
bool crossesInterior = GridMap.TryGetSegmentRectangleInteriorIntersection(
1.75,
3.0,
2.25,
3.0,
bounds.minH1,
bounds.maxH1,
bounds.minH2,
bounds.maxH2,
1.0,
out double enterT,
out double exitT);
Assert.IsTrue(crossesInterior);
Assert.IsTrue(enterT >= 0.0 && enterT <= exitT && exitT <= 1.0);
bool onlyTouchesBoundary = GridMap.TryGetSegmentRectangleInteriorIntersection(
1.5,
2.0,
1.5,
4.0,
bounds.minH1,
bounds.maxH1,
bounds.minH2,
bounds.maxH2,
1.0,
out _,
out _);
Assert.IsFalse(onlyTouchesBoundary, "贴着归属边界行走不应被当成穿过格子内部。");
}
[TestMethod]
public void YUp_GridMapGenerator_GetObstacleElevationRange_ShouldUseHostYInsteadOfWorldZ()
{
var generator = new GridMapGenerator();
var method = typeof(GridMapGenerator).GetMethod(
"GetObstacleElevationRange",
BindingFlags.Instance | BindingFlags.NonPublic,
null,
new[]
{
typeof(double), typeof(double), typeof(double),
typeof(double), typeof(double), typeof(double),
typeof(CoordinateSystemType)
},
null);
Assert.IsNotNull(method, "未找到 GetObstacleElevationRange 私有方法。");
var elevationRange = ((double min, double max))method.Invoke(generator, new object[]
{
-200.0, 10.0, -30.0,
-198.0, 30.0, -10.0,
CoordinateSystemType.YUp
});
Assert.AreEqual(10.0, elevationRange.min, 1e-6, "YUp 下障碍物最小高程应来自宿主 Y。");
Assert.AreEqual(30.0, elevationRange.max, 1e-6, "YUp 下障碍物最大高程应来自宿主 Y。");
}
[TestMethod]
public void YUp_GridMapGenerator_CalculateBoundingBoxGridCoverage_ShouldProjectToHostXZPlane()
{
var generator = new GridMapGenerator();
var method = typeof(GridMapGenerator).GetMethod(
"CalculateGridCoverageFromWorldExtents",
BindingFlags.Instance | BindingFlags.NonPublic,
null,
new[]
{
typeof(double), typeof(double), typeof(double),
typeof(double), typeof(double), typeof(double),
typeof(double), typeof(double), typeof(double),
typeof(int), typeof(int), typeof(double),
typeof(CoordinateSystemType)
},
null);
Assert.IsNotNull(method, "未找到 CalculateGridCoverageFromWorldExtents 私有方法。");
var coveredCells = (List<(int x, int y)>)method.Invoke(generator, new object[]
{
-200.0, 10.0, -30.0,
-198.0, 30.0, -10.0,
-210.0, 14.83, -80.0,
200, 200, 1.0,
CoordinateSystemType.YUp
});
Assert.AreEqual(63, coveredCells.Count, "YUp 下障碍物包围盒应按宿主 XZ 平面覆盖 3x21 个网格。");
CollectionAssert.Contains(coveredCells, (10, 50));
CollectionAssert.Contains(coveredCells, (12, 70));
}
[TestMethod]
public void YUp_GridMapGenerator_CalculateDoorOpeningCoverage_ShouldProjectToHostXZPlane()
{
var generator = new GridMapGenerator();
var method = typeof(GridMapGenerator).GetMethod(
"CalculateDoorOpeningCoverageFromWorldExtents",
BindingFlags.Instance | BindingFlags.NonPublic,
null,
new[]
{
typeof(double), typeof(double), typeof(double),
typeof(double), typeof(double), typeof(double),
typeof(double),
typeof(double), typeof(double), typeof(double),
typeof(int), typeof(int), typeof(double),
typeof(CoordinateSystemType)
},
null);
Assert.IsNotNull(method, "未找到 CalculateDoorOpeningCoverageFromWorldExtents 私有方法。");
var coveredCells = (List<(int x, int y)>)method.Invoke(generator, new object[]
{
10.0, 14.0, 20.0,
12.0, 16.0, 30.0,
4.0,
0.0, 0.0, 0.0,
100, 100, 1.0,
CoordinateSystemType.YUp
});
Assert.AreEqual(15, coveredCells.Count, "YUp 下门开口应按宿主 XZ 平面覆盖 3x5 个网格。");
CollectionAssert.Contains(coveredCells, (10, 23));
CollectionAssert.Contains(coveredCells, (12, 27));
}
[TestMethod]
public void YUp_PathPlanningManager_CalculateOptimalGridSize_ShouldUseHostXZHorizontalRange()
{
var method = typeof(PathPlanningManager).GetMethod(
"CalculateOptimalGridSizeFromBounds",
BindingFlags.Static | BindingFlags.NonPublic,
null,
new[]
{
typeof(double), typeof(double), typeof(double),
typeof(double), typeof(double), typeof(double),
typeof(CoordinateSystemType)
},
null);
Assert.IsNotNull(method, "未找到 CalculateOptimalGridSizeFromBounds 私有方法。");
var gridSize = (double)method.Invoke(null, new object[]
{
-10.0, 100.0, -300.0,
90.0, 120.0, 400.0,
CoordinateSystemType.YUp
});
Assert.AreEqual(2.0, gridSize, 1e-6, "YUp 下网格大小应基于宿主 XZ 平面最大跨度 700 计算。");
}
[TestMethod]
public void YUp_ChannelHeightDetector_GetBoundsElevationRange_ShouldUseHostY()
{
var method = typeof(ChannelHeightDetector).GetMethod(
"GetBoundsElevationRange",
BindingFlags.Static | BindingFlags.NonPublic,
null,
new[]
{
typeof(double), typeof(double), typeof(double),
typeof(double), typeof(double), typeof(double),
typeof(CoordinateSystemType)
},
null);
Assert.IsNotNull(method, "未找到 GetBoundsElevationRange 私有方法。");
var elevationRange = ((double min, double max))method.Invoke(null, new object[]
{
-10.0, 14.83, -30.0,
20.0, 18.50, 40.0,
CoordinateSystemType.YUp
});
Assert.AreEqual(14.83, elevationRange.min, 1e-6);
Assert.AreEqual(18.50, elevationRange.max, 1e-6);
}
[TestMethod]
public void YUp_ChannelHeightDetector_CreateHeightProfileSample_ShouldUseHostXZPlaneAndHostYElevation()
{
var method = typeof(ChannelHeightDetector).GetMethod(
"CreateHeightProfileSamplePoint3",
BindingFlags.Static | BindingFlags.NonPublic,
null,
new[]
{
typeof(double), typeof(double), typeof(double),
typeof(double), typeof(double), typeof(double),
typeof(CoordinateSystemType),
typeof(double)
},
null);
Assert.IsNotNull(method, "未找到 CreateHeightProfileSamplePoint3 私有方法。");
var samplePoint = (Vector3)method.Invoke(null, new object[]
{
-10.0, 14.83, -30.0,
20.0, 18.50, 40.0,
CoordinateSystemType.YUp,
0.5
});
Assert.AreEqual(5.0, samplePoint.X, 1e-6, "YUp 下采样点第一水平轴应沿宿主 X 插值。");
Assert.AreEqual(14.83, samplePoint.Y, 1e-6, "YUp 下采样点高程应落在宿主底面 Y。");
Assert.AreEqual(5.0, samplePoint.Z, 1e-6, "YUp 下采样点第二水平轴应沿宿主 Z 插值。");
}
[TestMethod]
public void YUp_ChannelHeightDetector_GetPickedSurfaceElevation_ShouldUseHostYInsteadOfWorldZ()
{
var method = typeof(ChannelHeightDetector).GetMethod(
"GetPickedSurfaceElevation",
BindingFlags.Static | BindingFlags.NonPublic,
null,
new[]
{
typeof(double), typeof(double), typeof(double),
typeof(CoordinateSystemType)
},
null);
Assert.IsNotNull(method, "未找到 GetPickedSurfaceElevation 私有方法。");
var elevation = (double)method.Invoke(null, new object[]
{
11.0, 22.0, 33.0,
CoordinateSystemType.YUp
});
Assert.AreEqual(22.0, elevation, 1e-6, "YUp 下拾取表面点的高程应来自宿主 Y。");
}
[TestMethod]
public void YUp_GridMapGenerator_ResolveBoundsMinElevation_ShouldUseHostYInsteadOfWorldZ()
{
var method = typeof(GridMapGenerator).GetMethod(
"ResolveBoundsMinElevation",
BindingFlags.Static | BindingFlags.NonPublic,
null,
new[]
{
typeof(double), typeof(double), typeof(double),
typeof(CoordinateSystemType)
},
null);
Assert.IsNotNull(method, "未找到 ResolveBoundsMinElevation 私有方法。");
var elevation = (double)method.Invoke(null, new object[]
{
0.0, 12.5, -100.0,
CoordinateSystemType.YUp
});
Assert.AreEqual(12.5, elevation, 1e-6, "YUp 下边界最小高程应来自宿主 Y而不是 bounds.Min.Z。");
}
private static void AssertPoint(Vector3 actual, double x, double y, double z)
{
Assert.AreEqual(x, actual.X, 1e-6);
Assert.AreEqual(y, actual.Y, 1e-6);
Assert.AreEqual(z, actual.Z, 1e-6);
}
}
}

View File

@ -1,6 +1,5 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using NavisworksTransport.Core;
using NavisworksTransport.Utils;
using NavisworksTransport.Utils.CoordinateSystem;
using System.Numerics;
@ -113,32 +112,6 @@ namespace NavisworksTransport.UnitTests.CoordinateSystem
AssertVector(trackedCenter, 0.0, 0.0, 3.5);
}
[TestMethod]
public void PreservedTrackedCenterOffset_ShouldBeMeasuredFromRailSemanticCenter()
{
Vector3 semanticTrackedCenter = new Vector3(10f, 20f, 30f);
Vector3 actualTrackedCenter = new Vector3(11.25f, 19.5f, 30.75f);
Vector3 offset = RailPathPoseHelper.CalculatePreservedTrackedCenterOffset(
actualTrackedCenter,
semanticTrackedCenter);
AssertVector(offset, 1.25, -0.5, 0.75);
}
[TestMethod]
public void ResolvePreservedTrackedCenterPosition_ShouldReuseRailSemanticCenterPlusOffset()
{
Vector3 semanticTrackedCenter = new Vector3(3f, -2f, 8f);
Vector3 preservedOffset = new Vector3(0.2f, -0.4f, 0.6f);
Vector3 preservedTrackedCenter = RailPathPoseHelper.ResolvePreservedTrackedCenterPosition(
semanticTrackedCenter,
preservedOffset);
AssertVector(preservedTrackedCenter, 3.2, -2.4, 8.6);
}
private static void AssertVector(Vector3 actual, double x, double y, double z)
{
Assert.AreEqual(x, actual.X, 1e-6);

View File

@ -1,17 +0,0 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using NavisworksTransport.Core;
namespace NavisworksTransport.UnitTests.CoordinateSystem
{
[TestClass]
public class GroundPassageSpaceOffsetTests
{
[TestMethod]
public void GroundPassageSpaceVerticalOffset_ShouldStayAtHalfHeight_WhenHeightAlreadyIncludesLift()
{
double verticalOffset = PathPointRenderPlugin.ResolveGroundPathObjectSpaceVerticalOffset(5.7);
Assert.AreEqual(2.85, verticalOffset, 1e-9);
}
}
}

View File

@ -1,42 +0,0 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using NavisworksTransport.Core.Animation;
using NavisworksTransport.Utils.CoordinateSystem;
using System.Numerics;
namespace NavisworksTransport.UnitTests.CoordinateSystem
{
[TestClass]
public class GroundPathObjectLiftOffsetTests
{
[TestMethod]
public void YUp_GroundPathLift_ShouldOffsetAlongHostY()
{
Vector3 offset = PathAnimationManager.ResolveGroundPathObjectLiftOffsetVector(1.25, CoordinateSystemType.YUp);
AssertVector(offset, 0.0, 1.25, 0.0);
}
[TestMethod]
public void ZUp_GroundPathLift_ShouldOffsetAlongHostZ()
{
Vector3 offset = PathAnimationManager.ResolveGroundPathObjectLiftOffsetVector(1.25, CoordinateSystemType.ZUp);
AssertVector(offset, 0.0, 0.0, 1.25);
}
[TestMethod]
public void GroundPathLift_ShouldReturnZero_WhenLiftIsNotPositive()
{
Vector3 offset = PathAnimationManager.ResolveGroundPathObjectLiftOffsetVector(0.0, CoordinateSystemType.ZUp);
AssertVector(offset, 0.0, 0.0, 0.0);
}
private static void AssertVector(Vector3 actual, double x, double y, double z, double tolerance = 1e-6)
{
Assert.AreEqual(x, actual.X, tolerance);
Assert.AreEqual(y, actual.Y, tolerance);
Assert.AreEqual(z, actual.Z, tolerance);
}
}
}

View File

@ -63,54 +63,11 @@ namespace NavisworksTransport.UnitTests.CoordinateSystem
AssertVector(transformedForward, 1.0, 0.0, 0.0, 1e-4);
}
[TestMethod]
public void CreateRotationFromPlanarBasePose_ShouldReturnBaseRotation_WhenYawUnchanged()
{
Quaternion baseRotation = Quaternion.Normalize(
Quaternion.CreateFromAxisAngle(Vector3.UnitZ, 0.31f) *
Quaternion.CreateFromAxisAngle(Vector3.UnitX, -0.42f));
Quaternion resultRotation = HoistingRealObjectPoseHelper.CreateRotationFromPlanarBasePose(
baseRotation,
baseYawRadians: 1.25,
targetYawRadians: 1.25,
hostUp: Vector3.UnitY);
AssertQuaternionEquivalent(resultRotation, baseRotation);
}
[TestMethod]
public void CreateRotationFromPlanarBasePose_ShouldPreserveTiltAndApplyDeltaYaw()
{
Quaternion baseRotation = Quaternion.Normalize(
Quaternion.CreateFromAxisAngle(Vector3.UnitY, 0.20f) *
Quaternion.CreateFromAxisAngle(Vector3.UnitZ, 0.35f) *
Quaternion.CreateFromAxisAngle(Vector3.UnitX, -0.40f));
Quaternion resultRotation = HoistingRealObjectPoseHelper.CreateRotationFromPlanarBasePose(
baseRotation,
baseYawRadians: 0.0,
targetYawRadians: System.Math.PI / 2.0,
hostUp: Vector3.UnitY);
Quaternion expectedRotation = Quaternion.Normalize(
Quaternion.CreateFromAxisAngle(Vector3.UnitY, (float)(System.Math.PI / 2.0)) *
baseRotation);
AssertQuaternionEquivalent(resultRotation, expectedRotation);
}
private static void AssertVector(Vector3 actual, double x, double y, double z, double tolerance = 1e-6)
{
Assert.AreEqual(x, actual.X, tolerance);
Assert.AreEqual(y, actual.Y, tolerance);
Assert.AreEqual(z, actual.Z, tolerance);
}
private static void AssertQuaternionEquivalent(Quaternion actual, Quaternion expected, double tolerance = 1e-6)
{
float dot = Quaternion.Dot(Quaternion.Normalize(actual), Quaternion.Normalize(expected));
Assert.AreEqual(1.0, System.Math.Abs(dot), tolerance);
}
}
}

View File

@ -1,144 +0,0 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using NavisworksTransport.Utils.CoordinateSystem;
using System;
using System.Numerics;
namespace NavisworksTransport.UnitTests.CoordinateSystem
{
[TestClass]
public class ObjectPassageProjectionOptimizerTests
{
[TestMethod]
public void Optimize_LongTallBox_ShouldFindThreeAxisPoseBetterThanYawOnly()
{
var request = CreateZUpRequest(
sizeX: 8.0,
sizeY: 4.0,
sizeZ: 2.0,
pathForward: Vector3.UnitX);
ObjectPassageProjectionOptimizationResult result = ObjectPassageProjectionOptimizer.Optimize(request);
ObjectPassageProjectionScore yawOnly = ObjectPassageProjectionOptimizer.Evaluate(
request,
new LocalEulerRotationCorrection(0.0, 0.0, 90.0));
Assert.IsTrue(result.Success, result.ErrorMessage);
Assert.IsTrue(result.Score.Area < yawOnly.Area - 1e-6,
$"三轴优化面积 {result.Score.Area:F6} 应小于单轴 yaw 面积 {yawOnly.Area:F6}");
Assert.AreEqual(4.0, result.Score.WidthAcrossPath, 1e-4);
Assert.AreEqual(2.0, result.Score.HeightAlongHostUp, 1e-4);
}
[TestMethod]
public void Optimize_WhenAreasAreClose_ShouldPreferLowerHeight()
{
var request = CreateZUpRequest(
sizeX: 6.0,
sizeY: 3.0,
sizeZ: 2.0,
pathForward: Vector3.UnitX,
areaRelativeTieTolerance: 0.05);
ObjectPassageProjectionOptimizationResult result = ObjectPassageProjectionOptimizer.Optimize(request);
Assert.IsTrue(result.Success, result.ErrorMessage);
Assert.AreEqual(3.0, result.Score.WidthAcrossPath, 1e-4);
Assert.AreEqual(2.0, result.Score.HeightAlongHostUp, 1e-4);
}
[TestMethod]
public void Optimize_WhenBaselineAlreadyAlignedToPath_ShouldKeepZeroCorrection()
{
Quaternion baselineRotation = Quaternion.CreateFromAxisAngle(
Vector3.UnitZ,
(float)(Math.PI / 4.0));
var request = new ObjectPassageProjectionOptimizationRequest(
sizeX: 6.0,
sizeY: 3.0,
sizeZ: 2.0,
hostPathForward: Vector3.Normalize(new Vector3(1.0f, 1.0f, 0.0f)),
hostUp: Vector3.UnitZ,
areaRelativeTieTolerance: 0.01,
baselineHostRotation: baselineRotation);
ObjectPassageProjectionOptimizationResult result = ObjectPassageProjectionOptimizer.Optimize(request);
Assert.IsTrue(result.Success, result.ErrorMessage);
Assert.AreEqual(0.0, result.Correction.XDegrees, 1e-6);
Assert.AreEqual(0.0, result.Correction.YDegrees, 1e-6);
Assert.AreEqual(0.0, result.Correction.ZDegrees, 1e-6);
Assert.AreEqual(3.0, result.Score.WidthAcrossPath, 1e-4);
Assert.AreEqual(2.0, result.Score.HeightAlongHostUp, 1e-4);
}
[TestMethod]
public void Evaluate_YUp_ShouldUseHostYAsHeight()
{
var request = new ObjectPassageProjectionOptimizationRequest(
sizeX: 6.0,
sizeY: 2.0,
sizeZ: 4.0,
hostPathForward: Vector3.UnitX,
hostUp: Vector3.UnitY);
ObjectPassageProjectionScore score = ObjectPassageProjectionOptimizer.Evaluate(
request,
LocalEulerRotationCorrection.Zero);
Assert.AreEqual(4.0, score.WidthAcrossPath, 1e-6);
Assert.AreEqual(2.0, score.HeightAlongHostUp, 1e-6);
Assert.AreEqual(8.0, score.Area, 1e-6);
}
[TestMethod]
public void Optimize_SameInputTwice_ShouldReturnSameCorrection()
{
var request = CreateZUpRequest(
sizeX: 9.0,
sizeY: 4.0,
sizeZ: 2.0,
pathForward: Vector3.Normalize(new Vector3(1.0f, 1.0f, 0.0f)));
ObjectPassageProjectionOptimizationResult first = ObjectPassageProjectionOptimizer.Optimize(request);
ObjectPassageProjectionOptimizationResult second = ObjectPassageProjectionOptimizer.Optimize(request);
Assert.IsTrue(first.Success, first.ErrorMessage);
Assert.IsTrue(second.Success, second.ErrorMessage);
Assert.AreEqual(first.Correction.XDegrees, second.Correction.XDegrees, 1e-9);
Assert.AreEqual(first.Correction.YDegrees, second.Correction.YDegrees, 1e-9);
Assert.AreEqual(first.Correction.ZDegrees, second.Correction.ZDegrees, 1e-9);
Assert.AreEqual(first.Score.Area, second.Score.Area, 1e-9);
}
[TestMethod]
public void Optimize_PathForwardParallelToHostUp_ShouldFail()
{
var request = CreateZUpRequest(
sizeX: 6.0,
sizeY: 4.0,
sizeZ: 2.0,
pathForward: Vector3.UnitZ);
ObjectPassageProjectionOptimizationResult result = ObjectPassageProjectionOptimizer.Optimize(request);
Assert.IsFalse(result.Success);
StringAssert.Contains(result.ErrorMessage, "路径方向退化");
}
private static ObjectPassageProjectionOptimizationRequest CreateZUpRequest(
double sizeX,
double sizeY,
double sizeZ,
Vector3 pathForward,
double areaRelativeTieTolerance = 0.01)
{
return new ObjectPassageProjectionOptimizationRequest(
sizeX,
sizeY,
sizeZ,
pathForward,
Vector3.UnitZ,
areaRelativeTieTolerance);
}
}
}

View File

@ -14,7 +14,6 @@ namespace NavisworksTransport.UnitTests.CoordinateSystem
Assert.AreEqual(ObjectStartPlacementMode.PreserveInitialPose, request.PlacementMode);
Assert.IsTrue(request.PreserveInitialPose);
Assert.AreEqual(LocalEulerRotationCorrection.Zero, request.RotationCorrection);
Assert.AreEqual(0.0, request.VerticalLiftInMeters, 1e-9);
}
[TestMethod]
@ -22,23 +21,11 @@ namespace NavisworksTransport.UnitTests.CoordinateSystem
{
var correction = new LocalEulerRotationCorrection(15.0, 30.0, 45.0);
var request = ObjectStartPlacementRequest.CreateRotationCorrection(correction, 0.35);
var request = ObjectStartPlacementRequest.CreateRotationCorrection(correction);
Assert.AreEqual(ObjectStartPlacementMode.AlignToPathPose, request.PlacementMode);
Assert.IsFalse(request.PreserveInitialPose);
Assert.AreEqual(correction, request.RotationCorrection);
Assert.AreEqual(0.35, request.VerticalLiftInMeters, 1e-9);
}
[TestMethod]
public void TranslationOnlyRequest_ShouldKeepVerticalLift()
{
var request = ObjectStartPlacementRequest.CreateTranslationOnly(0.28);
Assert.AreEqual(ObjectStartPlacementMode.PreserveInitialPose, request.PlacementMode);
Assert.IsTrue(request.PreserveInitialPose);
Assert.AreEqual(LocalEulerRotationCorrection.Zero, request.RotationCorrection);
Assert.AreEqual(0.28, request.VerticalLiftInMeters, 1e-9);
}
}
}

View File

@ -76,25 +76,6 @@ namespace NavisworksTransport.UnitTests.CoordinateSystem
Assert.AreEqual(0.0, Vector3.Dot(frame.Forward, frame.Up), 1e-6);
}
[TestMethod]
public void Hoisting_StartForward_ShouldUseEditedRealPathHorizontalSegment_FromDebugLog()
{
var pathPoints = new List<Vector3>
{
new Vector3(-197.436f, 14.833f, 29.413f),
new Vector3(-197.436f, 31.240f, 29.413f),
new Vector3(-213.650f, 31.240f, 29.413f)
};
bool ok = PathTargetFrameResolver.TryResolvePlanarStartHostForward(
NavisworksTransport.PathType.Hoisting,
pathPoints,
out Vector3 hostForward);
Assert.IsTrue(ok);
AssertVector(Vector3.Normalize(hostForward), -1.0, 0.0, 0.0, 1e-6);
}
[TestMethod]
public void YUp_PlanarYaw_ShouldUseHostXZPlane_NotHostXY()
{

View File

@ -1,118 +0,0 @@
using Autodesk.Navisworks.Api;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using NavisworksTransport.UI.WPF.ViewModels;
using System.Numerics;
namespace NavisworksTransport.UnitTests.CoordinateSystem
{
[TestClass]
public class RailAssemblyWorkflowContextTests
{
[TestMethod]
public void ResetEndFaceAnalysis_ClearsAnalysisStateAndSeedPoints()
{
var context = CreateContext();
context.HasEndFaceAnalysis = true;
context.EndFaceCenterPoint = new Point3D(1.0, 2.0, 3.0);
context.EndFaceNormal = new Vector3(1f, 0f, 0f);
context.EndFaceSeedPoints.Add(new Point3D(4.0, 5.0, 6.0));
context.ResetEndFaceAnalysis();
Assert.IsFalse(context.HasEndFaceAnalysis);
Assert.IsNull(context.EndFaceCenterPoint);
Assert.AreEqual(default(Vector3), context.EndFaceNormal);
Assert.AreEqual(0, context.EndFaceSeedPoints.Count);
}
[TestMethod]
public void ResetInstallationReference_RestoresDefaultOffsetAndClearsReferenceState()
{
var context = CreateContext();
context.HasInstallationReference = true;
context.InstallationPickPoint = new Point3D(1.0, 2.0, 3.0);
context.InstallationBaseAnchorPoint = new Point3D(4.0, 5.0, 6.0);
context.InstallationAnchorPoint = new Point3D(7.0, 8.0, 9.0);
context.InstallationPlaneNormal = new Vector3(0f, 1f, 0f);
context.InstallationPlaneSpanDirection = new Vector3(1f, 0f, 0f);
context.InstallationOffsetDistanceInMeters = 2.5;
context.InstallationReferenceRouteId = "route-1";
context.InstallationSeedPoints.Add(new Point3D(9.0, 8.0, 7.0));
context.AnchorVerticalOffsetInMeters = 3.5;
context.ResetInstallationReference();
Assert.IsFalse(context.HasInstallationReference);
Assert.IsNull(context.InstallationPickPoint);
Assert.IsNull(context.InstallationBaseAnchorPoint);
Assert.IsNull(context.InstallationAnchorPoint);
Assert.AreEqual(default(Vector3), context.InstallationPlaneNormal);
Assert.AreEqual(default(Vector3), context.InstallationPlaneSpanDirection);
Assert.AreEqual(0.0, context.InstallationOffsetDistanceInMeters, 1e-9);
Assert.IsNull(context.InstallationReferenceRouteId);
Assert.AreEqual(0, context.InstallationSeedPoints.Count);
Assert.AreEqual(0.25, context.AnchorVerticalOffsetInMeters, 1e-9);
}
[TestMethod]
public void ResetSession_ClearsTransientStateButPreservesConfiguration()
{
var context = CreateContext();
context.WorkflowMode = RailAssemblyWorkflowMode.EditSelectedRail;
context.TerminalObjectName = "箱体A";
context.TerminalObjectInfo = "info";
context.ReferenceRodLengthInMeters = 12.0;
context.ReferenceRodDiameterInMeters = 0.4;
context.SphereCenterX = 10.0;
context.SphereCenterY = 11.0;
context.SphereCenterZ = 12.0;
context.MountMode = RailMountMode.OverRail;
context.HasTerminalObject = true;
context.IsSelectingStartPoint = true;
context.IsSelectingEndFacePoints = true;
context.IsSelectingInstallationPoint = true;
context.StartPoint = new Point3D(1.0, 2.0, 3.0);
context.StartPointText = "(1,2,3)";
context.HasEndFaceAnalysis = true;
context.EndFaceCenterPoint = new Point3D(4.0, 5.0, 6.0);
context.HasInstallationReference = true;
context.InstallationReferenceRouteId = "route-2";
context.AnchorVerticalOffsetInMeters = 1.5;
context.EndFaceSeedPoints.Add(new Point3D(1.0, 0.0, 0.0));
context.InstallationSeedPoints.Add(new Point3D(0.0, 1.0, 0.0));
context.ResetSession();
Assert.AreEqual(RailAssemblyWorkflowMode.None, context.WorkflowMode);
Assert.IsFalse(context.HasTerminalObject);
Assert.IsFalse(context.IsSelectingStartPoint);
Assert.IsFalse(context.IsSelectingEndFacePoints);
Assert.IsFalse(context.IsSelectingInstallationPoint);
Assert.IsNull(context.TerminalObject);
Assert.IsNotNull(context.StartPoint);
Assert.AreEqual("未选择", context.TerminalObjectName);
Assert.AreEqual("请选择终点处已安装箱体", context.TerminalObjectInfo);
Assert.AreEqual("未选择", context.StartPointText);
Assert.IsFalse(context.HasEndFaceAnalysis);
Assert.IsFalse(context.HasInstallationReference);
Assert.AreEqual(0.25, context.AnchorVerticalOffsetInMeters, 1e-9);
Assert.AreEqual(12.0, context.ReferenceRodLengthInMeters, 1e-9);
Assert.AreEqual(0.4, context.ReferenceRodDiameterInMeters, 1e-9);
Assert.AreEqual(10.0, context.SphereCenterX, 1e-9);
Assert.AreEqual(11.0, context.SphereCenterY, 1e-9);
Assert.AreEqual(12.0, context.SphereCenterZ, 1e-9);
Assert.AreEqual(RailMountMode.OverRail, context.MountMode);
}
private static RailAssemblyWorkflowContext CreateContext()
{
return new RailAssemblyWorkflowContext(
defaultReferenceRodLengthInMeters: 20.0,
defaultReferenceRodDiameterInMeters: 0.3,
defaultAnchorVerticalOffsetInMeters: 0.25,
defaultSphereCenterX: 0.0,
defaultSphereCenterY: 0.0,
defaultSphereCenterZ: 0.0);
}
}
}

View File

@ -1,36 +0,0 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using NavisworksTransport.Utils;
using System.Numerics;
namespace NavisworksTransport.UnitTests.CoordinateSystem
{
[TestClass]
public class RailPathPoseHelperTests
{
[TestMethod]
public void NormalizePreferredNormalToHostUpHemisphere_ShouldFlip_WhenPreferredNormalOpposesHostUp()
{
Vector3 hostPreferredNormal = new Vector3(-0.1222f, -0.9676f, 0.2211f);
Vector3 normalized = RailPathPoseHelper.NormalizePreferredNormalToHostUpHemisphere(
hostPreferredNormal,
Vector3.UnitY);
Assert.IsTrue(Vector3.Dot(normalized, Vector3.UnitY) > 0f);
Assert.AreEqual(0.1222 / 1.0008249, normalized.X, 1e-4);
Assert.AreEqual(0.9676 / 1.0008249, normalized.Y, 1e-4);
Assert.AreEqual(-0.2211 / 1.0008249, normalized.Z, 1e-4);
}
[TestMethod]
public void NormalizePreferredNormalToHostUpHemisphere_ShouldKeepDirection_WhenPreferredNormalAlreadyMatchesHostUp()
{
Vector3 hostPreferredNormal = new Vector3(0.139f, 0.954f, 0.266f);
Vector3 normalized = RailPathPoseHelper.NormalizePreferredNormalToHostUpHemisphere(
hostPreferredNormal,
Vector3.UnitY);
Assert.IsTrue(Vector3.Dot(normalized, Vector3.UnitY) > 0f);
Assert.IsTrue(Vector3.Dot(Vector3.Normalize(hostPreferredNormal), normalized) > 0.9999f);
}
}
}

View File

@ -1,290 +0,0 @@
using Autodesk.Navisworks.Api;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using NavisworksTransport.Core.Animation;
using NavisworksTransport.Utils.CoordinateSystem;
using System.Numerics;
namespace NavisworksTransport.UnitTests.CoordinateSystem
{
[TestClass]
public class RailPreservedPoseTests
{
[TestMethod]
public void ResolvePreservedPoseRotation_ShouldPreferActualGeometryRotationForRealObjects()
{
Rotation3D fallbackRotation = new Rotation3D(0.0, 0.0, 0.0, 1.0);
Rotation3D actualGeometryRotation = new Rotation3D(0.0, 0.70710678, 0.0, 0.70710678);
Rotation3D resolved = PathAnimationManager.ResolvePreservedPoseRotation(
preferActualGeometryRotation: true,
hasActualGeometryRotation: true,
actualGeometryRotation: actualGeometryRotation,
fallbackRotation: fallbackRotation);
Assert.AreEqual(actualGeometryRotation.A, resolved.A, 1e-9);
Assert.AreEqual(actualGeometryRotation.B, resolved.B, 1e-9);
Assert.AreEqual(actualGeometryRotation.C, resolved.C, 1e-9);
Assert.AreEqual(actualGeometryRotation.D, resolved.D, 1e-9);
}
[TestMethod]
public void ResolvePreservedPoseRotation_ShouldFallbackWhenActualGeometryRotationUnavailable()
{
Rotation3D fallbackRotation = new Rotation3D(0.0, 0.0, 0.38268343, 0.92387953);
Rotation3D actualGeometryRotation = new Rotation3D(0.0, 0.70710678, 0.0, 0.70710678);
Rotation3D resolved = PathAnimationManager.ResolvePreservedPoseRotation(
preferActualGeometryRotation: true,
hasActualGeometryRotation: false,
actualGeometryRotation: actualGeometryRotation,
fallbackRotation: fallbackRotation);
Assert.AreEqual(fallbackRotation.A, resolved.A, 1e-9);
Assert.AreEqual(fallbackRotation.B, resolved.B, 1e-9);
Assert.AreEqual(fallbackRotation.C, resolved.C, 1e-9);
Assert.AreEqual(fallbackRotation.D, resolved.D, 1e-9);
}
[TestMethod]
public void ShouldPreservePathRotationForFrames_ShouldEnableHoistingWhenTranslationModeHasLockedRotation()
{
bool shouldPreserve = PathAnimationManager.ShouldPreservePathRotationForFrames(
PathType.Hoisting,
ObjectStartPlacementMode.PreserveInitialPose,
hasPreservedRotation: true);
Assert.IsTrue(shouldPreserve);
}
[TestMethod]
public void ShouldPreservePathRotationForFrames_ShouldEnableRailWhenTranslationModeHasLockedRotation()
{
bool shouldPreserve = PathAnimationManager.ShouldPreservePathRotationForFrames(
PathType.Rail,
ObjectStartPlacementMode.PreserveInitialPose,
hasPreservedRotation: true);
Assert.IsTrue(shouldPreserve);
}
[TestMethod]
public void ShouldPreservePathRotationForFrames_ShouldDisableGroundEvenInTranslationMode()
{
bool shouldPreserve = PathAnimationManager.ShouldPreservePathRotationForFrames(
PathType.Ground,
ObjectStartPlacementMode.PreserveInitialPose,
hasPreservedRotation: true);
Assert.IsFalse(shouldPreserve);
}
[TestMethod]
public void ShouldPreservePathRotationForFrames_ShouldDisableWhenLockedRotationMissing()
{
bool shouldPreserve = PathAnimationManager.ShouldPreservePathRotationForFrames(
PathType.Hoisting,
ObjectStartPlacementMode.PreserveInitialPose,
hasPreservedRotation: false);
Assert.IsFalse(shouldPreserve);
}
[TestMethod]
public void ShouldAllowFragmentPlanarFallback_ShouldDisableHoisting()
{
Assert.IsFalse(PathAnimationManager.ShouldAllowFragmentPlanarFallback(PathType.Hoisting));
Assert.IsFalse(PathAnimationManager.ShouldAllowFragmentPlanarFallback(PathType.Ground));
Assert.IsTrue(PathAnimationManager.ShouldAllowFragmentPlanarFallback(PathType.Rail));
}
[TestMethod]
public void ShouldUseReferenceBasedRealObjectPlanarPose_ShouldDisableGroundAndHoisting()
{
Assert.IsFalse(PathAnimationManager.ShouldUseReferenceBasedRealObjectPlanarPose(PathType.Ground));
Assert.IsFalse(PathAnimationManager.ShouldUseReferenceBasedRealObjectPlanarPose(PathType.Hoisting));
Assert.IsTrue(PathAnimationManager.ShouldUseReferenceBasedRealObjectPlanarPose(PathType.Rail));
}
[TestMethod]
public void TryResolveDisplayedPlanarYawFromRotation_ShouldResolveYUpFromDisplayedXAxis()
{
Rotation3D rotation = new Rotation3D(
0.0,
System.Math.Sin(-System.Math.PI / 4.0),
0.0,
System.Math.Cos(-System.Math.PI / 4.0));
bool ok = PathAnimationManager.TryResolveDisplayedPlanarYawFromRotation(
rotation,
CoordinateSystemType.YUp,
out double yawRadians);
Assert.IsTrue(ok);
Assert.AreEqual(-System.Math.PI / 2.0, yawRadians, 1e-6);
}
[TestMethod]
public void ShouldUsePlanarRealObjectPureIncrementFrames_ShouldEnableOnlyForPlanarRealObjects()
{
Assert.IsTrue(PathAnimationManager.ShouldUsePlanarRealObjectPureIncrementFrames(PathType.Ground, true));
Assert.IsTrue(PathAnimationManager.ShouldUsePlanarRealObjectPureIncrementFrames(PathType.Hoisting, true));
Assert.IsFalse(PathAnimationManager.ShouldUsePlanarRealObjectPureIncrementFrames(PathType.Ground, false));
Assert.IsFalse(PathAnimationManager.ShouldUsePlanarRealObjectPureIncrementFrames(PathType.Rail, true));
}
[TestMethod]
public void ResolveAnimationFramePlaybackYawRadians_ShouldPreferPlanarHostYawWithHostCorrection()
{
var frame = new AnimationFrame
{
YawRadians = 0.25,
PlanarHostYawRadians = 0.75,
HasCustomRotation = true
};
double resolved = PathAnimationManager.ResolveAnimationFramePlaybackYawRadians(
frame,
new LocalEulerRotationCorrection(10.0, 20.0, 30.0),
CoordinateSystemType.YUp);
Assert.AreEqual(0.75 + 20.0 * System.Math.PI / 180.0, resolved, 1e-9);
}
[TestMethod]
public void ApplyRecordedPlanarRealObjectFramePose_ShouldPreserveFullRotation_ForGroundCollisionLogCase()
{
var frame = new AnimationFrame();
double yawRadians = 115.85 * System.Math.PI / 180.0;
var hostLinear = new Matrix4x4(
-0.4360f, 0.0000f, -0.8999f, 0.0f,
-0.6364f, 0.7071f, 0.3083f, 0.0f,
0.6364f, 0.7071f, -0.3083f, 0.0f,
0.0f, 0.0f, 0.0f, 1.0f);
Quaternion hostQuaternion = Quaternion.Normalize(Quaternion.CreateFromRotationMatrix(hostLinear));
var rotation = new Rotation3D(hostQuaternion.X, hostQuaternion.Y, hostQuaternion.Z, hostQuaternion.W);
PathAnimationManager.ApplyRecordedPlanarRealObjectFramePose(frame, yawRadians, rotation);
Assert.IsTrue(frame.PlanarHostYawRadians.HasValue);
Assert.AreEqual(yawRadians, frame.PlanarHostYawRadians.Value, 1e-9);
Assert.IsTrue(frame.HasCustomRotation);
Matrix3 linear = new Transform3D(frame.Rotation).Linear;
Assert.AreEqual(-0.4360, linear.Get(0, 0), 5e-4);
Assert.AreEqual(-0.6364, linear.Get(1, 0), 5e-4);
Assert.AreEqual(0.6364, linear.Get(2, 0), 5e-4);
Assert.AreEqual(0.0000, linear.Get(0, 1), 5e-4);
Assert.AreEqual(0.7071, linear.Get(1, 1), 5e-4);
Assert.AreEqual(0.7071, linear.Get(2, 1), 5e-4);
Assert.AreEqual(-0.8999, linear.Get(0, 2), 5e-4);
Assert.AreEqual(0.3083, linear.Get(1, 2), 5e-4);
Assert.AreEqual(-0.3083, linear.Get(2, 2), 5e-4);
}
[TestMethod]
public void TryResolveGroundHostPlanarYawFromFramePoints_ShouldResolveYUpHostYaw()
{
bool ok = PathAnimationManager.TryResolveGroundHostPlanarYawFromFramePoints(
new Point3D(0, 0, 0),
new Point3D(1, 0, 0),
new Point3D(1, 0, -1),
CoordinateSystemType.YUp,
out double yawRadians);
Assert.IsTrue(ok);
Assert.AreEqual(-System.Math.PI / 4.0, yawRadians, 1e-6);
}
[TestMethod]
public void ResolvePlanarHostUpCorrectionRadians_ShouldUseYDegreesForYUp()
{
double radians = PathAnimationManager.ResolvePlanarHostUpCorrectionRadians(
new LocalEulerRotationCorrection(10.0, 20.0, 30.0),
CoordinateSystemType.YUp);
Assert.AreEqual(20.0 * System.Math.PI / 180.0, radians, 1e-9);
}
[TestMethod]
public void ResolvePlanarHostUpCorrectionRadians_ShouldUseZDegreesForZUp()
{
double radians = PathAnimationManager.ResolvePlanarHostUpCorrectionRadians(
new LocalEulerRotationCorrection(10.0, 20.0, 30.0),
CoordinateSystemType.ZUp);
Assert.AreEqual(30.0 * System.Math.PI / 180.0, radians, 1e-9);
}
[TestMethod]
public void ResolveCurrentRotationBaseline_ShouldPreferActualGeometryForHoisting()
{
Rotation3D trackedRotation = new Rotation3D(0.0, 0.0, 0.0, 1.0);
Rotation3D actualGeometryRotation = new Rotation3D(0.0, 0.70710678, 0.0, 0.70710678);
Rotation3D resolved = PathAnimationManager.ResolveCurrentRotationBaseline(
PathType.Hoisting,
isRealObjectMode: true,
hasTrackedRotation: true,
trackedRotation: trackedRotation,
hasReferenceRotation: false,
referenceRotation: Rotation3D.Identity,
transformRotation: Rotation3D.Identity,
hasActualGeometryRotation: true,
actualGeometryRotation: actualGeometryRotation);
Assert.AreEqual(actualGeometryRotation.A, resolved.A, 1e-9);
Assert.AreEqual(actualGeometryRotation.B, resolved.B, 1e-9);
Assert.AreEqual(actualGeometryRotation.C, resolved.C, 1e-9);
Assert.AreEqual(actualGeometryRotation.D, resolved.D, 1e-9);
}
[TestMethod]
public void ResolveCurrentRotationBaseline_ShouldKeepTrackedRotationForGround()
{
Rotation3D trackedRotation = new Rotation3D(0.0, 0.0, 0.38268343, 0.92387953);
Rotation3D actualGeometryRotation = new Rotation3D(0.0, 0.70710678, 0.0, 0.70710678);
Rotation3D resolved = PathAnimationManager.ResolveCurrentRotationBaseline(
PathType.Ground,
isRealObjectMode: true,
hasTrackedRotation: true,
trackedRotation: trackedRotation,
hasReferenceRotation: false,
referenceRotation: Rotation3D.Identity,
transformRotation: Rotation3D.Identity,
hasActualGeometryRotation: true,
actualGeometryRotation: actualGeometryRotation);
Assert.AreEqual(trackedRotation.A, resolved.A, 1e-9);
Assert.AreEqual(trackedRotation.B, resolved.B, 1e-9);
Assert.AreEqual(trackedRotation.C, resolved.C, 1e-9);
Assert.AreEqual(trackedRotation.D, resolved.D, 1e-9);
}
[TestMethod]
public void ShouldUseRealObjectOverrideRotation_ShouldEnableForRealObjects()
{
Assert.IsTrue(PathAnimationManager.ShouldUseRealObjectOverrideRotation(true));
Assert.IsFalse(PathAnimationManager.ShouldUseRealObjectOverrideRotation(false));
}
[TestMethod]
public void ShouldUseOriginalBoundingBoxCenterForBusinessTrackedPosition_ShouldEnableOnlyForGroundRealObjects()
{
Assert.IsTrue(PathAnimationManager.ShouldUseOriginalBoundingBoxCenterForBusinessTrackedPosition(
PathType.Ground,
isRealObjectMode: true));
Assert.IsFalse(PathAnimationManager.ShouldUseOriginalBoundingBoxCenterForBusinessTrackedPosition(
PathType.Ground,
isRealObjectMode: false));
Assert.IsFalse(PathAnimationManager.ShouldUseOriginalBoundingBoxCenterForBusinessTrackedPosition(
PathType.Hoisting,
isRealObjectMode: true));
Assert.IsFalse(PathAnimationManager.ShouldUseOriginalBoundingBoxCenterForBusinessTrackedPosition(
PathType.Rail,
isRealObjectMode: true));
}
}
}

View File

@ -51,85 +51,6 @@ namespace NavisworksTransport.UnitTests.CoordinateSystem
Assert.AreNotEqual(4.0, extents.upExtent, 1e-3, "双轴旋转后Ground 的法线尺寸不应停留在旧高度。");
}
[TestMethod]
public void Ground_DisplayedHostAxesProjection_ShouldKeepXAxisLengthAndSwapXZAsAxesRotate()
{
Vector3 targetForward = Vector3.UnitX;
Vector3 targetUp = Vector3.UnitY;
var noCorrection = RealObjectProjectedExtentResolver.CalculateProjectedDisplayedExtents(
displayedAxisXSize: 6.0,
displayedAxisYSize: 2.0,
displayedAxisZSize: 4.0,
displayedHostAxisX: Vector3.UnitX,
displayedHostAxisY: Vector3.UnitZ,
displayedHostAxisZ: -Vector3.UnitY,
targetForward,
targetUp);
Assert.AreEqual(6.0, noCorrection.forwardExtent, 1e-5);
Assert.AreEqual(2.0, noCorrection.sideExtent, 1e-5);
Assert.AreEqual(4.0, noCorrection.upExtent, 1e-5);
var xRotated = RealObjectProjectedExtentResolver.CalculateProjectedDisplayedExtents(
displayedAxisXSize: 6.0,
displayedAxisYSize: 2.0,
displayedAxisZSize: 4.0,
displayedHostAxisX: Vector3.UnitX,
displayedHostAxisY: Vector3.UnitY,
displayedHostAxisZ: Vector3.UnitZ,
targetForward,
targetUp);
Assert.AreEqual(6.0, xRotated.forwardExtent, 1e-5);
Assert.AreEqual(4.0, xRotated.sideExtent, 1e-5);
Assert.AreEqual(2.0, xRotated.upExtent, 1e-5);
var zRotated = RealObjectProjectedExtentResolver.CalculateProjectedDisplayedExtents(
displayedAxisXSize: 6.0,
displayedAxisYSize: 2.0,
displayedAxisZSize: 4.0,
displayedHostAxisX: -Vector3.UnitZ,
displayedHostAxisY: Vector3.UnitX,
displayedHostAxisZ: -Vector3.UnitY,
targetForward,
targetUp);
Assert.AreEqual(2.0, zRotated.forwardExtent, 1e-5);
Assert.AreEqual(6.0, zRotated.sideExtent, 1e-5);
Assert.AreEqual(4.0, zRotated.upExtent, 1e-5);
}
[TestMethod]
public void Ground_DisplayedHostAxesProjection_ShouldIgnoreVerticalComponentInForward()
{
Vector3 targetUp = Vector3.UnitY;
Vector3 planarForward = Vector3.Normalize(new Vector3(1f, 0f, 1f));
Vector3 slopedForward = Vector3.Normalize(new Vector3(1f, 2f, 1f));
var planar = RealObjectProjectedExtentResolver.CalculateProjectedDisplayedExtents(
displayedAxisXSize: 6.0,
displayedAxisYSize: 2.0,
displayedAxisZSize: 4.0,
displayedHostAxisX: Vector3.UnitX,
displayedHostAxisY: Vector3.UnitZ,
displayedHostAxisZ: -Vector3.UnitY,
planarForward,
targetUp);
var sloped = RealObjectProjectedExtentResolver.CalculateProjectedDisplayedExtents(
displayedAxisXSize: 6.0,
displayedAxisYSize: 2.0,
displayedAxisZSize: 4.0,
displayedHostAxisX: Vector3.UnitX,
displayedHostAxisY: Vector3.UnitZ,
displayedHostAxisZ: -Vector3.UnitY,
slopedForward,
targetUp);
Assert.AreNotEqual(planar.forwardExtent, sloped.forwardExtent, 1e-3, "原始带竖直分量的路径方向会污染 Ground 尺寸投影。");
}
private static double ProjectExtent(
Vector3 localSize,
Vector3 rotatedLocalX,

View File

@ -1,6 +1,5 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using NavisworksTransport.Utils.CoordinateSystem;
using System;
using System.Numerics;
namespace NavisworksTransport.UnitTests.CoordinateSystem
@ -87,236 +86,5 @@ namespace NavisworksTransport.UnitTests.CoordinateSystem
Assert.AreEqual(6.0, result.sideExtent, 1e-6);
Assert.AreEqual(2.0, result.upExtent, 1e-6);
}
[TestMethod]
public void Ground_YUp_HostY90_ShouldSwapForwardAndSide()
{
var result = RotatedObjectExtentHelper.CalculateGroundSemanticExtents(
CoordinateSystemType.YUp,
forwardSize: 6.0,
sideSize: 4.0,
upSize: 2.0,
hostCorrection: new LocalEulerRotationCorrection(0.0, 90.0, 0.0));
Assert.AreEqual(4.0, result.forwardExtent, 1e-6);
Assert.AreEqual(6.0, result.sideExtent, 1e-6);
Assert.AreEqual(2.0, result.upExtent, 1e-6);
}
[TestMethod]
public void Ground_YUp_HostY45_ShouldBlendForwardAndSide_AndKeepUp()
{
var result = RotatedObjectExtentHelper.CalculateGroundSemanticExtents(
CoordinateSystemType.YUp,
forwardSize: 6.0,
sideSize: 4.0,
upSize: 2.0,
hostCorrection: new LocalEulerRotationCorrection(0.0, 45.0, 0.0));
double expectedPlanar = 5.0 * Math.Sqrt(2.0);
Assert.AreEqual(expectedPlanar, result.forwardExtent, 1e-6);
Assert.AreEqual(expectedPlanar, result.sideExtent, 1e-6);
Assert.AreEqual(2.0, result.upExtent, 1e-6);
}
[TestMethod]
public void Ground_YUp_HostY135_ShouldBlendForwardAndSide_AndKeepUp()
{
var result = RotatedObjectExtentHelper.CalculateGroundSemanticExtents(
CoordinateSystemType.YUp,
forwardSize: 6.0,
sideSize: 4.0,
upSize: 2.0,
hostCorrection: new LocalEulerRotationCorrection(0.0, 135.0, 0.0));
double expectedPlanar = 5.0 * Math.Sqrt(2.0);
Assert.AreEqual(expectedPlanar, result.forwardExtent, 1e-6);
Assert.AreEqual(expectedPlanar, result.sideExtent, 1e-6);
Assert.AreEqual(2.0, result.upExtent, 1e-6);
}
[TestMethod]
public void Ground_YUp_HostY180_ShouldKeepSemanticExtents()
{
var result = RotatedObjectExtentHelper.CalculateGroundSemanticExtents(
CoordinateSystemType.YUp,
forwardSize: 6.0,
sideSize: 4.0,
upSize: 2.0,
hostCorrection: new LocalEulerRotationCorrection(0.0, 180.0, 0.0));
Assert.AreEqual(6.0, result.forwardExtent, 1e-6);
Assert.AreEqual(4.0, result.sideExtent, 1e-6);
Assert.AreEqual(2.0, result.upExtent, 1e-6);
}
[TestMethod]
public void Ground_YUp_HostY270_ShouldSwapForwardAndSide()
{
var result = RotatedObjectExtentHelper.CalculateGroundSemanticExtents(
CoordinateSystemType.YUp,
forwardSize: 6.0,
sideSize: 4.0,
upSize: 2.0,
hostCorrection: new LocalEulerRotationCorrection(0.0, 270.0, 0.0));
Assert.AreEqual(4.0, result.forwardExtent, 1e-6);
Assert.AreEqual(6.0, result.sideExtent, 1e-6);
Assert.AreEqual(2.0, result.upExtent, 1e-6);
}
[TestMethod]
public void Ground_YUp_HostX90_ShouldKeepForward_AndSwapSideWithUp()
{
var result = RotatedObjectExtentHelper.CalculateGroundSemanticExtents(
CoordinateSystemType.YUp,
forwardSize: 6.0,
sideSize: 4.0,
upSize: 2.0,
hostCorrection: new LocalEulerRotationCorrection(90.0, 0.0, 0.0));
Assert.AreEqual(6.0, result.forwardExtent, 1e-6);
Assert.AreEqual(2.0, result.sideExtent, 1e-6);
Assert.AreEqual(4.0, result.upExtent, 1e-6);
}
[TestMethod]
public void Ground_YUp_HostX45_ShouldKeepForward_AndBlendSideWithUp()
{
var result = RotatedObjectExtentHelper.CalculateGroundSemanticExtents(
CoordinateSystemType.YUp,
forwardSize: 6.0,
sideSize: 4.0,
upSize: 2.0,
hostCorrection: new LocalEulerRotationCorrection(45.0, 0.0, 0.0));
double expectedBlend = 3.0 * Math.Sqrt(2.0);
Assert.AreEqual(6.0, result.forwardExtent, 1e-6);
Assert.AreEqual(expectedBlend, result.sideExtent, 1e-6);
Assert.AreEqual(expectedBlend, result.upExtent, 1e-6);
}
[TestMethod]
public void Ground_YUp_HostX135_ShouldKeepForward_AndBlendSideWithUp()
{
var result = RotatedObjectExtentHelper.CalculateGroundSemanticExtents(
CoordinateSystemType.YUp,
forwardSize: 6.0,
sideSize: 4.0,
upSize: 2.0,
hostCorrection: new LocalEulerRotationCorrection(135.0, 0.0, 0.0));
double expectedBlend = 3.0 * Math.Sqrt(2.0);
Assert.AreEqual(6.0, result.forwardExtent, 1e-6);
Assert.AreEqual(expectedBlend, result.sideExtent, 1e-6);
Assert.AreEqual(expectedBlend, result.upExtent, 1e-6);
}
[TestMethod]
public void Ground_YUp_HostX180_ShouldKeepSemanticExtents()
{
var result = RotatedObjectExtentHelper.CalculateGroundSemanticExtents(
CoordinateSystemType.YUp,
forwardSize: 6.0,
sideSize: 4.0,
upSize: 2.0,
hostCorrection: new LocalEulerRotationCorrection(180.0, 0.0, 0.0));
Assert.AreEqual(6.0, result.forwardExtent, 1e-6);
Assert.AreEqual(4.0, result.sideExtent, 1e-6);
Assert.AreEqual(2.0, result.upExtent, 1e-6);
}
[TestMethod]
public void Ground_YUp_HostX270_ShouldKeepForward_AndSwapSideWithUp()
{
var result = RotatedObjectExtentHelper.CalculateGroundSemanticExtents(
CoordinateSystemType.YUp,
forwardSize: 6.0,
sideSize: 4.0,
upSize: 2.0,
hostCorrection: new LocalEulerRotationCorrection(270.0, 0.0, 0.0));
Assert.AreEqual(6.0, result.forwardExtent, 1e-6);
Assert.AreEqual(2.0, result.sideExtent, 1e-6);
Assert.AreEqual(4.0, result.upExtent, 1e-6);
}
[TestMethod]
public void Ground_YUp_HostZ90_ShouldPromoteUpToForward_AndKeepSide()
{
var result = RotatedObjectExtentHelper.CalculateGroundSemanticExtents(
CoordinateSystemType.YUp,
forwardSize: 6.0,
sideSize: 4.0,
upSize: 2.0,
hostCorrection: new LocalEulerRotationCorrection(0.0, 0.0, 90.0));
Assert.AreEqual(2.0, result.forwardExtent, 1e-6);
Assert.AreEqual(4.0, result.sideExtent, 1e-6);
Assert.AreEqual(6.0, result.upExtent, 1e-6);
}
[TestMethod]
public void Ground_YUp_HostZ45_ShouldBlendForwardWithUp_AndKeepSide()
{
var result = RotatedObjectExtentHelper.CalculateGroundSemanticExtents(
CoordinateSystemType.YUp,
forwardSize: 6.0,
sideSize: 4.0,
upSize: 2.0,
hostCorrection: new LocalEulerRotationCorrection(0.0, 0.0, 45.0));
double expectedBlend = 4.0 * Math.Sqrt(2.0);
Assert.AreEqual(expectedBlend, result.forwardExtent, 1e-6);
Assert.AreEqual(4.0, result.sideExtent, 1e-6);
Assert.AreEqual(expectedBlend, result.upExtent, 1e-6);
}
[TestMethod]
public void Ground_YUp_HostZ135_ShouldBlendForwardWithUp_AndKeepSide()
{
var result = RotatedObjectExtentHelper.CalculateGroundSemanticExtents(
CoordinateSystemType.YUp,
forwardSize: 6.0,
sideSize: 4.0,
upSize: 2.0,
hostCorrection: new LocalEulerRotationCorrection(0.0, 0.0, 135.0));
double expectedBlend = 4.0 * Math.Sqrt(2.0);
Assert.AreEqual(expectedBlend, result.forwardExtent, 1e-6);
Assert.AreEqual(4.0, result.sideExtent, 1e-6);
Assert.AreEqual(expectedBlend, result.upExtent, 1e-6);
}
[TestMethod]
public void Ground_YUp_HostZ180_ShouldKeepSemanticExtents()
{
var result = RotatedObjectExtentHelper.CalculateGroundSemanticExtents(
CoordinateSystemType.YUp,
forwardSize: 6.0,
sideSize: 4.0,
upSize: 2.0,
hostCorrection: new LocalEulerRotationCorrection(0.0, 0.0, 180.0));
Assert.AreEqual(6.0, result.forwardExtent, 1e-6);
Assert.AreEqual(4.0, result.sideExtent, 1e-6);
Assert.AreEqual(2.0, result.upExtent, 1e-6);
}
[TestMethod]
public void Ground_YUp_HostZ270_ShouldPromoteUpToForward_AndKeepSide()
{
var result = RotatedObjectExtentHelper.CalculateGroundSemanticExtents(
CoordinateSystemType.YUp,
forwardSize: 6.0,
sideSize: 4.0,
upSize: 2.0,
hostCorrection: new LocalEulerRotationCorrection(0.0, 0.0, 270.0));
Assert.AreEqual(2.0, result.forwardExtent, 1e-6);
Assert.AreEqual(4.0, result.sideExtent, 1e-6);
Assert.AreEqual(6.0, result.upExtent, 1e-6);
}
}
}

View File

@ -1,85 +0,0 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using NavisworksTransport.Utils;
using System.Numerics;
using NavisworksTransport.Core.Config;
namespace NavisworksTransport.UnitTests.CoordinateSystem
{
[TestClass]
public class ViewpointHelperTests
{
[TestMethod]
public void ResolvePathViewpointProfile_ShouldReturnExpectedDefaults()
{
var config = ConfigManager.Instance.Current.PathEditing;
var ground = ViewpointHelper.ResolvePathViewpointProfile(ViewpointHelper.ViewpointStrategy.PathGroundSelection);
var hoisting = ViewpointHelper.ResolvePathViewpointProfile(ViewpointHelper.ViewpointStrategy.PathHoistingSelection);
var rail = ViewpointHelper.ResolvePathViewpointProfile(ViewpointHelper.ViewpointStrategy.PathRailSelection);
Assert.AreEqual(12.0, ground.CameraDistanceMeters, 1e-9);
Assert.AreEqual(90.0, ground.ElevationDegrees, 1e-9);
Assert.AreEqual(config.HoistingViewDistanceMeters, hoisting.CameraDistanceMeters, 1e-9);
Assert.AreEqual(config.HoistingViewElevationDegrees, hoisting.ElevationDegrees, 1e-9);
Assert.AreEqual(ViewpointHelper.PathCameraHorizontalMode.Side, hoisting.HorizontalMode);
Assert.AreEqual(config.RailViewDistanceMeters, rail.CameraDistanceMeters, 1e-9);
Assert.AreEqual(config.RailViewElevationDegrees, rail.ElevationDegrees, 1e-9);
Assert.AreEqual(ViewpointHelper.PathCameraHorizontalMode.Side, rail.HorizontalMode);
}
[TestMethod]
public void ResolveCameraOffsetDirection_Hoisting_ShouldLookFromSideAndSlightlyAbove()
{
Vector3 hostUp = Vector3.UnitZ;
Vector3 forward = Vector3.UnitX;
var profile = ViewpointHelper.ResolvePathViewpointProfile(ViewpointHelper.ViewpointStrategy.PathHoistingSelection);
Vector3 offset = ViewpointHelper.ResolveCameraOffsetDirection(profile, hostUp, forward);
Assert.AreEqual(0.0f, offset.X, 1e-5f);
Assert.IsTrue(offset.Y > 0.0f);
Assert.IsTrue(offset.Z > 0.0f);
}
[TestMethod]
public void ResolveCameraOffsetDirection_Rail_ShouldLookFromSideAndSlightlyAbove()
{
Vector3 hostUp = Vector3.UnitZ;
Vector3 forward = Vector3.UnitX;
var profile = ViewpointHelper.ResolvePathViewpointProfile(ViewpointHelper.ViewpointStrategy.PathRailSelection);
Vector3 offset = ViewpointHelper.ResolveCameraOffsetDirection(profile, hostUp, forward);
Assert.AreEqual(0.0f, offset.X, 1e-5f);
Assert.IsTrue(offset.Y > 0.0f);
Assert.IsTrue(offset.Z > 0.0f);
}
[TestMethod]
public void ResolveHorizontalPathForward_ShouldFallbackWhenPathIsVertical()
{
Vector3 hostUp = Vector3.UnitZ;
Vector3 rawVertical = Vector3.UnitZ;
Vector3 fallback = Vector3.UnitY;
Vector3 resolved = ViewpointHelper.ResolveHorizontalPathForward(rawVertical, hostUp, fallback);
Assert.AreEqual(0.0f, resolved.Z, 1e-5f);
Assert.AreEqual(1.0f, resolved.Y, 1e-5f);
}
[TestMethod]
public void ResolveFocusViewpointProfile_ShouldCentralizeNamedFocusStrategies()
{
var modelFocus = ViewpointHelper.ResolveFocusViewpointProfile(ViewpointHelper.ViewpointStrategy.ModelFocus);
var collision = ViewpointHelper.ResolveFocusViewpointProfile(ViewpointHelper.ViewpointStrategy.CollisionCloseUp);
Assert.AreEqual(60.0, modelFocus.ViewAngleDegrees, 1e-9);
Assert.AreEqual(0.25, modelFocus.TargetViewRatio, 1e-9);
Assert.AreEqual(60.0, collision.ViewAngleDegrees, 1e-9);
Assert.AreEqual(0.25, collision.TargetViewRatio, 1e-9);
}
}
}

View File

@ -24,15 +24,5 @@ namespace NavisworksTransport.UnitTests.Core
Assert.AreEqual("副本_我的路径", duplicatedName);
}
[TestMethod]
public void GenerateCollisionReportFileName_ShouldUseUnifiedChinesePrefixAndTimestamp()
{
var now = new DateTime(2026, 4, 9, 23, 55, 1);
var fileName = PathHelper.GenerateCollisionReportFileName("人工_0409_235000", "html", now);
Assert.AreEqual("碰撞检测报告_人工_0409_235000_20260409_235501.html", fileName);
}
}
}

View File

@ -1,47 +0,0 @@
using System.Collections.Generic;
using Autodesk.Navisworks.Api;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace NavisworksTransport.UnitTests.Core
{
[TestClass]
public class PathPlanningManagerHoistingCompletionTests
{
[TestMethod]
public void ReuseLastHoistingPointAsDescendPoint_ShouldPreserveGeometryAndAvoidDuplicateSegment_FromRealLogCase()
{
var originalLastAerialPointPosition = new Point3D(-132.36, -16.89, -6.95);
var pathPoints = new List<PathPoint>
{
new PathPoint(new Point3D(-140.00, -30.01, -6.95), "起吊点", PathPointType.StartPoint) { Index = 0, Direction = HoistingPointDirection.Vertical },
new PathPoint(new Point3D(-140.00, -16.89, -6.95), "提升点", PathPointType.WayPoint) { Index = 1, Direction = HoistingPointDirection.Vertical },
new PathPoint(new Point3D(-150.00, -16.89, -6.95), "路径点7", PathPointType.WayPoint) { Index = 2, Direction = HoistingPointDirection.Longitudinal },
new PathPoint(originalLastAerialPointPosition, "路径点8", PathPointType.WayPoint) { Index = 3, Direction = HoistingPointDirection.Longitudinal }
};
var finalGroundPoint = new Point3D(-132.36, -30.01, -6.95);
var originalLastPoint = pathPoints[3];
PathPoint descendPoint = PathPlanningManager.ReuseLastHoistingPointAsDescendPoint(pathPoints);
var endPoint = new PathPoint(finalGroundPoint, "落地点", PathPointType.EndPoint)
{
Index = pathPoints.Count,
Direction = HoistingPointDirection.Vertical
};
pathPoints.Add(endPoint);
Assert.AreEqual(5, pathPoints.Count);
Assert.AreSame(pathPoints[3], descendPoint);
Assert.AreSame(originalLastPoint, descendPoint);
Assert.AreEqual("下降点", pathPoints[3].Name);
Assert.AreEqual(PathPointType.WayPoint, pathPoints[3].Type);
Assert.AreEqual(HoistingPointDirection.Vertical, pathPoints[3].Direction);
Assert.AreSame(originalLastAerialPointPosition, pathPoints[3].Position);
Assert.AreEqual("落地点", pathPoints[4].Name);
Assert.AreEqual(PathPointType.EndPoint, pathPoints[4].Type);
Assert.AreEqual(HoistingPointDirection.Vertical, pathPoints[4].Direction);
Assert.AreSame(finalGroundPoint, pathPoints[4].Position);
}
}
}

View File

@ -1,45 +0,0 @@
using System.Collections.Generic;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using NavisworksTransport.Utils;
namespace NavisworksTransport.UnitTests.Core
{
[TestClass]
public class SelectionClipBoxLockStateTests
{
[TestMethod]
public void ShouldRefreshOnSelectionChanged_ReturnsFalse_WhenSelectionClipBoxIsLocked()
{
var state = new SelectionClipBoxLockState();
state.LockSelection(new object[] { new object(), new object() });
bool shouldRefresh = state.ShouldRefreshOnSelectionChanged(isSelectionClipBoxMode: true);
Assert.IsFalse(shouldRefresh);
Assert.AreEqual(2, state.LockedSelectionCount);
}
[TestMethod]
public void ShouldRefreshOnSelectionChanged_ReturnsTrue_WhenSelectionClipBoxModeHasNoLockedSelection()
{
var state = new SelectionClipBoxLockState();
bool shouldRefresh = state.ShouldRefreshOnSelectionChanged(isSelectionClipBoxMode: true);
Assert.IsTrue(shouldRefresh);
Assert.AreEqual(0, state.LockedSelectionCount);
}
[TestMethod]
public void Clear_UnlocksSelectionClipBoxState()
{
var state = new SelectionClipBoxLockState();
state.LockSelection(new List<object> { new object() });
state.Clear();
Assert.IsFalse(state.IsLocked);
Assert.AreEqual(0, state.LockedSelectionCount);
}
}
}

View File

@ -1,66 +0,0 @@
using System;
using System.Threading.Tasks;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Newtonsoft.Json.Linq;
namespace NavisworksTransport.UnitTests.Integration
{
[TestClass]
[TestCategory("NavisworksIntegration")]
[TestCategory("NavisworksIntegration.Ground")]
public class AutoPathGridGenerationAutomationTests
{
private const string BaselineRouteName = "测试基准";
private const double BaselineObjectLengthInMeters = 0.4;
private const double BaselineObjectWidthInMeters = 0.4;
private const double BaselineSafetyMarginInMeters = 0.05;
private const double BaselineGridSizeInMeters = 0.3;
[TestMethod]
[Timeout(240000)]
public async Task GroundAutoPath_BaselineRoute_CreatesPath()
{
using (var client = new NavisworksTestAutomationClient())
{
await client.EnsureServiceReadyAsync(TimeSpan.FromSeconds(90)).ConfigureAwait(false);
JObject response = await client.RunAutoPathAsync(
"Ground",
BaselineRouteName,
BaselineObjectLengthInMeters,
BaselineObjectWidthInMeters,
BaselineSafetyMarginInMeters,
BaselineGridSizeInMeters).ConfigureAwait(false);
Assert.IsTrue(response.Value<bool>("ok"), "测试 HTTP 接口返回失败: " + (string)response["error"]);
JObject data = (JObject)response["data"];
Assert.IsNotNull(data, "缺少测试结果 data");
JObject sourceRoute = (JObject)data["sourceRoute"];
Assert.IsNotNull(sourceRoute, "缺少 sourceRoute");
Assert.AreEqual(BaselineRouteName, (string)sourceRoute["name"], "自动路径测试必须使用精确命名的测试基准路径作为起终点来源");
Assert.AreEqual("Ground", (string)sourceRoute["pathType"], "源路径类型不匹配");
JObject generatedRoute = (JObject)data["generatedRoute"];
Assert.IsNotNull(generatedRoute, "缺少 generatedRoute");
Assert.AreEqual("Ground", (string)generatedRoute["pathType"], "生成路径类型不匹配");
Assert.IsTrue((int)generatedRoute["pointCount"] >= 2, "生成路径至少应包含起点和终点");
Assert.IsTrue((double)generatedRoute["length"] > 0, "生成路径长度必须大于 0");
JObject planningParameters = (JObject)data["planningParameters"];
Assert.IsNotNull(planningParameters, "缺少 planningParameters");
Assert.AreEqual(BaselineObjectLengthInMeters, (double)planningParameters["objectLengthInMeters"], 1e-9, "物体长度参数不匹配");
Assert.AreEqual(BaselineObjectWidthInMeters, (double)planningParameters["objectWidthInMeters"], 1e-9, "物体宽度参数不匹配");
Assert.AreEqual(BaselineSafetyMarginInMeters, (double)planningParameters["safetyMarginInMeters"], 1e-9, "安全间隙参数不匹配");
Assert.AreEqual(BaselineGridSizeInMeters, (double)planningParameters["gridSizeInMeters"], 1e-9, "网格大小参数不匹配");
JObject segmentValidation = (JObject)data["segmentValidation"];
Assert.IsNotNull(segmentValidation, "缺少 segmentValidation");
Assert.AreEqual(0, (int)segmentValidation["blockedSampleCount"], "生成路径存在穿过不可通行网格的采样点");
Assert.AreEqual(0, (int)segmentValidation["blockedCellInteriorIntersectionCount"], "生成路径线段穿过了不可通行网格的实际归属区域");
Assert.AreEqual(0, (int)segmentValidation["invalidSampleCount"], "生成路径存在落在网格外的采样点");
}
}
}
}

View File

@ -1,116 +0,0 @@
using System;
using System.Globalization;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Newtonsoft.Json.Linq;
namespace NavisworksTransport.UnitTests.Integration
{
internal sealed class NavisworksTestAutomationClient : IDisposable
{
private readonly HttpClient _httpClient;
public NavisworksTestAutomationClient()
{
_httpClient = new HttpClient
{
BaseAddress = new Uri("http://127.0.0.1:18777"),
Timeout = TimeSpan.FromSeconds(20)
};
}
public async Task EnsureServiceReadyAsync(TimeSpan timeout)
{
DateTime deadlineUtc = DateTime.UtcNow.Add(timeout);
Exception lastError = null;
while (DateTime.UtcNow < deadlineUtc)
{
try
{
JObject pingResponse = await GetJsonAsync("/api/test/ping").ConfigureAwait(false);
if (pingResponse.Value<bool?>("ok") == true)
{
return;
}
}
catch (Exception ex)
{
lastError = ex;
}
await Task.Delay(TimeSpan.FromSeconds(1)).ConfigureAwait(false);
}
Assert.Fail(
"Navisworks 测试服务未就绪。请先用 start-navisworks.bat 启动 Navisworks并确保插件面板已加载。最后错误: {0}",
lastError?.Message ?? "unknown");
}
public async Task<JObject> RunVirtualCollisionTestAsync(string pathType, int timeoutSeconds)
{
string requestUri = string.Format(
"/api/test/run-virtual-collision-test?pathType={0}&timeoutSeconds={1}",
Uri.EscapeDataString(pathType),
timeoutSeconds);
return await PostJsonAsync(requestUri).ConfigureAwait(false);
}
public async Task<JObject> AnalyzeAutoPathGridAsync(string pathType)
{
string requestUri = string.Format(
"/api/test/analyze-auto-path-grid?pathType={0}",
Uri.EscapeDataString(pathType));
return await PostJsonAsync(requestUri).ConfigureAwait(false);
}
public async Task<JObject> RunAutoPathAsync(
string pathType,
string routeName,
double objectLengthInMeters,
double objectWidthInMeters,
double safetyMarginInMeters,
double gridSizeInMeters)
{
string requestUri = string.Format(
CultureInfo.InvariantCulture,
"/api/test/run-auto-path?pathType={0}&routeName={1}&objectLengthInMeters={2}&objectWidthInMeters={3}&safetyMarginInMeters={4}&gridSizeInMeters={5}",
Uri.EscapeDataString(pathType),
Uri.EscapeDataString(routeName),
objectLengthInMeters,
objectWidthInMeters,
safetyMarginInMeters,
gridSizeInMeters);
return await PostJsonAsync(requestUri).ConfigureAwait(false);
}
private async Task<JObject> GetJsonAsync(string requestUri)
{
using (HttpResponseMessage response = await _httpClient.GetAsync(requestUri).ConfigureAwait(false))
{
string content = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
response.EnsureSuccessStatusCode();
return JObject.Parse(content);
}
}
private async Task<JObject> PostJsonAsync(string requestUri)
{
using (HttpResponseMessage response = await _httpClient.PostAsync(requestUri, new StringContent(string.Empty)).ConfigureAwait(false))
{
string content = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
response.EnsureSuccessStatusCode();
return JObject.Parse(content);
}
}
public void Dispose()
{
_httpClient.Dispose();
}
}
}

View File

@ -1,68 +0,0 @@
using System;
using System.Threading.Tasks;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Newtonsoft.Json.Linq;
namespace NavisworksTransport.UnitTests.Integration
{
[TestClass]
[TestCategory("NavisworksIntegration")]
public class VirtualCollisionAutomationTests
{
private const int DefaultCollisionTimeoutSeconds = 240;
[TestMethod]
[Timeout(360000)]
public async Task GroundVirtualCollision_AutoTestRoute_Completes()
{
await AssertVirtualCollisionTestAsync("Ground").ConfigureAwait(false);
}
[TestMethod]
[Timeout(360000)]
public async Task HoistingVirtualCollision_AutoTestRoute_Completes()
{
await AssertVirtualCollisionTestAsync("Hoisting").ConfigureAwait(false);
}
[TestMethod]
[Timeout(360000)]
public async Task RailVirtualCollision_AutoTestRoute_Completes()
{
await AssertVirtualCollisionTestAsync("Rail").ConfigureAwait(false);
}
private static async Task AssertVirtualCollisionTestAsync(string pathType)
{
using (var client = new NavisworksTestAutomationClient())
{
await client.EnsureServiceReadyAsync(TimeSpan.FromSeconds(90)).ConfigureAwait(false);
JObject response = await client.RunVirtualCollisionTestAsync(pathType, DefaultCollisionTimeoutSeconds).ConfigureAwait(false);
Assert.IsTrue(response.Value<bool>("ok"), "测试 HTTP 接口返回失败: " + (string)response["error"]);
JObject data = (JObject)response["data"];
Assert.IsNotNull(data, "缺少测试结果 data");
Assert.AreEqual(pathType, (string)data["requestedPathType"], "请求的路径类型不匹配");
JObject route = (JObject)data["route"];
Assert.IsNotNull(route, "缺少 route");
Assert.AreEqual(pathType, (string)route["pathType"], "返回的路径类型不匹配");
Assert.IsFalse(string.IsNullOrWhiteSpace((string)route["name"]), "路径名为空");
JObject animatedObject = (JObject)data["animatedObject"];
Assert.IsNotNull(animatedObject, "缺少 animatedObject");
Assert.AreEqual("VirtualObject", (string)animatedObject["mode"], "当前集成测试应使用虚拟物体");
JObject animation = (JObject)data["animation"];
Assert.IsNotNull(animation, "缺少 animation");
Assert.AreEqual("Finished", (string)animation["currentState"], "动画未完成");
Assert.IsTrue((int)animation["totalFrames"] > 0, "动画总帧数应大于 0");
Assert.IsTrue(animation["detectionRecordId"] != null && (int)animation["detectionRecordId"] > 0, "检测记录 ID 无效");
Assert.IsNotNull(data["report"], "缺少碰撞报告");
}
}
}
}

View File

@ -1,400 +0,0 @@
using System;
using Autodesk.Navisworks.Api;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using NavisworksTransport.Utils;
namespace NavisworksTransport.UnitTests.Utils
{
/// <summary>
/// BoundingBoxGeometryUtils 的单元测试
/// 重点验证旋转后包围盒中心偏移计算的数学正确性
/// 使用大尺寸和夸张比例,确保结果清晰可见
/// </summary>
[TestClass]
public class BoundingBoxGeometryUtilsTests
{
#region CalculateRotatedBoundingBoxCenterOffset Tests -
/// <summary>
/// X方向超长的长方体类似长沙发/长桌绕Z轴旋转90度
/// 尺寸: 10000 x 100 x 50
/// 预期: X方向大偏移
/// </summary>
[TestMethod]
public void CalculateRotatedBoundingBoxCenterOffset_LongX_Rotate90AroundZ_LargeXOffset()
{
// X方向从 -5000 到 +5000总长10000
// Y方向从 -50 到 +50总长100
// Z方向从 -25 到 +25总长50
var bounds = new BoundingBox3D(
new Point3D(-5000, -50, -25),
new Point3D(5000, 50, 25));
// 原中心: (0, 0, 0)
// 8角点: (±5000, ±50, ±25)
// 绕Z轴旋转90度 (x'=-y, y'=x):
// (5000, 50) -> (-50, 5000)
// (5000, -50) -> (50, 5000)
// (-5000, 50) -> (-50, -5000)
// (-5000, -50) -> (50, -5000)
//
// 新X范围: [-50, 50]来自原Y范围
// 新Y范围: [-5000, 5000]来自原X范围
// 新中心: (0, 0, 0)
// 偏移: (0, 0, 0) - 等等这不应该是0
// 啊不对,让我重新算:
// 原中心是 (0, 0, 0)
// 旋转后各角点分布在 [-50,50]x[-5000,5000]x[-25,25]
// 新AABB中心仍然是 (0, 0, 0)
// 所以偏移应该是 0
// 等等,问题在于原中心是 (0,0,0),所以新中心也是 (0,0,0)
// 我需要测试非对称的,即原中心不在原点的情况
// 重新设计:让包围盒中心不在原点
// Min=(-1000, -50, -25), Max=(9000, 50, 25)
// 中心: (4000, 0, 0)
bounds = new BoundingBox3D(
new Point3D(-1000, -50, -25),
new Point3D(9000, 50, 25));
var rotation = new Rotation3D(new UnitVector3D(0, 0, 1), Math.PI / 2);
var offset = BoundingBoxGeometryUtils.CalculateRotatedBoundingBoxCenterOffset(bounds, rotation);
// 原中心: (4000, 0, 0)
// 8角点相对中心: (-5000,±50,±25), (5000,±50,±25)
//
// 旋转90度后:
// (-5000, 50) -> (-50, -5000)
// (-5000, -50) -> (50, -5000)
// (5000, 50) -> (-50, 5000)
// (5000, -50) -> (50, 5000)
//
// 新X范围: [-50, 50]
// 新Y范围: [-5000, 5000]
// 新中心: (0, 0, 0)
// 偏移: (0-4000, 0-0, 0-0) = (-4000, 0, 0)
Console.WriteLine($"LongX-Rotate90: offset=({offset.X:F1}, {offset.Y:F1}, {offset.Z:F1})");
Assert.AreEqual(-4000, offset.X, 1, "X方向大偏移应为-4000");
Assert.AreEqual(0, offset.Y, 1, "Y偏移应为0");
Assert.AreEqual(0, offset.Z, 0.1, "Z偏移应为0");
}
/// <summary>
/// Y方向超长的长方体绕X轴旋转90度
/// 尺寸: 100 x 10000 x 50
/// </summary>
[TestMethod]
public void CalculateRotatedBoundingBoxCenterOffset_LongY_Rotate90AroundX_LargeYOffset()
{
// Y方向从 -1000 到 +9000总长10000中心在4000
// X方向从 -50 到 +50总长100
// Z方向从 -25 到 +25总长50
var bounds = new BoundingBox3D(
new Point3D(-50, -1000, -25),
new Point3D(50, 9000, 25));
// 绕X轴旋转90度 (y'=-z, z'=y)
var rotation = new Rotation3D(new UnitVector3D(1, 0, 0), Math.PI / 2);
var offset = BoundingBoxGeometryUtils.CalculateRotatedBoundingBoxCenterOffset(bounds, rotation);
// 原中心: (0, 4000, 0)
// 8角点相对中心: (±50, -5000, ±25), (±50, 5000, ±25)
//
// 旋转90度后:
// y' = -z, z' = y
// (50, -5000, 25) -> (50, -25, -5000)
// (50, -5000, -25) -> (50, 25, -5000)
// (50, 5000, 25) -> (50, -25, 5000)
// (50, 5000, -25) -> (50, 25, 5000)
// (-50, -5000, 25) -> (-50, -25, -5000)
// (-50, -5000, -25) -> (-50, 25, -5000)
// (-50, 5000, 25) -> (-50, -25, 5000)
// (-50, 5000, -25) -> (-50, 25, 5000)
//
// 新X范围: [-50, 50]
// 新Y范围: [-25, 25]
// 新Z范围: [-5000, 5000]
// 新中心: (0, 0, 0)
// 偏移: (0-0, 0-4000, 0-0) = (0, -4000, 0)
Console.WriteLine($"LongY-Rotate90: offset=({offset.X:F1}, {offset.Y:F1}, {offset.Z:F1})");
Assert.AreEqual(0, offset.X, 1, "X偏移应为0");
Assert.AreEqual(-4000, offset.Y, 1, "Y方向大偏移应为-4000");
Assert.AreEqual(0, offset.Z, 0.1, "Z偏移应为0");
}
/// <summary>
/// Z方向超长的长方体绕Y轴旋转90度
/// 尺寸: 100 x 100 x 10000
/// </summary>
[TestMethod]
public void CalculateRotatedBoundingBoxCenterOffset_LongZ_Rotate90AroundY_LargeZOffset()
{
// Z方向从 -1000 到 +9000总长10000中心在4000
// X方向从 -50 到 +50总长100
// Y方向从 -50 到 +50总长100
var bounds = new BoundingBox3D(
new Point3D(-50, -50, -1000),
new Point3D(50, 50, 9000));
// 绕Y轴旋转90度 (z'=x, x'=-z) - 等等,让我确认旋转矩阵
// 绕Y轴旋转θ: x'=x*cosθ+z*sinθ, z'=-x*sinθ+z*cosθ
// 90度时: x'=z, z'=-x
var rotation = new Rotation3D(new UnitVector3D(0, 1, 0), Math.PI / 2);
var offset = BoundingBoxGeometryUtils.CalculateRotatedBoundingBoxCenterOffset(bounds, rotation);
// 原中心: (0, 0, 4000)
// 8角点相对中心: (±50,±50,-5000), (±50,±50,5000)
//
// 旋转90度后 (x'=z, z'=-x):
// (50, 50, -5000) -> (-5000, 50, -50)
// (50, 50, 5000) -> (5000, 50, -50)
// (-50, 50, -5000) -> (-5000, 50, 50)
// (-50, 50, 5000) -> (5000, 50, 50)
// 以及Y=-50的四个点...
//
// 新X范围: [-5000, 5000]
// 新Y范围: [-50, 50]
// 新Z范围: [-50, 50]
// 新中心: (0, 0, 0)
// 偏移: (0-0, 0-0, 0-4000) = (0, 0, -4000)
Console.WriteLine($"LongZ-Rotate90: offset=({offset.X:F1}, {offset.Y:F1}, {offset.Z:F1})");
Assert.AreEqual(0, offset.X, 1, "X偏移应为0");
Assert.AreEqual(0, offset.Y, 0.1, "Y偏移应为0");
Assert.AreEqual(-4000, offset.Z, 1, "Z方向大偏移应为-4000");
}
/// <summary>
/// 三轴都不同比例的长方体绕Z轴旋转45度
/// 尺寸: 1000 x 200 x 100
/// </summary>
[TestMethod]
public void CalculateRotatedBoundingBoxCenterOffset_AsymmetricXYZ_Rotate45AroundZ_MultipleOffsets()
{
// X: -500 ~ +500 (中心0但非对称也可测)
// Y: -100 ~ +100 (中心0)
// Z: -50 ~ +50 (中心0)
// 等等中心0的话偏移还是0...
// 改用非中心对称的
// X: -100 ~ +900 (总长1000中心400)
// Y: -80 ~ +120 (总长200中心20)
// Z: -30 ~ +70 (总长100中心20)
var bounds = new BoundingBox3D(
new Point3D(-100, -80, -30),
new Point3D(900, 120, 70));
// 绕Z轴旋转45度
var rotation = new Rotation3D(new UnitVector3D(0, 0, 1), Math.PI / 4);
var offset = BoundingBoxGeometryUtils.CalculateRotatedBoundingBoxCenterOffset(bounds, rotation);
// 原中心: (400, 20, 20)
//
// 8角点相对中心:
// (-500,-100,-50), (500,-100,-50), (-500,100,-50), (500,100,-50)
// (-500,-100,50), (500,-100,50), (-500,100,50), (500,100,50)
//
// 旋转45度后 (x'=(x-y)/√2, y'=(x+y)/√2):
// (-500,-100) -> (-400/√2, -600/√2) ≈ (-283, -424)
// (500,-100) -> (600/√2, 400/√2) ≈ (424, 283)
// (-500,100) -> (-600/√2, -400/√2) ≈ (-424, -283)
// (500,100) -> (400/√2, 600/√2) ≈ (283, 424)
//
// 新X范围: [-424, 424]
// 新Y范围: [-424, 424]
// 新中心: (0, 0, 20) - Z不变
// 偏移: (0-400, 0-20, 20-20) = (-400, -20, 0)
Console.WriteLine($"Asymmetric-Rotate45: offset=({offset.X:F1}, {offset.Y:F1}, {offset.Z:F1})");
// 允许一定误差(因为手动计算是近似值)
Assert.IsTrue(Math.Abs(offset.X - (-400)) < 50, $"X偏移应约-400实际是{offset.X:F1}");
Assert.IsTrue(Math.Abs(offset.Y - (-20)) < 50, $"Y偏移应约-20实际是{offset.Y:F1}");
Assert.AreEqual(0, offset.Z, 0.1, "Z偏移应为0");
}
/// <summary>
/// 实际沙发案例放大版
/// 模拟沙发: 长2000宽100高80中心偏移
/// </summary>
[TestMethod]
public void CalculateRotatedBoundingBoxCenterOffset_BigSofa_Rotate90AroundZ_RealisticCase()
{
// 沙发包围盒放大版
// X: -200 ~ +1800 (总长2000中心800)
// Y: -50 ~ +50 (总长100中心0)
// Z: 0 ~ +80 (总高80中心40)
var bounds = new BoundingBox3D(
new Point3D(-200, -50, 0),
new Point3D(1800, 50, 80));
var rotation = new Rotation3D(new UnitVector3D(0, 0, 1), Math.PI / 2);
var offset = BoundingBoxGeometryUtils.CalculateRotatedBoundingBoxCenterOffset(bounds, rotation);
// 原中心: (800, 0, 40)
//
// 8角点相对中心:
// (-1000,-50,-40), (1000,-50,-40), (-1000,50,-40), (1000,50,-40)
// (-1000,-50,40), (1000,-50,40), (-1000,50,40), (1000,50,40)
//
// 旋转90度后 (x'=-y, y'=x):
// (-1000,-50) -> (50, -1000)
// (1000,-50) -> (50, 1000)
// (-1000,50) -> (-50, -1000)
// (1000,50) -> (-50, 1000)
//
// 新X范围: [-50, 50]
// 新Y范围: [-1000, 1000]
// 新中心: (0, 0, 40) - Z不变
// 偏移: (0-800, 0-0, 40-40) = (-800, 0, 0)
Console.WriteLine($"BigSofa-Rotate90: offset=({offset.X:F1}, {offset.Y:F1}, {offset.Z:F1})");
Assert.AreEqual(-800, offset.X, 10, "X方向偏移应为-800");
Assert.AreEqual(0, offset.Y, 10, "Y偏移应为0");
Assert.AreEqual(0, offset.Z, 0.1, "Z偏移应为0");
}
/// <summary>
/// X方向超长绕Z轴旋转45度 - 测试非90度旋转
/// 尺寸: 10000 x 100 x 50
/// </summary>
[TestMethod]
public void CalculateRotatedBoundingBoxCenterOffset_LongX_Rotate45AroundZ_DiagonalOffset()
{
// X方向从 -1000 到 +9000总长10000中心4000
// Y方向从 -50 到 +50总长100
var bounds = new BoundingBox3D(
new Point3D(-1000, -50, -25),
new Point3D(9000, 50, 25));
// 绕Z轴旋转45度
var rotation = new Rotation3D(new UnitVector3D(0, 0, 1), Math.PI / 4);
var offset = BoundingBoxGeometryUtils.CalculateRotatedBoundingBoxCenterOffset(bounds, rotation);
// 原中心: (4000, 0, 0)
// 8角点相对中心: (-5000,±50,±25), (5000,±50,±25)
//
// 旋转45度后 (x'=(x-y)/√2, y'=(x+y)/√2):
// (-5000, 50) -> (-5050/√2, -4950/√2) ≈ (-3571, -3500)
// (-5000, -50) -> (-4950/√2, -5050/√2) ≈ (-3500, -3571)
// (5000, 50) -> (4950/√2, 5050/√2) ≈ (3500, 3571)
// (5000, -50) -> (5050/√2, 4950/√2) ≈ (3571, 3500)
//
// 新X范围: [-3571, 3571]
// 新Y范围: [-3571, 3571]
// 新中心: (0, 0, 0)
// 偏移: (0-4000, 0-0, 0-0) = (-4000, 0, 0)
Console.WriteLine($"LongX-Rotate45: offset=({offset.X:F1}, {offset.Y:F1}, {offset.Z:F1})");
// X方向应该有较大负偏移接近-4000
Assert.IsTrue(offset.X < -3500, $"45度旋转后X偏移应小于-3500实际是{offset.X:F1}");
Assert.IsTrue(offset.X > -4500, $"45度旋转后X偏移应大于-4500实际是{offset.X:F1}");
// Y方向偏移应该很小因为原Y中心是0
Assert.IsTrue(Math.Abs(offset.Y) < 100, $"45度旋转后Y偏移应接近0实际是{offset.Y:F1}");
Assert.AreEqual(0, offset.Z, 0.1, "Z偏移应为0");
}
/// <summary>
/// XY方向都非对称的长方体绕Z轴旋转45度 - 测试双向偏移
/// 尺寸: 8000 x 4000 x 100
/// </summary>
[TestMethod]
public void CalculateRotatedBoundingBoxCenterOffset_XYBothAsymmetric_Rotate45AroundZ_BothOffsets()
{
// X方向从 -3000 到 +5000总长8000中心2000
// Y方向从 -1000 到 +3000总长4000中心1000
var bounds = new BoundingBox3D(
new Point3D(-3000, -1000, -50),
new Point3D(5000, 3000, 50));
// 绕Z轴旋转45度
var rotation = new Rotation3D(new UnitVector3D(0, 0, 1), Math.PI / 4);
var offset = BoundingBoxGeometryUtils.CalculateRotatedBoundingBoxCenterOffset(bounds, rotation);
// 原中心: (2000, 1000, 0)
// 8角点相对中心: (-5000,-2000,±50), (3000,-2000,±50), (-5000,2000,±50), (3000,2000,±50)
//
// 旋转45度后:
// (-5000, -2000) -> (-3000/√2, -7000/√2) ≈ (-2121, -4950)
// (3000, -2000) -> (5000/√2, 1000/√2) ≈ (3536, 707)
// (-5000, 2000) -> (-7000/√2, -3000/√2) ≈ (-4950, -2121)
// (3000, 2000) -> (1000/√2, 5000/√2) ≈ (707, 3536)
//
// 新X范围: [-4950, 3536]
// 新Y范围: [-4950, 3536]
// 新中心: ((-4950+3536)/2, (-4950+3536)/2, 0) = (-707, -707, 0)
// 偏移: (-707-2000, -707-1000, 0-0) = (-2707, -1707, 0)
Console.WriteLine($"XY-Asymmetric-Rotate45: offset=({offset.X:F1}, {offset.Y:F1}, {offset.Z:F1})");
// X方向应该有较大负偏移
Assert.IsTrue(offset.X < -2000, $"45度旋转后X偏移应小于-2000实际是{offset.X:F1}");
Assert.IsTrue(offset.X > -3500, $"45度旋转后X偏移应大于-3500实际是{offset.X:F1}");
// Y方向也应该有负偏移但比X小
Assert.IsTrue(offset.Y < -1000, $"45度旋转后Y偏移应小于-1000实际是{offset.Y:F1}");
Assert.IsTrue(offset.Y > -2500, $"45度旋转后Y偏移应大于-2500实际是{offset.Y:F1}");
Assert.AreEqual(0, offset.Z, 0.1, "Z偏移应为0");
}
/// <summary>
/// 零旋转验证 - 无论什么形状不旋转偏移都应为0
/// </summary>
[TestMethod]
public void CalculateRotatedBoundingBoxCenterOffset_NoRotation_ZeroOffset()
{
var bounds = new BoundingBox3D(
new Point3D(-1000, -100, -50),
new Point3D(9000, 100, 50));
var rotation = Rotation3D.Identity;
var offset = BoundingBoxGeometryUtils.CalculateRotatedBoundingBoxCenterOffset(bounds, rotation);
Console.WriteLine($"No-Rotation: offset=({offset.X:F1}, {offset.Y:F1}, {offset.Z:F1})");
Assert.AreEqual(0, offset.X, 1e-6, "无旋转时X偏移应为0");
Assert.AreEqual(0, offset.Y, 1e-6, "无旋转时Y偏移应为0");
Assert.AreEqual(0, offset.Z, 1e-6, "无旋转时Z偏移应为0");
}
/// <summary>
/// 180度旋转验证 - 包围盒应该不变(只是翻转)
/// </summary>
[TestMethod]
public void CalculateRotatedBoundingBoxCenterOffset_180DegreeRotation_ZeroOffset()
{
var bounds = new BoundingBox3D(
new Point3D(-1000, -100, -50),
new Point3D(9000, 100, 50));
var rotation = new Rotation3D(new UnitVector3D(0, 0, 1), Math.PI);
var offset = BoundingBoxGeometryUtils.CalculateRotatedBoundingBoxCenterOffset(bounds, rotation);
Console.WriteLine($"180-Rotation: offset=({offset.X:F1}, {offset.Y:F1}, {offset.Z:F1})");
Assert.AreEqual(0, offset.X, 1e-6, "180度旋转后X偏移应为0");
Assert.AreEqual(0, offset.Y, 1e-6, "180度旋转后Y偏移应为0");
Assert.AreEqual(0, offset.Z, 1e-6, "180度旋转后Z偏移应为0");
}
#endregion
}
}

View File

@ -3,7 +3,7 @@ setlocal
set "TARGET_DIR=%PROGRAMDATA%\Autodesk\Navisworks Manage 2026\plugins\TransportPlugin"
set "BUILD_DIR=%~dp0bin\x64\Release"
pwsh -NoProfile -ExecutionPolicy Bypass -Command ^
powershell -NoProfile -ExecutionPolicy Bypass -Command ^
"$ErrorActionPreference = 'Stop';" ^
"$targetDir = [System.IO.Path]::GetFullPath('%TARGET_DIR%');" ^
"$buildDir = [System.IO.Path]::GetFullPath('%BUILD_DIR%');" ^
@ -16,11 +16,6 @@ pwsh -NoProfile -ExecutionPolicy Bypass -Command ^
" 'System.Data.SQLite.dll'," ^
" 'Tomlyn.dll'" ^
");" ^
"$deployRootFiles = @(" ^
" 'TransportRibbon.xaml'," ^
" 'TransportRibbon_16.png'," ^
" 'TransportRibbon_32.png'" ^
");" ^
"$stalePluginFiles = @(" ^
" 'Autodesk.Navisworks.Api.dll'," ^
" 'NavisworksTransport.UnitTests.dll'," ^
@ -68,17 +63,6 @@ pwsh -NoProfile -ExecutionPolicy Bypass -Command ^
" if (-not (Test-Path $sourceDll)) { throw ('Deployment source file missing: ' + $sourceDll) }" ^
" Copy-Item $sourceDll $targetDir -Force;" ^
"}" ^
"foreach ($rootFileName in $deployRootFiles) {" ^
" $sourceRootFile = Join-Path $buildDir $rootFileName;" ^
" if (Test-Path $sourceRootFile) {" ^
" Copy-Item $sourceRootFile $targetDir -Force;" ^
" foreach ($locale in @('en-US','zh-CN')) {" ^
" $localeDir = Join-Path $targetDir $locale;" ^
" New-Item -ItemType Directory -Force -Path $localeDir | Out-Null;" ^
" Copy-Item $sourceRootFile $localeDir -Force;" ^
" }" ^
" }" ^
"}" ^
"if (Test-Path (Join-Path $buildDir 'resources')) {" ^
" New-Item -ItemType Directory -Force -Path (Join-Path $targetDir 'resources') | Out-Null;" ^
" Copy-Item (Join-Path $buildDir 'resources\\*') (Join-Path $targetDir 'resources') -Recurse -Force;" ^
@ -86,10 +70,6 @@ pwsh -NoProfile -ExecutionPolicy Bypass -Command ^
"" ^
"$sourceFiles = @();" ^
"foreach ($dllName in $deployDllNames) { $sourceFiles += Get-Item (Join-Path $buildDir $dllName) }" ^
"foreach ($rootFileName in $deployRootFiles) {" ^
" $sourceRootFile = Join-Path $buildDir $rootFileName;" ^
" if (Test-Path $sourceRootFile) { $sourceFiles += Get-Item $sourceRootFile }" ^
"}" ^
"if (Test-Path (Join-Path $buildDir 'resources')) {" ^
" $sourceFiles += Get-ChildItem (Join-Path $buildDir 'resources') -File;" ^
"}" ^
@ -102,17 +82,6 @@ pwsh -NoProfile -ExecutionPolicy Bypass -Command ^
" throw ('Deployment verification failed for ' + $sourceFile.Name + ': source=' + $sourceFile.LastWriteTime.ToString('yyyy-MM-dd HH:mm:ss.fff') + ' (' + $sourceFile.Length + '), target=' + $targetInfo.LastWriteTime.ToString('yyyy-MM-dd HH:mm:ss.fff') + ' (' + $targetInfo.Length + ')');" ^
" }" ^
" Write-Host ('Verified ' + $sourceFile.Name + ': ' + $targetInfo.LastWriteTime.ToString('yyyy-MM-dd HH:mm:ss.fff'));" ^
" if ($deployRootFiles -contains $sourceFile.Name) {" ^
" foreach ($locale in @('en-US','zh-CN')) {" ^
" $localeTargetFile = Join-Path (Join-Path $targetDir $locale) $sourceFile.Name;" ^
" if (-not (Test-Path $localeTargetFile)) { throw ('Deployment verification failed: missing locale target file ' + $localeTargetFile) }" ^
" $localeTargetInfo = Get-Item $localeTargetFile;" ^
" if ($sourceFile.Length -ne $localeTargetInfo.Length -or $sourceFile.LastWriteTime -ne $localeTargetInfo.LastWriteTime) {" ^
" throw ('Deployment verification failed for locale copy ' + $localeTargetFile);" ^
" }" ^
" Write-Host ('Verified ' + $locale + '\\' + $sourceFile.Name + ': ' + $localeTargetInfo.LastWriteTime.ToString('yyyy-MM-dd HH:mm:ss.fff'));" ^
" }" ^
" }" ^
"}" ^
"" ^
"Write-Host 'Plugin deployed successfully!';"

View File

@ -1,105 +0,0 @@
# 数据库兼容性 / 剖面盒导出修复报告
## 修复目标
1. 打开旧版本数据库时崩溃schema 不兼容)
2. NWF 多文档剖面盒导出时,无命中子树泄露到导出文件
3. 反复剖面盒导出后崩溃COM 跨线程访问)
4. 导出后剖面盒被关闭,状态栏按钮不同步
---
## 问题一:数据库 schema 不兼容导致启动崩溃
### 现象
打开带有旧版本 `.db` 文件的模型时,插件初始化报错:
```
SQL logic error: no such column: DetectionRecordId
```
### 根因
`CreateTables()` 使用 `CREATE TABLE IF NOT EXISTS` + `CREATE INDEX IF NOT EXISTS` 组合。旧数据库表已存在但 schema 是旧版(缺 `DetectionRecordId` 列),`CREATE TABLE IF NOT EXISTS` 静默跳过,紧随的 `CREATE INDEX ... ON ...(DetectionRecordId)` 直接炸。
同时 `PathDatabase.InitializeDatabase()` 还有以下问题:
- `PRAGMA user_version` 只写入、从未读取
- `CreateTables()` 无异常保护
- `BatchQueueItems``ALTER TABLE ADD COLUMN` 用裸 `try-catch` 吞异常
- `PathPlanningManager.DatabaseInitialize()``catch``throw;` 导致同一错误双重日志
### 修复
**PathDatabase.cs**
- 新增 `SafeCreateIndex()` 方法:建索引前用 `PRAGMA table_info` 检查列是否存在,缺列记 warning 跳过,不崩
- 新增 `EnsureColumnExists()` 方法:用 `PRAGMA table_info` 检查列,缺了才 `ALTER TABLE ADD COLUMN`,不吞其他异常
- 删除无用的 `PRAGMA user_version` 写入
- `InitializeDatabase()` 内用 `try-catch` 包裹 `CreateTables()` + `EnsurePathRoutesTableExtended()`,失败时关闭连接
- 清理注释中重复/杂乱的编号
- `CollisionDetectionRecords` 建表处加外键依赖注释
**PathPlanningManager.cs**
- 去掉 `catch` 后的 `throw;`,外层 `OnModelsCollectionChanged` 已有独立日志
---
## 问题二NWF 多文档剖面盒导出时空白子树泄露
### 现象
NWF 文件引用多个 NWD各为一个独立 `Model`),剖面盒只覆盖其中一个子树的元素,导出后其他平行子树仍然出现在 NWD 中。
### 根因
`SectionBoxExporter.CalculateHiddenItems()` 通过"兄弟节点算法"确定隐藏项:收集可见项(剖面盒内元素及其祖先)的父节点,隐藏同父节点下的不可见兄弟。当模型有多个平行 `RootItem`(来自不同 `Model`)时,完全空白的子树没有任何节点进入 `visibleSet`,其 `RootItem` 不会出现在 `uniqueParents` 中,整棵子树被遗漏。
### 修复
**SectionBoxExporter.GetObjectsAndHiddenItems()**
`CalculateHiddenItems()` 之后补一步——遍历所有 `document.Models``RootItem`,不在 `visibleSet` 中的直接加入 `ItemsToHide`。`SetHidden` 会级联隐藏所有后代,无需递归。
---
## 问题三:反复剖面盒导出崩溃
### 现象
连续多次"设置剖面盒 → 导出"操作,前几次正常,后续某次崩溃(无 managed 异常日志)。
### 根因
`LayerManagementViewModel` 中有两处不必要的 `await Task.Run()`
- `GetObjectsAndHiddenItems()` 被包在 `Task.Run` 中,在后台线程调用 Navisworks COM API`Model.HasGeometry`、`BoundingBox()` 等)
- `NwdExportHelper.ExportToNwdWithPrecomputedHiddenItems()` 同样被包在 `Task.Run`
Navisworks COM API 要求 STA 线程访问,跨线程调用在少量使用时不崩,多次使用后触发 STA 违规导致进程崩溃。
### 修复
**LayerManagementViewModel.cs**
- 去掉 `GetObjectsAndHiddenItems` 外的 `await Task.Run()` 包装,直接在 UI 线程调用
- 去掉 `ExportToNwdWithPrecomputedHiddenItems` 外的 `await Task.Run()` 包装,该方法内部已有 `Dispatcher.Invoke`,无需外层再切换线程
---
## 涉及文件
| 文件 | 改动 |
|------|------|
| `src/Core/PathDatabase.cs` | 新增 `SafeCreateIndex`、`EnsureColumnExists``InitializeDatabase` 加 try-catch删除 user_version清理注释 |
| `src/Core/PathPlanningManager.cs` | 去掉 `DatabaseInitialize` catch 中的 re-throw |
| `src/Core/SectionBoxExporter.cs` | `GetObjectsAndHiddenItems` 补空白 RootItem 隐藏逻辑 |
| `src/Utils/NwdExportHelper.cs` | 调试日志(已清理) |
| `src/Utils/VisibilityHelper.cs` | 调试日志(已清理) |
| `src/UI/WPF/ViewModels/LayerManagementViewModel.cs` | 去掉两处 `Task.Run` |
| `deploy-plugin.bat` | `powershell``pwsh`(兼容环境) |
| `AGENTS.md` | 补充编译/部署调用方式说明 |
| `src/Utils/SectionClipHelper.cs` | 新增 `RestoreClipBox` 公开方法 |
---
## 问题四:导出后剖面盒被关闭,状态栏按钮不同步
### 现象
导出剖面盒后Navisworks `ExportToNwd` 内部会关闭剖分功能,但状态栏两个剖面盒图标按钮仍保持激活状态。
### 根因
`ExportToNwd` 是 Navisworks 内部行为,绕过我们的 `SetClipBoxState`。ViewModel 的 `IsClipBoxEnabled`/`IsSelectionClipBoxEnabled` 未感知到变化。
### 修复
**VisibilityHelper.ExecuteWithPrecomputedHiddenItems**
导出前用 `SectionClipHelper.IsClipBoxEnabled` + `TryGetCurrentClipBox` 保存剖面盒状态,导出后检查并在被关闭时恢复。
**SectionClipHelper**
新增 `RestoreClipBox(BoundingBox3D)` 公开方法。

View File

@ -1,195 +0,0 @@
# 自动调整物体姿态以最小化通行截面投影面积
更新时间2026-05-27
## 1. 背景
动画检测中的“调整物体”窗口目前支持手工输入 X/Y/Z 三轴角度修正。用户希望增加一个“自动调整”按钮:对任意动画物体,在路径起点处自动寻找一个合适姿态,让物体经过路径上的门洞、狭窄通道时具有最大通过性。
Navisworks 当前主要提供 AABB 包围盒,真实物体的 fragment 代表姿态只能大概判断方向,不足以作为自动调整的精确依据。因此本功能不能依赖 fragment 推断“物体真实方向”,也不能只做绕宿主 up 轴的单轴旋转。单轴 yaw 调整手工即可完成,不满足目标。
## 2. 目标
在“调整物体”窗口增加自动调整能力:
- 支持三轴联动搜索,允许任意三维旋转。
- 允许物体横放、倒放、斜放,不施加运输姿态限制。
- 以路径起点方向为基准,最小化物体在门洞截面上的投影面积。
- 在投影面积相同或接近时,优先选择投影高度更小的姿态,让物体尽量平放。
- 结果写回现有 X/Y/Z 三个角度输入框,用户可继续手工微调。
- 不依赖 fragment 方向作为优化真值。
- 遵守宿主坐标系 / 内部坐标系 / 资产坐标系语义,不硬编码世界 Z 为 up。
## 3. 非目标
- 不在本阶段判断真实门洞尺寸,也不保证某条路径一定可通过。
- 不修改路径规划算法。
- 不重建物体 mesh 或 OBB。
- 不引入姿态限制,例如“保持底面朝下”或“禁止倒置”。
- 不用静默 fallback 掩盖优化失败;失败应明确提示。
## 4. 坐标系语义
UI 输入、日志、用户看到的 X/Y/Z 角度仍按宿主坐标系解释。
路径起点方向使用宿主坐标系点计算:
1. 取路径起点到下一个有效路径点的方向。
2. 剔除宿主 up 分量,得到水平路径前进方向 `HostPathForward`
3. 通过 `HostCoordinateAdapter` 获取宿主 up`HostUp`。
4. 截面横向方向为 `HostSide = normalize(cross(HostPathForward, HostUp))`
如果路径起始段过短或剔除 up 后方向退化,应继续查找后续有效路径段;若仍无法得到方向,则自动调整失败并提示用户。
## 5. 目标函数
自动调整的主目标函数是门洞截面投影面积:
```text
Score(rotation) = ProjectedWidthAcrossPath(rotation) * ProjectedHeightAlongHostUp(rotation)
```
其中:
- `ProjectedWidthAcrossPath`:物体在 `HostSide` 方向上的投影宽度。
- `ProjectedHeightAlongHostUp`:物体在 `HostUp` 方向上的投影高度。
- 路径前进方向上的长度不参与目标函数。
该目标符合“穿门”语义:门洞主要限制的是横向净宽和竖向净高,而不是物体沿路径方向占多长。
在工程现场中,门洞通常“宽度大于高度”,因此仅比较面积不够。两个姿态可能具有相同或非常接近的面积,但一个姿态更高、更窄,另一个姿态更低、更宽;实际通行时应优先选择更低、更平放的姿态。
因此最终排序应采用词典序或近似平局策略:
1. 先最小化 `ProjectedArea`
2. 当面积差异小于容差时,选择 `ProjectedHeightAlongHostUp` 更小的姿态。
3. 当面积和高度都接近时,再选择 `ProjectedWidthAcrossPath` 更小的姿态。
面积容差建议使用相对容差,例如 `1%`,避免因为 AABB 或浮点误差导致高度明显更优的姿态被面积微小差异淘汰。
## 6. 推荐算法
采用业界常见的黑盒无导数优化路线:
### 6.1 全局粗搜索
在完整三维旋转空间中生成候选姿态,避免只绕某一轴或只优化 yaw。
候选生成应满足:
- 覆盖三维姿态空间。
- 结果确定性,不依赖随机数。
- 粗搜索粒度可配置,默认优先保证交互速度。
可选实现:
- 均匀方向采样加 roll 角采样。
- Fibonacci sphere / icosphere 风格方向采样。
- 固定欧拉角网格作为初始版本,但需要避免只覆盖少量手工角度。
### 6.2 局部精修
对粗搜索评分最好的若干候选执行无导数局部优化,例如:
- Powell method。
- Nelder-Mead simplex。
Navisworks AABB 目标函数不平滑,也不可求导,因此不使用梯度法。局部优化的终止条件应包含:
- 最大迭代次数。
- 角度步长阈值。
- 评分改善阈值。
### 6.3 输出角度
优化器内部可用四元数表示候选姿态,最终需要转换为当前窗口使用的宿主 X/Y/Z 三轴角度修正,并写回:
- `RotationXDegrees`
- `RotationYDegrees`
- `RotationZDegrees`
角度顺序必须遵守项目当前约束X -> Y -> Z对应 `qz * qy * qx`
## 7. 评估方式
第一阶段建议实现为纯几何优化器:
- 输入物体三轴尺寸。
- 输入宿主坐标系下的 `HostPathForward` / `HostSide` / `HostUp`
- 输入候选旋转。
- 输出截面投影宽度、高度、面积。
这样可以用单元测试充分锁定算法,不需要启动 Navisworks。
真实物体的 fragment 代表姿态不作为优化真值。若后续发现数学预测与 Navisworks 实际 AABB 存在明显偏差,再增加“慢速精确模式”:
1. 保存当前动画物体状态。
2. 临时应用候选姿态到路径起点。
3. 读取 Navisworks 更新后的 AABB。
4. 计算截面投影面积。
5. 恢复状态。
慢速模式必须复用 `PathAnimationManager` / `ModelItemTransformHelper` 的现有姿态链路,不能绕过主链路直接写入临时补丁。
## 8. UI 行为
“调整物体”窗口增加一个“自动调整”按钮。
建议行为:
1. 用户打开“调整物体”窗口。
2. 点击“自动调整”。
3. 系统根据当前路径起点方向、当前物体尺寸和宿主坐标系执行优化。
4. 将最优 X/Y/Z 角度写入输入框。
5. 用户可继续修改角度。
6. 点击“确认”后沿用现有 `ObjectStartPlacementRequest.CreateRotationCorrection(...)` 链路。
自动调整失败时,窗口不关闭,并显示明确错误:
- 未选择路径。
- 路径点不足。
- 起点方向退化。
- 未选择动画物体。
- 物体尺寸无效。
- 优化器未找到有效姿态。
## 9. 测试计划
新增纯几何单元测试,建议放在 `UnitTests/CoordinateSystem/`
必须覆盖:
- 长方体通过三轴旋转得到比初始姿态更小的截面面积。
- 三轴优化能找到单轴 yaw 无法达到的更小截面面积。
- `YUp``ZUp``HostUp` 高度语义正确。
- 路径横向宽度和宿主 up 高度共同参与目标函数。
- 面积接近时优先选择投影高度更小的平放姿态。
- 任意旋转允许倒置,不强制保持原 up 方向。
- 优化结果确定性:同一输入多次运行输出一致。
- 退化路径方向会失败而不是回退默认方向。
后续接入 UI 后,补充 ViewModel 或窗口级测试:
- 点击自动调整后会更新 X/Y/Z 输入值。
- 自动调整后确认仍走现有角度修正请求。
- 失败时保留原输入值。
## 10. 实现落点
候选落点:
- `src/Utils/CoordinateSystem/ObjectPassageProjectionOptimizer.cs`
- `UnitTests/CoordinateSystem/ObjectPassageProjectionOptimizerTests.cs`
- `src/UI/WPF/Views/EditRotationWindow.xaml`
- `src/UI/WPF/Views/EditRotationWindow.xaml.cs`
- `src/UI/WPF/ViewModels/AnimationControlViewModel.cs`
如果需要从 ViewModel 提供路径和尺寸上下文,可新增一个小的请求对象,避免窗口直接依赖大型 ViewModel。
## 11. 风险与注意事项
- Euler 角存在多解和万向节锁风险优化内部应优先用四元数Euler 只作为 UI 输出格式。
- AABB 只能近似真实几何,通过性结果应理解为基于包围盒的保守估计。
- 对真实物体fragment 仅可作为现有动画链路的弱参考,不作为自动调整的优化真值。
- 对 Ground / Hoisting / Rail 的路径姿态链路不得引入旧 yaw fallback。
- 若候选姿态会改变落位补偿,必须同步更新通行空间尺寸和起点预览。

View File

@ -2,21 +2,6 @@
## 功能点
### [2026/5/18]
1. [x] BUG修复剖面盒导出后程序偶尔崩溃的问题
2. [x] (优化)拆分剖面盒导出为遍历+导出两步,提高性能
### [2026/4/12]
1. [x] (功能)实现自动集成测试框架
2. [x] (优化)自动路径规划的坐标系适配
### [2026/4/2]
1. [x] (功能)增加按选择项显示剖面盒的功能
2. [x] 优化让地面和吊装路径摆脱fragment依赖
### [2026/3/20]
1. [x] 优化实现Yup模型兼容

View File

@ -1,254 +0,0 @@
# Ground 去 Fragment 依赖实施方案
更新时间2026-04-08
## 1. 这份方案现在只解决什么
只解决一件事:
- `Ground + 真实物体` 主链里,尽量去掉 `fragment` 参考姿态依赖
只允许改动:
- `PathAnimationManager.cs`
- 必要时补少量日志
明确不做:
- 不新建大范围工具链
- 不改 `Hoisting`
- 不改 `Rail`
- 不重写 `ModelItemTransformHelper`
- 不删除 `RealObjectReferencePoseResolver`
- 不做“整项目去 fragment”
这份方案的目标是:**缩小修改范围,先把 Ground 主链收干净。**
---
## 2. 当前已确认的事实
1. `Ground` 的变换更适合走最简单的宿主增量法:
- 宿主旋转增量
- 宿主平移增量
2. `Ground` 这条链不应该再扩散 `local/reference/fragment` 概念。
3. `fragment` 现在的问题,不在于“所有地方都要立刻删”,而在于:
- Ground 主链还会读它
- 导致旋转来源不稳定
4. `Ground` 这条链仍然有旋转目标,但这个旋转目标不再来自 `fragment` 参考姿态解释。
5. `Ground` 后续只需要求:
- 路径在宿主平面里的方向
- 对应的宿主平面旋转量
6. 也就是说,`Ground` 不再求“reference-based pose”而只求
- `hostForward`
- `hostUp`
- `host-planar rotation delta`
---
## 3. 只保留的改造目标
这轮只保留 3 个具体目标:
1. `Ground` 初始化时,不再优先读 fragment 参考姿态
2. `Ground` 平面姿态求解时,不再走 fragment 参考旋转入口,而只求宿主平面旋转量
3. `Ground` 不再允许 fragment planar fallback
只要这 3 点做到,就算这一轮完成。
---
## 4. 当前 Ground 需要处理的入口
### 4.1 初始化入口
当前重点看:
- `SyncTrackedRotationToObjectReference(...)`
要求:
- 当 `PathType == Ground` 且是真实物体时
- 不再去走 `TryCaptureRealObjectReferenceRotation(...)`
- 直接改用当前实际几何姿态,或现有非 fragment 入口
- 这一步的目的不是继续建立另一套“参考姿态定义”,而只是拿到当前增量起点
### 4.2 平面姿态求解入口
当前重点看:
- `TryGetRealObjectReferenceRotation(...)`
- `TryCreateReferenceBasedRealObjectPlanarPoseSolution(...)`
要求:
- `Ground` 不再从这里拿 fragment 参考旋转
- `Ground` 不再继续求 reference-based pose
- `Ground` 只求宿主平面旋转量:
- 路径 `hostForward`
- 宿主 `hostUp`
- 当前对象在宿主坐标系下要追加的平面旋转量
### 4.3 fallback 入口
当前重点看:
- `ShouldAllowFragmentPlanarFallback(PathType pathType)`
要求:
- `Ground` 改成和 `Hoisting` 一样,不再允许 fragment planar fallback
---
## 5. 实施顺序
只按下面顺序做,不扩展:
1. 先改 `Ground` 初始化入口
2. 再改 `Ground` 平面姿态求解入口
3. 再改 `Ground` 尺寸/通行空间入口,避免它们继续偷偷走 `reference-based pose`
4. 最后关掉 `Ground` 的 fragment fallback
每一步都要求:
- 先看日志
- 只改 `Ground`
- 不顺手改别的路径
### 5.1 2026-04-07 夜间分析结论
第一刀已经证明:
- `Ground` 初始化入口可以先切掉 fragment
- 但这还不够
这次日志暴露出的真正问题是:
- `Ground` 虽然不再直接读 fragment
- 但起点姿态求解仍然在走 `TryCreateRealObjectPlanarPoseSolution(...)`
- 也就是仍然在走 `reference-based pose` 入口
所以第二刀必须明确成:
1. `Ground` 不再进入 `TryCreateRealObjectPlanarPoseSolution(...)`
2. `Ground` 起点旋转改成:
- 当前显示旋转
- 当前宿主平面 yaw
- 路径目标宿主平面 yaw
- 基于这三者直接求目标旋转
3. 也就是 `Ground` 只保留:
- 当前显示状态作为增量起点
- 路径方向作为目标方向
- 宿主平面旋转量作为唯一旋转语义
### 5.2 现有链路与目标链路
当前代码里,`Ground + 真实物体` 至少有 3 个入口会接触姿态:
1. 起点
- `MoveObjectToPathStart(...)`
- `TryCreatePlanarPathRotationAtStart(...)`
- `TryCreateRealObjectPlanarRotationFromHostForward(...)`
2. 逐帧
- `ApplyGroundAnimationFrame(...)`
3. 尺寸/通行空间
- `TryCalculateCurrentRealObjectPlanarProjectedExtents(...)`
这一轮的目标链路应该统一成:
1. 初始化
- 当前显示姿态只用来拿“当前增量起点”
- 不再把它包装成 `referenceRotation`
2. 起点/逐帧旋转
- 只求 `hostForward`
- 只求宿主平面旋转量
- 不再进入 `TryCreateRealObjectPlanarPoseSolution(...)`
- 起点应用层不再走 `currentRotation -> targetRotation -> deltaRotation`
- 改成直接施加宿主轴旋转增量,再补平移把 tracked point 拉回目标点
3. 尺寸/通行空间
- 对 `Ground` 直接复用固定业务约定:
- `forward = PositiveX`
- `up = HostUp`
- 不再通过 `reference-based pose` 推导 `ModelAxisConvention`
这 3 条链必须保持同一件事:
- `Ground` 不再解释 reference pose
- `Ground` 只解释“当前显示状态 + 路径方向 + 宿主平面旋转量”
---
## 6. 验证标准
这轮不追求“大而全测试矩阵”,只看 3 条:
1. 起点
- `Ground + 真实物体` 到起点后不再读 fragment 姿态
2. 逐帧
- `Ground` 播放时旋转来源不再依赖 fragment
- `Ground` 只根据路径方向继续求宿主平面旋转量
3. 尺寸/通行空间
- `Ground` 的 projected extents 不再因为 fragment/reference pose 缺失而失败
- `Ground``ModelAxisConvention` 不再来自 `TryCreateRealObjectPlanarPoseSolution(...)`
4. fallback
- `Ground` 关闭 fragment fallback 后,主链要么成功,要么明确报错
- 不允许再偷偷回退
---
## 7. 当前停止线
如果做到下面这句话,就先停:
- **Ground 主链不再依赖 fragment但 Hoisting / Rail / 通用参考姿态系统暂时不动。**
不要在这一轮里再继续追求:
- 抽象统一工具类
- 清理全部 reference/local 命名
- 一次性删光 fragment 代码
- 统一三类路径的所有姿态入口
这些都属于下一轮的事。
---
## 8. 2026-04-08 夜间新增Ground 角度调整的最小实现策略
`Ground + 真实物体` 这条新纯增量链上,角度调整不再通过“完整目标姿态重建”实现,而只允许走最单纯的宿主增量法:
1. `up` 轴修正
- 不单独构造三维姿态
- 直接并入路径目标 `yaw`
- `YUp``YDegrees`
- `ZUp``ZDegrees`
2. 非 `up` 轴修正
- 不与 `yaw` 一次性组合
- 只在起点落位后,围绕业务跟踪点按宿主轴逐次增量应用
- 当前约定:
- `YUp`:应用 `XDegrees`、`ZDegrees`
- `ZUp`:应用 `XDegrees`、`YDegrees`
3. 逐帧播放
- 继续只吃“路径宿主平面角 + up 轴修正”
- 不在每帧重复叠加非 `up` 轴修正
- 非 `up` 轴修正应由起点姿态一次性建立,并在后续 `yaw` 增量中自然保持
### 明早优先验证
1. `Ground + 真实物体` 到起点
- 位置是否继续保持正确
- `X/Y/Z` 角度调整是否真正进入增量旋转链
2. `Ground` 逐帧播放
- 转弯是否仍保持当前已修好的效果
- `up` 轴修正是否能跟随路径持续生效
3. 非 `up` 轴修正
- 是否表现为“起点一次性按宿主轴旋转”
- 播放过程中不应每帧累加放大

View File

@ -1,338 +0,0 @@
# 自动路径规划坐标系重构实施方案
更新时间2026-04-13
状态:实施中
目标:将 `Ground` 自动路径规划整条链路并入当前稳定的“宿主坐标系 -> Canonical Space -> 宿主坐标系”架构,消除旧 `XY` 平面 / `Z` 高度硬编码,不再针对单一 `YUp` 现象打补丁。
---
## 1. 背景与问题定义
当前项目的坐标系架构已经明确:
- 宿主坐标系Host Space
- Navisworks 文档坐标系
- UI 输入输出、鼠标拾取、日志、渲染交互都属于这一层
- 内部坐标系Canonical Space
- 项目内部统一固定为 `ZUp`
- 几何、姿态、法向、路径计算优先在这一层完成
- 业务语义层
- 在 canonical 之上表达地面接触点、吊点、轨道法向、动画跟踪点等业务含义
但自动路径规划链路目前仍大量保留旧实现习惯:
- 默认 `XY` 是水平平面
- 默认 `Z` 是高度轴
- 默认世界 `Z` 可以直接代表“向上”
- 路径起终点修正仍然是“保留 `XY`,替换 `Z`
这说明当前问题不是“某个 `YUp` 细节没处理”,而是:
- 自动路径规划整条链没有接入当前稳定的坐标系架构
因此本方案的目标不是修一个 `YUp` bug而是把自动路径规划整体收编到现有坐标系体系中。
---
## 2. 现象与直接证据
`YUp` 文档中的地面自动路径为例,日志显示:
- 起点:`(-209.94, 14.83, -1.68)`
- 终点:`(-169.20, 14.83, -5.35)`
对当前文档来说,宿主 `up``Y`,所以:
- 地面高度应主要体现在 `Y`
- `Z` 应属于宿主水平面的一部分
但自动路径规划链中仍有如下旧假设:
1. `PathPlanningManager``X/Y` 计算边界与网格尺寸
- `src/Core/PathPlanningManager.cs`
- `GetModelBounds()`
- `CalculateOptimalGridSize(...)`
2. `AutoPathFinder` 把图节点还原为:
- `new Point3D(worldPos.X, worldPos.Y, nodeInfo.Z)`
3. `AutoPathFinder` 的起终点修正逻辑仍是:
- 保留原始 `X/Y`
- 只替换 `Z`
这套实现天然绑定旧 `ZUp` 语义,无法正确支持当前多坐标系架构。
---
## 3. 重构目标
本次重构要达成的不是“YUp 也能跑”,而是以下更高层目标:
1. 自动路径规划输入输出继续保持宿主坐标语义
- 用户点击点、UI 日志、路径显示、数据库路径点仍然按宿主坐标解释
2. 自动路径规划内部计算统一切到 canonical
- 网格范围
- 栅格映射
- A* 路径求解
- 高度层处理
- 曲线化前的路径点
3. Ground 自动路径与通行空间、路径渲染、动画主链共享同一套 up / 高度语义
4. 禁止继续保留旧 `XY/Z` 写死逻辑作为 fallback
---
## 4. 范围界定
### 4.1 本次重构范围
- `PathEditingViewModel`
- 自动路径起点/终点输入边界
- `PathPlanningManager`
- 自动路径入口、边界、网格参数、路径落库前收口
- `GridMapGenerator`
- BIM 到网格的平面/高度语义
- `AutoPathFinder`
- 路径搜索、节点转世界坐标、首尾点修正
- 与自动路径规划直接耦合的测试
### 4.2 暂不纳入本次范围
- 手动路径编辑主链
- 吊装路径自动生成链
- Rail 自动生成链
- 动画姿态链
- 碰撞恢复链
说明:
- 本次先聚焦 `Ground` 自动路径规划
- 但设计必须是通用的宿主/内部坐标边界,不允许做成“只为 `YUp` 单独补丁”
---
## 5. 关键设计原则
### 5.1 边界清晰
- 宿主坐标只在输入输出边界存在
- 内部计算统一使用 canonical
- 不允许在算法中间层直接假设世界 `Z` 是高度
### 5.2 不新增 fallback
不允许:
- canonical 转换失败时回退旧 `XY/Z` 逻辑
- `YUp` 失败时偷偷走旧分支
- 用“如果是 YUp 就特判”掩盖整体架构问题
### 5.3 优先复用现有坐标工具
优先复用:
- `HostCoordinateAdapter`
- `CanonicalTrackedPositionResolver`
- 坐标系统一设计文档中已经稳定的术语和边界
### 5.4 先锁测试,再动实现
本次重构必须先补针对多坐标系的自动路径测试,再改实现。
---
## 6. 目标架构
目标链路应收敛为:
```text
宿主点击点 / 宿主通道边界 / 宿主模型几何
↓ HostCoordinateAdapter
Canonical 起点 / 终点 / 边界 / 网格 / 高度层
↓ 自动路径规划算法(统一在 canonical 内)
Canonical 路径点
↓ HostCoordinateAdapter
宿主路径点 / 渲染 / UI / 数据库存储
```
关键语义:
- canonical 内部固定:
- `XY` 为水平平面
- `Z` 为高度
- 宿主坐标系的 `YUp/ZUp` 差异只由 `HostCoordinateAdapter` 负责吸收
---
## 7. 分阶段实施计划
### Phase 0现状锁定与测试补齐
目标:
- 先把当前正确/错误行为锁定,避免重构中语义漂移
任务:
- [ ] 新增 `Ground` 自动路径在 `ZUp` 文档下的首尾点测试
- [ ] 新增 `Ground` 自动路径在 `YUp` 文档下的首尾点测试
- [ ] 新增“宿主点 -> 自动路径 -> 路径点输出”边界测试
- [ ] 新增“路径点 / 路径线 / 通行空间”首尾地面一致性测试
验收标准:
- 在没有改实现前,测试至少能明确暴露 `YUp` 下当前错误
- `ZUp` 当前稳定行为被锁住,不允许回归
### Phase 1输入边界 canonical 化
目标:
- 自动路径规划入口不再把宿主点直接扔进旧 `XY/Z` 规划逻辑
任务:
- [ ] 梳理 `PathEditingViewModel` 起点/终点点击数据的宿主语义
- [ ] 在自动路径规划入口显式引入 `HostCoordinateAdapter`
- [ ] 将 `AutoPlanPath(...)` 的内部输入切换为 canonical 起点/终点
- [ ] 明确日志里同时打印宿主输入点与 canonical 输入点
验收标准:
- 自动路径规划入口不再直接依赖宿主 `YUp/ZUp`
- 起点终点进入算法前,坐标语义清晰可见
### Phase 2网格与边界 canonical 化
目标:
- 网格边界、障碍物、通道数据全部按 canonical 平面/高度工作
任务:
- [ ] 重构 `GetModelBounds()` / `CalculateChannelsBounds(...)`
- [ ] 重构 `CalculateOptimalGridSize(...)`,不再把 `X/Y` 固定当平面
- [ ] 重构 `GridMapGenerator.GenerateFromBIM(...)` 的输入语义
- [ ] 明确网格二维平面坐标与 canonical 三维坐标的边界
验收标准:
- 网格生成不再依赖宿主世界 `XY`
- `YUp` / `ZUp` 两类文档进入网格阶段后语义完全一致
### Phase 3路径求解与输出 canonical 化
目标:
- `AutoPathFinder` 内部只处理 canonical
- 路径输出统一经适配器返回宿主
任务:
- [ ] 重构图节点到三维点的转换逻辑
- [ ] 删除或重写“保留 XY、替换 Z”的旧首尾点修正
- [ ] 重构路径输出与落库前的宿主还原逻辑
- [ ] 日志里同时打印 canonical 路径首尾点与宿主路径首尾点
验收标准:
- 路径首尾点在 `YUp/ZUp` 下都能正确贴回宿主地面
- 不再出现“终点被压到地面下方”的旧 `ZUp` 语义残留
### Phase 4联动校验与收尾
目标:
- 自动路径规划与可视化、通行空间、动画链的基础语义重新对齐
任务:
- [ ] 检查路径点显示是否仍使用宿主坐标
- [ ] 检查路径线和通行空间首尾点是否与路径点一致
- [ ] 清理旧 `XY/Z` 注释、旧日志、旧辅助方法
- [ ] 更新 `current-engineering-state.md` 中自动路径规划状态说明
验收标准:
- 自动路径规划的起点、终点、路径线、通行空间在多坐标系文档下语义一致
- 旧 `XY/Z` 硬编码实现不再残留为正式执行链
---
## 8. 关键改动点清单
高风险文件:
- `src/UI/WPF/ViewModels/PathEditingViewModel.cs`
- `src/Core/PathPlanningManager.cs`
- `src/PathPlanning/GridMapGenerator.cs`
- `src/PathPlanning/AutoPathFinder.cs`
重点清理的旧逻辑:
- [ ] “默认 `XY` 为地面平面”的边界计算
- [ ] “默认 `Z` 为高度”的路径节点组装
- [ ] “保留 `XY`,替换 `Z`”的起终点修正
- [ ] 任何直接写死世界 `Z` 为 up 的自动路径规划实现
---
## 9. 测试与验证策略
### 9.1 单元/集成测试
至少补齐:
- [ ] `ZUp Ground` 自动路径首尾点测试
- [ ] `YUp Ground` 自动路径首尾点测试
- [ ] 起点/终点保持宿主地面接触语义测试
- [ ] 路径输出后首尾点与通行空间一致性测试
### 9.2 手工验证
在真实 Navisworks 场景中验证:
- [ ] `ZUp` 模型中自动地面路径仍正常显示
- [ ] `YUp` 模型中自动地面路径终点不再掉到地面下
- [ ] 默认显示模式下能看到起点、终点和路径线
- [ ] 打开通行空间后,首尾位置与路径点一致
---
## 10. 风险与注意事项
1. 自动路径规划链路历史较老,旧实现可能分散在多个辅助方法中
- 不能只改主入口必须顺着网格、A*、输出一路清理
2. 数据库存储仍然是宿主坐标语义
- 不能把 canonical 点直接落库
3. 路径渲染和自动路径规划不能各自维护一套“高度轴”解释
- 否则会再次出现“路径点对、通行空间错”或反过来的问题
4. 本次重构不是 `Ground` 的最后一步
- 后续 `Hoisting/Rail` 若要做自动路径,也应直接复用这次收好的边界层
---
## 11. 进度记录
### 2026-04-13
- [x] 确认当前自动路径规划主链仍大量保留旧 `XY/Z` 假设
- [x] 明确本次方向应为“并入现有坐标系架构”,而不是追加 `YUp` 特判
- [x] 建立本实施方案文档
- [x] 引入 `HostPlanarGridHelper`,明确“旧 ICoordinateSystem 水平/高程契约”与 `HostCoordinateAdapter` 的桥接边界
- [x] `YUpCoordinateSystem` / `ZUpCoordinateSystem` 改为复用统一桥接 helper而不是各自散落实现
- [x] 为宿主平面/高程桥接补充纯数学单测,锁定:
- `YUp` 高程走宿主 `Y`
- `YUp` 水平面走宿主 `XZ`
- `ZUp` 仍保持宿主 `XY + Z`
- [x] `AutoPathFinder` 第一轮收口完成:
- 起终点映射改为通过 `gridMap.GetWorldElevation(...)`
- 图节点 / 路径点还原改为通过 `gridMap.CreateWorldPoint(...)`
- 高度层匹配与路径恢复改为通过 `gridMap.GetWorldElevation(...) / SetWorldElevation(...)`
- [ ] Phase 0 剩余:补“自动路径首尾点”更贴近业务的回归测试
- [ ] Phase 2 尚未开始:`GridMapGenerator` 仍存在较多旧 `XY/Z` 硬编码

View File

@ -1,46 +0,0 @@
# Auto Object Rotation Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Add an automatic object rotation option that searches full 3D orientation space and minimizes passage cross-section projection area, preferring lower height when areas are close.
**Architecture:** Add a pure geometry optimizer under `src/Utils/CoordinateSystem` with deterministic candidate search and projection scoring. Wire it into `EditRotationWindow` through a small request/result context so the window stays reusable and writes the resulting host X/Y/Z correction into existing inputs.
**Tech Stack:** .NET Framework 4.8, C# 7.3, MSTest, `System.Numerics.Quaternion`.
---
### Task 1: Geometry Optimizer
**Files:**
- Create: `src/Utils/CoordinateSystem/ObjectPassageProjectionOptimizer.cs`
- Test: `UnitTests/CoordinateSystem/ObjectPassageProjectionOptimizerTests.cs`
- Modify: `TransportPlugin.csproj`
- Modify: `NavisworksTransport.UnitTests.csproj`
- [ ] Write failing tests for cross-section area scoring, height tie-break, deterministic output, and degenerate path directions.
- [ ] Run `./run-unit-tests.bat` and verify the new tests fail because the optimizer does not exist.
- [ ] Implement deterministic full-3D candidate search using quaternion candidates from axis permutations and Euler refinement.
- [ ] Run `./run-unit-tests.bat` and verify the new tests pass.
### Task 2: UI Context And Button
**Files:**
- Modify: `src/UI/WPF/Views/EditRotationWindow.xaml`
- Modify: `src/UI/WPF/Views/EditRotationWindow.xaml.cs`
- Modify: `src/UI/WPF/ViewModels/AnimationControlViewModel.cs`
- [ ] Add `ObjectPassageProjectionOptimizationRequest` construction in the ViewModel using current path start direction, object dimensions in model units, and `HostCoordinateAdapter`.
- [ ] Add an “自动调整” button in the adjustment window.
- [ ] On click, call the optimizer, update X/Y/Z text fields, and keep the dialog open for user review.
- [ ] Show a clear warning without changing values when optimization input is invalid.
### Task 3: Verification
**Files:**
- No new files.
- [ ] Run `./run-unit-tests.bat`.
- [ ] Run `./compile.bat`.
- [ ] Inspect `git diff` for unrelated changes and keep the change scoped.

View File

@ -1,18 +1,12 @@
# 当前工程状态
更新时间2026-04-01
更新时间2026-03-25
## 1. 当前稳定状态
- `Rail` 路径主链路已稳定:
- `ZUp` 模型下,轨上/轨下路径的真实物体与虚拟物体起点、动画、终点贴合正确。
- `YUp` 模型下,终端安装仿真、`Rail` 姿态、真实物体与虚拟物体通行空间、起点与动画主链路已基本跑通。
- `Rail` “调整角度”窗口中的“平移”模式当前已重新收稳:
- 以终点箱体当前原始位姿为真值;
- 记录终点原位相对路径终点参考点的固定残差;
- 把这份残差整体搬运到路径起点;
- 动画全程保持终点原始姿态;
- 动画结束时应精确回到终点箱体原位。
- 地面路径在 `YUp` 模型下:
- 虚拟物体起点姿态、动画姿态、转弯姿态已恢复正常。
- 真实物体地面路径当前已恢复到可用状态:
@ -26,24 +20,16 @@
- 对象级前进轴统一按 `PositiveX` 解释;
- 不再因为路径初始方向更偏 `Z` 就把对象前进轴自动切换成 `PositiveZ`
- 当前已禁止地面/吊装路径偷偷退回旧 `yaw` 链路。
- 吊装路径真实物体的角度调整链路已重新稳定:
- 起点“调整角度”后,预览、生成动画、播放前重置都复用同一份真实起点基姿态;
- 清除并重新选择物体时,旧角度修正必须被清空,不能继承上一次调整状态。
- 碰撞检测/恢复主链路已稳定:
- `ClashDetective` 三维恢复不能再先 `ResetPermanentTransform`
- 碰撞恢复、自动报告、自动截图已重新对齐到动画主链路。
- 虚拟物体资源问题已确认并修复:
- 旧 `unit_cube.nwc` 局部几何中心不在原点,会导致虚拟物体中心偏差。
- 新 `unit_cube.nwc` 已替换为几何中心在原点的版本。
- 旋转适配入口当前稳定分流:
- 虚拟物体继续走 `HostCoordinateAdapter``Legacy` 入口;
- 真实物体走 `Direct` 入口;
- 两者不能再强行共用同一条旋转转换链。
- `Rail` 终端安装仿真创建/编辑区当前已完成第一轮工作流拆分:
- 新建 Rail 与编辑选中 Rail 不再共用同一套按钮命令入口;
- 新建区和编辑区有独立的 `找端面 / 选安装点 / 取起点` 可用条件;
- 两个区域都已有明确的“取消装配”退出入口;
- 上方“路径编辑 -> 结束”在 Rail 装配工作流活跃时,也必须能真正退出 Rail 装配流程。
- 旋转适配入口当前稳定分流:
- 虚拟物体继续走 `HostCoordinateAdapter``Legacy` 入口;
- 真实物体走 `Direct` 入口;
- 两者不能再强行共用同一条旋转转换链。
## 2. 当前坐标系架构
@ -111,19 +97,8 @@
- `AnimatedObjectTrackedPosition` 的唯一语义:
- 当前动画主链路使用的跟踪点位置。
- `Ground + 真实物体` 当前必须明确区分两类“中心”:
- 业务跟踪点应固定为物体**原始包围盒中心**
- 不是旋转后的实时包围盒中心
- 实时包围盒中心的用途仅限于:
- 诊断旋转后漂移
- 计算当帧补偿
- 验证补偿结果
- 不允许把实时包围盒中心直接回写成 `Ground` 路径的业务跟踪点定义。
- 这条规则的直接影响范围包括:
- 起点落位
- 逐帧补偿
- 动画日志
- 碰撞与结果记录语义
- 当前主链路已经切到:
- 几何中心跟踪。
- 如果后续动画跟踪点再变,数据库和碰撞结果语义必须同步更新。
### 4.4 虚拟物体资源
@ -237,19 +212,6 @@
2. 再验证修正后 `forward` 是否仍沿 rail 切向
3. 最后再看 Navisworks 应用层是否正确按三步增量姿态落位
### 4.7.1 Rail 平移模式
- `Rail` 的“平移”不是重新按轨上/轨下求一套新的安装姿态。
- 当前稳定语义:
- 终点箱体当前原始位姿是唯一真值;
- 先计算终点箱体相对“路径终点参考点”的固定残差;
- 再把这份残差搬运到路径起点;
- 动画全程保持这套终点原始姿态和固定残差。
- 当前规则:
- 不能把“平移模式”误实现成重新按 `RailMountMode` 解目标 pose
- 不能锁到“参考姿态”而丢掉终点箱体当前真实几何姿态;
- 终点保位误差如果异常,优先检查“残差是相对终点参考点采样,还是误相对起点参考点采样”。
### 4.8 Rail 真实物体必须复用统一姿态解释层
- `Rail` 不应再单独退回默认 `PositiveX / PositiveY` 轴约定。
@ -281,40 +243,6 @@
- `AnimationControlViewModel.UpdatePassageSpaceVisualization()` 中,真实物体的 `Rail``Hoisting` 必须分开消费
- `Rail` 真实物体不能再复用 `Hoisting` 的法向/分段参数语义
### 4.10 Rail 终端安装仿真工作流
- 当前真实问题已经确认:
- “创建 Rail 路径”区和“Rail 参数”编辑区之前共用同一套装配状态、命令入口和按钮可用条件;
- 导致新建流程会误吃编辑态校验,或在切换路径后残留旧的装配状态。
- 当前第一轮稳定修复:
- 新建 Rail 与编辑 Rail 已拆成独立命令入口;
- “找端面”在新建 Rail 时不再要求先选中一条现有 Rail 路径;
- 新建区的“选安装点”不再受 `IsRailRouteSelected` 编辑态条件限制;
- 安装点拾取完成后,拾取点、安装点、安装参考面、中心线必须保留可视化,不能在收尾时被一并清掉;
- “隐藏辅助线”必须同时清理:
- 参考杆与锚点
- 球心到光轴参考点基准线
- 端面分析可视化
- 安装点、安装中心线、安装参考面
- 新建区和编辑区都必须有明确的“取消装配”;
- “路径编辑 -> 结束”在 Rail 装配流程活跃时,也必须能触发同一条取消/清理链路;
- 切换路径时必须自动清空旧的 Rail 装配上下文,不能把旧状态带到下一条路径。
- 当前仍要记住:
- 这块只是“第一轮工作流拆分”,还不是彻底的数据结构重构;
- 如果后续继续扩 Rail 终端安装仿真,优先方向应是把“创建态上下文”和“编辑态上下文”进一步拆成独立 context而不是继续在 ViewModel 顶层堆共享字段和布尔分支。
### 4.11 吊装路径角度调整
- 当前真实根因已经确认:
- 吊装真实物体在“起点调整角度后生成动画”时,如果基姿态缓存不跟着角度修正一起重建,就会出现:
- 动画里没有沿用调整后的角度;
- 甚至被错误立起或只抬高一点的异常现象。
- 当前稳定规则:
- 吊装路径必须缓存“真实起点基姿态 + 起始 yaw”
- 起点预览、动画生成、播放前重置,都要复用同一份基线;
- 只要角度修正变化,就必须清掉旧缓存并重新 `MoveObjectToPathStart()`
- 清除物体或重新选择物体时,角度修正状态必须归零,不允许继承上一轮对象的角度状态。
## 5. 当前保留的日志策略
- 保留:
@ -329,16 +257,8 @@
- 已降级或删除:
- 大量重复逐帧宿主姿态轴日志
- 虚拟物体 `Transform` 即时读回日志(容易误导)
- `Hoisting` 真实物体起点姿态与平面前进轴的刷屏 `Info` 日志,已降为 `Debug`
## 6. 最近关键提交
- `c3b103c` `Add explicit exit for rail assembly workflow`
- `c974469` `Separate rail creation and edit assembly flows`
- `d4c49fc` `Stabilize hoisting pose adjustment flow`
- `cb56737` `Preserve terminal pose when translating rail objects to path start`
## 7. 当前 Navisworks 变换 API 结论
## 6. 当前 Navisworks 变换 API 结论
- `ModelItem.Transform`
- 只表示原始设计文件变换;
@ -359,13 +279,13 @@
- 清掉的是永久增量层;
- 不会修改原始设计文件变换。
## 8. 当前还值得继续观察的点
## 6. 当前还值得继续观察的点
- 空轨路径里仍有一个旧 warning
- `[空轨] 双轨几何中心线提取失败,退回 OBB 主轴中线`
- 这与当前 `YUp` 地面路径问题无关,但后续可以单独处理。
## 9. 下一步建议
## 7. 下一步建议
- 继续按“先框架、先测试、后接业务”的方式推进。
- 新问题优先:
@ -373,12 +293,8 @@
2. 若日志不足,先补日志
3. 再补针对性的回归测试
4. 最后再改业务代码
- `Rail` 终端安装仿真的下一轮优化优先级:
1. 把创建态/编辑态从 ViewModel 顶层共享字段继续拆成独立 context
2. 为“取消装配 / 切换路径 / 中途退出后重新开始”补单元测试或更轻量的状态回归验证
3. 再考虑是否需要把创建区与编辑区在 UI 上做更明确的视觉分隔
## 10. 当前阶段的工作边界
## 8. 当前阶段的工作边界
- 不要回到“直接补旧 `yaw` 链路”的方式。
- 不要再增加隐藏错误的 fallback。

View File

@ -1,23 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<RibbonControl
xmlns="clr-namespace:Autodesk.Windows;assembly=AdWindows"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:Autodesk.Navisworks.Gui.Roamer.AIRLook;assembly=navisworks.gui.roamer">
<RibbonTab x:Uid="RibbonTab_Transport" Id="ID_TransportTab" Title="物流路径规划" KeyTip="WL">
<RibbonPanel x:Uid="RibbonPanel_TransportMain">
<RibbonPanelSource x:Uid="RibbonPanelSource_TransportMain" Title="物流路径规划" KeyTip="G">
<local:NWRibbonButton
x:Uid="RibbonButton_OpenLogisticsPanel"
Id="OpenLogisticsPanel"
Orientation="Vertical"
Size="Large"
ShowText="True"
Text="物流路径&#xA;规划"
ToolTip="打开物流路径规划面板"
Image="TransportRibbon_16.png"
LargeImage="TransportRibbon_32.png"
KeyTip="P" />
</RibbonPanelSource>
</RibbonPanel>
</RibbonTab>
</RibbonControl>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 614 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -26,19 +26,6 @@ default_path_turn_radius = 2.5
# 圆弧采样步长(米)- 推荐值0.02-0.1
arc_sampling_step = 0.05
# 吊装路径自动视角距离(米)
# 默认为近距离低侧视
hoisting_view_distance_meters = 6.0
# 吊装路径自动视角仰角(度)
hoisting_view_elevation_degrees = 30.0
# Rail 路径自动视角距离(米)
rail_view_distance_meters = 6.0
# Rail 路径自动视角仰角(度)
rail_view_elevation_degrees = 30.0
[visualization]
# 地图边距比例0-1之间
margin_ratio = 0.1

View File

@ -1,7 +0,0 @@
@echo off
setlocal
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0scripts\run-ground-virtual-collision-test.ps1" %*
if errorlevel 1 exit /b 1
exit /b 0

View File

@ -41,7 +41,7 @@ if errorlevel 1 (
exit /b 1
)
%VSTEST_PATH% "bin\x64\Release\NavisworksTransport.UnitTests.dll" /Platform:x64 /TestAdapterPath:%TEST_ADAPTER_PATH% /TestCaseFilter:"TestCategory!=NavisworksIntegration"
%VSTEST_PATH% "bin\x64\Release\NavisworksTransport.UnitTests.dll" /Platform:x64 /TestAdapterPath:%TEST_ADAPTER_PATH%
if errorlevel 1 (
echo Unit tests failed!
exit /b 1

View File

@ -1,213 +0,0 @@
[CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Medium')]
param(
[Parameter(Mandatory = $true)]
[string]$Path,
[switch]$Recurse
)
<#
用法示例
1. 批量修复当前目录下所有 XML 路径文件
powershell -ExecutionPolicy Bypass -File "C:\Users\Tellme\apps\NavisworksTransport-rail-mount-modes\scripts\Fix-RailPreferredNormals.ps1" -Path "C:\Users\Tellme\Documents\NavisworksTransport\模型"
2. 递归修复目录及其子目录中的所有 XML 路径文件
powershell -ExecutionPolicy Bypass -File "C:\Users\Tellme\apps\NavisworksTransport-rail-mount-modes\scripts\Fix-RailPreferredNormals.ps1" -Path "C:\Users\Tellme\Documents\NavisworksTransport\模型" -Recurse
3. 只修复单个 XML 路径文件
powershell -ExecutionPolicy Bypass -File "C:\Users\Tellme\apps\NavisworksTransport-rail-mount-modes\scripts\Fix-RailPreferredNormals.ps1" -Path "C:\Users\Tellme\Documents\NavisworksTransport\模型\副本_人工_0331_133243.xml"
脚本规则
- 只处理 pathType="Rail" railMountMode="OverRail" 的路径
- 只修复 railPreferredNormalY < 0 的路径
- 修复方式为将 railPreferredNormalX / Y / Z 三个分量一起取反
- 原文件会被直接覆盖同时自动保留一个同名 .bak 备份文件
#>
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
function Format-NormalValue {
param(
[Parameter(Mandatory = $true)]
[double]$Value
)
return $Value.ToString("0.###", [System.Globalization.CultureInfo]::InvariantCulture)
}
function Test-ParseableDoubleText {
param(
[Parameter(Mandatory = $true)]
[string]$Text,
[ref]$Value
)
return [double]::TryParse(
$Text,
[System.Globalization.NumberStyles]::Float -bor [System.Globalization.NumberStyles]::AllowLeadingSign,
[System.Globalization.CultureInfo]::InvariantCulture,
$Value)
}
function Update-RouteNode {
param(
[Parameter(Mandatory = $true)]
[System.Xml.XmlElement]$RouteNode,
[Parameter(Mandatory = $true)]
[string]$SourcePath
)
if ($RouteNode.GetAttribute("pathType") -ne "Rail") {
return $false
}
if ($RouteNode.GetAttribute("railMountMode") -ne "OverRail") {
return $false
}
$xText = $RouteNode.GetAttribute("railPreferredNormalX")
$yText = $RouteNode.GetAttribute("railPreferredNormalY")
$zText = $RouteNode.GetAttribute("railPreferredNormalZ")
if ([string]::IsNullOrWhiteSpace($xText) -or
[string]::IsNullOrWhiteSpace($yText) -or
[string]::IsNullOrWhiteSpace($zText)) {
return $false
}
$x = 0.0
$y = 0.0
$z = 0.0
if (-not (Test-ParseableDoubleText -Text $xText -Value ([ref]$x)) -or
-not (Test-ParseableDoubleText -Text $yText -Value ([ref]$y)) -or
-not (Test-ParseableDoubleText -Text $zText -Value ([ref]$z))) {
throw "文件 '$SourcePath' 中存在无法解析的 railPreferredNormal 值。"
}
if ($y -ge 0) {
return $false
}
$RouteNode.SetAttribute("railPreferredNormalX", (Format-NormalValue -Value (-$x)))
$RouteNode.SetAttribute("railPreferredNormalY", (Format-NormalValue -Value (-$y)))
$RouteNode.SetAttribute("railPreferredNormalZ", (Format-NormalValue -Value (-$z)))
return $true
}
function Save-XmlDocument {
param(
[Parameter(Mandatory = $true)]
[xml]$Document,
[Parameter(Mandatory = $true)]
[string]$TargetPath
)
$settings = New-Object System.Xml.XmlWriterSettings
$settings.Indent = $true
$settings.IndentChars = " "
$settings.Encoding = New-Object System.Text.UTF8Encoding($false)
$settings.NewLineChars = "`r`n"
$settings.NewLineHandling = [System.Xml.NewLineHandling]::Replace
$writer = [System.Xml.XmlWriter]::Create($TargetPath, $settings)
try {
$Document.Save($writer)
}
finally {
$writer.Dispose()
}
}
function Update-XmlFile {
param(
[Parameter(Mandatory = $true)]
[string]$FilePath
)
[xml]$xml = Get-Content -LiteralPath $FilePath -Raw
$routeNodes = @($xml.SelectNodes("//*[local-name()='Route']"))
if ($null -eq $routeNodes -or $routeNodes.Count -eq 0) {
return [pscustomobject]@{
FilePath = $FilePath
Modified = $false
RouteCount = 0
}
}
$modifiedRouteCount = 0
foreach ($routeNode in $routeNodes) {
if (Update-RouteNode -RouteNode $routeNode -SourcePath $FilePath) {
$modifiedRouteCount++
}
}
if ($modifiedRouteCount -gt 0) {
$backupPath = "$FilePath.bak"
if ($PSCmdlet.ShouldProcess($FilePath, "修复 railPreferredNormal 并写回 XML")) {
if (-not (Test-Path -LiteralPath $backupPath)) {
Copy-Item -LiteralPath $FilePath -Destination $backupPath
}
Save-XmlDocument -Document $xml -TargetPath $FilePath
}
}
return [pscustomobject]@{
FilePath = $FilePath
Modified = ($modifiedRouteCount -gt 0)
RouteCount = $modifiedRouteCount
}
}
$resolvedPath = Resolve-Path -LiteralPath $Path
$item = Get-Item -LiteralPath $resolvedPath
$files = @()
if ($item.PSIsContainer) {
$childParams = @{
LiteralPath = $item.FullName
File = $true
}
if ($Recurse) {
$childParams["Recurse"] = $true
}
$files = @(Get-ChildItem @childParams | Where-Object { $_.Extension -ieq ".xml" })
}
else {
$files = @($item)
}
if ($files.Count -eq 0) {
Write-Host "未找到可处理的 XML 文件。"
exit 0
}
$results = @()
foreach ($file in $files) {
$results += Update-XmlFile -FilePath $file.FullName
}
$results = @($results)
$modifiedFiles = @($results | Where-Object { $_.Modified })
$modifiedRoutes = 0
if ($modifiedFiles.Count -gt 0) {
$measure = $modifiedFiles | Measure-Object -Property RouteCount -Sum
if ($null -ne $measure -and $null -ne $measure.Sum) {
$modifiedRoutes = [int]$measure.Sum
}
}
Write-Host ("扫描文件: {0}" -f $results.Count)
Write-Host ("修改文件: {0}" -f $modifiedFiles.Count)
Write-Host ("修复路径: {0}" -f $modifiedRoutes)
foreach ($result in $modifiedFiles) {
Write-Host ("已修复: {0} (路径数={1})" -f $result.FilePath, $result.RouteCount)
}

View File

@ -1,154 +0,0 @@
[CmdletBinding()]
param(
[string]$ModelPath,
[int]$ServiceStartupTimeoutSeconds = 90,
[int]$TestTimeoutSeconds = 240
)
$ErrorActionPreference = 'Stop'
$scriptDirectory = Split-Path -Parent $MyInvocation.MyCommand.Path
$repoRoot = Split-Path -Parent $scriptDirectory
$startScriptPath = Join-Path $scriptDirectory 'start-navisworks.ps1'
$defaultsFilePath = Join-Path $scriptDirectory 'test-automation.defaults.json'
$testApiBaseUrl = 'http://127.0.0.1:18777'
function Resolve-DefaultModelPath {
if (-not (Test-Path $defaultsFilePath)) {
return $null
}
$defaults = Get-Content $defaultsFilePath -Raw | ConvertFrom-Json
$defaultModelPath = [string]$defaults.defaultModelPath
if ([string]::IsNullOrWhiteSpace($defaultModelPath)) {
return $null
}
return [System.IO.Path]::GetFullPath($defaultModelPath)
}
function Wait-TestApiReady {
param(
[string]$BaseUrl,
[int]$TimeoutSeconds
)
$deadline = (Get-Date).AddSeconds($TimeoutSeconds)
$lastError = $null
while ((Get-Date) -lt $deadline) {
try {
$pingResponse = Invoke-RestMethod -Uri ($BaseUrl + '/api/test/ping') -TimeoutSec 10
if ($pingResponse.ok) {
return
}
}
catch {
$lastError = $_.Exception.Message
}
Start-Sleep -Seconds 1
}
if ([string]::IsNullOrWhiteSpace($lastError)) {
throw "测试 HTTP 服务未在 $TimeoutSeconds 秒内就绪。"
}
throw "测试 HTTP 服务未在 $TimeoutSeconds 秒内就绪。最后错误: $lastError"
}
function Wait-GroundRouteReady {
param(
[string]$BaseUrl,
[int]$TimeoutSeconds
)
$deadline = (Get-Date).AddSeconds($TimeoutSeconds)
$lastError = $null
while ((Get-Date) -lt $deadline) {
try {
$routesResponse = Invoke-RestMethod -Uri ($BaseUrl + '/api/test/routes') -TimeoutSec 10
if ($routesResponse.ok -and $routesResponse.data -and $routesResponse.data.totalRouteCount -gt 0) {
$groundRoute = $routesResponse.data.suggestedAutoTestRoutes.ground
if (-not [string]::IsNullOrWhiteSpace([string]$groundRoute)) {
return [string]$groundRoute
}
}
}
catch {
$lastError = $_.Exception.Message
}
Start-Sleep -Seconds 1
}
if ([string]::IsNullOrWhiteSpace($lastError)) {
throw "默认地面测试路径未在 $TimeoutSeconds 秒内就绪。"
}
throw "默认地面测试路径未在 $TimeoutSeconds 秒内就绪。最后错误: $lastError"
}
function Save-JsonResult {
param(
[object]$Payload
)
$logRoot = Join-Path $env:ProgramData 'Autodesk\Navisworks Manage 2026\plugins\TransportPlugin\logs\test-automation'
New-Item -ItemType Directory -Path $logRoot -Force | Out-Null
$fileName = 'ground-virtual-collision-test-' + (Get-Date -Format 'yyyyMMdd-HHmmss') + '.json'
$outputPath = Join-Path $logRoot $fileName
$json = $Payload | ConvertTo-Json -Depth 16
[System.IO.File]::WriteAllText($outputPath, $json, [System.Text.UTF8Encoding]::new($false))
return $outputPath
}
if ([string]::IsNullOrWhiteSpace($ModelPath)) {
$ModelPath = Resolve-DefaultModelPath
}
if (-not [string]::IsNullOrWhiteSpace($ModelPath)) {
$ModelPath = [System.IO.Path]::GetFullPath($ModelPath)
if (-not (Test-Path $ModelPath)) {
throw "模型文件不存在: $ModelPath"
}
}
Write-Host '启动 Navisworks...'
if ([string]::IsNullOrWhiteSpace($ModelPath)) {
& $startScriptPath
}
else {
& $startScriptPath $ModelPath
}
Write-Host '等待测试 HTTP 服务就绪...'
Wait-TestApiReady -BaseUrl $testApiBaseUrl -TimeoutSeconds $ServiceStartupTimeoutSeconds
Write-Host '等待默认地面测试路径就绪...'
$groundRouteName = Wait-GroundRouteReady -BaseUrl $testApiBaseUrl -TimeoutSeconds $ServiceStartupTimeoutSeconds
Write-Host ('切换到默认地面测试路径: {0}' -f $groundRouteName)
$selectRouteResponse = Invoke-RestMethod -Method Post -Uri ($testApiBaseUrl + '/api/test/select-default-route?pathType=Ground') -TimeoutSec 15
if (-not $selectRouteResponse.ok) {
throw '切换默认地面测试路径失败。'
}
Write-Host '执行地面虚拟物体碰撞测试...'
$requestUrl = $testApiBaseUrl + "/api/test/run-ground-collision-test?useVirtualObject=true&timeoutSeconds=$TestTimeoutSeconds"
$testResult = Invoke-RestMethod -Method Post -Uri $requestUrl -TimeoutSec ($TestTimeoutSeconds + 30)
$outputFilePath = Save-JsonResult -Payload $testResult
Write-Host ('结果已保存: {0}' -f $outputFilePath)
if ($testResult.ok -and $testResult.data) {
Write-Host ('路径: {0}' -f $testResult.data.route.name)
Write-Host ('动画状态: {0}' -f $testResult.data.animation.currentState)
Write-Host ('检测记录ID: {0}' -f $testResult.data.animation.detectionRecordId)
if ($testResult.data.report) {
Write-Host ('碰撞数: {0}' -f $testResult.data.report.totalCollisions)
Write-Host ('截图数: {0}' -f $testResult.data.report.screenshotCount)
}
}

View File

@ -1,167 +0,0 @@
[CmdletBinding()]
param(
[Parameter(Position = 0)]
[string]$ModelPath
)
$ErrorActionPreference = 'Stop'
$scriptDirectory = Split-Path -Parent $MyInvocation.MyCommand.Path
$repoRoot = Split-Path -Parent $scriptDirectory
$defaultsFilePath = Join-Path $scriptDirectory 'test-automation.defaults.json'
function Resolve-RoamerPath {
$candidates = New-Object System.Collections.Generic.List[string]
$appPathKeys = @(
'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\Roamer.exe',
'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\App Paths\Roamer.exe'
)
foreach ($key in $appPathKeys) {
try {
$value = (Get-ItemProperty -Path $key -ErrorAction Stop).'(default)'
if (-not [string]::IsNullOrWhiteSpace($value)) {
$candidates.Add($value)
}
}
catch {
}
}
$wellKnownPaths = @(
(Join-Path ${env:ProgramFiles} 'Autodesk\Navisworks Manage 2026\Roamer.exe'),
(Join-Path ${env:ProgramFiles(x86)} 'Autodesk\Navisworks Manage 2026\Roamer.exe'),
(Join-Path ${env:ProgramFiles} 'Autodesk\Navisworks Simulate 2026\Roamer.exe'),
(Join-Path ${env:ProgramFiles(x86)} 'Autodesk\Navisworks Simulate 2026\Roamer.exe')
) | Where-Object { $_ }
foreach ($path in $wellKnownPaths) {
$candidates.Add($path)
}
foreach ($candidate in $candidates | Select-Object -Unique) {
if (-not [string]::IsNullOrWhiteSpace($candidate) -and (Test-Path $candidate)) {
return [System.IO.Path]::GetFullPath($candidate)
}
}
throw '找不到 Roamer.exe。请确认 Navisworks Manage 2026 已安装。'
}
function Resolve-DefaultModelPath {
if (-not (Test-Path $defaultsFilePath)) {
return $null
}
try {
$defaults = Get-Content $defaultsFilePath -Raw | ConvertFrom-Json
$defaultModelPath = [string]$defaults.defaultModelPath
if ([string]::IsNullOrWhiteSpace($defaultModelPath)) {
return $null
}
$resolvedPath = [System.IO.Path]::GetFullPath($defaultModelPath)
if (-not (Test-Path $resolvedPath)) {
throw "默认模型文件不存在: $resolvedPath"
}
return $resolvedPath
}
catch {
throw "读取默认测试配置失败: $($_.Exception.Message)"
}
}
function Dismiss-RecoveryPromptIfPresent {
param(
[int]$TimeoutSeconds = 20
)
Add-Type -AssemblyName System.Windows.Forms | Out-Null
$shell = New-Object -ComObject WScript.Shell
$windowTitles = @(
'是否从自动保存重新载入?',
'是否从自动保存重新载入?'
)
$deadline = (Get-Date).AddSeconds($TimeoutSeconds)
while ((Get-Date) -lt $deadline) {
foreach ($windowTitle in $windowTitles) {
try {
if ($shell.AppActivate($windowTitle)) {
Start-Sleep -Milliseconds 300
[System.Windows.Forms.SendKeys]::SendWait('%N')
Write-Host '已自动选择“否”以跳过自动保存恢复窗口。'
return
}
}
catch {
}
}
Start-Sleep -Milliseconds 500
}
}
if ([string]::IsNullOrWhiteSpace($ModelPath)) {
$ModelPath = Resolve-DefaultModelPath
}
if (-not [string]::IsNullOrWhiteSpace($ModelPath)) {
$ModelPath = [System.IO.Path]::GetFullPath($ModelPath)
if (-not (Test-Path $ModelPath)) {
throw "模型文件不存在: $ModelPath"
}
}
$roamerPath = Resolve-RoamerPath
$existingProcess = Get-Process Roamer -ErrorAction SilentlyContinue | Select-Object -First 1
$env:TRANSPORTPLUGIN_TEST_AUTOMATION = '1'
if ($existingProcess) {
Write-Host ("Navisworks 已在运行PID={0}" -f $existingProcess.Id)
if (-not [string]::IsNullOrWhiteSpace($ModelPath)) {
Start-Process -FilePath $roamerPath -ArgumentList @($ModelPath) | Out-Null
Write-Host ("已请求 Navisworks 打开模型: {0}" -f $ModelPath)
}
Dismiss-RecoveryPromptIfPresent
Write-Host '提示: 当前测试 HTTP 服务在插件 DockPane OnLoaded 中启动,首次测试前请确保插件面板已打开。'
exit 0
}
$arguments = @()
if (-not [string]::IsNullOrWhiteSpace($ModelPath)) {
$arguments += $ModelPath
}
$startProcessParams = @{
FilePath = $roamerPath
PassThru = $true
}
if ($arguments.Count -gt 0) {
$startProcessParams.ArgumentList = $arguments
}
$process = Start-Process @startProcessParams
Write-Host ("Navisworks 启动中PID={0}" -f $process.Id)
$deadline = (Get-Date).AddSeconds(30)
do {
Start-Sleep -Milliseconds 500
$running = Get-Process -Id $process.Id -ErrorAction SilentlyContinue
} while (-not $running -and (Get-Date) -lt $deadline)
if (-not $running) {
throw 'Navisworks 启动超时30 秒内未检测到 Roamer.exe 进程。'
}
Dismiss-RecoveryPromptIfPresent
Write-Host ("Navisworks 已启动: {0}" -f $roamerPath)
if (-not [string]::IsNullOrWhiteSpace($ModelPath)) {
Write-Host ("已打开模型参数: {0}" -f $ModelPath)
}
Write-Host '提示: 当前测试 HTTP 服务在插件 DockPane OnLoaded 中启动,首次测试前请确保插件面板已打开。'

View File

@ -1,4 +0,0 @@
{
"defaultModelPath": "C:\\Users\\Tellme\\Documents\\NavisworksTransport\\模型\\Floor2_mobile_yup.nwd",
"defaultRouteNames": []
}

View File

@ -564,7 +564,7 @@ namespace NavisworksTransport.Commands
if (restoreViaAnimationManager && pam != null && pam.ControlsAnimatedObject(animatedObject))
{
pam.RestoreAnimatedObjectState(animatedObject);
LogManager.Debug(string.Format("已通过动画主链路恢复物体状态: {0}", animatedObject.DisplayName));
LogManager.Info(string.Format("已通过动画主链路恢复物体状态: {0}", animatedObject.DisplayName));
}
else
{
@ -602,22 +602,20 @@ namespace NavisworksTransport.Commands
ShowReport(result);
}
if (!_parameters.ShowDialog)
// 🔥 自动导出HTML报告
try
{
try
string htmlPath = Utils.CollisionReportHtmlGenerator.AutoSaveHtmlReport(result);
if (!string.IsNullOrEmpty(htmlPath))
{
string htmlPath = Utils.CollisionReportHtmlGenerator.AutoSaveHtmlReport(result);
if (!string.IsNullOrEmpty(htmlPath))
{
LogManager.Info($"HTML报告已自动导出: {htmlPath}");
}
}
catch (Exception ex)
{
LogManager.Error($"自动导出HTML报告失败: {ex.Message}");
// HTML导出失败不影响报告生成
LogManager.Info($"HTML报告已自动导出: {htmlPath}");
}
}
catch (Exception ex)
{
LogManager.Error($"自动导出HTML报告失败: {ex.Message}");
// HTML导出失败不影响报告生成
}
}
catch (OperationCanceledException)
{

View File

@ -1,79 +0,0 @@
using System;
using Autodesk.Navisworks.Api.Plugins;
using NavisworksTransport.Utils;
namespace NavisworksTransport.Commands
{
[Plugin("TransportRibbonHandler", "NavisworksTransport",
DisplayName = "物流路径规划工具栏",
ToolTip = "物流路径规划工具栏命令")]
[RibbonLayout("TransportRibbon.xaml")]
[RibbonTab("ID_TransportTab", DisplayName = "物流路径规划")]
[Command("OpenLogisticsPanel",
DisplayName = "物流路径规划",
ToolTip = "打开物流路径规划面板",
LoadForCanExecute = true)]
public class TransportRibbonHandler : CommandHandlerPlugin
{
private const string MainDockPanePluginKey = "NavisworksTransport.Main.Tian";
private const string OpenLogisticsPanelCommandId = "OpenLogisticsPanel";
private const string TransportRibbonTabId = "ID_TransportTab";
public override int ExecuteCommand(string commandId, params string[] parameters)
{
try
{
if (!string.Equals(commandId, OpenLogisticsPanelCommandId, StringComparison.OrdinalIgnoreCase))
{
LogManager.Warning($"[Ribbon] 未知命令: {commandId}");
return 1;
}
var pluginRecord = Autodesk.Navisworks.Api.Application.Plugins.FindPlugin(MainDockPanePluginKey) as DockPanePluginRecord;
if (pluginRecord == null)
{
throw new InvalidOperationException($"未找到物流路径规划面板插件: {MainDockPanePluginKey}");
}
var dockPane = pluginRecord.LoadPlugin() as DockPanePlugin;
if (dockPane == null)
{
throw new InvalidOperationException("物流路径规划面板加载失败");
}
dockPane.Visible = true;
dockPane.ActivatePane();
LogManager.Info("[Ribbon] 已通过正式工具栏打开物流路径规划面板");
return 0;
}
catch (Exception ex)
{
LogManager.Error($"[Ribbon] 执行命令失败: {ex.Message}", ex);
return 1;
}
}
public override CommandState CanExecuteCommand(string commandId)
{
if (string.Equals(commandId, OpenLogisticsPanelCommandId, StringComparison.OrdinalIgnoreCase))
{
return new CommandState
{
IsEnabled = true,
IsVisible = true
};
}
return new CommandState
{
IsEnabled = false,
IsVisible = false
};
}
public override bool CanExecuteRibbonTab(string ribbonTabId)
{
return string.Equals(ribbonTabId, TransportRibbonTabId, StringComparison.OrdinalIgnoreCase);
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -804,7 +804,7 @@ namespace NavisworksTransport
if (candidate.AnimatedObjectHasTrackedRotation && targetRotation != null)
{
LogManager.Info(
$"[ClashDetective验证] 已跟踪姿态候选恢复: " +
$"[ClashDetective验证] 三维候选恢复: " +
$"对象={testAnimatedObject.DisplayName}, " +
$"目标跟踪点=({targetPosition.X:F3},{targetPosition.Y:F3},{targetPosition.Z:F3})");
@ -821,15 +821,55 @@ namespace NavisworksTransport
else
{
throw new InvalidOperationException(
$"ClashDetective已跟踪姿态候选恢复失败:对象 {testAnimatedObject.DisplayName} 不受当前动画管理器控制。");
$"ClashDetective三维候选恢复失败:对象 {testAnimatedObject.DisplayName} 不受当前动画管理器控制。");
}
}
else
{
throw new InvalidOperationException(
$"ClashDetective候选恢复缺少完整姿态对象 {testAnimatedObject.DisplayName}" +
$"目标跟踪点=({targetPosition.X:F3},{targetPosition.Y:F3},{targetPosition.Z:F3})" +
$"TrackedYaw={targetYaw:F6}rad。");
// 旧二维候选验证仍沿用“回到 CAD 原始位置再应用增量”的成熟语义。
doc.Models.ResetPermanentTransform(modelItems);
var originalBounds = testAnimatedObject.BoundingBox();
var originalPos = new Point3D(
(originalBounds.Min.X + originalBounds.Max.X) / 2,
(originalBounds.Min.Y + originalBounds.Max.Y) / 2,
(originalBounds.Min.Z + originalBounds.Max.Z) / 2
);
var originalYaw = ModelItemTransformHelper.GetYawFromTransform(testAnimatedObject.Transform);
var deltaPos = new Vector3D(
targetPosition.X - originalPos.X,
targetPosition.Y - originalPos.Y,
targetPosition.Z - originalPos.Z
);
double deltaYaw = targetYaw - originalYaw;
Transform3D transform;
if (Math.Abs(deltaYaw) > 0.001)
{
double cos = Math.Cos(deltaYaw);
double sin = Math.Sin(deltaYaw);
double rotatedX = originalPos.X * cos - originalPos.Y * sin;
double rotatedY = originalPos.X * sin + originalPos.Y * cos;
var compensatedTranslation = new Vector3D(
targetPosition.X - rotatedX,
targetPosition.Y - rotatedY,
deltaPos.Z
);
var identity = Transform3D.CreateTranslation(new Vector3D(0, 0, 0));
var components = identity.Factor();
components.Rotation = new Rotation3D(new UnitVector3D(0, 0, 1), deltaYaw);
components.Translation = compensatedTranslation;
transform = components.Combine();
}
else
{
transform = Transform3D.CreateTranslation(deltaPos);
}
doc.Models.OverridePermanentTransform(modelItems, transform, false);
}
var tempTestName = $"临时验证_{confirmedCount + 1}_{i}_{DateTime.Now:HHmmss_fff}";

View File

@ -394,10 +394,6 @@ namespace NavisworksTransport.Core.Config
config.PathEditing.SafetyMarginMeters = GetDoubleValueWithDefault(pathEdit, "safety_margin_meters", 0.1, missingItems);
config.PathEditing.DefaultPathTurnRadiusMeters = GetDoubleValueWithDefault(pathEdit, "default_path_turn_radius", 2.5, missingItems);
config.PathEditing.ArcSamplingStepMeters = GetDoubleValueWithDefault(pathEdit, "arc_sampling_step", 0.05, missingItems);
config.PathEditing.HoistingViewDistanceMeters = GetDoubleValueWithDefault(pathEdit, "hoisting_view_distance_meters", 6.0, missingItems);
config.PathEditing.HoistingViewElevationDegrees = GetDoubleValueWithDefault(pathEdit, "hoisting_view_elevation_degrees", 30.0, missingItems);
config.PathEditing.RailViewDistanceMeters = GetDoubleValueWithDefault(pathEdit, "rail_view_distance_meters", 6.0, missingItems);
config.PathEditing.RailViewElevationDegrees = GetDoubleValueWithDefault(pathEdit, "rail_view_elevation_degrees", 30.0, missingItems);
}
}

View File

@ -119,26 +119,6 @@ namespace NavisworksTransport.Core.Config
/// </summary>
public double ArcSamplingStepMeters { get; set; }
/// <summary>
/// 吊装路径自动视角距离(米)
/// </summary>
public double HoistingViewDistanceMeters { get; set; }
/// <summary>
/// 吊装路径自动视角仰角(度)
/// </summary>
public double HoistingViewElevationDegrees { get; set; }
/// <summary>
/// Rail 路径自动视角距离(米)
/// </summary>
public double RailViewDistanceMeters { get; set; }
/// <summary>
/// Rail 路径自动视角仰角(度)
/// </summary>
public double RailViewElevationDegrees { get; set; }
// === 模型单位接口(用于内部计算) ===
/// <summary>
@ -180,16 +160,6 @@ namespace NavisworksTransport.Core.Config
/// 圆弧采样步长(模型单位)
/// </summary>
public double ArcSamplingStep => ArcSamplingStepMeters * Utils.UnitsConverter.GetMetersToUnitsConversionFactor();
/// <summary>
/// 吊装路径自动视角距离(模型单位)
/// </summary>
public double HoistingViewDistance => HoistingViewDistanceMeters * Utils.UnitsConverter.GetMetersToUnitsConversionFactor();
/// <summary>
/// Rail 路径自动视角距离(模型单位)
/// </summary>
public double RailViewDistance => RailViewDistanceMeters * Utils.UnitsConverter.GetMetersToUnitsConversionFactor();
}
/// <summary>

View File

@ -7,7 +7,6 @@ using System.Windows.Forms.Integration;
using NavisworksTransport.Core;
using NavisworksTransport.Core.Animation;
using NavisworksTransport.Core.Config;
using NavisworksTransport.Core.Services;
using NavisworksTransport.Utils;
using NavisworksTransport.Utils.CoordinateSystem;
using NavisApplication = Autodesk.Navisworks.Api.Application;
@ -382,9 +381,6 @@ namespace NavisworksTransport
// 订阅文档事件
SubscribeToDocumentEvents();
// 启动本地测试自动化 HTTP 控制面
TestAutomationHttpService.Instance.Start();
// 检查是否已有活动文档且包含模型
var activeDoc = NavisApplication.ActiveDocument;
@ -416,9 +412,6 @@ namespace NavisworksTransport
// 清理管理器
CleanupManagers();
// 停止本地测试自动化 HTTP 控制面
TestAutomationHttpService.Instance.Stop();
base.OnUnloading();
}
@ -581,8 +574,43 @@ namespace NavisworksTransport
{
LogManager.Info($"[文档管理] *** 状态分析: 从无到有(可能是打开文档)***");
// 文档加载完成,仅连接已存在的数据库,不为未使用插件的文档自动创建 .db
TryConnectPathDatabase(activeDoc);
// 文档加载完成初始化PathDatabase
if (!string.IsNullOrEmpty(activeDoc?.FileName))
{
try
{
var pathManager = PathPlanningManager.GetActivePathManager();
if (pathManager != null)
{
pathManager.DatabaseInitialize();
}
}
catch (Exception ex)
{
// 数据库初始化失败是严重错误,需要明确提示用户
var errorMsg = $"数据库初始化失败:{ex.Message}\n\n";
errorMsg += "可能的原因:\n";
errorMsg += "1. 文件夹权限不足\n";
errorMsg += "2. 磁盘空间不足\n";
errorMsg += "3. 数据库文件被锁定\n";
errorMsg += "4. SQLite 组件损坏\n\n";
errorMsg += "详细信息请查看日志文件。";
LogManager.Error($"[文档管理] 数据库初始化失败: {ex.Message}", ex);
LogManager.Error($"[文档管理] 文档路径: {activeDoc.FileName}");
LogManager.Error($"[文档管理] 异常类型: {ex.GetType().Name}");
LogManager.Error($"[文档管理] 堆栈信息: {ex.StackTrace}");
System.Windows.MessageBox.Show(
errorMsg,
"数据库初始化失败",
System.Windows.MessageBoxButton.OK,
System.Windows.MessageBoxImage.Error
);
// 不重新抛出,避免插件崩溃,但批处理等功能将不可用
}
}
}
else if (_hasModels && !currentHasModels)
{
@ -682,9 +710,6 @@ namespace NavisworksTransport
// 初始化坐标系(文档已加载,可以正确检测)
InitializeCoordinateSystemForDocument();
// 连接文档对应的路径数据库
TryConnectPathDatabase(activeDoc);
// 通知DocumentStateManager文档已就绪
DocumentStateManager.Instance.OnDocumentReady();
@ -910,20 +935,6 @@ namespace NavisworksTransport
LogManager.Error($"[文档管理] 取消订阅文档事件失败: {ex.Message}");
}
}
/// <summary>
/// 连接活动文档对应的路径数据库(仅连接已存在的 .db不新建
/// </summary>
private static void TryConnectPathDatabase(Document activeDoc)
{
if (activeDoc == null || string.IsNullOrEmpty(activeDoc.FileName))
{
return;
}
var pathManager = PathPlanningManager.GetActivePathManager();
pathManager?.DatabaseInitialize(createIfMissing: false);
}
}
}
}

View File

@ -61,6 +61,10 @@ namespace NavisworksTransport
ExecuteNonQuery("PRAGMA journal_mode=WAL");
ExecuteNonQuery("PRAGMA foreign_keys=ON");
// 创建数据库表结构
CreateTables();
EnsurePathRoutesTableExtended();
if (isNewDatabase)
{
LogManager.Info($"创建新数据库: {_dbPath}");
@ -69,18 +73,6 @@ namespace NavisworksTransport
{
LogManager.Info($"打开现有数据库: {_dbPath}");
}
try
{
CreateTables();
EnsurePathRoutesTableExtended();
}
catch (Exception ex)
{
LogManager.Error($"数据库初始化失败: {ex.Message}", ex);
try { _connection?.Close(); } catch { }
throw;
}
}
/// <summary>
@ -88,7 +80,7 @@ namespace NavisworksTransport
/// </summary>
private void CreateTables()
{
// 路径基本信息表
// 1. 路径基本信息表
// 注意TotalLength 字段已从数据库中删除
// 路径长度由 PathRoute.TotalLength 计算属性实时计算:
// - 地面路径:从 Edges.PhysicalLength 累加(支持圆弧)
@ -118,7 +110,7 @@ namespace NavisworksTransport
)
");
// 碰撞报告表替换原有的CollisionResults表
// 2. 碰撞报告表替换原有的CollisionResults表
ExecuteNonQuery(@"
CREATE TABLE IF NOT EXISTS CollisionReports (
Id INTEGER PRIMARY KEY AUTOINCREMENT,
@ -134,7 +126,7 @@ namespace NavisworksTransport
)
");
// 分析结果表扩展版包含5维度评分
// 3. 分析结果表扩展版包含5维度评分
ExecuteNonQuery(@"
CREATE TABLE IF NOT EXISTS AnalysisResults (
RouteId TEXT PRIMARY KEY,
@ -154,7 +146,7 @@ namespace NavisworksTransport
)
");
// 路径点表(新增 CustomTurnRadius
// 4. 路径点表(新增 CustomTurnRadius
ExecuteNonQuery(@"
CREATE TABLE IF NOT EXISTS PathPoints (
Id TEXT PRIMARY KEY,
@ -170,7 +162,7 @@ namespace NavisworksTransport
)
");
// 路径边表新增SequenceNumber字段
// 5. 路径边表新增SequenceNumber字段
ExecuteNonQuery(@"
CREATE TABLE IF NOT EXISTS PathEdges (
Id TEXT PRIMARY KEY,
@ -197,10 +189,8 @@ namespace NavisworksTransport
)
");
// 碰撞检测记录表
// 6. 碰撞检测记录表
// 包含检测配置和检测结果(原 ClashDetectiveResults 表已合并)
// 注意:以下表的外键依赖此表 — ClashDetectiveCollisionObjects, CollisionReportScreenshots,
// ClashDetectiveExcludedObjects, CollisionDetectionExcludedObjects, CollisionDetectionManualTargets
ExecuteNonQuery(@"
CREATE TABLE IF NOT EXISTS CollisionDetectionRecords (
Id INTEGER PRIMARY KEY AUTOINCREMENT,
@ -236,7 +226,7 @@ namespace NavisworksTransport
ExecuteNonQuery("CREATE INDEX IF NOT EXISTS idx_detection_route ON CollisionDetectionRecords(RouteId)");
ExecuteNonQuery("CREATE INDEX IF NOT EXISTS idx_detection_time ON CollisionDetectionRecords(CreatedTime DESC)");
// ClashDetective碰撞对象表
// 7. ClashDetective碰撞对象表
// 注意:外键关联到 CollisionDetectionRecords 表
// 扩展字段:记录碰撞时运动物体的位置和朝向,用于还原碰撞场景
ExecuteNonQuery(@"
@ -262,7 +252,7 @@ namespace NavisworksTransport
)
");
// 碰撞报告截图表(支持多张截图)
// 8. 碰撞报告截图表(支持多张截图)
// 注意:外键关联到 CollisionDetectionRecords 表
ExecuteNonQuery(@"
CREATE TABLE IF NOT EXISTS CollisionReportScreenshots (
@ -279,7 +269,7 @@ namespace NavisworksTransport
)
");
// 时标配置表
// 10. 时标配置表
ExecuteNonQuery(@"
CREATE TABLE IF NOT EXISTS TimeTagProfiles (
Id TEXT PRIMARY KEY,
@ -292,7 +282,7 @@ namespace NavisworksTransport
)
");
// 时标事件表
// 11. 时标事件表
ExecuteNonQuery(@"
CREATE TABLE IF NOT EXISTS TimeTagEvents (
Id TEXT PRIMARY KEY,
@ -311,6 +301,11 @@ namespace NavisworksTransport
)
");
// 12. 设置数据库版本SQLite内置user_version
// 版本号格式:主版本*10000 + 次版本*100 + 修订号
// 例如2.1.6 = 20106
ExecuteNonQuery("PRAGMA user_version = 20106"); // v2.1.6 - 删除无效的 RailReferenceToAnchorOffset 字段
// 创建索引
ExecuteNonQuery("CREATE INDEX IF NOT EXISTS idx_reports_route ON CollisionReports(RouteId)");
ExecuteNonQuery("CREATE INDEX IF NOT EXISTS idx_pathpoints_route ON PathPoints(RouteId)");
@ -319,12 +314,12 @@ namespace NavisworksTransport
ExecuteNonQuery("CREATE INDEX IF NOT EXISTS idx_timetag_events_profile ON TimeTagEvents(ProfileId)");
ExecuteNonQuery("CREATE INDEX IF NOT EXISTS idx_detection_test_name ON CollisionDetectionRecords(TestName)");
ExecuteNonQuery("CREATE INDEX IF NOT EXISTS idx_detection_test_time ON CollisionDetectionRecords(TestTime DESC)");
SafeCreateIndex("CREATE INDEX IF NOT EXISTS idx_clash_objects_detection ON ClashDetectiveCollisionObjects(DetectionRecordId)", "ClashDetectiveCollisionObjects", "DetectionRecordId");
SafeCreateIndex("CREATE INDEX IF NOT EXISTS idx_clash_objects_path ON ClashDetectiveCollisionObjects(PathId)", "ClashDetectiveCollisionObjects", "PathId");
SafeCreateIndex("CREATE INDEX IF NOT EXISTS idx_screenshots_detection ON CollisionReportScreenshots(DetectionRecordId)", "CollisionReportScreenshots", "DetectionRecordId");
SafeCreateIndex("CREATE INDEX IF NOT EXISTS idx_screenshots_sort ON CollisionReportScreenshots(DetectionRecordId, SortOrder)", "CollisionReportScreenshots", "DetectionRecordId", "SortOrder");
ExecuteNonQuery("CREATE INDEX IF NOT EXISTS idx_clash_objects_detection ON ClashDetectiveCollisionObjects(DetectionRecordId)");
ExecuteNonQuery("CREATE INDEX IF NOT EXISTS idx_clash_objects_path ON ClashDetectiveCollisionObjects(PathId)");
ExecuteNonQuery("CREATE INDEX IF NOT EXISTS idx_screenshots_detection ON CollisionReportScreenshots(DetectionRecordId)");
ExecuteNonQuery("CREATE INDEX IF NOT EXISTS idx_screenshots_sort ON CollisionReportScreenshots(DetectionRecordId, SortOrder)");
// 批处理队列表简化版只维护FIFO队列
// 9. 批处理队列表简化版只维护FIFO队列
ExecuteNonQuery(@"
CREATE TABLE IF NOT EXISTS BatchQueueItems (
Id INTEGER PRIMARY KEY AUTOINCREMENT,
@ -370,7 +365,7 @@ namespace NavisworksTransport
ExecuteNonQuery("CREATE INDEX IF NOT EXISTS idx_model_ref_type ON ModelItemReferences(ReferenceType)");
ExecuteNonQuery("CREATE INDEX IF NOT EXISTS idx_model_ref_path ON ModelItemReferences(PathId)");
// 排除对象表(用于存储用户排除的碰撞检测对象)
// 10. 排除对象表(用于存储用户排除的碰撞检测对象)
ExecuteNonQuery(@"
CREATE TABLE IF NOT EXISTS ExcludedObjects (
Id INTEGER PRIMARY KEY AUTOINCREMENT,
@ -389,7 +384,7 @@ namespace NavisworksTransport
ExecuteNonQuery("CREATE INDEX IF NOT EXISTS idx_excluded_path ON ExcludedObjects(PathId)");
ExecuteNonQuery("CREATE INDEX IF NOT EXISTS idx_excluded_global ON ExcludedObjects(IsGlobal)");
// 碰撞报告排除对象关联表
// 11. 碰撞报告排除对象关联表
ExecuteNonQuery(@"
CREATE TABLE IF NOT EXISTS CollisionReportExcludedObjects (
Id INTEGER PRIMARY KEY AUTOINCREMENT,
@ -402,7 +397,7 @@ namespace NavisworksTransport
ExecuteNonQuery("CREATE INDEX IF NOT EXISTS idx_report_excluded_report ON CollisionReportExcludedObjects(ReportId)");
ExecuteNonQuery("CREATE INDEX IF NOT EXISTS idx_report_excluded_object ON CollisionReportExcludedObjects(ExcludedObjectId)");
// 碰撞检测记录排除对象关联表(记录每次检测时排除的对象)
// 12. 碰撞检测记录排除对象关联表(记录每次检测时排除的对象)
ExecuteNonQuery(@"
CREATE TABLE IF NOT EXISTS ClashDetectiveExcludedObjects (
Id INTEGER PRIMARY KEY AUTOINCREMENT,
@ -412,13 +407,22 @@ namespace NavisworksTransport
FOREIGN KEY(ExcludedObjectId) REFERENCES ExcludedObjects(Id) ON DELETE CASCADE
)
");
SafeCreateIndex("CREATE INDEX IF NOT EXISTS idx_clash_excluded_detection ON ClashDetectiveExcludedObjects(DetectionRecordId)", "ClashDetectiveExcludedObjects", "DetectionRecordId");
ExecuteNonQuery("CREATE INDEX IF NOT EXISTS idx_clash_excluded_detection ON ClashDetectiveExcludedObjects(DetectionRecordId)");
ExecuteNonQuery("CREATE INDEX IF NOT EXISTS idx_clash_excluded_object ON ClashDetectiveExcludedObjects(ExcludedObjectId)");
// 为 BatchQueueItems 补充 DetectionRecordId 列(兼容旧数据库)
EnsureColumnExists("BatchQueueItems", "DetectionRecordId", "ALTER TABLE BatchQueueItems ADD COLUMN DetectionRecordId INTEGER");
// 13. 碰撞检测记录表(每次点击"生成动画"创建一条记录,保存当时的完整配置)
// 先添加新列到 BatchQueueItems 表
try
{
ExecuteNonQuery("ALTER TABLE BatchQueueItems ADD COLUMN DetectionRecordId INTEGER");
LogManager.Info("[数据库] 已添加 DetectionRecordId 列到 BatchQueueItems 表");
}
catch
{
// 列已存在,忽略错误
}
// 碰撞检测记录排除对象关联表(记录每次检测时的排除列表)
// 14. 碰撞检测记录排除对象关联表(记录每次检测时的排除列表)
ExecuteNonQuery(@"
CREATE TABLE IF NOT EXISTS CollisionDetectionExcludedObjects (
Id INTEGER PRIMARY KEY AUTOINCREMENT,
@ -431,9 +435,9 @@ namespace NavisworksTransport
FOREIGN KEY(DetectionRecordId) REFERENCES CollisionDetectionRecords(Id) ON DELETE CASCADE
)
");
SafeCreateIndex("CREATE INDEX IF NOT EXISTS idx_detection_excluded_record ON CollisionDetectionExcludedObjects(DetectionRecordId)", "CollisionDetectionExcludedObjects", "DetectionRecordId");
ExecuteNonQuery("CREATE INDEX IF NOT EXISTS idx_detection_excluded_record ON CollisionDetectionExcludedObjects(DetectionRecordId)");
// 碰撞检测记录手工目标关联表(记录每次检测时的手工碰撞目标)
// 15. 碰撞检测记录手工目标关联表(记录每次检测时的手工碰撞目标)
ExecuteNonQuery(@"
CREATE TABLE IF NOT EXISTS CollisionDetectionManualTargets (
Id INTEGER PRIMARY KEY AUTOINCREMENT,
@ -445,67 +449,7 @@ namespace NavisworksTransport
FOREIGN KEY(DetectionRecordId) REFERENCES CollisionDetectionRecords(Id) ON DELETE CASCADE
)
");
SafeCreateIndex("CREATE INDEX IF NOT EXISTS idx_detection_manual_record ON CollisionDetectionManualTargets(DetectionRecordId)", "CollisionDetectionManualTargets", "DetectionRecordId");
}
/// <summary>
/// 安全创建索引:先检查列是否存在,若旧数据库缺列则跳过并记录警告
/// </summary>
private void SafeCreateIndex(string indexSql, string tableName, params string[] columnNames)
{
try
{
var existingColumns = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
using (var cmd = new SQLiteCommand($"PRAGMA table_info({tableName})", _connection))
using (var reader = cmd.ExecuteReader())
{
while (reader.Read())
{
existingColumns.Add(reader["name"].ToString());
}
}
foreach (var col in columnNames)
{
if (!existingColumns.Contains(col))
{
LogManager.Warning($"[数据库] 表 {tableName} 缺少列 {col},跳过索引创建");
return;
}
}
ExecuteNonQuery(indexSql);
}
catch (Exception ex)
{
LogManager.Warning($"[数据库] 创建索引失败: {ex.Message}");
}
}
/// <summary>
/// 按需添加列:检查表是否已有指定列,没有则执行 ALTER TABLE
/// </summary>
private void EnsureColumnExists(string tableName, string columnName, string alterSql)
{
try
{
using (var cmd = new SQLiteCommand($"PRAGMA table_info({tableName})", _connection))
using (var reader = cmd.ExecuteReader())
{
while (reader.Read())
{
if (string.Equals(reader["name"].ToString(), columnName, StringComparison.OrdinalIgnoreCase))
return; // 列已存在
}
}
ExecuteNonQuery(alterSql);
LogManager.Info($"[数据库] 已添加 {columnName} 列到 {tableName} 表");
}
catch (Exception ex)
{
LogManager.Warning($"[数据库] 添加列 {tableName}.{columnName} 失败: {ex.Message}");
}
ExecuteNonQuery("CREATE INDEX IF NOT EXISTS idx_detection_manual_record ON CollisionDetectionManualTargets(DetectionRecordId)");
}
/// <summary>

View File

@ -49,7 +49,7 @@ namespace NavisworksTransport
if (currentTool != Tool.CustomToolPlugin)
{
LogManager.Info($"[InputMonitor] 用户按空格键,当前工具为{currentTool}强制恢复ToolPlugin焦点");
bool restored = pathManager.RestoreToolPluginFocusForCurrentContext();
bool restored = pathManager.ForceReinitializeToolPlugin(subscribeToEvents: false);
if (restored)
{
return true;

View File

@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Autodesk.Navisworks.Api;
@ -32,7 +31,6 @@ namespace NavisworksTransport
// 路径分析相关
private PathDatabase _pathDatabase;
private PathAnalysisService _analysisService;
private string _databaseDocumentPath;
private List<ModelItem> _walkableAreas;
private List<PathRoute> _routes;
private PathRoute _currentRoute;
@ -169,69 +167,40 @@ namespace NavisworksTransport
LogManager.Info($"PathPlanningManager初始化完成ManagerId: {_managerId}");
// 注意:数据库采用按需初始化。
// 文档加载时只尝试连接已存在的数据库,不再为未使用插件的文档自动创建 .db 文件。
// 注意:数据库初始化延迟到文档加载完成后
// 在 MainPlugin.OnModelsCollectionChanged 中会调用 DatabaseInitialize()
}
/// <summary>
/// 路径分析数据库初始化
/// 此方法应在文档加载完成后调用
/// </summary>
public void DatabaseInitialize(bool createIfMissing = true)
public void DatabaseInitialize()
{
try
{
var documentPath = Application.ActiveDocument?.FileName;
if (string.IsNullOrEmpty(documentPath))
if (!string.IsNullOrEmpty(documentPath))
{
_pathDatabase = new PathDatabase(documentPath);
_analysisService = new PathAnalysisService(_pathDatabase);
LogManager.Info($"路径分析数据库初始化成功,文档路径: {documentPath}");
// 从数据库加载历史路径到内存
LoadHistoricalRoutesFromDatabase();
}
else
{
// 文档路径为空时,静默跳过(这种情况不应该发生,因为调用者应该确保文档已加载)
LogManager.Debug("文档路径为空,跳过数据库初始化");
return;
}
if (_pathDatabase != null &&
string.Equals(_databaseDocumentPath, documentPath, StringComparison.OrdinalIgnoreCase))
{
return;
}
string dbPath = Path.ChangeExtension(documentPath, ".db");
if (!createIfMissing && !File.Exists(dbPath))
{
LogManager.Info($"文档对应数据库不存在,跳过自动连接: {dbPath}");
return;
}
if (_pathDatabase != null)
{
try
{
_pathDatabase.Dispose();
}
catch (Exception disposeEx)
{
LogManager.Warning($"释放旧数据库连接时出现警告: {disposeEx.Message}");
}
_pathDatabase = null;
_analysisService = null;
_databaseDocumentPath = null;
}
_pathDatabase = new PathDatabase(documentPath);
_analysisService = new PathAnalysisService(_pathDatabase);
_databaseDocumentPath = documentPath;
LogManager.Info($"路径分析数据库初始化成功,文档路径: {documentPath}");
// 从数据库加载历史路径到内存
LoadHistoricalRoutesFromDatabase();
}
catch (Exception ex)
{
LogManager.Error($"初始化路径分析数据库失败: {ex.Message}", ex);
LogManager.Error($"数据库路径: {Application.ActiveDocument?.FileName}");
LogManager.Error($"异常类型: {ex.GetType().Name}");
// 不重新抛出:外层 OnModelsCollectionChanged 已有独立日志;
// _pathDatabase 保持 null后续操作自行判断。
throw; // 重新抛出异常,让调用者知道初始化失败
}
}
@ -1073,12 +1042,7 @@ namespace NavisworksTransport
// 1. 获取模型边界
var bounds = GetModelBounds() ?? throw new Exception("无法获取模型边界,请确保模型已加载");
var hostAdapter = new HostCoordinateAdapter(CoordinateSystemManager.Instance.ResolvedType);
var hostBoundsMin = new System.Numerics.Vector3((float)bounds.Min.X, (float)bounds.Min.Y, (float)bounds.Min.Z);
var hostBoundsMax = new System.Numerics.Vector3((float)bounds.Max.X, (float)bounds.Max.Y, (float)bounds.Max.Z);
var (boundsMinH1, boundsMaxH1, boundsMinH2, boundsMaxH2) =
HostPlanarGridHelper.GetHorizontalRange3(hostBoundsMin, hostBoundsMax, hostAdapter);
LogManager.Info($"模型边界: H1[{boundsMinH1:F2}, {boundsMaxH1:F2}], H2[{boundsMinH2:F2}, {boundsMaxH2:F2}]");
LogManager.Info($"模型边界: Min({bounds.Min.X:F2}, {bounds.Min.Y:F2}), Max({bounds.Max.X:F2}, {bounds.Max.Y:F2})");
// 智能选择网格大小
if (gridSize <= 0)
@ -1102,7 +1066,7 @@ namespace NavisworksTransport
// 获取当前文档
var document = Application.ActiveDocument;
LogManager.Info($"网格生成参数 - 边界: H1 {boundsMinH1:F2}->{boundsMaxH1:F2}, H2 {boundsMinH2:F2}->{boundsMaxH2:F2}");
LogManager.Info($"网格生成参数 - 边界: {bounds.Min.X:F2},{bounds.Min.Y:F2} -> {bounds.Max.X:F2},{bounds.Max.Y:F2}");
LogManager.Info($"网格生成参数 - 网格大小: {gridSize}m, 物体半径: {objectRadius}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})");
@ -1358,12 +1322,6 @@ namespace NavisworksTransport
if (route != null)
{
LogManager.Info($"路径 '{route.Name}' 没有关联的GridMap");
if (IsAnyGridVisualizationEnabled)
{
LogManager.Info("检测到网格可视化已启用尝试按路径保存的自动规划参数重建GridMap");
RefreshGridVisualization();
}
}
}
@ -1757,8 +1715,17 @@ namespace NavisworksTransport
var lastPoint = CurrentRoute.Points.Last();
LogManager.Info($"[吊装路径] 最后一个路径点: {lastPoint.Name}, 位置: ({lastPoint.Position.X:F2}, {lastPoint.Position.Y:F2}, {lastPoint.Position.Z:F2})");
var descendPoint = ReuseLastHoistingPointAsDescendPoint(CurrentRoute.Points);
LogManager.Info($"[吊装路径] 已将最后一个空中点复用为下降点(终点正上方): ({descendPoint.Position.X:F2}, {descendPoint.Position.Y:F2}, {descendPoint.Position.Z:F2})");
// 计算下一个索引
int nextIndex = CurrentRoute.Points.Count;
// 创建下降点:在落地点正上方,保持当前空中高程(沿宿主 up
var descendPoint = new PathPoint(
lastPoint.Position,
"下降点",
PathPointType.WayPoint);
descendPoint.Direction = HoistingPointDirection.Vertical;
descendPoint.Index = nextIndex++;
LogManager.Info($"[吊装路径] 已生成下降点(在终点正上方): ({descendPoint.Position.X:F2}, {descendPoint.Position.Y:F2}, {descendPoint.Position.Z:F2})");
Point3D finalGroundPoint = _hoistingGroundIntermediatePoints.Count > 0
? _hoistingGroundIntermediatePoints[_hoistingGroundIntermediatePoints.Count - 1]
@ -1770,12 +1737,15 @@ namespace NavisworksTransport
"落地点",
PathPointType.EndPoint);
endPoint.Direction = HoistingPointDirection.Vertical;
endPoint.Index = CurrentRoute.Points.Count;
endPoint.Index = nextIndex++;
LogManager.Info($"[吊装路径] 已创建落地点(终点): ({endPoint.Position.X:F2}, {endPoint.Position.Y:F2}, {endPoint.Position.Z:F2})");
// 添加下降点
CurrentRoute.Points.Add(descendPoint);
// 添加落地点(终点)
CurrentRoute.Points.Add(endPoint);
LogManager.Info($"[吊装路径] 已复用最后一个空中点作为下降点,并添加落地点,当前共 {CurrentRoute.Points.Count} 个路径点");
LogManager.Info($"[吊装路径] 已添加下降点和落地点,当前共 {CurrentRoute.Points.Count} 个路径点");
// 注意:不需要再次调用 OrthogonalizePath因为添加的只是垂直线段
// 垂直线段不会形成斜线,所以不需要优化
@ -4506,11 +4476,6 @@ namespace NavisworksTransport
{
DeactivateToolPlugin();
}
_pathDatabase?.Dispose();
_pathDatabase = null;
_analysisService = null;
_databaseDocumentPath = null;
}
catch (Exception ex)
{
@ -4597,33 +4562,6 @@ namespace NavisworksTransport
}
}
/// <summary>
/// 根据当前业务上下文恢复 ToolPlugin 焦点。
/// 普通手动建/编辑路径应恢复 PathPlanningManager 主链;
/// 自动路径、装配拾取、多层吊装等外部自管拾取模式则保持外部事件处理。
/// </summary>
public bool RestoreToolPluginFocusForCurrentContext()
{
try
{
bool shouldSubscribeToManagerEvents = !IsInAutoPathMode;
string contextLabel = shouldSubscribeToManagerEvents
? "普通手动取点"
: "外部自管拾取";
LogManager.Info(
$"[工具插件恢复] 按当前上下文恢复焦点: 模式={contextLabel}, " +
$"PathEditState={PathEditState}, 订阅PathPlanningManager事件={shouldSubscribeToManagerEvents}");
return ForceReinitializeToolPlugin(shouldSubscribeToManagerEvents);
}
catch (Exception ex)
{
LogManager.Error($"[工具插件恢复] 按当前上下文恢复焦点失败: {ex.Message}", ex);
return false;
}
}
#endregion
#region
@ -4729,10 +4667,14 @@ namespace NavisworksTransport
{
try
{
return CalculateOptimalGridSizeFromBounds(
bounds.Min.X, bounds.Min.Y, bounds.Min.Z,
bounds.Max.X, bounds.Max.Y, bounds.Max.Z,
CoordinateSystemManager.Instance.ResolvedType);
var width = bounds.Max.X - bounds.Min.X;
var height = bounds.Max.Y - bounds.Min.Y;
var maxDimension = Math.Max(width, height);
// 基于模型大小智能选择网格大小
if (maxDimension > 1000) return 5.0; // 大型模型5米
if (maxDimension > 500) return 2.0; // 中型模型2米
return 0.5; // 小型模型0.5米
}
catch
{
@ -4740,32 +4682,6 @@ namespace NavisworksTransport
}
}
/// <summary>
/// 根据宿主包围盒和当前宿主坐标系语义,计算自动路径规划的最优网格大小。
/// 关键约束:
/// - 仅修正“水平平面”的解释,不改变历史阈值与业务策略。
/// - ZUp: 使用 Host XY 平面跨度。
/// - YUp: 使用 Host XZ 平面跨度。
/// </summary>
private static double CalculateOptimalGridSizeFromBounds(
double minX, double minY, double minZ,
double maxX, double maxY, double maxZ,
CoordinateSystemType coordinateSystemType)
{
var adapter = new HostCoordinateAdapter(coordinateSystemType);
var hostMin = new System.Numerics.Vector3((float)minX, (float)minY, (float)minZ);
var hostMax = new System.Numerics.Vector3((float)maxX, (float)maxY, (float)maxZ);
var (min1, max1, min2, max2) = HostPlanarGridHelper.GetHorizontalRange3(hostMin, hostMax, adapter);
var width = max1 - min1;
var height = max2 - min2;
var maxDimension = Math.Max(width, height);
if (maxDimension > 1000) return 5.0;
if (maxDimension > 500) return 2.0;
return 0.5;
}
/// <summary>
/// 获取通道模型项用于高度计算
/// </summary>
@ -5146,124 +5062,6 @@ namespace NavisworksTransport
get { return _showWalkableGrid || _showObstacleGrid || _showUnknownGrid || _showDoorGrid; }
}
/// <summary>
/// 按自动路径保存的规划参数重建GridMap用于数据库重载后恢复网格可视化和诊断。
/// </summary>
public bool TryRestoreGridMapForRoute(PathRoute route, out GridMap gridMap, out string message)
{
gridMap = null;
message = null;
try
{
if (route == null)
{
message = "路径为空无法重建GridMap";
LogManager.Warning($"[网格可视化] {message}");
return false;
}
if (route.AssociatedGridMap != null)
{
gridMap = route.AssociatedGridMap;
message = $"路径 '{route.Name}' 已有关联GridMap";
return true;
}
List<PathPoint> orderedPoints = route.Points?.OrderBy(point => point.Index).ToList() ?? new List<PathPoint>();
if (orderedPoints.Count < 2)
{
message = $"路径 '{route.Name}' 点数不足无法重建GridMap";
LogManager.Warning($"[网格可视化] {message}");
return false;
}
if (route.GridSize <= 0)
{
message = $"路径 '{route.Name}' 缺少有效网格大小无法重建GridMap";
LogManager.Warning($"[网格可视化] {message}");
return false;
}
double objectRadius = Math.Max(route.MaxObjectLength, route.MaxObjectWidth) / 2.0;
if (objectRadius <= 0)
{
message = $"路径 '{route.Name}' 缺少有效物体长宽无法重建GridMap";
LogManager.Warning($"[网格可视化] {message}");
return false;
}
if (route.SafetyMargin < 0)
{
message = $"路径 '{route.Name}' 安全间隙为负数无法重建GridMap: {route.SafetyMargin}";
LogManager.Warning($"[网格可视化] {message}");
return false;
}
if (route.MaxObjectHeight <= 0)
{
message = $"路径 '{route.Name}' 缺少有效物体高度无法重建GridMap";
LogManager.Warning($"[网格可视化] {message}");
return false;
}
BoundingBox3D bounds = GetModelBounds();
if (bounds == null)
{
message = "无法获取模型边界无法重建GridMap";
LogManager.Warning($"[网格可视化] {message}");
return false;
}
PathPoint startPoint = orderedPoints.First();
PathPoint endPoint = orderedPoints.Last();
LogManager.Info(
$"[网格可视化] 开始按路径参数重建GridMap: 路径={route.Name}, 网格={route.GridSize}m, " +
$"物体半径={objectRadius}m, 安全间隙={route.SafetyMargin}m, 高度={route.MaxObjectHeight}m");
var gridMapGenerator = new GridMapGenerator();
gridMap = gridMapGenerator.GenerateFromBIM(
bounds,
route.GridSize,
objectRadius,
route.SafetyMargin,
startPoint.Position,
endPoint.Position,
route.MaxObjectHeight);
if (gridMap == null)
{
message = $"路径 '{route.Name}' 的GridMap重建失败";
LogManager.Warning($"[网格可视化] {message}");
return false;
}
route.AssociatedGridMap = gridMap;
if (_currentRoute == route || (_currentRoute != null && _currentRoute.Id == route.Id))
{
_currentGridMap = gridMap;
_currentObjectHeight = route.MaxObjectHeight;
}
var renderPlugin = PathPointRenderPlugin.Instance;
if (renderPlugin != null)
{
renderPlugin.SetGridSize(UnitsConverter.ConvertToMeters(gridMap.CellSize));
}
message = $"路径 '{route.Name}' 的GridMap已重建: {gridMap.GetStatistics()}";
LogManager.Info($"[网格可视化] {message}");
return true;
}
catch (Exception ex)
{
message = $"重建GridMap失败: {ex.Message}";
LogManager.Error($"[网格可视化] {message}", ex);
gridMap = null;
return false;
}
}
/// <summary>
/// 刷新网格可视化(根据当前设置重新显示)
/// </summary>
@ -5274,33 +5072,19 @@ namespace NavisworksTransport
// 清除当前网格可视化
ClearGridVisualization();
if (!IsAnyGridVisualizationEnabled)
{
LogManager.Info("[网格可视化] 网格可视化设置已关闭,跳过渲染");
return;
}
if (_currentGridMap == null && _currentRoute != null)
{
if (!TryRestoreGridMapForRoute(_currentRoute, out GridMap restoredGridMap, out string restoreMessage))
{
LogManager.Info($"[网格可视化] {restoreMessage}");
}
else
{
_currentGridMap = restoredGridMap;
}
}
// 如果存在缓存的网格地图且有网格可视化设置启用,重新进行可视化
if (_currentGridMap != null)
if (_currentGridMap != null && IsAnyGridVisualizationEnabled)
{
LogManager.Info("[网格可视化] 使用缓存的网格地图重新渲染");
VisualizeGridCells(_currentGridMap, _currentObjectHeight);
}
else if (_currentGridMap == null)
{
LogManager.Info("[网格可视化] 无缓存网格地图,需要重新进行路径规划以查看网格");
}
else
{
LogManager.Info("[网格可视化] 无缓存网格地图,且无法通过当前路径参数重建");
LogManager.Info("[网格可视化] 网格可视化设置已关闭,跳过渲染");
}
}
catch (Exception ex)
@ -5364,10 +5148,11 @@ namespace NavisworksTransport
{
var cell = gridMap.Cells[x, y];
// 当前 GridMap 的索引语义是采样节点GridToWorld2D(x,y)
// 必须直接作为可视化平面位置,不能再按左下角单元取 (x+1,y+1) 平均。
// 计算网格单元格的XY中心位置Z坐标在多层逻辑中单独处理
var gridPos = new NavisworksTransport.PathPlanning.GridPoint2D(x, y);
var gridNode2D = gridMap.GridToWorld2D(gridPos);
var gridCorner = gridMap.GridToWorld3D(gridPos);
double centerX = gridCorner.X + gridMap.CellSize / 2;
double centerY = gridCorner.Y + gridMap.CellSize / 2;
// 所有网格都基于高度层,统一处理
if (cell.HeightLayers != null && cell.HeightLayers.Count > 0)
@ -5415,8 +5200,8 @@ namespace NavisworksTransport
// 创建该层的可视化标记
if (cellType.HasValue)
{
var gridNode = gridMap.SetWorldElevation(gridNode2D, layerZ);
var marker = new GridMarker(gridNode, cellType.Value, x, y, layerZ);
var gridCenter = new Point3D(centerX, centerY, layerZ);
var marker = new GridMarker(gridCenter, cellType.Value, x, y, layerZ);
// 根据类型添加到对应列表
switch (cellType.Value)
@ -5443,9 +5228,8 @@ namespace NavisworksTransport
// Unknown网格没有高度层用于调试
if (_showUnknownGrid)
{
double unknownElevation = gridMap.GetWorldElevation(gridNode2D);
var gridNode = gridMap.SetWorldElevation(gridNode2D, unknownElevation);
var marker = new GridMarker(gridNode, GridVisualizationType.Unknown, x, y, unknownElevation);
var gridCenter = new Point3D(centerX, centerY, gridCorner.Z);
var marker = new GridMarker(gridCenter, GridVisualizationType.Unknown, x, y, gridCorner.Z);
gridVis.Unknown.Add(marker);
unknownCells++;
totalVisualized++;
@ -5631,17 +5415,7 @@ namespace NavisworksTransport
{
try
{
if (route == null)
{
return;
}
if (_pathDatabase == null)
{
DatabaseInitialize(createIfMissing: true);
}
if (_pathDatabase != null)
if (_pathDatabase != null && route != null)
{
_pathDatabase.SavePathRoute(route);
LogManager.Info($"路径已保存到数据库: {route.Name}");
@ -5682,26 +5456,6 @@ namespace NavisworksTransport
return _activePathManager;
}
internal static PathPoint ReuseLastHoistingPointAsDescendPoint(IList<PathPoint> points)
{
if (points == null)
{
throw new ArgumentNullException(nameof(points));
}
if (points.Count == 0)
{
throw new ArgumentException("吊装路径至少需要一个空中点才能复用为下降点。", nameof(points));
}
PathPoint descendPoint = points[points.Count - 1];
descendPoint.Name = "下降点";
descendPoint.Type = PathPointType.WayPoint;
descendPoint.Direction = HoistingPointDirection.Vertical;
descendPoint.Index = points.Count - 1;
return descendPoint;
}
#endregion
}
}

View File

@ -566,7 +566,7 @@ namespace NavisworksTransport
[Serializable]
public class PathRoute
{
public static bool IsTopReferenceFaceForMountMode(RailMountMode railMountMode)
public static bool IsTopPayloadAnchorForMountMode(RailMountMode railMountMode)
{
return railMountMode == RailMountMode.UnderRail;
}
@ -706,27 +706,27 @@ namespace NavisworksTransport
public NavisworksTransport.PathPlanning.GridMap AssociatedGridMap { get; set; }
/// <summary>
/// 网格大小(
/// 网格大小(模型单位
/// </summary>
public double GridSize { get; set; }
/// <summary>
/// 最大物体长度( - 路径适用的物体长度限制
/// 最大物体长度(模型单位 - 路径适用的物体长度限制
/// </summary>
public double MaxObjectLength { get; set; }
/// <summary>
/// 最大物体宽度( - 路径适用的物体宽度限制
/// 最大物体宽度(模型单位 - 路径适用的物体宽度限制
/// </summary>
public double MaxObjectWidth { get; set; }
/// <summary>
/// 最大物体高度( - 路径适用的物体高度限制
/// 最大物体高度(模型单位 - 路径适用的物体高度限制
/// </summary>
public double MaxObjectHeight { get; set; }
/// <summary>
/// 安全间隙(
/// 安全间隙(模型单位
/// </summary>
public double SafetyMargin { get; set; }

View File

@ -1594,7 +1594,7 @@ namespace NavisworksTransport
if (visualization.PathRoute.PathType == NavisworksTransport.PathType.Ground)
{
// 地面路径:路径点是地面位置,通行空间底面在地面,中心需要向上偏移半个高度
verticalOffset = ResolveGroundPathObjectSpaceVerticalOffset(height);
verticalOffset = height / 2.0;
}
else if (visualization.PathRoute.PathType == NavisworksTransport.PathType.Rail)
{
@ -2144,37 +2144,6 @@ namespace NavisworksTransport
return new Vector3D(vector.X / length, vector.Y / length, vector.Z / length);
}
private static Point3D ApplyVectorOffset(Point3D point, Vector3D direction, double distance)
{
var normalized = Normalize(direction);
return new Point3D(
point.X + normalized.X * distance,
point.Y + normalized.Y * distance,
point.Z + normalized.Z * distance);
}
private static (Vector3D axis1, Vector3D axis2) GetHostPlanarAxes(Vector3D hostUp)
{
var normalizedUp = Normalize(hostUp);
var reference = Math.Abs(normalizedUp.X) > 0.9
? new Vector3D(0, 1, 0)
: new Vector3D(1, 0, 0);
double projection = reference.X * normalizedUp.X + reference.Y * normalizedUp.Y + reference.Z * normalizedUp.Z;
var axis1 = Normalize(new Vector3D(
reference.X - projection * normalizedUp.X,
reference.Y - projection * normalizedUp.Y,
reference.Z - projection * normalizedUp.Z));
if (axis1.X == 0 && axis1.Y == 0 && axis1.Z == 0)
{
axis1 = new Vector3D(0, 0, 1);
}
var axis2 = Normalize(Cross(normalizedUp, axis1));
return (axis1, axis2);
}
/// <summary>
/// 添加切点标记
/// </summary>
@ -2345,12 +2314,7 @@ namespace NavisworksTransport
/// <param name="passageAlongPath">沿路径方向的通行空间尺寸(模型单位)</param>
/// <param name="passageNormalToPathVertical">垂直段的高度(物体长度 + 2×安全间隙模型单位</param>
/// <param name="passageNormalToPathHorizontal">水平段的高度(物体高度 + 2×安全间隙模型单位</param>
public void SetPassageSpaceParameters(
double passageAcrossPath,
double passageNormalToPath,
double passageAlongPath,
double passageNormalToPathVertical,
double passageNormalToPathHorizontal)
public void SetPassageSpaceParameters(double passageAcrossPath, double passageNormalToPath, double passageAlongPath, double passageNormalToPathVertical, double passageNormalToPathHorizontal)
{
// 直接存储模型单位(调用方已转换)
_passageAlongPath = passageAlongPath;
@ -2365,11 +2329,6 @@ namespace NavisworksTransport
RefreshNormalPaths();
}
internal static double ResolveGroundPathObjectSpaceVerticalOffset(double objectSpaceHeight)
{
return objectSpaceHeight / 2.0;
}
#region
/// <summary>
@ -3117,18 +3076,15 @@ namespace NavisworksTransport
// 计算正方形的边长(直径)
double sideLength = pointMarker.Radius * 2;
// 网格矩形必须落在宿主水平平面内,不能写死世界 XY。
var hostUp = GetHostUpVector();
var (planarAxis1, planarAxis2) = GetHostPlanarAxes(hostUp);
var xVector = new Vector3D(planarAxis1.X * sideLength, planarAxis1.Y * sideLength, planarAxis1.Z * sideLength);
var yVector = new Vector3D(planarAxis2.X * sideLength, planarAxis2.Y * sideLength, planarAxis2.Z * sideLength);
// 定义正方形的两个轴向量沿X、Y轴
var xVector = new Vector3D(sideLength, 0, 0);
var yVector = new Vector3D(0, sideLength, 0);
// 沿宿主 up 略微抬高,避免与模型重叠。
var liftedCenter = ApplyVectorOffset(pointMarker.Center, hostUp, 0.01);
// 计算正方形原点(中心点减去各轴向量的一半),略微抬高以避免与模型重叠
var origin = new Point3D(
liftedCenter.X - planarAxis1.X * pointMarker.Radius - planarAxis2.X * pointMarker.Radius,
liftedCenter.Y - planarAxis1.Y * pointMarker.Radius - planarAxis2.Y * pointMarker.Radius,
liftedCenter.Z - planarAxis1.Z * pointMarker.Radius - planarAxis2.Z * pointMarker.Radius
pointMarker.Center.X - pointMarker.Radius,
pointMarker.Center.Y - pointMarker.Radius,
pointMarker.Center.Z + 0.01
);
// 使用Rectangle API渲染正方形
@ -3142,8 +3098,12 @@ namespace NavisworksTransport
/// <param name="pointMarker">点标记</param>
private void RenderCircleMarker(Graphics graphics, CircleMarker pointMarker)
{
// 调整中心点位置,沿宿主 up 略微抬高以避免与模型重叠
Point3D adjustCenterPoint = ApplyVectorOffset(pointMarker.Center, pointMarker.Normal, 0.01);
// 调整中心点位置,略微抬高以避免与模型重叠
Point3D adjustCenterPoint = new Point3D(
pointMarker.Center.X,
pointMarker.Center.Y,
pointMarker.Center.Z + 0.01
);
// 使用Circle API渲染圆形
graphics.Circle(adjustCenterPoint, pointMarker.Normal, pointMarker.Radius, true);

View File

@ -4,6 +4,8 @@ using System.IO;
using System.Linq;
using System.Windows.Forms;
using Autodesk.Navisworks.Api;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using NavisworksTransport.Utils;
using NavisApplication = Autodesk.Navisworks.Api.Application;
@ -14,204 +16,99 @@ namespace NavisworksTransport.Core
/// </summary>
public class SectionBoxExporter
{
private SectionBoxTraversalResult _lastTraversal;
/// <summary>
/// 导出结果
/// </summary>
public class ExportSectionBoxResult
{
public bool Success { get; set; }
public string FilePath { get; set; }
public int ObjectCount { get; set; }
public int HiddenNodeCount { get; set; }
public long FileSize { get; set; }
public string ErrorMessage { get; set; }
}
/// <summary>
/// 验证剖面盒并遍历模型树(不含导出,适合后台线程执行)
/// </summary>
public ExportSectionBoxResult ValidateAndTraverse(Document document)
{
var result = new ExportSectionBoxResult();
if (document == null)
{
result.ErrorMessage = "没有活动的文档!";
return result;
}
LogManager.Debug($"[剖面盒导出] 开始验证剖面盒Models={document.Models.Count}");
var viewpoint = document.CurrentViewpoint.Value;
var clipPlanes = viewpoint.ClipPlanes;
if (clipPlanes == null || !clipPlanes.Enabled)
{
result.ErrorMessage = "当前没有活动的剖面盒!";
return result;
}
if (clipPlanes.Mode != ClipPlaneSetMode.Box)
{
result.ErrorMessage = "请切换为剖面盒模式Box 模式)。";
return result;
}
if (!GetSectionBoxBounds(clipPlanes, out BoundingBox3D bounds))
{
result.ErrorMessage = "无法获取剖面盒边界。";
return result;
}
LogManager.Debug("[剖面盒导出] 开始遍历模型树...");
var sw = System.Diagnostics.Stopwatch.StartNew();
var traversal = GetObjectsAndHiddenItems(document, bounds);
sw.Stop();
_lastTraversal = traversal;
result.ObjectCount = traversal.ObjectsInBox.Count;
result.HiddenNodeCount = traversal.ItemsToHide.Count;
LogManager.Debug($"[剖面盒导出] 遍历完成: 耗时={sw.Elapsed.TotalSeconds:F1}s, 盒内对象={result.ObjectCount}, 隐藏节点={result.HiddenNodeCount}");
if (traversal.ObjectsInBox.Count == 0)
{
result.ErrorMessage = "剖面盒内没有找到对象!";
return result;
}
result.Success = true;
return result;
}
/// <summary>
/// 导出遍历结果到 NWD必须在 UI 线程调用)
/// </summary>
public ExportSectionBoxResult ExportTraversalToNwd(Document document, string filePath)
{
var result = new ExportSectionBoxResult();
if (_lastTraversal == null || _lastTraversal.ObjectsInBox.Count == 0)
{
result.ErrorMessage = "没有可导出的遍历结果,请先调用 ValidateAndTraverse。";
return result;
}
result.ObjectCount = _lastTraversal.ObjectsInBox.Count;
result.HiddenNodeCount = _lastTraversal.ItemsToHide.Count;
LogManager.Debug($"[剖面盒导出] 开始导出: 对象={result.ObjectCount}, 隐藏={result.HiddenNodeCount}, 路径={filePath}");
var sw = System.Diagnostics.Stopwatch.StartNew();
LogManager.Info($"[SectionBoxExporter] 剖面盒内找到 {result.ObjectCount} 个对象,可隐藏 {result.HiddenNodeCount} 个节点");
try
{
var exportedPath = NwdExportHelper.ExportToNwdWithPrecomputedHiddenItems(
_lastTraversal.ObjectsInBox,
_lastTraversal.ItemsToHide,
filePath,
"剖面盒导出");
if (string.IsNullOrEmpty(exportedPath) || !File.Exists(exportedPath))
{
result.ErrorMessage = "导出失败:未生成文件。";
return result;
}
result.Success = true;
result.FilePath = exportedPath;
result.FileSize = new FileInfo(exportedPath).Length;
sw.Stop();
LogManager.Debug($"[剖面盒导出] 完成: 耗时={sw.Elapsed.TotalSeconds:F1}s, 文件大小={result.FileSize / 1024} KB");
}
catch (Exception ex)
{
result.ErrorMessage = $"导出失败:{ex.Message}";
}
return result;
}
/// <summary>
/// 从当前视点导出剖面盒到 NWD完整业务流程验证→遍历→导出
/// 导出剖面盒信息
/// </summary>
/// <param name="document">Navisworks文档</param>
/// <param name="filePath">目标文件路径</param>
/// <returns>导出结果</returns>
public ExportSectionBoxResult ExportSectionBoxFromActiveView(Document document, string filePath)
/// <returns>导出的文件路径失败返回null</returns>
public string ExportToJson(Document document)
{
var result = new ExportSectionBoxResult();
if (document == null)
{
result.ErrorMessage = "没有活动的文档!请先打开一个模型。";
return result;
}
throw new ArgumentNullException(nameof(document));
// 验证剖面盒
// 1. 获取当前视点的裁剪平面
var viewpoint = document.CurrentViewpoint.Value;
var clipPlanes = viewpoint.ClipPlanes;
if (clipPlanes == null || !clipPlanes.Enabled)
if (clipPlanes == null)
{
result.ErrorMessage = "当前没有活动的剖面盒!\n请先使用 Navisworks 的剖面工具创建剖面盒。";
return result;
MessageBox.Show(
"当前没有活动的剖面盒!\n请先使用 Navisworks 的剖面工具创建剖面盒。",
"提示",
MessageBoxButtons.OK,
MessageBoxIcon.Information);
return null;
}
// 检查是否启用
if (!clipPlanes.Enabled)
{
MessageBox.Show(
"剖面盒未启用!\n请先在菜单【视点】-【启用剖分】,然后在菜单【剖分工具】的【模式】中选择【长方体】。\n【移动】、【缩放】或【旋转】剖面盒到适当的位置或者用【适应选择】调整",
"提示",
MessageBoxButtons.OK,
MessageBoxIcon.Information);
return null;
}
// 检查是否为长方体模式Box排除平面裁剪模式Clip
if (clipPlanes.Mode != ClipPlaneSetMode.Box)
{
result.ErrorMessage = "当前是平面裁剪模式,请切换为剖面盒模式!\n在 Navisworks 剖面工具中选择'Box'模式。";
return result;
MessageBox.Show(
"当前是平面裁剪模式,请切换为剖面盒模式!\n在 Navisworks 剖面工具中选择'Box'模式。",
"提示",
MessageBoxButtons.OK,
MessageBoxIcon.Information);
return null;
}
// 提取边界
if (!GetSectionBoxBounds(clipPlanes, out BoundingBox3D sectionBoxBounds))
// 2. 获取剖面盒的包围盒
BoundingBox3D sectionBoxBounds;
if (!GetSectionBoxBounds(clipPlanes, out sectionBoxBounds))
{
result.ErrorMessage = "无法获取剖面盒边界!\n请确保剖面盒已正确设置。";
return result;
MessageBox.Show(
"无法获取剖面盒边界!\n请确保剖面盒已正确设置。",
"错误",
MessageBoxButtons.OK,
MessageBoxIcon.Error);
return null;
}
// 遍历模型树
var traversalResult = GetObjectsAndHiddenItems(document, sectionBoxBounds);
result.ObjectCount = traversalResult.ObjectsInBox.Count;
result.HiddenNodeCount = traversalResult.ItemsToHide.Count;
// 3. 选择保存路径和格式
string filePath = ShowSaveFileDialog(out bool exportAsNwd);
if (string.IsNullOrEmpty(filePath))
return null;
if (traversalResult.ObjectsInBox.Count == 0)
{
result.ErrorMessage = "剖面盒内没有找到对象!";
return result;
}
LogManager.Info($"[SectionBoxExporter] 剖面盒内找到 {result.ObjectCount} 个对象,可隐藏 {result.HiddenNodeCount} 个节点");
// 导出
try
{
var exportedPath = NwdExportHelper.ExportToNwdWithPrecomputedHiddenItems(
traversalResult.ObjectsInBox,
traversalResult.ItemsToHide,
filePath,
"剖面盒导出");
// 4. 【优化】一次遍历同时获取剖面盒内对象和需要隐藏的节点
var traversalResult = GetObjectsAndHiddenItems(document, sectionBoxBounds);
if (string.IsNullOrEmpty(exportedPath) || !File.Exists(exportedPath))
if (exportAsNwd)
{
result.ErrorMessage = "导出失败:未生成文件。";
return result;
// 导出为NWD格式使用预计算结果
return ExportToNwd(document, traversalResult, filePath);
}
else
{
// 导出为JSON格式原有逻辑
var exportData = BuildExportData(sectionBoxBounds, traversalResult.ObjectsInBox, document);
string json = JsonConvert.SerializeObject(exportData, Formatting.Indented);
File.WriteAllText(filePath, json);
LogManager.Info($"剖面盒导出完成: {traversalResult.ObjectsInBox.Count} 个对象 -> {filePath}");
return filePath;
}
result.Success = true;
result.FilePath = exportedPath;
result.FileSize = new FileInfo(exportedPath).Length;
}
catch (Exception ex)
{
LogManager.Error($"[SectionBoxExporter] 导出失败: {ex.Message}");
result.ErrorMessage = $"导出失败:{ex.Message}";
LogManager.Error($"导出剖面盒失败: {ex.Message}", ex);
MessageBox.Show(
$"导出失败:{ex.Message}",
"错误",
MessageBoxButtons.OK,
MessageBoxIcon.Error);
return null;
}
return result;
}
/// <summary>
@ -284,15 +181,6 @@ namespace NavisworksTransport.Core
// 只遍历 uniqueParents 的子节点,不是整个模型
CalculateHiddenItems(visibleSet, result.ItemsToHide);
// 处理空白顶级子树:模型树无统一根节点时,完全无命中的子树不会被
// CalculateHiddenItems 覆盖,需显式隐藏其 RootItem
foreach (var model in document.Models)
{
if (model.RootItem == null) continue;
if (!visibleSet.Contains(model.RootItem))
result.ItemsToHide.Add(model.RootItem);
}
return result;
}
@ -393,6 +281,134 @@ namespace NavisworksTransport.Core
(a.Min.Z <= b.Max.Z && a.Max.Z >= b.Min.Z);
}
/// <summary>
/// 构建导出数据
/// </summary>
private JObject BuildExportData(BoundingBox3D sectionBox, List<ModelItem> objects, Document document)
{
var root = new JObject();
// 1. 导出剖面盒信息
var sectionBoxInfo = new JObject
{
["min"] = new JArray { sectionBox.Min.X, sectionBox.Min.Y, sectionBox.Min.Z },
["max"] = new JArray { sectionBox.Max.X, sectionBox.Max.Y, sectionBox.Max.Z },
["center"] = new JArray
{
(sectionBox.Min.X + sectionBox.Max.X) / 2,
(sectionBox.Min.Y + sectionBox.Max.Y) / 2,
(sectionBox.Min.Z + sectionBox.Max.Z) / 2
},
["size"] = new JArray
{
sectionBox.Max.X - sectionBox.Min.X,
sectionBox.Max.Y - sectionBox.Min.Y,
sectionBox.Max.Z - sectionBox.Min.Z
}
};
root["sectionBox"] = sectionBoxInfo;
// 2. 导出文档信息
var docInfo = new JObject
{
["fileName"] = document.FileName ?? "Unknown",
["units"] = document.Units.ToString(),
["exportTime"] = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"),
["objectCount"] = objects.Count
};
root["documentInfo"] = docInfo;
// 3. 导出对象列表
var objectsArray = new JArray();
foreach (var item in objects)
{
var bbox = item.BoundingBox();
var objInfo = new JObject
{
["name"] = GetFullPathName(item),
["boundingBox"] = new JObject
{
["min"] = new JArray { bbox.Min.X, bbox.Min.Y, bbox.Min.Z },
["max"] = new JArray { bbox.Max.X, bbox.Max.Y, bbox.Max.Z },
["center"] = new JArray
{
(bbox.Min.X + bbox.Max.X) / 2,
(bbox.Min.Y + bbox.Max.Y) / 2,
(bbox.Min.Z + bbox.Max.Z) / 2
},
["size"] = new JArray
{
bbox.Max.X - bbox.Min.X,
bbox.Max.Y - bbox.Min.Y,
bbox.Max.Z - bbox.Min.Z
}
}
};
// 添加Transform信息仅非单位变换
try
{
var components = item.Transform.Factor();
var rotation = components.Rotation.ToAxisAndAngle();
bool isIdentity = Math.Abs(rotation.Angle) < 0.0001 &&
Math.Abs(components.Scale.X - 1) < 0.0001 &&
Math.Abs(components.Scale.Y - 1) < 0.0001 &&
Math.Abs(components.Scale.Z - 1) < 0.0001 &&
Math.Abs(components.Translation.X) < 0.0001 &&
Math.Abs(components.Translation.Y) < 0.0001 &&
Math.Abs(components.Translation.Z) < 0.0001;
if (!isIdentity)
{
objInfo["transform"] = new JObject
{
["translation"] = new JArray { components.Translation.X, components.Translation.Y, components.Translation.Z },
["rotation"] = new JObject
{
["axis"] = new JArray { rotation.Axis.X, rotation.Axis.Y, rotation.Axis.Z },
["angle"] = rotation.Angle
},
["scale"] = new JArray { components.Scale.X, components.Scale.Y, components.Scale.Z }
};
}
}
catch (Exception ex)
{
LogManager.Debug($"获取Transform信息失败: {ex.Message}");
}
objectsArray.Add(objInfo);
}
root["objects"] = objectsArray;
return root;
}
/// <summary>
/// 获取对象的全路径名称
/// </summary>
private string GetFullPathName(ModelItem item)
{
if (item == null) return "";
var pathParts = new List<string>();
// 从当前节点向上遍历到根节点
var current = item;
while (current != null)
{
string name = current.DisplayName;
if (!string.IsNullOrEmpty(name))
{
pathParts.Insert(0, name);
}
current = current.Parent;
}
// 使用 "/" 连接路径
return string.Join("/", pathParts);
}
/// <summary>
/// 导出为NWD文件委托给 NwdExportHelper
/// 适用于通用场景(如保存选择项)
@ -423,5 +439,63 @@ namespace NavisworksTransport.Core
"剖面盒导出");
}
/// <summary>
/// 导出为JSON文件公开方法供外部调用
/// </summary>
public string ExportToJson(Document document, List<ModelItem> objects, string filePath)
{
try
{
// 获取当前剖面盒边界用于JSON中的元数据
var viewpoint = document.CurrentViewpoint.Value;
var clipPlanes = viewpoint.ClipPlanes;
BoundingBox3D sectionBox = default;
if (clipPlanes?.Enabled == true && clipPlanes.Mode == ClipPlaneSetMode.Box && clipPlanes.Box != null)
{
var box = clipPlanes.Box;
sectionBox = new BoundingBox3D(
new Point3D(box.Min.X, box.Min.Y, box.Min.Z),
new Point3D(box.Max.X, box.Max.Y, box.Max.Z)
);
}
var exportData = BuildExportData(sectionBox, objects, document);
string json = JsonConvert.SerializeObject(exportData, Formatting.Indented);
File.WriteAllText(filePath, json);
LogManager.Info($"[SectionBoxExporter] JSON导出完成: {objects.Count} 个对象 -> {filePath}");
return filePath;
}
catch (Exception ex)
{
LogManager.Error($"[SectionBoxExporter] JSON导出失败: {ex.Message}");
return null;
}
}
/// <summary>
/// 显示保存文件对话框
/// </summary>
private string ShowSaveFileDialog(out bool exportAsNwd)
{
exportAsNwd = true; // 默认导出为NWD
using (var dialog = new SaveFileDialog())
{
// NWD 作为默认格式JSON 作为备选
dialog.Filter = "Navisworks files (*.nwd)|*.nwd|JSON files (*.json)|*.json";
dialog.DefaultExt = "nwd";
dialog.FileName = $"SectionBox_Export_{DateTime.Now:yyyyMMdd_HHmmss}";
dialog.Title = "导出剖面盒信息";
if (dialog.ShowDialog() == DialogResult.OK)
{
// 如果用户选择了JSONFilterIndex为2或文件扩展名为json
exportAsNwd = !(dialog.FilterIndex == 2 ||
dialog.FileName.EndsWith(".json", StringComparison.OrdinalIgnoreCase));
return dialog.FileName;
}
}
return null;
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -215,7 +215,7 @@ namespace NavisworksTransport.Core
ModelItemTransformHelper.MoveItemToPositionAndRotation(_virtualObjectModelItem, position, rotation);
var actualBounds = _virtualObjectModelItem.BoundingBox();
Point3D actualCenter = actualBounds?.Center ?? new Point3D(0, 0, 0);
LogManager.Debug(
LogManager.Info(
$"[虚拟物体姿态] 应用后中心=({actualCenter.X:F3},{actualCenter.Y:F3},{actualCenter.Z:F3}), " +
$"目标中心=({position.X:F3},{position.Y:F3},{position.Z:F3}), " +
$"偏差=({actualCenter.X - position.X:F3},{actualCenter.Y - position.Y:F3},{actualCenter.Z - position.Z:F3})");
@ -305,7 +305,7 @@ namespace NavisworksTransport.Core
if (itemBounds != null)
{
LogManager.Debug(
LogManager.Info(
$"{prefix} Item包围盒: Min=({itemBounds.Min.X:F3},{itemBounds.Min.Y:F3},{itemBounds.Min.Z:F3}), " +
$"Max=({itemBounds.Max.X:F3},{itemBounds.Max.Y:F3},{itemBounds.Max.Z:F3}), " +
$"Center=({itemBounds.Center.X:F3},{itemBounds.Center.Y:F3},{itemBounds.Center.Z:F3})");

View File

@ -7,7 +7,6 @@ using Roy_T.AStar.Primitives;
using Roy_T.AStar.Paths;
using NavisworksTransport.Utils;
using NavisworksTransport.Core.Config;
using NavisworksTransport.Utils.CoordinateSystem;
namespace NavisworksTransport.PathPlanning
{
@ -496,11 +495,8 @@ namespace NavisworksTransport.PathPlanning
var endGridPos = gridMap.WorldToGrid(end);
// 找到起点和终点最近的3D节点
double startElevation = gridMap.GetWorldElevation(start);
double endElevation = gridMap.GetWorldElevation(end);
var startNode = FindClosest3DNode(graph3D, startGridPos, startElevation);
var endNode = FindClosest3DNode(graph3D, endGridPos, endElevation);
var startNode = FindClosest3DNode(graph3D, startGridPos, start.Z);
var endNode = FindClosest3DNode(graph3D, endGridPos, end.Z);
// 起点和终点必须检查可达性
bool startOnObstacle = (startNode == null);
@ -528,7 +524,8 @@ namespace NavisworksTransport.PathPlanning
foreach (var node in graph3D.AllNodes)
{
var info = graph3D.NodeInfo[node];
var nodeWorldPos = gridMap.CreateWorldPoint(new GridPoint2D(info.X, info.Y), info.Z);
var nodeWorldPos = gridMap.GridToWorld3D(new GridPoint2D(info.X, info.Y));
nodeWorldPos = new Point3D(nodeWorldPos.X, nodeWorldPos.Y, info.Z);
double distance = Math.Sqrt(
Math.Pow(nodeWorldPos.X - end.X, 2) +
@ -557,8 +554,8 @@ namespace NavisworksTransport.PathPlanning
var startInfo = graph3D.NodeInfo[startNode];
var endInfo = graph3D.NodeInfo[endNode];
LogManager.Info($"[A*执行-3D] 起点映射: ({startGridPos.X},{startGridPos.Y},高程={startElevation:F2}) -> 节点({startInfo.X},{startInfo.Y},层{startInfo.LayerIndex},高程={startInfo.Z:F2})");
LogManager.Info($"[A*执行-3D] 终点映射: ({endGridPos.X},{endGridPos.Y},高程={endElevation:F2}) -> 节点({endInfo.X},{endInfo.Y},层{endInfo.LayerIndex},高程={endInfo.Z:F2})");
LogManager.Info($"[A*执行-3D] 起点映射: ({startGridPos.X},{startGridPos.Y},Z={start.Z:F2}) -> 节点({startInfo.X},{startInfo.Y},层{startInfo.LayerIndex},Z={startInfo.Z:F2})");
LogManager.Info($"[A*执行-3D] 终点映射: ({endGridPos.X},{endGridPos.Y},Z={end.Z:F2}) -> 节点({endInfo.X},{endInfo.Y},层{endInfo.LayerIndex},Z={endInfo.Z:F2})");
// 执行A*
var pathfinder = new Roy_T.AStar.Paths.PathFinder();
@ -702,7 +699,7 @@ namespace NavisworksTransport.PathPlanning
path3D.Add(originalStart);
int pointIndex = 0;
LogManager.Debug($"[路径点{pointIndex}] 原始起点: ({originalStart.X:F2}, {originalStart.Y:F2}, {originalStart.Z:F2}), 高程={gridMap.GetWorldElevation(originalStart):F2}");
LogManager.Debug($"[路径点{pointIndex}] 原始起点: ({originalStart.X:F2}, {originalStart.Y:F2}, Z={originalStart.Z:F2})");
pointIndex++;
// 添加第一条边的起点如果与originalStart不同
@ -713,13 +710,14 @@ namespace NavisworksTransport.PathPlanning
if (firstNode != null && graph3D.NodeInfo.ContainsKey(firstNode))
{
var firstInfo = graph3D.NodeInfo[firstNode];
var firstPoint = gridMap.CreateWorldPoint(new GridPoint2D(firstInfo.X, firstInfo.Y), firstInfo.Z);
var firstPos = gridMap.GridToWorld2D(new GridPoint2D(firstInfo.X, firstInfo.Y));
var firstPoint = new Point3D(firstPos.X, firstPos.Y, firstInfo.Z);
// 如果与起点高度不同,添加这个节点(避免直接跳跃)
if (Math.Abs(gridMap.GetWorldElevation(firstPoint) - gridMap.GetWorldElevation(originalStart)) > 0.01)
if (Math.Abs(firstPoint.Z - originalStart.Z) > 0.01)
{
path3D.Add(firstPoint);
LogManager.Debug($"[路径点{pointIndex}] A*起点: 网格({firstInfo.X},{firstInfo.Y}), 层{firstInfo.LayerIndex}, 高程={firstInfo.Z:F2}, 类型={firstInfo.CellType}");
LogManager.Debug($"[路径点{pointIndex}] A*起点: 网格({firstInfo.X},{firstInfo.Y}), 层{firstInfo.LayerIndex}, Z={firstInfo.Z:F2}, 类型={firstInfo.CellType}");
pointIndex++;
}
}
@ -733,9 +731,10 @@ namespace NavisworksTransport.PathPlanning
continue;
var nodeInfo = graph3D.NodeInfo[endNode];
path3D.Add(gridMap.CreateWorldPoint(new GridPoint2D(nodeInfo.X, nodeInfo.Y), nodeInfo.Z));
var worldPos = gridMap.GridToWorld2D(new GridPoint2D(nodeInfo.X, nodeInfo.Y));
path3D.Add(new Point3D(worldPos.X, worldPos.Y, nodeInfo.Z));
LogManager.Debug($"[路径点{pointIndex}] 网格({nodeInfo.X},{nodeInfo.Y}), 层{nodeInfo.LayerIndex}, 高程={nodeInfo.Z:F2}, 类型={nodeInfo.CellType}");
LogManager.Debug($"[路径点{pointIndex}] 网格({nodeInfo.X},{nodeInfo.Y}), 层{nodeInfo.LayerIndex}, Z={nodeInfo.Z:F2}, 类型={nodeInfo.CellType}");
pointIndex++;
}
@ -830,10 +829,10 @@ namespace NavisworksTransport.PathPlanning
// 初始化激活层字典
var activeLayerZ = new Dictionary<GridPoint2D, double>();
double targetZ = gridMap.GetWorldElevation(endPos); // 使用宿主坐标语义下的终点高程作为目标高度
double targetZ = endPos.Z; // 使用终点Z作为目标高度
double baseSpeed = 5.0; // km/h
LogManager.Info($"[A*转换-2.5D] 目标高: {targetZ:F2}m基础速度: {baseSpeed}km/h");
LogManager.Info($"[A*转换-2.5D] 目标高: {targetZ:F2}m基础速度: {baseSpeed}km/h");
// 输出详细的网格统计信息
LogManager.Info($"[A*转换-2.5D] 输入网格统计信息:\n{gridMap.GetStatistics()}");
@ -1084,7 +1083,7 @@ namespace NavisworksTransport.PathPlanning
// 初始化激活高度层字典和目标高度
var activeLayerZ = new Dictionary<GridPoint2D, double>();
double targetZ = gridMap.GetWorldElevation(endPos);
double targetZ = endPos.Z;
// 1. 创建网格基础结构
double cellSizeInMeters = UnitsConverter.ConvertToMeters(gridMap.CellSize);
@ -1355,7 +1354,7 @@ namespace NavisworksTransport.PathPlanning
{
foreach (var layer in cell.HeightLayers)
{
if (layer.IsWalkable && layer.PassableHeight.GetSpan() >= objectHeight)
if (layer.PassableHeight.GetSpan() >= objectHeight)
return true;
}
return false;
@ -1380,7 +1379,7 @@ namespace NavisworksTransport.PathPlanning
foreach (var layer in cell.HeightLayers)
{
if (layer.IsWalkable && layer.PassableHeight.GetSpan() >= objectHeight)
if (layer.PassableHeight.GetSpan() >= objectHeight)
return true;
}
@ -1404,9 +1403,6 @@ namespace NavisworksTransport.PathPlanning
foreach (var layer in layers)
{
if (!layer.IsWalkable)
continue;
// 必须满足物体高度
if (layer.PassableHeight.GetSpan() < objectHeight)
continue;
@ -1568,19 +1564,19 @@ namespace NavisworksTransport.PathPlanning
if (endCell.HasValue)
{
// 调试检查门网格的Z坐标
double endElevation = endCell.Value.HeightLayers != null && endCell.Value.HeightLayers.Count > 0
double endZ = endCell.Value.HeightLayers != null && endCell.Value.HeightLayers.Count > 0
? endCell.Value.HeightLayers[0].Z
: 0;
if (endCell.Value.CellType == "门")
{
LogManager.Info($"[路径转换-门] 门网格({endGridPos.X},{endGridPos.Y}) HeightLayer[0].高程={endElevation:F3}, 位置({endWorldPos.X:F2},{endWorldPos.Y:F2})");
LogManager.Info($"[路径转换-门] 门网格({endGridPos.X},{endGridPos.Y}) HeightLayer[0].Z={endZ:F3}, 位置({endWorldPos.X:F2},{endWorldPos.Y:F2})");
}
endWorldPos = gridMap.SetWorldElevation(endWorldPos, endElevation);
endWorldPos = new Point3D(endWorldPos.X, endWorldPos.Y, endZ);
}
else
{
LogManager.Warning($"[路径转换] 网格({endGridPos.X},{endGridPos.Y})获取失败,位置({endWorldPos.X:F2},{endWorldPos.Y:F2})高程保持为默认值");
LogManager.Warning($"[路径转换] 网格({endGridPos.X},{endGridPos.Y})获取失败,位置({endWorldPos.X:F2},{endWorldPos.Y:F2})Z坐标保持为0");
}
// 检查是否与上一个点重复A*可能在转弯点有重复)
@ -1627,7 +1623,8 @@ namespace NavisworksTransport.PathPlanning
{
var startPoint = optimizedPath[currentIndex];
var endPoint = optimizedPath[testIndex];
var distance = CalculateHostHorizontalDistance(startPoint, endPoint, gridMap);
var distance = Math.Sqrt(Math.Pow(endPoint.X - startPoint.X, 2) +
Math.Pow(endPoint.Y - startPoint.Y, 2));
// 🔥 修复:检查高度变化,有高度变化就不能优化掉
// 1. 起终点高度差不能超过阈值(原有逻辑:防止跨越大高度差,例如楼梯)
@ -1635,8 +1632,8 @@ namespace NavisworksTransport.PathPlanning
bool canSkip = false;
double maxAllowedHeightDiff = ConfigManager.Instance.Current.PathEditing.MaxHeightDiff; // 从配置文件读取高度差阈值(模型单位)
double startZ = gridMap.GetWorldElevation(startPoint);
double endZ = gridMap.GetWorldElevation(endPoint);
double startZ = startPoint.Z;
double endZ = endPoint.Z;
double totalHeightDiff = Math.Abs(endZ - startZ);
// 检查1起终点高度差必须在阈值内
@ -1648,7 +1645,7 @@ namespace NavisworksTransport.PathPlanning
for (int midIndex = currentIndex + 1; midIndex < testIndex; midIndex++)
{
double midZ = gridMap.GetWorldElevation(optimizedPath[midIndex]);
double midZ = optimizedPath[midIndex].Z;
// 如果中间点高度与起点或终点不同,说明有高度变化,不能优化掉
if (Math.Abs(midZ - startZ) > heightTolerance ||
Math.Abs(midZ - endZ) > heightTolerance)
@ -1728,7 +1725,7 @@ namespace NavisworksTransport.PathPlanning
try
{
// 计算线段长度
var distance = CalculateHostHorizontalDistance(start, end, gridMap);
var distance = Math.Sqrt(Math.Pow(end.X - start.X, 2) + Math.Pow(end.Y - start.Y, 2));
// 如果距离太小,直接检查起点和终点
if (distance < gridMap.CellSize * 0.1)
@ -1749,34 +1746,31 @@ namespace NavisworksTransport.PathPlanning
{
// 线性插值计算采样点
double t = samples > 0 ? (double)i / samples : 0;
double interpolatedElevation =
gridMap.GetWorldElevation(start) +
t * (gridMap.GetWorldElevation(end) - gridMap.GetWorldElevation(start));
var samplePoint = gridMap.SetWorldElevation(
new Point3D(
start.X + t * (end.X - start.X),
start.Y + t * (end.Y - start.Y),
start.Z + t * (end.Z - start.Z)),
interpolatedElevation);
var samplePoint = new Point3D(
start.X + t * (end.X - start.X),
start.Y + t * (end.Y - start.Y),
start.Z + t * (end.Z - start.Z)
);
// 🔥 修复检查采样点的具体Z高度是否在可通行层范围内
// 使用IsPassableAt3DPoint而不是IsWalkable确保不穿过障碍物层
var gridPos = gridMap.WorldToGrid(samplePoint);
double tolerance = 0.1; // Z坐标容差模型单位
if (!gridMap.IsValidGridPosition(gridPos) || !gridMap.IsPassableAtElevation(gridPos, gridMap.GetWorldElevation(samplePoint), tolerance))
if (!gridMap.IsValidGridPosition(gridPos) || !gridMap.IsPassableAt3DPoint(gridPos, samplePoint.Z, tolerance))
{
// 🔥 修复使用Origin计算网格左下角而不是Bounds.Min
var (gridMinH1, gridMinH2) = GetGridCellMinHorizontalCoords(gridMap, gridPos);
var gridCenterH1 = gridMinH1 + 0.5 * gridMap.CellSize;
var gridCenterH2 = gridMinH2 + 0.5 * gridMap.CellSize;
var gridCenterZ = gridMap.GetWorldElevation(samplePoint);
var gridMinX = gridMap.Origin.X + gridPos.X * gridMap.CellSize;
var gridMinY = gridMap.Origin.Y + gridPos.Y * gridMap.CellSize;
// 计算网格中心点世界坐标(基于左下角)
var gridCenterX = gridMinX + 0.5 * gridMap.CellSize;
var gridCenterY = gridMinY + 0.5 * gridMap.CellSize;
var gridCenterZ = samplePoint.Z; // Z保持不变
// 计算偏差
var sampleHorizontal = GetHostHorizontalCoords(samplePoint, gridMap);
var deltaH1 = sampleHorizontal.h1 - gridCenterH1;
var deltaH2 = sampleHorizontal.h2 - gridCenterH2;
var deltaX = samplePoint.X - gridCenterX;
var deltaY = samplePoint.Y - gridCenterY;
// LogManager.Debug($"[斜线检查] 采样点{i}/{samples}失败:" +
// $"采样点({samplePoint.X:F3},{samplePoint.Y:F3},{samplePoint.Z:F3})" +
@ -1791,12 +1785,14 @@ namespace NavisworksTransport.PathPlanning
if (!IsSamplePointSafeFromNeighborObstacles(samplePoint, gridPos, gridMap))
{
// 🔥 修复使用Origin计算网格左下角
var (gridMinH1, gridMinH2) = GetGridCellMinHorizontalCoords(gridMap, gridPos);
var gridCenterH1 = gridMinH1 + 0.5 * gridMap.CellSize;
var gridCenterH2 = gridMinH2 + 0.5 * gridMap.CellSize;
var sampleHorizontal = GetHostHorizontalCoords(samplePoint, gridMap);
var deltaH1 = sampleHorizontal.h1 - gridCenterH1;
var deltaH2 = sampleHorizontal.h2 - gridCenterH2;
var gridMinX = gridMap.Origin.X + gridPos.X * gridMap.CellSize;
var gridMinY = gridMap.Origin.Y + gridPos.Y * gridMap.CellSize;
// 计算网格中心点世界坐标
var gridCenterX = gridMinX + 0.5 * gridMap.CellSize;
var gridCenterY = gridMinY + 0.5 * gridMap.CellSize;
var deltaX = samplePoint.X - gridCenterX;
var deltaY = samplePoint.Y - gridCenterY;
// LogManager.Debug($"[斜线检查] 采样点{i}/{samples}邻居障碍检查失败:" +
// $"采样点({samplePoint.X:F3},{samplePoint.Y:F3})" +
@ -1807,11 +1803,6 @@ namespace NavisworksTransport.PathPlanning
}
}
if (SegmentIntersectsBlockedGridCellInterior(start, end, gridMap))
{
return false;
}
return true;
}
catch (Exception ex)
@ -1834,11 +1825,11 @@ namespace NavisworksTransport.PathPlanning
{
// 🔥 修复使用Origin计算网格左下角而不是Bounds.Min
// 计算采样点在所在网格内的相对位置 (0.0 到 1.0)
var (gridMinH1, gridMinH2) = GetGridCellMinHorizontalCoords(gridMap, gridPos);
var sampleHorizontal = GetHostHorizontalCoords(samplePoint, gridMap);
var gridMinX = gridMap.Origin.X + gridPos.X * gridMap.CellSize;
var gridMinY = gridMap.Origin.Y + gridPos.Y * gridMap.CellSize;
var relativeX = (sampleHorizontal.h1 - gridMinH1) / gridMap.CellSize;
var relativeY = (sampleHorizontal.h2 - gridMinH2) / gridMap.CellSize;
var relativeX = (samplePoint.X - gridMinX) / gridMap.CellSize;
var relativeY = (samplePoint.Y - gridMinY) / gridMap.CellSize;
// 确保相对位置在有效范围内
relativeX = Math.Max(0.0, Math.Min(1.0, relativeX));
@ -1872,18 +1863,18 @@ namespace NavisworksTransport.PathPlanning
if (!constraintSatisfied)
{
// 🔥 修复:基于正确的网格左下角计算网格中心点
var gridCenterH1 = gridMinH1 + 0.5 * gridMap.CellSize;
var gridCenterH2 = gridMinH2 + 0.5 * gridMap.CellSize;
var deltaH1 = sampleHorizontal.h1 - gridCenterH1;
var deltaH2 = sampleHorizontal.h2 - gridCenterH2;
var gridCenterX = gridMinX + 0.5 * gridMap.CellSize;
var gridCenterY = gridMinY + 0.5 * gridMap.CellSize;
var deltaX = samplePoint.X - gridCenterX;
var deltaY = samplePoint.Y - gridCenterY;
LogManager.Debug($"[邻居障碍] 位置约束失败:" +
$"{directionNames[i]}方向有障碍," +
$"网格({gridPos.X},{gridPos.Y})" +
$"采样点({samplePoint.X:F3},{samplePoint.Y:F3})" +
$"网格左下角H=({gridMinH1:F3},{gridMinH2:F3})" +
$"网格中心H=({gridCenterH1:F3},{gridCenterH2:F3})" +
$"偏差(ΔH1={deltaH1:F3}, ΔH2={deltaH2:F3})" +
$"网格左下角({gridMinX:F3},{gridMinY:F3})" +
$"网格中心({gridCenterX:F3},{gridCenterY:F3})" +
$"偏差(ΔX={deltaX:F3}, ΔY={deltaY:F3})" +
$"相对位置({relativeX:F3},{relativeY:F3})");
return false;
}
@ -1962,76 +1953,6 @@ namespace NavisworksTransport.PathPlanning
return result;
}
private static double CalculateHostHorizontalDistance(Point3D start, Point3D end, GridMap gridMap)
{
var startHorizontal = GetHostHorizontalCoords(start, gridMap);
var endHorizontal = GetHostHorizontalCoords(end, gridMap);
double dh1 = endHorizontal.h1 - startHorizontal.h1;
double dh2 = endHorizontal.h2 - startHorizontal.h2;
return Math.Sqrt(dh1 * dh1 + dh2 * dh2);
}
private static (double h1, double h2) GetGridCellMinHorizontalCoords(GridMap gridMap, GridPoint2D gridPosition)
{
var (minH1, _, minH2, _) = gridMap.GetGridCellPlanarBounds(gridPosition);
return (minH1, minH2);
}
private static (double h1, double h2) GetHostHorizontalCoords(Point3D point, GridMap gridMap)
{
var adapter = new HostCoordinateAdapter(gridMap.CoordinateSystemType);
return HostPlanarGridHelper.GetHorizontalCoords3(
new System.Numerics.Vector3((float)point.X, (float)point.Y, (float)point.Z),
adapter);
}
private bool SegmentIntersectsBlockedGridCellInterior(Point3D start, Point3D end, GridMap gridMap)
{
var startHorizontal = GetHostHorizontalCoords(start, gridMap);
var endHorizontal = GetHostHorizontalCoords(end, gridMap);
var originHorizontal = GetHostHorizontalCoords(gridMap.Origin, gridMap);
int minGridX = (int)Math.Round((Math.Min(startHorizontal.h1, endHorizontal.h1) - originHorizontal.h1) / gridMap.CellSize) - 1;
int maxGridX = (int)Math.Round((Math.Max(startHorizontal.h1, endHorizontal.h1) - originHorizontal.h1) / gridMap.CellSize) + 1;
int minGridY = (int)Math.Round((Math.Min(startHorizontal.h2, endHorizontal.h2) - originHorizontal.h2) / gridMap.CellSize) - 1;
int maxGridY = (int)Math.Round((Math.Max(startHorizontal.h2, endHorizontal.h2) - originHorizontal.h2) / gridMap.CellSize) + 1;
minGridX = Math.Max(0, minGridX);
minGridY = Math.Max(0, minGridY);
maxGridX = Math.Min(gridMap.Width - 1, maxGridX);
maxGridY = Math.Min(gridMap.Height - 1, maxGridY);
for (int x = minGridX; x <= maxGridX; x++)
{
for (int y = minGridY; y <= maxGridY; y++)
{
var gridPosition = new GridPoint2D(x, y);
if (!gridMap.TryGetSegmentGridCellInteriorIntersection(start, end, gridPosition, out double enterT, out double exitT))
{
continue;
}
double sampleT = Math.Max(0.0, Math.Min(1.0, (enterT + exitT) * 0.5));
Point3D samplePoint = new Point3D(
start.X + sampleT * (end.X - start.X),
start.Y + sampleT * (end.Y - start.Y),
start.Z + sampleT * (end.Z - start.Z));
double sampleElevation = gridMap.GetWorldElevation(samplePoint);
if (!gridMap.IsPassableAtElevation(gridPosition, sampleElevation, 0.1))
{
LogManager.Debug(
$"[斜线检查] 线段穿过不可通行网格内部: 网格({x},{y}), " +
$"t=[{enterT:F3},{exitT:F3}], sampleT={sampleT:F3}");
return true;
}
}
}
return false;
}
/// <summary>
/// 计算路径总长度
@ -2094,7 +2015,7 @@ namespace NavisworksTransport.PathPlanning
/// <param name="originalStart">原始起点坐标</param>
/// <param name="originalEnd">原始终点坐标</param>
/// <returns>修正后的路径</returns>
private List<Point3D> CorrectStartEndPoints(List<Point3D> path, Point3D originalStart, Point3D originalEnd, GridMap gridMap)
private List<Point3D> CorrectStartEndPoints(List<Point3D> path, Point3D originalStart, Point3D originalEnd)
{
if (path == null || path.Count == 0)
return path;
@ -2104,23 +2025,21 @@ namespace NavisworksTransport.PathPlanning
LogManager.Info($"[坐标修正] 开始修正路径起终点坐标");
var correctedPath = new List<Point3D>(path);
// 修正起点:保持原始宿主水平位置,复用路径求得的高程
// 修正起点:保持原始XY坐标使用路径的Z坐标
if (correctedPath.Count > 0)
{
double startElevation = gridMap.GetWorldElevation(correctedPath[0]);
var correctedStart = gridMap.SetWorldElevation(originalStart, startElevation);
LogManager.Info($"[坐标修正] 起点: ({correctedPath[0].X:F3}, {correctedPath[0].Y:F3}, {correctedPath[0].Z:F3}) -> ({correctedStart.X:F3}, {correctedStart.Y:F3}, {correctedStart.Z:F3}), 高程={startElevation:F3}");
correctedPath[0] = correctedStart;
var originalStartWithZ = new Point3D(originalStart.X, originalStart.Y, correctedPath[0].Z);
LogManager.Info($"[坐标修正] 起点: ({correctedPath[0].X:F3}, {correctedPath[0].Y:F3}, {correctedPath[0].Z:F3}) -> ({originalStartWithZ.X:F3}, {originalStartWithZ.Y:F3}, {originalStartWithZ.Z:F3})");
correctedPath[0] = originalStartWithZ;
}
// 修正终点:保持原始宿主水平位置,复用路径求得的高程
// 修正终点:保持原始XY坐标使用路径的Z坐标
if (correctedPath.Count > 1)
{
var lastIndex = correctedPath.Count - 1;
double endElevation = gridMap.GetWorldElevation(correctedPath[lastIndex]);
var correctedEnd = gridMap.SetWorldElevation(originalEnd, endElevation);
LogManager.Info($"[坐标修正] 终点: ({correctedPath[lastIndex].X:F3}, {correctedPath[lastIndex].Y:F3}, {correctedPath[lastIndex].Z:F3}) -> ({correctedEnd.X:F3}, {correctedEnd.Y:F3}, {correctedEnd.Z:F3}), 高程={endElevation:F3}");
correctedPath[lastIndex] = correctedEnd;
var originalEndWithZ = new Point3D(originalEnd.X, originalEnd.Y, correctedPath[lastIndex].Z);
LogManager.Info($"[坐标修正] 终点: ({correctedPath[lastIndex].X:F3}, {correctedPath[lastIndex].Y:F3}, {correctedPath[lastIndex].Z:F3}) -> ({originalEndWithZ.X:F3}, {originalEndWithZ.Y:F3}, {originalEndWithZ.Z:F3})");
correctedPath[lastIndex] = originalEndWithZ;
}
LogManager.Info($"[坐标修正] 路径起终点坐标修正完成");
@ -2168,7 +2087,7 @@ namespace NavisworksTransport.PathPlanning
if (cell.HeightLayers != null && cell.HeightLayers.Count > 0)
{
// 查找包含指定Z坐标且满足物体高度的层
var matchingLayer = gridMap.FindLayerContainingElevation(gridPos, gridMap.GetWorldElevation(point), tolerance: 0.5);
var matchingLayer = gridMap.FindLayerContainingZ(gridPos, point.Z, tolerance: 0.5);
if (matchingLayer.HasValue)
{
@ -2179,12 +2098,12 @@ namespace NavisworksTransport.PathPlanning
}
else
{
LogManager.Warning($"[高度检查] ❌ 找到匹配高程={gridMap.GetWorldElevation(point):F2}的层高程={matchingLayer.Value.Z:F2},但高度不足:物体高度{objectHeight:F2},层高度跨度{matchingLayer.Value.PassableHeight.GetSpan():F2}");
LogManager.Warning($"[高度检查] ❌ 找到匹配Z={point.Z:F2}的层Z={matchingLayer.Value.Z:F2},但高度不足:物体高度{objectHeight:F2},层高度跨度{matchingLayer.Value.PassableHeight.GetSpan():F2}");
}
}
else
{
LogManager.Warning($"[高度检查] ❌ 单元格({gridX}, {gridY})有{cell.HeightLayers.Count}个高度层,但无法找到包含高程={gridMap.GetWorldElevation(point):F2}的层");
LogManager.Warning($"[高度检查] ❌ 单元格({gridX}, {gridY})有{cell.HeightLayers.Count}个高度层,但无法找到包含Z={point.Z:F2}的层");
}
return false;
}
@ -2217,17 +2136,17 @@ namespace NavisworksTransport.PathPlanning
var adjustedPath = new List<Point3D>();
// 1. 使用用户指定的原始起点和终点高程(而不是从转换后的路径中提取)
double startZ = gridMap.GetWorldElevation(originalStart);
double endZ = gridMap.GetWorldElevation(originalEnd);
// 1. 使用用户指定的原始起点和终点Z坐标(而不是从转换后的路径中提取)
double startZ = originalStart.Z;
double endZ = originalEnd.Z;
// 对比日志:显示原始坐标与路径坐标的差异
var pathStartZ = gridMap.GetWorldElevation(pathWithGridCoords[0].point);
var pathEndZ = gridMap.GetWorldElevation(pathWithGridCoords[pathWithGridCoords.Count - 1].point);
var pathStartZ = pathWithGridCoords[0].point.Z;
var pathEndZ = pathWithGridCoords[pathWithGridCoords.Count - 1].point.Z;
LogManager.Info($"[智能选层] 原始起点高程={startZ:F3}, 原始终点高程={endZ:F3}");
LogManager.Info($"[智能选层] 路径起点高程={pathStartZ:F3}, 路径终点高程={pathEndZ:F3}");
LogManager.Info($"[智能选层] 使用原始高程进行智能选层,高程变化={endZ - startZ:F3}");
LogManager.Info($"[智能选层] 原始起点Z={startZ:F3}, 原始终点Z={endZ:F3}");
LogManager.Info($"[智能选层] 路径起点Z={pathStartZ:F3}, 路径终点Z={pathEndZ:F3}");
LogManager.Info($"[智能选层] 使用原始Z坐标进行智能选层高度变化={endZ - startZ:F3}");
// 2. 为每个路径点选择高度层
for (int i = 0; i < pathWithGridCoords.Count; i++)
@ -2256,8 +2175,8 @@ namespace NavisworksTransport.PathPlanning
var selectedLayer = SelectBestLayer(cell.HeightLayers, startZ, objectHeight, startZ, endZ, i, pathWithGridCoords.Count, true);
if (selectedLayer.HasValue)
{
adjustedPath.Add(gridMap.SetWorldElevation(point, selectedLayer.Value.Z));
LogManager.Debug($"[智能选层] 起点({gridPos.X},{gridPos.Y}) 高程={selectedLayer.Value.Z:F3}");
adjustedPath.Add(new Point3D(point.X, point.Y, selectedLayer.Value.Z));
LogManager.Debug($"[智能选层] 起点({gridPos.X},{gridPos.Y}) Z={selectedLayer.Value.Z:F3}");
}
else
{
@ -2269,8 +2188,8 @@ namespace NavisworksTransport.PathPlanning
var selectedLayer = SelectBestLayer(cell.HeightLayers, endZ, objectHeight, startZ, endZ, i, pathWithGridCoords.Count, true);
if (selectedLayer.HasValue)
{
adjustedPath.Add(gridMap.SetWorldElevation(point, selectedLayer.Value.Z));
LogManager.Debug($"[智能选层] 终点({gridPos.X},{gridPos.Y}) 高程={selectedLayer.Value.Z:F3}");
adjustedPath.Add(new Point3D(point.X, point.Y, selectedLayer.Value.Z));
LogManager.Debug($"[智能选层] 终点({gridPos.X},{gridPos.Y}) Z={selectedLayer.Value.Z:F3}");
}
else
{
@ -2280,12 +2199,12 @@ namespace NavisworksTransport.PathPlanning
else
{
// 中间点:根据高度趋势选择最佳层
var prevZ = gridMap.GetWorldElevation(adjustedPath[i - 1]);
var prevZ = adjustedPath[i - 1].Z;
var selectedLayer = SelectBestLayer(cell.HeightLayers, prevZ, objectHeight, startZ, endZ, i, pathWithGridCoords.Count, false);
if (selectedLayer.HasValue)
{
adjustedPath.Add(gridMap.SetWorldElevation(point, selectedLayer.Value.Z));
adjustedPath.Add(new Point3D(point.X, point.Y, selectedLayer.Value.Z));
}
else
{
@ -2324,34 +2243,36 @@ namespace NavisworksTransport.PathPlanning
for (int i = 0; i < gridPath.Count; i++)
{
var gridPos = gridPath[i];
double elevation;
var worldXY = gridMap.GridToWorld3D(gridPos);
double z;
// 起点使用原始起点Z
if (i == 0)
{
elevation = gridMap.GetWorldElevation(originalStart);
LogManager.Info($"[3D还原] 起点 ({gridPos.X},{gridPos.Y}) 高程={elevation:F2}m (原始起点)");
z = originalStart.Z;
LogManager.Info($"[3D还原] 起点 ({gridPos.X},{gridPos.Y}) Z={z:F2}m (原始起点)");
}
// 终点使用原始终点Z
else if (i == gridPath.Count - 1)
{
elevation = gridMap.GetWorldElevation(originalEnd);
LogManager.Info($"[3D还原] 终点 ({gridPos.X},{gridPos.Y}) 高程={elevation:F2}m (原始终点)");
z = originalEnd.Z;
LogManager.Info($"[3D还原] 终点 ({gridPos.X},{gridPos.Y}) Z={z:F2}m (原始终点)");
}
// 中间点使用A*阶段记录的激活层
else if (activeLayerZ.TryGetValue(gridPos, out double activeZ))
{
elevation = activeZ;
LogManager.Debug($"[3D还原] 点{i} ({gridPos.X},{gridPos.Y}) 高程={elevation:F2}m (激活层)");
z = activeZ;
LogManager.Debug($"[3D还原] 点{i} ({gridPos.X},{gridPos.Y}) Z={z:F2}m (激活层)");
}
else
{
// 降级:无激活层记录,使用前一点高度
elevation = gridMap.GetWorldElevation(path3D[i - 1]);
LogManager.Warning($"[3D还原] 点{i} ({gridPos.X},{gridPos.Y}) 无激活层记录,使用前点高程={elevation:F2}m");
z = path3D[i - 1].Z;
LogManager.Warning($"[3D还原] 点{i} ({gridPos.X},{gridPos.Y}) 无激活层记录,使用前点Z={z:F2}m");
}
path3D.Add(gridMap.CreateWorldPoint(gridPos, elevation));
path3D.Add(new Point3D(worldXY.X, worldXY.Y, z));
}
LogManager.Info($"[3D还原] 完成,生成了 {path3D.Count} 个3D路径点");
@ -2380,7 +2301,7 @@ namespace NavisworksTransport.PathPlanning
if (layers.Count == 1)
{
var layer = layers[0];
if (layer.IsWalkable && layer.PassableHeight.GetSpan() >= objectHeight)
if (layer.PassableHeight.GetSpan() >= objectHeight)
return layer;
return null;
}
@ -2416,9 +2337,6 @@ namespace NavisworksTransport.PathPlanning
foreach (var layer in layers)
{
if (!layer.IsWalkable)
continue;
if (layer.PassableHeight.GetSpan() < objectHeight)
continue;
@ -2659,6 +2577,9 @@ namespace NavisworksTransport.PathPlanning
// 更新路径结果
pathResult.PathPoints = optimizedPoints;
// 🔥 步骤2在现有优化基础上应用专门的斜线优化
// 2D场景高度差为0不影响原有逻辑
// 3D场景通过高度差检查保护楼梯路径
var diagonalOptimizedPoints = ApplyDiagonalOptimization(optimizedPoints, gridMap);
if (diagonalOptimizedPoints.Count != optimizedPoints.Count)
@ -2706,7 +2627,7 @@ namespace NavisworksTransport.PathPlanning
// 初始化激活高度层字典和目标高度
var activeLayerZ = new Dictionary<GridPoint2D, double>();
double targetZ = gridMap.GetWorldElevation(endPos);
double targetZ = endPos.Z;
// 清空日志记录集合,确保每次执行都能看到映射关系
_loggedDistances.Clear();

View File

@ -147,7 +147,6 @@ namespace NavisworksTransport.PathPlanning
double minZ = double.MaxValue;
double maxZ = double.MinValue;
int walkableCount = 0;
var elevationCounts = new Dictionary<double, int>();
LogManager.Info("[通道Z值计算] 开始计算可通行网格Z值范围");
@ -169,16 +168,6 @@ namespace NavisworksTransport.PathPlanning
minZ = cellZ;
if (cellZ > maxZ)
maxZ = cellZ;
double roundedElevation = Math.Round(cellZ, 3);
if (elevationCounts.ContainsKey(roundedElevation))
{
elevationCounts[roundedElevation]++;
}
else
{
elevationCounts[roundedElevation] = 1;
}
}
}
}
@ -192,17 +181,6 @@ namespace NavisworksTransport.PathPlanning
}
LogManager.Info($"[通道Z值计算] 计算完成 - 可通行网格: {walkableCount}个, Z范围: [{minZ:F2}, {maxZ:F2}], 高度差: {maxZ - minZ:F2}");
if (elevationCounts.Count > 0)
{
string distributionSummary = string.Join(
", ",
elevationCounts
.OrderByDescending(kvp => kvp.Value)
.ThenBy(kvp => kvp.Key)
.Take(8)
.Select(kvp => $"{kvp.Key:F3}:{kvp.Value}"));
LogManager.Info($"[通道Z值计算] 可通行格高程分布样本(Top8): {distributionSummary}");
}
return (minZ, maxZ, walkableCount);
}
@ -229,9 +207,6 @@ namespace NavisworksTransport.PathPlanning
return;
}
double upwardTriangleMinElevation = double.MaxValue;
double upwardTriangleMaxElevation = double.MinValue;
// 统一投影处理
int processedTriangles = 0;
foreach (var triangle in triangles)
@ -241,17 +216,6 @@ namespace NavisworksTransport.PathPlanning
// 根据法向量决定处理方式(使用坐标系判断向上)
if (GetUpComponent(normal) > 0) // 朝上的面
{
upwardTriangleMinElevation = Math.Min(
upwardTriangleMinElevation,
Math.Min(
_coordinateSystem.GetElevation(triangle.Point1),
Math.Min(_coordinateSystem.GetElevation(triangle.Point2), _coordinateSystem.GetElevation(triangle.Point3))));
upwardTriangleMaxElevation = Math.Max(
upwardTriangleMaxElevation,
Math.Max(
_coordinateSystem.GetElevation(triangle.Point1),
Math.Max(_coordinateSystem.GetElevation(triangle.Point2), _coordinateSystem.GetElevation(triangle.Point3))));
RasterizeTriangleToGrid(gridMap, triangle, channel, channelSpeedLimit);
processedTriangles++;
}
@ -259,11 +223,6 @@ namespace NavisworksTransport.PathPlanning
}
LogManager.Info($"[通道网格构建器] 投影完成:{processedTriangles}/{triangles.Count} 个三角形");
if (processedTriangles > 0)
{
LogManager.Info(
$"[通道网格构建器] 朝上三角高程范围: [{upwardTriangleMinElevation:F2}, {upwardTriangleMaxElevation:F2}]");
}
}
/// <summary>

View File

@ -65,9 +65,8 @@ namespace NavisworksTransport.PathPlanning
var containingChannel = FindContainingChannel(position, channelItems);
if (containingChannel == null)
{
double fallbackElevation = _coordinateSystem.GetElevation(position);
LogManager.Info($"[高度检测] ❌ 位置 ({position.X:F2}, {position.Y:F2}, {position.Z:F2}) 未找到包含的通道,使用当前宿主高程: {fallbackElevation:F2}");
return fallbackElevation;
LogManager.Info($"[高度检测] ❌ 位置 ({position.X:F2}, {position.Y:F2}) 未找到包含的通道使用原始Z坐标: {position.Z:F2}");
return position.Z;
}
//LogManager.Info($"[高度检测] ✅ 找到包含通道: {containingChannel.DisplayName}");
@ -102,7 +101,7 @@ namespace NavisworksTransport.PathPlanning
catch (Exception ex)
{
LogManager.Error($"[高度检测] 检测高度时发生错误: {ex.Message}");
return _coordinateSystem.GetElevation(position);
return position.Z; // 发生错误时使用原始Z坐标
}
}
@ -128,9 +127,8 @@ namespace NavisworksTransport.PathPlanning
var containingChannel = FindContainingChannel(position, channelItems);
if (containingChannel == null)
{
double fallbackElevation = _coordinateSystem.GetElevation(position);
LogManager.Info($"[高度检测] ❌ 位置 ({position.X:F2}, {position.Y:F2}, {position.Z:F2}) 未找到包含的通道,使用当前宿主高程: {fallbackElevation:F2}");
return fallbackElevation;
LogManager.Info($"[高度检测] ❌ 位置 ({position.X:F2}, {position.Y:F2}) 未找到包含的通道使用原始Z坐标: {position.Z:F2}");
return position.Z;
}
//LogManager.Info($"[高度检测] ✅ 找到包含通道: {containingChannel.DisplayName}");
@ -161,20 +159,19 @@ namespace NavisworksTransport.PathPlanning
var bounds = containingChannel.BoundingBox();
if (bounds != null)
{
var (_, fallbackTopHeight) = GetBoundsElevationRange(bounds, _coordinateSystem);
var fallbackTopHeight = bounds.Max.Z;
LogManager.Info($"[高度检测] 使用边界框顶面高度: {fallbackTopHeight:F2}");
return fallbackTopHeight;
}
// 最后的备选方案
double finalFallbackElevation = _coordinateSystem.GetElevation(position);
LogManager.Warning($"[高度检测] ⚠️ 无法获取通道顶面高度,使用当前宿主高程: {finalFallbackElevation:F2}");
return finalFallbackElevation;
LogManager.Warning($"[高度检测] ⚠️ 无法获取通道顶面高度使用原始Z坐标: {position.Z:F2}");
return position.Z;
}
catch (Exception ex)
{
LogManager.Error($"[高度检测] 检测顶面高度时发生错误: {ex.Message}");
return _coordinateSystem.GetElevation(position);
return position.Z; // 发生错误时使用原始Z坐标
}
}
@ -201,13 +198,11 @@ namespace NavisworksTransport.PathPlanning
return null;
}
var (floorHeight, ceilingHeight) = GetBoundsElevationRange(bounds, _coordinateSystem);
// 创建高度信息对象
var heightInfo = new ChannelHeightInfo
{
FloorHeight = floorHeight,
CeilingHeight = ceilingHeight,
FloorHeight = bounds.Min.Z,
CeilingHeight = bounds.Max.Z,
Type = ChannelType.Other, // 不再依赖类型判断
HeightProfile = new List<HeightSample>()
};
@ -260,7 +255,11 @@ namespace NavisworksTransport.PathPlanning
return false;
var bounds = channel.Geometry.BoundingBox;
return IsPositionWithinBounds(position, bounds, _coordinateSystem, 2.0);
// 检查XY平面是否在边界内Z方向放宽检查
return position.X >= bounds.Min.X && position.X <= bounds.Max.X &&
position.Y >= bounds.Min.Y && position.Y <= bounds.Max.Y &&
position.Z >= bounds.Min.Z - 2.0 && position.Z <= bounds.Max.Z + 2.0; // Z方向容差2米
}
catch
{
@ -284,14 +283,14 @@ namespace NavisworksTransport.PathPlanning
for (int i = 0; i < sampleCount; i++)
{
double ratio = (double)i / (sampleCount - 1);
var samplePosition = CreateHeightProfileSamplePoint(bounds, _coordinateSystem, ratio);
var (floorHeight, ceilingHeight) = GetBoundsElevationRange(bounds, _coordinateSystem);
var sampleX = bounds.Min.X + (bounds.Max.X - bounds.Min.X) * ratio;
var sampleY = bounds.Min.Y + (bounds.Max.Y - bounds.Min.Y) * ratio;
var sample = new HeightSample
{
Position = samplePosition,
FloorHeight = floorHeight,
CeilingHeight = ceilingHeight
Position = new Point3D(sampleX, sampleY, bounds.Min.Z),
FloorHeight = bounds.Min.Z,
CeilingHeight = bounds.Max.Z
};
heightInfo.HeightProfile.Add(sample);
@ -347,7 +346,7 @@ namespace NavisworksTransport.PathPlanning
// 找到最近的两个采样点进行插值
var closestSamples = heightProfile
.OrderBy(s => CalculatePlanarDistance(s.Position, position, _coordinateSystem))
.OrderBy(s => Math.Sqrt(Math.Pow(s.Position.X - position.X, 2) + Math.Pow(s.Position.Y - position.Y, 2)))
.Take(2)
.ToList();
@ -360,8 +359,8 @@ namespace NavisworksTransport.PathPlanning
var sample1 = closestSamples[0];
var sample2 = closestSamples[1];
var distance1 = CalculatePlanarDistance(sample1.Position, position, _coordinateSystem);
var distance2 = CalculatePlanarDistance(sample2.Position, position, _coordinateSystem);
var distance1 = Math.Sqrt(Math.Pow(sample1.Position.X - position.X, 2) + Math.Pow(sample1.Position.Y - position.Y, 2));
var distance2 = Math.Sqrt(Math.Pow(sample2.Position.X - position.X, 2) + Math.Pow(sample2.Position.Y - position.Y, 2));
var totalDistance = distance1 + distance2;
if (totalDistance < 0.001) return sample1.CeilingHeight;
@ -387,29 +386,29 @@ namespace NavisworksTransport.PathPlanning
{
var bbox = channel.Geometry.BoundingBox;
if (IsPositionWithinBounds(position, bbox, _coordinateSystem, 0.0))
// 检查位置是否在通道的X,Y范围内
if (position.X >= bbox.Min.X && position.X <= bbox.Max.X &&
position.Y >= bbox.Min.Y && position.Y <= bbox.Max.Y)
{
// 位置在通道范围内,尝试分析表面高度
var (floorHeight, ceilingHeight) = GetBoundsElevationRange(bbox, _coordinateSystem);
var surfaceHeight = AnalyzeSurfaceHeightAtPosition(position, channel, floorHeight, ceilingHeight);
var surfaceHeight = AnalyzeSurfaceHeightAtPosition(position, channel, bbox.Min.Z, bbox.Max.Z);
LogManager.Debug($"[射线投射] 表面高度分析结果: {surfaceHeight:F2}");
return surfaceHeight;
}
else
{
LogManager.Warning($"[射线投射] 位置({position.X:F2}, {position.Y:F2}, {position.Z:F2})不在通道范围内");
return _coordinateSystem.GetElevation(position);
LogManager.Warning($"[射线投射] 位置({position.X:F2}, {position.Y:F2})不在通道范围内");
return position.Z;
}
}
double fallbackElevation = _coordinateSystem.GetElevation(position);
LogManager.Warning($"[射线投射] 通道几何信息无效,使用当前宿主高程: {fallbackElevation:F2}");
return fallbackElevation;
LogManager.Warning($"[射线投射] 通道几何信息无效使用原始Z坐标: {position.Z:F2}");
return position.Z;
}
catch (Exception ex)
{
LogManager.Error($"[射线投射] 射线投射计算失败: {ex.Message}");
return _coordinateSystem.GetElevation(position);
return position.Z;
}
}
@ -490,11 +489,7 @@ namespace NavisworksTransport.PathPlanning
// 验证拾取到的模型是否为目标通道(或其子项)
if (pickResult.ModelItem.Equals(channel) || ModelItemAnalysisHelper.IsChildOf(pickResult.ModelItem, channel))
{
height = GetPickedSurfaceElevation(
pickResult.Point.X,
pickResult.Point.Y,
pickResult.Point.Z,
_coordinateSystem.Type);
height = pickResult.Point.Z;
LogManager.Info($"[几何分析] ✅ 使用投影法获得精确表面高度: {height:F2},通道: {channel.DisplayName}");
return true;
}
@ -561,14 +556,9 @@ namespace NavisworksTransport.PathPlanning
/// </summary>
private Point3D CalculateRelativePositionInChannel(Point3D position, BoundingBox3D bbox)
{
var (positionH1, positionH2) = _coordinateSystem.GetHorizontalCoords(position);
var (minH1, maxH1, minH2, maxH2) = _coordinateSystem.GetHorizontalRange(bbox);
var (minElevation, maxElevation) = GetBoundsElevationRange(bbox, _coordinateSystem);
var relativeX = Math.Abs(maxH1 - minH1) < 1e-9 ? 0.0 : (positionH1 - minH1) / (maxH1 - minH1);
var relativeY = Math.Abs(maxH2 - minH2) < 1e-9 ? 0.0 : (positionH2 - minH2) / (maxH2 - minH2);
var currentElevation = _coordinateSystem.GetElevation(position);
var relativeZ = Math.Abs(maxElevation - minElevation) < 1e-9 ? 0.0 : (currentElevation - minElevation) / (maxElevation - minElevation);
var relativeX = (position.X - bbox.Min.X) / (bbox.Max.X - bbox.Min.X);
var relativeY = (position.Y - bbox.Min.Y) / (bbox.Max.Y - bbox.Min.Y);
var relativeZ = 0.0; // Z方向待计算
return new Point3D(relativeX, relativeY, relativeZ);
}
@ -593,9 +583,8 @@ namespace NavisworksTransport.PathPlanning
private string GenerateCacheKey(ModelItem channel, Point3D position)
{
// 使用通道的实例ID和位置的网格坐标作为缓存键
var (h1, h2) = _coordinateSystem.GetHorizontalCoords(position);
var gridX = (int)(h1 / 1.0);
var gridY = (int)(h2 / 1.0);
var gridX = (int)(position.X / 1.0); // 1米网格
var gridY = (int)(position.Y / 1.0);
return $"{channel.InstanceGuid}_{gridX}_{gridY}";
}
@ -643,7 +632,7 @@ namespace NavisworksTransport.PathPlanning
foreach (var testHeight in testHeights)
{
var testPoint = _coordinateSystem.SetElevation(position, testHeight);
var testPoint = new Point3D(position.X, position.Y, testHeight);
// 将3D点投影到屏幕
if (TryProjectWorldToScreen(testPoint, activeView, out GridPoint2D screenPoint))
@ -656,9 +645,8 @@ namespace NavisworksTransport.PathPlanning
var pickResult = activeView.PickItemFromPoint(screenPoint.X, screenPoint.Y);
if (pickResult != null && (pickResult.ModelItem.Equals(channel) || ModelItemAnalysisHelper.IsChildOf(pickResult.ModelItem, channel)))
{
double detectedElevation = _coordinateSystem.GetElevation(pickResult.Point);
LogManager.Info($"[射线投射] ✅ 成功检测到表面点高程: {detectedElevation:F2}");
return detectedElevation;
LogManager.Info($"[射线投射] ✅ 成功检测到表面点: Z={pickResult.Point.Z:F2}");
return pickResult.Point.Z;
}
}
}
@ -802,11 +790,11 @@ namespace NavisworksTransport.PathPlanning
var sampleRadius = 50.0; // 50单位半径内采样
var samplePoints = new[]
{
CreatePlanarOffsetPoint(position, _coordinateSystem, 0.0, 0.0),
CreatePlanarOffsetPoint(position, _coordinateSystem, sampleRadius, 0.0),
CreatePlanarOffsetPoint(position, _coordinateSystem, -sampleRadius, 0.0),
CreatePlanarOffsetPoint(position, _coordinateSystem, 0.0, sampleRadius),
CreatePlanarOffsetPoint(position, _coordinateSystem, 0.0, -sampleRadius),
new Point3D(position.X, position.Y, 0), // 中心点
new Point3D(position.X + sampleRadius, position.Y, 0), // 右
new Point3D(position.X - sampleRadius, position.Y, 0), // 左
new Point3D(position.X, position.Y + sampleRadius, 0), // 上
new Point3D(position.X, position.Y - sampleRadius, 0), // 下
};
foreach (var samplePoint in samplePoints)
@ -819,9 +807,8 @@ namespace NavisworksTransport.PathPlanning
var pickResult = activeView.PickItemFromPoint((int)screenPoint.X, (int)screenPoint.Y);
if (pickResult != null && (pickResult.ModelItem.Equals(channel) || ModelItemAnalysisHelper.IsChildOf(pickResult.ModelItem, channel)))
{
double detectedElevation = _coordinateSystem.GetElevation(pickResult.Point);
validHeights.Add(detectedElevation);
LogManager.Debug($"[多点采样] 采样点({samplePoint.X:F1}, {samplePoint.Y:F1}, {samplePoint.Z:F1})高程: {detectedElevation:F2}");
validHeights.Add(pickResult.Point.Z);
LogManager.Debug($"[多点采样] 采样点({samplePoint.X:F1}, {samplePoint.Y:F1})高度: {pickResult.Point.Z:F2}");
}
}
}
@ -845,132 +832,6 @@ namespace NavisworksTransport.PathPlanning
return null;
}
}
private static (double min, double max) GetBoundsElevationRange(BoundingBox3D bounds, ICoordinateSystem coordinateSystem)
{
if (bounds == null)
{
throw new ArgumentNullException(nameof(bounds));
}
if (coordinateSystem == null)
{
throw new ArgumentNullException(nameof(coordinateSystem));
}
return GetBoundsElevationRange(
bounds.Min.X, bounds.Min.Y, bounds.Min.Z,
bounds.Max.X, bounds.Max.Y, bounds.Max.Z,
coordinateSystem.Type);
}
private static Point3D CreateHeightProfileSamplePoint(BoundingBox3D bounds, ICoordinateSystem coordinateSystem, double ratio)
{
if (bounds == null)
{
throw new ArgumentNullException(nameof(bounds));
}
if (coordinateSystem == null)
{
throw new ArgumentNullException(nameof(coordinateSystem));
}
return CreateHeightProfileSamplePoint(
bounds.Min.X, bounds.Min.Y, bounds.Min.Z,
bounds.Max.X, bounds.Max.Y, bounds.Max.Z,
coordinateSystem.Type,
ratio);
}
private static bool IsPositionWithinBounds(Point3D position, BoundingBox3D bounds, ICoordinateSystem coordinateSystem, double elevationTolerance)
{
if (position == null || bounds == null || coordinateSystem == null)
{
return false;
}
var (positionH1, positionH2) = coordinateSystem.GetHorizontalCoords(position);
var (minH1, maxH1, minH2, maxH2) = coordinateSystem.GetHorizontalRange(bounds);
var (minElevation, maxElevation) = coordinateSystem.GetHeightRange(bounds);
var elevation = coordinateSystem.GetElevation(position);
return positionH1 >= minH1 && positionH1 <= maxH1 &&
positionH2 >= minH2 && positionH2 <= maxH2 &&
elevation >= minElevation - elevationTolerance &&
elevation <= maxElevation + elevationTolerance;
}
private static double CalculatePlanarDistance(Point3D pointA, Point3D pointB, ICoordinateSystem coordinateSystem)
{
var (aH1, aH2) = coordinateSystem.GetHorizontalCoords(pointA);
var (bH1, bH2) = coordinateSystem.GetHorizontalCoords(pointB);
return Math.Sqrt(Math.Pow(aH1 - bH1, 2) + Math.Pow(aH2 - bH2, 2));
}
private static Point3D CreatePlanarOffsetPoint(Point3D position, ICoordinateSystem coordinateSystem, double deltaH1, double deltaH2)
{
var (h1, h2) = coordinateSystem.GetHorizontalCoords(position);
var elevation = coordinateSystem.GetElevation(position);
return coordinateSystem.CreatePoint(h1 + deltaH1, h2 + deltaH2, elevation);
}
private static (double min, double max) GetBoundsElevationRange(
double minX, double minY, double minZ,
double maxX, double maxY, double maxZ,
CoordinateSystemType coordinateSystemType)
{
var adapter = new HostCoordinateAdapter(coordinateSystemType);
double elevation1 = HostPlanarGridHelper.GetElevation3(
new System.Numerics.Vector3((float)minX, (float)minY, (float)minZ),
adapter);
double elevation2 = HostPlanarGridHelper.GetElevation3(
new System.Numerics.Vector3((float)maxX, (float)maxY, (float)maxZ),
adapter);
return (Math.Min(elevation1, elevation2), Math.Max(elevation1, elevation2));
}
private static Point3D CreateHeightProfileSamplePoint(
double minX, double minY, double minZ,
double maxX, double maxY, double maxZ,
CoordinateSystemType coordinateSystemType,
double ratio)
{
var samplePoint = CreateHeightProfileSamplePoint3(
minX, minY, minZ,
maxX, maxY, maxZ,
coordinateSystemType,
ratio);
return new Point3D(samplePoint.X, samplePoint.Y, samplePoint.Z);
}
private static System.Numerics.Vector3 CreateHeightProfileSamplePoint3(
double minX, double minY, double minZ,
double maxX, double maxY, double maxZ,
CoordinateSystemType coordinateSystemType,
double ratio)
{
var adapter = new HostCoordinateAdapter(coordinateSystemType);
var hostMin = new System.Numerics.Vector3((float)minX, (float)minY, (float)minZ);
var hostMax = new System.Numerics.Vector3((float)maxX, (float)maxY, (float)maxZ);
var (minH1, maxH1, minH2, maxH2) = HostPlanarGridHelper.GetHorizontalRange3(hostMin, hostMax, adapter);
var (minElevation, _) = GetBoundsElevationRange(minX, minY, minZ, maxX, maxY, maxZ, coordinateSystemType);
double sampleH1 = minH1 + (maxH1 - minH1) * ratio;
double sampleH2 = minH2 + (maxH2 - minH2) * ratio;
return HostPlanarGridHelper.CreateHostPoint3(sampleH1, sampleH2, minElevation, adapter);
}
private static double GetPickedSurfaceElevation(
double pointX,
double pointY,
double pointZ,
CoordinateSystemType coordinateSystemType)
{
var adapter = new HostCoordinateAdapter(coordinateSystemType);
return HostPlanarGridHelper.GetElevation3(
new System.Numerics.Vector3((float)pointX, (float)pointY, (float)pointZ),
adapter);
}
}
/// <summary>
@ -1051,4 +912,4 @@ namespace NavisworksTransport.PathPlanning
Other
}
}
}

View File

@ -22,11 +22,6 @@ namespace NavisworksTransport.PathPlanning
/// 当前使用的坐标系
/// </summary>
private readonly ICoordinateSystem _coordinateSystem;
/// <summary>
/// 当前网格采用的宿主坐标系类型。
/// </summary>
public CoordinateSystemType CoordinateSystemType => _coordinateSystem.Type;
/// <summary>
/// 网格宽度(单元格数量)
/// </summary>
@ -142,36 +137,6 @@ namespace NavisworksTransport.PathPlanning
LogManager.Info($"[网格地图] 创建完成: {Width}x{Height}, 坐标系: {_coordinateSystem.Type}");
}
/// <summary>
/// 轻量构造函数,主要用于单元测试与脱离 Navisworks 原生边界句柄的纯坐标语义验证。
/// </summary>
public GridMap(Point3D origin, int width, int height, double cellSize, ICoordinateSystem coordinateSystem)
{
if (origin == null)
throw new ArgumentNullException(nameof(origin));
if (width <= 0)
throw new ArgumentException("网格宽度必须大于0", nameof(width));
if (height <= 0)
throw new ArgumentException("网格高度必须大于0", nameof(height));
if (cellSize <= 0)
throw new ArgumentException("网格单元格大小必须大于0", nameof(cellSize));
if (coordinateSystem == null)
throw new ArgumentNullException(nameof(coordinateSystem));
Origin = origin;
Width = width;
Height = height;
CellSize = cellSize;
_coordinateSystem = coordinateSystem;
Bounds = null;
Cells = new GridCell[Width, Height];
InitializeCells();
InitializeHeightCalculation();
LogManager.Info($"[网格地图] 轻量创建完成: {Width}x{Height}, 坐标系: {_coordinateSystem.Type}");
}
/// <summary>
/// 初始化高度计算组件
/// </summary>
@ -230,11 +195,10 @@ namespace NavisworksTransport.PathPlanning
}
/// <summary>
/// 将网格坐标转换为世界2D坐标纯坐标转换不涉及高度计算
/// 当前网格索引语义是采样节点WorldToGrid 使用 Round 将点归属到最近节点。
/// 将网格坐标转换为世界2D坐标纯坐标转换不涉及高度计算
/// </summary>
/// <param name="gridPosition">网格坐标</param>
/// <returns>世界2D坐标点网格采样节点高度设为0</returns>
/// <returns>世界2D坐标点网格左下角高度设为0</returns>
public Point3D GridToWorld2D(GridPoint2D gridPosition)
{
var (originH1, originH2) = _coordinateSystem.GetHorizontalCoords(Origin);
@ -266,174 +230,6 @@ namespace NavisworksTransport.PathPlanning
return _coordinateSystem.SetElevation(world2D, elevation);
}
/// <summary>
/// 获取宿主坐标点在当前网格坐标语义下的高程值。
/// 注意:
/// - 对 ZUp 文档,高程是 point.Z
/// - 对 YUp 文档,高程是 point.Y
/// </summary>
public double GetWorldElevation(Point3D worldPoint)
{
if (worldPoint == null)
{
throw new ArgumentNullException(nameof(worldPoint));
}
return _coordinateSystem.GetElevation(worldPoint);
}
/// <summary>
/// 在当前网格坐标语义下,为一个宿主点设置高程。
/// </summary>
public Point3D SetWorldElevation(Point3D worldPoint, double elevation)
{
if (worldPoint == null)
{
throw new ArgumentNullException(nameof(worldPoint));
}
return _coordinateSystem.SetElevation(worldPoint, elevation);
}
/// <summary>
/// 根据网格坐标和指定高程创建宿主坐标点。
/// </summary>
public Point3D CreateWorldPoint(GridPoint2D gridPosition, double elevation)
{
Point3D world2D = GridToWorld2D(gridPosition);
return _coordinateSystem.SetElevation(world2D, elevation);
}
/// <summary>
/// 获取网格索引在宿主水平平面上的最近节点归属边界。
/// 该边界必须与 WorldToGrid 的 Round 语义一致:索引点前后各半个 CellSize。
/// </summary>
public (double minH1, double maxH1, double minH2, double maxH2) GetGridCellPlanarBounds(GridPoint2D gridPosition)
{
Point3D centerPoint = GridToWorld2D(gridPosition);
var (centerH1, centerH2) = _coordinateSystem.GetHorizontalCoords(centerPoint);
return CalculateNearestNodePlanarBounds(centerH1, centerH2, CellSize);
}
/// <summary>
/// 检查宿主空间线段是否穿过指定网格归属区域的内部。
/// 只检查宿主水平投影;返回相交线段的参数范围,便于调用方按高度层再判定可通行性。
/// </summary>
public bool TryGetSegmentGridCellInteriorIntersection(
Point3D start,
Point3D end,
GridPoint2D gridPosition,
out double enterT,
out double exitT)
{
if (start == null)
throw new ArgumentNullException(nameof(start));
if (end == null)
throw new ArgumentNullException(nameof(end));
enterT = 0.0;
exitT = 1.0;
var (startH1, startH2) = _coordinateSystem.GetHorizontalCoords(start);
var (endH1, endH2) = _coordinateSystem.GetHorizontalCoords(end);
var (minH1, maxH1, minH2, maxH2) = GetGridCellPlanarBounds(gridPosition);
return TryGetSegmentRectangleInteriorIntersection(
startH1,
startH2,
endH1,
endH2,
minH1,
maxH1,
minH2,
maxH2,
CellSize,
out enterT,
out exitT);
}
public static (double minH1, double maxH1, double minH2, double maxH2) CalculateNearestNodePlanarBounds(
double centerH1,
double centerH2,
double cellSize)
{
double halfCell = cellSize * 0.5;
return (
centerH1 - halfCell,
centerH1 + halfCell,
centerH2 - halfCell,
centerH2 + halfCell);
}
public static bool TryGetSegmentRectangleInteriorIntersection(
double startH1,
double startH2,
double endH1,
double endH2,
double minH1,
double maxH1,
double minH2,
double maxH2,
double cellSize,
out double enterT,
out double exitT)
{
enterT = 0.0;
exitT = 1.0;
double epsilon = Math.Max(cellSize * 1e-9, 1e-9);
minH1 += epsilon;
maxH1 -= epsilon;
minH2 += epsilon;
maxH2 -= epsilon;
if (minH1 > maxH1 || minH2 > maxH2)
{
return false;
}
double directionH1 = endH1 - startH1;
double directionH2 = endH2 - startH2;
return ClipSegmentToAxis(startH1, directionH1, minH1, maxH1, ref enterT, ref exitT) &&
ClipSegmentToAxis(startH2, directionH2, minH2, maxH2, ref enterT, ref exitT) &&
enterT <= exitT;
}
private static bool ClipSegmentToAxis(
double start,
double direction,
double min,
double max,
ref double enterT,
ref double exitT)
{
const double parallelTolerance = 1e-12;
if (Math.Abs(direction) < parallelTolerance)
{
return start >= min && start <= max;
}
double t1 = (min - start) / direction;
double t2 = (max - start) / direction;
if (t1 > t2)
{
double temp = t1;
t1 = t2;
t2 = temp;
}
if (t1 > enterT)
enterT = t1;
if (t2 < exitT)
exitT = t2;
return enterT <= exitT;
}
/// <summary>
@ -548,7 +344,7 @@ namespace NavisworksTransport.PathPlanning
/// <param name="z">Z坐标</param>
/// <param name="tolerance">容差默认0.1m</param>
/// <returns>符合条件的高度层如果没有则返回null</returns>
public HeightLayer? FindLayerContainingElevation(GridPoint2D gridPosition, double elevation, double tolerance = DEFAULT_LAYER_TOLERANCE_METERS)
public HeightLayer? FindLayerContainingZ(GridPoint2D gridPosition, double z, double tolerance = DEFAULT_LAYER_TOLERANCE_METERS)
{
if (!IsValidGridPosition(gridPosition))
return null;
@ -563,7 +359,7 @@ namespace NavisworksTransport.PathPlanning
foreach (var layer in cell.HeightLayers)
{
double distance = Math.Abs(layer.Z - elevation);
double distance = Math.Abs(layer.Z - z);
if (distance < minDistance && distance <= tolerance)
{
minDistance = distance;
@ -574,11 +370,6 @@ namespace NavisworksTransport.PathPlanning
return bestLayer;
}
public HeightLayer? FindLayerContainingZ(GridPoint2D gridPosition, double z, double tolerance = DEFAULT_LAYER_TOLERANCE_METERS)
{
return FindLayerContainingElevation(gridPosition, z, tolerance);
}
/// <summary>
/// 获取指定网格位置的所有高度层
/// </summary>
@ -650,7 +441,7 @@ namespace NavisworksTransport.PathPlanning
/// <param name="zCoord">Z坐标模型单位</param>
/// <param name="tolerance">Z坐标容差模型单位默认0.1</param>
/// <returns>是否可通行</returns>
public bool IsPassableAtElevation(GridPoint2D gridPosition, double elevation, double tolerance = DEFAULT_LAYER_TOLERANCE_METERS)
public bool IsPassableAt3DPoint(GridPoint2D gridPosition, double zCoord, double tolerance = DEFAULT_LAYER_TOLERANCE_METERS)
{
if (!IsValidGridPosition(gridPosition))
return false;
@ -672,7 +463,7 @@ namespace NavisworksTransport.PathPlanning
double layerMinZ = layer.Z;
double layerMaxZ = layer.Z + layer.PassableHeight.GetSpan();
if (elevation >= layerMinZ - tolerance && elevation <= layerMaxZ + tolerance)
if (zCoord >= layerMinZ - tolerance && zCoord <= layerMaxZ + tolerance)
{
return true;
}
@ -682,11 +473,6 @@ namespace NavisworksTransport.PathPlanning
return false;
}
public bool IsPassableAt3DPoint(GridPoint2D gridPosition, double zCoord, double tolerance = DEFAULT_LAYER_TOLERANCE_METERS)
{
return IsPassableAtElevation(gridPosition, zCoord, tolerance);
}
/// <summary>
/// 获取所有通道类型的网格单元
/// </summary>

View File

@ -1,11 +1,9 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using Autodesk.Navisworks.Api;
using NavisworksTransport.Utils;
using NavisworksTransport.Core.Config;
using NavisworksTransport.Utils.CoordinateSystem;
namespace NavisworksTransport.PathPlanning
{
@ -281,13 +279,8 @@ namespace NavisworksTransport.PathPlanning
string heightLimitStr = CategoryAttributeManager.GetLogisticsPropertyValue(doorItem, CategoryAttributeManager.LogisticsProperties.HEIGHT_LIMIT);
double configuredHeight = CategoryAttributeManager.ParseLogisticsLimitValue(heightLimitStr, "限高");
var (doorBottomElevation, doorTopElevation) = GetObstacleElevationRange(
bbox.Min.X, bbox.Min.Y, bbox.Min.Z,
bbox.Max.X, bbox.Max.Y, bbox.Max.Z,
gridMap.CoordinateSystemType);
// 计算门的实际物理高度
double actualDoorHeight = doorTopElevation - doorBottomElevation;
double actualDoorHeight = bbox.Max.Z - bbox.Min.Z;
double doorHeight;
if (configuredHeight <= 0)
@ -307,6 +300,9 @@ namespace NavisworksTransport.PathPlanning
LogManager.Debug($"【门元素处理】门元素 {doorItem.DisplayName} 实际高度: {actualDoorHeight:F2}, 限高:{configuredHeightInModelUnits:F2}, 有效高度: {doorHeight:F2}");
}
// 获取门底部的Z坐标
double doorBottomZ = bbox.Min.Z;
// 获取门的限速
double doorSpeedLimit = CategoryAttributeManager.GetSpeedLimit(doorItem);
LogManager.Info($"[门网格限速] 门物品 '{doorItem.DisplayName}' 限速: {doorSpeedLimit:F2}m/s");
@ -318,7 +314,7 @@ namespace NavisworksTransport.PathPlanning
// 计算门网格的精确世界坐标
var world2D = gridMap.GridToWorld2D(gridPos);
var preciseWorldPosition = gridMap.SetWorldElevation(world2D, doorBottomElevation);
var preciseWorldPosition = new Point3D(world2D.X, world2D.Y, doorBottomZ);
// 使用GridCellBuilder创建完整配置的门GridCell
var cell = GridCellBuilder.Door(preciseWorldPosition, doorItem, doorSpeedLimit);
@ -335,15 +331,15 @@ namespace NavisworksTransport.PathPlanning
var doorLayer = new HeightLayer
{
Type = "门",
Z = doorBottomElevation,
PassableHeight = new HeightInterval(doorBottomElevation, doorBottomElevation + doorHeight),
Z = doorBottomZ,
PassableHeight = new HeightInterval(doorBottomZ, doorBottomZ + doorHeight),
IsWalkable = true, // 关键:门是可通行的
SpeedLimit = doorSpeedLimit,
SourceItem = doorItem,
IsBoundary = false
};
gridMap.AddHeightLayer(gridPos, doorLayer);
LogManager.Info($"[门高度层添加] 网格({gridPos.X},{gridPos.Y}) 高度范围: [{doorBottomElevation:F2}, {doorBottomElevation + doorHeight:F2}], 可通行高度: {doorHeight:F2}");
LogManager.Info($"[门高度层添加] 网格({gridPos.X},{gridPos.Y}) 高度范围: [{doorBottomZ:F2}, {doorBottomZ + doorHeight:F2}], 可通行高度: {doorHeight:F2}");
}
processedCount++;
@ -542,13 +538,7 @@ namespace NavisworksTransport.PathPlanning
else
{
// 邻居无层Unknown使用网格底部Z值
neighborZ = gridMap.Bounds != null
? ResolveBoundsMinElevation(
gridMap.Bounds.Min.X,
gridMap.Bounds.Min.Y,
gridMap.Bounds.Min.Z,
gridMap.CoordinateSystemType)
: 0.0;
neighborZ = gridMap.Bounds.Min.Z;
}
// 计算层间高度差
@ -634,9 +624,9 @@ namespace NavisworksTransport.PathPlanning
// 重要修复:使用通道顶面作为垂直扫描的起点,而不是底面
// 从通道构建器的日志可知通道总边界MaxZ = 38.8,这是正确的扫描起点
double channelTopElevation = GetChannelTopZ(worldPos, gridMap);
points.Add(gridMap.SetWorldElevation(worldPos, channelTopElevation));
double channelTopZ = GetChannelTopZ(worldPos, gridMap);
points.Add(new Point3D(worldPos.X, worldPos.Y, channelTopZ));
}
}
}
@ -654,25 +644,28 @@ namespace NavisworksTransport.PathPlanning
{
try
{
if (gridMap.Bounds != null)
// 从通道构建器的日志信息可知:
// 通道总边界: [-242.7,-62.3,35.4] - [10.8,70.9,38.8]
// 通道顶面Z坐标为38.8,这应该作为垂直扫描的起点
// 方法1: 优先使用网格边界的MaxZ通道顶面
double channelTopZ = gridMap.Bounds.Max.Z;
// 方法2: 备选方案 - 如果网格边界不可用,使用固定的通道高度
if (channelTopZ <= gridMap.Bounds.Min.Z)
{
var (minElevation, maxElevation) =
GetObstacleElevationRange(gridMap.Bounds.Min, gridMap.Bounds.Max, gridMap);
if (maxElevation > minElevation)
{
return maxElevation;
}
// 基于日志观察到的通道Z范围 [35.4, 38.8],使用顶面
channelTopZ = worldPos.Z + 3.4; // 假设通道高度为3.4米
LogManager.Debug($"[通道顶面计算] 使用备选方案: 底面{worldPos.Z:F2} + 3.4m = 顶面{channelTopZ:F2}");
}
double fallbackTopElevation = gridMap.GetWorldElevation(worldPos) + 3.4;
LogManager.Debug($"[通道顶面计算] 使用备选方案: 底面高程{gridMap.GetWorldElevation(worldPos):F2} + 3.4m = 顶面高程{fallbackTopElevation:F2}");
return fallbackTopElevation;
// LogManager.Debug($"[通道顶面计算] 世界位置({worldPos.X:F2}, {worldPos.Y:F2}) -> 通道顶面Z={channelTopZ:F2}"); // 调试完成,日志已删除
return channelTopZ;
}
catch (Exception ex)
{
LogManager.Warning($"[通道顶面计算] 计算失败: {ex.Message},使用世界位置高程");
return gridMap.GetWorldElevation(worldPos);
LogManager.Warning($"[通道顶面计算] 计算失败: {ex.Message},使用世界位置Z坐标");
return worldPos.Z;
}
}
@ -693,7 +686,7 @@ namespace NavisworksTransport.PathPlanning
var interval = kvp.Value;
// 将世界坐标转换为网格坐标
var gridPos = gridMap.WorldToGrid(point);
var gridPos = gridMap.WorldToGrid(new Point3D(point.X, point.Y, 0));
int gridX = gridPos.X;
int gridY = gridPos.Y;
@ -1313,10 +1306,7 @@ namespace NavisworksTransport.PathPlanning
var filterElapsed = (DateTime.Now - filterStart).TotalMilliseconds;
LogManager.Info($"[高性能障碍物处理] 阶段3完成: 从缓存中筛选出 {validItems.Count} 个有效项,耗时: {filterElapsed:F1}ms");
int horizontalOverlapCount = validItems.Count(kvp => IntersectsGridPlanarExtents(kvp.Value.BoundingBox, gridMap));
LogManager.Info($"[高性能障碍物处理] 阶段3.5: 与当前网格平面范围相交的有效几何体: {horizontalOverlapCount}/{validItems.Count}");
// 获取可通行网格的实际高度范围信息
var walkableMinZ = channelCoverage.WalkableMinZ;
var walkableMaxZ = channelCoverage.WalkableMaxZ;
@ -1335,12 +1325,10 @@ namespace NavisworksTransport.PathPlanning
{
// 精确高度检查:使用几何体中心点对应的网格高度
var bbox = kvp.Value.BoundingBox;
var centerPoint = new Point3D(
(bbox.Min.X + bbox.Max.X) / 2,
(bbox.Min.Y + bbox.Max.Y) / 2,
(bbox.Min.Z + bbox.Max.Z) / 2);
var centerX = (bbox.Min.X + bbox.Max.X) / 2;
var centerY = (bbox.Min.Y + bbox.Max.Y) / 2;
var centerGridPos = gridMap.WorldToGrid(centerPoint);
var centerGridPos = gridMap.WorldToGrid(new Point3D(centerX, centerY, 0));
if (!gridMap.IsValidGridPosition(centerGridPos))
{
return false;
@ -1354,9 +1342,7 @@ namespace NavisworksTransport.PathPlanning
var scanMax = walkableMaxZ + scanHeightInModelUnits;
// 只有与该网格高度范围重叠的几何体才进入处理
var (obstacleMinElevation, obstacleMaxElevation) =
GetObstacleElevationRange(bbox.Min, bbox.Max, gridMap);
bool isInRange = !(obstacleMaxElevation <= scanMin || obstacleMinElevation > scanMax);
bool isInRange = !(bbox.Max.Z <= scanMin || bbox.Min.Z > scanMax);
return isInRange;
})
@ -1380,12 +1366,9 @@ namespace NavisworksTransport.PathPlanning
var gridCalcElapsed = (DateTime.Now - gridCalcStart).TotalMilliseconds;
var totalCheckedItems = validItems.Count;
var filteredItems = totalCheckedItems - heightCheckedItems.Count;
int overlappingItemsRetained = heightCheckedItems.Count(kvp => IntersectsGridPlanarExtents(kvp.Value.BoundingBox, gridMap));
int overlappingItemsFiltered = validItems.Count(kvp => IntersectsGridPlanarExtents(kvp.Value.BoundingBox, gridMap)) - overlappingItemsRetained;
LogManager.Info($"[高性能障碍物处理] 阶段4完成: 生成 {gridUpdates.Count} 个网格更新操作,耗时: {gridCalcElapsed:F1}ms");
LogManager.Info($"[精确高度检查] 检查了 {totalCheckedItems} 个几何体,过滤掉 {filteredItems} 个高度范围外的几何体,保留 {heightCheckedItems.Count} 个");
LogManager.Info($"[精确高度检查] 其中与当前网格平面范围相交的有效几何体: 过滤 {overlappingItemsFiltered} 个, 保留 {overlappingItemsRetained} 个");
// 阶段5网格状态更新单线程写入3D高度检查
LogManager.Info("[高性能障碍物处理] 阶段5: 单线程网格状态更新3D高度检查");
@ -1426,7 +1409,8 @@ namespace NavisworksTransport.PathPlanning
double layerMaxZ = layer.Z + layer.PassableHeight.GetSpan();
// 障碍物的高度范围
var (obstacleMinZ, obstacleMaxZ) = GetObstacleElevationRange(bbox.Min, bbox.Max, gridMap);
double obstacleMinZ = bbox.Min.Z;
double obstacleMaxZ = bbox.Max.Z;
// 检查是否重叠
bool overlaps = !(obstacleMaxZ <= layerMinZ || obstacleMinZ > layerMaxZ);
@ -1482,138 +1466,6 @@ namespace NavisworksTransport.PathPlanning
}
/// <summary>
/// 获取世界坐标范围在当前网格坐标语义下的高程区间。
/// </summary>
private (double min, double max) GetObstacleElevationRange(Point3D minPoint, Point3D maxPoint, GridMap gridMap)
{
return GetObstacleElevationRange(
minPoint.X, minPoint.Y, minPoint.Z,
maxPoint.X, maxPoint.Y, maxPoint.Z,
gridMap.CoordinateSystemType);
}
/// <summary>
/// 根据世界坐标最小/最大点,计算其在当前网格坐标语义下覆盖的平面网格范围。
/// </summary>
private List<(int x, int y)> CalculateGridCoverageFromWorldExtents(Point3D minPoint, Point3D maxPoint, GridMap gridMap)
{
return CalculateGridCoverageFromWorldExtents(
minPoint.X, minPoint.Y, minPoint.Z,
maxPoint.X, maxPoint.Y, maxPoint.Z,
gridMap.Origin.X, gridMap.Origin.Y, gridMap.Origin.Z,
gridMap.Width, gridMap.Height, gridMap.CellSize,
gridMap.CoordinateSystemType);
}
/// <summary>
/// 获取世界坐标范围在当前宿主坐标语义下的高程区间。
/// 仅使用纯数值输入,便于单元测试验证多坐标系语义。
/// </summary>
private (double min, double max) GetObstacleElevationRange(
double minX, double minY, double minZ,
double maxX, double maxY, double maxZ,
CoordinateSystemType coordinateSystemType)
{
var adapter = new HostCoordinateAdapter(coordinateSystemType);
double elevation1 = HostPlanarGridHelper.GetElevation3(new Vector3((float)minX, (float)minY, (float)minZ), adapter);
double elevation2 = HostPlanarGridHelper.GetElevation3(new Vector3((float)maxX, (float)maxY, (float)maxZ), adapter);
return (Math.Min(elevation1, elevation2), Math.Max(elevation1, elevation2));
}
/// <summary>
/// 解析边界最小点在当前宿主坐标语义下的高程值。
/// 仅修正“高程轴”的解释,不改变原有边界层业务逻辑。
/// </summary>
private static double ResolveBoundsMinElevation(
double minX, double minY, double minZ,
CoordinateSystemType coordinateSystemType)
{
var adapter = new HostCoordinateAdapter(coordinateSystemType);
return HostPlanarGridHelper.GetElevation3(new Vector3((float)minX, (float)minY, (float)minZ), adapter);
}
/// <summary>
/// 根据世界坐标最小/最大点与网格元数据,计算覆盖的平面网格范围。
/// 仅使用纯数值输入,便于单元测试验证多坐标系语义。
/// </summary>
private List<(int x, int y)> CalculateGridCoverageFromWorldExtents(
double minX, double minY, double minZ,
double maxX, double maxY, double maxZ,
double originX, double originY, double originZ,
int width, int height, double cellSize,
CoordinateSystemType coordinateSystemType)
{
var adapter = new HostCoordinateAdapter(coordinateSystemType);
var minPoint = new Vector3((float)minX, (float)minY, (float)minZ);
var maxPoint = new Vector3((float)maxX, (float)maxY, (float)maxZ);
var originPoint = new Vector3((float)originX, (float)originY, (float)originZ);
var (minH1, minH2) = HostPlanarGridHelper.GetHorizontalCoords3(minPoint, adapter);
var (maxH1, maxH2) = HostPlanarGridHelper.GetHorizontalCoords3(maxPoint, adapter);
var (originH1, originH2) = HostPlanarGridHelper.GetHorizontalCoords3(originPoint, adapter);
int minGridX = (int)Math.Round((minH1 - originH1) / cellSize);
int minGridY = (int)Math.Round((minH2 - originH2) / cellSize);
int maxGridX = (int)Math.Round((maxH1 - originH1) / cellSize);
int maxGridY = (int)Math.Round((maxH2 - originH2) / cellSize);
int clampedMinX = Math.Max(0, Math.Min(minGridX, maxGridX));
int clampedMinY = Math.Max(0, Math.Min(minGridY, maxGridY));
int clampedMaxX = Math.Min(width - 1, Math.Max(minGridX, maxGridX));
int clampedMaxY = Math.Min(height - 1, Math.Max(minGridY, maxGridY));
if (clampedMaxX < clampedMinX) clampedMaxX = clampedMinX;
if (clampedMaxY < clampedMinY) clampedMaxY = clampedMinY;
var coveredCells = new List<(int x, int y)>();
for (int x = clampedMinX; x <= clampedMaxX; x++)
{
for (int y = clampedMinY; y <= clampedMaxY; y++)
{
coveredCells.Add((x, y));
}
}
return coveredCells;
}
/// <summary>
/// 判断包围盒在当前宿主平面语义下,是否与当前网格的平面范围真实相交。
/// 这里只做诊断,不改变原有网格覆盖或裁剪逻辑。
/// </summary>
private bool IntersectsGridPlanarExtents(BoundingBox3D bbox, GridMap gridMap)
{
if (bbox == null || gridMap == null || gridMap.Bounds == null)
{
return false;
}
var adapter = new HostCoordinateAdapter(gridMap.CoordinateSystemType);
var hostMin = new Vector3((float)bbox.Min.X, (float)bbox.Min.Y, (float)bbox.Min.Z);
var hostMax = new Vector3((float)bbox.Max.X, (float)bbox.Max.Y, (float)bbox.Max.Z);
var gridHostMin = new Vector3((float)gridMap.Bounds.Min.X, (float)gridMap.Bounds.Min.Y, (float)gridMap.Bounds.Min.Z);
var gridHostMax = new Vector3((float)gridMap.Bounds.Max.X, (float)gridMap.Bounds.Max.Y, (float)gridMap.Bounds.Max.Z);
var (bboxMinH1, bboxMaxH1, bboxMinH2, bboxMaxH2) = HostPlanarGridHelper.GetHorizontalRange3(hostMin, hostMax, adapter);
var (gridMinH1, gridMaxH1, gridMinH2, gridMaxH2) = HostPlanarGridHelper.GetHorizontalRange3(gridHostMin, gridHostMax, adapter);
bool overlapH1 = !(bboxMaxH1 < gridMinH1 || bboxMinH1 > gridMaxH1);
bool overlapH2 = !(bboxMaxH2 < gridMinH2 || bboxMinH2 > gridMaxH2);
return overlapH1 && overlapH2;
}
private static (double minH1, double maxH1, double minH2, double maxH2) GetPlanarRange(
Point3D minPoint,
Point3D maxPoint,
CoordinateSystemType coordinateSystemType)
{
var adapter = new HostCoordinateAdapter(coordinateSystemType);
var hostMin = new Vector3((float)minPoint.X, (float)minPoint.Y, (float)minPoint.Z);
var hostMax = new Vector3((float)maxPoint.X, (float)maxPoint.Y, (float)maxPoint.Z);
return HostPlanarGridHelper.GetHorizontalRange3(hostMin, hostMax, adapter);
}
/// <summary>
/// 计算包围盒在网格上覆盖的单元坐标
/// 严格按照"网格坐标代表左下角"的设计原则进行边界处理
@ -1623,88 +1475,36 @@ namespace NavisworksTransport.PathPlanning
/// <returns>覆盖的网格单元坐标列表</returns>
private List<(int x, int y)> CalculateBoundingBoxGridCoverage(BoundingBox3D bbox, GridMap gridMap)
{
var coveredCells = new List<(int x, int y)>();
try
{
return CalculateGridCoverageFromWorldExtents(bbox.Min, bbox.Max, gridMap);
// 直接使用WorldToGrid进行坐标转换
var minGridPos = gridMap.WorldToGrid(new Point3D(bbox.Min.X, bbox.Min.Y, 0));
var maxGridPos = gridMap.WorldToGrid(new Point3D(bbox.Max.X, bbox.Max.Y, 0));
// 确保坐标在有效范围内
int minX = Math.Max(0, minGridPos.X);
int minY = Math.Max(0, minGridPos.Y);
int maxX = Math.Min(gridMap.Width - 1, maxGridPos.X);
int maxY = Math.Min(gridMap.Height - 1, maxGridPos.Y);
// 确保至少覆盖一个网格(防止空包围盒)
if (maxX < minX) maxX = minX;
if (maxY < minY) maxY = minY;
// 遍历覆盖的矩形区域
for (int x = minX; x <= maxX; x++)
{
for (int y = minY; y <= maxY; y++)
{
coveredCells.Add((x, y));
}
}
}
catch (Exception ex)
{
LogManager.Debug($"[包围盒覆盖计算] 计算失败: {ex.Message}");
return new List<(int x, int y)>();
}
}
/// <summary>
/// 根据门包围盒、限宽和当前宿主平面语义,计算门开口在网格上的覆盖范围。
/// 关键约束:
/// - 仅替换“哪两个轴组成水平平面”的解释,不改变历史限宽业务规则。
/// - 门宽仍取平面上较大的那一维;限宽仍只裁剪门宽方向。
/// </summary>
private List<(int x, int y)> CalculateDoorOpeningCoverageFromWorldExtents(
double minX, double minY, double minZ,
double maxX, double maxY, double maxZ,
double limitWidthInModelUnits,
double originX, double originY, double originZ,
int width, int height, double cellSize,
CoordinateSystemType coordinateSystemType)
{
var adapter = new HostCoordinateAdapter(coordinateSystemType);
var hostMin = new Vector3((float)minX, (float)minY, (float)minZ);
var hostMax = new Vector3((float)maxX, (float)maxY, (float)maxZ);
var hostOrigin = new Vector3((float)originX, (float)originY, (float)originZ);
var (minH1, maxH1, minH2, maxH2) = HostPlanarGridHelper.GetHorizontalRange3(hostMin, hostMax, adapter);
var (originH1, originH2) = HostPlanarGridHelper.GetHorizontalCoords3(hostOrigin, adapter);
double planarWidth = maxH1 - minH1;
double planarDepth = maxH2 - minH2;
bool widthAlongH1 = planarWidth > planarDepth;
double doorWidth = Math.Max(planarWidth, planarDepth);
double effectiveWidth = Math.Min(limitWidthInModelUnits, doorWidth);
double openingMinH1;
double openingMaxH1;
double openingMinH2;
double openingMaxH2;
if (widthAlongH1)
{
double centerH1 = (minH1 + maxH1) / 2.0;
double halfWidth = effectiveWidth / 2.0;
openingMinH1 = centerH1 - halfWidth;
openingMaxH1 = centerH1 + halfWidth;
openingMinH2 = minH2;
openingMaxH2 = maxH2;
}
else
{
double centerH2 = (minH2 + maxH2) / 2.0;
double halfWidth = effectiveWidth / 2.0;
openingMinH1 = minH1;
openingMaxH1 = maxH1;
openingMinH2 = centerH2 - halfWidth;
openingMaxH2 = centerH2 + halfWidth;
}
int minGridX = (int)Math.Round((openingMinH1 - originH1) / cellSize);
int minGridY = (int)Math.Round((openingMinH2 - originH2) / cellSize);
int maxGridX = (int)Math.Round((openingMaxH1 - originH1) / cellSize);
int maxGridY = (int)Math.Round((openingMaxH2 - originH2) / cellSize);
int clampedMinX = Math.Max(0, Math.Min(minGridX, maxGridX));
int clampedMinY = Math.Max(0, Math.Min(minGridY, maxGridY));
int clampedMaxX = Math.Min(width - 1, Math.Max(minGridX, maxGridX));
int clampedMaxY = Math.Min(height - 1, Math.Max(minGridY, maxGridY));
if (clampedMaxX < clampedMinX) clampedMaxX = clampedMinX;
if (clampedMaxY < clampedMinY) clampedMaxY = clampedMinY;
var coveredCells = new List<(int x, int y)>();
for (int x = clampedMinX; x <= clampedMaxX; x++)
{
for (int y = clampedMinY; y <= clampedMaxY; y++)
{
coveredCells.Add((x, y));
}
}
return coveredCells;
@ -1745,35 +1545,74 @@ namespace NavisworksTransport.PathPlanning
double metersToModelUnits = UnitsConverter.GetMetersToUnitsConversionFactor(Application.ActiveDocument.Units);
double limitWidthInModelUnits = limitWidthMeters * metersToModelUnits;
var adapter = new HostCoordinateAdapter(gridMap.CoordinateSystemType);
var hostMin = new Vector3((float)bbox.Min.X, (float)bbox.Min.Y, (float)bbox.Min.Z);
var hostMax = new Vector3((float)bbox.Max.X, (float)bbox.Max.Y, (float)bbox.Max.Z);
var (minH1, maxH1, minH2, maxH2) = HostPlanarGridHelper.GetHorizontalRange3(hostMin, hostMax, adapter);
double planarWidth = maxH1 - minH1;
double planarDepth = maxH2 - minH2;
double doorWidth = Math.Max(planarWidth, planarDepth);
coveredCells = CalculateDoorOpeningCoverageFromWorldExtents(
bbox.Min.X, bbox.Min.Y, bbox.Min.Z,
bbox.Max.X, bbox.Max.Y, bbox.Max.Z,
limitWidthInModelUnits,
gridMap.Origin.X, gridMap.Origin.Y, gridMap.Origin.Z,
gridMap.Width, gridMap.Height, gridMap.CellSize,
gridMap.CoordinateSystemType);
// 4. 分析门的方向:哪个数大,哪个就是门的宽度
double width = bbox.Max.X - bbox.Min.X; // X方向宽度
double depth = bbox.Max.Y - bbox.Min.Y; // Y方向深度
double doorWidth = Math.Max(width, depth); // 门宽(较大的那个方向)
bool isDoorWidthInXDirection = width > depth; // 门宽是否在X方向
LogManager.Debug($"【门开口计算】{doorItem.DisplayName} - 尺寸: {width:F2}x{depth:F2}, 门宽: {doorWidth:F2}, 门宽方向: {(isDoorWidthInXDirection ? "X" : "Y")}, 限宽: {limitWidthMeters}m");
// 5. 限宽约束:限宽不能超过门宽
double effectiveWidth = Math.Min(limitWidthInModelUnits, doorWidth);
if (limitWidthInModelUnits > doorWidth)
{
LogManager.Info($"【门开口计算】{doorItem.DisplayName} 限宽({limitWidthMeters}m)超过门宽({doorWidth / metersToModelUnits:F2}m),按门宽处理");
}
if (coveredCells.Any())
// 6. 直接计算开口区域坐标范围
double minX, maxX, minY, maxY;
if (isDoorWidthInXDirection)
{
int gridMinX = coveredCells.Min(cell => cell.x);
int gridMinY = coveredCells.Min(cell => cell.y);
int gridMaxX = coveredCells.Max(cell => cell.x);
int gridMaxY = coveredCells.Max(cell => cell.y);
LogManager.Info($"【门开口计算】{doorItem.DisplayName} 成功处理 - 门宽: {doorWidth / metersToModelUnits:F2}m, 限宽: {limitWidthMeters}m, 开口覆盖: {coveredCells.Count}个网格 (范围: [{gridMinX},{gridMinY}]-[{gridMaxX},{gridMaxY}])");
// 门宽在X方向在X方向应用限宽Y方向保持完整
double centerX = (bbox.Min.X + bbox.Max.X) / 2;
double halfWidth = effectiveWidth / 2;
minX = centerX - halfWidth;
maxX = centerX + halfWidth;
minY = bbox.Min.Y;
maxY = bbox.Max.Y;
}
else
{
// 门宽在Y方向在Y方向应用限宽X方向保持完整
double centerY = (bbox.Min.Y + bbox.Max.Y) / 2;
double halfWidth = effectiveWidth / 2;
minX = bbox.Min.X;
maxX = bbox.Max.X;
minY = centerY - halfWidth;
maxY = centerY + halfWidth;
}
LogManager.Debug($"【门开口计算】{doorItem.DisplayName} 开口区域坐标: [{minX:F2},{minY:F2}]-[{maxX:F2},{maxY:F2}]");
// 7. 将开口区域坐标转换为网格坐标(严格按照左下角原则)
var minGridPos = gridMap.WorldToGrid(new Point3D(minX, minY, 0));
var maxGridPos = gridMap.WorldToGrid(new Point3D(maxX, maxY, 0));
// 确保坐标在有效范围内
int gridMinX = Math.Max(0, minGridPos.X);
int gridMinY = Math.Max(0, minGridPos.Y);
int gridMaxX = Math.Min(gridMap.Width - 1, maxGridPos.X);
int gridMaxY = Math.Min(gridMap.Height - 1, maxGridPos.Y);
// 确保至少有1个网格可通行
if (gridMaxX < gridMinX) gridMaxX = gridMinX;
if (gridMaxY < gridMinY) gridMaxY = gridMinY;
// 8. 生成覆盖的网格单元列表
for (int x = gridMinX; x <= gridMaxX; x++)
{
for (int y = gridMinY; y <= gridMaxY; y++)
{
coveredCells.Add((x, y));
}
}
LogManager.Info($"【门开口计算】{doorItem.DisplayName} 成功处理 - 门宽: {doorWidth / metersToModelUnits:F2}m, 限宽: {limitWidthMeters}m, 开口覆盖: {coveredCells.Count}个网格 (范围: [{gridMinX},{gridMinY}]-[{gridMaxX},{gridMaxY}])");
}
catch (Exception ex)
{

View File

@ -2,7 +2,6 @@ using System;
using System.Collections.Generic;
using System.Linq;
using Autodesk.Navisworks.Api;
using NavisworksTransport.Utils.CoordinateSystem;
namespace NavisworksTransport.PathPlanning
{
@ -15,7 +14,6 @@ namespace NavisworksTransport.PathPlanning
private readonly Dictionary<string, double> _heightSamples;
private readonly double _sampleInterval;
private readonly ChannelHeightDetector _heightDetector;
private readonly ICoordinateSystem _coordinateSystem;
/// <summary>
/// 构造函数
@ -25,8 +23,7 @@ namespace NavisworksTransport.PathPlanning
{
_heightSamples = new Dictionary<string, double>();
_sampleInterval = sampleInterval;
_coordinateSystem = CoordinateSystemManager.Instance.Current;
_heightDetector = new ChannelHeightDetector(_coordinateSystem);
_heightDetector = new ChannelHeightDetector();
LogManager.Info($"[优化高度计算] 初始化完成,采样间隔: {sampleInterval}米");
}
@ -53,9 +50,8 @@ namespace NavisworksTransport.PathPlanning
// 计算采样网格
double sampleIntervalModel = _sampleInterval * 39.37; // 转换为模型单位
var (minH1, maxH1, minH2, maxH2) = _coordinateSystem.GetHorizontalRange(bounds);
int samplesX = (int)Math.Ceiling((maxH1 - minH1) / sampleIntervalModel);
int samplesY = (int)Math.Ceiling((maxH2 - minH2) / sampleIntervalModel);
int samplesX = (int)Math.Ceiling((bounds.Max.X - bounds.Min.X) / sampleIntervalModel);
int samplesY = (int)Math.Ceiling((bounds.Max.Y - bounds.Min.Y) / sampleIntervalModel);
LogManager.Info($"[优化高度计算] 采样网格: {samplesX}x{samplesY} = {samplesX * samplesY}个采样点");
@ -67,13 +63,13 @@ namespace NavisworksTransport.PathPlanning
{
for (int y = 0; y < samplesY; y++)
{
double worldH1 = minH1 + x * sampleIntervalModel;
double worldH2 = minH2 + y * sampleIntervalModel;
double worldX = bounds.Min.X + x * sampleIntervalModel;
double worldY = bounds.Min.Y + y * sampleIntervalModel;
var sampleKey = GenerateSampleKey(worldH1, worldH2);
var sampleKey = GenerateSampleKey(worldX, worldY);
// 只对通道区域进行精确计算
var position = _coordinateSystem.CreatePoint(worldH1, worldH2, 0);
var position = new Point3D(worldX, worldY, 0);
var containingChannel = FindNearestChannel(position, channels);
if (containingChannel != null)
@ -160,8 +156,7 @@ namespace NavisworksTransport.PathPlanning
// 对于大多数建筑元素,表面接近顶面
double surfaceRatio = DetermineChannelSurfaceRatio(channel);
var (minElevation, maxElevation) = _coordinateSystem.GetHeightRange(bbox);
double height = minElevation + (maxElevation - minElevation) * surfaceRatio;
double height = bbox.Min.Z + (bbox.Max.Z - bbox.Min.Z) * surfaceRatio;
return height;
}
@ -171,7 +166,7 @@ namespace NavisworksTransport.PathPlanning
LogManager.Debug($"[优化高度计算] 几何高度计算失败: {ex.Message}");
}
return _coordinateSystem.GetElevation(position);
return position.Z;
}
/// <summary>
@ -192,9 +187,8 @@ namespace NavisworksTransport.PathPlanning
var pickResult = view.PickItemFromPoint((int)screenPoint.X, (int)screenPoint.Y);
if (pickResult?.Point != null && pickResult.ModelItem == channel)
{
double detectedElevation = _coordinateSystem.GetElevation(pickResult.Point);
LogManager.Debug($"[优化高度计算] View API精确检测: {worldPosition} -> {detectedElevation:F3}");
return detectedElevation;
LogManager.Debug($"[优化高度计算] View API精确检测: {worldPosition} -> {pickResult.Point.Z:F3}");
return pickResult.Point.Z;
}
}
}
@ -251,15 +245,13 @@ namespace NavisworksTransport.PathPlanning
var bbox = channel.Geometry.BoundingBox;
// 检查位置是否在通道边界内(带容差)
var (positionH1, positionH2) = _coordinateSystem.GetHorizontalCoords(position);
var (minH1, maxH1, minH2, maxH2) = _coordinateSystem.GetHorizontalRange(bbox);
if (positionH1 >= minH1 - 50 && positionH1 <= maxH1 + 50 &&
positionH2 >= minH2 - 50 && positionH2 <= maxH2 + 50)
if (position.X >= bbox.Min.X - 50 && position.X <= bbox.Max.X + 50 &&
position.Y >= bbox.Min.Y - 50 && position.Y <= bbox.Max.Y + 50)
{
// 计算到边界中心的距离
double centerH1 = (minH1 + maxH1) / 2;
double centerH2 = (minH2 + maxH2) / 2;
double distance = Math.Sqrt(Math.Pow(positionH1 - centerH1, 2) + Math.Pow(positionH2 - centerH2, 2));
double centerX = (bbox.Min.X + bbox.Max.X) / 2;
double centerY = (bbox.Min.Y + bbox.Max.Y) / 2;
double distance = Math.Sqrt(Math.Pow(position.X - centerX, 2) + Math.Pow(position.Y - centerY, 2));
if (distance < minDistance)
{
@ -376,4 +368,4 @@ namespace NavisworksTransport.PathPlanning
return $"采样点数: {_heightSamples.Count}, 采样间隔: {_sampleInterval}米";
}
}
}
}

View File

@ -118,7 +118,7 @@ namespace NavisworksTransport.PathPlanning
}
else
{
optimizedPoints = SimplifyCollinearPoints(optimizedPoints, gridMap);
optimizedPoints = SimplifyCollinearPoints(optimizedPoints);
LogManager.Info($"[路径优化] 简化完成,点数:{originalCount} -> {optimizedPoints.Count}");
}
}
@ -170,7 +170,7 @@ namespace NavisworksTransport.PathPlanning
// 3D场景保留同网格不同高度的点
bool hasGridChange = i == 0 || !gridPath[i].Equals(gridPath[i-1]);
bool hasSpeedChange = i > 0 && HasSpeedLimitChange(points[i-1], points[i]);
bool hasHeightChange = i > 0 && HasHeightChange(points[i], points[i - 1], gridMap);
bool hasHeightChange = i > 0 && Math.Abs(points[i].Position.Z - points[i-1].Position.Z) > 1e-6;
bool shouldKeep = hasGridChange || hasSpeedChange || hasHeightChange;
@ -213,8 +213,7 @@ namespace NavisworksTransport.PathPlanning
bool hasHeightChange = HasHeightChange(
dedupedPoints[i-2],
dedupedPoints[i-1],
dedupedPoints[i],
gridMap
dedupedPoints[i]
);
// 检查限速变化
@ -265,7 +264,7 @@ namespace NavisworksTransport.PathPlanning
/// </summary>
/// <param name="points">原始路径点列表</param>
/// <returns>简化后的路径点列表</returns>
private List<PathPoint> SimplifyCollinearPoints(List<PathPoint> points, GridMap gridMap = null)
private List<PathPoint> SimplifyCollinearPoints(List<PathPoint> points)
{
if (points.Count <= 2) return points;
@ -282,7 +281,7 @@ namespace NavisworksTransport.PathPlanning
var nextPoint = points[i + 1];
// 检查是否在同一直线上
bool isCollinear = IsCollinear(prevPoint.Position, currPoint.Position, nextPoint.Position, gridMap);
bool isCollinear = IsCollinear(prevPoint.Position, currPoint.Position, nextPoint.Position);
// 🔥 关键修复:检查限速变化,即使共线也不能移除限速边界点
bool hasSpeedLimitChange = HasSpeedLimitChange(prevPoint, currPoint, nextPoint);
@ -317,12 +316,10 @@ namespace NavisworksTransport.PathPlanning
/// <param name="point1">第一个路径点</param>
/// <param name="point2">第二个路径点</param>
/// <returns>如果有高度变化返回true需要保留中间点</returns>
private bool HasHeightChange(PathPoint point1, PathPoint point2, GridMap gridMap = null)
private bool HasHeightChange(PathPoint point1, PathPoint point2)
{
const double heightTolerance = 1e-6; // 仅覆盖浮点数精度误差
double elevation1 = GetElevation(point1.Position, gridMap);
double elevation2 = GetElevation(point2.Position, gridMap);
return Math.Abs(elevation1 - elevation2) > heightTolerance;
return Math.Abs(point1.Position.Z - point2.Position.Z) > heightTolerance;
}
/// <summary>
@ -332,17 +329,13 @@ namespace NavisworksTransport.PathPlanning
/// <param name="p2">中间点的3D坐标</param>
/// <param name="p3">第三个点的3D坐标</param>
/// <returns>如果有高度变化返回true不能跳过中间点</returns>
private bool HasHeightChange(Point3D p1, Point3D p2, Point3D p3, GridMap gridMap = null)
private bool HasHeightChange(Point3D p1, Point3D p2, Point3D p3)
{
const double heightTolerance = 1e-6; // 仅覆盖浮点数精度误差
// 检查任意两点间是否有高度变化
double elevation1 = GetElevation(p1, gridMap);
double elevation2 = GetElevation(p2, gridMap);
double elevation3 = GetElevation(p3, gridMap);
if (Math.Abs(elevation1 - elevation2) > heightTolerance ||
Math.Abs(elevation2 - elevation3) > heightTolerance)
if (Math.Abs(p1.Z - p2.Z) > heightTolerance ||
Math.Abs(p2.Z - p3.Z) > heightTolerance)
{
return true;
}
@ -356,9 +349,9 @@ namespace NavisworksTransport.PathPlanning
/// <param name="point2">中间路径点</param>
/// <param name="point3">第三个路径点</param>
/// <returns>如果有高度变化返回true不能跳过中间点</returns>
private bool HasHeightChange(PathPoint point1, PathPoint point2, PathPoint point3, GridMap gridMap = null)
private bool HasHeightChange(PathPoint point1, PathPoint point2, PathPoint point3)
{
return HasHeightChange(point1.Position, point2.Position, point3.Position, gridMap);
return HasHeightChange(point1.Position, point2.Position, point3.Position);
}
/// <summary>
@ -402,41 +395,25 @@ namespace NavisworksTransport.PathPlanning
/// <param name="p2">第二个点(中间点)</param>
/// <param name="p3">第三个点</param>
/// <returns>如果三点在同一直线上且方向一致返回true</returns>
private bool IsCollinear(Point3D p1, Point3D p2, Point3D p3, GridMap gridMap = null)
private bool IsCollinear(Point3D p1, Point3D p2, Point3D p3)
{
const double tolerance = COLLINEAR_TOLERANCE_METERS; // 容差值,单位:模型单位
// 🔥 修复:使用统一的高度检查函数
if (HasHeightChange(p1, p2, p3, gridMap))
if (HasHeightChange(p1, p2, p3))
{
LogManager.Debug($"[共线检测] ❌ 拒绝Z方向变化({p1.X:F3},{p1.Y:F3},{p1.Z:F3}) -> ({p2.X:F3},{p2.Y:F3},{p2.Z:F3}) -> ({p3.X:F3},{p3.Y:F3},{p3.Z:F3})");
return false;
}
double dx12;
double dy12;
double dx23;
double dy23;
if (gridMap != null)
{
var g1 = gridMap.WorldToGrid(p1);
var g2 = gridMap.WorldToGrid(p2);
var g3 = gridMap.WorldToGrid(p3);
dx12 = g2.X - g1.X;
dy12 = g2.Y - g1.Y;
dx23 = g3.X - g2.X;
dy23 = g3.Y - g2.Y;
}
else
{
// 回退到历史 ZUp 语义,仅用于未提供网格地图的旧调用链。
dx12 = p2.X - p1.X;
dy12 = p2.Y - p1.Y;
dx23 = p3.X - p2.X;
dy23 = p3.Y - p2.Y;
}
// 🔥 关键修复:分别检查两个线段的方向
// 线段1: p1 -> p2
double dx12 = p2.X - p1.X;
double dy12 = p2.Y - p1.Y;
// 线段2: p2 -> p3
double dx23 = p3.X - p2.X;
double dy23 = p3.Y - p2.Y;
// 🔥 修复:使用更严格的容差检查真正的重复点(坐标完全相同)
const double duplicatePointTolerance = DUPLICATE_POINT_TOLERANCE_METERS; // 用很小的值检查真正的重复点
@ -476,11 +453,6 @@ namespace NavisworksTransport.PathPlanning
}
}
private static double GetElevation(Point3D point, GridMap gridMap)
{
return gridMap?.GetWorldElevation(point) ?? point.Z;
}
/// <summary>
/// 创建优化后的路径对象
/// </summary>
@ -549,4 +521,4 @@ namespace NavisworksTransport.PathPlanning
#endregion
}
}
}

View File

@ -2,7 +2,6 @@ using System;
using System.Collections.Generic;
using System.Linq;
using Autodesk.Navisworks.Api;
using NavisworksTransport.Utils.CoordinateSystem;
namespace NavisworksTransport.PathPlanning
{
@ -14,7 +13,6 @@ namespace NavisworksTransport.PathPlanning
{
private readonly Dictionary<string, ChannelSlopeInfo> _slopeCache;
private readonly double _minSlopeAngle; // 最小坡度角度(弧度)
private readonly ICoordinateSystem _coordinateSystem;
/// <summary>
/// 构造函数
@ -24,7 +22,6 @@ namespace NavisworksTransport.PathPlanning
{
_slopeCache = new Dictionary<string, ChannelSlopeInfo>();
_minSlopeAngle = minSlopeAngle * Math.PI / 180.0; // 转换为弧度
_coordinateSystem = CoordinateSystemManager.Instance.Current;
}
/// <summary>
@ -160,10 +157,8 @@ namespace NavisworksTransport.PathPlanning
// 根据几何特征判断
var bounds = channel.Geometry.BoundingBox;
var (minElevation, maxElevation) = _coordinateSystem.GetHeightRange(bounds);
var (minH1, maxH1, minH2, maxH2) = _coordinateSystem.GetHorizontalRange(bounds);
var heightDiff = maxElevation - minElevation;
var horizontalLength = Math.Max(maxH1 - minH1, maxH2 - minH2);
var heightDiff = bounds.Max.Z - bounds.Min.Z;
var horizontalLength = Math.Max(bounds.Max.X - bounds.Min.X, bounds.Max.Y - bounds.Min.Y);
if (heightDiff > 0.1 && horizontalLength > 0.1) // 有明显高度差
{
@ -190,26 +185,25 @@ namespace NavisworksTransport.PathPlanning
private void AddBoundaryPoints(ChannelSlopeInfo slopeInfo, BoundingBox3D bounds)
{
// 添加8个边界框顶点
var (minElevation, maxElevation) = _coordinateSystem.GetHeightRange(bounds);
var (minH1, maxH1, minH2, maxH2) = _coordinateSystem.GetHorizontalRange(bounds);
slopeInfo.SlopePoints.Add(_coordinateSystem.CreatePoint(minH1, minH2, minElevation));
slopeInfo.SlopePoints.Add(_coordinateSystem.CreatePoint(maxH1, minH2, minElevation));
slopeInfo.SlopePoints.Add(_coordinateSystem.CreatePoint(minH1, maxH2, minElevation));
slopeInfo.SlopePoints.Add(_coordinateSystem.CreatePoint(maxH1, maxH2, minElevation));
slopeInfo.SlopePoints.Add(_coordinateSystem.CreatePoint(minH1, minH2, maxElevation));
slopeInfo.SlopePoints.Add(_coordinateSystem.CreatePoint(maxH1, minH2, maxElevation));
slopeInfo.SlopePoints.Add(_coordinateSystem.CreatePoint(minH1, maxH2, maxElevation));
slopeInfo.SlopePoints.Add(_coordinateSystem.CreatePoint(maxH1, maxH2, maxElevation));
slopeInfo.SlopePoints.Add(new Point3D(bounds.Min.X, bounds.Min.Y, bounds.Min.Z));
slopeInfo.SlopePoints.Add(new Point3D(bounds.Max.X, bounds.Min.Y, bounds.Min.Z));
slopeInfo.SlopePoints.Add(new Point3D(bounds.Min.X, bounds.Max.Y, bounds.Min.Z));
slopeInfo.SlopePoints.Add(new Point3D(bounds.Max.X, bounds.Max.Y, bounds.Min.Z));
slopeInfo.SlopePoints.Add(new Point3D(bounds.Min.X, bounds.Min.Y, bounds.Max.Z));
slopeInfo.SlopePoints.Add(new Point3D(bounds.Max.X, bounds.Min.Y, bounds.Max.Z));
slopeInfo.SlopePoints.Add(new Point3D(bounds.Min.X, bounds.Max.Y, bounds.Max.Z));
slopeInfo.SlopePoints.Add(new Point3D(bounds.Max.X, bounds.Max.Y, bounds.Max.Z));
// 添加中心线上的采样点
int sampleCount = 5;
for (int i = 0; i < sampleCount; i++)
{
double ratio = (double)i / (sampleCount - 1);
var samplePoint = _coordinateSystem.CreatePoint(
minH1 + (maxH1 - minH1) * ratio,
minH2 + (maxH2 - minH2) * ratio,
minElevation + (maxElevation - minElevation) * ratio);
var samplePoint = new Point3D(
bounds.Min.X + (bounds.Max.X - bounds.Min.X) * ratio,
bounds.Min.Y + (bounds.Max.Y - bounds.Min.Y) * ratio,
bounds.Min.Z + (bounds.Max.Z - bounds.Min.Z) * ratio
);
slopeInfo.SlopePoints.Add(samplePoint);
}
}
@ -222,29 +216,26 @@ namespace NavisworksTransport.PathPlanning
private void CalculateSlopeAngleAndDirection(ChannelSlopeInfo slopeInfo, BoundingBox3D bounds)
{
// 计算主要方向向量
var (minElevation, maxElevation) = _coordinateSystem.GetHeightRange(bounds);
var (minH1, maxH1, minH2, maxH2) = _coordinateSystem.GetHorizontalRange(bounds);
var directionH1 = maxH1 - minH1;
var directionH2 = maxH2 - minH2;
var directionElevation = maxElevation - minElevation;
var directionX = bounds.Max.X - bounds.Min.X;
var directionY = bounds.Max.Y - bounds.Min.Y;
var directionZ = bounds.Max.Z - bounds.Min.Z;
// 选择主要的水平方向
var horizontalLength = Math.Max(Math.Abs(directionH1), Math.Abs(directionH2));
var horizontalLength = Math.Max(Math.Abs(directionX), Math.Abs(directionY));
if (horizontalLength > 0.001)
{
// 计算坡度角度
slopeInfo.SlopeAngle = Math.Atan(Math.Abs(directionElevation) / horizontalLength);
slopeInfo.SlopeAngle = Math.Atan(Math.Abs(directionZ) / horizontalLength);
// 计算坡度方向向量
var directionPoint = _coordinateSystem.CreatePoint(directionH1, directionH2, directionElevation);
var length = Math.Sqrt(directionPoint.X * directionPoint.X + directionPoint.Y * directionPoint.Y + directionPoint.Z * directionPoint.Z);
var length = Math.Sqrt(directionX * directionX + directionY * directionY + directionZ * directionZ);
if (length > 0.001)
{
slopeInfo.SlopeDirection = new Vector3D(
directionPoint.X / length,
directionPoint.Y / length,
directionPoint.Z / length
directionX / length,
directionY / length,
directionZ / length
);
}
else
@ -255,8 +246,7 @@ namespace NavisworksTransport.PathPlanning
else
{
slopeInfo.SlopeAngle = 0;
var up = _coordinateSystem.UpVector;
slopeInfo.SlopeDirection = new Vector3D(up.X, up.Y, up.Z);
slopeInfo.SlopeDirection = new Vector3D(0, 0, 1);
}
}
@ -289,10 +279,8 @@ namespace NavisworksTransport.PathPlanning
try
{
var bounds = channel.Geometry.BoundingBox;
var (minElevation, maxElevation) = _coordinateSystem.GetHeightRange(bounds);
var (minH1, maxH1, minH2, maxH2) = _coordinateSystem.GetHorizontalRange(bounds);
var totalHeight = maxElevation - minElevation;
var totalLength = Math.Max(maxH1 - minH1, maxH2 - minH2);
var totalHeight = bounds.Max.Z - bounds.Min.Z;
var totalLength = Math.Max(bounds.Max.X - bounds.Min.X, bounds.Max.Y - bounds.Min.Y);
// 估算楼梯级数假设每级高度15-20cm
var estimatedSteps = (int)(totalHeight / 0.175); // 平均17.5cm每级
@ -322,9 +310,8 @@ namespace NavisworksTransport.PathPlanning
var bounds = channel.Geometry.BoundingBox;
// 检查是否为弯曲坡道(通过长宽比判断)
var (minH1, maxH1, minH2, maxH2) = _coordinateSystem.GetHorizontalRange(bounds);
var width = Math.Min(maxH1 - minH1, maxH2 - minH2);
var length = Math.Max(maxH1 - minH1, maxH2 - minH2);
var width = Math.Min(bounds.Max.X - bounds.Min.X, bounds.Max.Y - bounds.Min.Y);
var length = Math.Max(bounds.Max.X - bounds.Min.X, bounds.Max.Y - bounds.Min.Y);
if (width > 0 && length / width > 3.0) // 长宽比大于3:1可能是弯曲坡道
{
@ -365,21 +352,18 @@ namespace NavisworksTransport.PathPlanning
}
// 找到楼梯的起点和终点
var startPoint = slopeInfo.SlopePoints.OrderBy(p => _coordinateSystem.GetElevation(p)).First();
var endPoint = slopeInfo.SlopePoints.OrderBy(p => _coordinateSystem.GetElevation(p)).Last();
var startPoint = slopeInfo.SlopePoints.OrderBy(p => p.Z).First();
var endPoint = slopeInfo.SlopePoints.OrderBy(p => p.Z).Last();
// 计算位置在楼梯长度方向上的投影
var (startH1, startH2) = _coordinateSystem.GetHorizontalCoords(startPoint);
var (endH1, endH2) = _coordinateSystem.GetHorizontalCoords(endPoint);
var (positionH1, positionH2) = _coordinateSystem.GetHorizontalCoords(position);
var totalDistance = Math.Sqrt(
Math.Pow(endH1 - startH1, 2) +
Math.Pow(endH2 - startH2, 2)
Math.Pow(endPoint.X - startPoint.X, 2) +
Math.Pow(endPoint.Y - startPoint.Y, 2)
);
var currentDistance = Math.Sqrt(
Math.Pow(positionH1 - startH1, 2) +
Math.Pow(positionH2 - startH2, 2)
Math.Pow(position.X - startPoint.X, 2) +
Math.Pow(position.Y - startPoint.Y, 2)
);
if (totalDistance <= 0.001)
@ -538,4 +522,4 @@ namespace NavisworksTransport.PathPlanning
Other
}
}
}

View File

@ -1,127 +0,0 @@
using System;
using System.Globalization;
using System.Windows.Data;
namespace NavisworksTransport.UI.WPF.Converters
{
/// <summary>
/// Keeps numeric TextBox editing humane: incomplete text is allowed while editing,
/// and only complete numeric values are written back to the bound source.
/// </summary>
public class EditableNumberConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value == null)
{
return string.Empty;
}
string format = parameter as string;
if (string.IsNullOrWhiteSpace(format))
{
return System.Convert.ToString(value, culture);
}
if (value is IFormattable formattable)
{
return formattable.ToString(format, culture);
}
return System.Convert.ToString(value, culture);
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
string text = value as string;
if (IsIntermediateText(text))
{
throw new FormatException("请输入有效的数字。");
}
text = text.Trim();
Type actualTargetType = Nullable.GetUnderlyingType(targetType) ?? targetType;
if (TryParseNumber(text, culture, actualTargetType, out object parsedValue))
{
return parsedValue;
}
throw new FormatException("请输入有效的数字。");
}
public static bool IsIntermediateText(string text)
{
if (string.IsNullOrWhiteSpace(text))
{
return true;
}
text = text.Trim();
return text == "-" ||
text == "+" ||
text == "." ||
text == "-." ||
text == "+.";
}
public static bool TryParseNumber(string text, CultureInfo culture, Type targetType, out object parsedValue)
{
NumberStyles styles = NumberStyles.Float | NumberStyles.AllowThousands;
if (targetType == typeof(double))
{
if (TryParseDouble(text, styles, culture, out double doubleValue))
{
parsedValue = doubleValue;
return true;
}
}
else if (targetType == typeof(float))
{
if (TryParseDouble(text, styles, culture, out double doubleValue))
{
parsedValue = (float)doubleValue;
return true;
}
}
else if (targetType == typeof(decimal))
{
if (decimal.TryParse(text, styles, culture, out decimal decimalValue) ||
decimal.TryParse(text, styles, CultureInfo.InvariantCulture, out decimalValue))
{
parsedValue = decimalValue;
return true;
}
}
else if (targetType == typeof(int))
{
if (int.TryParse(text, NumberStyles.Integer, culture, out int intValue) ||
int.TryParse(text, NumberStyles.Integer, CultureInfo.InvariantCulture, out intValue))
{
parsedValue = intValue;
return true;
}
}
else if (targetType == typeof(long))
{
if (long.TryParse(text, NumberStyles.Integer, culture, out long longValue) ||
long.TryParse(text, NumberStyles.Integer, CultureInfo.InvariantCulture, out longValue))
{
parsedValue = longValue;
return true;
}
}
parsedValue = null;
return false;
}
private static bool TryParseDouble(string text, NumberStyles styles, CultureInfo culture, out double value)
{
return double.TryParse(text, styles, culture, out value) ||
double.TryParse(text, styles, CultureInfo.InvariantCulture, out value);
}
}
}

View File

@ -2,7 +2,6 @@ using System;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using Autodesk.Navisworks.Api;
using System.Collections.Generic;
namespace NavisworksTransport.UI.WPF.Models
{
@ -54,47 +53,4 @@ namespace NavisworksTransport.UI.WPF.Models
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
/// <summary>
/// 吊装路径层高编辑项
/// </summary>
public class HoistingLayerEditItem : INotifyPropertyChanged
{
private int _displayIndex;
private double _heightInMeters;
private string _pointSummary;
public int DisplayIndex
{
get => _displayIndex;
set { _displayIndex = value; OnPropertyChanged(nameof(DisplayIndex)); }
}
/// <summary>
/// 相对起吊点的绝对高度(米)
/// </summary>
public double HeightInMeters
{
get => _heightInMeters;
set { _heightInMeters = value; OnPropertyChanged(nameof(HeightInMeters)); }
}
public string PointSummary
{
get => _pointSummary;
set { _pointSummary = value; OnPropertyChanged(nameof(PointSummary)); }
}
/// <summary>
/// 当前层关联的路径点 Id 列表
/// </summary>
public List<string> PointIds { get; } = new List<string>();
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}

View File

@ -204,11 +204,6 @@ NavisworksTransport 共享样式资源字典 - Navisworks 2026风格
M17,3H14.76C14.37,1.69 13.17,0.75 11.75,0.75C10.33,0.75 9.13,1.69 8.74,3H6.5C5.12,3 4,4.12 4,5.5V19.5C4,20.88 5.12,22 6.5,22H17C18.38,22 19.5,20.88 19.5,19.5V5.5C19.5,4.12 18.38,3 17,3ZM11.75,3C12.16,3 12.5,3.34 12.5,3.75C12.5,4.16 12.16,4.5 11.75,4.5C11.34,4.5 11,4.16 11,3.75C11,3.34 11.34,3 11.75,3ZM17,19.5H6.5V5.5H8.75V7.25H14.75V5.5H17V19.5Z
</Geometry>
<!-- 旋转图标按钮 (Material Refresh) -->
<Geometry x:Key="RotateIconGeometry">
M17.65,6.35C16.2,4.9 14.21,4 12,4C7.58,4 4.01,7.58 4.01,12C4.01,16.42 7.58,20 12,20C15.73,20 18.84,17.45 19.73,14H17.65C16.83,16.33 14.61,18 12,18C8.69,18 6,15.31 6,12C6,8.69 8.69,6 12,6C13.66,6 15.14,6.69 16.22,7.78L13,11H20V4L17.65,6.35Z
</Geometry>
<!-- 靶心/准心图标 (测量工具) -->
<Geometry x:Key="TargetIconGeometry">
M12,2C6.48,2 2,6.48 2,12C2,17.52 6.48,22 12,22C17.52,22 22,17.52 22,12C22,6.48 17.52,2 12,2ZM12,20C7.59,20 4,16.41 4,12C4,7.59 7.59,4 12,4C16.41,4 20,7.59 20,12C20,16.41 16.41,20 12,20ZM12,6C8.69,6 6,8.69 6,12C6,15.31 8.69,18 12,18C15.31,18 18,15.31 18,12C18,8.69 15.31,6 12,6ZM12,16C9.79,16 8,14.21 8,12C8,9.79 9.79,8 12,8C14.21,8 16,9.79 16,12C16,14.21 14.21,16 12,16Z

View File

@ -15,7 +15,6 @@ using Autodesk.Navisworks.Api.Clash;
using NavisworksTransport.Core;
using NavisworksTransport.Core.Models;
using NavisworksTransport.Core.Config;
using NavisworksTransport.Core.Services;
using NavisworksTransport.Commands;
using NavisworksTransport.UI.WPF.Collections;
using NavisworksTransport.UI.WPF.Views;
@ -358,7 +357,6 @@ namespace NavisworksTransport.UI.WPF.ViewModels
// 角度修正相关字段
private LocalEulerRotationCorrection _objectRotationCorrection = LocalEulerRotationCorrection.Zero; // 物体绕宿主 X/Y/Z 轴的角度修正
private double _objectGroundLiftHeightInMeters;
// 移动物体原始尺寸(米,在选择物体时保存)
private double _objectOriginalLength; // 物体原始长度X方向
@ -489,14 +487,6 @@ namespace NavisworksTransport.UI.WPF.ViewModels
}
}
/// <summary>
/// 最近一次自动生成的碰撞报告。
/// 仅用于测试自动化读取结果,不参与业务流程分支。
/// </summary>
public CollisionReportResult LastGeneratedReport => _lastGeneratedReport;
public bool HasGeneratedCollisionReport => _lastGeneratedReport != null;
/// <summary>
@ -1685,36 +1675,32 @@ namespace NavisworksTransport.UI.WPF.ViewModels
_pathAnimationManager?.SetRealObjectDimensions(objectLength, objectWidth, objectHeight);
LogManager.Debug($"[选择物体] 保存原始尺寸: 长度={_objectOriginalLength:F2}m, 宽度={_objectOriginalWidth:F2}m, 高度={_objectOriginalHeight:F2}m");
// 3. 选择实体物体意味着切换到实体模式,避免后续起点同步仍走虚拟物体分支。
// 3. 先注册到动画管理器,再设置 SelectedAnimatedObject。
// SelectedAnimatedObject 的 setter 会立即触发 MoveAnimatedObjectToPathStart()
// 如果先移动、后 SetAnimatedObject会把“已摆到起点后的当前姿态”再次当成参考姿态缓存。
_pathAnimationManager?.SetAnimatedObject(newObject);
SelectedAnimatedObject = newObject;
LogManager.Info($"已选择移动物体: {SelectedAnimatedObject.DisplayName}");
// 选择实体物体意味着切换到实体模式,避免后续起点同步仍走虚拟物体分支。
if (UseVirtualObject)
{
UseVirtualObject = false;
LogManager.Debug("[选择物体] 已切换到实体物体模式");
}
// 4. 对“新物体”先清空角度修正,再触发 SelectedAnimatedObject 的起点落位。
// SelectedAnimatedObject 的 setter 会立即触发 MoveAnimatedObjectToPathStart()
// 如果角度在后面才归零,就会先按旧角度把新选择的物体摆到起点。
// 只有选择不同的物体时,才重置角度修正值
if (!isSameObject)
{
_pathAnimationManager?.SetObjectRotationCorrectionDirect(LocalEulerRotationCorrection.Zero);
_pathAnimationManager?.SetObjectStartVerticalLiftDirect(0.0);
// 重置 ViewModel 中的角度修正值(会自动同步到 PathAnimationManager
ObjectRotationCorrection = LocalEulerRotationCorrection.Zero;
_objectGroundLiftHeightInMeters = 0.0;
LogManager.Debug("[选择物体] 已重置角度修正值为0新物体");
}
else
{
LogManager.Debug("[选择物体] 保持当前角度修正值(同一物体)");
LogManager.Debug($"[选择物体] 保持当前角度修正值(同一物体)");
}
// 5. 先注册到动画管理器,再设置 SelectedAnimatedObject。
// SelectedAnimatedObject 的 setter 会立即触发 MoveAnimatedObjectToPathStart()
// 如果先移动、后 SetAnimatedObject会把“已摆到起点后的当前姿态”再次当成参考姿态缓存。
_pathAnimationManager?.SetAnimatedObject(newObject);
SelectedAnimatedObject = newObject;
LogManager.Info($"已选择移动物体: {SelectedAnimatedObject.DisplayName}");
}
catch (Exception ex)
{
@ -2003,11 +1989,11 @@ namespace NavisworksTransport.UI.WPF.ViewModels
if (clashResults == null || clashResults.Count == 0)
{
LogManager.Info("🎉 恭喜!本次动画未检测到任何碰撞,路径规划非常安全!");
DialogHelper.ShowAutoClosingMessageBox(
System.Windows.MessageBox.Show(
"🎉 恭喜!本次动画仿真过程中未发现任何碰撞!\n\n您的物流路径规划非常成功物体可以安全通行。",
"仿真完成 - 无碰撞",
System.Windows.MessageBoxImage.Information,
autoCloseMilliseconds: 4000);
System.Windows.MessageBoxButton.OK,
System.Windows.MessageBoxImage.Information);
}
// 统一流程:刷新历史列表并显示报告(无论有无碰撞)
@ -2135,9 +2121,6 @@ namespace NavisworksTransport.UI.WPF.ViewModels
// 1. 归位并清理
_pathAnimationManager.RestoreObjectToCADPosition();
_pathAnimationManager.ClearAnimationResults();
_pathAnimationManager.SetObjectRotationCorrectionDirect(LocalEulerRotationCorrection.Zero);
_pathAnimationManager.SetObjectStartVerticalLiftDirect(0.0);
_pathAnimationManager.SetObjectStartPlacementMode(ObjectStartPlacementMode.AlignToPathPose);
LogManager.Info("已清除PathAnimationManager中的动画数据并归位物体");
}
@ -2146,7 +2129,6 @@ namespace NavisworksTransport.UI.WPF.ViewModels
// 3. 重置角度修正值为0
ObjectRotationCorrection = LocalEulerRotationCorrection.Zero;
_objectGroundLiftHeightInMeters = 0.0;
LogManager.Debug("[清除物体] 已重置角度修正值为0");
// 4. 重置属性
@ -2173,35 +2155,24 @@ namespace NavisworksTransport.UI.WPF.ViewModels
{
try
{
bool enableGroundLift = CurrentPathRoute?.PathType == PathType.Ground;
var autoAdjustmentRequest = CreateObjectPassageProjectionOptimizationRequest();
var dialog = new Views.EditRotationWindow(
_objectRotationCorrection,
_objectGroundLiftHeightInMeters,
enableGroundLift,
autoAdjustmentRequest,
autoAdjustmentRequest == null
? (Func<ObjectPassageProjectionOptimizationResult>)null
: (() => OptimizeObjectPassageProjectionByMeasuredAabb(autoAdjustmentRequest)));
var dialog = new Views.EditRotationWindow(_objectRotationCorrection);
if (dialog.ShowDialog() == true)
{
var placementRequest = dialog.AdjustmentRequest;
_objectGroundLiftHeightInMeters = placementRequest.VerticalLiftInMeters;
_pathAnimationManager?.ApplyObjectStartPlacementRequest(placementRequest);
if (placementRequest.PreserveInitialPose)
{
ApplyTranslationOnlyObjectPlacement();
return;
}
_pathAnimationManager?.SetObjectStartPlacementMode(ObjectStartPlacementMode.AlignToPathPose);
ObjectRotationCorrection = placementRequest.RotationCorrection;
string liftSummary = enableGroundLift
? $", 上下偏移={_objectGroundLiftHeightInMeters:F3}m"
: string.Empty;
string statusSummary = placementRequest.PreserveInitialPose
? $"物体已调整为平移模式{liftSummary}"
: $"物体角度修正: {_objectRotationCorrection}{liftSummary}";
LogManager.Info(
$"物体调整已更新: 模式={placementRequest.PlacementMode}, 角度={_objectRotationCorrection}, " +
$"上下偏移={_objectGroundLiftHeightInMeters:F3}m");
UpdateMainStatus(statusSummary);
LogManager.Info($"物体角度修正已更新: {_objectRotationCorrection}");
UpdateMainStatus($"物体角度修正: {_objectRotationCorrection}");
// 应用角度修正到物体
UpdateObjectRotation();
// 更新通行空间可视化(考虑旋转后的尺寸)
UpdatePassageSpaceVisualization();
}
@ -2213,229 +2184,6 @@ namespace NavisworksTransport.UI.WPF.ViewModels
}
}
private ObjectPassageProjectionOptimizationResult OptimizeObjectPassageProjectionByMeasuredAabb(
ObjectPassageProjectionOptimizationRequest request)
{
if (_pathAnimationManager == null || !HasSelectedAnimatedObject)
{
return ObjectPassageProjectionOptimizationResult.CreateFailure("未选择动画物体,无法自动调整。");
}
ModelItem animatedObject = UseVirtualObject
? VirtualObjectManager.Instance.CurrentVirtualObject
: SelectedAnimatedObject;
if (animatedObject == null)
{
return ObjectPassageProjectionOptimizationResult.CreateFailure("动画物体为空,无法自动调整。");
}
if (!TryCreatePassageProjectionAxes(request, out Vector3 hostSide, out Vector3 hostUp))
{
return ObjectPassageProjectionOptimizationResult.CreateFailure("路径方向退化,无法计算通行截面。");
}
var originalCorrection = _objectRotationCorrection;
double originalLiftInMeters = _objectGroundLiftHeightInMeters;
ObjectPassageProjectionOptimizationResult result;
try
{
result = ObjectPassageProjectionOptimizer.OptimizeWithEvaluator(
request,
correction =>
{
// 从 correction 扣除路径 yaw动画链路会在起点再次加上
// 这样 targetYaw = pathYaw + (correctionY - pathYaw) = correctionY
// correction 成为「从 CAD 出发的总 yaw 角度」
CoordinateSystemType hst = CoordinateSystemManager.Instance.ResolvedType;
double pyAdj = 0;
if (PathTargetFrameResolver.TryResolvePlanarHostYaw(request.HostPathForward, hst, out double pyRad))
{
pyAdj = pyRad * 180.0 / Math.PI;
}
var corr = correction;
var baseAdjusted = new LocalEulerRotationCorrection(
corr.XDegrees,
hst == CoordinateSystemType.YUp ? corr.YDegrees - pyAdj : corr.YDegrees,
hst == CoordinateSystemType.ZUp ? corr.ZDegrees - pyAdj : corr.ZDegrees);
var placementRequest = ObjectStartPlacementRequest.CreateRotationCorrection(
baseAdjusted,
originalLiftInMeters);
_pathAnimationManager.ApplyObjectStartPlacementRequest(placementRequest);
BoundingBox3D bounds = animatedObject.BoundingBox();
return CalculateMeasuredAabbProjectionScore(bounds, hostSide, hostUp);
});
}
finally
{
var restoreRequest = ObjectStartPlacementRequest.CreateRotationCorrection(
originalCorrection,
originalLiftInMeters);
_pathAnimationManager.ApplyObjectStartPlacementRequest(restoreRequest);
}
if (result.Success)
{
LogManager.Info(
$"[自动调整实测] 最优角度={result.Correction}, " +
$"实测截面宽度={result.Score.WidthAcrossPath:F3}, 高度={result.Score.HeightAlongHostUp:F3}, 面积={result.Score.Area:F3}");
}
return result;
}
private ObjectPassageProjectionOptimizationRequest CreateObjectPassageProjectionOptimizationRequest()
{
if (CurrentPathRoute?.Points == null || CurrentPathRoute.Points.Count < 2)
{
LogManager.Warning("[自动调整] 当前路径点不足,无法构造优化请求");
return null;
}
if (!TryResolveStartPathDirection(out Vector3 hostPathForward))
{
LogManager.Warning("[自动调整] 路径起点方向退化,无法构造优化请求");
return null;
}
double metersToUnits = UnitsConverter.GetMetersToUnitsConversionFactor();
double sizeX;
double sizeY;
double sizeZ;
Quaternion baselineHostRotation = Quaternion.Identity;
// 基线为路径方向 yaw动画系统已旋转到该方向
var hostType = CoordinateSystemManager.Instance.ResolvedType;
if (PathTargetFrameResolver.TryResolvePlanarHostYaw(hostPathForward, hostType, out double pathYawRad))
{
var upVec = hostType == CoordinateSystemType.YUp ? Vector3.UnitY : Vector3.UnitZ;
baselineHostRotation = Quaternion.CreateFromAxisAngle(upVec, (float)pathYawRad);
LogManager.Info($"[自动调整] 基线 yaw={pathYawRad * 180.0 / Math.PI:F1}°, 四元数=({baselineHostRotation.W:F4},{baselineHostRotation.X:F4},{baselineHostRotation.Y:F4},{baselineHostRotation.Z:F4})");
}
else
{
LogManager.Warning("[自动调整] 无法从路径方向计算 yaw基线保持恒等");
}
if (UseVirtualObject)
{
sizeX = VirtualObjectLengthInMeters * metersToUnits;
sizeY = VirtualObjectWidthInMeters * metersToUnits;
sizeZ = VirtualObjectHeightInMeters * metersToUnits;
}
else if (SelectedAnimatedObject != null)
{
sizeX = _objectOriginalLength * metersToUnits;
sizeY = _objectOriginalWidth * metersToUnits;
sizeZ = _objectOriginalHeight * metersToUnits;
}
else
{
LogManager.Warning("[自动调整] 未选择动画物体,无法构造优化请求");
return null;
}
var adapter = CoordinateSystemManager.Instance.CreateHostAdapter();
return new ObjectPassageProjectionOptimizationRequest(
sizeX,
sizeY,
sizeZ,
hostPathForward,
adapter.HostUpVector3,
baselineHostRotation: baselineHostRotation);
}
private bool TryResolveStartPathDirection(out Vector3 hostPathForward)
{
hostPathForward = Vector3.Zero;
if (CurrentPathRoute?.Points == null || CurrentPathRoute.Points.Count < 2)
{
return false;
}
var start = CurrentPathRoute.Points[0];
var startVector = new Vector3((float)start.X, (float)start.Y, (float)start.Z);
for (int i = 1; i < CurrentPathRoute.Points.Count; i++)
{
var point = CurrentPathRoute.Points[i];
var candidate = new Vector3((float)point.X, (float)point.Y, (float)point.Z) - startVector;
if (candidate.LengthSquared() > 1e-8f)
{
hostPathForward = candidate;
return true;
}
}
return false;
}
private static bool TryCreatePassageProjectionAxes(
ObjectPassageProjectionOptimizationRequest request,
out Vector3 hostSide,
out Vector3 hostUp)
{
hostSide = Vector3.Zero;
hostUp = Vector3.Zero;
if (request == null || request.HostUp.LengthSquared() < 1e-8f)
{
return false;
}
hostUp = Vector3.Normalize(request.HostUp);
Vector3 hostForward = request.HostPathForward - hostUp * Vector3.Dot(request.HostPathForward, hostUp);
if (hostForward.LengthSquared() < 1e-8f)
{
return false;
}
hostForward = Vector3.Normalize(hostForward);
hostSide = Vector3.Cross(hostForward, hostUp);
if (hostSide.LengthSquared() < 1e-8f)
{
return false;
}
hostSide = Vector3.Normalize(hostSide);
return true;
}
private static ObjectPassageProjectionScore CalculateMeasuredAabbProjectionScore(
BoundingBox3D bounds,
Vector3 hostSide,
Vector3 hostUp)
{
double sizeX = bounds.Max.X - bounds.Min.X;
double sizeY = bounds.Max.Y - bounds.Min.Y;
double sizeZ = bounds.Max.Z - bounds.Min.Z;
double width = ProjectAabbExtent(sizeX, sizeY, sizeZ, hostSide);
double height = ProjectAabbExtent(sizeX, sizeY, sizeZ, hostUp);
return new ObjectPassageProjectionScore(width, height);
}
private static double ProjectAabbExtent(
double sizeX,
double sizeY,
double sizeZ,
Vector3 targetAxis)
{
Vector3 normalized = Vector3.Normalize(targetAxis);
return Math.Abs(normalized.X) * sizeX +
Math.Abs(normalized.Y) * sizeY +
Math.Abs(normalized.Z) * sizeZ;
}
private static Quaternion Rotation3DToHostQuaternion(Rotation3D rotation)
{
var linear = new Transform3D(rotation).Linear;
var matrix = new Matrix4x4(
(float)linear.Get(0, 0), (float)linear.Get(0, 1), (float)linear.Get(0, 2), 0f,
(float)linear.Get(1, 0), (float)linear.Get(1, 1), (float)linear.Get(1, 2), 0f,
(float)linear.Get(2, 0), (float)linear.Get(2, 1), (float)linear.Get(2, 2), 0f,
0f, 0f, 0f, 1f);
return Quaternion.Normalize(Quaternion.CreateFromRotationMatrix(matrix));
}
private void ApplyTranslationOnlyObjectPlacement()
{
try
@ -2447,14 +2195,13 @@ namespace NavisworksTransport.UI.WPF.ViewModels
_objectRotationCorrection = LocalEulerRotationCorrection.Zero;
_pathAnimationManager.SetObjectRotationCorrectionDirect(LocalEulerRotationCorrection.Zero);
_pathAnimationManager.SetObjectStartVerticalLiftDirect(0.0);
_pathAnimationManager.SetObjectStartPlacementMode(ObjectStartPlacementMode.PreserveInitialPose);
OnPropertyChanged(nameof(ObjectRotationCorrection));
if (_pathAnimationManager.MoveObjectToPathStartPreservingInitialPose())
{
LogManager.Info("[角度修正] 已按终点原始位姿整体搬运到路径起点");
UpdateMainStatus("物体已按终点原始位姿平移到路径起点");
LogManager.Info("[角度修正] 已按平移模式将物体移动到路径起点,保持初始位姿");
UpdateMainStatus("物体已平移到路径起点并保持初始位姿");
}
else
{
@ -2707,7 +2454,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
ModelHighlightHelper.HighlightItems(ManualTargetsHighlightCategory, items);
// 聚焦到对象斜上方45度视角
ViewpointHelper.FocusOnModelItem(target.ModelItem, ViewpointHelper.ViewpointStrategy.ModelFocus);
ViewpointHelper.FocusOnModelItem(target.ModelItem, viewAngleDegrees: 60.0, targetViewRatio: 0.25);
UpdateMainStatus($"已聚焦到: {target.DisplayName}");
LogManager.Info($"聚焦到手工指定对象: {target.DisplayName}");
@ -2733,7 +2480,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
ModelHighlightHelper.HighlightItems(ExcludedObjectsHighlightCategory, items);
// 聚焦到对象斜上方45度视角
ViewpointHelper.FocusOnModelItem(excludedObject.ModelItem, ViewpointHelper.ViewpointStrategy.ModelFocus);
ViewpointHelper.FocusOnModelItem(excludedObject.ModelItem, viewAngleDegrees: 60.0, targetViewRatio: 0.25);
UpdateMainStatus($"已聚焦到排除对象: {excludedObject.DisplayName}");
LogManager.Info($"聚焦到排除对象: {excludedObject.DisplayName}");
@ -3165,13 +2912,6 @@ namespace NavisworksTransport.UI.WPF.ViewModels
{
try
{
if (DialogHelper.IsTestAutomationModeEnabled)
{
LogManager.Info("[碰撞分析] 自动测试模式下跳过预计算碰撞分析");
SaveCollisionDetectionRecord();
return;
}
// 1. 获取预计算碰撞结果从PathAnimationManager现在缓存帧中已包含碰撞结果
var allResults = _pathAnimationManager?.AllCollisionResults;
if (allResults == null || allResults.Count == 0)
@ -3402,35 +3142,28 @@ namespace NavisworksTransport.UI.WPF.ViewModels
}
string routeId = CurrentPathRoute?.Id ?? "";
if (!DialogHelper.IsTestAutomationModeEnabled)
// 🔥 检查是否已有相同配置的检测记录
var existingRecordResult = CheckExistingDetectionRecord();
if (existingRecordResult.HasValue)
{
// 🔥 检查是否已有相同配置的检测记录
var existingRecordResult = CheckExistingDetectionRecord();
if (existingRecordResult.HasValue)
var existingId = existingRecordResult.Value.id;
var userChoice = existingRecordResult.Value.userChoice;
if (userChoice == UserChoice.UseExisting)
{
var existingId = existingRecordResult.Value.id;
var userChoice = existingRecordResult.Value.userChoice;
if (userChoice == UserChoice.UseExisting)
{
// 用户选择使用历史记录
LogManager.Info($"[检测记录] 用户选择使用历史记录 (Id={existingId})");
_pathAnimationManager.CurrentDetectionRecordId = existingId;
_pathAnimationManager.IsUsingHistoryRecord = true; // 标记使用历史记录动画完成后跳过ClashDetective
// 在历史列表中选中该记录
SelectHistoryRecordById(existingId);
return existingId;
}
// 用户选择重新生成,继续创建新记录
LogManager.Info($"[检测记录] 用户选择重新生成,创建新记录");
// 用户选择使用历史记录
LogManager.Info($"[检测记录] 用户选择使用历史记录 (Id={existingId})");
_pathAnimationManager.CurrentDetectionRecordId = existingId;
_pathAnimationManager.IsUsingHistoryRecord = true; // 标记使用历史记录动画完成后跳过ClashDetective
// 在历史列表中选中该记录
SelectHistoryRecordById(existingId);
return existingId;
}
}
else
{
LogManager.Info("[检测记录] 自动测试模式下跳过重复检测记录检查,直接创建新记录");
// 用户选择重新生成,继续创建新记录
LogManager.Info($"[检测记录] 用户选择重新生成,创建新记录");
}
int recordId = CreateCollisionDetectionRecordSnapshot("动画生成", bindToAnimationManager: true);
@ -3696,13 +3429,6 @@ namespace NavisworksTransport.UI.WPF.ViewModels
/// </summary>
private (int id, UserChoice userChoice)? ShowDuplicateConfigDialog(CollisionDetectionRecord record)
{
if (TestAutomationHttpService.ShouldAutoChooseCreateNewDetectionRecord)
{
_lastReusedRecordId = null;
LogManager.Info($"[检测记录] 测试自动化已接管重复配置对话框,自动选择重新生成新记录 (ExistingId={record.Id})");
return (record.Id, UserChoice.CreateNew);
}
var message = $"检测配置已存在!\n\n" +
$"找到一条相同配置的历史检测记录:\n" +
$"• 测试名称:{record.TestName ?? ""}\n" +
@ -4736,7 +4462,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
// 地面路径物体的长度方向X轴前进方向朝向路径方向
// 通行空间沿路径方向 = X方向尺寸长度垂直于路径方向 = Y方向尺寸宽度高度上方加间隙下方不需要因为有地面
passageAcrossPath = effectiveWidth + 2 * safetyMargin; // 旋转后宽度 + 2*间隙(垂直于路径方向)
passageNormalToPath = effectiveHeight + (_objectGroundLiftHeightInMeters * metersToUnitsPassage) + safetyMargin; // 物体高度 + 上下偏移 + 上方间隙
passageNormalToPath = effectiveHeight + safetyMargin; // 法线方向高度 + 间隙(法线方向,只有上方)
passageAlongPath = effectiveLength; // 旋转后长度(沿路径方向)
// 地面路径不需要分段参数
passageNormalToPathVertical = passageNormalToPath;
@ -4780,7 +4506,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
// 物体的长度方向X轴前进方向朝向路径方向
// 通行空间沿路径方向 = X方向尺寸长度垂直于路径方向 = Y方向尺寸宽度高度上方加间隙下方不需要因为有地面
passageAcrossPath = effectiveWidth + 2 * safetyMargin; // 旋转后宽度 + 2*间隙(垂直于路径方向)
passageNormalToPath = effectiveHeight + (_objectGroundLiftHeightInMeters * metersToUnitsPassage) + safetyMargin; // 物体高度 + 上下偏移 + 上方间隙
passageNormalToPath = effectiveHeight + safetyMargin; // 局部up方向高度 + 间隙(法线方向,只有上方)
passageAlongPath = effectiveLength; // 旋转后长度(沿路径方向)
// 地面路径不需要分段参数
passageNormalToPathVertical = passageNormalToPath;
@ -4824,7 +4550,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
passageAlongPath,
passageNormalToPathVertical,
passageNormalToPathHorizontal);
LogManager.Debug($"[通行空间可视化] 已设置通行空间参数: 沿路径={passageAlongPath / metersToUnitsPassage:F2}m, 垂直路径={passageAcrossPath / metersToUnitsPassage:F2}m, 法线={passageNormalToPath / metersToUnitsPassage:F2}m, 上下偏移={_objectGroundLiftHeightInMeters:F2}m, 安全间隙={_safetyMarginInMeters:F2}m");
LogManager.Debug($"[通行空间可视化] 已设置通行空间参数: 沿路径={passageAlongPath / metersToUnitsPassage:F2}m, 垂直路径={passageAcrossPath / metersToUnitsPassage:F2}m, 法线={passageNormalToPath / metersToUnitsPassage:F2}m, 安全间隙={_safetyMarginInMeters:F2}m");
// 根据路径类型决定是否切换到物体通行空间模式
bool enableObjectSpace = false;

View File

@ -917,7 +917,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
Title = "导出碰撞报告",
Filter = "HTML报告 (*.html)|*.html|碰撞报告 (*.txt)|*.txt|所有文件 (*.*)|*.*",
DefaultExt = "html",
FileName = PathHelper.GenerateCollisionReportFileName(CurrentReport?.PathName ?? PathName, "html")
FileName = $"NavisworksTransport_CollisionReport_{DateTime.Now:yyyyMMdd_HHmmss}"
};
if (saveFileDialog.ShowDialog() == true)
@ -946,10 +946,11 @@ namespace NavisworksTransport.UI.WPF.ViewModels
{
try
{
if (CurrentReport == null) return;
if (CurrentReport == null || !HasCollisions) return;
// 生成默认文件名和路径
var fileName = PathHelper.GenerateCollisionReportFileName(CurrentReport.PathName, "html");
var sanitizedName = PathHelper.SanitizeFileName(CurrentReport.PathName ?? "未知路径");
var fileName = $"NavisworksTransport_CollisionReport_{sanitizedName}_{DateTime.Now:yyyyMMdd_HHmmss}.html";
var reportDir = PathHelper.GetReportDirectory();
var filePath = Path.Combine(reportDir, fileName);

View File

@ -1836,14 +1836,15 @@ namespace NavisworksTransport.UI.WPF.ViewModels
var document = Autodesk.Navisworks.Api.Application.ActiveDocument;
var originalSelection = restoreSelection ? document.CurrentSelection.SelectedItems.ToList() : null;
// 获取保存路径
// 获取保存路径和导出格式
string saveFilePath = null;
bool exportAsJson = false;
System.Windows.Application.Current.Dispatcher.Invoke(() =>
{
var saveDialog = new Microsoft.Win32.SaveFileDialog
{
Title = dialogTitle,
Filter = "Navisworks文件 (*.nwd)|*.nwd",
Filter = "Navisworks文件 (*.nwd)|*.nwd|JSON文件 (*.json)|*.json",
DefaultExt = "nwd",
FileName = defaultFileName
};
@ -1851,6 +1852,9 @@ namespace NavisworksTransport.UI.WPF.ViewModels
if (saveDialog.ShowDialog() == true)
{
saveFilePath = saveDialog.FileName;
// 如果用户选择了JSONFilterIndex为2或文件扩展名为json
exportAsJson = (saveDialog.FilterIndex == 2 ||
saveFilePath.EndsWith(".json", StringComparison.OrdinalIgnoreCase));
}
});
@ -1859,10 +1863,28 @@ namespace NavisworksTransport.UI.WPF.ViewModels
return;
}
// NWD导出
var result = NwdExportHelper.ExportToNwd(items, saveFilePath);
bool exportResult = !string.IsNullOrEmpty(result);
string errorMessage = result == null ? "导出失败" : "";
bool exportResult = false;
string errorMessage = "";
if (exportAsJson)
{
// JSON导出使用 SectionBoxExporter
var exporter = new SectionBoxExporter();
await Task.Run(() =>
{
var result = exporter.ExportToJson(document, items, saveFilePath);
exportResult = !string.IsNullOrEmpty(result);
});
}
else
{
// NWD导出使用 NwdExportHelper
await Task.Run(() =>
{
var result = NwdExportHelper.ExportToNwd(items, saveFilePath);
exportResult = !string.IsNullOrEmpty(result);
});
}
// 恢复原始选择(如果需要)
if (restoreSelection && originalSelection != null)
@ -1887,14 +1909,16 @@ namespace NavisworksTransport.UI.WPF.ViewModels
if (File.Exists(saveFilePath))
{
var fileInfo = new FileInfo(saveFilePath);
string format = exportAsJson ? "JSON" : "NWD";
MessageBox.Show(
$"NWD导出成功!\n\n文件: {saveFilePath}\n大小: {fileInfo.Length / 1024} KB",
$"{format}导出成功!\n\n文件: {saveFilePath}\n大小: {fileInfo.Length / 1024} KB",
"导出结果", MessageBoxButton.OK, MessageBoxImage.Information);
}
}
else
{
MessageBox.Show($"NWD导出失败: {errorMessage}", "导出错误", MessageBoxButton.OK, MessageBoxImage.Error);
string format = exportAsJson ? "JSON" : "NWD";
MessageBox.Show($"{format}导出失败: {errorMessage}", "导出错误", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
catch (Exception ex)
@ -1910,7 +1934,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
}
/// <summary>
/// 导出剖面盒内的对象到 NWD
/// 导出剖面盒内的对象
/// </summary>
private async Task ExportSectionBoxAsync()
{
@ -1926,52 +1950,63 @@ namespace NavisworksTransport.UI.WPF.ViewModels
return;
}
// 显示保存对话框
string saveFilePath = null;
string defaultFileName = $"SectionBox_Export_{DateTime.Now:yyyyMMdd_HHmmss}";
System.Windows.Application.Current.Dispatcher.Invoke(() =>
{
var saveDialog = new Microsoft.Win32.SaveFileDialog
{
Title = "导出剖面盒",
Filter = "Navisworks文件 (*.nwd)|*.nwd",
DefaultExt = "nwd",
FileName = defaultFileName
};
// 检查剖面盒
var viewpoint = document.CurrentViewpoint.Value;
var clipPlanes = viewpoint.ClipPlanes;
if (saveDialog.ShowDialog() == true)
saveFilePath = saveDialog.FileName;
});
if (string.IsNullOrEmpty(saveFilePath))
return;
// 第一步:验证剖面盒 + 遍历模型树(后台线程)
CurrentOperationText = "正在查找剖面盒内的对象...";
var exporter = new SectionBoxExporter();
SectionBoxExporter.ExportSectionBoxResult traverseResult = await Task.Run(() =>
exporter.ValidateAndTraverse(document));
if (!traverseResult.Success)
{
MessageBox.Show(traverseResult.ErrorMessage, "提示", MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}
// 第二步导出UI 线程,内部通过 Dispatcher.Invoke 执行)
CurrentOperationText = "正在导出...";
var exportResult = exporter.ExportTraversalToNwd(document, saveFilePath);
if (exportResult.Success)
if (clipPlanes == null || !clipPlanes.Enabled)
{
MessageBox.Show(
$"NWD导出成功\n\n文件: {exportResult.FilePath}\n大小: {exportResult.FileSize / 1024} KB\n对象数: {exportResult.ObjectCount}",
"导出结果", MessageBoxButton.OK, MessageBoxImage.Information);
"当前没有活动的剖面盒!\n请先使用 Navisworks 的剖面工具创建剖面盒。",
"提示", MessageBoxButton.OK, MessageBoxImage.Information);
return;
}
else
if (clipPlanes.Mode != ClipPlaneSetMode.Box)
{
MessageBox.Show(exportResult.ErrorMessage, "提示", MessageBoxButton.OK, MessageBoxImage.Warning);
MessageBox.Show(
"当前是平面裁剪模式,请切换为剖面盒模式!\n在 Navisworks 剖面工具中选择'Box'模式。",
"提示", MessageBoxButton.OK, MessageBoxImage.Information);
return;
}
// 获取剖面盒边界
var box = clipPlanes.Box;
if (box == null)
{
MessageBox.Show("无法获取剖面盒边界!", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
return;
}
var sectionBoxBounds = new BoundingBox3D(
new Point3D(box.Min.X, box.Min.Y, box.Min.Z),
new Point3D(box.Max.X, box.Max.Y, box.Max.Z)
);
// 【优化】异步获取剖面盒内的对象和需要隐藏的节点(一次遍历完成)
CurrentOperationText = "正在查找剖面盒内的对象...";
SectionBoxExporter.SectionBoxTraversalResult traversalResult = null;
await Task.Run(() =>
{
var exporter = new SectionBoxExporter();
traversalResult = exporter.GetObjectsAndHiddenItems(document, sectionBoxBounds);
});
if (traversalResult.ObjectsInBox.Count == 0)
{
MessageBox.Show("剖面盒内没有找到对象!", "提示", MessageBoxButton.OK, MessageBoxImage.Information);
return;
}
LogManager.Info($"[LayerManagementViewModel] 剖面盒内找到 {traversalResult.ObjectsInBox.Count} 个对象,可隐藏 {traversalResult.ItemsToHide.Count} 个节点");
// 生成默认文件名
string timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss");
string defaultFileName = $"SectionBox_Export_{timestamp}";
// 【优化】使用预计算结果的导出方法
await ExportSectionBoxToNwdAsync(traversalResult, "导出剖面盒", defaultFileName);
}
catch (Exception ex)
{
@ -1994,6 +2029,99 @@ namespace NavisworksTransport.UI.WPF.ViewModels
return exporter.GetObjectsInSectionBox(document, sectionBox);
}
/// <summary>
/// 【优化】导出剖面盒到NWD使用预计算的遍历结果
/// 避免重复遍历模型树
/// </summary>
private async Task ExportSectionBoxToNwdAsync(
SectionBoxExporter.SectionBoxTraversalResult traversalResult,
string dialogTitle,
string defaultFileName)
{
if (traversalResult?.ObjectsInBox == null || traversalResult.ObjectsInBox.Count == 0)
{
MessageBox.Show("没有要导出的对象", "导出提示", MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}
try
{
LogManager.Info($"[LayerManagementViewModel] 开始导出 {traversalResult.ObjectsInBox.Count} 个对象,预计算隐藏节点 {traversalResult.ItemsToHide.Count} 个");
IsProcessing = true;
CurrentOperationText = "准备导出...";
var document = Autodesk.Navisworks.Api.Application.ActiveDocument;
// 获取保存路径
string saveFilePath = null;
System.Windows.Application.Current.Dispatcher.Invoke(() =>
{
var saveDialog = new Microsoft.Win32.SaveFileDialog
{
Title = dialogTitle,
Filter = "Navisworks文件 (*.nwd)|*.nwd",
DefaultExt = "nwd",
FileName = defaultFileName
};
if (saveDialog.ShowDialog() == true)
{
saveFilePath = saveDialog.FileName;
}
});
if (string.IsNullOrEmpty(saveFilePath))
{
return;
}
bool exportResult = false;
string errorMessage = "";
// 【优化】使用预计算结果直接导出,无需再次计算隐藏节点
await Task.Run(() =>
{
try
{
var result = NwdExportHelper.ExportToNwdWithPrecomputedHiddenItems(
traversalResult.ObjectsInBox,
traversalResult.ItemsToHide,
saveFilePath,
"剖面盒导出");
exportResult = !string.IsNullOrEmpty(result);
}
catch (Exception ex)
{
errorMessage = ex.Message;
LogManager.Error($"[LayerManagementViewModel] 导出失败: {ex.Message}");
}
});
// 显示结果
if (exportResult && File.Exists(saveFilePath))
{
var fileInfo = new FileInfo(saveFilePath);
MessageBox.Show(
$"NWD导出成功\n\n文件: {saveFilePath}\n大小: {fileInfo.Length / 1024} KB\n对象数: {traversalResult.ObjectsInBox.Count}",
"导出结果", MessageBoxButton.OK, MessageBoxImage.Information);
}
else
{
MessageBox.Show($"NWD导出失败: {errorMessage}", "导出错误", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
catch (Exception ex)
{
LogManager.Error($"[LayerManagementViewModel] 导出过程异常: {ex.Message}", ex);
MessageBox.Show($"导出过程异常: {ex.Message}", "导出错误", MessageBoxButton.OK, MessageBoxImage.Error);
}
finally
{
IsProcessing = false;
CurrentOperationText = "";
}
}
/// <summary>
/// 测试ExportToNwd API - 专门的导出API
/// </summary>

View File

@ -14,13 +14,6 @@ namespace NavisworksTransport.UI.WPF.ViewModels
/// </summary>
public class LogisticsControlViewModel : ViewModelBase
{
private enum ClipBoxFocusMode
{
None,
Path,
Selection
}
#region
private string _statusText;
@ -151,10 +144,6 @@ namespace NavisworksTransport.UI.WPF.ViewModels
}
private bool _isClipBoxEnabled;
private bool _isSelectionClipBoxEnabled;
private bool _isUpdatingClipBoxState;
private ClipBoxFocusMode _clipBoxFocusMode = ClipBoxFocusMode.None;
private readonly SelectionClipBoxLockState _selectionClipBoxLockState = new SelectionClipBoxLockState();
/// <summary>
/// 是否启用剖面盒(聚焦当前路径)
@ -166,45 +155,14 @@ namespace NavisworksTransport.UI.WPF.ViewModels
{
if (SetProperty(ref _isClipBoxEnabled, value))
{
if (_isUpdatingClipBoxState)
{
return;
}
if (value)
{
ActivatePathClipBox();
EnableClipBoxForCurrentRoute();
}
else if (_clipBoxFocusMode == ClipBoxFocusMode.Path)
else
{
ClearActiveClipBox();
}
}
}
}
/// <summary>
/// 是否启用剖面盒(聚焦当前选择对象)
/// </summary>
public bool IsSelectionClipBoxEnabled
{
get => _isSelectionClipBoxEnabled;
set
{
if (SetProperty(ref _isSelectionClipBoxEnabled, value))
{
if (_isUpdatingClipBoxState)
{
return;
}
if (value)
{
ActivateSelectionClipBox();
}
else if (_clipBoxFocusMode == ClipBoxFocusMode.Selection)
{
ClearActiveClipBox();
SectionClipHelper.ClearClipBox();
LogManager.Info("[剖面盒] 已关闭");
}
}
}
@ -280,25 +238,14 @@ namespace NavisworksTransport.UI.WPF.ViewModels
// 用于剖面盒自动跟随路径切换
_pathPlanningManager.CurrentRouteSwitched += (s, e) =>
{
if (_clipBoxFocusMode == ClipBoxFocusMode.Path)
if (IsClipBoxEnabled)
{
if (_pathPlanningManager.PathEditState == PathEditState.Creating)
{
LogManager.Info("[剖面盒] 当前处于新建路径流程,跳过自动跟随");
return;
}
LogManager.Info("[剖面盒] 路径已切换,重新设置剖面盒聚焦到新路径");
EnableClipBoxForCurrentRoute();
}
};
}
if (NavisApplication.ActiveDocument?.CurrentSelection != null)
{
NavisApplication.ActiveDocument.CurrentSelection.Changed += OnCurrentSelectionChanged;
}
// 初始化状态
StatusText = "插件已就绪";
@ -380,70 +327,6 @@ namespace NavisworksTransport.UI.WPF.ViewModels
#region
private void ActivatePathClipBox()
{
SetClipBoxState(ClipBoxFocusMode.Path);
EnableClipBoxForCurrentRoute();
}
private void ActivateSelectionClipBox()
{
SetClipBoxState(ClipBoxFocusMode.Selection);
EnableClipBoxForCurrentSelection();
}
private void ClearActiveClipBox()
{
SetClipBoxState(ClipBoxFocusMode.None);
_selectionClipBoxLockState.Clear();
SectionClipHelper.ClearClipBox();
LogManager.Info("[剖面盒] 已关闭");
}
private void SetClipBoxState(ClipBoxFocusMode mode)
{
_clipBoxFocusMode = mode;
bool pathEnabled = mode == ClipBoxFocusMode.Path;
bool selectionEnabled = mode == ClipBoxFocusMode.Selection;
_isUpdatingClipBoxState = true;
try
{
if (_isClipBoxEnabled != pathEnabled)
{
_isClipBoxEnabled = pathEnabled;
OnPropertyChanged(nameof(IsClipBoxEnabled));
}
if (_isSelectionClipBoxEnabled != selectionEnabled)
{
_isSelectionClipBoxEnabled = selectionEnabled;
OnPropertyChanged(nameof(IsSelectionClipBoxEnabled));
}
}
finally
{
_isUpdatingClipBoxState = false;
}
}
private void OnCurrentSelectionChanged(object sender, EventArgs e)
{
if (_clipBoxFocusMode != ClipBoxFocusMode.Selection)
{
return;
}
if (!_selectionClipBoxLockState.ShouldRefreshOnSelectionChanged(isSelectionClipBoxMode: true))
{
LogManager.Info($"[剖面盒] 选择已变化,但当前剖面盒保持锁定,锁定对象数: {_selectionClipBoxLockState.LockedSelectionCount}");
return;
}
LogManager.Info("[剖面盒] 选择已变化,重新聚焦到当前选择对象");
EnableClipBoxForCurrentSelection();
}
/// <summary>
/// 启用剖面盒并聚焦到当前路径
/// </summary>
@ -455,8 +338,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
if (currentRoute?.Points == null || currentRoute.Points.Count == 0)
{
LogManager.Warning("[剖面盒] 当前没有路径,无法设置剖面盒");
SectionClipHelper.ClearClipBox();
SetClipBoxState(ClipBoxFocusMode.None);
IsClipBoxEnabled = false;
return;
}
@ -502,63 +384,13 @@ namespace NavisworksTransport.UI.WPF.ViewModels
else
{
LogManager.Warning("[剖面盒] 设置失败");
SectionClipHelper.ClearClipBox();
SetClipBoxState(ClipBoxFocusMode.None);
IsClipBoxEnabled = false;
}
}
catch (Exception ex)
{
LogManager.Error($"[剖面盒] 启用失败: {ex.Message}");
SectionClipHelper.ClearClipBox();
SetClipBoxState(ClipBoxFocusMode.None);
}
}
/// <summary>
/// 启用剖面盒并聚焦到当前选择对象
/// </summary>
private void EnableClipBoxForCurrentSelection()
{
try
{
var document = NavisApplication.ActiveDocument;
var selectedItems = document?.CurrentSelection?.SelectedItems?.Cast<ModelItem>().Where(item => item != null).ToList();
if (selectedItems == null || selectedItems.Count == 0)
{
LogManager.Warning("[剖面盒] 当前没有选中的模型对象,无法设置剖面盒");
_selectionClipBoxLockState.Clear();
SectionClipHelper.ClearClipBox();
SetClipBoxState(ClipBoxFocusMode.None);
return;
}
_selectionClipBoxLockState.LockSelection(selectedItems);
SectionClipHelper.ClearClipBox();
bool success = SectionClipHelper.SetClipBoxByModelItems(
selectedItems,
marginInMeters: 2.0,
heightMarginInMeters: 2.0);
if (success)
{
ViewpointHelper.FocusOnModelItems(selectedItems, ViewpointHelper.ViewpointStrategy.ModelFocus);
LogManager.Info($"[剖面盒] 已按当前选择对象启用,选择数量: {selectedItems.Count}");
}
else
{
LogManager.Warning("[剖面盒] 按选择对象设置失败");
_selectionClipBoxLockState.Clear();
SectionClipHelper.ClearClipBox();
SetClipBoxState(ClipBoxFocusMode.None);
}
}
catch (Exception ex)
{
LogManager.Error($"[剖面盒] 按选择对象启用失败: {ex.Message}");
_selectionClipBoxLockState.Clear();
SectionClipHelper.ClearClipBox();
SetClipBoxState(ClipBoxFocusMode.None);
IsClipBoxEnabled = false;
}
}
@ -586,4 +418,4 @@ namespace NavisworksTransport.UI.WPF.ViewModels
#endregion
}
}
}

View File

@ -1597,7 +1597,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
try
{
// 聚焦到模型斜上方60度视角
ViewpointHelper.FocusOnModelItem(logisticsModel.NavisworksItem, ViewpointHelper.ViewpointStrategy.ModelFocus);
ViewpointHelper.FocusOnModelItem(logisticsModel.NavisworksItem, viewAngleDegrees: 60.0, targetViewRatio: 0.25);
UpdateMainStatus($"已聚焦到物流模型: {logisticsModel.Name}");
LogManager.Info($"聚焦到物流模型: {logisticsModel.Name}, 类别: {logisticsModel.Category}");
@ -1684,4 +1684,4 @@ namespace NavisworksTransport.UI.WPF.ViewModels
#endregion
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,154 +0,0 @@
using System.Collections.Generic;
using System.Numerics;
using Autodesk.Navisworks.Api;
namespace NavisworksTransport.UI.WPF.ViewModels
{
internal enum RailAssemblyWorkflowMode
{
None,
CreateNewRailPath,
EditSelectedRail
}
internal sealed class RailAssemblyWorkflowContext
{
private const string DefaultTerminalObjectName = "未选择";
private const string DefaultTerminalObjectInfo = "请选择终点处已安装箱体";
private const string DefaultAssemblyStartPointText = "未选择";
private readonly double _defaultReferenceRodLengthInMeters;
private readonly double _defaultReferenceRodDiameterInMeters;
private readonly double _defaultAnchorVerticalOffsetInMeters;
private readonly double _defaultSphereCenterX;
private readonly double _defaultSphereCenterY;
private readonly double _defaultSphereCenterZ;
public RailAssemblyWorkflowContext(
double defaultReferenceRodLengthInMeters,
double defaultReferenceRodDiameterInMeters,
double defaultAnchorVerticalOffsetInMeters,
double defaultSphereCenterX,
double defaultSphereCenterY,
double defaultSphereCenterZ)
{
_defaultReferenceRodLengthInMeters = defaultReferenceRodLengthInMeters;
_defaultReferenceRodDiameterInMeters = defaultReferenceRodDiameterInMeters;
_defaultAnchorVerticalOffsetInMeters = defaultAnchorVerticalOffsetInMeters;
_defaultSphereCenterX = defaultSphereCenterX;
_defaultSphereCenterY = defaultSphereCenterY;
_defaultSphereCenterZ = defaultSphereCenterZ;
TerminalObjectName = DefaultTerminalObjectName;
TerminalObjectInfo = DefaultTerminalObjectInfo;
ReferenceRodLengthInMeters = _defaultReferenceRodLengthInMeters;
ReferenceRodDiameterInMeters = _defaultReferenceRodDiameterInMeters;
AnchorVerticalOffsetInMeters = _defaultAnchorVerticalOffsetInMeters;
SphereCenterX = _defaultSphereCenterX;
SphereCenterY = _defaultSphereCenterY;
SphereCenterZ = _defaultSphereCenterZ;
StartPointText = DefaultAssemblyStartPointText;
MountMode = RailMountMode.UnderRail;
InstallationSeedPoints = new List<Point3D>();
EndFaceSeedPoints = new List<Point3D>();
}
public RailAssemblyWorkflowMode WorkflowMode { get; set; }
public string TerminalObjectName { get; set; }
public string TerminalObjectInfo { get; set; }
public double ReferenceRodLengthInMeters { get; set; }
public double ReferenceRodDiameterInMeters { get; set; }
public double AnchorVerticalOffsetInMeters { get; set; }
public double SphereCenterX { get; set; }
public double SphereCenterY { get; set; }
public double SphereCenterZ { get; set; }
public string StartPointText { get; set; }
public RailMountMode MountMode { get; set; }
public bool HasTerminalObject { get; set; }
public bool IsSelectingStartPoint { get; set; }
public bool IsSelectingEndFacePoints { get; set; }
public bool IsSelectingInstallationPoint { get; set; }
public bool HasEndFaceAnalysis { get; set; }
public bool HasInstallationReference { get; set; }
public ModelItem TerminalObject { get; set; }
public Point3D StartPoint { get; set; }
public Point3D EndFaceCenterPoint { get; set; }
public Vector3 EndFaceNormal { get; set; }
public Point3D InstallationPickPoint { get; set; }
public Point3D InstallationBaseAnchorPoint { get; set; }
public Point3D InstallationAnchorPoint { get; set; }
public Vector3 InstallationPlaneNormal { get; set; }
public Vector3 InstallationPlaneSpanDirection { get; set; }
public double InstallationOffsetDistanceInMeters { get; set; }
public string InstallationReferenceRouteId { get; set; }
public List<Point3D> InstallationSeedPoints { get; }
public List<Point3D> EndFaceSeedPoints { get; }
public void ResetEndFaceAnalysis()
{
HasEndFaceAnalysis = false;
EndFaceCenterPoint = null;
EndFaceNormal = default(Vector3);
EndFaceSeedPoints.Clear();
}
public void ResetInstallationReference()
{
HasInstallationReference = false;
InstallationPickPoint = null;
InstallationBaseAnchorPoint = null;
InstallationAnchorPoint = null;
InstallationPlaneNormal = default(Vector3);
InstallationPlaneSpanDirection = default(Vector3);
InstallationOffsetDistanceInMeters = 0.0;
InstallationReferenceRouteId = null;
InstallationSeedPoints.Clear();
AnchorVerticalOffsetInMeters = _defaultAnchorVerticalOffsetInMeters;
}
public void ResetSession()
{
WorkflowMode = RailAssemblyWorkflowMode.None;
HasTerminalObject = false;
IsSelectingStartPoint = false;
IsSelectingEndFacePoints = false;
IsSelectingInstallationPoint = false;
TerminalObject = null;
StartPoint = Point3D.Origin;
TerminalObjectName = DefaultTerminalObjectName;
TerminalObjectInfo = DefaultTerminalObjectInfo;
StartPointText = DefaultAssemblyStartPointText;
ResetEndFaceAnalysis();
ResetInstallationReference();
}
}
}

View File

@ -551,11 +551,11 @@ namespace NavisworksTransport.UI.WPF.ViewModels
_documentRotationHintShown.Add(documentKey);
_documentRotationHintPending.Remove(documentKey);
LogManager.Info($"[模型整体旋转提示] {hintMessage}");
DialogHelper.ShowAutoClosingMessageBox(
System.Windows.MessageBox.Show(
hintMessage,
"坐标系提示",
System.Windows.MessageBoxImage.Information,
autoCloseMilliseconds: 4000);
System.Windows.MessageBoxButton.OK,
System.Windows.MessageBoxImage.Information);
}
catch (Exception ex)
{
@ -832,13 +832,14 @@ namespace NavisworksTransport.UI.WPF.ViewModels
{
try
{
// 使用模态配置编辑器,确保键盘输入不会被 Navisworks 主窗口截获
// 使用单例模式显示配置编辑器(非模态,避免独占焦点)
// 不传递 owner让 ShowEditor 自己通过 DialogHelper 处理
var dialog = NavisworksTransport.UI.WPF.Views.ConfigEditorDialog.ShowEditor();
if (dialog != null)
{
UpdateMainStatus("配置编辑器已关闭");
LogManager.Info("配置编辑器:已打开并关闭(模态)");
UpdateMainStatus("配置编辑器已打开");
LogManager.Info("配置编辑器:已打开模态)");
}
else
{
@ -1022,7 +1023,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
}
else
{
DatabaseVersion = "未创建";
DatabaseVersion = "未连接";
PathCount = "--";
DetectionRecordCount = "--";
}
@ -1031,30 +1032,9 @@ namespace NavisworksTransport.UI.WPF.ViewModels
{
LogManager.Error($"刷新数据库状态失败: {ex.Message}");
DatabaseVersion = "错误";
PathCount = "--";
DetectionRecordCount = "--";
}
}
private PathDatabase GetDatabaseForSystemManagement(string operationName)
{
var pathDatabase = PathPlanningManager.Instance?.GetPathDatabase();
if (pathDatabase != null)
{
return pathDatabase;
}
System.Windows.MessageBox.Show(
"当前文档还没有数据库文件。\n\n请先执行会产生持久化数据的操作例如保存路径或先恢复一个数据库备份再使用这个功能。",
operationName,
System.Windows.MessageBoxButton.OK,
System.Windows.MessageBoxImage.Information);
UpdateMainStatus($"{operationName}未执行:当前文档没有数据库");
LogManager.Info($"[{operationName}] 当前文档没有数据库,已阻止操作");
RefreshDatabaseStatus();
return null;
}
/// <summary>
/// 执行数据备份
/// </summary>
@ -1064,8 +1044,16 @@ namespace NavisworksTransport.UI.WPF.ViewModels
{
try
{
var pathDatabase = GetDatabaseForSystemManagement("备份数据");
if (pathDatabase == null) return;
var pathDatabase = PathPlanningManager.Instance?.GetPathDatabase();
if (pathDatabase == null)
{
System.Windows.MessageBox.Show(
"未找到数据库连接,请确保已加载模型",
"备份数据",
System.Windows.MessageBoxButton.OK,
System.Windows.MessageBoxImage.Warning);
return;
}
UpdateMainStatus("正在备份数据库...");
LogManager.Info("开始数据库文件备份");
@ -1114,8 +1102,16 @@ namespace NavisworksTransport.UI.WPF.ViewModels
{
try
{
var pathDatabase = GetDatabaseForSystemManagement("恢复数据");
if (pathDatabase == null) return;
var pathDatabase = PathPlanningManager.Instance?.GetPathDatabase();
if (pathDatabase == null)
{
System.Windows.MessageBox.Show(
"未找到数据库连接,请确保已加载模型",
"恢复数据",
System.Windows.MessageBoxButton.OK,
System.Windows.MessageBoxImage.Warning);
return;
}
// 选择备份文件
var openFileDialog = new Microsoft.Win32.OpenFileDialog
@ -1198,8 +1194,16 @@ namespace NavisworksTransport.UI.WPF.ViewModels
{
try
{
var pathDatabase = GetDatabaseForSystemManagement("修复数据库");
if (pathDatabase == null) return;
var pathDatabase = PathPlanningManager.Instance?.GetPathDatabase();
if (pathDatabase == null)
{
System.Windows.MessageBox.Show(
"未找到数据库连接",
"修复数据库",
System.Windows.MessageBoxButton.OK,
System.Windows.MessageBoxImage.Warning);
return;
}
UpdateMainStatus("正在检查数据库...");
LogManager.Info("开始数据库修复检查");
@ -1246,8 +1250,16 @@ namespace NavisworksTransport.UI.WPF.ViewModels
{
try
{
var pathDatabase = GetDatabaseForSystemManagement("清空数据");
if (pathDatabase == null) return;
var pathDatabase = PathPlanningManager.Instance?.GetPathDatabase();
if (pathDatabase == null)
{
System.Windows.MessageBox.Show(
"未找到数据库连接",
"清空数据",
System.Windows.MessageBoxButton.OK,
System.Windows.MessageBoxImage.Warning);
return;
}
// 双重确认
var result1 = System.Windows.MessageBox.Show(

View File

@ -27,7 +27,6 @@ NavisworksTransport 检测动画页签视图 - 采用与类别设置和分层管
<!-- 转换器资源 -->
<converters:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter"/>
<converters:EditableNumberConverter x:Key="EditableNumberConverter"/>
<!-- 动画控制页面特有的样式 -->
<Style x:Key="ProgressBarStyle" TargetType="ProgressBar">
@ -73,7 +72,7 @@ NavisworksTransport 检测动画页签视图 - 采用与类别设置和分层管
Content="持续时间:"
Style="{StaticResource ParameterLabelStyle}"/>
<TextBox Grid.Column="4"
Text="{Binding AnimationDuration, Mode=TwoWay, UpdateSourceTrigger=LostFocus, Converter={StaticResource EditableNumberConverter}, ConverterParameter=0.0###, ValidatesOnExceptions=True, NotifyOnValidationError=True}"
Text="{Binding AnimationDuration, StringFormat=0.0###}"
Style="{StaticResource ParameterInputStyle}"/>
<Label Grid.Column="5" Content="秒" Style="{StaticResource UnitLabelStyle}"/>
</Grid>
@ -347,7 +346,7 @@ NavisworksTransport 检测动画页签视图 - 采用与类别设置和分层管
<Button Content="选择物体"
Command="{Binding SelectAnimatedObjectCommand}"
Style="{StaticResource SecondaryButtonStyle}"/>
<Button Content="调整物体"
<Button Content="调整角度"
Command="{Binding EditObjectRotationCommand}"
IsEnabled="{Binding HasSelectedAnimatedObject}"
Style="{StaticResource SecondaryButtonStyle}"/>
@ -384,7 +383,7 @@ NavisworksTransport 检测动画页签视图 - 采用与类别设置和分层管
<Run Text="m"/>
</TextBlock>
<Button Grid.Column="2"
Content="调整物体"
Content="调整角度"
Command="{Binding EditObjectRotationCommand}"
Style="{StaticResource SecondaryButtonStyle}"
Margin="10,0,0,0"/>

View File

@ -3,10 +3,8 @@ using System.Collections.Generic;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Threading;
using System.Windows.Media;
using Autodesk.Navisworks.Api;
using NavisworksTransport.Core.Services;
using NavisworksTransport.UI.WPF.ViewModels;
using NavisworksTransport.Utils;
@ -36,7 +34,6 @@ namespace NavisworksTransport.UI.WPF.Views
{
InitializeComponent();
ExcludedObjects = new List<ModelItem>();
Loaded += CollisionAnalysisDialog_Loaded;
// 更新统计信息
UpdateStatsText(hotspots.Count, totalCollisions);
@ -65,27 +62,6 @@ namespace NavisworksTransport.UI.WPF.Views
LogManager.Info($"[碰撞分析] 自动选中 {autoSelectedCount}/{viewModels.Count} 个高频碰撞物体(>100次或>50%");
}
private void CollisionAnalysisDialog_Loaded(object sender, RoutedEventArgs e)
{
if (!TestAutomationHttpService.ShouldAutoConfirmCollisionAnalysisDialogs)
{
return;
}
Dispatcher.BeginInvoke(new Action(() =>
{
if (!IsLoaded || !IsVisible)
{
return;
}
LogManager.Info("[碰撞分析] 测试自动化已接管分析窗口,自动选择继续生成");
ModelHighlightHelper.ClearCategory(PrecomputedHighlightCategory);
SelectedAction = CollisionAnalysisAction.Continue;
Close();
}), DispatcherPriority.Background);
}
/// <summary>
/// 更新统计文本
/// </summary>

View File

@ -17,7 +17,7 @@ NavisworksTransport 碰撞检测报告对话框 - 采用与主界面一致的Nav
xmlns:local="clr-namespace:NavisworksTransport.UI.WPF.Converters"
mc:Ignorable="d"
Title="碰撞检测报告"
Height="960"
Height="1080"
Width="900"
ResizeMode="CanResize"
WindowStartupLocation="CenterOwner"

View File

@ -94,8 +94,11 @@ namespace NavisworksTransport.UI.WPF.Views
{
try
{
LogManager.Info("用户关闭碰撞报告对话框,正在自动导出最终报告...");
LogManager.Info("用户关闭碰撞报告对话框,正在自动导出报告...");
// 关闭前自动导出报告
await AutoExportReportAsync();
this.Close();
}
catch (Exception ex)
@ -104,6 +107,24 @@ namespace NavisworksTransport.UI.WPF.Views
}
}
/// <summary>
/// 自动导出报告
/// </summary>
private async System.Threading.Tasks.Task AutoExportReportAsync()
{
try
{
if (_viewModel != null)
{
await _viewModel.AutoExportReportAsync();
}
}
catch (Exception ex)
{
LogManager.Error($"自动导出报告失败: {ex.Message}");
}
}
/// <summary>
/// 窗口关闭事件
/// </summary>
@ -148,14 +169,6 @@ namespace NavisworksTransport.UI.WPF.Views
}
}
private async System.Threading.Tasks.Task AutoExportReportAsync()
{
if (_viewModel != null)
{
await _viewModel.AutoExportReportAsync();
}
}
/// <summary>
/// 窗口加载事件
/// </summary>
@ -457,4 +470,4 @@ namespace NavisworksTransport.UI.WPF.Views
#endregion
}
}
}

View File

@ -2,15 +2,13 @@ using System;
using System.IO;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Threading;
using System.Diagnostics;
using NavisworksTransport.Core.Config;
namespace NavisworksTransport.UI.WPF.Views
{
/// <summary>
/// 配置编辑器对话框
/// 配置编辑器对话框 - 非模态窗口,避免独占焦点
/// </summary>
public partial class ConfigEditorDialog : Window
{
@ -299,7 +297,7 @@ namespace NavisworksTransport.UI.WPF.Views
}
/// <summary>
/// 窗口初始化后,确保获得稳定的键盘焦点
/// 窗口加载事件 - 处理激活逻辑避免独占焦点
/// </summary>
protected override void OnSourceInitialized(EventArgs e)
{
@ -307,10 +305,11 @@ namespace NavisworksTransport.UI.WPF.Views
try
{
// 激活并前置窗口,但不独占焦点
// 先置顶再取消置顶,让窗口前置但不系统级置顶
this.Activate();
this.Topmost = true;
this.Topmost = false;
Dispatcher.BeginInvoke(new Action(FocusEditorTextBox), DispatcherPriority.Input);
LogManager.Debug("配置编辑器对话框已激活并前置显示");
}
@ -320,15 +319,6 @@ namespace NavisworksTransport.UI.WPF.Views
}
}
/// <summary>
/// 窗口内容渲染完成后再次请求焦点,避免宿主窗口抢占键盘
/// </summary>
protected override void OnContentRendered(EventArgs e)
{
base.OnContentRendered(e);
Dispatcher.BeginInvoke(new Action(FocusEditorTextBox), DispatcherPriority.Input);
}
/// <summary>
/// 激活并显示已存在的窗口
/// </summary>
@ -342,10 +332,10 @@ namespace NavisworksTransport.UI.WPF.Views
this.WindowState = WindowState.Normal;
}
// 激活并前置窗口(先置顶再取消置顶,避免独占焦点)
this.Activate();
this.Topmost = true;
this.Topmost = false;
FocusEditorTextBox();
LogManager.Info("配置编辑器对话框已激活并前置显示");
}
@ -355,29 +345,10 @@ namespace NavisworksTransport.UI.WPF.Views
}
}
private void FocusEditorTextBox()
{
try
{
if (ConfigContentTextBox == null)
{
return;
}
ConfigContentTextBox.Focus();
Keyboard.Focus(ConfigContentTextBox);
ConfigContentTextBox.CaretIndex = ConfigContentTextBox.Text?.Length ?? 0;
}
catch (Exception ex)
{
LogManager.Debug($"配置编辑器焦点设置失败: {ex.Message}");
}
}
#region
/// <summary>
/// 创建并显示配置编辑器对话框(单例模式,模态)
/// 创建并显示配置编辑器对话框(单例模式,非模态)
/// </summary>
/// <returns>对话框实例</returns>
public static ConfigEditorDialog ShowEditor()
@ -414,19 +385,16 @@ namespace NavisworksTransport.UI.WPF.Views
// 创建新的对话框实例
var dialog = new ConfigEditorDialog();
// 优先设置 WPF Owner失败时回退到 Navisworks 主窗口句柄
if (!NavisworksTransport.Utils.DialogHelper.SetOwnerSafely(dialog))
{
NavisworksTransport.Utils.DialogHelper.SetWin32Owner(dialog);
}
// 使用 DialogHelper 安全地设置 owner
NavisworksTransport.Utils.DialogHelper.SetOwnerSafely(dialog);
// 设置为当前实例
_currentInstance = dialog;
// 使用 ShowDialog() 确保键盘输入不会被宿主窗口吃掉
dialog.ShowDialog();
// 使用 Show() 非模态显示,避免独占焦点
dialog.Show();
LogManager.Info("新的配置编辑器对话框已显示(模态)");
LogManager.Info("新的配置编辑器对话框已显示(模态)");
return dialog;
}
}

View File

@ -1,7 +1,6 @@
<Window x:Class="NavisworksTransport.UI.WPF.Views.EditCoordinatesWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:converters="clr-namespace:NavisworksTransport.UI.WPF.Converters"
Title="编辑路径点坐标" Height="380" Width="400"
WindowStartupLocation="CenterScreen"
ResizeMode="NoResize"
@ -13,7 +12,6 @@
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="pack://application:,,,/TransportPlugin;component/src/UI/WPF/Resources/NavisworksStyles.xaml"/>
</ResourceDictionary.MergedDictionaries>
<converters:EditableNumberConverter x:Key="EditableNumberConverter"/>
</ResourceDictionary>
</Window.Resources>
@ -65,8 +63,7 @@
</Grid.ColumnDefinitions>
<TextBlock Text="X 坐标:" VerticalAlignment="Center" FontSize="11" Foreground="#FF333333"/>
<TextBox Grid.Column="1"
x:Name="XTextBox"
Text="{Binding X, Mode=TwoWay, UpdateSourceTrigger=LostFocus, Converter={StaticResource EditableNumberConverter}, ConverterParameter=0.000, ValidatesOnExceptions=True, NotifyOnValidationError=True}"
Text="{Binding X, StringFormat=0.000, UpdateSourceTrigger=PropertyChanged}"
Height="28"
VerticalContentAlignment="Center"
Padding="8,0"
@ -83,8 +80,7 @@
</Grid.ColumnDefinitions>
<TextBlock Text="Y 坐标:" VerticalAlignment="Center" FontSize="11" Foreground="#FF333333"/>
<TextBox Grid.Column="1"
x:Name="YTextBox"
Text="{Binding Y, Mode=TwoWay, UpdateSourceTrigger=LostFocus, Converter={StaticResource EditableNumberConverter}, ConverterParameter=0.000, ValidatesOnExceptions=True, NotifyOnValidationError=True}"
Text="{Binding Y, StringFormat=0.000, UpdateSourceTrigger=PropertyChanged}"
Height="28"
VerticalContentAlignment="Center"
Padding="8,0"
@ -101,8 +97,7 @@
</Grid.ColumnDefinitions>
<TextBlock Text="Z 坐标:" VerticalAlignment="Center" FontSize="11" Foreground="#FF333333"/>
<TextBox Grid.Column="1"
x:Name="ZTextBox"
Text="{Binding Z, Mode=TwoWay, UpdateSourceTrigger=LostFocus, Converter={StaticResource EditableNumberConverter}, ConverterParameter=0.000, ValidatesOnExceptions=True, NotifyOnValidationError=True}"
Text="{Binding Z, StringFormat=0.000, UpdateSourceTrigger=PropertyChanged}"
Height="28"
VerticalContentAlignment="Center"
Padding="8,0"

View File

@ -1,10 +1,8 @@
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Media;
using System.Windows.Threading;
using NavisworksTransport.UI.WPF.Converters;
namespace NavisworksTransport.UI.WPF.Views
@ -38,11 +36,6 @@ namespace NavisworksTransport.UI.WPF.Views
private void OnSaveClick(object sender, RoutedEventArgs e)
{
if (!TryCommitInputValues())
{
return;
}
DialogResult = true;
Close();
}
@ -79,14 +72,14 @@ namespace NavisworksTransport.UI.WPF.Views
string text = Clipboard.GetText().Trim();
string[] parts = text.Split(new[] { ',', ' ', '\t' }, StringSplitOptions.RemoveEmptyEntries);
if (parts.Length >= 3 &&
EditableNumberConverter.TryParseNumber(parts[0], System.Globalization.CultureInfo.CurrentCulture, typeof(double), out object xValue) &&
EditableNumberConverter.TryParseNumber(parts[1], System.Globalization.CultureInfo.CurrentCulture, typeof(double), out object yValue) &&
EditableNumberConverter.TryParseNumber(parts[2], System.Globalization.CultureInfo.CurrentCulture, typeof(double), out object zValue))
if (parts.Length >= 3 &&
double.TryParse(parts[0], out double x) &&
double.TryParse(parts[1], out double y) &&
double.TryParse(parts[2], out double z))
{
X = (double)xValue;
Y = (double)yValue;
Z = (double)zValue;
X = x;
Y = y;
Z = z;
// 刷新绑定
DataContext = null;
DataContext = this;
@ -135,41 +128,5 @@ namespace NavisworksTransport.UI.WPF.Views
};
timer.Start();
}
private bool TryCommitInputValues()
{
return TryUpdateNumberTextBox(XTextBox, "请输入有效的 X 坐标。") &&
TryUpdateNumberTextBox(YTextBox, "请输入有效的 Y 坐标。") &&
TryUpdateNumberTextBox(ZTextBox, "请输入有效的 Z 坐标。");
}
private static bool TryUpdateNumberTextBox(TextBox textBox, string message)
{
BindingExpression binding = textBox.GetBindingExpression(TextBox.TextProperty);
if (EditableNumberConverter.IsIntermediateText(textBox.Text) ||
!EditableNumberConverter.TryParseNumber(textBox.Text.Trim(), System.Globalization.CultureInfo.CurrentCulture, typeof(double), out _))
{
ShowInputError(message, textBox);
return false;
}
binding?.UpdateSource();
if (Validation.GetHasError(textBox))
{
ShowInputError(message, textBox);
return false;
}
return true;
}
private static void ShowInputError(string message, TextBox textBox)
{
MessageBox.Show(message, "输入错误", MessageBoxButton.OK, MessageBoxImage.Warning);
textBox.Focus();
textBox.SelectAll();
}
}
}

View File

@ -1,8 +1,7 @@
<Window x:Class="NavisworksTransport.UI.WPF.Views.EditRotationWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:converters="clr-namespace:NavisworksTransport.UI.WPF.Converters"
Title="调整物体" Height="380" Width="470"
Title="调整物体角度" Height="360" Width="400"
WindowStartupLocation="CenterScreen"
ResizeMode="NoResize"
ShowInTaskbar="False"
@ -13,7 +12,6 @@
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="pack://application:,,,/TransportPlugin;component/src/UI/WPF/Resources/NavisworksStyles.xaml"/>
</ResourceDictionary.MergedDictionaries>
<converters:EditableNumberConverter x:Key="EditableNumberConverter"/>
</ResourceDictionary>
</Window.Resources>
@ -27,7 +25,8 @@
<!-- 标题栏 -->
<Border Grid.Row="0" Background="#FFF8FBFF" BorderBrush="#FFD4E7FF" BorderThickness="0,0,0,1" Padding="20,15">
<StackPanel>
<TextBlock Text="调整物体" FontWeight="SemiBold" FontSize="14" Foreground="#FF2B579A"/>
<TextBlock Text="调整物体角度" FontWeight="SemiBold" FontSize="14" Foreground="#FF2B579A"/>
<TextBlock Text="设置物体在路径起点处绕宿主 X/Y/Z 轴的旋转角度" FontSize="10" Foreground="#FF666666" Margin="0,3,0,0"/>
</StackPanel>
</Border>
@ -45,7 +44,7 @@
<TextBox Grid.Column="1"
x:Name="XAxisTextBox"
GotFocus="OnAxisTextBoxGotFocus"
Text="{Binding RotationXDegrees, Mode=TwoWay, UpdateSourceTrigger=Explicit, Converter={StaticResource EditableNumberConverter}, ConverterParameter=0.###, ValidatesOnExceptions=True, NotifyOnValidationError=True}"
Text="{Binding RotationXDegrees, StringFormat=0.0, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
Height="28"
VerticalContentAlignment="Center"
Padding="8,0"
@ -63,41 +62,10 @@
<ColumnDefinition Width="50"/>
</Grid.ColumnDefinitions>
<TextBlock Text="绕 Y 轴:" VerticalAlignment="Center" FontSize="11" Foreground="#FF333333"/>
<TextBox Grid.Column="1"
<TextBox Grid.Column="1"
x:Name="YAxisTextBox"
GotFocus="OnAxisTextBoxGotFocus"
Text="{Binding RotationYDegrees, Mode=TwoWay, UpdateSourceTrigger=Explicit, Converter={StaticResource EditableNumberConverter}, ConverterParameter=0.###, ValidatesOnExceptions=True, NotifyOnValidationError=True}"
Height="28"
VerticalContentAlignment="Center"
Padding="8,0"
FontSize="11"
BorderBrush="#FFCCCCCC"
BorderThickness="1"/>
<StackPanel Grid.Column="2" Orientation="Horizontal" VerticalAlignment="Center">
<TextBlock Text="°" FontSize="11" Foreground="#FF666666" Margin="8,0,4,0" VerticalAlignment="Center"/>
<Button x:Name="YAxisRotateButton"
Click="OnYAxisRotateClick"
Style="{StaticResource IconButtonStyle}"
ToolTip="当前值增加 90°">
<Path Data="{StaticResource RotateIconGeometry}"
Fill="{StaticResource NavisworksPrimaryBrush}"
Width="16" Height="16" Stretch="Uniform"/>
</Button>
</StackPanel>
</Grid>
<!-- Z轴 -->
<Grid Margin="0,0,0,8">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="110"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="50"/>
</Grid.ColumnDefinitions>
<TextBlock Text="绕 Z 轴:" VerticalAlignment="Center" FontSize="11" Foreground="#FF333333"/>
<TextBox Grid.Column="1"
x:Name="ZAxisTextBox"
GotFocus="OnAxisTextBoxGotFocus"
Text="{Binding RotationZDegrees, Mode=TwoWay, UpdateSourceTrigger=Explicit, Converter={StaticResource EditableNumberConverter}, ConverterParameter=0.###, ValidatesOnExceptions=True, NotifyOnValidationError=True}"
Text="{Binding RotationYDegrees, StringFormat=0.0, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
Height="28"
VerticalContentAlignment="Center"
Padding="8,0"
@ -107,29 +75,30 @@
<TextBlock Grid.Column="2" Text="°" VerticalAlignment="Center" FontSize="11" Foreground="#FF666666" Margin="8,0,0,0"/>
</Grid>
<Grid Margin="0,0,0,8">
<!-- Z轴 -->
<Grid Margin="0,0,0,12">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="110"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="50"/>
</Grid.ColumnDefinitions>
<TextBlock Text="上下偏移:" VerticalAlignment="Center" FontSize="11" Foreground="#FF333333"/>
<TextBox Grid.Column="1"
x:Name="GroundLiftTextBox"
Text="{Binding GroundPathLiftHeightInMeters, Mode=TwoWay, UpdateSourceTrigger=Explicit, Converter={StaticResource EditableNumberConverter}, ConverterParameter=0.###, ValidatesOnExceptions=True, NotifyOnValidationError=True}"
IsEnabled="{Binding IsGroundLiftEnabled}"
<TextBlock Text="绕 Z 轴:" VerticalAlignment="Center" FontSize="11" Foreground="#FF333333"/>
<TextBox Grid.Column="1"
x:Name="ZAxisTextBox"
GotFocus="OnAxisTextBoxGotFocus"
Text="{Binding RotationZDegrees, StringFormat=0.0, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
Height="28"
VerticalContentAlignment="Center"
Padding="8,0"
FontSize="11"
BorderBrush="#FFCCCCCC"
BorderThickness="1"/>
<TextBlock Grid.Column="2" Text="m" VerticalAlignment="Center" FontSize="11" Foreground="#FF666666" Margin="8,0,0,0"/>
<TextBlock Grid.Column="2" Text="°" VerticalAlignment="Center" FontSize="11" Foreground="#FF666666" Margin="8,0,0,0"/>
</Grid>
<!-- 快捷按钮 -->
<TextBlock Text="快捷角度(作用于当前焦点输入框):" FontSize="10" Foreground="#FF666666" Margin="0,0,0,8"/>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
<StackPanel Orientation="Horizontal">
<Button Content="0°"
Click="OnQuickAngleClick"
Tag="0"
@ -163,35 +132,29 @@
<!-- 按钮栏 -->
<Border Grid.Row="2" Background="#FFF8FBFF" BorderBrush="#FFD4E7FF" BorderThickness="0,1,0,0" Padding="20,12">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
<Button Content="初始值"
Click="OnResetClick"
Style="{StaticResource SecondaryButtonStyle}"
Width="64"
Width="70"
Height="32"
Margin="0,0,8,0"/>
<Button Content="自动调整"
Click="OnAutoAdjustClick"
Style="{StaticResource SecondaryButtonStyle}"
Width="64"
Height="32"
Margin="0,0,8,0"/>
Margin="0,0,10,0"/>
<Button Content="平移"
Click="OnTranslateClick"
Style="{StaticResource SecondaryButtonStyle}"
Width="64"
Width="70"
Height="32"
Margin="0,0,8,0"/>
Margin="0,0,10,0"/>
<Button Content="确认"
Click="OnConfirmClick"
Style="{StaticResource ActionButtonStyle}"
Width="64"
Width="70"
Height="32"
Margin="0,0,8,0"/>
Margin="0,0,10,0"/>
<Button Content="取消"
Click="OnCancelClick"
Style="{StaticResource SecondaryButtonStyle}"
Width="64"
Width="70"
Height="32"/>
</StackPanel>
</Border>

View File

@ -3,8 +3,6 @@ using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using NavisworksTransport.UI.WPF.Converters;
using NavisworksTransport.Utils.CoordinateSystem;
namespace NavisworksTransport.UI.WPF.Views
@ -21,12 +19,8 @@ namespace NavisworksTransport.UI.WPF.Views
private double _rotationXDegrees;
private double _rotationYDegrees;
private double _rotationZDegrees;
private double _groundPathLiftHeightInMeters;
private RotationAxisTarget _activeAxisTarget = RotationAxisTarget.Z;
private ObjectStartPlacementRequest _adjustmentRequest;
private readonly ObjectPassageProjectionOptimizationRequest _autoAdjustmentRequest;
private readonly Func<ObjectPassageProjectionOptimizationResult> _autoAdjustmentProvider;
private readonly bool _isGroundLiftEnabled;
public double RotationXDegrees
{
@ -73,33 +67,9 @@ namespace NavisworksTransport.UI.WPF.Views
public LocalEulerRotationCorrection RotationCorrection =>
new LocalEulerRotationCorrection(RotationXDegrees, RotationYDegrees, RotationZDegrees);
public double GroundPathLiftHeightInMeters
{
get => _groundPathLiftHeightInMeters;
set
{
if (Math.Abs(_groundPathLiftHeightInMeters - value) > 1e-9)
{
_groundPathLiftHeightInMeters = value;
OnPropertyChanged();
}
}
}
public bool IsGroundLiftEnabled => _isGroundLiftEnabled;
public string GroundLiftHint => _isGroundLiftEnabled
? "仅地面路径生效,正值向上偏移,负值向下偏移。"
: "上下偏移当前仅对地面路径生效。";
public ObjectStartPlacementRequest AdjustmentRequest => _adjustmentRequest;
public EditRotationWindow(
LocalEulerRotationCorrection currentRotation,
double currentGroundLiftHeightInMeters,
bool isGroundLiftEnabled,
ObjectPassageProjectionOptimizationRequest autoAdjustmentRequest = null,
Func<ObjectPassageProjectionOptimizationResult> autoAdjustmentProvider = null)
public EditRotationWindow(LocalEulerRotationCorrection currentRotation)
{
try
{
@ -107,11 +77,7 @@ namespace NavisworksTransport.UI.WPF.Views
RotationXDegrees = currentRotation.XDegrees;
RotationYDegrees = currentRotation.YDegrees;
RotationZDegrees = currentRotation.ZDegrees;
_isGroundLiftEnabled = isGroundLiftEnabled;
_autoAdjustmentRequest = autoAdjustmentRequest;
_autoAdjustmentProvider = autoAdjustmentProvider;
GroundPathLiftHeightInMeters = currentGroundLiftHeightInMeters;
_adjustmentRequest = ObjectStartPlacementRequest.CreateRotationCorrection(currentRotation, currentGroundLiftHeightInMeters);
_adjustmentRequest = ObjectStartPlacementRequest.CreateRotationCorrection(currentRotation);
DataContext = this;
Loaded += (sender, args) => ZAxisTextBox.Focus();
}
@ -122,40 +88,6 @@ namespace NavisworksTransport.UI.WPF.Views
}
}
private void OnYAxisRotateClick(object sender, RoutedEventArgs e)
{
double newValue = RotationYDegrees + 90.0;
RotationYDegrees = NormalizeDisplayDegrees(newValue);
YAxisTextBox.Focus();
YAxisTextBox.SelectAll();
LogManager.Debug($"[Y轴旋转] 增加 90°: {newValue:F3} → {RotationYDegrees:F3}");
}
private void OnAutoAdjustClick(object sender, RoutedEventArgs e)
{
if (_autoAdjustmentProvider == null && _autoAdjustmentRequest == null)
{
MessageBox.Show("当前路径或物体尺寸不足,无法自动调整。", "自动调整", MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}
ObjectPassageProjectionOptimizationResult result = _autoAdjustmentProvider != null
? _autoAdjustmentProvider()
: ObjectPassageProjectionOptimizer.Optimize(_autoAdjustmentRequest);
if (!result.Success)
{
MessageBox.Show(result.ErrorMessage ?? "自动调整失败。", "自动调整", MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}
RotationXDegrees = NormalizeDisplayDegrees(result.Correction.XDegrees);
RotationYDegrees = NormalizeDisplayDegrees(result.Correction.YDegrees);
RotationZDegrees = NormalizeDisplayDegrees(result.Correction.ZDegrees);
LogManager.Info(
$"[自动调整] 已写入角度: {RotationCorrection}, " +
$"截面宽度={result.Score.WidthAcrossPath:F3}, 高度={result.Score.HeightAlongHostUp:F3}, 面积={result.Score.Area:F3}");
}
private void OnQuickAngleClick(object sender, RoutedEventArgs e)
{
if (sender is Button button)
@ -213,24 +145,14 @@ namespace NavisworksTransport.UI.WPF.Views
private void OnConfirmClick(object sender, RoutedEventArgs e)
{
if (!TryCommitInputValues())
{
return;
}
_adjustmentRequest = ObjectStartPlacementRequest.CreateRotationCorrection(RotationCorrection, GroundPathLiftHeightInMeters);
_adjustmentRequest = ObjectStartPlacementRequest.CreateRotationCorrection(RotationCorrection);
DialogResult = true;
Close();
}
private void OnTranslateClick(object sender, RoutedEventArgs e)
{
if (!TryCommitInputValues())
{
return;
}
_adjustmentRequest = ObjectStartPlacementRequest.CreateTranslationOnly(GroundPathLiftHeightInMeters);
_adjustmentRequest = ObjectStartPlacementRequest.TranslationOnly;
DialogResult = true;
Close();
}
@ -264,83 +186,5 @@ namespace NavisworksTransport.UI.WPF.Views
break;
}
}
private bool TryCommitInputValues()
{
if (!TryUpdateNumberTextBox(XAxisTextBox, "请输入有效的 X 轴角度。"))
{
return false;
}
if (!TryUpdateNumberTextBox(YAxisTextBox, "请输入有效的 Y 轴角度。"))
{
return false;
}
if (!TryUpdateNumberTextBox(ZAxisTextBox, "请输入有效的 Z 轴角度。"))
{
return false;
}
if (!TryUpdateNumberTextBox(GroundLiftTextBox, "请输入有效的上下偏移值。"))
{
return false;
}
return true;
}
private static bool TryUpdateNumberTextBox(TextBox textBox, string message)
{
BindingExpression binding = textBox.GetBindingExpression(TextBox.TextProperty);
if (EditableNumberConverter.IsIntermediateText(textBox.Text))
{
ShowInputError(message, textBox);
return false;
}
if (!EditableNumberConverter.TryParseNumber(textBox.Text.Trim(), System.Globalization.CultureInfo.CurrentCulture, typeof(double), out _))
{
ShowInputError(message, textBox);
return false;
}
try
{
binding?.UpdateSource();
}
catch (Exception)
{
ShowInputError(message, textBox);
return false;
}
if (Validation.GetHasError(textBox))
{
ShowInputError(message, textBox);
return false;
}
return true;
}
private static void ShowInputError(string message, TextBox textBox)
{
MessageBox.Show(message, "输入错误", MessageBoxButton.OK, MessageBoxImage.Warning);
textBox.Focus();
textBox.SelectAll();
}
private static double NormalizeDisplayDegrees(double degrees)
{
double normalized = degrees % 360.0;
if (normalized < 0.0)
{
normalized += 360.0;
}
return Math.Abs(normalized) < 1e-9 ? 0.0 : normalized;
}
}
}

View File

@ -146,48 +146,18 @@ NavisworksTransport 主控制面板 - 采用与其他视图一致的Navisworks 2
Margin="8,0,6,0"
Foreground="#FFCCCCCC"
FontSize="14"/>
<!-- 选择对象剖面盒开关 -->
<ToggleButton IsChecked="{Binding IsSelectionClipBoxEnabled}"
ToolTip="剖面盒(聚焦当前选择对象)"
Width="28" Height="24"
Margin="2,0">
<Viewbox Width="18" Height="18" Stretch="Uniform">
<Canvas Width="20" Height="20">
<Path Data="M 2,6 L 14,6 L 14,18 L 2,18 Z M 2,6 L 6,2 L 18,2 L 14,6 M 14,18 L 18,14 L 18,2 L 14,6"
Stroke="{Binding IsSelectionClipBoxEnabled, Converter={StaticResource BoolToBrushConverter}, ConverterParameter=Green}"
StrokeThickness="1.3"
Fill="Transparent"/>
<Rectangle Canvas.Left="6"
Canvas.Top="10"
Width="4"
Height="4"
Stroke="{Binding IsSelectionClipBoxEnabled, Converter={StaticResource BoolToBrushConverter}, ConverterParameter=Green}"
StrokeThickness="1.2"
Fill="Transparent"/>
</Canvas>
</Viewbox>
</ToggleButton>
<!-- 路径剖面盒开关 -->
<!-- 剖面盒开关 -->
<ToggleButton IsChecked="{Binding IsClipBoxEnabled}"
ToolTip="剖面盒(聚焦当前路径)"
Width="28" Height="24"
Margin="2,0">
<Viewbox Width="18" Height="18" Stretch="Uniform">
<Canvas Width="20" Height="20">
<Path Data="M 2,6 L 14,6 L 14,18 L 2,18 Z M 2,6 L 6,2 L 18,2 L 14,6 M 14,18 L 18,14 L 18,2 L 14,6"
Stroke="{Binding IsClipBoxEnabled, Converter={StaticResource BoolToBrushConverter}, ConverterParameter=Green}"
StrokeThickness="1.3"
Fill="Transparent"/>
<Polyline Points="5.5,13 7.2,11 8.1,12 10.5,10"
Stroke="{Binding IsClipBoxEnabled, Converter={StaticResource BoolToBrushConverter}, ConverterParameter=Green}"
StrokeThickness="1.2"
StrokeLineJoin="Round"
StrokeStartLineCap="Round"
StrokeEndLineCap="Round"/>
</Canvas>
</Viewbox>
<Path Data="M 2,6 L 14,6 L 14,18 L 2,18 Z M 2,6 L 6,2 L 18,2 L 14,6 M 14,18 L 18,14 L 18,2 L 14,6"
Stroke="{Binding IsClipBoxEnabled, Converter={StaticResource BoolToBrushConverter}, ConverterParameter=Red}"
StrokeThickness="1.5"
Fill="Transparent"
Stretch="Uniform"
Margin="2"/>
</ToggleButton>
</StackPanel>

View File

@ -137,13 +137,6 @@ namespace NavisworksTransport.UI.WPF
LogManager.Info("使用已存在的PathPlanningManager实例");
}
// 连接文档对应的路径数据库(覆盖插件后于文档加载的场景)
var activeDoc = NavisApplication.ActiveDocument;
if (!string.IsNullOrEmpty(activeDoc?.FileName))
{
_pathPlanningManager.DatabaseInitialize(createIfMissing: false);
}
LogManager.Info("PathPlanningManager初始化完成");
}
catch (Exception ex)

View File

@ -1,7 +1,7 @@
<Window x:Class="NavisworksTransport.UI.WPF.Views.ModelItemBoundsWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="元素包围盒信息" Height="640" Width="440"
Title="元素包围盒信息" Height="460" Width="420"
WindowStartupLocation="CenterScreen"
ResizeMode="CanResize"
ShowInTaskbar="True"
@ -26,7 +26,7 @@
<Border Grid.Row="0" Background="#FFF8FBFF" BorderBrush="#FFD4E7FF" BorderThickness="0,0,0,1" Padding="20,12">
<StackPanel>
<TextBlock Text="元素包围盒信息" FontWeight="SemiBold" FontSize="14" Foreground="#FF2B579A"/>
<TextBlock Text="同时显示宿主包围盒顶/底中心与对象姿态语义顶/底中心(单位:模型单位)" FontSize="10" Foreground="#FF666666" Margin="0,3,0,0"/>
<TextBlock Text="当前选中模型元素的包围盒信息(单位:模型单位)" FontSize="10" Foreground="#FF666666" Margin="0,3,0,0"/>
</StackPanel>
</Border>
@ -138,8 +138,8 @@
</Button>
</Grid>
<!-- 宿主包围盒顶面中心点 -->
<TextBlock Text="宿主包围盒顶面中心点坐标" FontWeight="SemiBold" FontSize="11" Foreground="#FF333333" Margin="0,0,0,6"/>
<!-- 顶面中心点 -->
<TextBlock Text="顶面中心点坐标" FontWeight="SemiBold" FontSize="11" Foreground="#FF333333" Margin="0,0,0,6"/>
<Grid Margin="0,0,0,12">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
@ -155,7 +155,7 @@
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="X:" FontSize="11" Foreground="#FF666666" VerticalAlignment="Center" Margin="0,0,3,0"/>
<TextBox Grid.Column="1" Text="{Binding HostTopCenterX, StringFormat=0.000}" FontSize="11" IsReadOnly="True" Padding="4,2"/>
<TextBox Grid.Column="1" Text="{Binding TopCenterX, StringFormat=0.000}" FontSize="11" IsReadOnly="True" Padding="4,2"/>
</Grid>
<!-- Y -->
@ -165,7 +165,7 @@
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="Y:" FontSize="11" Foreground="#FF666666" VerticalAlignment="Center" Margin="0,0,3,0"/>
<TextBox Grid.Column="1" Text="{Binding HostTopCenterY, StringFormat=0.000}" FontSize="11" IsReadOnly="True" Padding="4,2"/>
<TextBox Grid.Column="1" Text="{Binding TopCenterY, StringFormat=0.000}" FontSize="11" IsReadOnly="True" Padding="4,2"/>
</Grid>
<!-- Z -->
@ -175,7 +175,7 @@
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="Z:" FontSize="11" Foreground="#FF666666" VerticalAlignment="Center" Margin="0,0,3,0"/>
<TextBox Grid.Column="1" Text="{Binding HostTopCenterZ, StringFormat=0.000}" FontSize="11" IsReadOnly="True" Padding="4,2"/>
<TextBox Grid.Column="1" Text="{Binding TopCenterZ, StringFormat=0.000}" FontSize="11" IsReadOnly="True" Padding="4,2"/>
</Grid>
<!-- 复制按钮 -->
@ -183,7 +183,7 @@
x:Name="CopyTopButton"
Style="{StaticResource IconButtonStyle}"
Click="OnCopyTopCenterClick"
ToolTip="复制宿主包围盒顶面中心点坐标"
ToolTip="复制顶面中心点坐标"
Margin="3,0,0,0">
<Path Data="{StaticResource CopyIconGeometry}"
Fill="{StaticResource NavisworksPrimaryBrush}"
@ -191,9 +191,9 @@
</Button>
</Grid>
<!-- 宿主包围盒底面中心点 -->
<TextBlock Text="宿主包围盒底面中心点坐标" FontWeight="SemiBold" FontSize="11" Foreground="#FF333333" Margin="0,0,0,6"/>
<Grid Margin="0,0,0,12">
<!-- 底面中心点 -->
<TextBlock Text="底面中心点坐标" FontWeight="SemiBold" FontSize="11" Foreground="#FF333333" Margin="0,0,0,6"/>
<Grid Margin="0,0,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
@ -208,7 +208,7 @@
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="X:" FontSize="11" Foreground="#FF666666" VerticalAlignment="Center" Margin="0,0,3,0"/>
<TextBox Grid.Column="1" Text="{Binding HostBottomCenterX, StringFormat=0.000}" FontSize="11" IsReadOnly="True" Padding="4,2"/>
<TextBox Grid.Column="1" Text="{Binding BottomCenterX, StringFormat=0.000}" FontSize="11" IsReadOnly="True" Padding="4,2"/>
</Grid>
<!-- Y -->
@ -218,7 +218,7 @@
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="Y:" FontSize="11" Foreground="#FF666666" VerticalAlignment="Center" Margin="0,0,3,0"/>
<TextBox Grid.Column="1" Text="{Binding HostBottomCenterY, StringFormat=0.000}" FontSize="11" IsReadOnly="True" Padding="4,2"/>
<TextBox Grid.Column="1" Text="{Binding BottomCenterY, StringFormat=0.000}" FontSize="11" IsReadOnly="True" Padding="4,2"/>
</Grid>
<!-- Z -->
@ -228,7 +228,7 @@
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="Z:" FontSize="11" Foreground="#FF666666" VerticalAlignment="Center" Margin="0,0,3,0"/>
<TextBox Grid.Column="1" Text="{Binding HostBottomCenterZ, StringFormat=0.000}" FontSize="11" IsReadOnly="True" Padding="4,2"/>
<TextBox Grid.Column="1" Text="{Binding BottomCenterZ, StringFormat=0.000}" FontSize="11" IsReadOnly="True" Padding="4,2"/>
</Grid>
<!-- 复制按钮 -->
@ -236,111 +236,13 @@
x:Name="CopyBottomButton"
Style="{StaticResource IconButtonStyle}"
Click="OnCopyBottomCenterClick"
ToolTip="复制宿主包围盒底面中心点坐标"
ToolTip="复制底面中心点坐标"
Margin="3,0,0,0">
<Path Data="{StaticResource CopyIconGeometry}"
Fill="{StaticResource NavisworksPrimaryBrush}"
Width="14" Height="14" Stretch="Uniform"/>
</Button>
</Grid>
<!-- 对象姿态语义顶面中心点 -->
<TextBlock Text="对象姿态语义顶面中心点坐标" FontWeight="SemiBold" FontSize="11" Foreground="#FF333333" Margin="0,0,0,6"/>
<Grid Margin="0,0,0,12">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Grid Grid.Column="0" Margin="0,0,3,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="X:" FontSize="11" Foreground="#FF666666" VerticalAlignment="Center" Margin="0,0,3,0"/>
<TextBox Grid.Column="1" Text="{Binding OrientedTopCenterX, StringFormat=0.000}" FontSize="11" IsReadOnly="True" Padding="4,2"/>
</Grid>
<Grid Grid.Column="1" Margin="3,0,3,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="Y:" FontSize="11" Foreground="#FF666666" VerticalAlignment="Center" Margin="0,0,3,0"/>
<TextBox Grid.Column="1" Text="{Binding OrientedTopCenterY, StringFormat=0.000}" FontSize="11" IsReadOnly="True" Padding="4,2"/>
</Grid>
<Grid Grid.Column="2" Margin="3,0,3,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="Z:" FontSize="11" Foreground="#FF666666" VerticalAlignment="Center" Margin="0,0,3,0"/>
<TextBox Grid.Column="1" Text="{Binding OrientedTopCenterZ, StringFormat=0.000}" FontSize="11" IsReadOnly="True" Padding="4,2"/>
</Grid>
<Button Grid.Column="3"
x:Name="CopyOrientedTopButton"
Style="{StaticResource IconButtonStyle}"
Click="OnCopyOrientedTopCenterClick"
ToolTip="复制对象姿态语义顶面中心点坐标"
Margin="3,0,0,0">
<Path Data="{StaticResource CopyIconGeometry}"
Fill="{StaticResource NavisworksPrimaryBrush}"
Width="14" Height="14" Stretch="Uniform"/>
</Button>
</Grid>
<!-- 对象姿态语义底面中心点 -->
<TextBlock Text="对象姿态语义底面中心点坐标" FontWeight="SemiBold" FontSize="11" Foreground="#FF333333" Margin="0,0,0,6"/>
<Grid Margin="0,0,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Grid Grid.Column="0" Margin="0,0,3,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="X:" FontSize="11" Foreground="#FF666666" VerticalAlignment="Center" Margin="0,0,3,0"/>
<TextBox Grid.Column="1" Text="{Binding OrientedBottomCenterX, StringFormat=0.000}" FontSize="11" IsReadOnly="True" Padding="4,2"/>
</Grid>
<Grid Grid.Column="1" Margin="3,0,3,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="Y:" FontSize="11" Foreground="#FF666666" VerticalAlignment="Center" Margin="0,0,3,0"/>
<TextBox Grid.Column="1" Text="{Binding OrientedBottomCenterY, StringFormat=0.000}" FontSize="11" IsReadOnly="True" Padding="4,2"/>
</Grid>
<Grid Grid.Column="2" Margin="3,0,3,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="Z:" FontSize="11" Foreground="#FF666666" VerticalAlignment="Center" Margin="0,0,3,0"/>
<TextBox Grid.Column="1" Text="{Binding OrientedBottomCenterZ, StringFormat=0.000}" FontSize="11" IsReadOnly="True" Padding="4,2"/>
</Grid>
<Button Grid.Column="3"
x:Name="CopyOrientedBottomButton"
Style="{StaticResource IconButtonStyle}"
Click="OnCopyOrientedBottomCenterClick"
ToolTip="复制对象姿态语义底面中心点坐标"
Margin="3,0,0,0">
<Path Data="{StaticResource CopyIconGeometry}"
Fill="{StaticResource NavisworksPrimaryBrush}"
Width="14" Height="14" Stretch="Uniform"/>
</Button>
</Grid>
</StackPanel>
</Border>

View File

@ -4,8 +4,6 @@ using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Threading;
using Autodesk.Navisworks.Api;
using NavisworksTransport.Utils;
using NavisworksTransport.Utils.CoordinateSystem;
namespace NavisworksTransport.UI.WPF.Views
{
@ -37,41 +35,23 @@ namespace NavisworksTransport.UI.WPF.Views
/// <summary>中心点Z坐标</summary>
public double CenterZ { get; set; }
/// <summary>宿主包围盒顶面中心点X坐标</summary>
public double HostTopCenterX { get; set; }
/// <summary>顶面中心点X坐标</summary>
public double TopCenterX { get; set; }
/// <summary>宿主包围盒顶面中心点Y坐标</summary>
public double HostTopCenterY { get; set; }
/// <summary>顶面中心点Y坐标</summary>
public double TopCenterY { get; set; }
/// <summary>宿主包围盒顶面中心点Z坐标</summary>
public double HostTopCenterZ { get; set; }
/// <summary>顶面中心点Z坐标</summary>
public double TopCenterZ { get; set; }
/// <summary>宿主包围盒底面中心点X坐标</summary>
public double HostBottomCenterX { get; set; }
/// <summary>底面中心点X坐标</summary>
public double BottomCenterX { get; set; }
/// <summary>宿主包围盒底面中心点Y坐标</summary>
public double HostBottomCenterY { get; set; }
/// <summary>底面中心点Y坐标</summary>
public double BottomCenterY { get; set; }
/// <summary>宿主包围盒底面中心点Z坐标</summary>
public double HostBottomCenterZ { get; set; }
/// <summary>对象姿态语义顶面中心点X坐标</summary>
public double OrientedTopCenterX { get; set; }
/// <summary>对象姿态语义顶面中心点Y坐标</summary>
public double OrientedTopCenterY { get; set; }
/// <summary>对象姿态语义顶面中心点Z坐标</summary>
public double OrientedTopCenterZ { get; set; }
/// <summary>对象姿态语义底面中心点X坐标</summary>
public double OrientedBottomCenterX { get; set; }
/// <summary>对象姿态语义底面中心点Y坐标</summary>
public double OrientedBottomCenterY { get; set; }
/// <summary>对象姿态语义底面中心点Z坐标</summary>
public double OrientedBottomCenterZ { get; set; }
/// <summary>底面中心点Z坐标</summary>
public double BottomCenterZ { get; set; }
#endregion
@ -147,36 +127,15 @@ namespace NavisworksTransport.UI.WPF.Views
CenterY = (bounds.Min.Y + bounds.Max.Y) / 2.0;
CenterZ = (bounds.Min.Z + bounds.Max.Z) / 2.0;
HostCoordinateAdapter adapter = CoordinateSystemManager.Instance.CreateHostAdapter();
// 计算顶面中心点Z最大
TopCenterX = CenterX;
TopCenterY = CenterY;
TopCenterZ = bounds.Max.Z;
Point3D hostBottomCenter = ModelItemTransformHelper.GetHostBottomAnchorPoint(bounds, adapter);
double hostHeight = ModelItemTransformHelper.GetHostHeight(bounds, adapter);
Point3D hostTopCenter = new Point3D(
hostBottomCenter.X + adapter.HostUpVector.X * hostHeight,
hostBottomCenter.Y + adapter.HostUpVector.Y * hostHeight,
hostBottomCenter.Z + adapter.HostUpVector.Z * hostHeight);
HostBottomCenterX = hostBottomCenter.X;
HostBottomCenterY = hostBottomCenter.Y;
HostBottomCenterZ = hostBottomCenter.Z;
HostTopCenterX = hostTopCenter.X;
HostTopCenterY = hostTopCenter.Y;
HostTopCenterZ = hostTopCenter.Z;
Point3D orientedBottomCenter = ModelItemTransformHelper.GetStableBottomAnchorPoint(bounds, item.Transform);
Vector3D objectUp = ModelItemTransformHelper.GetUpDirectionFromTransform(item.Transform);
Vector3D localSize = ModelItemTransformHelper.EstimateLocalBoxSize(bounds, item.Transform);
Point3D orientedTopCenter = new Point3D(
orientedBottomCenter.X + objectUp.X * localSize.Z,
orientedBottomCenter.Y + objectUp.Y * localSize.Z,
orientedBottomCenter.Z + objectUp.Z * localSize.Z);
OrientedBottomCenterX = orientedBottomCenter.X;
OrientedBottomCenterY = orientedBottomCenter.Y;
OrientedBottomCenterZ = orientedBottomCenter.Z;
OrientedTopCenterX = orientedTopCenter.X;
OrientedTopCenterY = orientedTopCenter.Y;
OrientedTopCenterZ = orientedTopCenter.Z;
// 计算底面中心点Z最小
BottomCenterX = CenterX;
BottomCenterY = CenterY;
BottomCenterZ = bounds.Min.Z;
}
catch (Exception ex)
{
@ -198,10 +157,8 @@ namespace NavisworksTransport.UI.WPF.Views
{
SizeX = SizeY = SizeZ = 0;
CenterX = CenterY = CenterZ = 0;
HostTopCenterX = HostTopCenterY = HostTopCenterZ = 0;
HostBottomCenterX = HostBottomCenterY = HostBottomCenterZ = 0;
OrientedTopCenterX = OrientedTopCenterY = OrientedTopCenterZ = 0;
OrientedBottomCenterX = OrientedBottomCenterY = OrientedBottomCenterZ = 0;
TopCenterX = TopCenterY = TopCenterZ = 0;
BottomCenterX = BottomCenterY = BottomCenterZ = 0;
}
/// <summary>
@ -245,7 +202,7 @@ namespace NavisworksTransport.UI.WPF.Views
/// </summary>
private void OnCopyTopCenterClick(object sender, RoutedEventArgs e)
{
string coordinate = string.Format("{0:0.000}, {1:0.000}, {2:0.000}", HostTopCenterX, HostTopCenterY, HostTopCenterZ);
string coordinate = string.Format("{0:0.000}, {1:0.000}, {2:0.000}", TopCenterX, TopCenterY, TopCenterZ);
bool success = ClipboardHelper.TrySetText(coordinate, "元素包围盒信息");
ShowButtonFeedback(CopyTopButton, success);
}
@ -255,25 +212,11 @@ namespace NavisworksTransport.UI.WPF.Views
/// </summary>
private void OnCopyBottomCenterClick(object sender, RoutedEventArgs e)
{
string coordinate = string.Format("{0:0.000}, {1:0.000}, {2:0.000}", HostBottomCenterX, HostBottomCenterY, HostBottomCenterZ);
string coordinate = string.Format("{0:0.000}, {1:0.000}, {2:0.000}", BottomCenterX, BottomCenterY, BottomCenterZ);
bool success = ClipboardHelper.TrySetText(coordinate, "元素包围盒信息");
ShowButtonFeedback(CopyBottomButton, success);
}
private void OnCopyOrientedTopCenterClick(object sender, RoutedEventArgs e)
{
string coordinate = string.Format("{0:0.000}, {1:0.000}, {2:0.000}", OrientedTopCenterX, OrientedTopCenterY, OrientedTopCenterZ);
bool success = ClipboardHelper.TrySetText(coordinate, "元素包围盒信息");
ShowButtonFeedback(CopyOrientedTopButton, success);
}
private void OnCopyOrientedBottomCenterClick(object sender, RoutedEventArgs e)
{
string coordinate = string.Format("{0:0.000}, {1:0.000}, {2:0.000}", OrientedBottomCenterX, OrientedBottomCenterY, OrientedBottomCenterZ);
bool success = ClipboardHelper.TrySetText(coordinate, "元素包围盒信息");
ShowButtonFeedback(CopyOrientedBottomButton, success);
}
/// <summary>
/// 显示按钮点击反馈
/// </summary>

View File

@ -28,7 +28,6 @@ NavisworksTransport 类别设置页签视图 - 参考分层管理页签的设计
<!-- 转换器资源 -->
<converters:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter"/>
<converters:IndexConverter x:Key="IndexConverter"/>
<converters:EditableNumberConverter x:Key="EditableNumberConverter"/>
</ResourceDictionary>
</UserControl.Resources>
@ -116,14 +115,14 @@ NavisworksTransport 类别设置页签视图 - 参考分层管理页签的设计
<Label Grid.Column="0" Content="限宽:" Style="{StaticResource ParameterLabelStyle}"/>
<TextBox Grid.Column="1"
Text="{Binding WidthLimit, Mode=TwoWay, UpdateSourceTrigger=LostFocus, Converter={StaticResource EditableNumberConverter}, ConverterParameter=0.0###, ValidatesOnExceptions=True, NotifyOnValidationError=True}"
Text="{Binding WidthLimit, StringFormat=0.0###}"
Style="{StaticResource ParameterInputStyle}"
ToolTip="通行宽度限制(米)"/>
<Label Grid.Column="2" Content="米" Style="{StaticResource UnitLabelStyle}"/>
<Label Grid.Column="3" Content="限高:" VerticalAlignment="Center" Margin="15,0,0,0"/>
<TextBox Grid.Column="4"
Text="{Binding HeightLimit, Mode=TwoWay, UpdateSourceTrigger=LostFocus, Converter={StaticResource EditableNumberConverter}, ConverterParameter=0.0###, ValidatesOnExceptions=True, NotifyOnValidationError=True}"
Text="{Binding HeightLimit, StringFormat=0.0###}"
Style="{StaticResource ParameterInputStyle}"
ToolTip="通行高度限制(米)"/>
<Label Grid.Column="5" Content="米" Style="{StaticResource UnitLabelStyle}"/>
@ -140,7 +139,7 @@ NavisworksTransport 类别设置页签视图 - 参考分层管理页签的设计
<Label Grid.Column="0" Content="限速:" Style="{StaticResource ParameterLabelStyle}"/>
<TextBox Grid.Column="1"
Text="{Binding SpeedLimit, Mode=TwoWay, UpdateSourceTrigger=LostFocus, Converter={StaticResource EditableNumberConverter}, ConverterParameter=0.0###, ValidatesOnExceptions=True, NotifyOnValidationError=True}"
Text="{Binding SpeedLimit, StringFormat=0.0###}"
Style="{StaticResource ParameterInputStyle}"
ToolTip="速度限制(米/秒)"/>
<Label Grid.Column="2" Content="米/秒" Style="{StaticResource UnitLabelStyle}" Width="40"/>

View File

@ -29,7 +29,6 @@ NavisworksTransport 路径编辑页签视图 - 采用与动画控制和分层管
<converters:PathTypeConverter x:Key="PathTypeConverter"/>
<converters:BoolToBrushConverter x:Key="BoolToBrushConverter"/>
<converters:BoolToOpacityConverter x:Key="BoolToOpacityConverter"/>
<converters:EditableNumberConverter x:Key="EditableNumberConverter"/>
<!-- 多层吊装转换器 -->
<converters:IndexPlusOneConverter x:Key="IndexPlusOneConverter"/>
@ -90,28 +89,28 @@ NavisworksTransport 路径编辑页签视图 - 采用与动画控制和分层管
<!-- 物体长度 -->
<Label Grid.Row="0" Grid.Column="0" Content="物体长度:" Style="{StaticResource ParameterLabelStyle}"/>
<TextBox Grid.Row="0" Grid.Column="1"
Text="{Binding ObjectLength, Mode=TwoWay, UpdateSourceTrigger=LostFocus, Converter={StaticResource EditableNumberConverter}, ConverterParameter=0.0###, ValidatesOnExceptions=True, NotifyOnValidationError=True}"
Text="{Binding ObjectLength, StringFormat=0.0###}"
Style="{StaticResource ParameterInputStyle}"/>
<Label Grid.Row="0" Grid.Column="2" Content="米" Style="{StaticResource UnitLabelStyle}"/>
<!-- 物体宽度 -->
<Label Grid.Row="0" Grid.Column="3" Content="物体宽度:" Style="{StaticResource ParameterLabelStyle}"/>
<TextBox Grid.Row="0" Grid.Column="4"
Text="{Binding ObjectWidth, Mode=TwoWay, UpdateSourceTrigger=LostFocus, Converter={StaticResource EditableNumberConverter}, ConverterParameter=0.0###, ValidatesOnExceptions=True, NotifyOnValidationError=True}"
Text="{Binding ObjectWidth, StringFormat=0.0###}"
Style="{StaticResource ParameterInputStyle}"/>
<Label Grid.Row="0" Grid.Column="5" Content="米" Style="{StaticResource UnitLabelStyle}"/>
<!-- 物体高度 -->
<Label Grid.Row="1" Grid.Column="0" Content="物体高度:" Style="{StaticResource ParameterLabelStyle}"/>
<TextBox Grid.Row="1" Grid.Column="1"
Text="{Binding ObjectHeight, Mode=TwoWay, UpdateSourceTrigger=LostFocus, Converter={StaticResource EditableNumberConverter}, ConverterParameter=0.0###, ValidatesOnExceptions=True, NotifyOnValidationError=True}"
Text="{Binding ObjectHeight, StringFormat=0.0###}"
Style="{StaticResource ParameterInputStyle}"/>
<Label Grid.Row="1" Grid.Column="2" Content="米" Style="{StaticResource UnitLabelStyle}"/>
<!-- 安全间隙 -->
<Label Grid.Row="1" Grid.Column="3" Content="安全间隙:" Style="{StaticResource ParameterLabelStyle}"/>
<TextBox Grid.Row="1" Grid.Column="4"
Text="{Binding SafetyMargin, Mode=TwoWay, UpdateSourceTrigger=LostFocus, Converter={StaticResource EditableNumberConverter}, ConverterParameter=0.00###, ValidatesOnExceptions=True, NotifyOnValidationError=True}"
Text="{Binding SafetyMargin, StringFormat=0.00###}"
Style="{StaticResource ParameterInputStyle}"/>
<Label Grid.Row="1" Grid.Column="5" Content="米" Style="{StaticResource UnitLabelStyle}"/>
</Grid>
@ -372,59 +371,6 @@ NavisworksTransport 路径编辑页签视图 - 采用与动画控制和分层管
</GridView>
</ListView.View>
</ListView>
<Expander Header="吊装参数" IsExpanded="True" Margin="0,8,0,0">
<StackPanel Margin="10,6,0,0">
<StackPanel Visibility="{Binding IsHoistingRouteSelected, Converter={StaticResource BoolToVisibilityConverter}, ConverterParameter=Inverse}">
<TextBlock Text="选择一条吊装路径后,可在这里编辑各空中层的绝对高度。"
Style="{StaticResource StatusTextStyle}"
Foreground="#FF666666"/>
</StackPanel>
<StackPanel Visibility="{Binding IsHoistingRouteSelected, Converter={StaticResource BoolToVisibilityConverter}}">
<TextBlock Text="{Binding HoistingLayerEditHintText}"
Style="{StaticResource StatusTextStyle}"
TextWrapping="Wrap"
Foreground="#FF666666"/>
<ListView ItemsSource="{Binding HoistingLayerEditItems}"
Margin="0,6,0,0"
Height="120"
BorderBrush="#FFCCCCCC"
BorderThickness="1">
<ListView.View>
<GridView>
<GridViewColumn Header="层级" Width="48">
<GridViewColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding DisplayIndex}"
HorizontalAlignment="Center"/>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
<GridViewColumn Header="高度(米)" Width="88">
<GridViewColumn.CellTemplate>
<DataTemplate>
<TextBox Text="{Binding HeightInMeters, Mode=TwoWay, UpdateSourceTrigger=LostFocus, Converter={StaticResource EditableNumberConverter}, ConverterParameter=0.0###, ValidatesOnExceptions=True, NotifyOnValidationError=True}"
HorizontalContentAlignment="Center"
Style="{StaticResource ParameterInputStyle}"/>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
<GridViewColumn Header="关联点" DisplayMemberBinding="{Binding PointSummary}" Width="92"/>
</GridView>
</ListView.View>
</ListView>
<WrapPanel Margin="0,8,0,0">
<Button Content="应用层高"
Command="{Binding ApplyHoistingLayerHeightsCommand}"
IsEnabled="{Binding CanApplyHoistingLayerHeights}"
Style="{StaticResource ActionButtonStyle}"/>
</WrapPanel>
</StackPanel>
</StackPanel>
</Expander>
</StackPanel>
</Border>
@ -467,7 +413,7 @@ NavisworksTransport 路径编辑页签视图 - 采用与动画控制和分层管
Margin="6,0,0,0"
VerticalAlignment="Center">
<TextBox Width="72"
Text="{Binding AssemblyAnchorVerticalOffsetInMeters, Mode=TwoWay, UpdateSourceTrigger=LostFocus, Converter={StaticResource EditableNumberConverter}, ConverterParameter=0.0###, ValidatesOnExceptions=True, NotifyOnValidationError=True}"
Text="{Binding AssemblyAnchorVerticalOffsetInMeters, StringFormat=0.0###}"
Style="{StaticResource ParameterInputStyle}"/>
<Label Content="米" Style="{StaticResource UnitLabelStyle}"/>
</StackPanel>
@ -514,15 +460,15 @@ NavisworksTransport 路径编辑页签视图 - 采用与动画控制和分层管
Margin="6,0,0,0"
VerticalAlignment="Center">
<TextBox Width="56"
Text="{Binding AssemblySphereCenterX, Mode=TwoWay, UpdateSourceTrigger=LostFocus, Converter={StaticResource EditableNumberConverter}, ConverterParameter=0.0###, ValidatesOnExceptions=True, NotifyOnValidationError=True}"
Text="{Binding AssemblySphereCenterX, StringFormat=0.0###}"
Style="{StaticResource ParameterInputStyle}"/>
<Label Content="X" Style="{StaticResource UnitLabelStyle}"/>
<TextBox Width="56"
Text="{Binding AssemblySphereCenterY, Mode=TwoWay, UpdateSourceTrigger=LostFocus, Converter={StaticResource EditableNumberConverter}, ConverterParameter=0.0###, ValidatesOnExceptions=True, NotifyOnValidationError=True}"
Text="{Binding AssemblySphereCenterY, StringFormat=0.0###}"
Style="{StaticResource ParameterInputStyle}"/>
<Label Content="Y" Style="{StaticResource UnitLabelStyle}"/>
<TextBox Width="56"
Text="{Binding AssemblySphereCenterZ, Mode=TwoWay, UpdateSourceTrigger=LostFocus, Converter={StaticResource EditableNumberConverter}, ConverterParameter=0.0###, ValidatesOnExceptions=True, NotifyOnValidationError=True}"
Text="{Binding AssemblySphereCenterZ, StringFormat=0.0###}"
Style="{StaticResource ParameterInputStyle}"/>
<Label Content="Z" Style="{StaticResource UnitLabelStyle}"/>
</StackPanel>
@ -532,27 +478,27 @@ NavisworksTransport 路径编辑页签视图 - 采用与动画控制和分层管
<WrapPanel Margin="0,8,0,0">
<Button Content="捕获箱体"
Command="{Binding CaptureAssemblyTerminalObjectForCreateCommand}"
Command="{Binding CaptureAssemblyTerminalObjectCommand}"
Style="{StaticResource SecondaryButtonStyle}"/>
<Button Content="找端面"
Command="{Binding AnalyzeAssemblyTerminalFaceForCreateCommand}"
Command="{Binding AnalyzeAssemblyTerminalFaceCommand}"
Style="{StaticResource SecondaryButtonStyle}"
IsEnabled="{Binding CanAnalyzeAssemblyTerminalFaceForCreate}"/>
<Button Content="{Binding CreateAssemblyInstallationPointButtonText}"
Command="{Binding SelectAssemblyInstallationPointForCreateCommand}"
IsEnabled="{Binding CanAnalyzeAssemblyTerminalFace}"/>
<Button Content="{Binding AssemblyInstallationPointButtonText}"
Command="{Binding SelectAssemblyInstallationPointCommand}"
Style="{StaticResource SecondaryButtonStyle}"
IsEnabled="{Binding CanSelectAssemblyInstallationPointForCreate}"/>
<Button Content="取起点"
Command="{Binding SelectAssemblyStartPointForCreateCommand}"
IsEnabled="{Binding CanSelectAssemblyInstallationPoint}"/>
<Button Content="生成辅助线"
Command="{Binding GenerateAssemblyReferenceRodCommand}"
Style="{StaticResource ActionButtonStyle}"
IsEnabled="{Binding CanSelectAssemblyStartPointForCreate}"/>
IsEnabled="{Binding CanGenerateAssemblyReferenceRod}"/>
<Button Content="取起点"
Command="{Binding SelectAssemblyStartPointCommand}"
Style="{StaticResource ActionButtonStyle}"
IsEnabled="{Binding CanSelectAssemblyStartPoint}"/>
<Button Content="隐藏辅助线"
Command="{Binding ClearAssemblyReferenceRodCommand}"
Style="{StaticResource SecondaryButtonStyle}"/>
<Button Content="取消装配"
Command="{Binding CancelRailAssemblyWorkflowCommand}"
Style="{StaticResource SecondaryButtonStyle}"
IsEnabled="{Binding CanCancelRailAssemblyWorkflow}"/>
</WrapPanel>
</StackPanel>
</Expander>
@ -614,32 +560,26 @@ NavisworksTransport 路径编辑页签视图 - 采用与动画控制和分层管
Orientation="Horizontal"
VerticalAlignment="Center"
ItemHeight="28">
<Button Content="重选箱体"
<Button Content="捕获箱体"
Padding="10,2"
Margin="0,0,10,0"
Command="{Binding ReCaptureAssemblyTerminalObjectForEditCommand}"
Command="{Binding CaptureAssemblyTerminalObjectCommand}"
Style="{StaticResource SecondaryButtonStyle}"/>
<TextBox Width="150"
Margin="0,0,10,0"
Text="{Binding AssemblyTerminalObjectName}"
Style="{StaticResource ReadOnlyTextBoxStyle}"/>
<Button Content="{Binding EditAssemblyInstallationPointButtonText}"
<Button Content="{Binding AssemblyInstallationPointButtonText}"
Padding="10,2"
Margin="0,0,10,0"
Command="{Binding SelectAssemblyInstallationPointForEditCommand}"
IsEnabled="{Binding CanSelectAssemblyInstallationPointForEdit}"
Command="{Binding SelectAssemblyInstallationPointCommand}"
IsEnabled="{Binding CanSelectAssemblyInstallationPoint}"
Style="{StaticResource SecondaryButtonStyle}"/>
<Button Content="重选起点"
Padding="10,2"
Command="{Binding RepositionRailStartPointCommand}"
IsEnabled="{Binding CanRepositionRailStartPoint}"
Style="{StaticResource SecondaryButtonStyle}"/>
<Button Content="取消装配"
Padding="10,2"
Margin="10,0,0,0"
Command="{Binding CancelRailAssemblyWorkflowCommand}"
IsEnabled="{Binding CanCancelRailAssemblyWorkflow}"
Style="{StaticResource SecondaryButtonStyle}"/>
</WrapPanel>
<Label Grid.Column="0"
Grid.Row="2"
@ -662,7 +602,7 @@ NavisworksTransport 路径编辑页签视图 - 采用与动画控制和分层管
<TextBox Width="84"
Margin="6,0"
HorizontalContentAlignment="Center"
Text="{Binding SelectedRailNormalOffsetInMeters, Mode=TwoWay, UpdateSourceTrigger=LostFocus, Converter={StaticResource EditableNumberConverter}, ConverterParameter=0.0###, ValidatesOnExceptions=True, NotifyOnValidationError=True}"
Text="{Binding SelectedRailNormalOffsetInMeters, Mode=TwoWay, UpdateSourceTrigger=LostFocus, StringFormat=0.0###}"
Style="{StaticResource ParameterInputStyle}"/>
<Button Content="+"
Width="24"

View File

@ -86,14 +86,8 @@ namespace NavisworksTransport.UI.WPF.Views
// 确保服务已初始化
if (_timeTagService == null)
{
var pathManager = PathPlanningManager.Instance;
pathManager.DatabaseInitialize(createIfMissing: true);
var db = pathManager.GetPathDatabase();
if (db == null)
{
throw new InvalidOperationException("时标服务初始化失败:无法获取数据库实例");
}
var doc = Autodesk.Navisworks.Api.Application.ActiveDocument;
var db = new PathDatabase(doc.FileName);
_timeTagService = new TimeTagService(db);
LogManager.Info("时标服务已初始化");
}

View File

@ -3,7 +3,6 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:NavisworksTransport.UI.WPF.Views"
xmlns:controls="clr-namespace:NavisworksTransport.UI.WPF.Controls"
xmlns:converters="clr-namespace:NavisworksTransport.UI.WPF.Converters"
Title="时标设置"
Height="750" Width="1100"
WindowStartupLocation="CenterOwner"
@ -18,7 +17,6 @@
<!-- 转换器 -->
<BooleanToVisibilityConverter x:Key="BoolToVisibilityConverter"/>
<local:InverseBooleanToVisibilityConverter x:Key="InverseBoolToVisibilityConverter"/>
<converters:EditableNumberConverter x:Key="EditableNumberConverter"/>
</ResourceDictionary>
</Window.Resources>
@ -175,7 +173,7 @@
Visibility="{Binding IsSelectedDistanceEditable, Converter={StaticResource BoolToVisibilityConverter}}"
PreviewMouseLeftButtonDown="DistanceSlider_PreviewMouseLeftButtonDown"
PreviewMouseLeftButtonUp="DistanceSlider_PreviewMouseLeftButtonUp"/>
<TextBox Text="{Binding SelectedDistance, Mode=TwoWay, UpdateSourceTrigger=LostFocus, Converter={StaticResource EditableNumberConverter}, ConverterParameter=F2, ValidatesOnExceptions=True, NotifyOnValidationError=True}"
<TextBox Text="{Binding SelectedDistance, StringFormat=F2, Mode=TwoWay}"
Margin="0,0,0,15" HorizontalAlignment="Right" Width="80"
Visibility="{Binding IsSelectedDistanceEditable, Converter={StaticResource BoolToVisibilityConverter}}"/>
<!-- 距离不可编辑时显示只读文本 -->
@ -187,14 +185,14 @@
<Slider Minimum="0" Maximum="60"
Value="{Binding SelectedWaitTime, Mode=TwoWay}"
TickFrequency="1" IsSnapToTickEnabled="True"/>
<TextBox Text="{Binding SelectedWaitTime, Mode=TwoWay, UpdateSourceTrigger=LostFocus, Converter={StaticResource EditableNumberConverter}, ConverterParameter=F1, ValidatesOnExceptions=True, NotifyOnValidationError=True}"
<TextBox Text="{Binding SelectedWaitTime, StringFormat=F1, Mode=TwoWay}"
Margin="0,0,0,15" HorizontalAlignment="Right" Width="80"/>
<TextBlock Text="动作时间 (秒):" FontWeight="SemiBold" Margin="0,0,0,3"/>
<Slider Minimum="0" Maximum="60"
Value="{Binding SelectedActionTime, Mode=TwoWay}"
TickFrequency="1" IsSnapToTickEnabled="True"/>
<TextBox Text="{Binding SelectedActionTime, Mode=TwoWay, UpdateSourceTrigger=LostFocus, Converter={StaticResource EditableNumberConverter}, ConverterParameter=F1, ValidatesOnExceptions=True, NotifyOnValidationError=True}"
<TextBox Text="{Binding SelectedActionTime, StringFormat=F1, Mode=TwoWay}"
Margin="0,0,0,15" HorizontalAlignment="Right" Width="80"/>
<Separator Margin="0,10"/>

View File

@ -133,29 +133,6 @@ namespace NavisworksTransport.Utils.GeometryAnalysis
return new Point3D(point.X, point.Y, point.Z);
}
public static Vector3 OrientNormalTowardTarget(Vector3 normal, Vector3 originPoint, Vector3 targetPoint)
{
if (normal.LengthSquared() < 1e-12f)
{
throw new ArgumentException("端面法向长度过小,无法定向。", nameof(normal));
}
Vector3 normalizedNormal = Vector3.Normalize(normal);
Vector3 targetDirection = targetPoint - originPoint;
if (targetDirection.LengthSquared() < 1e-12f)
{
return normalizedNormal;
}
Vector3 normalizedTargetDirection = Vector3.Normalize(targetDirection);
if (Vector3.Dot(normalizedNormal, normalizedTargetDirection) < 0f)
{
normalizedNormal = -normalizedNormal;
}
return normalizedNormal;
}
private static AnalysisTriangle3 ConvertTriangle(Triangle3D triangle)
{
return new AnalysisTriangle3(ToVector3(triangle.Point1), ToVector3(triangle.Point2), ToVector3(triangle.Point3));

Some files were not shown because too many files have changed in this diff Show More