Compare commits

..

No commits in common. "main" and "codex/rail-mount-modes" have entirely different histories.

26 changed files with 1092 additions and 1784 deletions

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 项目工具类使用指南
## 概述

589
AGENTS.md
View File

@ -1,148 +1,587 @@
# 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`
后续会话中的 AI 助手必须把命令分成两类:
- **允许并行的纯读取操作**
- `rg`
- `Get-Content`
- `ls / Get-ChildItem`
- 读取日志
- 查询状态
- **禁止并行的产出型/宿主相关操作**
- `run-unit-tests.bat`
- `compile.bat`
- `deploy-plugin.bat`
- 启动/关闭 Navisworks
- 会占用 DLL、修改 `bin/obj`、写插件部署目录、依赖上一步产物的任何命令
硬约束:
- 只有“纯读取、无副作用、且互不依赖”的命令才允许并行
- 只要命令会生成、覆盖、部署、锁定文件、启动宿主进程,必须串行执行
- 对本仓库,默认把 `run-unit-tests -> compile -> deploy -> start Navisworks` 视为**单通道流水线**
- 不允许把这条流水线放进任何并行工具里,即使只是为了节省时间
### 插件部署目录
- `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.2 坐标命名规则
不是第四套坐标系,而是业务解释层:把对象哪根局部轴解释为 forward/up/side。
以后凡是变量名、字段名、日志名里出现 `X/Y/Z`、正负轴、forward/up/side 等方向语义,必须在名字上直接带出所属坐标系,避免只看名字时无法判断语义层
- 真实物体:**不要依赖 `ModelItem.Transform`**Revit 导入件多为单位旋转),优先通过 fragment 代表姿态 + Fragment默认Up 解释
- 虚拟物体:对象局部轴业务映射来自资产轴约定
允许的前缀示例:
### 4.4 Quaternion 固定定义(不可再质疑)
- `Host...`
- 宿主坐标系
- `Canonical...`
- 内部坐标系
- `Asset...`
- 资产坐标系
- `Local...`
- 仅当这里明确表示“对象自身局部轴”时才允许使用
正负轴命名示例:
- `LocalPositiveX`
- `LocalNegativeY`
- `HostPositiveZ`
- `CanonicalPositiveX`
- `AssetPositiveY`
方向/向量命名示例:
- `hostForward`
- `hostUp`
- `canonicalForward`
- `assetUp`
- `localXAxis`
- `localZAxis`
禁止继续使用这类脱离坐标系语义的名字:
- `PositiveX`
- `NegativeZ`
- `xAxis`
- `upAxis`
- `forwardAxis`
除非变量名里已经明确出现了所属坐标系前缀。
目标是做到:只看名字,就能知道这个方向、轴、向量到底属于宿主、内部、资产,还是对象自身局部轴。
### 4.1.3 对象局部轴业务映射
以后关于 `Local...`、前进轴、up 轴、side 轴,统一优先使用这个说法:
- **对象局部轴业务映射**
- 它不是第四套全局坐标系
- 也不是几何天然自带的“真理”
- 它表示:在当前业务链路里,我们准备把对象自身哪根局部轴解释成:
- `forward`
- `up`
- `side`
这个概念的边界必须非常明确:
- 它是**业务解释层**
- 不是宿主坐标系
- 不是内部 `Canonical Space`
- 不是插件资源资产坐标系
- 它不能脱离上下文单独存在
- `up` 往往要结合宿主 `up` 语义来解释
- `forward` 往往取决于:
- fragment 代表姿态解释
- 资源默认轴约定
- 用户选择
- 当前路径类型
为什么必须保留这层概念:
- 路径姿态求解必须回答:
- 对象哪根局部轴去对齐路径 `forward`
- 对象哪根局部轴去对齐目标 `up`
- 如果没有这层映射,下面这些都无法稳定定义:
- `Ground / Hoisting / Rail` 起点姿态
- 逐帧姿态
- `Rail` 角度修正
- 通行空间尺寸投影
- 终点原始姿态保持
- `Rail` 平移模式的终点原位搬运
当前项目里的硬约束:
- 不要把“对象局部轴业务映射”写成“对象自身天然就有 forward/up 定义”
- 对真实物体:
- 不能简单把 `ModelItem.Transform` 当成可靠的局部轴真值
- 应优先通过 fragment 代表姿态 + `Fragment默认Up` 去解释
- 对虚拟物体:
- 对象局部轴业务映射通常来自明确的资产轴约定
- 如果只是讨论对象自身局部轴语义,允许继续使用 `Local...` 命名
- 但文档、日志、设计讨论里优先说“对象局部轴业务映射”
### 4.1.1 Quaternion / Rotation3D 固定定义
这是项目级硬约束,不允许在任何修复中重新猜测、重新验证或临时改口:
- 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` 方案
- 起点、逐帧、终点、通行空间,必须共享同一套尺寸语义
- `Ground + 真实物体` 的业务跟踪点固定为**原始包围盒中心**
- 旋转后的实时 `BoundingBox.Center` 只允许用于:
- 诊断旋转后漂移
- 计算当帧补偿
- 校验补偿结果
- 不允许把实时 `BoundingBox.Center` 直接当成:
- 起点目标跟踪点
- 逐帧业务跟踪点
- 碰撞记录主语义跟踪点
- 如果 `Ground` 出现“起点正确但越走越偏”或“起点偏了但拐弯后偏差反而减小”,优先检查是否把实时包围盒中心误当成了业务跟踪点
#### 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 彻底禁止 fallback
这是当前项目的第一编码原则,优先级高于其他“先跑起来”的考虑。
不允许出现以下行为:
- 新姿态链失败时,静默退回旧姿态链
- 新变换链失败时,静默退回旧变换链
- 正确姿势/正确位置/正确尺寸语义拿不到时,用“差不多”的旧值、缓存值、默认值顶上
- 只打印一条 warning然后继续使用错误语义把流程跑完
尤其禁止这类做法:
- 偷偷退回旧 `yaw`
- 偷偷用硬编码 `Z-up`
- 偷偷在错误时给默认值掩盖问题
- 偷偷在新链失败时自动掉回旧链
- 读不到当前实际几何旋转时,回退到 `_trackedRotation`
- 读不到当前真实姿态时,回退到 `referenceRotation`
- 读不到当前显示姿态时,回退到 `ModelItem.Transform`
正确做法只有两种:
1. 在进入新链前把前置条件补齐
2. 直接暴露失败并修根因
不允许把“旧链兜底”当成正式实现的一部分。
### 5.2 不向后兼容
项目只针对 Navisworks 2026。不要写旧版本兼容代码。
### 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

@ -70,7 +70,6 @@
<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" />

View File

@ -340,7 +340,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" />

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

@ -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%');" ^

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,11 +2,6 @@
## 功能点
### [2026/5/18]
1. [x] BUG修复剖面盒导出后程序偶尔崩溃的问题
2. [x] (优化)拆分剖面盒导出为遍历+导出两步,提高性能
### [2026/4/12]
1. [x] (功能)实现自动集成测试框架

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

@ -133,7 +133,7 @@ namespace NavisworksTransport.Core.Animation
// === 角度修正 ===
private LocalEulerRotationCorrection _objectRotationCorrection = LocalEulerRotationCorrection.Zero; // 物体绕宿主 X/Y/Z 轴的角度修正
private double _groundPathObjectLiftHeight; // 地面路径物体上下偏移(模型单位,负值向下
private double _groundPathObjectLiftHeight; // 地面路径物体额外提升高度(模型单位
// === 碰撞排除列表缓存管理从LogisticsAnimationManager迁移===
private ModelItem _currentCachedAnimationObject;
@ -4372,13 +4372,7 @@ namespace NavisworksTransport.Core.Animation
return false;
}
// 提取非 up 轴修正角度
double angleXRadians = _objectRotationCorrection.XDegrees * Math.PI / 180.0;
double angleNonUpRadians = (adapter.HostType == CoordinateSystemType.YUp
? _objectRotationCorrection.ZDegrees
: _objectRotationCorrection.YDegrees) * Math.PI / 180.0;
var nonUpAxis = adapter.HostType == CoordinateSystemType.YUp
? Vector3.UnitZ : Vector3.UnitY;
ApplyPlanarNonUpAxisCorrectionsAtTrackedPosition(pathType, _trackedPosition, adapter.HostType);
double upAxisCorrectionRadians = ResolvePlanarHostUpCorrectionRadians(
_objectRotationCorrection,
@ -4386,23 +4380,18 @@ namespace NavisworksTransport.Core.Animation
targetYawRadians = NormalizeRadians(targetYawRadians + upAxisCorrectionRadians);
double currentYawRadians = _currentYaw;
double deltaYawRadians = NormalizeRadians(targetYawRadians - currentYawRadians);
LogManager.Info(
$"[{pathType.GetDisplayName()}纯增量起点] 当前Yaw={currentYawRadians * 180.0 / Math.PI:F2}°, " +
$"目标Yaw={targetYawRadians * 180.0 / Math.PI:F2}°, 增量Yaw={deltaYawRadians * 180.0 / Math.PI:F2}°, " +
$"当前跟踪点=({_trackedPosition.X:F3},{_trackedPosition.Y:F3},{_trackedPosition.Z:F3}), " +
$"目标跟踪点=({targetTrackedPosition.X:F3},{targetTrackedPosition.Y:F3},{targetTrackedPosition.Z:F3})");
if (Math.Abs(angleXRadians) > 1e-9 || Math.Abs(angleNonUpRadians) > 1e-9 || Math.Abs(deltaYawRadians) > 1e-9)
{
LogManager.Info(
$"[{pathType.GetDisplayName()}纯增量起点] 当前Yaw={currentYawRadians * 180.0 / Math.PI:F2}°, " +
$"目标Yaw={targetYawRadians * 180.0 / Math.PI:F2}°, 增量Yaw={deltaYawRadians * 180.0 / Math.PI:F2}°, " +
$"当前跟踪点=({_trackedPosition.X:F3},{_trackedPosition.Y:F3},{_trackedPosition.Z:F3}), " +
$"目标跟踪点=({targetTrackedPosition.X:F3},{targetTrackedPosition.Y:F3},{targetTrackedPosition.Z:F3})");
ModelItemTransformHelper.MoveItemCombinedAxisRotationAndTranslation(
CurrentControlledObject,
_trackedPosition,
Vector3.UnitX, angleXRadians,
nonUpAxis, angleNonUpRadians,
adapter.HostUpVector3, deltaYawRadians,
targetTrackedPosition);
}
ModelItemTransformHelper.MoveItemIncrementallyByAxisRotationAndTranslation(
CurrentControlledObject,
_trackedPosition,
adapter.HostUpVector3,
deltaYawRadians,
targetTrackedPosition);
_trackedPosition = targetTrackedPosition;
@ -5439,7 +5428,7 @@ namespace NavisworksTransport.Core.Animation
LogManager.Info(
$"[起点摆放] 已应用物体调整请求: 模式={_objectStartPlacementMode}, 角度={_objectRotationCorrection}, " +
$"上下偏移={request.VerticalLiftInMeters:F3}m");
$"提升高度={request.VerticalLiftInMeters:F3}m");
if (_animatedObject != null && _pathPoints != null && _pathPoints.Count > 0)
{
@ -5475,7 +5464,7 @@ namespace NavisworksTransport.Core.Animation
public void SetObjectStartVerticalLiftDirect(double verticalLiftInMeters)
{
_groundPathObjectLiftHeight = UnitsConverter.ConvertFromMeters(verticalLiftInMeters);
LogManager.Debug($"[起点摆放] 直接设置地面路径上下偏移: {verticalLiftInMeters:F3}m");
LogManager.Debug($"[起点摆放] 直接设置地面路径提升高度: {verticalLiftInMeters:F3}m");
}
public void SetObjectStartPlacementMode(ObjectStartPlacementMode placementMode)
@ -5515,7 +5504,7 @@ namespace NavisworksTransport.Core.Animation
double verticalLiftModelUnits,
CoordinateSystemType hostType)
{
if (Math.Abs(verticalLiftModelUnits) < 1e-9)
if (verticalLiftModelUnits <= 0.0)
{
return Vector3.Zero;
}
@ -5526,7 +5515,7 @@ namespace NavisworksTransport.Core.Animation
private Point3D ApplyGroundPathObjectLiftOffset(Point3D trackedCenter)
{
if (_route == null || _route.PathType != PathType.Ground || Math.Abs(_groundPathObjectLiftHeight) < 1e-9)
if (_route == null || _route.PathType != PathType.Ground || _groundPathObjectLiftHeight <= 0.0)
{
return trackedCenter;
}
@ -5774,6 +5763,11 @@ namespace NavisworksTransport.Core.Animation
Point3D currentPosition = _trackedPosition;
double currentYawRadians = _currentYaw;
targetYawRadians = NormalizeRadians(
targetYawRadians +
ResolvePlanarHostUpCorrectionRadians(
_objectRotationCorrection,
CoordinateSystemManager.Instance.ResolvedType));
double deltaYawRadians = NormalizeRadians(targetYawRadians - currentYawRadians);
Vector3 hostUp = Vector3.Normalize(CoordinateSystemManager.Instance.CreateHostAdapter().HostUpVector3);
string pathTypeName = (_route?.PathType ?? PathType.Ground).GetDisplayName();

View File

@ -582,7 +582,24 @@ namespace NavisworksTransport
LogManager.Info($"[文档管理] *** 状态分析: 从无到有(可能是打开文档)***");
// 文档加载完成,仅连接已存在的数据库,不为未使用插件的文档自动创建 .db
TryConnectPathDatabase(activeDoc);
if (!string.IsNullOrEmpty(activeDoc?.FileName))
{
try
{
var pathManager = PathPlanningManager.GetActivePathManager();
if (pathManager != null)
{
pathManager.DatabaseInitialize(createIfMissing: false);
}
}
catch (Exception ex)
{
LogManager.Error($"[文档管理] 连接现有数据库失败: {ex.Message}", ex);
LogManager.Error($"[文档管理] 文档路径: {activeDoc.FileName}");
LogManager.Error($"[文档管理] 异常类型: {ex.GetType().Name}");
LogManager.Error($"[文档管理] 堆栈信息: {ex.StackTrace}");
}
}
}
else if (_hasModels && !currentHasModels)
{
@ -682,9 +699,6 @@ namespace NavisworksTransport
// 初始化坐标系(文档已加载,可以正确检测)
InitializeCoordinateSystemForDocument();
// 连接文档对应的路径数据库
TryConnectPathDatabase(activeDoc);
// 通知DocumentStateManager文档已就绪
DocumentStateManager.Instance.OnDocumentReady();
@ -910,20 +924,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

@ -230,8 +230,7 @@ namespace NavisworksTransport
LogManager.Error($"初始化路径分析数据库失败: {ex.Message}", ex);
LogManager.Error($"数据库路径: {Application.ActiveDocument?.FileName}");
LogManager.Error($"异常类型: {ex.GetType().Name}");
// 不重新抛出:外层 OnModelsCollectionChanged 已有独立日志;
// _pathDatabase 保持 null后续操作自行判断。
throw; // 重新抛出异常,让调用者知道初始化失败
}
}

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

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

@ -2174,15 +2174,10 @@ 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)));
enableGroundLift);
if (dialog.ShowDialog() == true)
{
var placementRequest = dialog.AdjustmentRequest;
@ -2191,7 +2186,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
ObjectRotationCorrection = placementRequest.RotationCorrection;
string liftSummary = enableGroundLift
? $", 上下偏移={_objectGroundLiftHeightInMeters:F3}m"
? $", 提升高度={_objectGroundLiftHeightInMeters:F3}m"
: string.Empty;
string statusSummary = placementRequest.PreserveInitialPose
? $"物体已调整为平移模式{liftSummary}"
@ -2199,7 +2194,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
LogManager.Info(
$"物体调整已更新: 模式={placementRequest.PlacementMode}, 角度={_objectRotationCorrection}, " +
$"上下偏移={_objectGroundLiftHeightInMeters:F3}m");
$"提升高度={_objectGroundLiftHeightInMeters:F3}m");
UpdateMainStatus(statusSummary);
// 更新通行空间可视化(考虑旋转后的尺寸)
@ -2213,229 +2208,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
@ -4736,7 +4508,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
// 地面路径物体的长度方向X轴前进方向朝向路径方向
// 通行空间沿路径方向 = X方向尺寸长度垂直于路径方向 = Y方向尺寸宽度高度上方加间隙下方不需要因为有地面
passageAcrossPath = effectiveWidth + 2 * safetyMargin; // 旋转后宽度 + 2*间隙(垂直于路径方向)
passageNormalToPath = effectiveHeight + (_objectGroundLiftHeightInMeters * metersToUnitsPassage) + safetyMargin; // 物体高度 + 上下偏移 + 上方间隙
passageNormalToPath = effectiveHeight + (_objectGroundLiftHeightInMeters * metersToUnitsPassage) + safetyMargin; // 物体高度 + 提升高度 + 上方间隙
passageAlongPath = effectiveLength; // 旋转后长度(沿路径方向)
// 地面路径不需要分段参数
passageNormalToPathVertical = passageNormalToPath;
@ -4780,7 +4552,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
// 物体的长度方向X轴前进方向朝向路径方向
// 通行空间沿路径方向 = X方向尺寸长度垂直于路径方向 = Y方向尺寸宽度高度上方加间隙下方不需要因为有地面
passageAcrossPath = effectiveWidth + 2 * safetyMargin; // 旋转后宽度 + 2*间隙(垂直于路径方向)
passageNormalToPath = effectiveHeight + (_objectGroundLiftHeightInMeters * metersToUnitsPassage) + safetyMargin; // 物体高度 + 上下偏移 + 上方间隙
passageNormalToPath = effectiveHeight + (_objectGroundLiftHeightInMeters * metersToUnitsPassage) + safetyMargin; // 物体高度 + 提升高度 + 上方间隙
passageAlongPath = effectiveLength; // 旋转后长度(沿路径方向)
// 地面路径不需要分段参数
passageNormalToPathVertical = passageNormalToPath;
@ -4824,7 +4596,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, 提升高度={_objectGroundLiftHeightInMeters:F2}m, 安全间隙={_safetyMarginInMeters:F2}m");
// 根据路径类型决定是否切换到物体通行空间模式
bool enableObjectSpace = false;

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

@ -2,7 +2,7 @@
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="430" Width="400"
WindowStartupLocation="CenterScreen"
ResizeMode="NoResize"
ShowInTaskbar="False"
@ -28,6 +28,7 @@
<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="设置物体在路径起点处的角度修正;地面路径可额外输入提升高度" FontSize="10" Foreground="#FF666666" Margin="0,3,0,0"/>
</StackPanel>
</Border>
@ -45,7 +46,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, Mode=TwoWay, UpdateSourceTrigger=LostFocus, Converter={StaticResource EditableNumberConverter}, ConverterParameter=0.###, ValidatesOnExceptions=True, NotifyOnValidationError=True}"
Height="28"
VerticalContentAlignment="Center"
Padding="8,0"
@ -63,41 +64,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, Mode=TwoWay, UpdateSourceTrigger=LostFocus, Converter={StaticResource EditableNumberConverter}, ConverterParameter=0.###, ValidatesOnExceptions=True, NotifyOnValidationError=True}"
Height="28"
VerticalContentAlignment="Center"
Padding="8,0"
@ -107,16 +77,37 @@
<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"/>
<TextBlock Text="绕 Z 轴:" VerticalAlignment="Center" FontSize="11" Foreground="#FF333333"/>
<TextBox Grid.Column="1"
x:Name="ZAxisTextBox"
GotFocus="OnAxisTextBoxGotFocus"
Text="{Binding RotationZDegrees, Mode=TwoWay, UpdateSourceTrigger=LostFocus, Converter={StaticResource EditableNumberConverter}, ConverterParameter=0.###, ValidatesOnExceptions=True, NotifyOnValidationError=True}"
Height="28"
VerticalContentAlignment="Center"
Padding="8,0"
FontSize="11"
BorderBrush="#FFCCCCCC"
BorderThickness="1"/>
<TextBlock Grid.Column="2" Text="°" VerticalAlignment="Center" FontSize="11" Foreground="#FF666666" Margin="8,0,0,0"/>
</Grid>
<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}"
Text="{Binding GroundPathLiftHeightInMeters, Mode=TwoWay, UpdateSourceTrigger=LostFocus, Converter={StaticResource EditableNumberConverter}, ConverterParameter=0.###, ValidatesOnExceptions=True, NotifyOnValidationError=True}"
IsEnabled="{Binding IsGroundLiftEnabled}"
Height="28"
VerticalContentAlignment="Center"
@ -127,9 +118,14 @@
<TextBlock Grid.Column="2" Text="m" VerticalAlignment="Center" FontSize="11" Foreground="#FF666666" Margin="8,0,0,0"/>
</Grid>
<TextBlock Text="{Binding GroundLiftHint}"
FontSize="10"
Foreground="#FF666666"
Margin="0,0,0,12"/>
<!-- 快捷按钮 -->
<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 +159,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

@ -24,8 +24,6 @@ namespace NavisworksTransport.UI.WPF.Views
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
@ -89,17 +87,15 @@ namespace NavisworksTransport.UI.WPF.Views
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)
bool isGroundLiftEnabled)
{
try
{
@ -108,8 +104,6 @@ namespace NavisworksTransport.UI.WPF.Views
RotationYDegrees = currentRotation.YDegrees;
RotationZDegrees = currentRotation.ZDegrees;
_isGroundLiftEnabled = isGroundLiftEnabled;
_autoAdjustmentRequest = autoAdjustmentRequest;
_autoAdjustmentProvider = autoAdjustmentProvider;
GroundPathLiftHeightInMeters = currentGroundLiftHeightInMeters;
_adjustmentRequest = ObjectStartPlacementRequest.CreateRotationCorrection(currentRotation, currentGroundLiftHeightInMeters);
DataContext = this;
@ -122,40 +116,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)
@ -282,11 +242,17 @@ namespace NavisworksTransport.UI.WPF.Views
return false;
}
if (!TryUpdateNumberTextBox(GroundLiftTextBox, "请输入有效的上下偏移值。"))
if (!TryUpdateNumberTextBox(GroundLiftTextBox, "请输入有效的提升高度。"))
{
return false;
}
if (GroundPathLiftHeightInMeters < 0.0)
{
ShowInputError("提升高度必须大于或等于 0。", GroundLiftTextBox);
return false;
}
return true;
}
@ -294,27 +260,14 @@ namespace NavisworksTransport.UI.WPF.Views
{
BindingExpression binding = textBox.GetBindingExpression(TextBox.TextProperty);
if (EditableNumberConverter.IsIntermediateText(textBox.Text))
if (EditableNumberConverter.IsIntermediateText(textBox.Text) ||
!EditableNumberConverter.TryParseNumber(textBox.Text.Trim(), System.Globalization.CultureInfo.CurrentCulture, typeof(double), out _))
{
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;
}
binding?.UpdateSource();
if (Validation.GetHasError(textBox))
{
@ -331,16 +284,5 @@ namespace NavisworksTransport.UI.WPF.Views
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

@ -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,381 +0,0 @@
using System;
using System.Collections.Generic;
using System.Numerics;
namespace NavisworksTransport.Utils.CoordinateSystem
{
public sealed class ObjectPassageProjectionOptimizationRequest
{
public ObjectPassageProjectionOptimizationRequest(
double sizeX,
double sizeY,
double sizeZ,
Vector3 hostPathForward,
Vector3 hostUp,
double areaRelativeTieTolerance = 0.01,
Quaternion? baselineHostRotation = null)
{
SizeX = sizeX;
SizeY = sizeY;
SizeZ = sizeZ;
HostPathForward = hostPathForward;
HostUp = hostUp;
AreaRelativeTieTolerance = areaRelativeTieTolerance;
BaselineHostRotation = Quaternion.Normalize(baselineHostRotation ?? Quaternion.Identity);
}
public double SizeX { get; }
public double SizeY { get; }
public double SizeZ { get; }
public Vector3 HostPathForward { get; }
public Vector3 HostUp { get; }
public double AreaRelativeTieTolerance { get; }
public Quaternion BaselineHostRotation { get; }
}
public sealed class ObjectPassageProjectionOptimizationResult
{
private ObjectPassageProjectionOptimizationResult(
bool success,
LocalEulerRotationCorrection correction,
ObjectPassageProjectionScore score,
string errorMessage)
{
Success = success;
Correction = correction;
Score = score;
ErrorMessage = errorMessage;
}
public bool Success { get; }
public LocalEulerRotationCorrection Correction { get; }
public ObjectPassageProjectionScore Score { get; }
public string ErrorMessage { get; }
public static ObjectPassageProjectionOptimizationResult CreateSuccess(
LocalEulerRotationCorrection correction,
ObjectPassageProjectionScore score)
{
return new ObjectPassageProjectionOptimizationResult(true, correction, score, null);
}
public static ObjectPassageProjectionOptimizationResult CreateFailure(string errorMessage)
{
return new ObjectPassageProjectionOptimizationResult(
false,
LocalEulerRotationCorrection.Zero,
ObjectPassageProjectionScore.Invalid,
errorMessage);
}
}
public struct ObjectPassageProjectionScore
{
public static readonly ObjectPassageProjectionScore Invalid =
new ObjectPassageProjectionScore(double.PositiveInfinity, double.PositiveInfinity);
public ObjectPassageProjectionScore(double widthAcrossPath, double heightAlongHostUp)
{
WidthAcrossPath = widthAcrossPath;
HeightAlongHostUp = heightAlongHostUp;
Area = widthAcrossPath * heightAlongHostUp;
}
public double WidthAcrossPath { get; }
public double HeightAlongHostUp { get; }
public double Area { get; }
}
public static class ObjectPassageProjectionOptimizer
{
private const double Epsilon = 1e-9;
public static ObjectPassageProjectionOptimizationResult Optimize(
ObjectPassageProjectionOptimizationRequest request)
{
if (!TryValidateRequest(request, out string errorMessage))
{
return ObjectPassageProjectionOptimizationResult.CreateFailure(errorMessage);
}
return OptimizeWithEvaluator(request, correction => Evaluate(request, correction));
}
public static ObjectPassageProjectionOptimizationResult OptimizeWithEvaluator(
ObjectPassageProjectionOptimizationRequest request,
Func<LocalEulerRotationCorrection, ObjectPassageProjectionScore> evaluator)
{
if (!TryValidateRequest(request, out string errorMessage))
{
return ObjectPassageProjectionOptimizationResult.CreateFailure(errorMessage);
}
if (evaluator == null)
{
return ObjectPassageProjectionOptimizationResult.CreateFailure("自动调整评分器为空。");
}
LocalEulerRotationCorrection bestCorrection = LocalEulerRotationCorrection.Zero;
ObjectPassageProjectionScore bestScore = ObjectPassageProjectionScore.Invalid;
foreach (double xDegrees in CandidateAngles())
{
foreach (double yDegrees in CandidateAngles())
{
foreach (double zDegrees in CandidateAngles())
{
var correction = new LocalEulerRotationCorrection(xDegrees, yDegrees, zDegrees);
ObjectPassageProjectionScore score = evaluator(correction);
if (IsBetter(score, bestScore, request.AreaRelativeTieTolerance))
{
bestScore = score;
bestCorrection = correction;
}
}
}
}
RefineByPatternSearch(request, evaluator, ref bestCorrection, ref bestScore);
if (double.IsInfinity(bestScore.Area))
{
return ObjectPassageProjectionOptimizationResult.CreateFailure("优化器未找到有效姿态。");
}
return ObjectPassageProjectionOptimizationResult.CreateSuccess(bestCorrection, bestScore);
}
public static ObjectPassageProjectionScore Evaluate(
ObjectPassageProjectionOptimizationRequest request,
LocalEulerRotationCorrection correction)
{
if (!TryCreateFrame(request, out Vector3 hostSide, out Vector3 hostUp))
{
throw new InvalidOperationException("路径方向退化,无法计算通行截面。");
}
Quaternion correctionRotation = correction.CreateQuaternion(Vector3.UnitX, Vector3.UnitY, Vector3.UnitZ);
Quaternion rotation = Quaternion.Normalize(correctionRotation * request.BaselineHostRotation);
Vector3 rotatedHostX = Vector3.Normalize(Vector3.Transform(Vector3.UnitX, rotation));
Vector3 rotatedHostY = Vector3.Normalize(Vector3.Transform(Vector3.UnitY, rotation));
Vector3 rotatedHostZ = Vector3.Normalize(Vector3.Transform(Vector3.UnitZ, rotation));
double width = ProjectExtent(
request.SizeX,
request.SizeY,
request.SizeZ,
rotatedHostX,
rotatedHostY,
rotatedHostZ,
hostSide);
double height = ProjectExtent(
request.SizeX,
request.SizeY,
request.SizeZ,
rotatedHostX,
rotatedHostY,
rotatedHostZ,
hostUp);
return new ObjectPassageProjectionScore(width, height);
}
private static bool TryValidateRequest(
ObjectPassageProjectionOptimizationRequest request,
out string errorMessage)
{
if (request == null)
{
errorMessage = "自动调整请求为空。";
return false;
}
if (request.SizeX <= Epsilon || request.SizeY <= Epsilon || request.SizeZ <= Epsilon)
{
errorMessage = "物体尺寸无效。";
return false;
}
if (!TryCreateFrame(request, out _, out _))
{
errorMessage = "路径方向退化,无法计算通行截面。";
return false;
}
errorMessage = null;
return true;
}
private static bool TryCreateFrame(
ObjectPassageProjectionOptimizationRequest request,
out Vector3 hostSide,
out Vector3 hostUp)
{
hostSide = Vector3.Zero;
hostUp = Vector3.Zero;
if (request == null || request.HostUp.LengthSquared() < Epsilon)
{
return false;
}
hostUp = Vector3.Normalize(request.HostUp);
Vector3 hostPathForward = request.HostPathForward - hostUp * Vector3.Dot(request.HostPathForward, hostUp);
if (hostPathForward.LengthSquared() < Epsilon)
{
return false;
}
hostPathForward = Vector3.Normalize(hostPathForward);
hostSide = Vector3.Cross(hostPathForward, hostUp);
if (hostSide.LengthSquared() < Epsilon)
{
return false;
}
hostSide = Vector3.Normalize(hostSide);
return true;
}
private static bool IsBetter(
ObjectPassageProjectionScore candidate,
ObjectPassageProjectionScore currentBest,
double areaRelativeTieTolerance)
{
if (double.IsInfinity(currentBest.Area))
{
return true;
}
double tolerance = Math.Max(0.0, areaRelativeTieTolerance) * Math.Max(1.0, currentBest.Area);
if (candidate.Area < currentBest.Area - tolerance)
{
return true;
}
if (Math.Abs(candidate.Area - currentBest.Area) <= tolerance)
{
if (candidate.HeightAlongHostUp < currentBest.HeightAlongHostUp - Epsilon)
{
return true;
}
if (Math.Abs(candidate.HeightAlongHostUp - currentBest.HeightAlongHostUp) <= Epsilon &&
candidate.WidthAcrossPath < currentBest.WidthAcrossPath - Epsilon)
{
return true;
}
}
return false;
}
private static double ProjectExtent(
double sizeX,
double sizeY,
double sizeZ,
Vector3 rotatedHostX,
Vector3 rotatedHostY,
Vector3 rotatedHostZ,
Vector3 targetAxis)
{
return Math.Abs(Vector3.Dot(rotatedHostX, targetAxis)) * sizeX +
Math.Abs(Vector3.Dot(rotatedHostY, targetAxis)) * sizeY +
Math.Abs(Vector3.Dot(rotatedHostZ, targetAxis)) * sizeZ;
}
private static double[] CandidateAngles()
{
return new[]
{
0.0,
90.0,
180.0,
270.0,
45.0,
135.0,
225.0,
315.0
};
}
private static void RefineByPatternSearch(
ObjectPassageProjectionOptimizationRequest request,
Func<LocalEulerRotationCorrection, ObjectPassageProjectionScore> evaluator,
ref LocalEulerRotationCorrection bestCorrection,
ref ObjectPassageProjectionScore bestScore)
{
double stepDegrees = 22.5;
while (stepDegrees >= 1.0)
{
bool improved = false;
foreach (var delta in PatternSearchDeltas(stepDegrees))
{
var candidate = new LocalEulerRotationCorrection(
NormalizeDegrees(bestCorrection.XDegrees + delta.X),
NormalizeDegrees(bestCorrection.YDegrees + delta.Y),
NormalizeDegrees(bestCorrection.ZDegrees + delta.Z));
ObjectPassageProjectionScore score = evaluator(candidate);
if (IsBetter(score, bestScore, request.AreaRelativeTieTolerance))
{
bestCorrection = candidate;
bestScore = score;
improved = true;
}
}
if (!improved)
{
stepDegrees *= 0.5;
}
}
}
private static (double X, double Y, double Z)[] PatternSearchDeltas(double stepDegrees)
{
return new[]
{
( stepDegrees, 0.0, 0.0),
(-stepDegrees, 0.0, 0.0),
(0.0, stepDegrees, 0.0),
(0.0, -stepDegrees, 0.0),
(0.0, 0.0, stepDegrees),
(0.0, 0.0, -stepDegrees),
( stepDegrees, stepDegrees, 0.0),
( stepDegrees, -stepDegrees, 0.0),
(-stepDegrees, stepDegrees, 0.0),
(-stepDegrees, -stepDegrees, 0.0),
( stepDegrees, 0.0, stepDegrees),
( stepDegrees, 0.0, -stepDegrees),
(-stepDegrees, 0.0, stepDegrees),
(-stepDegrees, 0.0, -stepDegrees),
(0.0, stepDegrees, stepDegrees),
(0.0, stepDegrees, -stepDegrees),
(0.0, -stepDegrees, stepDegrees),
(0.0, -stepDegrees, -stepDegrees)
};
}
private static double NormalizeDegrees(double degrees)
{
double normalized = degrees % 360.0;
if (normalized < 0.0)
{
normalized += 360.0;
}
return Math.Abs(normalized) < Epsilon ? 0.0 : normalized;
}
}
}

View File

@ -592,48 +592,6 @@ namespace NavisworksTransport.Utils
LogIncrementalTransformActual(item, targetPosition, targetRotation);
}
/// <summary>
/// 合并三次绕宿主轴的旋转 + 一次平移为单次 OverridePermanentTransform 调用。
/// 等价于依次调用三次 MoveItemIncrementallyByAxisRotationAndTranslation但 NW API 调用次数减少 66%。
/// </summary>
public static void MoveItemCombinedAxisRotationAndTranslation(
ModelItem item,
Point3D trackedPosition,
Vector3 axis1, double angle1Radians,
Vector3 axis2, double angle2Radians,
Vector3 axis3, double angle3Radians,
Point3D targetPosition)
{
if (item == null) throw new ArgumentNullException(nameof(item));
// 用 System.Numerics 合成三个旋转
var q1 = Quaternion.CreateFromAxisAngle(Vector3.Normalize(axis1), (float)angle1Radians);
var q2 = Quaternion.CreateFromAxisAngle(Vector3.Normalize(axis2), (float)angle2Radians);
var q3 = Quaternion.CreateFromAxisAngle(Vector3.Normalize(axis3), (float)angle3Radians);
// q3 * q2 * q1: apply q1 first, then q2, then q3与 Override 左乘积累顺序一致)
var totalQ = Quaternion.Normalize(q3 * q2 * q1);
// 计算旋后跟踪点位置
var tp = new Vector3((float)trackedPosition.X, (float)trackedPosition.Y, (float)trackedPosition.Z);
var rotatedTp = Vector3.Transform(tp, totalQ);
// 计算组合变换
var doc = Application.ActiveDocument;
var modelItems = new ModelItemCollection { item };
doc.Models.ResetPermanentTransform(modelItems);
var identity = Transform3D.CreateTranslation(new Vector3D(0, 0, 0));
var components = identity.Factor();
components.Rotation = new Rotation3D(totalQ.X, totalQ.Y, totalQ.Z, totalQ.W);
components.Translation = new Vector3D(
targetPosition.X - rotatedTp.X,
targetPosition.Y - rotatedTp.Y,
targetPosition.Z - rotatedTp.Z);
var combined = components.Combine();
doc.Models.OverridePermanentTransform(modelItems, combined, false);
}
/// <summary>
/// 直接对当前显示结果施加宿主轴旋转增量,再补一段平移把业务跟踪点拉回目标点。
/// 不先构造 targetRotation也不从 current/target 姿态反推 deltaRotation。

View File

@ -246,14 +246,6 @@ namespace NavisworksTransport.Utils
}
}
/// <summary>
/// 恢复剖面盒(用于被 Navisworks ExportToNwd 内部关闭后重新启用)
/// </summary>
public static void RestoreClipBox(BoundingBox3D box)
{
ApplyClipBox(box);
}
/// <summary>
/// 获取当前剖面盒
/// </summary>

View File

@ -2,7 +2,6 @@ using System;
using System.Collections.Generic;
using System.Linq;
using Autodesk.Navisworks.Api;
using NavisworksTransport.Utils;
using NavisApplication = Autodesk.Navisworks.Api.Application;
namespace NavisworksTransport
@ -262,14 +261,9 @@ namespace NavisworksTransport
return default(T);
}
// 保存可见性状态
// 保存当前状态
var savedState = SaveVisibilityStateInternal(document);
// 保存剖面盒状态Navisworks ExportToNwd 会内部关闭剖分)
bool clipWasEnabled = SectionClipHelper.IsClipBoxEnabled;
BoundingBox3D savedClipBox = default;
bool hasClipBox = SectionClipHelper.TryGetCurrentClipBox(out savedClipBox);
try
{
// 1. 全部显示
@ -287,14 +281,8 @@ namespace NavisworksTransport
}
finally
{
// 4. 恢复可见性状态
// 4. 恢复状态
RestoreVisibilityStateInternal(document, savedState);
// 5. 恢复剖面盒Navisworks 导出时会关闭)
if (clipWasEnabled && !SectionClipHelper.IsClipBoxEnabled && hasClipBox)
{
SectionClipHelper.RestoreClipBox(savedClipBox);
}
}
}