From 0c83c739176a0a3eb186ca6f66563e46db4bab57 Mon Sep 17 00:00:00 2001
From: tian <11429339@qq.com>
Date: Thu, 19 Mar 2026 00:18:24 +0800
Subject: [PATCH 01/87] feat: add linear assembly rail path workflow
---
TransportPlugin.csproj | 4 +-
.../2026/double-rail-mount-path-design.md | 299 +++++++++
.../2026/linear-assembly-reference-path.md | 299 +++++++++
doc/design/2026/直线装配路径_用户操作说明.md | 142 ++++
resources/unit_cylinder.obj | 78 +++
src/Core/Animation/PathAnimationManager.cs | 137 +++-
src/Core/AssemblyReferencePathManager.cs | 408 ++++++++++++
src/Core/PathDataManager.cs | 54 +-
src/Core/PathDatabase.cs | 67 +-
src/Core/PathPlanningManager.cs | 87 ++-
src/Core/PathPlanningModels.cs | 86 ++-
src/Core/PathPointRenderPlugin.cs | 84 ++-
src/PathPlanning/RailGeometryHelper.cs | 412 +++++++++++-
src/UI/WPF/ViewModels/PathEditingViewModel.cs | 629 +++++++++++++++++-
src/UI/WPF/Views/PathEditingView.xaml | 181 ++++-
src/Utils/ModelItemTransformHelper.cs | 228 ++++++-
src/Utils/RailPathPoseHelper.cs | 279 ++++++++
17 files changed, 3384 insertions(+), 90 deletions(-)
create mode 100644 doc/design/2026/double-rail-mount-path-design.md
create mode 100644 doc/design/2026/linear-assembly-reference-path.md
create mode 100644 doc/design/2026/直线装配路径_用户操作说明.md
create mode 100644 resources/unit_cylinder.obj
create mode 100644 src/Core/AssemblyReferencePathManager.cs
create mode 100644 src/Utils/RailPathPoseHelper.cs
diff --git a/TransportPlugin.csproj b/TransportPlugin.csproj
index 9fe4324..919214e 100644
--- a/TransportPlugin.csproj
+++ b/TransportPlugin.csproj
@@ -133,6 +133,7 @@
+
@@ -338,6 +339,7 @@
+
@@ -496,4 +498,4 @@
-
\ No newline at end of file
+
diff --git a/doc/design/2026/double-rail-mount-path-design.md b/doc/design/2026/double-rail-mount-path-design.md
new file mode 100644
index 0000000..aa9211a
--- /dev/null
+++ b/doc/design/2026/double-rail-mount-path-design.md
@@ -0,0 +1,299 @@
+# 双轨安装头空轨路径扩展设计
+
+## 背景
+
+当前项目中的 `PathType.Rail` 语义实际等同于“轨下悬挂运输”:
+
+- 基准路径来自空轨下表面中心线
+- 动画默认将物体沿世界 `Z` 方向向下偏移一个物体高度
+- 通行空间也默认按“路径点在物体上方”渲染
+
+这套模型无法准确表达新的双轨安装头场景:
+
+- 轨道为两根平行轨道
+- 安装头在两轨中间滑动
+- 箱型构件与安装头刚性连接
+- 构件以顶面中心或底面中心与安装头对接
+- 安装头支持轨上、轨下两种安装方式
+- 轨道支持斜向上、斜向下
+
+## 目标
+
+将现有“空轨路径”升级为“轨道导向装配运输路径”,支持以下 4 种基本构型:
+
+1. 轨上 + 斜向上
+2. 轨上 + 斜向下
+3. 轨下 + 斜向上
+4. 轨下 + 斜向下
+
+并支持两种构件对接方式:
+
+1. 顶面中心对接安装头
+2. 底面中心对接安装头
+
+## 设计原则
+
+1. 不再以“轨下中心线”作为 Rail 路径的唯一几何语义。
+2. 路径数据应表达“安装头中心的参考路径”。
+3. 构件实际动画位置和碰撞包络,应从“参考路径 + 安装构型”推导。
+4. 对斜轨路径,偏移和姿态必须基于轨道局部坐标系,而不是简单沿世界 `Z` 轴处理。
+5. 保持旧 Rail 路径可加载,并为旧数据提供默认兼容语义。
+
+## 新数据模型
+
+### RailMountMode
+
+```csharp
+public enum RailMountMode
+{
+ UnderRail = 0,
+ OverRail = 1
+}
+```
+
+含义:安装头位于双轨参考平面的下方或上方。
+
+### RailPayloadAnchorMode
+
+```csharp
+public enum RailPayloadAnchorMode
+{
+ TopCenter = 0,
+ BottomCenter = 1
+}
+```
+
+含义:构件以顶面中心或底面中心与安装头刚性对接。
+
+### RailPathDefinitionMode
+
+```csharp
+public enum RailPathDefinitionMode
+{
+ LegacyBottomCenterLine = 0,
+ InstallationHeadCenterLine = 1
+}
+```
+
+含义:
+
+- `LegacyBottomCenterLine` 用于兼容旧空轨路径
+- `InstallationHeadCenterLine` 表示新的安装头中心参考路径
+
+### PathRoute 新增字段
+
+建议为 `PathRoute` 增加以下字段:
+
+```csharp
+public RailMountMode RailMountMode { get; set; }
+public RailPayloadAnchorMode RailPayloadAnchorMode { get; set; }
+public RailPathDefinitionMode RailPathDefinitionMode { get; set; }
+public double RailHeadToPayloadAnchorOffset { get; set; }
+public double RailGaugeCenterOffset { get; set; }
+```
+
+说明:
+
+- `RailHeadToPayloadAnchorOffset`
+ - 安装头中心到构件对接中心的距离
+ - 使用模型单位
+ - 用于精确表达安装头结构厚度和连接件高度
+- `RailGaugeCenterOffset`
+ - 双轨中心线到安装头中心线的附加偏移
+ - 默认通常为 `0`
+ - 为后续非对称安装场景留扩展位
+
+## 局部坐标系
+
+每一段 Rail 路径在采样时建立局部坐标系:
+
+- `T`:沿轨道方向的单位向量
+- `N`:轨道平面法向,用于区分轨上 / 轨下
+- `B`:横向向量,满足右手系
+
+建议约束:
+
+- 双轨所在平面由轨道主方向和全局竖直方向共同确定
+- 当轨道接近竖直时,仍应优先走显式异常,而不是隐式回退
+
+构件实际锚点位置:
+
+```text
+PayloadAnchorPosition = RailReferencePoint + sign(mountMode) * N * offset
+```
+
+其中:
+
+- `sign(UnderRail) = -1`
+- `sign(OverRail) = +1`
+
+构件几何中心再根据 `TopCenter` / `BottomCenter` 与物体高度换算得到。
+
+## 参考路径定义
+
+### 旧逻辑
+
+当前 `RailGeometryHelper` 输出的是:
+
+- 空轨下表面中心线
+
+### 新逻辑
+
+建议升级为输出:
+
+- 双轨中间的安装头参考中心线
+
+实现上分两层:
+
+1. `ExtractRailReferencePath`
+ - 负责提取轨道导向参考线
+2. `ResolvePayloadPath`
+ - 根据安装构型计算构件实际运动参考路径
+
+## 兼容策略
+
+### 历史路径
+
+旧路径未包含 `RailMountMode`、`RailPayloadAnchorMode` 等信息时,按以下默认值兼容:
+
+```text
+RailMountMode = UnderRail
+RailPayloadAnchorMode = TopCenter
+RailPathDefinitionMode = LegacyBottomCenterLine
+RailHeadToPayloadAnchorOffset = 0
+RailGaugeCenterOffset = 0
+```
+
+兼容语义:
+
+- 保持旧项目行为尽量不变
+- 旧路径仍按“轨下悬挂”解释
+
+### 新路径
+
+新创建的 Rail 路径统一采用:
+
+```text
+RailPathDefinitionMode = InstallationHeadCenterLine
+```
+
+## 需要修改的模块
+
+### 1. 模型层
+
+文件:
+
+- `src/Core/PathPlanningModels.cs`
+- `src/Commands/CreateAerialPathCommand.cs`
+
+工作:
+
+- 增加 Rail 相关枚举
+- 为 `PathRoute` 增加 Rail 构型字段
+- 新建 Rail 路径时写入默认配置
+
+### 2. Rail 几何提取
+
+文件:
+
+- `src/PathPlanning/RailGeometryHelper.cs`
+
+工作:
+
+- 将“下表面中心线”抽象为“参考路径提取”
+- 区分旧路径提取和新路径提取
+- 为双轨场景预留“轨道中心参考线”提取接口
+
+### 3. 交互吸附
+
+文件:
+
+- `src/Core/PathPlanningManager.cs`
+- `src/UI/WPF/ViewModels/PathEditingViewModel.cs`
+
+工作:
+
+- 用户点击继续吸附到 Rail 参考路径
+- 新建 Rail 路径时允许指定安装构型
+
+### 4. 渲染
+
+文件:
+
+- `src/Core/PathPointRenderPlugin.cs`
+
+工作:
+
+- 将 `PathType.Rail` 的固定“向上/向下 Z 偏移”改为局部法向偏移
+- 通行空间不再假定路径点永远位于物体顶部
+
+### 5. 动画
+
+文件:
+
+- `src/Core/Animation/PathAnimationManager.cs`
+
+工作:
+
+- 将 Rail 路径下的构件位置计算改为:
+ - 先得到安装头参考点
+ - 再根据 `RailMountMode` 和 `RailPayloadAnchorMode` 推导物体位置
+- 后续增加 pitch/roll 对齐能力
+
+### 6. 导出
+
+文件:
+
+- `src/Core/PathDataManager.cs`
+
+工作:
+
+- 将 `suspension` 语义调整为更中性的 rail-guided aerial path
+- 视下游格式能力决定是否补充导出字段
+
+## 分阶段实现建议
+
+### Phase 1:最小可用版本
+
+目标:先支持 4 种构型的碰撞检测和动画位置正确。
+
+范围:
+
+- 增加 Rail 构型字段
+- Rail 动画和通行空间偏移改为基于安装构型计算
+- 继续沿用现有参考线提取,但将其抽象为“参考路径”
+
+限制:
+
+- 暂不处理完整 pitch/roll 姿态
+- 暂不精确重建双轨实体几何关系
+
+### Phase 2:几何语义升级
+
+目标:把参考路径从“轨下中心线”升级为“安装头中心线”。
+
+范围:
+
+- 重构 `RailGeometryHelper`
+- 为双轨模型提取真正的中线
+- 新旧路径分别兼容
+
+### Phase 3:姿态精化
+
+目标:对斜轨和长臂构件提供更真实的空间姿态。
+
+范围:
+
+- 动画支持 pitch / roll
+- 构件包络与局部坐标系一致
+- 提升碰撞检测准确性
+
+## 当前建议
+
+本次开发先从 Phase 1 开始:
+
+1. 先把路径模型改为可表达 4 种构型
+2. 先把渲染和动画从“世界 Z 偏移”改成“构型驱动偏移”
+3. 再进入轨道参考线提取的升级
+
+这样风险更低,也更容易验证。
diff --git a/doc/design/2026/linear-assembly-reference-path.md b/doc/design/2026/linear-assembly-reference-path.md
new file mode 100644
index 0000000..002141f
--- /dev/null
+++ b/doc/design/2026/linear-assembly-reference-path.md
@@ -0,0 +1,299 @@
+# 终点反推的直线装配路径设计
+
+## 背景
+
+当前空轨路径已经扩展出轨上/轨下、顶面对接/底面对接、局部姿态等能力,但用户进一步澄清了真实业务:
+
+- 已知箱体最终安装后的准确位置和空间方向
+- 该场景只需要一条起点到终点的直线运动
+- Navisworks 里用户无法点击空气中的点,必须点击到某个实际对象
+
+因此,这类路径不应继续建模为“轨道识别”问题,而应建模为“已知终点位姿的直线装配运动”问题。
+
+## 核心思路
+
+### 1. 终点位姿作为主输入
+
+由用户选择终点处已经组装好的箱体,系统读取:
+
+- 包围盒
+- 当前朝向
+- 终点构件中心 / 顶面中心 / 底面中心
+
+终点位姿是整个运动仿真的主基准。
+
+### 2. 生成可点击的参考杆
+
+为了满足 Navisworks 的点击限制,系统不只绘制一条渲染线,而是生成一个临时参考实体:
+
+- 形态:细长杆体
+- 初版建议:直接复用 `unit_cube.nwc`,通过缩放生成细长方杆
+- 作用:
+ - 可视化直线参考路径
+ - 允许用户点击取点
+ - 为起点吸附提供稳定对象
+
+### 3. 用户点击参考杆确定起点
+
+用户在三维视图中点击参考杆上的某个位置后:
+
+- 获取点击坐标
+- 将点击点投影到参考杆轴线
+- 该投影点作为真实起点
+
+### 4. 生成最终直线路径
+
+系统根据:
+
+- 起点
+- 终点位姿
+- 轨上/轨下
+- 顶面对接/底面对接
+
+生成直线路径点列,并继续复用现有:
+
+- 动画
+- 通行空间
+- 碰撞检测
+
+## 与当前 Rail 方案的关系
+
+### 保留
+
+- `RailMountMode`
+- `RailPayloadAnchorMode`
+- `RailPathDefinitionMode`
+- `RailPathPoseHelper`
+- 动画完整姿态支持
+
+这些都仍然有价值,因为终点和路径参考点之间仍需要姿态推导。
+
+### 弱化 / 退出主路径
+
+- 轨道几何识别
+- 双轨自动分析
+- 基于轨道装置的中心线提取
+
+这些能力可保留为辅助模式,但不再作为该场景的主生成逻辑。
+
+## 最小可行实现
+
+### Phase 1
+
+新增“终点反推参考杆”基础能力:
+
+- 选择终点箱体
+- 读取终点包围盒和朝向
+- 生成参考杆临时实体
+- 支持显示 / 隐藏 / 更新参考杆
+
+### Phase 2
+
+新增“点击参考杆生成直线路径”:
+
+- 用户点击参考杆
+- 起点投影到参考线
+- 终点由终点箱体位姿推导
+- 自动生成两点直线路径
+
+### Phase 3
+
+补齐 UI 与持久化:
+
+- 在 PathEditingViewModel / PathEditingView 中新增入口
+- 保存终点箱体引用、参考线方向、参考杆长度等参数
+
+## 推荐复用点
+
+- `VirtualObjectManager`
+ - 复用 `unit_cube.nwc` 的加载、缩放、隐藏逻辑
+- `ModelItemTransformHelper`
+ - 复用带缩放的位姿变换
+- `NavisworksSelectionHelper`
+ - 复用当前选择对象读取
+- `PathPlanningManager`
+ - 复用点击工具和路径点落点链路
+
+## 需要新增的能力
+
+建议新增一个专用管理器,而不是继续塞进 `VirtualObjectManager`:
+
+- `AssemblyReferencePathManager`
+
+职责:
+
+- 管理参考杆临时实体
+- 根据终点箱体位姿生成参考轴线
+- 提供点击点到参考线的投影
+- 输出最终直线路径点
+
+## 结论
+
+本需求的主问题是“箱体的直线装配运动仿真”,不是“轨道识别”。
+推荐将实现重心切换为:
+
+`终点箱体位姿 -> 参考杆 -> 点击起点 -> 直线路径 -> 动画/碰撞`
+
+这条路线比轨道几何分析更稳定、更符合 Navisworks 交互约束,也更贴近用户真实使用方式。
+
+## 当前实现状态
+
+当前 worktree 中已经完成一版可试用实现,核心能力包括:
+
+- 在路径编辑页新增“直线装配参考”区域
+- 支持捕获终点处已安装箱体
+- 支持选择 `轨上安装 / 轨下安装`
+- 支持选择 `顶面对接 / 底面对接`
+- 根据终点箱体位姿生成一根可点击的参考杆
+- 在三维中渲染终点对接点标记
+- 用户点击参考杆后,自动吸附到参考线并生成两点直线路径
+- 新生成路径仍使用 `PathType.Rail`,因此可继续复用现有 Rail 动画、通行空间和碰撞检测链路
+
+说明:
+
+- 顶部原有“空轨路径”入口仍保留,但已改名为“传统空轨路径”
+- 当前推荐优先使用“直线装配参考”流程
+
+## 当前操作流程
+
+### 1. 选择终点箱体
+
+先在 Navisworks 三维视图中选中终点处已经安装好的箱体,然后在路径编辑页点击:
+
+- `捕获终点箱体`
+
+系统会记录:
+
+- 箱体名称
+- 包围盒中心与尺寸
+- 当前选择的安装方式
+- 当前选择的对接基准
+- 推导出的终点对接点
+
+### 2. 设置装配构型
+
+在“直线装配参考”区域中设置:
+
+- `安装方式`
+ - `轨下安装`
+ - `轨上安装`
+- `对接基准`
+ - `顶面对接`
+ - `底面对接`
+
+这两个参数会直接写入最终生成的 `PathRoute`:
+
+- `RailMountMode`
+- `RailPayloadAnchorMode`
+
+### 3. 生成参考杆
+
+设置参考杆参数后点击:
+
+- `生成参考杆`
+
+当前实现会:
+
+- 从终点对接点沿箱体前向反推出参考杆
+- 在三维场景中生成一根可点击的细长参考杆
+- 同时渲染终点对接点标记,便于确认当前到底是顶面中心还是底面中心
+
+参考杆资源优先使用:
+
+- `resources/unit_cylinder.nwc`
+
+如果该资源暂未放入,则会临时退回:
+
+- `resources/unit_cube.nwc`
+
+### 4. 点击参考杆生成路径
+
+点击:
+
+- `取起点并生成路径`
+
+然后在三维中点击参考杆附近任意可拾取位置。系统会:
+
+- 获取点击坐标
+- 投影到参考杆轴线
+- 将投影点作为真实起点
+- 将终点对接点作为真实终点
+- 自动创建一条两点直线路径
+
+生成后的路径名称形如:
+
+- `直线装配_1`
+
+## 当前界面入口
+
+相关界面位于:
+
+- `路径编辑页 -> 直线装配参考`
+
+当前主要按钮:
+
+- `捕获终点箱体`
+- `生成参考杆`
+- `取起点并生成路径`
+- `隐藏参考杆`
+
+传统模式入口:
+
+- `传统空轨路径`
+
+## 当前已知边界
+
+### 1. 终点对接点依赖包围盒和当前朝向
+
+当前终点对接点通过以下信息推导:
+
+- 世界包围盒
+- 当前变换的局部 Z 方向
+- 估算出的局部箱体高度
+
+这对规则箱型构件是可行的,但如果对象几何明显偏离箱体、或包围盒受附属零件影响较大,终点锚点可能仍需进一步校核。
+
+### 2. 参考杆方向当前取箱体前向
+
+当前参考杆方向来自:
+
+- 箱体变换后的局部 X 轴
+
+这适合“装配移动方向与构件主朝向一致”的场景。
+如果后续遇到“箱体朝向正确,但装配退让方向不是局部 X 轴”的场景,需要再补一个“参考方向选择”输入。
+
+### 3. 仍然复用 Rail 路径类型
+
+当前新路径仍记为:
+
+- `PathType.Rail`
+
+这是有意为之,目的是复用已有 Rail 动画和通行空间逻辑。
+如果未来这类路径越来越多,可以再考虑单独抽象成新的路径类型,例如 `LinearAssembly`。
+
+## 建议测试用例
+
+建议至少验证以下组合:
+
+1. `轨下安装 + 顶面对接`
+2. `轨下安装 + 底面对接`
+3. `轨上安装 + 顶面对接`
+4. `轨上安装 + 底面对接`
+
+每组建议检查:
+
+- 终点对接点标记是否落在预期位置
+- 参考杆是否从终点沿正确方向反推
+- 点击参考杆后起点是否吸附到参考线上
+- 生成路径后,动画位置和通行空间偏移是否符合构型
+
+## 后续建议
+
+下一阶段建议优先做这几项:
+
+1. 增加“参考方向来源”选项
+ 例如允许用户直接指定反推方向,而不只使用箱体局部 X 轴。
+2. 为直线装配路径增加更明确的导出字段
+ 便于区分“传统空轨路径”和“终点反推的直线装配路径”。
+3. 补一份面向最终用户的简短操作说明
+ 可以从本节内容提炼成更短的 UI 使用指南。
diff --git a/doc/design/2026/直线装配路径_用户操作说明.md b/doc/design/2026/直线装配路径_用户操作说明.md
new file mode 100644
index 0000000..85b9764
--- /dev/null
+++ b/doc/design/2026/直线装配路径_用户操作说明.md
@@ -0,0 +1,142 @@
+# 直线装配路径用户操作说明
+
+## 适用场景
+
+适用于以下场景:
+
+- 已知箱体最终安装后的准确位置和朝向
+- 只需要做起点到终点的直线运动仿真
+- 目标是检查运动过程中的碰撞
+
+不推荐优先用于以下场景:
+
+- 需要沿复杂轨道中心线连续编辑路径
+- 需要依赖传统空轨模型吸附逐点绘制路径
+
+## 推荐流程
+
+### 1. 选择终点箱体
+
+先在 Navisworks 三维视图中选中终点处已经安装好的箱体。
+
+然后在路径编辑页的“直线装配参考”区域点击:
+
+- `捕获终点箱体`
+
+### 2. 设置装配构型
+
+在“直线装配参考”区域设置:
+
+- `安装方式`
+ - `轨下安装`
+ - `轨上安装`
+- `对接基准`
+ - `顶面对接`
+ - `底面对接`
+
+### 3. 生成参考杆
+
+设置参考杆长度、直径后点击:
+
+- `生成参考杆`
+
+生成后,三维中会看到:
+
+- 一根可点击的参考杆
+- 一个终点对接点标记
+
+说明:
+
+- 终点对接点标记用于确认当前终点是落在顶面中心还是底面中心
+
+### 4. 取起点并生成路径
+
+点击:
+
+- `取起点并生成路径`
+
+然后在三维里点击参考杆附近位置。系统会:
+
+- 自动吸附到参考杆轴线
+- 用吸附点作为起点
+- 用终点对接点作为终点
+- 自动生成一条两点直线路径
+
+### 5. 检查路径与动画
+
+路径生成后,建议检查:
+
+- 终点对接点是否正确
+- 起点是否吸附在参考杆上
+- 安装方式是否正确
+- 顶面/底面对接是否正确
+- 动画过程中构件位置是否合理
+
+## 当前界面入口
+
+主要入口:
+
+- `路径编辑页 -> 直线装配参考`
+
+当前主要按钮:
+
+- `捕获终点箱体`
+- `生成参考杆`
+- `取起点并生成路径`
+- `隐藏参考杆`
+
+## 与传统空轨路径的区别
+
+### 直线装配路径
+
+特点:
+
+- 以终点箱体位姿为主输入
+- 自动生成参考杆
+- 只需要点一次起点
+- 适合直线装配仿真
+
+### 传统空轨路径
+
+入口:
+
+- `传统空轨路径`
+
+特点:
+
+- 基于空轨基准线吸附
+- 适合沿空轨逐点编辑路径
+- 不作为当前装配仿真场景的首选入口
+
+## 建议试用检查项
+
+建议至少试以下四组:
+
+1. `轨下安装 + 顶面对接`
+2. `轨下安装 + 底面对接`
+3. `轨上安装 + 顶面对接`
+4. `轨上安装 + 底面对接`
+
+每组建议检查:
+
+- 终点标记位置是否正确
+- 参考杆方向是否正确
+- 起点吸附是否准确
+- 路径生成后动画是否符合预期
+
+## 常见提示
+
+如果无法生成参考杆,先检查:
+
+- 是否已经在三维里选中终点箱体
+- 当前选择对象是否是有效模型对象
+
+如果无法取起点,先检查:
+
+- 是否已经生成参考杆
+- 是否进入了“取起点并生成路径”状态
+
+如果终点位置不符合预期,优先检查:
+
+- 当前是否选对了 `轨上安装 / 轨下安装`
+- 当前是否选对了 `顶面对接 / 底面对接`
diff --git a/resources/unit_cylinder.obj b/resources/unit_cylinder.obj
new file mode 100644
index 0000000..f920599
--- /dev/null
+++ b/resources/unit_cylinder.obj
@@ -0,0 +1,78 @@
+# unit cylinder for assembly reference rod
+# dimensions: length 0.01m along +X, diameter 0.01m
+# centered at origin
+o unit_cylinder
+v -0.005000 0.000000 0.000000
+v -0.005000 0.005000 0.000000
+v -0.005000 0.004330 0.002500
+v -0.005000 0.002500 0.004330
+v -0.005000 0.000000 0.005000
+v -0.005000 -0.002500 0.004330
+v -0.005000 -0.004330 0.002500
+v -0.005000 -0.005000 0.000000
+v -0.005000 -0.004330 -0.002500
+v -0.005000 -0.002500 -0.004330
+v -0.005000 0.000000 -0.005000
+v -0.005000 0.002500 -0.004330
+v -0.005000 0.004330 -0.002500
+v 0.005000 0.000000 0.000000
+v 0.005000 0.005000 0.000000
+v 0.005000 0.004330 0.002500
+v 0.005000 0.002500 0.004330
+v 0.005000 0.000000 0.005000
+v 0.005000 -0.002500 0.004330
+v 0.005000 -0.004330 0.002500
+v 0.005000 -0.005000 0.000000
+v 0.005000 -0.004330 -0.002500
+v 0.005000 -0.002500 -0.004330
+v 0.005000 0.000000 -0.005000
+v 0.005000 0.002500 -0.004330
+v 0.005000 0.004330 -0.002500
+f 2 15 16
+f 2 16 3
+f 3 16 17
+f 3 17 4
+f 4 17 18
+f 4 18 5
+f 5 18 19
+f 5 19 6
+f 6 19 20
+f 6 20 7
+f 7 20 21
+f 7 21 8
+f 8 21 22
+f 8 22 9
+f 9 22 23
+f 9 23 10
+f 10 23 24
+f 10 24 11
+f 11 24 25
+f 11 25 12
+f 12 25 26
+f 12 26 13
+f 13 26 15
+f 13 15 2
+f 1 3 2
+f 1 4 3
+f 1 5 4
+f 1 6 5
+f 1 7 6
+f 1 8 7
+f 1 9 8
+f 1 10 9
+f 1 11 10
+f 1 12 11
+f 1 13 12
+f 1 2 13
+f 14 15 16
+f 14 16 17
+f 14 17 18
+f 14 18 19
+f 14 19 20
+f 14 20 21
+f 14 21 22
+f 14 22 23
+f 14 23 24
+f 14 24 25
+f 14 25 26
+f 14 26 15
diff --git a/src/Core/Animation/PathAnimationManager.cs b/src/Core/Animation/PathAnimationManager.cs
index f9de754..3d05eef 100644
--- a/src/Core/Animation/PathAnimationManager.cs
+++ b/src/Core/Animation/PathAnimationManager.cs
@@ -72,6 +72,8 @@ namespace NavisworksTransport.Core.Animation
public double Progress { get; set; } // 进度(0-1)
public Point3D Position { get; set; } // 该帧的位置
public double YawRadians { get; set; } // 绕Z轴的偏航角(弧度)
+ public Rotation3D Rotation { get; set; } // 可选的完整三维姿态
+ public bool HasCustomRotation { get; set; } // 是否使用完整三维姿态
public List Collisions { get; set; } // 该帧的碰撞结果
public bool HasCollision => Collisions?.Count > 0;
@@ -79,6 +81,8 @@ namespace NavisworksTransport.Core.Animation
{
Collisions = new List();
YawRadians = 0.0;
+ Rotation = Rotation3D.Identity;
+ HasCustomRotation = false;
}
}
@@ -156,6 +160,8 @@ namespace NavisworksTransport.Core.Animation
private AnimationState _currentState = AnimationState.Idle;
private double _pausedProgress = 0.0; // 暂停时的进度(0-1之间)
private double _currentYaw = 0.0; // 当前偏航角(弧度)
+ private Rotation3D _currentRotation = Rotation3D.Identity; // 当前完整姿态
+ private bool _hasCurrentRotation = false; // 当前是否使用完整姿态
// TimeLiner 集成
private TimeLinerIntegrationManager _timeLinerManager;
@@ -167,6 +173,8 @@ namespace NavisworksTransport.Core.Animation
// === 物体状态保存和恢复 ===
private Point3D _savedObjectPosition;
private double _savedObjectYaw;
+ private Rotation3D _savedObjectRotation = Rotation3D.Identity;
+ private bool _savedObjectHasCustomRotation = false;
private bool _hasSavedObjectState = false;
///
@@ -413,6 +421,8 @@ namespace NavisworksTransport.Core.Animation
// 2. 重置内部跟踪变量(同步到原始状态)
// 注意:ResetPermanentTransform 后物体的 Transform 属性会自动变回原始值
_currentYaw = ModelItemTransformHelper.GetYawFromTransform(objectToRestore.Transform);
+ _currentRotation = Rotation3D.Identity;
+ _hasCurrentRotation = false;
var originalBoundingBox = objectToRestore.BoundingBox();
_currentPosition = new Point3D(
@@ -639,10 +649,23 @@ namespace NavisworksTransport.Core.Animation
}
else if (_route.PathType == PathType.Rail)
{
- // 空轨路径:路径点是悬挂点,物体悬挂在下方
- double objectHeight = _animatedObject.BoundingBox().Max.Z - _animatedObject.BoundingBox().Min.Z;
- startPosition = new Point3D(startPosition.X, startPosition.Y, startPosition.Z - objectHeight);
- LogManager.Debug($"[移动到起点] 空轨路径调整: 悬挂点Z={_pathPoints[0].Z:F2}, 物体底面Z={startPosition.Z:F2}, 物体高度={objectHeight:F2}");
+ double objectHeight = GetAnimatedObjectHeight();
+ Point3D previousPoint = _pathPoints[0];
+ Point3D nextPoint = _pathPoints.Count > 1 ? _pathPoints[1] : _pathPoints[0];
+ startPosition = RailPathPoseHelper.ResolveBottomPosition(_route, startPosition, previousPoint, nextPoint, objectHeight);
+ LogManager.Debug($"[移动到起点] Rail路径调整: 参考点=({_pathPoints[0].X:F2},{_pathPoints[0].Y:F2},{_pathPoints[0].Z:F2}), 物体底面=({startPosition.X:F2},{startPosition.Y:F2},{startPosition.Z:F2}), 物体高度={objectHeight:F2}, 安装={_route.RailMountMode}, 对接={_route.RailPayloadAnchorMode}, 偏移={_route.RailReferenceToAnchorOffset:F2}");
+
+ if (!_isVirtualObject && _animatedObject != null &&
+ RailPathPoseHelper.TryCreateRailRotation(previousPoint, _pathPoints[0], nextPoint, out var railRotation))
+ {
+ ModelItemTransformHelper.MoveItemToPositionAndRotationWithCurrentScale(_animatedObject, startPosition, railRotation);
+ _currentPosition = startPosition;
+ _currentRotation = railRotation;
+ _hasCurrentRotation = true;
+ _currentYaw = ModelItemTransformHelper.GetYawFromRotation(railRotation);
+ LogManager.Debug("[移动到起点] Rail路径已应用完整三维姿态");
+ return true;
+ }
}
else
{
@@ -814,6 +837,8 @@ namespace NavisworksTransport.Core.Animation
double targetDistance = i * distancePerFrameInModelUnits; // 按距离采样(模型单位),不是按进度
Point3D framePosition;
+ Point3D previousFramePoint = Point3D.Origin;
+ Point3D nextFramePoint = Point3D.Origin;
double yawRadians;
if (isAerialPath)
@@ -832,6 +857,8 @@ namespace NavisworksTransport.Core.Animation
// 线性插值
Point3D p1 = allSampledPoints[segmentIndex];
Point3D p2 = allSampledPoints[segmentIndex + 1];
+ previousFramePoint = p1;
+ nextFramePoint = p2;
framePosition = new Point3D(
p1.X + segmentProgress * (p2.X - p1.X),
p1.Y + segmentProgress * (p2.Y - p1.Y),
@@ -848,7 +875,7 @@ namespace NavisworksTransport.Core.Animation
}
// 🔥 空中路径:根据路径类型和线段索引调整物体位置
- double objectHeight = _animatedObject.BoundingBox().Max.Z - _animatedObject.BoundingBox().Min.Z;
+ double objectHeight = GetAnimatedObjectHeight();
if (_route.PathType == PathType.Hoisting)
{
@@ -896,10 +923,8 @@ namespace NavisworksTransport.Core.Animation
}
else if (_route.PathType == PathType.Rail)
{
- // 空轨路径:路径点是悬挂点,物体悬挂在下方
- // 物体顶面应该在路径点,所以物体底面 = 路径点 - 物体高度
- framePosition = new Point3D(framePosition.X, framePosition.Y, framePosition.Z - objectHeight);
- LogManager.Debug($"[空轨路径] 调整物体位置: 悬挂点Z={framePosition.Z + objectHeight:F2}, 物体底面Z={framePosition.Z:F2}");
+ framePosition = RailPathPoseHelper.ResolveBottomPosition(_route, framePosition, p1, p2, objectHeight);
+ LogManager.Debug($"[Rail路径] 调整物体位置: 参考点=({p1.X:F2},{p1.Y:F2},{p1.Z:F2})->({p2.X:F2},{p2.Y:F2},{p2.Z:F2}), 物体底面=({framePosition.X:F2},{framePosition.Y:F2},{framePosition.Z:F2}), 安装={_route.RailMountMode}, 对接={_route.RailPayloadAnchorMode}");
}
}
else
@@ -926,6 +951,8 @@ namespace NavisworksTransport.Core.Animation
}
yawRadians = ComputeYawOnPath(i, allSampledPoints, edgeIndex, edgeProgress);
+ previousFramePoint = framePosition;
+ nextFramePoint = framePosition;
}
// 创建帧并检测碰撞
@@ -938,6 +965,13 @@ namespace NavisworksTransport.Core.Animation
Collisions = new List()
};
+ if (_route.PathType == PathType.Rail &&
+ RailPathPoseHelper.TryCreateRailRotation(previousFramePoint, framePosition, nextFramePoint, out var railRotation))
+ {
+ frame.Rotation = railRotation;
+ frame.HasCustomRotation = true;
+ }
+
// 记录第一帧的角度(用于调试)
if (i == 0)
{
@@ -2092,6 +2126,8 @@ namespace NavisworksTransport.Core.Animation
incrementalTransform = components.Combine();
_currentYaw = newYaw;
+ _currentRotation = Rotation3D.Identity;
+ _hasCurrentRotation = false;
}
else
{
@@ -2112,6 +2148,31 @@ namespace NavisworksTransport.Core.Animation
}
}
+ ///
+ /// 使用完整三维姿态更新对象位置。
+ /// 当前先用于 Rail 路径,避免影响现有地面/吊装路径的 yaw 流程。
+ ///
+ private void UpdateObjectPosition(Point3D newPosition, Rotation3D newRotation)
+ {
+ try
+ {
+ if (_animatedObject == null)
+ {
+ return;
+ }
+
+ ModelItemTransformHelper.MoveItemToPositionAndRotationWithCurrentScale(_animatedObject, newPosition, newRotation);
+ _currentPosition = newPosition;
+ _currentRotation = newRotation;
+ _hasCurrentRotation = true;
+ _currentYaw = ModelItemTransformHelper.GetYawFromRotation(newRotation);
+ }
+ catch (Exception ex)
+ {
+ LogManager.Error($"使用完整姿态更新对象位置失败: {ex.Message}");
+ }
+ }
+
///
/// 获取文档单位信息(用于日志记录)
///
@@ -2228,8 +2289,10 @@ namespace NavisworksTransport.Core.Animation
var (position, yaw) = GetObjectCurrentPosition(obj);
_savedObjectPosition = position;
_savedObjectYaw = yaw;
+ _savedObjectRotation = _currentRotation;
+ _savedObjectHasCustomRotation = _hasCurrentRotation;
_hasSavedObjectState = true;
- LogManager.Info($"已保存物体状态: pos=({position.X:F2},{position.Y:F2},{position.Z:F2}), yaw={yaw * 180 / Math.PI:F2}度");
+ LogManager.Info($"已保存物体状态: pos=({position.X:F2},{position.Y:F2},{position.Z:F2}), yaw={yaw * 180 / Math.PI:F2}度, customRotation={_savedObjectHasCustomRotation}");
}
catch (Exception ex)
{
@@ -2262,14 +2325,21 @@ namespace NavisworksTransport.Core.Animation
var bbox = obj.BoundingBox();
_currentPosition = new Point3D(bbox.Center.X, bbox.Center.Y, bbox.Min.Z);
- // 使用保存的朝向作为当前朝向,避免UpdateObjectPosition计算不必要的大角度差
- // 这样能确保物体直接从当前动画位置平滑恢复到保存位置,不会突然转动
_currentYaw = _savedObjectYaw;
+ _currentRotation = _savedObjectRotation;
+ _hasCurrentRotation = _savedObjectHasCustomRotation;
- UpdateObjectPosition(_savedObjectPosition, _savedObjectYaw);
+ if (_savedObjectHasCustomRotation)
+ {
+ UpdateObjectPosition(_savedObjectPosition, _savedObjectRotation);
+ }
+ else
+ {
+ UpdateObjectPosition(_savedObjectPosition, _savedObjectYaw);
+ }
_animatedObject = originalAnimatedObject;
- LogManager.Info($"已恢复物体状态: pos=({_savedObjectPosition.X:F2},{_savedObjectPosition.Y:F2},{_savedObjectPosition.Z:F2}), yaw={_savedObjectYaw * 180 / Math.PI:F2}度");
+ LogManager.Info($"已恢复物体状态: pos=({_savedObjectPosition.X:F2},{_savedObjectPosition.Y:F2},{_savedObjectPosition.Z:F2}), yaw={_savedObjectYaw * 180 / Math.PI:F2}度, customRotation={_savedObjectHasCustomRotation}");
}
catch (Exception ex)
{
@@ -2343,7 +2413,14 @@ namespace NavisworksTransport.Core.Animation
// 更新对象位置和朝向
var frameData = _animationFrames[_currentFrameIndex];
- UpdateObjectPosition(frameData.Position, frameData.YawRadians);
+ if (frameData.HasCustomRotation && _route?.PathType == PathType.Rail)
+ {
+ UpdateObjectPosition(frameData.Position, frameData.Rotation);
+ }
+ else
+ {
+ UpdateObjectPosition(frameData.Position, frameData.YawRadians);
+ }
// 更新碰撞高亮
UpdateCollisionHighlightFromFrame();
@@ -2721,6 +2798,25 @@ namespace NavisworksTransport.Core.Animation
///
public double CurrentYaw => _currentYaw;
+ ///
+ /// 获取当前动画物体高度(模型单位)
+ ///
+ private double GetAnimatedObjectHeight()
+ {
+ if (_isVirtualObject)
+ {
+ return _virtualObjectHeight;
+ }
+
+ if (_animatedObject == null)
+ {
+ throw new InvalidOperationException("动画对象为空,无法获取物体高度");
+ }
+
+ var boundingBox = _animatedObject.BoundingBox();
+ return boundingBox.Max.Z - boundingBox.Min.Z;
+ }
+
///
/// 获取当前碰撞检测精度
///
@@ -2984,7 +3080,14 @@ namespace NavisworksTransport.Core.Animation
actualYaw += correctionRad;
}
- UpdateObjectPosition(frameData.Position, actualYaw);
+ if (frameData.HasCustomRotation && _route?.PathType == PathType.Rail)
+ {
+ UpdateObjectPosition(frameData.Position, frameData.Rotation);
+ }
+ else
+ {
+ UpdateObjectPosition(frameData.Position, actualYaw);
+ }
// 更新碰撞高亮(基于预计算结果)
UpdateCollisionHighlightFromFrame();
@@ -3800,4 +3903,4 @@ namespace NavisworksTransport.Core.Animation
#endregion
}
-}
\ No newline at end of file
+}
diff --git a/src/Core/AssemblyReferencePathManager.cs b/src/Core/AssemblyReferencePathManager.cs
new file mode 100644
index 0000000..8505cc3
--- /dev/null
+++ b/src/Core/AssemblyReferencePathManager.cs
@@ -0,0 +1,408 @@
+using System;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using Autodesk.Navisworks.Api;
+using NavisworksTransport.Utils;
+
+namespace NavisworksTransport.Core
+{
+ ///
+ /// 直线装配参考路径管理器。
+ /// 优先使用单位圆柱体资源生成一根可点击的临时参考杆,供用户在 3D 视图中选择起点。
+ /// 如果圆柱资源缺失,则退回到单位立方体资源。
+ ///
+ public class AssemblyReferencePathManager
+ {
+ private const double ReferenceRodBaseSizeInMeters = 0.01;
+
+ private static AssemblyReferencePathManager _instance;
+ private static readonly object _lock = new object();
+
+ private Model _referenceRodModel;
+ private ModelItem _referenceRodModelItem;
+ private bool _isUpdatingReferenceRod;
+ private bool _isReferenceRodVisible;
+ private Point3D _referenceLineStart = Point3D.Origin;
+ private Point3D _referenceLineEnd = Point3D.Origin;
+ private double _referenceRodDiameterInMeters;
+ private string _referenceResourceName;
+
+ public static AssemblyReferencePathManager Instance
+ {
+ get
+ {
+ if (_instance == null)
+ {
+ lock (_lock)
+ {
+ if (_instance == null)
+ {
+ _instance = new AssemblyReferencePathManager();
+ }
+ }
+ }
+
+ return _instance;
+ }
+ }
+
+ public ModelItem CurrentReferenceRod => _referenceRodModelItem;
+ public bool IsReferenceRodVisible => _isReferenceRodVisible;
+ public bool IsUpdatingReferenceRod => _isUpdatingReferenceRod;
+ public bool HasReferenceLine => _referenceRodModelItem != null && _isReferenceRodVisible;
+ public Point3D ReferenceLineStart => _referenceLineStart;
+ public Point3D ReferenceLineEnd => _referenceLineEnd;
+ public string ReferenceResourceName => _referenceResourceName;
+
+ private AssemblyReferencePathManager()
+ {
+ _isReferenceRodVisible = false;
+ }
+
+ ///
+ /// 创建或更新参考杆。
+ ///
+ public ModelItem CreateOrUpdateReferenceRod(Point3D startPoint, Point3D endPoint, double diameterInMeters)
+ {
+ var doc = Application.ActiveDocument;
+ if (doc == null || doc.IsClear)
+ {
+ throw new InvalidOperationException("当前没有可用文档,无法创建装配参考杆");
+ }
+
+ double referenceLength = GeometryHelper.CalculatePointDistance(startPoint, endPoint);
+ if (referenceLength <= 1e-6)
+ {
+ throw new InvalidOperationException("参考线长度为 0,无法创建装配参考杆");
+ }
+
+ _isUpdatingReferenceRod = true;
+ try
+ {
+ EnsureReferenceRodModelLoaded();
+
+ double lengthInMeters = UnitsConverter.ConvertToMeters(referenceLength);
+ ScaleReferenceRod(lengthInMeters, diameterInMeters);
+ PositionReferenceRod(startPoint, endPoint);
+
+ ShowReferenceRod();
+
+ _referenceLineStart = startPoint;
+ _referenceLineEnd = endPoint;
+ _referenceRodDiameterInMeters = diameterInMeters;
+
+ LogManager.Info($"[装配参考杆] 已创建/更新,长度={lengthInMeters:F3}m,直径={diameterInMeters:F3}m");
+ return _referenceRodModelItem;
+ }
+ finally
+ {
+ _isUpdatingReferenceRod = false;
+ }
+ }
+
+ public void ShowReferenceRod()
+ {
+ if (_referenceRodModelItem == null)
+ {
+ return;
+ }
+
+ var items = new ModelItemCollection { _referenceRodModelItem };
+ Application.ActiveDocument.Models.SetHidden(items, false);
+ _isReferenceRodVisible = true;
+ }
+
+ public void HideReferenceRod()
+ {
+ if (_referenceRodModelItem == null)
+ {
+ return;
+ }
+
+ var items = new ModelItemCollection { _referenceRodModelItem };
+ Application.ActiveDocument.Models.SetHidden(items, true);
+ _isReferenceRodVisible = false;
+ }
+
+ ///
+ /// 将任意点击点投影到当前参考线。
+ ///
+ public Point3D ProjectPointToReferenceLine(Point3D point)
+ {
+ if (!HasReferenceLine)
+ {
+ throw new InvalidOperationException("当前没有可用的装配参考线");
+ }
+
+ return ProjectPointToSegment(point, _referenceLineStart, _referenceLineEnd);
+ }
+
+ public void Cleanup()
+ {
+ try
+ {
+ if (_referenceRodModel != null)
+ {
+ var doc = Application.ActiveDocument;
+ if (doc != null && !doc.IsClear)
+ {
+ int index = doc.Models.IndexOf(_referenceRodModel);
+ if (index >= 0)
+ {
+ doc.TryRemoveFile(index);
+ }
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ LogManager.Error($"清理装配参考杆失败: {ex.Message}");
+ }
+ finally
+ {
+ _referenceRodModel = null;
+ _referenceRodModelItem = null;
+ _isReferenceRodVisible = false;
+ _referenceLineStart = Point3D.Origin;
+ _referenceLineEnd = Point3D.Origin;
+ _referenceRodDiameterInMeters = 0.0;
+ _referenceResourceName = null;
+ }
+ }
+
+ private void EnsureReferenceRodModelLoaded()
+ {
+ if (_referenceRodModel != null &&
+ Application.ActiveDocument.Models.IndexOf(_referenceRodModel) >= 0 &&
+ _referenceRodModelItem != null)
+ {
+ return;
+ }
+
+ string resourcePath = GetReferenceRodResourcePath(out string resourceName);
+ if (string.IsNullOrEmpty(resourcePath) || !File.Exists(resourcePath))
+ {
+ throw new FileNotFoundException($"找不到参考杆资源文件: {resourcePath}");
+ }
+
+ var doc = Application.ActiveDocument;
+ int modelCountBefore = doc.Models.Count;
+ if (!doc.TryAppendFile(resourcePath))
+ {
+ throw new InvalidOperationException($"无法追加文件: {resourcePath}");
+ }
+
+ if (doc.Models.Count <= modelCountBefore)
+ {
+ throw new InvalidOperationException("追加参考杆资源后模型数量未增加");
+ }
+
+ _referenceRodModel = doc.Models.Last();
+ _referenceRodModelItem = FindFirstGeometryItem(_referenceRodModel.RootItem);
+ _isReferenceRodVisible = true;
+ _referenceResourceName = resourceName;
+ LogManager.Info($"[装配参考杆] 已加载资源: {resourceName}");
+ }
+
+ private void ScaleReferenceRod(double lengthInMeters, double diameterInMeters)
+ {
+ if (_referenceRodModel == null)
+ {
+ return;
+ }
+
+ var currentTransform = _referenceRodModel.Transform;
+ var components = currentTransform.Factor();
+ components.Scale = new Vector3D(
+ lengthInMeters / ReferenceRodBaseSizeInMeters,
+ diameterInMeters / ReferenceRodBaseSizeInMeters,
+ diameterInMeters / ReferenceRodBaseSizeInMeters);
+
+ Transform3D scaledTransform = components.Combine();
+ Application.ActiveDocument.Models.SetModelUnitsAndTransform(
+ _referenceRodModel,
+ _referenceRodModel.Units,
+ scaledTransform,
+ false);
+ }
+
+ private void PositionReferenceRod(Point3D startPoint, Point3D endPoint)
+ {
+ if (_referenceRodModelItem == null)
+ {
+ return;
+ }
+
+ var doc = Application.ActiveDocument;
+ var modelItems = new ModelItemCollection { _referenceRodModelItem };
+ var currentScale = _referenceRodModelItem.Transform.Factor().Scale;
+
+ doc.Models.ResetPermanentTransform(modelItems);
+
+ var originalBounds = _referenceRodModelItem.BoundingBox();
+ var originalCenter = originalBounds.Center;
+ var targetCenter = new Point3D(
+ (startPoint.X + endPoint.X) / 2.0,
+ (startPoint.Y + endPoint.Y) / 2.0,
+ (startPoint.Z + endPoint.Z) / 2.0);
+
+ Rotation3D rotation = CreateLineRotation(startPoint, endPoint);
+
+ var identity = Transform3D.CreateTranslation(new Vector3D(0, 0, 0));
+ var components = identity.Factor();
+ components.Scale = currentScale;
+ components.Rotation = rotation;
+
+ var preTransform = components.Combine();
+ var linear = preTransform.Linear;
+ var transformedCenter = new Point3D(
+ linear.Get(0, 0) * originalCenter.X + linear.Get(0, 1) * originalCenter.Y + linear.Get(0, 2) * originalCenter.Z,
+ linear.Get(1, 0) * originalCenter.X + linear.Get(1, 1) * originalCenter.Y + linear.Get(1, 2) * originalCenter.Z,
+ linear.Get(2, 0) * originalCenter.X + linear.Get(2, 1) * originalCenter.Y + linear.Get(2, 2) * originalCenter.Z);
+
+ components.Translation = new Vector3D(
+ targetCenter.X - transformedCenter.X,
+ targetCenter.Y - transformedCenter.Y,
+ targetCenter.Z - transformedCenter.Z);
+
+ Transform3D transform = components.Combine();
+ doc.Models.OverridePermanentTransform(modelItems, transform, false);
+ }
+
+ private static Rotation3D CreateLineRotation(Point3D startPoint, Point3D endPoint)
+ {
+ var tangent = Normalize(new Vector3D(
+ endPoint.X - startPoint.X,
+ endPoint.Y - startPoint.Y,
+ endPoint.Z - startPoint.Z));
+
+ var worldUp = new Vector3D(0, 0, 1);
+ var projectedNormal = new Vector3D(
+ worldUp.X - Dot(worldUp, tangent) * tangent.X,
+ worldUp.Y - Dot(worldUp, tangent) * tangent.Y,
+ worldUp.Z - Dot(worldUp, tangent) * tangent.Z);
+
+ if (VectorLengthSquared(projectedNormal) < 1e-9)
+ {
+ var fallbackUp = new Vector3D(0, 1, 0);
+ projectedNormal = new Vector3D(
+ fallbackUp.X - Dot(fallbackUp, tangent) * tangent.X,
+ fallbackUp.Y - Dot(fallbackUp, tangent) * tangent.Y,
+ fallbackUp.Z - Dot(fallbackUp, tangent) * tangent.Z);
+ }
+
+ var normal = Normalize(projectedNormal);
+ var lateral = Normalize(Cross(normal, tangent));
+
+ return new Rotation3D(
+ new UnitVector3D(tangent),
+ new UnitVector3D(lateral),
+ new UnitVector3D(normal));
+ }
+
+ private static Point3D ProjectPointToSegment(Point3D point, Point3D segmentStart, Point3D segmentEnd)
+ {
+ double segmentX = segmentEnd.X - segmentStart.X;
+ double segmentY = segmentEnd.Y - segmentStart.Y;
+ double segmentZ = segmentEnd.Z - segmentStart.Z;
+ double lengthSquared = segmentX * segmentX + segmentY * segmentY + segmentZ * segmentZ;
+
+ if (lengthSquared < 1e-9)
+ {
+ return segmentStart;
+ }
+
+ double pointX = point.X - segmentStart.X;
+ double pointY = point.Y - segmentStart.Y;
+ double pointZ = point.Z - segmentStart.Z;
+ double projection = (pointX * segmentX + pointY * segmentY + pointZ * segmentZ) / lengthSquared;
+ projection = Math.Max(0.0, Math.Min(1.0, projection));
+
+ return new Point3D(
+ segmentStart.X + projection * segmentX,
+ segmentStart.Y + projection * segmentY,
+ segmentStart.Z + projection * segmentZ);
+ }
+
+ private static Vector3D Cross(Vector3D a, Vector3D b)
+ {
+ return new Vector3D(
+ a.Y * b.Z - a.Z * b.Y,
+ a.Z * b.X - a.X * b.Z,
+ a.X * b.Y - a.Y * b.X);
+ }
+
+ private static double Dot(Vector3D a, Vector3D b)
+ {
+ return a.X * b.X + a.Y * b.Y + a.Z * b.Z;
+ }
+
+ private static Vector3D Normalize(Vector3D vector)
+ {
+ double lengthSquared = VectorLengthSquared(vector);
+ if (lengthSquared < 1e-9)
+ {
+ return new Vector3D(1, 0, 0);
+ }
+
+ double length = Math.Sqrt(lengthSquared);
+ return new Vector3D(vector.X / length, vector.Y / length, vector.Z / length);
+ }
+
+ private static double VectorLengthSquared(Vector3D vector)
+ {
+ return vector.X * vector.X + vector.Y * vector.Y + vector.Z * vector.Z;
+ }
+
+ private static ModelItem FindFirstGeometryItem(ModelItem item)
+ {
+ if (item == null)
+ {
+ return null;
+ }
+
+ if (item.HasGeometry)
+ {
+ return item;
+ }
+
+ foreach (var child in item.Children)
+ {
+ var geometryItem = FindFirstGeometryItem(child);
+ if (geometryItem != null)
+ {
+ return geometryItem;
+ }
+ }
+
+ return item;
+ }
+
+ private static string GetReferenceRodResourcePath(out string resourceName)
+ {
+ string pluginDir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
+ string[] preferredPaths = new[]
+ {
+ Path.Combine(pluginDir, "resources", "unit_cylinder.nwc"),
+ Path.Combine(pluginDir, "..", "..", "resources", "unit_cylinder.nwc"),
+ @"c:\Users\Tellme\apps\NavisworksTransport\resources\unit_cylinder.nwc",
+ Path.Combine(pluginDir, "resources", "unit_cube.nwc"),
+ Path.Combine(pluginDir, "..", "..", "resources", "unit_cube.nwc"),
+ @"c:\Users\Tellme\apps\NavisworksTransport\resources\unit_cube.nwc"
+ };
+
+ foreach (string path in preferredPaths)
+ {
+ if (File.Exists(path))
+ {
+ resourceName = Path.GetFileName(path);
+ return path;
+ }
+ }
+
+ resourceName = "unit_cylinder.nwc";
+ return preferredPaths[0];
+ }
+ }
+}
diff --git a/src/Core/PathDataManager.cs b/src/Core/PathDataManager.cs
index c797993..31812b6 100644
--- a/src/Core/PathDataManager.cs
+++ b/src/Core/PathDataManager.cs
@@ -48,6 +48,10 @@ namespace NavisworksTransport
public string name { get; set; }
public string description { get; set; }
public string pathType { get; set; }
+ public string railMountMode { get; set; }
+ public string railPayloadAnchorMode { get; set; }
+ public string railPathDefinitionMode { get; set; }
+ public double railReferenceToAnchorOffset { get; set; }
public double totalLength { get; set; }
public JsonObjectLimits objectLimits { get; set; }
public double gridSize { get; set; }
@@ -275,6 +279,10 @@ namespace NavisworksTransport
name = route.Name,
description = route.Description ?? "",
pathType = route.PathType.ToString(),
+ railMountMode = route.RailMountMode.ToString(),
+ railPayloadAnchorMode = route.RailPayloadAnchorMode.ToString(),
+ railPathDefinitionMode = route.RailPathDefinitionMode.ToString(),
+ railReferenceToAnchorOffset = Math.Round(route.RailReferenceToAnchorOffset, exportSettings?.Precision ?? 3),
totalLength = Math.Round(route.TotalLength, exportSettings?.Precision ?? 3),
objectLimits = new
{
@@ -512,6 +520,26 @@ namespace NavisworksTransport
continue;
}
+ if (!string.IsNullOrEmpty(jsonRoute.railMountMode) &&
+ Enum.TryParse(jsonRoute.railMountMode, out var railMountMode))
+ {
+ route.RailMountMode = railMountMode;
+ }
+
+ if (!string.IsNullOrEmpty(jsonRoute.railPayloadAnchorMode) &&
+ Enum.TryParse(jsonRoute.railPayloadAnchorMode, out var railPayloadAnchorMode))
+ {
+ route.RailPayloadAnchorMode = railPayloadAnchorMode;
+ }
+
+ if (!string.IsNullOrEmpty(jsonRoute.railPathDefinitionMode) &&
+ Enum.TryParse(jsonRoute.railPathDefinitionMode, out var railPathDefinitionMode))
+ {
+ route.RailPathDefinitionMode = railPathDefinitionMode;
+ }
+
+ route.RailReferenceToAnchorOffset = jsonRoute.railReferenceToAnchorOffset;
+
// 解析路径点
var points = new List();
foreach (var jsonPoint in jsonRoute.points)
@@ -1369,6 +1397,10 @@ namespace NavisworksTransport
routeElement.SetAttribute("name", route.Name);
routeElement.SetAttribute("description", route.Description ?? "");
routeElement.SetAttribute("pathType", route.PathType.ToString());
+ routeElement.SetAttribute("railMountMode", route.RailMountMode.ToString());
+ routeElement.SetAttribute("railPayloadAnchorMode", route.RailPayloadAnchorMode.ToString());
+ routeElement.SetAttribute("railPathDefinitionMode", route.RailPathDefinitionMode.ToString());
+ routeElement.SetAttribute("railReferenceToAnchorOffset", route.RailReferenceToAnchorOffset.ToString("F3"));
routeElement.SetAttribute("totalLength", route.TotalLength.ToString("F3"));
routeElement.SetAttribute("maxObjectLength", route.MaxObjectLength.ToString("F3"));
routeElement.SetAttribute("maxObjectWidth", route.MaxObjectWidth.ToString("F3"));
@@ -1501,6 +1533,26 @@ namespace NavisworksTransport
LiftHeight = double.TryParse(routeNode.Attributes?["liftHeight"]?.Value, out double liftHeight) ? liftHeight : 0
};
+ if (Enum.TryParse(routeNode.Attributes?["railMountMode"]?.Value, out var railMountMode))
+ {
+ route.RailMountMode = railMountMode;
+ }
+
+ if (Enum.TryParse(routeNode.Attributes?["railPayloadAnchorMode"]?.Value, out var railPayloadAnchorMode))
+ {
+ route.RailPayloadAnchorMode = railPayloadAnchorMode;
+ }
+
+ if (Enum.TryParse(routeNode.Attributes?["railPathDefinitionMode"]?.Value, out var railPathDefinitionMode))
+ {
+ route.RailPathDefinitionMode = railPathDefinitionMode;
+ }
+
+ if (double.TryParse(routeNode.Attributes?["railReferenceToAnchorOffset"]?.Value, out double railReferenceToAnchorOffset))
+ {
+ route.RailReferenceToAnchorOffset = railReferenceToAnchorOffset;
+ }
+
// 解析创建时间
if (DateTime.TryParse(routeNode.Attributes?["created"]?.Value, out DateTime createdTime))
{
@@ -1727,4 +1779,4 @@ namespace NavisworksTransport
return Errors.Concat(Warnings);
}
}
-}
\ No newline at end of file
+}
diff --git a/src/Core/PathDatabase.cs b/src/Core/PathDatabase.cs
index b1757cd..b9d27e2 100644
--- a/src/Core/PathDatabase.cs
+++ b/src/Core/PathDatabase.cs
@@ -63,6 +63,7 @@ namespace NavisworksTransport
// 创建数据库表结构
CreateTables();
+ EnsurePathRoutesTableExtended();
if (isNewDatabase)
{
@@ -98,6 +99,10 @@ namespace NavisworksTransport
GridSize REAL,
PathType INTEGER,
LiftHeight REAL,
+ RailMountMode INTEGER,
+ RailPayloadAnchorMode INTEGER,
+ RailPathDefinitionMode INTEGER,
+ RailReferenceToAnchorOffset REAL,
CreatedTime DATETIME,
LastModified DATETIME
)
@@ -455,8 +460,8 @@ namespace NavisworksTransport
// 路径长度由 PathRoute.TotalLength 计算属性实时从 Edges/Points 计算
var sql = @"
INSERT OR REPLACE INTO PathRoutes
- (Id, Name, EstimatedTime, TurnRadius, IsCurved, MaxObjectLength, MaxObjectWidth, MaxObjectHeight, SafetyMargin, GridSize, PathType, LiftHeight, CreatedTime, LastModified)
- VALUES (@id, @name, @time, @turnRadius, @isCurved, @maxLength, @maxWidth, @maxHeight, @safetyMargin, @gridSize, @pathType, @liftHeightMeters, @created, @modified)
+ (Id, Name, EstimatedTime, TurnRadius, IsCurved, MaxObjectLength, MaxObjectWidth, MaxObjectHeight, SafetyMargin, GridSize, PathType, LiftHeight, RailMountMode, RailPayloadAnchorMode, RailPathDefinitionMode, RailReferenceToAnchorOffset, CreatedTime, LastModified)
+ VALUES (@id, @name, @time, @turnRadius, @isCurved, @maxLength, @maxWidth, @maxHeight, @safetyMargin, @gridSize, @pathType, @liftHeightMeters, @railMountMode, @railPayloadAnchorMode, @railPathDefinitionMode, @railReferenceToAnchorOffset, @created, @modified)
";
using (var cmd = new SQLiteCommand(sql, _connection))
@@ -473,6 +478,10 @@ namespace NavisworksTransport
cmd.Parameters.AddWithValue("@gridSize", route.GridSize);
cmd.Parameters.AddWithValue("@pathType", (int)route.PathType);
cmd.Parameters.AddWithValue("@liftHeightMeters", route.LiftHeight);
+ cmd.Parameters.AddWithValue("@railMountMode", (int)route.RailMountMode);
+ cmd.Parameters.AddWithValue("@railPayloadAnchorMode", (int)route.RailPayloadAnchorMode);
+ cmd.Parameters.AddWithValue("@railPathDefinitionMode", (int)route.RailPathDefinitionMode);
+ cmd.Parameters.AddWithValue("@railReferenceToAnchorOffset", route.RailReferenceToAnchorOffset);
cmd.Parameters.AddWithValue("@created", route.CreatedTime);
cmd.Parameters.AddWithValue("@modified", DateTime.Now);
cmd.ExecuteNonQuery();
@@ -1515,6 +1524,10 @@ namespace NavisworksTransport
GridSize = Convert.ToDouble(reader["GridSize"]),
PathType = (PathType)Convert.ToInt32(reader["PathType"]),
LiftHeight = Convert.ToDouble(reader["LiftHeight"]),
+ RailMountMode = (RailMountMode)Convert.ToInt32(reader["RailMountMode"]),
+ RailPayloadAnchorMode = (RailPayloadAnchorMode)Convert.ToInt32(reader["RailPayloadAnchorMode"]),
+ RailPathDefinitionMode = (RailPathDefinitionMode)Convert.ToInt32(reader["RailPathDefinitionMode"]),
+ RailReferenceToAnchorOffset = Convert.ToDouble(reader["RailReferenceToAnchorOffset"]),
CreatedTime = Convert.ToDateTime(reader["CreatedTime"]),
LastModified = Convert.ToDateTime(reader["LastModified"])
};
@@ -2696,7 +2709,7 @@ namespace NavisworksTransport
cmd.CommandText = @"
SELECT Id, Name, CreatedTime, LastModified,
TurnRadius, IsCurved, MaxObjectLength, MaxObjectWidth, MaxObjectHeight,
- SafetyMargin, GridSize, PathType, LiftHeight
+ SafetyMargin, GridSize, PathType, LiftHeight, RailMountMode, RailPayloadAnchorMode, RailPathDefinitionMode, RailReferenceToAnchorOffset
FROM PathRoutes
WHERE Id = @Id";
@@ -2720,7 +2733,11 @@ namespace NavisworksTransport
SafetyMargin = Convert.ToDouble(reader["SafetyMargin"]),
GridSize = Convert.ToDouble(reader["GridSize"]),
PathType = (PathType)Convert.ToInt32(reader["PathType"]),
- LiftHeight = Convert.ToDouble(reader["LiftHeight"])
+ LiftHeight = Convert.ToDouble(reader["LiftHeight"]),
+ RailMountMode = (RailMountMode)Convert.ToInt32(reader["RailMountMode"]),
+ RailPayloadAnchorMode = (RailPayloadAnchorMode)Convert.ToInt32(reader["RailPayloadAnchorMode"]),
+ RailPathDefinitionMode = (RailPathDefinitionMode)Convert.ToInt32(reader["RailPathDefinitionMode"]),
+ RailReferenceToAnchorOffset = Convert.ToDouble(reader["RailReferenceToAnchorOffset"])
};
// 加载路径点和路径边
@@ -2735,6 +2752,48 @@ namespace NavisworksTransport
#endregion
+ ///
+ /// 检查并扩展 PathRoutes 表结构
+ ///
+ public void EnsurePathRoutesTableExtended()
+ {
+ try
+ {
+ var checkSql = "PRAGMA table_info(PathRoutes)";
+ var existingColumns = new HashSet();
+
+ using (var cmd = new SQLiteCommand(checkSql, _connection))
+ using (var reader = cmd.ExecuteReader())
+ {
+ while (reader.Read())
+ {
+ existingColumns.Add(reader["name"].ToString().ToLower());
+ }
+ }
+
+ var columnsToAdd = new Dictionary
+ {
+ ["railmountmode"] = "ALTER TABLE PathRoutes ADD COLUMN RailMountMode INTEGER DEFAULT 0",
+ ["railpayloadanchormode"] = "ALTER TABLE PathRoutes ADD COLUMN RailPayloadAnchorMode INTEGER DEFAULT 0",
+ ["railpathdefinitionmode"] = "ALTER TABLE PathRoutes ADD COLUMN RailPathDefinitionMode INTEGER DEFAULT 0",
+ ["railreferencetoanchoroffset"] = "ALTER TABLE PathRoutes ADD COLUMN RailReferenceToAnchorOffset REAL DEFAULT 0"
+ };
+
+ foreach (var column in columnsToAdd)
+ {
+ if (!existingColumns.Contains(column.Key))
+ {
+ ExecuteNonQuery(column.Value);
+ LogManager.Info($"扩展 PathRoutes 表: 添加字段 {column.Key}");
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ LogManager.Error($"扩展 PathRoutes 表失败: {ex.Message}", ex);
+ }
+ }
+
#region 批处理队列管理(简化版)
///
diff --git a/src/Core/PathPlanningManager.cs b/src/Core/PathPlanningManager.cs
index e61ac37..f4b23f4 100644
--- a/src/Core/PathPlanningManager.cs
+++ b/src/Core/PathPlanningManager.cs
@@ -1600,6 +1600,12 @@ namespace NavisworksTransport
{
PathType = pathType
};
+
+ if (pathType == PathType.Rail)
+ {
+ newRoute.RailPathDefinitionMode = RailPathDefinitionMode.RailCenterLine;
+ }
+
_editingRoute = newRoute;
// 立即添加到路径集合,确保UI能正确找到Core路径
@@ -4194,27 +4200,30 @@ namespace NavisworksTransport
return clickedPoint;
}
- // 找到最近的基准路径点
+ // 找到最近的基准路径投影点
Point3D nearestPoint = null;
double minDistance = double.MaxValue;
double maxSnapDistance = 2.0; // 最大吸附距离(模型单位)
foreach (var pathPoints in baselinePaths.Values)
{
- foreach (var point in pathPoints)
+ var snappedPoint = FindNearestPointOnPolyline(pathPoints, clickedPoint);
+ if (snappedPoint == null)
{
- double distance = CalculateDistance3D(clickedPoint, point);
- if (distance < minDistance)
- {
- minDistance = distance;
- nearestPoint = point;
- }
+ continue;
+ }
+
+ double distance = CalculateDistance3D(clickedPoint, snappedPoint);
+ if (distance < minDistance)
+ {
+ minDistance = distance;
+ nearestPoint = snappedPoint;
}
}
if (nearestPoint != null && minDistance <= maxSnapDistance)
{
- LogManager.Info($"[空轨吸附] 点击点 ({clickedPoint.X:F2}, {clickedPoint.Y:F2}, {clickedPoint.Z:F2}) 吸附到基准路径点 ({nearestPoint.X:F2}, {nearestPoint.Y:F2}, {nearestPoint.Z:F2}),距离 {minDistance:F2}");
+ LogManager.Info($"[空轨吸附] 点击点 ({clickedPoint.X:F2}, {clickedPoint.Y:F2}, {clickedPoint.Z:F2}) 吸附到基准路径投影点 ({nearestPoint.X:F2}, {nearestPoint.Y:F2}, {nearestPoint.Z:F2}),距离 {minDistance:F2}");
return nearestPoint;
}
else
@@ -4241,6 +4250,66 @@ namespace NavisworksTransport
return Math.Sqrt(dx * dx + dy * dy + dz * dz);
}
+ ///
+ /// 查找折线上距离目标点最近的投影点
+ ///
+ private Point3D FindNearestPointOnPolyline(List pathPoints, Point3D targetPoint)
+ {
+ if (pathPoints == null || pathPoints.Count == 0)
+ {
+ return null;
+ }
+
+ if (pathPoints.Count == 1)
+ {
+ return pathPoints[0];
+ }
+
+ Point3D nearestPoint = null;
+ double minDistance = double.MaxValue;
+
+ for (int i = 0; i < pathPoints.Count - 1; i++)
+ {
+ var projectedPoint = ProjectPointToSegment(targetPoint, pathPoints[i], pathPoints[i + 1]);
+ double distance = CalculateDistance3D(targetPoint, projectedPoint);
+ if (distance < minDistance)
+ {
+ minDistance = distance;
+ nearestPoint = projectedPoint;
+ }
+ }
+
+ return nearestPoint ?? pathPoints[0];
+ }
+
+ ///
+ /// 将点投影到线段上
+ ///
+ private Point3D ProjectPointToSegment(Point3D point, Point3D segmentStart, Point3D segmentEnd)
+ {
+ double segmentX = segmentEnd.X - segmentStart.X;
+ double segmentY = segmentEnd.Y - segmentStart.Y;
+ double segmentZ = segmentEnd.Z - segmentStart.Z;
+
+ double segmentLengthSquared = segmentX * segmentX + segmentY * segmentY + segmentZ * segmentZ;
+ if (segmentLengthSquared < 1e-9)
+ {
+ return segmentStart;
+ }
+
+ double pointX = point.X - segmentStart.X;
+ double pointY = point.Y - segmentStart.Y;
+ double pointZ = point.Z - segmentStart.Z;
+
+ double projection = (pointX * segmentX + pointY * segmentY + pointZ * segmentZ) / segmentLengthSquared;
+ projection = Math.Max(0.0, Math.Min(1.0, projection));
+
+ return new Point3D(
+ segmentStart.X + projection * segmentX,
+ segmentStart.Y + projection * segmentY,
+ segmentStart.Z + projection * segmentZ);
+ }
+
///
/// 搜索并设置可通行的通道
///
diff --git a/src/Core/PathPlanningModels.cs b/src/Core/PathPlanningModels.cs
index ec31b81..6c595e5 100644
--- a/src/Core/PathPlanningModels.cs
+++ b/src/Core/PathPlanningModels.cs
@@ -113,6 +113,54 @@ namespace NavisworksTransport
Lateral
}
+ ///
+ /// Rail 路径安装方式
+ ///
+ public enum RailMountMode
+ {
+ ///
+ /// 轨下安装
+ ///
+ UnderRail = 0,
+
+ ///
+ /// 轨上安装
+ ///
+ OverRail = 1
+ }
+
+ ///
+ /// Rail 路径中构件与安装头的对接方式
+ ///
+ public enum RailPayloadAnchorMode
+ {
+ ///
+ /// 构件顶面中心与安装头对接
+ ///
+ TopCenter = 0,
+
+ ///
+ /// 构件底面中心与安装头对接
+ ///
+ BottomCenter = 1
+ }
+
+ ///
+ /// Rail 路径的参考线定义方式
+ ///
+ public enum RailPathDefinitionMode
+ {
+ ///
+ /// 旧版兼容模式:路径点即轨下悬挂参考点
+ ///
+ LegacySuspensionPoint = 0,
+
+ ///
+ /// 新版模式:路径点表示双轨参考中心线
+ ///
+ RailCenterLine = 1
+ }
+
///
/// 通道检测结果
///
@@ -690,6 +738,28 @@ namespace NavisworksTransport
///
public double LiftHeight { get; set; }
+ ///
+ /// Rail 路径安装方式(仅当 PathType == Rail 时有效)
+ ///
+ public RailMountMode RailMountMode { get; set; }
+
+ ///
+ /// Rail 路径中构件与安装头的对接方式(仅当 PathType == Rail 时有效)
+ ///
+ public RailPayloadAnchorMode RailPayloadAnchorMode { get; set; }
+
+ ///
+ /// Rail 路径参考线定义方式(仅当 PathType == Rail 时有效)
+ ///
+ public RailPathDefinitionMode RailPathDefinitionMode { get; set; }
+
+ ///
+ /// Rail 参考线到构件对接点的偏移距离(模型单位)
+ /// 在 LegacySuspensionPoint 模式下通常为 0。
+ /// 在 RailCenterLine 模式下,结合 RailMountMode 解释为相对轨道中心线的上下偏移。
+ ///
+ public double RailReferenceToAnchorOffset { get; set; }
+
// 数据库分析相关属性
///
/// 碰撞数量(从数据库加载)
@@ -728,6 +798,10 @@ namespace NavisworksTransport
Description = string.Empty;
TurnRadius = 0.0; // 0.0 表示未设置,实际使用时从配置获取默认值
IsCurved = false;
+ RailMountMode = RailMountMode.UnderRail;
+ RailPayloadAnchorMode = RailPayloadAnchorMode.TopCenter;
+ RailPathDefinitionMode = RailPathDefinitionMode.LegacySuspensionPoint;
+ RailReferenceToAnchorOffset = 0.0;
}
///
@@ -748,6 +822,10 @@ namespace NavisworksTransport
IsCurved = false;
LastModified = DateTime.Now;
Description = string.Empty;
+ RailMountMode = RailMountMode.UnderRail;
+ RailPayloadAnchorMode = RailPayloadAnchorMode.TopCenter;
+ RailPathDefinitionMode = RailPathDefinitionMode.LegacySuspensionPoint;
+ RailReferenceToAnchorOffset = 0.0;
}
///
@@ -980,7 +1058,11 @@ namespace NavisworksTransport
MaxObjectLength = MaxObjectLength,
MaxObjectWidth = MaxObjectWidth,
MaxObjectHeight = MaxObjectHeight,
- SafetyMargin = SafetyMargin
+ SafetyMargin = SafetyMargin,
+ RailMountMode = RailMountMode,
+ RailPayloadAnchorMode = RailPayloadAnchorMode,
+ RailPathDefinitionMode = RailPathDefinitionMode,
+ RailReferenceToAnchorOffset = RailReferenceToAnchorOffset
};
// 克隆所有路径点
@@ -2627,4 +2709,4 @@ namespace NavisworksTransport
}
#endregion
-}
\ No newline at end of file
+}
diff --git a/src/Core/PathPointRenderPlugin.cs b/src/Core/PathPointRenderPlugin.cs
index 3656bcb..d3fd3ab 100644
--- a/src/Core/PathPointRenderPlugin.cs
+++ b/src/Core/PathPointRenderPlugin.cs
@@ -1212,7 +1212,14 @@ namespace NavisworksTransport
var result = new Dictionary>();
foreach (var kvp in _railBaselineVisualizations)
{
- result[kvp.Key] = kvp.Value.PathLineMarkers.Select(m => m.StartPoint).ToList();
+ var pathPoints = kvp.Value.PathLineMarkers.Select(m => m.StartPoint).ToList();
+ var lastMarker = kvp.Value.PathLineMarkers.LastOrDefault();
+ if (lastMarker != null)
+ {
+ pathPoints.Add(lastMarker.EndPoint);
+ }
+
+ result[kvp.Key] = pathPoints;
}
return result;
}
@@ -1490,8 +1497,9 @@ namespace NavisworksTransport
}
else if (visualization.PathRoute.PathType == NavisworksTransport.PathType.Rail)
{
- // 空轨路径:路径点是轨道中心线位置,通行空间顶面对齐轨道,中心需要向上偏移半个高度
- verticalOffset = height / 2.0;
+ verticalOffset = RailPathPoseHelper.GetObjectSpaceCenterZOffset(
+ visualization.PathRoute,
+ height);
}
else if (visualization.PathRoute.PathType == NavisworksTransport.PathType.Hoisting)
{
@@ -1577,7 +1585,22 @@ namespace NavisworksTransport
Point3D adjustedStartPoint;
Point3D adjustedEndPoint;
- if (_showObjectSpace && Math.Abs(verticalOffset) > 0.001)
+ if (_showObjectSpace && visualization.PathRoute.PathType == NavisworksTransport.PathType.Rail)
+ {
+ adjustedStartPoint = RailPathPoseHelper.ResolveObjectSpaceCenterPosition(
+ visualization.PathRoute,
+ startPoint,
+ startPoint,
+ endPoint,
+ height);
+ adjustedEndPoint = RailPathPoseHelper.ResolveObjectSpaceCenterPosition(
+ visualization.PathRoute,
+ endPoint,
+ startPoint,
+ endPoint,
+ height);
+ }
+ else if (_showObjectSpace && Math.Abs(verticalOffset) > 0.001)
{
// 通行空间模式且有垂直偏移时,沿Z轴平移(确保只有垂直偏移,没有水平偏移)
adjustedStartPoint = new Point3D(
@@ -1648,7 +1671,39 @@ namespace NavisworksTransport
Point3D adjustedEndPoint;
List adjustedSampledPoints;
- if (_showObjectSpace && Math.Abs(verticalOffset) > 0.001)
+ if (_showObjectSpace && visualization.PathRoute.PathType == NavisworksTransport.PathType.Rail)
+ {
+ adjustedStartPoint = RailPathPoseHelper.ResolveObjectSpaceCenterPosition(
+ visualization.PathRoute,
+ startPoint,
+ startPoint,
+ endPoint,
+ height);
+ adjustedEndPoint = RailPathPoseHelper.ResolveObjectSpaceCenterPosition(
+ visualization.PathRoute,
+ endPoint,
+ startPoint,
+ endPoint,
+ height);
+
+ adjustedSampledPoints = new List();
+ if (edge.SampledPoints != null)
+ {
+ for (int sampleIndex = 0; sampleIndex < edge.SampledPoints.Count; sampleIndex++)
+ {
+ var currentPoint = edge.SampledPoints[sampleIndex];
+ var previousPoint = sampleIndex > 0 ? edge.SampledPoints[sampleIndex - 1] : currentPoint;
+ var nextPoint = sampleIndex < edge.SampledPoints.Count - 1 ? edge.SampledPoints[sampleIndex + 1] : currentPoint;
+ adjustedSampledPoints.Add(RailPathPoseHelper.ResolveObjectSpaceCenterPosition(
+ visualization.PathRoute,
+ currentPoint,
+ previousPoint,
+ nextPoint,
+ height));
+ }
+ }
+ }
+ else if (_showObjectSpace && Math.Abs(verticalOffset) > 0.001)
{
// 通行空间模式且有垂直偏移时,沿Z轴平移(确保只有垂直偏移,没有水平偏移)
adjustedStartPoint = new Point3D(
@@ -1888,7 +1943,22 @@ namespace NavisworksTransport
Point3D adjustedStartPoint;
Point3D adjustedEndPoint;
- if (_showObjectSpace && Math.Abs(verticalOffset) > 0.001)
+ if (_showObjectSpace && visualization.PathRoute.PathType == NavisworksTransport.PathType.Rail)
+ {
+ adjustedStartPoint = RailPathPoseHelper.ResolveObjectSpaceCenterPosition(
+ visualization.PathRoute,
+ startPoint.Position,
+ startPoint.Position,
+ endPoint.Position,
+ height);
+ adjustedEndPoint = RailPathPoseHelper.ResolveObjectSpaceCenterPosition(
+ visualization.PathRoute,
+ endPoint.Position,
+ startPoint.Position,
+ endPoint.Position,
+ height);
+ }
+ else if (_showObjectSpace && Math.Abs(verticalOffset) > 0.001)
{
// 通行空间模式且有垂直偏移时
// 所有空中路径:统一沿Z轴向下偏移,顶面中心对齐路径点
@@ -3417,4 +3487,4 @@ namespace NavisworksTransport
return $"CircleMarker[序号={SequenceNumber}, 类型={PointType}, 中心=({Center.X:F2},{Center.Y:F2},{Center.Z:F2}), 半径={Radius:F2}]";
}
}
-}
\ No newline at end of file
+}
diff --git a/src/PathPlanning/RailGeometryHelper.cs b/src/PathPlanning/RailGeometryHelper.cs
index e74e717..3a3b932 100644
--- a/src/PathPlanning/RailGeometryHelper.cs
+++ b/src/PathPlanning/RailGeometryHelper.cs
@@ -106,9 +106,14 @@ namespace NavisworksTransport.PathPlanning
public class RailBaselinePath
{
///
- /// 完整的基准路径点列表(下表面中心线)
+ /// 完整的基准路径点列表
///
public List PathPoints { get; set; }
+
+ ///
+ /// 基准路径的定义方式
+ ///
+ public RailPathDefinitionMode DefinitionMode { get; set; }
///
/// 路径总长度(模型单位)
@@ -162,13 +167,28 @@ namespace NavisworksTransport.PathPlanning
public static RailBaselinePath ExtractRailBaselinePath(
ModelItem railModel,
double samplingInterval = 0.5)
+ {
+ return ExtractRailBaselinePath(railModel, RailPathDefinitionMode.RailCenterLine, samplingInterval);
+ }
+
+ ///
+ /// 步骤1: 提取空轨完整基准路径(用于可视化和吸附)
+ ///
+ /// 空轨模型项
+ /// 基准线定义模式
+ /// 采样间隔(模型单位)
+ /// 空轨基准路径信息
+ public static RailBaselinePath ExtractRailBaselinePath(
+ ModelItem railModel,
+ RailPathDefinitionMode definitionMode,
+ double samplingInterval)
{
try
{
- LogManager.Debug($"[空轨] 步骤1: 开始提取完整基准路径: {railModel.DisplayName}");
+ LogManager.Debug($"[空轨] 步骤1: 开始提取完整基准路径: {railModel.DisplayName}, 模式={definitionMode}");
// 检查缓存
- var cacheKey = $"baseline_{railModel.DisplayName}_{railModel.GetHashCode()}";
+ var cacheKey = $"baseline_{definitionMode}_{railModel.DisplayName}_{railModel.GetHashCode()}";
if (_baselinePathCache.TryGetValue(cacheKey, out var cachedPath))
{
// 检查缓存是否过期
@@ -185,7 +205,9 @@ namespace NavisworksTransport.PathPlanning
}
// 使用 OBB 方法提取基准路径
- var pathPoints = ExtractRailBottomCenterLineByOBB(railModel, samplingInterval);
+ var pathPoints = definitionMode == RailPathDefinitionMode.RailCenterLine
+ ? ExtractRailCenterLineByOBB(railModel, samplingInterval)
+ : ExtractRailBottomCenterLineByOBB(railModel, samplingInterval);
LogManager.Debug($"[空轨] 生成 {pathPoints.Count} 个采样点");
// 计算路径长度
@@ -195,6 +217,7 @@ namespace NavisworksTransport.PathPlanning
var baselinePath = new RailBaselinePath
{
PathPoints = pathPoints,
+ DefinitionMode = definitionMode,
TotalLength = totalLength,
StartPoint = pathPoints.First(),
EndPoint = pathPoints.Last(),
@@ -205,7 +228,7 @@ namespace NavisworksTransport.PathPlanning
// 加入缓存
_baselinePathCache[cacheKey] = baselinePath;
- LogManager.Debug($"[空轨] 步骤1完成: 基准路径总长度 {totalLength:F2},{pathPoints.Count}个采样点");
+ LogManager.Debug($"[空轨] 步骤1完成: 基准路径总长度 {totalLength:F2},{pathPoints.Count}个采样点,模式={definitionMode}");
return baselinePath;
}
catch (Exception ex)
@@ -241,19 +264,8 @@ namespace NavisworksTransport.PathPlanning
LogManager.Debug($"[空轨] 步骤3: 吸附点到基准路径,输入点=({inputPoint.X:F2}, {inputPoint.Y:F2}, {inputPoint.Z:F2})");
- Point3D nearestPoint = baselinePath.PathPoints[0];
- double minDistance = double.MaxValue;
-
- // 找到最近的路径点
- foreach (var pathPoint in baselinePath.PathPoints)
- {
- double distance = CalculateDistance2D(inputPoint, pathPoint);
- if (distance < minDistance)
- {
- minDistance = distance;
- nearestPoint = pathPoint;
- }
- }
+ Point3D nearestPoint = FindNearestPointOnPath(baselinePath.PathPoints, inputPoint);
+ double minDistance = CalculateDistance2D(inputPoint, nearestPoint);
// 检查是否在最大吸附距离内
if (minDistance <= maxSnapDistance)
@@ -361,6 +373,66 @@ namespace NavisworksTransport.PathPlanning
return nearestIndex;
}
+ ///
+ /// 找到路径上距离目标点最近的投影点
+ ///
+ private static Point3D FindNearestPointOnPath(List pathPoints, Point3D targetPoint)
+ {
+ if (pathPoints == null || pathPoints.Count == 0)
+ {
+ return new Point3D();
+ }
+
+ if (pathPoints.Count == 1)
+ {
+ return pathPoints[0];
+ }
+
+ Point3D nearestPoint = pathPoints[0];
+ double minDistance = double.MaxValue;
+
+ for (int i = 0; i < pathPoints.Count - 1; i++)
+ {
+ var projectedPoint = ProjectPointToSegment(targetPoint, pathPoints[i], pathPoints[i + 1]);
+ double distance = CalculateDistance2D(targetPoint, projectedPoint);
+ if (distance < minDistance)
+ {
+ minDistance = distance;
+ nearestPoint = projectedPoint;
+ }
+ }
+
+ return nearestPoint;
+ }
+
+ ///
+ /// 将点投影到线段上
+ ///
+ private static Point3D ProjectPointToSegment(Point3D point, Point3D segmentStart, Point3D segmentEnd)
+ {
+ double segmentX = segmentEnd.X - segmentStart.X;
+ double segmentY = segmentEnd.Y - segmentStart.Y;
+ double segmentZ = segmentEnd.Z - segmentStart.Z;
+
+ double segmentLengthSquared = segmentX * segmentX + segmentY * segmentY + segmentZ * segmentZ;
+ if (segmentLengthSquared < 1e-9)
+ {
+ return segmentStart;
+ }
+
+ double pointX = point.X - segmentStart.X;
+ double pointY = point.Y - segmentStart.Y;
+ double pointZ = point.Z - segmentStart.Z;
+
+ double projection = (pointX * segmentX + pointY * segmentY + pointZ * segmentZ) / segmentLengthSquared;
+ projection = Math.Max(0.0, Math.Min(1.0, projection));
+
+ return new Point3D(
+ segmentStart.X + projection * segmentX,
+ segmentStart.Y + projection * segmentY,
+ segmentStart.Z + projection * segmentZ);
+ }
+
///
/// 重采样路径
///
@@ -395,8 +467,10 @@ namespace NavisworksTransport.PathPlanning
///
public static void ClearBaselinePathCache(ModelItem railModel)
{
- var cacheKey = $"baseline_{railModel.DisplayName}_{railModel.GetHashCode()}";
- _baselinePathCache.Remove(cacheKey);
+ var legacyCacheKey = $"baseline_{RailPathDefinitionMode.LegacySuspensionPoint}_{railModel.DisplayName}_{railModel.GetHashCode()}";
+ var centerCacheKey = $"baseline_{RailPathDefinitionMode.RailCenterLine}_{railModel.DisplayName}_{railModel.GetHashCode()}";
+ _baselinePathCache.Remove(legacyCacheKey);
+ _baselinePathCache.Remove(centerCacheKey);
LogManager.Debug($"[空轨] 已清除模型 {railModel.DisplayName} 的基准路径缓存");
}
@@ -465,7 +539,114 @@ namespace NavisworksTransport.PathPlanning
}
///
- /// 使用 OBB 包围体提取空轨下表面中心线(新方法,支持倾斜)
+ /// 使用 OBB 包围体提取空轨参考中心线
+ ///
+ /// 空轨模型项
+ /// 采样间隔(模型单位),默认0.5
+ /// 路径点列表(轨道参考中心线)
+ public static List ExtractRailCenterLineByOBB(
+ ModelItem railModel,
+ double samplingInterval = 0.5)
+ {
+ try
+ {
+ LogManager.Debug($"[空轨] 使用 OBB 方法提取轨道参考中心线: {railModel.DisplayName}");
+
+ var obb = ComputeOrientedBoundingBox(railModel);
+ var geometryCenterLine = TryExtractDualRailCenterLine(railModel, obb, samplingInterval);
+ if (geometryCenterLine != null && geometryCenterLine.Count >= 2)
+ {
+ LogManager.Debug($"[空轨] 双轨几何中心线提取完成: 采样点数={geometryCenterLine.Count}");
+ return geometryCenterLine;
+ }
+
+ LogManager.Warning("[空轨] 双轨几何中心线提取失败,退回 OBB 主轴中线");
+ return BuildCenterLineFromOffsets(obb, 0.0, 0.0, samplingInterval);
+ }
+ catch (Exception ex)
+ {
+ LogManager.Error($"[空轨] 提取轨道参考中心线失败: {ex.Message}");
+ throw;
+ }
+ }
+
+ private static List TryExtractDualRailCenterLine(
+ ModelItem railModel,
+ OrientedBoundingBox obb,
+ double samplingInterval)
+ {
+ var triangles = GeometryHelper.ExtractTriangles(new[] { railModel });
+ var pointSet = new HashSet();
+ foreach (var triangle in triangles)
+ {
+ pointSet.Add(triangle.Point1);
+ pointSet.Add(triangle.Point2);
+ pointSet.Add(triangle.Point3);
+ }
+
+ var points = pointSet.ToList();
+ if (points.Count < 20)
+ {
+ LogManager.Warning($"[空轨] 双轨几何中心线样本不足: {points.Count}");
+ return null;
+ }
+
+ int verticalAxisIndex = GetMostVerticalAxisIndex(obb);
+ int acrossAxisIndex = verticalAxisIndex == 1 ? 2 : 1;
+
+ var localPoints = points
+ .Select(point => ProjectPointToObbLocal(point, obb))
+ .ToList();
+
+ var railBandPoints = ExtractDominantRailBand(localPoints, verticalAxisIndex);
+ if (railBandPoints.Count < 10)
+ {
+ LogManager.Warning($"[空轨] 双轨几何中心线有效轨面点不足: {railBandPoints.Count}");
+ return null;
+ }
+
+ double centerAcross = EstimateDualRailAcrossCenter(railBandPoints, acrossAxisIndex, obb.GetAxisLength(acrossAxisIndex));
+ double centerVertical = railBandPoints.Average(point => GetAxisValue(point, verticalAxisIndex));
+
+ LogManager.Debug($"[空轨] 双轨几何中心估计: across={centerAcross:F3}, vertical={centerVertical:F3}, verticalAxis={verticalAxisIndex}, acrossAxis={acrossAxisIndex}");
+ return BuildCenterLineFromOffsets(obb, centerAcross, centerVertical, samplingInterval, acrossAxisIndex, verticalAxisIndex);
+ }
+
+ private static List BuildCenterLineFromOffsets(
+ OrientedBoundingBox obb,
+ double acrossOffset,
+ double verticalOffset,
+ double samplingInterval,
+ int acrossAxisIndex = 1,
+ int verticalAxisIndex = 2)
+ {
+ var principalAxis = obb.GetAxis(0);
+ var principalLength = obb.GetAxisLength(0);
+ var halfLength = principalLength / 2.0;
+ var acrossAxis = obb.GetAxis(acrossAxisIndex);
+ var verticalAxis = obb.GetAxis(verticalAxisIndex);
+
+ var offsetCenter = new Point3D(
+ obb.Center.X + acrossAxis.X * acrossOffset + verticalAxis.X * verticalOffset,
+ obb.Center.Y + acrossAxis.Y * acrossOffset + verticalAxis.Y * verticalOffset,
+ obb.Center.Z + acrossAxis.Z * acrossOffset + verticalAxis.Z * verticalOffset);
+
+ var start = new Point3D(
+ offsetCenter.X - principalAxis.X * halfLength,
+ offsetCenter.Y - principalAxis.Y * halfLength,
+ offsetCenter.Z - principalAxis.Z * halfLength);
+ var end = new Point3D(
+ offsetCenter.X + principalAxis.X * halfLength,
+ offsetCenter.Y + principalAxis.Y * halfLength,
+ offsetCenter.Z + principalAxis.Z * halfLength);
+
+ var sampledPath = SampleLine(start, end, samplingInterval);
+ LogManager.Debug($"[空轨] 轨道参考中心线提取完成: 主轴长度={principalLength:F2}, 采样点数={sampledPath.Count}");
+ return sampledPath;
+ }
+
+ ///
+ /// 使用 OBB 包围体提取空轨下表面中心线(旧方法,支持倾斜)
///
/// 空轨模型项
/// 采样间隔(模型单位),默认0.5
@@ -794,6 +975,193 @@ namespace NavisworksTransport.PathPlanning
return result;
}
+ private static int GetMostVerticalAxisIndex(OrientedBoundingBox obb)
+ {
+ double bestAbsDot = double.MinValue;
+ int bestIndex = 2;
+
+ for (int i = 1; i < 3; i++)
+ {
+ var axis = obb.GetAxis(i);
+ double absDot = Math.Abs(axis.Z);
+ if (absDot > bestAbsDot)
+ {
+ bestAbsDot = absDot;
+ bestIndex = i;
+ }
+ }
+
+ return bestIndex;
+ }
+
+ private static Point3D ProjectPointToObbLocal(Point3D point, OrientedBoundingBox obb)
+ {
+ double dx = point.X - obb.Center.X;
+ double dy = point.Y - obb.Center.Y;
+ double dz = point.Z - obb.Center.Z;
+
+ return new Point3D(
+ dx * obb.Axes[0].X + dy * obb.Axes[0].Y + dz * obb.Axes[0].Z,
+ dx * obb.Axes[1].X + dy * obb.Axes[1].Y + dz * obb.Axes[1].Z,
+ dx * obb.Axes[2].X + dy * obb.Axes[2].Y + dz * obb.Axes[2].Z);
+ }
+
+ private static List ExtractDominantRailBand(List localPoints, int verticalAxisIndex)
+ {
+ const int BinCount = 24;
+
+ double minValue = localPoints.Min(point => GetAxisValue(point, verticalAxisIndex));
+ double maxValue = localPoints.Max(point => GetAxisValue(point, verticalAxisIndex));
+ double range = maxValue - minValue;
+ if (range < 1e-6)
+ {
+ return new List(localPoints);
+ }
+
+ double binSize = range / BinCount;
+ var bins = new List[BinCount];
+ for (int i = 0; i < BinCount; i++)
+ {
+ bins[i] = new List();
+ }
+
+ foreach (var point in localPoints)
+ {
+ double value = GetAxisValue(point, verticalAxisIndex);
+ int binIndex = (int)((value - minValue) / binSize);
+ binIndex = Math.Max(0, Math.Min(BinCount - 1, binIndex));
+ bins[binIndex].Add(point);
+ }
+
+ int dominantBin = 0;
+ int dominantCount = bins[0].Count;
+ for (int i = 1; i < bins.Length; i++)
+ {
+ if (bins[i].Count > dominantCount)
+ {
+ dominantBin = i;
+ dominantCount = bins[i].Count;
+ }
+ }
+
+ var dominantPoints = new List();
+ int startBin = Math.Max(0, dominantBin - 1);
+ int endBin = Math.Min(BinCount - 1, dominantBin + 1);
+ for (int i = startBin; i <= endBin; i++)
+ {
+ dominantPoints.AddRange(bins[i]);
+ }
+
+ return dominantPoints.Count > 0 ? dominantPoints : localPoints;
+ }
+
+ private static double EstimateDualRailAcrossCenter(List railBandPoints, int acrossAxisIndex, double acrossAxisLength)
+ {
+ var acrossValues = railBandPoints
+ .Select(point => GetAxisValue(point, acrossAxisIndex))
+ .OrderBy(value => value)
+ .ToList();
+
+ if (acrossValues.Count < 2)
+ {
+ return acrossValues.Count == 1 ? acrossValues[0] : 0.0;
+ }
+
+ double minAcross = acrossValues.First();
+ double maxAcross = acrossValues.Last();
+ double totalWidth = maxAcross - minAcross;
+ if (totalWidth < Math.Max(1e-6, acrossAxisLength * 0.05))
+ {
+ return (minAcross + maxAcross) / 2.0;
+ }
+
+ double centerA = Percentile(acrossValues, 0.25);
+ double centerB = Percentile(acrossValues, 0.75);
+
+ for (int iteration = 0; iteration < 8; iteration++)
+ {
+ double sumA = 0.0;
+ double sumB = 0.0;
+ int countA = 0;
+ int countB = 0;
+
+ foreach (double value in acrossValues)
+ {
+ if (Math.Abs(value - centerA) <= Math.Abs(value - centerB))
+ {
+ sumA += value;
+ countA++;
+ }
+ else
+ {
+ sumB += value;
+ countB++;
+ }
+ }
+
+ if (countA == 0 || countB == 0)
+ {
+ break;
+ }
+
+ centerA = sumA / countA;
+ centerB = sumB / countB;
+ }
+
+ if (Math.Abs(centerA - centerB) < acrossAxisLength * 0.1)
+ {
+ return Median(acrossValues);
+ }
+
+ return (centerA + centerB) / 2.0;
+ }
+
+ private static double GetAxisValue(Point3D point, int axisIndex)
+ {
+ switch (axisIndex)
+ {
+ case 0:
+ return point.X;
+ case 1:
+ return point.Y;
+ case 2:
+ return point.Z;
+ default:
+ throw new ArgumentOutOfRangeException(nameof(axisIndex));
+ }
+ }
+
+ private static double Percentile(List sortedValues, double percentile)
+ {
+ if (sortedValues == null || sortedValues.Count == 0)
+ {
+ return 0.0;
+ }
+
+ if (sortedValues.Count == 1)
+ {
+ return sortedValues[0];
+ }
+
+ double clamped = Math.Max(0.0, Math.Min(1.0, percentile));
+ double position = clamped * (sortedValues.Count - 1);
+ int lowerIndex = (int)Math.Floor(position);
+ int upperIndex = (int)Math.Ceiling(position);
+
+ if (lowerIndex == upperIndex)
+ {
+ return sortedValues[lowerIndex];
+ }
+
+ double t = position - lowerIndex;
+ return sortedValues[lowerIndex] + (sortedValues[upperIndex] - sortedValues[lowerIndex]) * t;
+ }
+
+ private static double Median(List sortedValues)
+ {
+ return Percentile(sortedValues, 0.5);
+ }
+
///
/// 筛选下表面三角形(法线向下,Z < 0)
///
@@ -1070,4 +1438,4 @@ namespace NavisworksTransport.PathPlanning
}
}
}
-}
\ No newline at end of file
+}
diff --git a/src/UI/WPF/ViewModels/PathEditingViewModel.cs b/src/UI/WPF/ViewModels/PathEditingViewModel.cs
index 84139bf..db79b0b 100644
--- a/src/UI/WPF/ViewModels/PathEditingViewModel.cs
+++ b/src/UI/WPF/ViewModels/PathEditingViewModel.cs
@@ -43,6 +43,28 @@ namespace NavisworksTransport.UI.WPF.ViewModels
public string Description { get; set; }
}
+ ///
+ /// Rail 构型选项辅助类
+ ///
+ /// 枚举类型
+ public class RailConfigOption
+ {
+ ///
+ /// 配置值
+ ///
+ public T Value { get; set; }
+
+ ///
+ /// 显示名称
+ ///
+ public string DisplayName { get; set; }
+
+ ///
+ /// 详细描述
+ ///
+ public string Description { get; set; }
+ }
+
///
/// 路径编辑页面专用ViewModel
///
@@ -96,6 +118,22 @@ namespace NavisworksTransport.UI.WPF.ViewModels
// 路径策略参数
private PathStrategyOption _selectedPathStrategy;
private ObservableCollection _pathStrategyOptions;
+ private ObservableCollection> _railMountModeOptions;
+ private ObservableCollection> _railPayloadAnchorModeOptions;
+ private const double DefaultAssemblyReferenceRodLengthInMeters = 20.0;
+ private const double DefaultAssemblyReferenceRodDiameterInMeters = 0.2;
+ private string _assemblyTerminalObjectName = "未选择";
+ private string _assemblyTerminalObjectInfo = "请选择终点处已安装箱体";
+ private double _assemblyReferenceRodLengthInMeters = DefaultAssemblyReferenceRodLengthInMeters;
+ private double _assemblyReferenceRodDiameterInMeters = DefaultAssemblyReferenceRodDiameterInMeters;
+ private string _assemblyStartPointText = "未选择";
+ private RailMountMode _assemblyMountMode = RailMountMode.UnderRail;
+ private RailPayloadAnchorMode _assemblyPayloadAnchorMode = RailPayloadAnchorMode.TopCenter;
+ private bool _hasAssemblyTerminalObject;
+ private bool _isSelectingAssemblyStartPoint;
+ private ModelItem _assemblyTerminalObject;
+ private Point3D _assemblyStartPoint;
+ private const string AssemblyAnchorMarkerPathId = "assembly_anchor_marker";
// 自动路径起点和终点的路径对象引用(用于正确的ID管理)
private PathRoute _autoPathStartPointRoute = null;
@@ -192,6 +230,9 @@ namespace NavisworksTransport.UI.WPF.ViewModels
// 🔥 路径类型改变时,检查是否可以使用路径线
OnPropertyChanged(nameof(CanUsePathLines));
+ OnPropertyChanged(nameof(IsRailRouteSelected));
+ OnPropertyChanged(nameof(SelectedRailMountMode));
+ OnPropertyChanged(nameof(SelectedRailPayloadAnchorMode));
if (!CanUsePathLines && ShowPathLines)
{
// 吊装和空轨路径不能使用路径线,自动关闭
@@ -248,6 +289,131 @@ namespace NavisworksTransport.UI.WPF.ViewModels
}
}
}
+
+ public bool IsRailRouteSelected => SelectedPathRoute?.PathType == PathType.Rail;
+
+ public ObservableCollection> RailMountModeOptions
+ {
+ get => _railMountModeOptions;
+ }
+
+ public ObservableCollection> RailPayloadAnchorModeOptions
+ {
+ get => _railPayloadAnchorModeOptions;
+ }
+
+ public RailMountMode SelectedRailMountMode
+ {
+ get => GetSelectedCoreRoute()?.RailMountMode ?? RailMountMode.UnderRail;
+ set
+ {
+ var coreRoute = GetSelectedCoreRoute();
+ if (coreRoute == null || coreRoute.PathType != PathType.Rail || coreRoute.RailMountMode == value)
+ {
+ return;
+ }
+
+ coreRoute.RailMountMode = value;
+ ApplyRailRouteConfigurationChange(coreRoute, $"安装方式已更新为 {value}");
+ OnPropertyChanged(nameof(SelectedRailMountMode));
+ }
+ }
+
+ public RailPayloadAnchorMode SelectedRailPayloadAnchorMode
+ {
+ get => GetSelectedCoreRoute()?.RailPayloadAnchorMode ?? RailPayloadAnchorMode.TopCenter;
+ set
+ {
+ var coreRoute = GetSelectedCoreRoute();
+ if (coreRoute == null || coreRoute.PathType != PathType.Rail || coreRoute.RailPayloadAnchorMode == value)
+ {
+ return;
+ }
+
+ coreRoute.RailPayloadAnchorMode = value;
+ ApplyRailRouteConfigurationChange(coreRoute, $"对接基准已更新为 {value}");
+ OnPropertyChanged(nameof(SelectedRailPayloadAnchorMode));
+ }
+ }
+
+ public string AssemblyTerminalObjectName
+ {
+ get => _assemblyTerminalObjectName;
+ set => SetProperty(ref _assemblyTerminalObjectName, value);
+ }
+
+ public string AssemblyTerminalObjectInfo
+ {
+ get => _assemblyTerminalObjectInfo;
+ set => SetProperty(ref _assemblyTerminalObjectInfo, value);
+ }
+
+ public double AssemblyReferenceRodLengthInMeters
+ {
+ get => _assemblyReferenceRodLengthInMeters;
+ set
+ {
+ if (SetProperty(ref _assemblyReferenceRodLengthInMeters, value))
+ {
+ OnPropertyChanged(nameof(CanGenerateAssemblyReferenceRod));
+ }
+ }
+ }
+
+ public double AssemblyReferenceRodDiameterInMeters
+ {
+ get => _assemblyReferenceRodDiameterInMeters;
+ set
+ {
+ if (SetProperty(ref _assemblyReferenceRodDiameterInMeters, value))
+ {
+ OnPropertyChanged(nameof(CanGenerateAssemblyReferenceRod));
+ }
+ }
+ }
+
+ public bool HasAssemblyTerminalObject
+ {
+ get => _hasAssemblyTerminalObject;
+ set => SetProperty(ref _hasAssemblyTerminalObject, value);
+ }
+
+ public string AssemblyStartPointText
+ {
+ get => _assemblyStartPointText;
+ set => SetProperty(ref _assemblyStartPointText, value);
+ }
+
+ public RailPayloadAnchorMode AssemblyPayloadAnchorMode
+ {
+ get => _assemblyPayloadAnchorMode;
+ set
+ {
+ if (SetProperty(ref _assemblyPayloadAnchorMode, value) && HasAssemblyTerminalObject)
+ {
+ RefreshAssemblyTerminalObjectInfo();
+ RefreshAssemblyReferenceRodIfNeeded();
+ }
+ }
+ }
+
+ public RailMountMode AssemblyMountMode
+ {
+ get => _assemblyMountMode;
+ set
+ {
+ if (SetProperty(ref _assemblyMountMode, value) && HasAssemblyTerminalObject)
+ {
+ RefreshAssemblyTerminalObjectInfo();
+ }
+ }
+ }
+
+ public bool IsSelectingAssemblyStartPoint
+ {
+ get => _isSelectingAssemblyStartPoint;
+ set => SetProperty(ref _isSelectingAssemblyStartPoint, value);
+ }
///
/// 更新路径可视化显示:清理其他路径,只显示当前选中的路径
///
@@ -763,6 +929,10 @@ namespace NavisworksTransport.UI.WPF.ViewModels
public ICommand ImportPathCommand { get; private set; }
public ICommand ExportPathCommand { get; private set; }
public ICommand SaveAsPathCommand { get; private set; }
+ public ICommand CaptureAssemblyTerminalObjectCommand { get; private set; }
+ public ICommand GenerateAssemblyReferenceRodCommand { get; private set; }
+ public ICommand SelectAssemblyStartPointCommand { get; private set; }
+ public ICommand ClearAssemblyReferenceRodCommand { get; private set; }
// 多层吊装命令
public ICommand SelectMultiLevelStartPointCommand { get; private set; }
@@ -798,6 +968,13 @@ namespace NavisworksTransport.UI.WPF.ViewModels
public bool CanExecuteExportPath => _pathPlanningManager?.Routes?.Count > 0 || SelectedPathRoute != null;
public bool CanExecuteSaveAsPath => SelectedPathRoute != null && SelectedPathRoute.Points.Count > 0;
+ public bool CanGenerateAssemblyReferenceRod => HasAssemblyTerminalObject &&
+ AssemblyReferenceRodLengthInMeters > 0 &&
+ AssemblyReferenceRodDiameterInMeters > 0;
+ public bool CanSelectAssemblyStartPoint => HasAssemblyTerminalObject &&
+ _pathPlanningManager != null &&
+ AssemblyReferencePathManager.Instance.HasReferenceLine &&
+ !IsSelectingAssemblyStartPoint;
public bool CanExecuteModifyPoint => SelectedPathPoint != null &&
_pathPlanningManager?.PathEditState != PathEditState.EditingPoint;
@@ -835,6 +1012,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
// 初始化路径策略选项
InitializePathStrategyOptions();
+ InitializeRailConfigOptions();
// 从配置加载参数
LoadParametersFromConfig();
@@ -897,6 +1075,66 @@ namespace NavisworksTransport.UI.WPF.ViewModels
_selectedPathStrategy = _pathStrategyOptions.FirstOrDefault(x => x.Value == PathStrategy.Shortest);
}
+ ///
+ /// 初始化 Rail 构型选项
+ ///
+ private void InitializeRailConfigOptions()
+ {
+ _railMountModeOptions = new ObservableCollection>
+ {
+ new RailConfigOption
+ {
+ Value = RailMountMode.UnderRail,
+ DisplayName = "轨下安装",
+ Description = "安装头位于双轨下方,构件从轨道平面下侧连接"
+ },
+ new RailConfigOption
+ {
+ Value = RailMountMode.OverRail,
+ DisplayName = "轨上安装",
+ Description = "安装头位于双轨上方,构件从轨道平面上侧连接"
+ }
+ };
+
+ _railPayloadAnchorModeOptions = new ObservableCollection>
+ {
+ new RailConfigOption
+ {
+ Value = RailPayloadAnchorMode.TopCenter,
+ DisplayName = "顶面对接",
+ Description = "以箱体顶面中心与安装头刚性对接"
+ },
+ new RailConfigOption
+ {
+ Value = RailPayloadAnchorMode.BottomCenter,
+ DisplayName = "底面对接",
+ Description = "以箱体底面中心与安装头刚性对接"
+ }
+ };
+ }
+
+ private PathRoute GetSelectedCoreRoute()
+ {
+ if (_pathPlanningManager == null || SelectedPathRoute == null)
+ {
+ return null;
+ }
+
+ return _pathPlanningManager.Routes?.FirstOrDefault(route => route.Name == SelectedPathRoute.Name);
+ }
+
+ private void ApplyRailRouteConfigurationChange(PathRoute coreRoute, string logMessage)
+ {
+ coreRoute.LastModified = DateTime.Now;
+
+ SyncObjectParametersToRenderPlugin();
+ _pathPlanningManager.DrawRouteVisualization(coreRoute, isAutoPath: false);
+ _pathPlanningManager.SavePathToDatabase(coreRoute);
+ _pathPlanningManager.RaiseVisualizationStateChanged();
+
+ LogManager.Info($"[Rail构型] {coreRoute.Name}: {logMessage}");
+ }
+
///
/// 从配置文件加载参数
///
@@ -1031,6 +1269,10 @@ namespace NavisworksTransport.UI.WPF.ViewModels
ImportPathCommand = new RelayCommand(async () => await ExecuteImportPathAsync());
ExportPathCommand = new RelayCommand(async () => await ExecuteExportPathAsync(), () => CanExecuteExportPath);
SaveAsPathCommand = new RelayCommand(async () => await ExecuteSaveAsPathAsync(), () => CanExecuteSaveAsPath);
+ CaptureAssemblyTerminalObjectCommand = new RelayCommand(async () => await ExecuteCaptureAssemblyTerminalObjectAsync());
+ GenerateAssemblyReferenceRodCommand = new RelayCommand(async () => await ExecuteGenerateAssemblyReferenceRodAsync(), () => CanGenerateAssemblyReferenceRod);
+ SelectAssemblyStartPointCommand = new RelayCommand(async () => await ExecuteSelectAssemblyStartPointAsync(), () => CanSelectAssemblyStartPoint);
+ ClearAssemblyReferenceRodCommand = new RelayCommand(() => ExecuteClearAssemblyReferenceRod());
}
#endregion
@@ -1039,6 +1281,349 @@ namespace NavisworksTransport.UI.WPF.ViewModels
#region 路径管理命令
+ private async Task ExecuteCaptureAssemblyTerminalObjectAsync()
+ {
+ await SafeExecuteAsync(() =>
+ {
+ var document = NavisApplication.ActiveDocument;
+ var selectedItem = document?.CurrentSelection?.SelectedItems?.FirstOrDefault();
+ if (selectedItem == null)
+ {
+ throw new InvalidOperationException("请先在 Navisworks 中选择终点处已安装的箱体");
+ }
+
+ if (!ModelItemAnalysisHelper.IsModelItemValid(selectedItem))
+ {
+ throw new InvalidOperationException("当前选择对象无效,请重新选择终点箱体");
+ }
+
+ _assemblyTerminalObject = selectedItem;
+ _assemblyStartPoint = Point3D.Origin;
+ HasAssemblyTerminalObject = true;
+ AssemblyStartPointText = "未选择";
+ AssemblyTerminalObjectName = ModelItemAnalysisHelper.GetSafeDisplayName(selectedItem);
+ RefreshAssemblyTerminalObjectInfo();
+ RenderAssemblyAnchorMarker();
+ OnPropertyChanged(nameof(CanGenerateAssemblyReferenceRod));
+ OnPropertyChanged(nameof(CanSelectAssemblyStartPoint));
+ UpdateMainStatus($"已捕获终点箱体: {AssemblyTerminalObjectName}");
+ LogManager.Info($"[直线装配] 已捕获终点箱体: {AssemblyTerminalObjectName}");
+ }, "捕获终点箱体");
+ }
+
+ private async Task ExecuteGenerateAssemblyReferenceRodAsync()
+ {
+ await SafeExecuteAsync(() =>
+ {
+ if (_assemblyTerminalObject == null || !ModelItemAnalysisHelper.IsModelItemValid(_assemblyTerminalObject))
+ {
+ throw new InvalidOperationException("终点箱体未设置或已失效,请重新捕获终点箱体");
+ }
+
+ var forward = ModelItemTransformHelper.GetForwardDirectionFromTransform(_assemblyTerminalObject.Transform);
+ var endPoint = GetAssemblyTerminalAnchorPoint();
+ double rodLength = UnitsConverter.ConvertFromMeters(AssemblyReferenceRodLengthInMeters);
+ var startPoint = new Point3D(
+ endPoint.X - forward.X * rodLength,
+ endPoint.Y - forward.Y * rodLength,
+ endPoint.Z - forward.Z * rodLength);
+
+ AssemblyReferencePathManager.Instance.CreateOrUpdateReferenceRod(
+ startPoint,
+ endPoint,
+ AssemblyReferenceRodDiameterInMeters);
+
+ _assemblyStartPoint = Point3D.Origin;
+ AssemblyStartPointText = "未选择";
+ RefreshAssemblyTerminalObjectInfo(startPoint, endPoint);
+ RenderAssemblyAnchorMarker();
+
+ UpdateMainStatus("已生成直线装配参考杆,请在三维视图中确认位置");
+ OnPropertyChanged(nameof(CanSelectAssemblyStartPoint));
+ LogManager.Info($"[直线装配] 已生成参考杆: {AssemblyTerminalObjectName}");
+ }, "生成装配参考杆");
+ }
+
+ private async Task ExecuteSelectAssemblyStartPointAsync()
+ {
+ await SafeExecuteAsync(() =>
+ {
+ if (_pathPlanningManager == null)
+ {
+ throw new InvalidOperationException("路径规划管理器未初始化,无法选择装配起点");
+ }
+
+ if (_assemblyTerminalObject == null || !ModelItemAnalysisHelper.IsModelItemValid(_assemblyTerminalObject))
+ {
+ throw new InvalidOperationException("终点箱体未设置或已失效,请重新捕获终点箱体");
+ }
+
+ if (!AssemblyReferencePathManager.Instance.HasReferenceLine)
+ {
+ throw new InvalidOperationException("请先生成直线装配参考杆");
+ }
+
+ CleanupAssemblyReferenceSelection();
+
+ _pathPlanningManager.DisableMouseHandling();
+ IsSelectingAssemblyStartPoint = true;
+ OnPropertyChanged(nameof(CanSelectAssemblyStartPoint));
+
+ PathClickToolPlugin.MouseClicked -= OnAssemblyReferenceMouseClicked;
+ PathClickToolPlugin.MouseClicked += OnAssemblyReferenceMouseClicked;
+
+ if (!ForceReinitializeToolPlugin(subscribeToEvents: false))
+ {
+ CleanupAssemblyReferenceSelection();
+ throw new InvalidOperationException("ToolPlugin 初始化失败,请重试");
+ }
+
+ _pathPlanningManager.StartClickTool(PathPointType.StartPoint);
+ UpdateMainStatus("请在参考杆附近点击一个起点,系统会自动吸附到参考线上并生成直线路径");
+ LogManager.Info("[直线装配] 已进入起点拾取模式");
+ }, "选择直线装配起点");
+ }
+
+ private void ExecuteClearAssemblyReferenceRod()
+ {
+ try
+ {
+ CleanupAssemblyReferenceSelection();
+ AssemblyReferencePathManager.Instance.HideReferenceRod();
+ ClearAssemblyAnchorMarker();
+ AssemblyStartPointText = "未选择";
+ OnPropertyChanged(nameof(CanSelectAssemblyStartPoint));
+ UpdateMainStatus("已隐藏直线装配参考杆");
+ LogManager.Info("[直线装配] 已隐藏参考杆");
+ }
+ catch (Exception ex)
+ {
+ LogManager.Error($"[直线装配] 隐藏参考杆失败: {ex.Message}");
+ }
+ }
+
+ private async void OnAssemblyReferenceMouseClicked(object sender, PickItemResult pickResult)
+ {
+ try
+ {
+ if (!IsSelectingAssemblyStartPoint || pickResult == null)
+ {
+ return;
+ }
+
+ Point3D projectedStartPoint = AssemblyReferencePathManager.Instance.ProjectPointToReferenceLine(pickResult.Point);
+ _assemblyStartPoint = projectedStartPoint;
+ AssemblyStartPointText = $"({projectedStartPoint.X:F2}, {projectedStartPoint.Y:F2}, {projectedStartPoint.Z:F2})";
+
+ await SafeExecuteAsync(() =>
+ {
+ CreateAssemblyLinearRoute(projectedStartPoint);
+ CleanupAssemblyReferenceSelection();
+ UpdateMainStatus("已根据参考杆起点生成直线装配路径");
+ }, "生成直线装配路径");
+ }
+ catch (Exception ex)
+ {
+ LogManager.Error($"[直线装配] 处理起点拾取失败: {ex.Message}", ex);
+ UpdateMainStatus($"直线装配起点拾取失败: {ex.Message}");
+ CleanupAssemblyReferenceSelection();
+ }
+ }
+
+ private void CreateAssemblyLinearRoute(Point3D startPoint)
+ {
+ Point3D endPoint = AssemblyReferencePathManager.Instance.ReferenceLineEnd;
+ string routeName = $"直线装配_{_pathPlanningManager.Routes.Count + 1}";
+
+ var route = new PathRoute(routeName)
+ {
+ Description = $"直线装配路径 - {AssemblyTerminalObjectName}",
+ PathType = PathType.Rail,
+ RailMountMode = AssemblyMountMode,
+ RailPayloadAnchorMode = AssemblyPayloadAnchorMode,
+ RailPathDefinitionMode = RailPathDefinitionMode.RailCenterLine
+ };
+
+ route.AddPoint(new PathPoint(_assemblyStartPoint, "装配起点", PathPointType.StartPoint));
+ route.AddPoint(new PathPoint(endPoint, "装配终点", PathPointType.EndPoint));
+
+ if (!_pathPlanningManager.AddRoute(route))
+ {
+ throw new InvalidOperationException("装配路径添加失败");
+ }
+
+ _pathPlanningManager.SetCurrentRoute(route);
+ _pathPlanningManager.DrawRouteVisualization(route, isAutoPath: false);
+ _pathPlanningManager.SavePathToDatabase(route);
+
+ if (!_pathPlanningManager.GeneratePath(route))
+ {
+ throw new InvalidOperationException("装配路径生成失败");
+ }
+
+ AssemblyTerminalObjectInfo = string.Format(
+ "对接终点=({0:F2}, {1:F2}, {2:F2}),装配起点={3},安装方式={4},对接基准={5}",
+ endPoint.X,
+ endPoint.Y,
+ endPoint.Z,
+ AssemblyStartPointText,
+ AssemblyMountMode == RailMountMode.OverRail ? "轨上安装" : "轨下安装",
+ AssemblyPayloadAnchorMode == RailPayloadAnchorMode.TopCenter ? "顶面对接" : "底面对接");
+
+ OnPropertyChanged(nameof(PathRoutes));
+ LogManager.Info($"[直线装配] 已生成路径: {route.Name}");
+ }
+
+ private Point3D GetAssemblyTerminalAnchorPoint()
+ {
+ if (_assemblyTerminalObject == null || !ModelItemAnalysisHelper.IsModelItemValid(_assemblyTerminalObject))
+ {
+ throw new InvalidOperationException("终点箱体未设置或已失效,无法计算对接点");
+ }
+
+ var bounds = _assemblyTerminalObject.BoundingBox();
+ var center = bounds.Center;
+ Vector3D up = ModelItemTransformHelper.GetUpDirectionFromTransform(_assemblyTerminalObject.Transform);
+ Vector3D localSize = ModelItemTransformHelper.EstimateLocalBoxSize(bounds, _assemblyTerminalObject.Transform);
+ double halfHeight = Math.Max(0.0, localSize.Z / 2.0);
+ double direction = AssemblyPayloadAnchorMode == RailPayloadAnchorMode.TopCenter ? 1.0 : -1.0;
+
+ return new Point3D(
+ center.X + up.X * halfHeight * direction,
+ center.Y + up.Y * halfHeight * direction,
+ center.Z + up.Z * halfHeight * direction);
+ }
+
+ private void RefreshAssemblyTerminalObjectInfo(Point3D referenceStartPoint = null, Point3D referenceEndPoint = null)
+ {
+ if (_assemblyTerminalObject == null || !ModelItemAnalysisHelper.IsModelItemValid(_assemblyTerminalObject))
+ {
+ return;
+ }
+
+ var bounds = _assemblyTerminalObject.BoundingBox();
+ Point3D anchorPoint = GetAssemblyTerminalAnchorPoint();
+ string anchorText = AssemblyPayloadAnchorMode == RailPayloadAnchorMode.TopCenter ? "顶面对接" : "底面对接";
+ string mountText = AssemblyMountMode == RailMountMode.OverRail ? "轨上安装" : "轨下安装";
+
+ if (referenceStartPoint != null && referenceEndPoint != null)
+ {
+ AssemblyTerminalObjectInfo = string.Format(
+ "中心=({0:F2}, {1:F2}, {2:F2}),尺寸=({3:F2}, {4:F2}, {5:F2}),{6},{7}点=({8:F2}, {9:F2}, {10:F2}),参考杆起点=({11:F2}, {12:F2}, {13:F2})",
+ bounds.Center.X,
+ bounds.Center.Y,
+ bounds.Center.Z,
+ bounds.Max.X - bounds.Min.X,
+ bounds.Max.Y - bounds.Min.Y,
+ bounds.Max.Z - bounds.Min.Z,
+ mountText,
+ anchorText,
+ referenceEndPoint.X,
+ referenceEndPoint.Y,
+ referenceEndPoint.Z,
+ referenceStartPoint.X,
+ referenceStartPoint.Y,
+ referenceStartPoint.Z);
+ return;
+ }
+
+ AssemblyTerminalObjectInfo = string.Format(
+ "中心=({0:F2}, {1:F2}, {2:F2}),尺寸=({3:F2}, {4:F2}, {5:F2}),{6},{7}点=({8:F2}, {9:F2}, {10:F2})",
+ bounds.Center.X,
+ bounds.Center.Y,
+ bounds.Center.Z,
+ bounds.Max.X - bounds.Min.X,
+ bounds.Max.Y - bounds.Min.Y,
+ bounds.Max.Z - bounds.Min.Z,
+ mountText,
+ anchorText,
+ anchorPoint.X,
+ anchorPoint.Y,
+ anchorPoint.Z);
+ }
+
+ private void RefreshAssemblyReferenceRodIfNeeded()
+ {
+ if (!AssemblyReferencePathManager.Instance.HasReferenceLine || _assemblyTerminalObject == null)
+ {
+ return;
+ }
+
+ try
+ {
+ Point3D endPoint = GetAssemblyTerminalAnchorPoint();
+ Vector3D forward = ModelItemTransformHelper.GetForwardDirectionFromTransform(_assemblyTerminalObject.Transform);
+ double rodLength = UnitsConverter.ConvertFromMeters(AssemblyReferenceRodLengthInMeters);
+ Point3D startPoint = new Point3D(
+ endPoint.X - forward.X * rodLength,
+ endPoint.Y - forward.Y * rodLength,
+ endPoint.Z - forward.Z * rodLength);
+
+ AssemblyReferencePathManager.Instance.CreateOrUpdateReferenceRod(
+ startPoint,
+ endPoint,
+ AssemblyReferenceRodDiameterInMeters);
+
+ RefreshAssemblyTerminalObjectInfo(startPoint, endPoint);
+ RenderAssemblyAnchorMarker();
+ OnPropertyChanged(nameof(CanSelectAssemblyStartPoint));
+ }
+ catch (Exception ex)
+ {
+ LogManager.Error($"[直线装配] 刷新参考杆失败: {ex.Message}", ex);
+ }
+ }
+
+ private void RenderAssemblyAnchorMarker()
+ {
+ if (_assemblyTerminalObject == null || !ModelItemAnalysisHelper.IsModelItemValid(_assemblyTerminalObject))
+ {
+ return;
+ }
+
+ var renderPlugin = PathPointRenderPlugin.Instance;
+ if (renderPlugin == null)
+ {
+ return;
+ }
+
+ Point3D anchorPoint = GetAssemblyTerminalAnchorPoint();
+ var markerRoute = new PathRoute("装配对接点")
+ {
+ Id = AssemblyAnchorMarkerPathId,
+ Description = "直线装配终点对接标记"
+ };
+ markerRoute.AddPoint(new PathPoint(anchorPoint, "对接点", PathPointType.EndPoint));
+ renderPlugin.RenderPointOnly(markerRoute);
+ }
+
+ private void ClearAssemblyAnchorMarker()
+ {
+ var renderPlugin = PathPointRenderPlugin.Instance;
+ if (renderPlugin == null)
+ {
+ return;
+ }
+
+ renderPlugin.RemovePath(AssemblyAnchorMarkerPathId);
+ }
+
+ private void CleanupAssemblyReferenceSelection()
+ {
+ try
+ {
+ PathClickToolPlugin.MouseClicked -= OnAssemblyReferenceMouseClicked;
+ IsSelectingAssemblyStartPoint = false;
+ _pathPlanningManager?.EnableMouseHandling();
+ _pathPlanningManager?.StopClickTool();
+ OnPropertyChanged(nameof(CanSelectAssemblyStartPoint));
+ }
+ catch (Exception ex)
+ {
+ LogManager.Error($"[直线装配] 清理起点拾取状态失败: {ex.Message}", ex);
+ }
+ }
+
private async Task ExecuteNewPathAsync()
{
await SafeExecuteAsync(() =>
@@ -1117,7 +1702,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
{
await SafeExecuteAsync(() =>
{
- UpdateMainStatus("正在创建新空轨路径...");
+ UpdateMainStatus("正在创建传统空轨路径...");
// 检查是否有空轨基准路径
if (PathPointRenderPlugin.Instance != null)
@@ -1126,11 +1711,11 @@ namespace NavisworksTransport.UI.WPF.ViewModels
{
// 清除所有现有路径的可视化显示
PathPointRenderPlugin.Instance.ClearPathsExcept("grid_visualization_all", "grid_visualization_channel", "grid_visualization_unknown", "grid_visualization_obstacle", "grid_visualization_door");
- LogManager.Info("新建空轨路径:已清除现有路径可视化显示(保留网格可视化)");
+ LogManager.Info("新建传统空轨路径:已清除现有路径可视化显示(保留网格可视化)");
}
catch (Exception ex)
{
- LogManager.Error($"新建空轨路径:清除现有路径可视化失败: {ex.Message}", ex);
+ LogManager.Error($"新建传统空轨路径:清除现有路径可视化失败: {ex.Message}", ex);
throw;
}
}
@@ -1140,7 +1725,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
if (_pathPlanningManager != null)
{
- // 创建空轨路径
+ // 创建传统空轨路径
var newRoute = _pathPlanningManager.StartCreatingNewRoute(
isRailPath: true,
pathType: PathType.Rail);
@@ -1170,22 +1755,22 @@ namespace NavisworksTransport.UI.WPF.ViewModels
if (!ForceReinitializeToolPlugin(subscribeToEvents: true))
{
UpdateMainStatus("ToolPlugin初始化失败,请重试");
- LogManager.Error("新建空轨路径:ToolPlugin初始化失败");
+ LogManager.Error("新建传统空轨路径:ToolPlugin初始化失败");
return;
}
// 启动点击工具,设置空轨吸附模式
_pathPlanningManager.StartClickTool(PathPointType.WayPoint, enableRailSnapping: true);
- LogManager.Info($"已启动空轨路径点击工具(吸附模式): {newRoute.Name}");
+ LogManager.Info($"已启动传统空轨路径点击工具(吸附模式): {newRoute.Name}");
- UpdateMainStatus($"已进入新建空轨路径模式: {newRoute.Name} - 请在3D视图中点击空轨模型设置路径点(将自动吸附到基准路径)");
- LogManager.Info($"开始新建空轨路径: {newRoute.Name},已强制重新初始化ToolPlugin获取鼠标焦点");
+ UpdateMainStatus($"已进入传统空轨路径模式: {newRoute.Name} - 请在3D视图中点击空轨模型设置路径点(将自动吸附到基准路径)");
+ LogManager.Info($"开始新建传统空轨路径: {newRoute.Name},已强制重新初始化ToolPlugin获取鼠标焦点");
}
else
{
- UpdateMainStatus("创建新空轨路径失败:没有可通行的物流模型");
- LogManager.Error("创建新空轨路径失败:没有可通行的物流模型");
- MessageBox.Show("创建新空轨路径失败:没有找到任何可通行的物流模型。\n请先为模型设置可通行的物流属性,然后再尝试创建路径。", "错误",
+ UpdateMainStatus("创建传统空轨路径失败:没有可通行的物流模型");
+ LogManager.Error("创建传统空轨路径失败:没有可通行的物流模型");
+ MessageBox.Show("创建传统空轨路径失败:没有找到任何可通行的物流模型。\n请先为模型设置可通行的物流属性,然后再尝试创建路径。", "错误",
MessageBoxButton.OK, MessageBoxImage.Warning);
}
}
@@ -1194,7 +1779,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
UpdateMainStatus("路径规划管理器未初始化");
LogManager.Error("路径规划管理器未初始化");
}
- }, "新建空轨路径");
+ }, "新建传统空轨路径");
}
///
@@ -4468,6 +5053,24 @@ namespace NavisworksTransport.UI.WPF.ViewModels
LogManager.Warning($"清理自动路径事件订阅时发生异常: {ex.Message}");
}
+ try
+ {
+ CleanupAssemblyReferenceSelection();
+ }
+ catch (Exception ex)
+ {
+ LogManager.Warning($"清理直线装配事件订阅时发生异常: {ex.Message}");
+ }
+
+ try
+ {
+ ClearAssemblyAnchorMarker();
+ }
+ catch (Exception ex)
+ {
+ LogManager.Warning($"清理直线装配对接点标记时发生异常: {ex.Message}");
+ }
+
// 确保停止任何活动的点击工具
try
{
@@ -4536,4 +5139,4 @@ namespace NavisworksTransport.UI.WPF.ViewModels
public double Height { get; set; }
public double SafetyMargin { get; set; }
}
-}
\ No newline at end of file
+}
diff --git a/src/UI/WPF/Views/PathEditingView.xaml b/src/UI/WPF/Views/PathEditingView.xaml
index 9724bcd..e8f87cb 100644
--- a/src/UI/WPF/Views/PathEditingView.xaml
+++ b/src/UI/WPF/Views/PathEditingView.xaml
@@ -206,10 +206,10 @@ NavisworksTransport 路径编辑页签视图 - 采用与动画控制和分层管
-
+ Style="{StaticResource SecondaryButtonStyle}"
+ ToolTip="创建基于空轨基准线吸附的传统 Rail 路径"/>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Utils/ModelItemTransformHelper.cs b/src/Utils/ModelItemTransformHelper.cs
index bf8814d..35af3a2 100644
--- a/src/Utils/ModelItemTransformHelper.cs
+++ b/src/Utils/ModelItemTransformHelper.cs
@@ -53,31 +53,39 @@ namespace NavisworksTransport.Utils
try
{
- // 获取变换的组件
var components = transform.Factor();
- var rotation = components.Rotation;
+ return GetYawFromRotation(components.Rotation);
+ }
+ catch (Exception)
+ {
+ return 0.0;
+ }
+ }
- // 使用ToAxisAndAngle()提取轴和角度
+ ///
+ /// 从 Rotation3D 中提取 XY 平面的 yaw 角(弧度)。
+ ///
+ public static double GetYawFromRotation(Rotation3D rotation)
+ {
+ if (rotation == null)
+ {
+ return 0.0;
+ }
+
+ try
+ {
var rotationResult = rotation.ToAxisAndAngle();
double angle = rotationResult.Angle;
double ux = rotationResult.Axis.X;
double uy = rotationResult.Axis.Y;
double uz = rotationResult.Axis.Z;
- // 旋转公式(罗德里格旋转公式简化版,提取局部X轴(1,0,0)旋转后的方向)
- // 旋转后的向量 v' = v*cosθ + (u×v)*sinθ + u*(u·v)*(1-cosθ)
- // 当 v = (1,0,0) 时:
- // v'.x = cosθ + ux*ux*(1-cosθ)
- // v'.y = uz*sinθ + ux*uy*(1-cosθ)
-
double cos = Math.Cos(angle);
double sin = Math.Sin(angle);
- // 计算旋转后的X轴在世界坐标系XY平面上的分量
double vx = cos + ux * ux * (1 - cos);
double vy = uz * sin + ux * uy * (1 - cos);
- // 在XY平面上通过Atan2计算偏航角
return Math.Atan2(vy, vx);
}
catch (Exception)
@@ -86,6 +94,134 @@ namespace NavisworksTransport.Utils
}
}
+ ///
+ /// 获取变换后局部 X 轴在世界坐标中的方向,通常可作为物体主朝向。
+ ///
+ public static Vector3D GetForwardDirectionFromTransform(Transform3D transform)
+ {
+ return GetAxisDirectionFromTransform(transform, 0, new Vector3D(1, 0, 0));
+ }
+
+ ///
+ /// 获取变换后局部 Z 轴在世界坐标中的方向,可作为构件顶/底面法向。
+ ///
+ public static Vector3D GetUpDirectionFromTransform(Transform3D transform)
+ {
+ return GetAxisDirectionFromTransform(transform, 2, new Vector3D(0, 0, 1));
+ }
+
+ ///
+ /// 根据世界轴对齐包围盒和物体变换,估算物体在局部坐标系中的尺寸。
+ /// 适用于刚性箱体类对象,用于顶/底面对接点推导。
+ ///
+ public static Vector3D EstimateLocalBoxSize(BoundingBox3D bounds, Transform3D transform)
+ {
+ double halfXWorld = Math.Max(0.0, (bounds.Max.X - bounds.Min.X) / 2.0);
+ double halfYWorld = Math.Max(0.0, (bounds.Max.Y - bounds.Min.Y) / 2.0);
+ double halfZWorld = Math.Max(0.0, (bounds.Max.Z - bounds.Min.Z) / 2.0);
+
+ if (transform == null)
+ {
+ return new Vector3D(halfXWorld * 2.0, halfYWorld * 2.0, halfZWorld * 2.0);
+ }
+
+ try
+ {
+ var forward = GetForwardDirectionFromTransform(transform);
+ var localY = GetAxisDirectionFromTransform(transform, 1, new Vector3D(0, 1, 0));
+ var up = GetUpDirectionFromTransform(transform);
+
+ double[,] matrix =
+ {
+ { Math.Abs(forward.X), Math.Abs(localY.X), Math.Abs(up.X) },
+ { Math.Abs(forward.Y), Math.Abs(localY.Y), Math.Abs(up.Y) },
+ { Math.Abs(forward.Z), Math.Abs(localY.Z), Math.Abs(up.Z) }
+ };
+
+ double[] worldHalfExtents = { halfXWorld, halfYWorld, halfZWorld };
+ double[] localHalfExtents = SolveLinearSystem3x3(matrix, worldHalfExtents);
+
+ return new Vector3D(
+ Math.Max(0.0, localHalfExtents[0] * 2.0),
+ Math.Max(0.0, localHalfExtents[1] * 2.0),
+ Math.Max(0.0, localHalfExtents[2] * 2.0));
+ }
+ catch (Exception)
+ {
+ return new Vector3D(halfXWorld * 2.0, halfYWorld * 2.0, halfZWorld * 2.0);
+ }
+ }
+
+ private static Vector3D GetAxisDirectionFromTransform(Transform3D transform, int axisIndex, Vector3D fallback)
+ {
+ if (transform == null)
+ {
+ return fallback;
+ }
+
+ try
+ {
+ var linear = transform.Linear;
+ var direction = new Vector3D(
+ linear.Get(0, axisIndex),
+ linear.Get(1, axisIndex),
+ linear.Get(2, axisIndex));
+
+ double lengthSquared = direction.X * direction.X + direction.Y * direction.Y + direction.Z * direction.Z;
+ if (lengthSquared < 1e-9)
+ {
+ return fallback;
+ }
+
+ double length = Math.Sqrt(lengthSquared);
+ return new Vector3D(direction.X / length, direction.Y / length, direction.Z / length);
+ }
+ catch (Exception)
+ {
+ return fallback;
+ }
+ }
+
+ private static double[] SolveLinearSystem3x3(double[,] matrix, double[] values)
+ {
+ double determinant =
+ matrix[0, 0] * (matrix[1, 1] * matrix[2, 2] - matrix[1, 2] * matrix[2, 1]) -
+ matrix[0, 1] * (matrix[1, 0] * matrix[2, 2] - matrix[1, 2] * matrix[2, 0]) +
+ matrix[0, 2] * (matrix[1, 0] * matrix[2, 1] - matrix[1, 1] * matrix[2, 0]);
+
+ if (Math.Abs(determinant) < 1e-9)
+ {
+ return new[] { values[0], values[1], values[2] };
+ }
+
+ double inverseDeterminant = 1.0 / determinant;
+ double[,] inverse =
+ {
+ {
+ (matrix[1, 1] * matrix[2, 2] - matrix[1, 2] * matrix[2, 1]) * inverseDeterminant,
+ (matrix[0, 2] * matrix[2, 1] - matrix[0, 1] * matrix[2, 2]) * inverseDeterminant,
+ (matrix[0, 1] * matrix[1, 2] - matrix[0, 2] * matrix[1, 1]) * inverseDeterminant
+ },
+ {
+ (matrix[1, 2] * matrix[2, 0] - matrix[1, 0] * matrix[2, 2]) * inverseDeterminant,
+ (matrix[0, 0] * matrix[2, 2] - matrix[0, 2] * matrix[2, 0]) * inverseDeterminant,
+ (matrix[0, 2] * matrix[1, 0] - matrix[0, 0] * matrix[1, 2]) * inverseDeterminant
+ },
+ {
+ (matrix[1, 0] * matrix[2, 1] - matrix[1, 1] * matrix[2, 0]) * inverseDeterminant,
+ (matrix[0, 1] * matrix[2, 0] - matrix[0, 0] * matrix[2, 1]) * inverseDeterminant,
+ (matrix[0, 0] * matrix[1, 1] - matrix[0, 1] * matrix[1, 0]) * inverseDeterminant
+ }
+ };
+
+ return new[]
+ {
+ inverse[0, 0] * values[0] + inverse[0, 1] * values[1] + inverse[0, 2] * values[2],
+ inverse[1, 0] * values[0] + inverse[1, 1] * values[1] + inverse[1, 2] * values[2],
+ inverse[2, 0] * values[0] + inverse[2, 1] * values[1] + inverse[2, 2] * values[2]
+ };
+ }
+
///
/// 恢复物体到原位置
///
@@ -165,6 +301,76 @@ namespace NavisworksTransport.Utils
doc.Models.OverridePermanentTransform(modelItems, transform, false);
}
+ ///
+ /// 将物体移动到指定位置和完整三维朝向(先回到CAD原始位置,再移动)
+ ///
+ public static void MoveItemToPositionAndRotation(ModelItem item, Point3D targetPosition, Rotation3D targetRotation)
+ {
+ ApplyAbsoluteTransform(item, targetPosition, targetRotation, preserveCurrentScale: false);
+ }
+
+ ///
+ /// 将物体移动到指定位置和完整三维朝向,同时保留当前缩放。
+ ///
+ public static void MoveItemToPositionAndRotationWithCurrentScale(ModelItem item, Point3D targetPosition, Rotation3D targetRotation)
+ {
+ ApplyAbsoluteTransform(item, targetPosition, targetRotation, preserveCurrentScale: true);
+ }
+
+ private static void ApplyAbsoluteTransform(ModelItem item, Point3D targetPosition, Rotation3D targetRotation, bool preserveCurrentScale)
+ {
+ var doc = Application.ActiveDocument;
+ var modelItems = new ModelItemCollection { item };
+ Vector3D currentScale = new Vector3D(1, 1, 1);
+
+ if (preserveCurrentScale)
+ {
+ var currentComponents = item.Transform.Factor();
+ currentScale = currentComponents.Scale;
+ }
+
+ doc.Models.ResetPermanentTransform(modelItems);
+
+ var originalBounds = item.BoundingBox();
+ var originalGroundPos = new Point3D(
+ originalBounds.Center.X,
+ originalBounds.Center.Y,
+ originalBounds.Min.Z
+ );
+
+ var rotationTransform = new Transform3D(targetRotation);
+ var linear = rotationTransform.Linear;
+
+ var rotatedGroundPos = new Point3D(
+ linear.Get(0, 0) * originalGroundPos.X + linear.Get(0, 1) * originalGroundPos.Y + linear.Get(0, 2) * originalGroundPos.Z,
+ linear.Get(1, 0) * originalGroundPos.X + linear.Get(1, 1) * originalGroundPos.Y + linear.Get(1, 2) * originalGroundPos.Z,
+ linear.Get(2, 0) * originalGroundPos.X + linear.Get(2, 1) * originalGroundPos.Y + linear.Get(2, 2) * originalGroundPos.Z
+ );
+
+ var translation = new Vector3D(
+ targetPosition.X - rotatedGroundPos.X,
+ targetPosition.Y - rotatedGroundPos.Y,
+ targetPosition.Z - rotatedGroundPos.Z
+ );
+
+ Transform3D transform;
+ if (preserveCurrentScale)
+ {
+ var identity = Transform3D.CreateTranslation(new Vector3D(0, 0, 0));
+ var components = identity.Factor();
+ components.Scale = currentScale;
+ components.Rotation = targetRotation;
+ components.Translation = translation;
+ transform = components.Combine();
+ }
+ else
+ {
+ transform = new Transform3D(targetRotation, translation);
+ }
+
+ doc.Models.OverridePermanentTransform(modelItems, transform, false);
+ }
+
///
/// 将物体移动到指定位置和朝向,同时保持缩放比例
/// 专为虚拟物体设计,避免缩放被覆盖
diff --git a/src/Utils/RailPathPoseHelper.cs b/src/Utils/RailPathPoseHelper.cs
new file mode 100644
index 0000000..24e5c3d
--- /dev/null
+++ b/src/Utils/RailPathPoseHelper.cs
@@ -0,0 +1,279 @@
+using System;
+using Autodesk.Navisworks.Api;
+
+namespace NavisworksTransport.Utils
+{
+ ///
+ /// Rail 路径姿态辅助工具。
+ /// LegacySuspensionPoint 模式下保持世界 Z 偏移;
+ /// RailCenterLine 模式下按路径切线推导局部法向,支持斜轨的轨上/轨下偏移。
+ ///
+ public static class RailPathPoseHelper
+ {
+ private const double TangentEpsilon = 1e-9;
+
+ ///
+ /// 获取 Rail 参考点到构件对接点的有符号偏移(模型单位)。
+ /// LegacySuspensionPoint 模式下,路径点本身就是构件对接点,因此偏移为 0。
+ ///
+ public static double GetAnchorOffset(PathRoute route)
+ {
+ if (route == null || route.PathType != PathType.Rail)
+ {
+ return 0.0;
+ }
+
+ if (route.RailPathDefinitionMode == RailPathDefinitionMode.LegacySuspensionPoint)
+ {
+ return 0.0;
+ }
+
+ return route.RailMountMode == RailMountMode.OverRail
+ ? route.RailReferenceToAnchorOffset
+ : -route.RailReferenceToAnchorOffset;
+ }
+
+ ///
+ /// 计算构件底面中心相对于 Rail 参考点的 Z 偏移(模型单位)。
+ ///
+ public static double GetBottomZOffset(PathRoute route, double objectHeight)
+ {
+ double anchorOffset = GetAnchorOffset(route);
+
+ if (route == null || route.PathType != PathType.Rail)
+ {
+ return 0.0;
+ }
+
+ if (route.RailPayloadAnchorMode == RailPayloadAnchorMode.TopCenter)
+ {
+ return anchorOffset - objectHeight;
+ }
+
+ return anchorOffset;
+ }
+
+ ///
+ /// 计算通行空间中心相对于 Rail 参考点的 Z 偏移(模型单位)。
+ /// 对于当前渲染实现,top-center 对接对应正半高,bottom-center 对接对应负半高。
+ ///
+ public static double GetObjectSpaceCenterZOffset(PathRoute route, double objectSpaceHeight)
+ {
+ double anchorOffset = GetAnchorOffset(route);
+
+ if (route == null || route.PathType != PathType.Rail)
+ {
+ return 0.0;
+ }
+
+ if (route.RailPayloadAnchorMode == RailPayloadAnchorMode.TopCenter)
+ {
+ return anchorOffset + objectSpaceHeight / 2.0;
+ }
+
+ return anchorOffset - objectSpaceHeight / 2.0;
+ }
+
+ ///
+ /// 根据 Rail 路径参考点计算构件底面中心位置。
+ ///
+ public static Point3D ResolveBottomPosition(PathRoute route, Point3D referencePoint, double objectHeight)
+ {
+ double bottomOffset = GetBottomZOffset(route, objectHeight);
+ return new Point3D(
+ referencePoint.X,
+ referencePoint.Y,
+ referencePoint.Z + bottomOffset);
+ }
+
+ ///
+ /// 根据 Rail 路径参考点和相邻点计算构件底面中心位置。
+ /// RailCenterLine 模式下沿轨道局部法向偏移,适用于斜轨。
+ ///
+ public static Point3D ResolveBottomPosition(
+ PathRoute route,
+ Point3D referencePoint,
+ Point3D previousPoint,
+ Point3D nextPoint,
+ double objectHeight)
+ {
+ if (route == null || route.PathType != PathType.Rail)
+ {
+ return referencePoint;
+ }
+
+ if (route.RailPathDefinitionMode == RailPathDefinitionMode.LegacySuspensionPoint)
+ {
+ return ResolveBottomPosition(route, referencePoint, objectHeight);
+ }
+
+ var normal = ResolveRailNormal(previousPoint, referencePoint, nextPoint);
+ double bottomOffset = GetBottomOffsetMagnitude(route, objectHeight);
+
+ return new Point3D(
+ referencePoint.X + normal.X * bottomOffset,
+ referencePoint.Y + normal.Y * bottomOffset,
+ referencePoint.Z + normal.Z * bottomOffset);
+ }
+
+ ///
+ /// 根据 Rail 路径参考点和相邻点计算通行空间中心位置。
+ ///
+ public static Point3D ResolveObjectSpaceCenterPosition(
+ PathRoute route,
+ Point3D referencePoint,
+ Point3D previousPoint,
+ Point3D nextPoint,
+ double objectSpaceHeight)
+ {
+ if (route == null || route.PathType != PathType.Rail)
+ {
+ return referencePoint;
+ }
+
+ double centerOffset = GetObjectSpaceCenterOffsetMagnitude(route, objectSpaceHeight);
+
+ if (route.RailPathDefinitionMode == RailPathDefinitionMode.LegacySuspensionPoint)
+ {
+ return new Point3D(
+ referencePoint.X,
+ referencePoint.Y,
+ referencePoint.Z + centerOffset);
+ }
+
+ var normal = ResolveRailNormal(previousPoint, referencePoint, nextPoint);
+ return new Point3D(
+ referencePoint.X + normal.X * centerOffset,
+ referencePoint.Y + normal.Y * centerOffset,
+ referencePoint.Z + normal.Z * centerOffset);
+ }
+
+ ///
+ /// 根据轨道路径局部坐标系创建 Rail 构件姿态。
+ /// 约定:局部 X 轴沿轨道切向,局部 Z 轴沿轨道法向,局部 Y 轴为横向。
+ ///
+ public static bool TryCreateRailRotation(
+ Point3D previousPoint,
+ Point3D currentPoint,
+ Point3D nextPoint,
+ out Rotation3D rotation)
+ {
+ rotation = Rotation3D.Identity;
+
+ var tangent = ResolveTangent(previousPoint, currentPoint, nextPoint);
+ double tangentLengthSquared = tangent.X * tangent.X + tangent.Y * tangent.Y + tangent.Z * tangent.Z;
+ if (tangentLengthSquared < TangentEpsilon)
+ {
+ return false;
+ }
+
+ var normal = ResolveRailNormal(previousPoint, currentPoint, nextPoint);
+ var lateral = Cross(normal, tangent);
+ double lateralLengthSquared = lateral.X * lateral.X + lateral.Y * lateral.Y + lateral.Z * lateral.Z;
+ if (lateralLengthSquared < TangentEpsilon)
+ {
+ return false;
+ }
+
+ tangent = Normalize(tangent);
+ normal = Normalize(normal);
+ lateral = Normalize(lateral);
+
+ rotation = new Rotation3D(
+ new UnitVector3D(tangent.X, tangent.Y, tangent.Z),
+ new UnitVector3D(lateral.X, lateral.Y, lateral.Z),
+ new UnitVector3D(normal.X, normal.Y, normal.Z));
+ return true;
+ }
+
+ private static double GetBottomOffsetMagnitude(PathRoute route, double objectHeight)
+ {
+ double anchorOffset = GetAnchorOffset(route);
+ return route.RailPayloadAnchorMode == RailPayloadAnchorMode.TopCenter
+ ? anchorOffset - objectHeight
+ : anchorOffset;
+ }
+
+ private static double GetObjectSpaceCenterOffsetMagnitude(PathRoute route, double objectSpaceHeight)
+ {
+ double anchorOffset = GetAnchorOffset(route);
+ return route.RailPayloadAnchorMode == RailPayloadAnchorMode.TopCenter
+ ? anchorOffset + objectSpaceHeight / 2.0
+ : anchorOffset - objectSpaceHeight / 2.0;
+ }
+
+ private static Vector3D ResolveRailNormal(Point3D previousPoint, Point3D currentPoint, Point3D nextPoint)
+ {
+ var tangent = ResolveTangent(previousPoint, currentPoint, nextPoint);
+ double tangentLengthSquared = tangent.X * tangent.X + tangent.Y * tangent.Y + tangent.Z * tangent.Z;
+
+ if (tangentLengthSquared < TangentEpsilon)
+ {
+ return new Vector3D(0, 0, 1);
+ }
+
+ tangent = new Vector3D(
+ tangent.X / Math.Sqrt(tangentLengthSquared),
+ tangent.Y / Math.Sqrt(tangentLengthSquared),
+ tangent.Z / Math.Sqrt(tangentLengthSquared));
+
+ var worldUp = new Vector3D(0, 0, 1);
+ double projection = worldUp.X * tangent.X + worldUp.Y * tangent.Y + worldUp.Z * tangent.Z;
+ var normal = new Vector3D(
+ worldUp.X - projection * tangent.X,
+ worldUp.Y - projection * tangent.Y,
+ worldUp.Z - projection * tangent.Z);
+
+ double normalLengthSquared = normal.X * normal.X + normal.Y * normal.Y + normal.Z * normal.Z;
+ if (normalLengthSquared < TangentEpsilon)
+ {
+ return new Vector3D(0, 0, 1);
+ }
+
+ double normalLength = Math.Sqrt(normalLengthSquared);
+ return new Vector3D(
+ normal.X / normalLength,
+ normal.Y / normalLength,
+ normal.Z / normalLength);
+ }
+
+ private static Vector3D ResolveTangent(Point3D previousPoint, Point3D currentPoint, Point3D nextPoint)
+ {
+ var tangent = new Vector3D(
+ nextPoint.X - previousPoint.X,
+ nextPoint.Y - previousPoint.Y,
+ nextPoint.Z - previousPoint.Z);
+
+ double tangentLengthSquared = tangent.X * tangent.X + tangent.Y * tangent.Y + tangent.Z * tangent.Z;
+ if (tangentLengthSquared < TangentEpsilon)
+ {
+ tangent = new Vector3D(
+ nextPoint.X - currentPoint.X,
+ nextPoint.Y - currentPoint.Y,
+ nextPoint.Z - currentPoint.Z);
+ }
+
+ return tangent;
+ }
+
+ private static Vector3D Normalize(Vector3D vector)
+ {
+ double lengthSquared = vector.X * vector.X + vector.Y * vector.Y + vector.Z * vector.Z;
+ if (lengthSquared < TangentEpsilon)
+ {
+ return new Vector3D(0, 0, 0);
+ }
+
+ double length = Math.Sqrt(lengthSquared);
+ return new Vector3D(vector.X / length, vector.Y / length, vector.Z / length);
+ }
+
+ private static Vector3D Cross(Vector3D a, Vector3D b)
+ {
+ return new Vector3D(
+ a.Y * b.Z - a.Z * b.Y,
+ a.Z * b.X - a.X * b.Z,
+ a.X * b.Y - a.Y * b.X);
+ }
+ }
+}
From a55ef42f71fd7911ec623af09a200d5ee59d0bdc Mon Sep 17 00:00:00 2001
From: tian <11429339@qq.com>
Date: Thu, 19 Mar 2026 22:00:04 +0800
Subject: [PATCH 02/87] Add linear assembly reference path and rod alignment
---
TransportPlugin.csproj | 17 +-
deploy-plugin.bat | 15 +-
doc/design/2026/NavisworksAPI使用方法.md | 125 +++++++++
doc/requirement/todo_features.md | 4 +
resources/unit_cylinder.nwc | Bin 0 -> 1980 bytes
src/Core/AssemblyReferencePathManager.cs | 155 ++++--------
src/Core/PathCurveEngine.cs | 4 +-
src/Core/PathPointRenderPlugin.cs | 18 +-
src/UI/WPF/ViewModels/PathEditingViewModel.cs | 237 ++++++++++++++++--
src/Utils/ModelItemTransformHelper.cs | 206 +++++++++++++++
10 files changed, 628 insertions(+), 153 deletions(-)
create mode 100644 resources/unit_cylinder.nwc
diff --git a/TransportPlugin.csproj b/TransportPlugin.csproj
index 919214e..79d391a 100644
--- a/TransportPlugin.csproj
+++ b/TransportPlugin.csproj
@@ -470,13 +470,6 @@
-
-
-
- PreserveNewest
- TransportPlugin.name.txt
-
-
@@ -486,6 +479,11 @@
PreserveNewest
resources\default_config.toml
+
+
+ PreserveNewest
+ SQLite.Interop.dll
+
PreserveNewest
@@ -496,6 +494,11 @@
PreserveNewest
resources\unit_cube.nwc
+
+
+ PreserveNewest
+ resources\unit_cylinder.nwc
+
diff --git a/deploy-plugin.bat b/deploy-plugin.bat
index 55e87ff..1cce12f 100644
--- a/deploy-plugin.bat
+++ b/deploy-plugin.bat
@@ -1,6 +1,7 @@
@echo off
+setlocal
-:: 如果 Navisworks 正在运行,关闭它以释放 DLL 锁定
+:: Stop Navisworks to release locked plugin DLLs.
taskkill /F /IM Roamer.exe 2>nul
if %errorlevel% == 0 (
echo Navisworks process terminated.
@@ -8,16 +9,18 @@ if %errorlevel% == 0 (
)
set "TARGET_DIR=%PROGRAMDATA%\Autodesk\Navisworks Manage 2026\plugins\TransportPlugin"
+set "BUILD_DIR=bin\x64\Release"
if not exist "%TARGET_DIR%" mkdir "%TARGET_DIR%"
-copy "bin\x64\Release\TransportPlugin.dll" "%TARGET_DIR%\" >nul
+copy "%BUILD_DIR%\*.dll" "%TARGET_DIR%\" >nul
+echo Plugin DLLs deployed.
-:: 复制resources文件夹(包含chart.js等本地资源)
-if exist "bin\x64\Release\resources" (
+:: Copy bundled resources.
+if exist "%BUILD_DIR%\resources" (
if not exist "%TARGET_DIR%\resources" mkdir "%TARGET_DIR%\resources"
- copy "bin\x64\Release\resources\*" "%TARGET_DIR%\resources\" >nul
+ xcopy "%BUILD_DIR%\resources\*" "%TARGET_DIR%\resources\" /E /I /Y /Q >nul
echo Resources folder deployed.
)
-echo Plugin deployed successfully!
\ No newline at end of file
+echo Plugin deployed successfully!
diff --git a/doc/design/2026/NavisworksAPI使用方法.md b/doc/design/2026/NavisworksAPI使用方法.md
index 7caa4f3..a9d16fd 100644
--- a/doc/design/2026/NavisworksAPI使用方法.md
+++ b/doc/design/2026/NavisworksAPI使用方法.md
@@ -703,6 +703,131 @@ var transform = components.Combine();
// 结果:物体"公转"到错误位置
```
+#### 11.7.2.1 Rotation3D 构造函数的真实语义(2026-03-19 新增,重要)
+
+这次在“直线装配参考杆”功能中,验证了一个很容易误用的点:
+
+- `Rotation3D(UnitVector3D, UnitVector3D, UnitVector3D)` **不是**“传入本地 X/Y/Z 三个轴来直接构造三维姿态”
+- 之前如果把它当成“三轴姿态构造器”使用,会得到完全错误的旋转结果
+
+根据 Navisworks .NET API HTML 文档:
+
+- `Rotation3D(UnitVector3D vector1, UnitVector3D vector2)`
+ - 文档摘要:`Creates rotation that rotates vector1 to same direction as vector2`
+ - 正确语义:**将向量1旋转到与向量2同方向**
+- `Rotation3D(UnitVector3D axis, double angle)`
+ - 文档摘要:`Creates rotation about given axis by angle in radians`
+ - 正确语义:**绕指定轴按弧度旋转**
+- `Rotation3D(UnitVector3D axis, UnitVector3D vector1, UnitVector3D vector2)`
+ - 文档摘要:`Creates rotation about axis by angle between v1 and v2 projected onto plane normal to axis.`
+ - 正确语义:**绕 axis 旋转,旋转角由 vector1 和 vector2 在垂直于 axis 的平面上的投影夹角决定**
+
+**结论:**
+
+- 3 参数版本是“轴 + 两个参考向量”的构造,不是“三个基向量/三个坐标轴”
+- 如果需求是“让物体本地某个轴对齐任意空间方向”,优先使用 **2 参数版本**
+
+#### 11.7.2.2 三维参考杆/任意向量对齐的正确用法
+
+适用场景:
+
+- 参考杆、箭头、圆柱体等“有明确本地长度轴”的资源
+- 需要将资源的本地某个轴,对齐到一个任意空间向量
+
+例如:
+
+- 资源中心在原点
+- 长轴沿本地 `+X`
+- 目标方向是参考线方向 `referenceDirection`
+
+那么旋转应直接写成:
+
+```csharp
+var rotation = new Rotation3D(
+ new UnitVector3D(1, 0, 0),
+ new UnitVector3D(referenceDirection));
+```
+
+其含义非常直接:
+
+- 把资源的本地 `+X` 轴
+- 旋转到 `referenceDirection`
+
+这正是“直线装配参考杆”最终验证通过的做法。
+
+**不要写成下面这样:**
+
+```csharp
+// ❌ 错误:把三个向量误当成 X/Y/Z 三个姿态轴
+var rotation = new Rotation3D(
+ new UnitVector3D(tangent),
+ new UnitVector3D(lateral),
+ new UnitVector3D(normal));
+```
+
+这段代码的真实语义并不是“建立一个由 tangent/lateral/normal 组成的姿态”,
+而是“绕 tangent 这个轴,根据 lateral 和 normal 的投影夹角去旋转”,
+所以结果通常会完全错误。
+
+#### 11.7.2.3 三维旋转与平移补偿必须分开理解
+
+即使 `Rotation3D(vector1, vector2)` 构造正确,仍然不能忽略 Navisworks 的旋转中心限制:
+
+- `OverridePermanentTransform()` 仍然是**增量变换**
+- 旋转仍然是**绕世界原点**
+- 因此三维姿态正确后,位置仍需要像 yaw 场景一样做补偿
+
+对于“资源中心在原点、目标中心为 `targetCenter`”的对象,推荐模式是:
+
+```csharp
+// 1. 从资源原始状态出发
+doc.Models.ResetPermanentTransform(modelItems);
+
+// 2. 计算旋转:本地 X -> 目标方向
+var rotation = new Rotation3D(
+ new UnitVector3D(1, 0, 0),
+ new UnitVector3D(referenceDirection));
+
+// 3. 计算原始中心绕世界原点旋转后的位置
+var originalCenter = item.BoundingBox().Center;
+var rotationTransform = new Transform3D(rotation);
+var linear = rotationTransform.Linear;
+var rotatedCenter = new Point3D(
+ linear.Get(0, 0) * originalCenter.X + linear.Get(0, 1) * originalCenter.Y + linear.Get(0, 2) * originalCenter.Z,
+ linear.Get(1, 0) * originalCenter.X + linear.Get(1, 1) * originalCenter.Y + linear.Get(1, 2) * originalCenter.Z,
+ linear.Get(2, 0) * originalCenter.X + linear.Get(2, 1) * originalCenter.Y + linear.Get(2, 2) * originalCenter.Z);
+
+// 4. 用补偿平移把旋转后的中心送到目标中心
+var translation = new Vector3D(
+ targetCenter.X - rotatedCenter.X,
+ targetCenter.Y - rotatedCenter.Y,
+ targetCenter.Z - rotatedCenter.Z);
+
+// 5. 组合并应用增量变换
+var transform = new Transform3D(rotation, translation);
+doc.Models.OverridePermanentTransform(modelItems, transform, false);
+```
+
+**关键原则:**
+
+- 先把“旋转方向”算对
+- 再按“绕原点旋转后的实际位置”计算平移补偿
+- 不要把“姿态构造错误”和“旋转中心补偿缺失”混成一个问题
+
+#### 11.7.2.4 这类问题的调试建议
+
+当三维物体“位置大概对,但方向明显错”时,优先检查:
+
+1. 是否误用了 `Rotation3D(UnitVector3D, UnitVector3D, UnitVector3D)`
+2. 资源文件的本地长度轴是否真的与你的代码约定一致
+3. 是否把 `ModelItem.Transform` 错当成 override 后的真实世界姿态
+
+推荐观察项:
+
+- 参考线/目标向量本身是否正确
+- 资源本地长度轴约定(例如是否沿本地 `+X`)
+- 当前实际显示位置,优先看 `BoundingBox()`,不要只看 `ModelItem.Transform`
+
#### 11.7.3 正确实现"绕物体中心旋转"
**解决方案:手动计算旋转导致的位置偏移并补偿**
diff --git a/doc/requirement/todo_features.md b/doc/requirement/todo_features.md
index e7a8cce..17d59b3 100644
--- a/doc/requirement/todo_features.md
+++ b/doc/requirement/todo_features.md
@@ -2,6 +2,10 @@
## 功能点
+### [2026/3/18]
+
+1. [x](功能)增加轨上路径(角度可倾斜)
+
### [2026/3/11]
1. [x] (功能)给“导出剖面盒”增加导出nwd文件的选项
diff --git a/resources/unit_cylinder.nwc b/resources/unit_cylinder.nwc
new file mode 100644
index 0000000000000000000000000000000000000000..540b4e6bcbcc8d9b3037500d27ab9fdabeaf179e
GIT binary patch
literal 1980
zcma)7dpOi-8~%-HoW^0O)>*%hoCY&g);f&D$7GP96pcAFn2bY%F_W`uo8&MU5wp~W
zl0KxBgw`pU%w(KOtIQ-RvY&QmS(5Cu{qyaA`~LO5&wE|Zb3gZeU(aLeM+w0vF{q?i
z;f%C<@`V58MJ%
zPYULPm$*0X;tXK9xQE!MQX?+u>D|bu2U5@Q+7)(Vc;bW#Fb=YHZ#{Q)^bYd<@TbgB
zwF>K!mMVD5_;&5qP)}^ouv9Q)^82-5HIJa#IH8pGV!+VZ)qq!Vh#Yyn$uKSIcz~PZ
zp!u}hyXp7|(W4H@>|hT!rOxjq{Z*aTI2*S%eZJUmPoFUQQS`k4o=tQroZPChXYcNf
zDal9XFZd^=I6Jh;nk@s~b9JXW+E&{`+ooWA%%be^?G^&ky{ut*XzJN}*?fGPbva!W
zQr+BXp(-0Dc)t^y6Qgfamgr>3V@Zbl1;6NC0NFlSNumq3MhH5Iq8e
z{^yk=_o^n$2CfA%n{Fim00dz150Z#P4s<3MybB@Qfh$mQ-YYxUGV?1NrR^?biTiJcTHQwWJqsY=vrH#G((P
zWinkvmKJ9$E7X{?357rvSBU}oI5(on{(Lj&pk2lrf!o>zOoY-buP$ot=o_=QHJDGC
zhW2K`}VJU9oN
z8SNAnug*T^3p;t^CZM}I2bWc@&Z&;0vz~Y6WP2zNKJoRp@jdut*xFWvL*{Dw*jSM;
z5HN8pJ5-GW8xc+X-5C|Uypgul9|d0_I!x_rb%Ku30mpfB(O1Ic1qq5yJ_(06W8;An
z%>b%I0pYnTD0|Y7qnGWZ&JlJ0sJ=3ZVm8m+a2{0tJ_#&{UZt3lSKpq8!ZmE+wSnXouAknW(ALQpL7{6fT#cfGv5-=;=iVLYm8^6x=*8G
zestBKS*#K@uQll2-~1GmMk};xoWbxj{MO6G!RV`;^zH;51AUxfx1*A(KKIS@db+Vg
zsmN2)8Y`k)ZDHRE-m8gsb;jLtty$=ER05v1A}v|`b-Bwk6~WLn+q-oi2%(s;mCP|o
zt$QhkoQGM|=+D7~SCk#qt`5Ika=EPdSWVfHDj@(NSVaybe*5-pTkto+jAnL~kx$-s
zPRZ61haf$K)Lk3%>`bF^WtbQO^tU@d`-e|<74)d~+9M9EBU9sGP3@}2uF4t)rFO
zbLP$I#g|MnIGHiIWh6yo;rd>jOtI^L0&JtGN`FVym2+@kMz%+6nA58R%q168fpr^x
zfRBgejm&QN3=s2@jS?6%jo-1yxOlbV?>ydipte2PgABo1oHOY=2JcAQ@y*g?F`Z?+
zAd_H@$5$_`K&iD(Ri^)Hz9%O|I)QN{CM_`~o)MGi?C9tupZKTgG_tDP8ea~Je{OUb
z+h?x}d=QH8r8_^9l)6+-{~_Hv*>0Ja99nARqB|8I(;R!U00}i*~L{gj#j)VC)Do`q+86Kg|1q>sbB`t{>W1
zXN@q@rPo+o`1wkaRA3Tu{b+xypvAu-&JF5DHCDINRkrH9vM5vD7`u5oHOJqSLp^pc
zC23#7=Zlqrf{eEt2Y#lk{+hcKB~00+Gqf&_o)}K~dB!mFf2R?BWIrbNVPUNVW4F1?
zG(Wqre5q&AWy?$Ee1uqHK=?EJa;nYQAy%(7+g-f&{nY;Jjj{cdBij)p+oEcYq;x%y
r;F4Zjmxqs?M~Izt
/// 直线装配参考路径管理器。
- /// 优先使用单位圆柱体资源生成一根可点击的临时参考杆,供用户在 3D 视图中选择起点。
- /// 如果圆柱资源缺失,则退回到单位立方体资源。
+ /// 使用单位立方体资源生成一根可点击的临时参考杆,供用户在 3D 视图中选择起点。
+ /// 参考杆本质上表达的是一段 start->end 参考轴线,而不是终点处的独立构件。
///
public class AssemblyReferencePathManager
{
@@ -27,7 +27,6 @@ namespace NavisworksTransport.Core
private Point3D _referenceLineEnd = Point3D.Origin;
private double _referenceRodDiameterInMeters;
private string _referenceResourceName;
-
public static AssemblyReferencePathManager Instance
{
get
@@ -50,7 +49,10 @@ namespace NavisworksTransport.Core
public ModelItem CurrentReferenceRod => _referenceRodModelItem;
public bool IsReferenceRodVisible => _isReferenceRodVisible;
public bool IsUpdatingReferenceRod => _isUpdatingReferenceRod;
- public bool HasReferenceLine => _referenceRodModelItem != null && _isReferenceRodVisible;
+ public bool HasReferenceLine => VectorLengthSquared(new Vector3D(
+ _referenceLineEnd.X - _referenceLineStart.X,
+ _referenceLineEnd.Y - _referenceLineStart.Y,
+ _referenceLineEnd.Z - _referenceLineStart.Z)) > 1e-9;
public Point3D ReferenceLineStart => _referenceLineStart;
public Point3D ReferenceLineEnd => _referenceLineEnd;
public string ReferenceResourceName => _referenceResourceName;
@@ -62,6 +64,7 @@ namespace NavisworksTransport.Core
///
/// 创建或更新参考杆。
+ /// 先记录参考线主数据,再基于参考线更新可点击实体外壳。
///
public ModelItem CreateOrUpdateReferenceRod(Point3D startPoint, Point3D endPoint, double diameterInMeters)
{
@@ -80,19 +83,19 @@ namespace NavisworksTransport.Core
_isUpdatingReferenceRod = true;
try
{
- EnsureReferenceRodModelLoaded();
-
- double lengthInMeters = UnitsConverter.ConvertToMeters(referenceLength);
- ScaleReferenceRod(lengthInMeters, diameterInMeters);
- PositionReferenceRod(startPoint, endPoint);
-
- ShowReferenceRod();
-
_referenceLineStart = startPoint;
_referenceLineEnd = endPoint;
_referenceRodDiameterInMeters = diameterInMeters;
- LogManager.Info($"[装配参考杆] 已创建/更新,长度={lengthInMeters:F3}m,直径={diameterInMeters:F3}m");
+ EnsureReferenceRodModelLoaded();
+
+ ScaleReferenceRod(referenceLength, diameterInMeters);
+ PositionReferenceRod();
+
+ ShowReferenceRod();
+
+ double referenceLengthInMeters = UnitsConverter.ConvertToMeters(referenceLength);
+ LogManager.Info($"[装配参考杆] 已创建/更新,长度={referenceLengthInMeters:F3}m,直径={diameterInMeters:F3}m");
return _referenceRodModelItem;
}
finally
@@ -108,7 +111,7 @@ namespace NavisworksTransport.Core
return;
}
- var items = new ModelItemCollection { _referenceRodModelItem };
+ var items = new ModelItemCollection { _referenceRodModel.RootItem };
Application.ActiveDocument.Models.SetHidden(items, false);
_isReferenceRodVisible = true;
}
@@ -120,7 +123,7 @@ namespace NavisworksTransport.Core
return;
}
- var items = new ModelItemCollection { _referenceRodModelItem };
+ var items = new ModelItemCollection { _referenceRodModel.RootItem };
Application.ActiveDocument.Models.SetHidden(items, true);
_isReferenceRodVisible = false;
}
@@ -199,19 +202,22 @@ namespace NavisworksTransport.Core
}
_referenceRodModel = doc.Models.Last();
- _referenceRodModelItem = FindFirstGeometryItem(_referenceRodModel.RootItem);
+ _referenceRodModelItem = _referenceRodModel.RootItem;
_isReferenceRodVisible = true;
_referenceResourceName = resourceName;
LogManager.Info($"[装配参考杆] 已加载资源: {resourceName}");
}
- private void ScaleReferenceRod(double lengthInMeters, double diameterInMeters)
+ private void ScaleReferenceRod(double length, double diameterInMeters)
{
if (_referenceRodModel == null)
{
return;
}
+ var doc = Application.ActiveDocument;
+ double lengthInMeters = UnitsConverter.ConvertToMeters(length);
+
var currentTransform = _referenceRodModel.Transform;
var components = currentTransform.Factor();
components.Scale = new Vector3D(
@@ -220,54 +226,36 @@ namespace NavisworksTransport.Core
diameterInMeters / ReferenceRodBaseSizeInMeters);
Transform3D scaledTransform = components.Combine();
- Application.ActiveDocument.Models.SetModelUnitsAndTransform(
+ doc.Models.SetModelUnitsAndTransform(
_referenceRodModel,
_referenceRodModel.Units,
scaledTransform,
false);
+
+ LogManager.Info(
+ $"[装配参考杆] 缩放完成,目标长度={lengthInMeters:F3}m,目标直径={diameterInMeters:F3}m,模型单位={_referenceRodModel.Units}");
}
- private void PositionReferenceRod(Point3D startPoint, Point3D endPoint)
+ private void PositionReferenceRod()
{
- if (_referenceRodModelItem == null)
+ if (_referenceRodModelItem == null || _referenceRodModel == null)
{
return;
}
- var doc = Application.ActiveDocument;
- var modelItems = new ModelItemCollection { _referenceRodModelItem };
- var currentScale = _referenceRodModelItem.Transform.Factor().Scale;
+ Point3D targetCenter = new Point3D(
+ (_referenceLineStart.X + _referenceLineEnd.X) / 2.0,
+ (_referenceLineStart.Y + _referenceLineEnd.Y) / 2.0,
+ (_referenceLineStart.Z + _referenceLineEnd.Z) / 2.0);
- doc.Models.ResetPermanentTransform(modelItems);
+ Rotation3D rotation = CreateLineRotation(_referenceLineStart, _referenceLineEnd);
+ ModelItemTransformHelper.MoveItemToCenterAndRotation(
+ _referenceRodModelItem,
+ targetCenter,
+ rotation);
- var originalBounds = _referenceRodModelItem.BoundingBox();
- var originalCenter = originalBounds.Center;
- var targetCenter = new Point3D(
- (startPoint.X + endPoint.X) / 2.0,
- (startPoint.Y + endPoint.Y) / 2.0,
- (startPoint.Z + endPoint.Z) / 2.0);
-
- Rotation3D rotation = CreateLineRotation(startPoint, endPoint);
-
- var identity = Transform3D.CreateTranslation(new Vector3D(0, 0, 0));
- var components = identity.Factor();
- components.Scale = currentScale;
- components.Rotation = rotation;
-
- var preTransform = components.Combine();
- var linear = preTransform.Linear;
- var transformedCenter = new Point3D(
- linear.Get(0, 0) * originalCenter.X + linear.Get(0, 1) * originalCenter.Y + linear.Get(0, 2) * originalCenter.Z,
- linear.Get(1, 0) * originalCenter.X + linear.Get(1, 1) * originalCenter.Y + linear.Get(1, 2) * originalCenter.Z,
- linear.Get(2, 0) * originalCenter.X + linear.Get(2, 1) * originalCenter.Y + linear.Get(2, 2) * originalCenter.Z);
-
- components.Translation = new Vector3D(
- targetCenter.X - transformedCenter.X,
- targetCenter.Y - transformedCenter.Y,
- targetCenter.Z - transformedCenter.Z);
-
- Transform3D transform = components.Combine();
- doc.Models.OverridePermanentTransform(modelItems, transform, false);
+ LogManager.Info(
+ $"[装配参考杆] 定位完成,中心=({targetCenter.X:F3}, {targetCenter.Y:F3}, {targetCenter.Z:F3}),起点=({_referenceLineStart.X:F3}, {_referenceLineStart.Y:F3}, {_referenceLineStart.Z:F3}),终点=({_referenceLineEnd.X:F3}, {_referenceLineEnd.Y:F3}, {_referenceLineEnd.Z:F3})");
}
private static Rotation3D CreateLineRotation(Point3D startPoint, Point3D endPoint)
@@ -277,28 +265,12 @@ namespace NavisworksTransport.Core
endPoint.Y - startPoint.Y,
endPoint.Z - startPoint.Z));
- var worldUp = new Vector3D(0, 0, 1);
- var projectedNormal = new Vector3D(
- worldUp.X - Dot(worldUp, tangent) * tangent.X,
- worldUp.Y - Dot(worldUp, tangent) * tangent.Y,
- worldUp.Z - Dot(worldUp, tangent) * tangent.Z);
-
- if (VectorLengthSquared(projectedNormal) < 1e-9)
- {
- var fallbackUp = new Vector3D(0, 1, 0);
- projectedNormal = new Vector3D(
- fallbackUp.X - Dot(fallbackUp, tangent) * tangent.X,
- fallbackUp.Y - Dot(fallbackUp, tangent) * tangent.Y,
- fallbackUp.Z - Dot(fallbackUp, tangent) * tangent.Z);
- }
-
- var normal = Normalize(projectedNormal);
- var lateral = Normalize(Cross(normal, tangent));
-
+ // 参考杆资源约定:几何中心在原点,长度轴沿局部 +X。
+ // Navisworks API 文档中 Rotation3D(vector1, vector2) 的语义
+ // 是“将 vector1 旋转到与 vector2 同方向”,这里直接将本地 X 轴对齐参考线方向。
return new Rotation3D(
- new UnitVector3D(tangent),
- new UnitVector3D(lateral),
- new UnitVector3D(normal));
+ new UnitVector3D(1, 0, 0),
+ new UnitVector3D(tangent));
}
private static Point3D ProjectPointToSegment(Point3D point, Point3D segmentStart, Point3D segmentEnd)
@@ -325,19 +297,6 @@ namespace NavisworksTransport.Core
segmentStart.Z + projection * segmentZ);
}
- private static Vector3D Cross(Vector3D a, Vector3D b)
- {
- return new Vector3D(
- a.Y * b.Z - a.Z * b.Y,
- a.Z * b.X - a.X * b.Z,
- a.X * b.Y - a.Y * b.X);
- }
-
- private static double Dot(Vector3D a, Vector3D b)
- {
- return a.X * b.X + a.Y * b.Y + a.Z * b.Z;
- }
-
private static Vector3D Normalize(Vector3D vector)
{
double lengthSquared = VectorLengthSquared(vector);
@@ -355,30 +314,6 @@ namespace NavisworksTransport.Core
return vector.X * vector.X + vector.Y * vector.Y + vector.Z * vector.Z;
}
- private static ModelItem FindFirstGeometryItem(ModelItem item)
- {
- if (item == null)
- {
- return null;
- }
-
- if (item.HasGeometry)
- {
- return item;
- }
-
- foreach (var child in item.Children)
- {
- var geometryItem = FindFirstGeometryItem(child);
- if (geometryItem != null)
- {
- return geometryItem;
- }
- }
-
- return item;
- }
-
private static string GetReferenceRodResourcePath(out string resourceName)
{
string pluginDir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
diff --git a/src/Core/PathCurveEngine.cs b/src/Core/PathCurveEngine.cs
index b895442..fac612c 100644
--- a/src/Core/PathCurveEngine.cs
+++ b/src/Core/PathCurveEngine.cs
@@ -439,7 +439,6 @@ namespace NavisworksTransport
var edge = BuildStraightEdge(sortedPoints[0], sortedPoints[1], samplingStep);
route.Edges.Add(edge);
route.IsCurved = true;
- RecalculateRouteLength(route);
return;
}
@@ -493,7 +492,6 @@ namespace NavisworksTransport
route.Edges.Add(lastEdge);
route.IsCurved = true;
- RecalculateRouteLength(route);
}
///
@@ -510,4 +508,4 @@ namespace NavisworksTransport
// 长度会自动从 Edges 或 Points 计算
}
}
-}
\ No newline at end of file
+}
diff --git a/src/Core/PathPointRenderPlugin.cs b/src/Core/PathPointRenderPlugin.cs
index d3fd3ab..9355a2a 100644
--- a/src/Core/PathPointRenderPlugin.cs
+++ b/src/Core/PathPointRenderPlugin.cs
@@ -127,6 +127,11 @@ namespace NavisworksTransport
///
RailBaseline,
+ ///
+ /// 直线装配基准向量样式(绿色)
+ ///
+ AssemblyGuideLine,
+
///
/// 吊装路径样式(紫色)
///
@@ -1126,6 +1131,14 @@ namespace NavisworksTransport
/// 空轨模型ID
/// 基准路径点列表
public void RenderRailBaseline(string railModelId, List baselinePoints)
+ {
+ RenderRailBaseline(railModelId, baselinePoints, RenderStyleName.RailBaseline);
+ }
+
+ ///
+ /// 按指定样式渲染基准路径。
+ ///
+ public void RenderRailBaseline(string railModelId, List baselinePoints, RenderStyleName renderStyleName)
{
if (string.IsNullOrEmpty(railModelId) || baselinePoints == null || baselinePoints.Count < 2)
{
@@ -1146,7 +1159,7 @@ namespace NavisworksTransport
LastUpdated = DateTime.Now
};
- var renderStyle = GetRenderStyle(RenderStyleName.RailBaseline);
+ var renderStyle = GetRenderStyle(renderStyleName);
// 创建连线标记
for (int i = 0; i < baselinePoints.Count - 1; i++)
@@ -2328,6 +2341,9 @@ namespace NavisworksTransport
case RenderStyleName.RailBaseline:
return new RenderStyle(Color.FromByteRGB(255, 138, 128), 0.7); // 浅红色,30%透明
+ case RenderStyleName.AssemblyGuideLine:
+ return new RenderStyle(Color.FromByteRGB(76, 175, 80), 0.85); // Material Green装配基准向量
+
case RenderStyleName.HoistingLine:
return new RenderStyle(Color.FromByteRGB(156, 39, 176), 0.8); // Material Purple吊装路径,20%透明
diff --git a/src/UI/WPF/ViewModels/PathEditingViewModel.cs b/src/UI/WPF/ViewModels/PathEditingViewModel.cs
index db79b0b..37eb74a 100644
--- a/src/UI/WPF/ViewModels/PathEditingViewModel.cs
+++ b/src/UI/WPF/ViewModels/PathEditingViewModel.cs
@@ -121,7 +121,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
private ObservableCollection> _railMountModeOptions;
private ObservableCollection> _railPayloadAnchorModeOptions;
private const double DefaultAssemblyReferenceRodLengthInMeters = 20.0;
- private const double DefaultAssemblyReferenceRodDiameterInMeters = 0.2;
+ private static readonly double DefaultAssemblyReferenceRodDiameterInMeters = CalculateDefaultAssemblyReferenceRodDiameterInMeters();
private string _assemblyTerminalObjectName = "未选择";
private string _assemblyTerminalObjectInfo = "请选择终点处已安装箱体";
private double _assemblyReferenceRodLengthInMeters = DefaultAssemblyReferenceRodLengthInMeters;
@@ -134,6 +134,8 @@ namespace NavisworksTransport.UI.WPF.ViewModels
private ModelItem _assemblyTerminalObject;
private Point3D _assemblyStartPoint;
private const string AssemblyAnchorMarkerPathId = "assembly_anchor_marker";
+ private const string AssemblyCenterGuideLinePathId = "assembly_center_guide_line";
+ private const string AssemblyReferenceLinePathId = "assembly_reference_line";
// 自动路径起点和终点的路径对象引用(用于正确的ID管理)
private PathRoute _autoPathStartPointRoute = null;
@@ -1320,27 +1322,29 @@ namespace NavisworksTransport.UI.WPF.ViewModels
throw new InvalidOperationException("终点箱体未设置或已失效,请重新捕获终点箱体");
}
- var forward = ModelItemTransformHelper.GetForwardDirectionFromTransform(_assemblyTerminalObject.Transform);
- var endPoint = GetAssemblyTerminalAnchorPoint();
- double rodLength = UnitsConverter.ConvertFromMeters(AssemblyReferenceRodLengthInMeters);
- var startPoint = new Point3D(
- endPoint.X - forward.X * rodLength,
- endPoint.Y - forward.Y * rodLength,
- endPoint.Z - forward.Z * rodLength);
+ AssemblyReferenceLine referenceLine = BuildAssemblyReferenceLine();
AssemblyReferencePathManager.Instance.CreateOrUpdateReferenceRod(
- startPoint,
- endPoint,
+ referenceLine.StartPoint,
+ referenceLine.EndPoint,
AssemblyReferenceRodDiameterInMeters);
_assemblyStartPoint = Point3D.Origin;
AssemblyStartPointText = "未选择";
- RefreshAssemblyTerminalObjectInfo(startPoint, endPoint);
+ RefreshAssemblyTerminalObjectInfo(referenceLine.StartPoint, referenceLine.EndPoint);
RenderAssemblyAnchorMarker();
+ RenderAssemblyCenterGuideLine();
+ RenderAssemblyReferenceLine(referenceLine);
+ FocusOnAssemblyReferenceArea(referenceLine.StartPoint, referenceLine.EndPoint);
- UpdateMainStatus("已生成直线装配参考杆,请在三维视图中确认位置");
+ UpdateMainStatus("已生成直线装配参考杆,请在三维视图中确认终点锚点、方向、外端位置和杆体贴合情况");
OnPropertyChanged(nameof(CanSelectAssemblyStartPoint));
- LogManager.Info($"[直线装配] 已生成参考杆: {AssemblyTerminalObjectName}");
+ LogManager.Info(
+ $"[直线装配] 已生成参考杆: {AssemblyTerminalObjectName}, " +
+ $"终点锚点=({referenceLine.EndPoint.X:F2}, {referenceLine.EndPoint.Y:F2}, {referenceLine.EndPoint.Z:F2}), " +
+ $"参考线外端=({referenceLine.StartPoint.X:F2}, {referenceLine.StartPoint.Y:F2}, {referenceLine.StartPoint.Z:F2}), " +
+ $"方向=({referenceLine.Direction.X:F3}, {referenceLine.Direction.Y:F3}, {referenceLine.Direction.Z:F3}), " +
+ $"资源={AssemblyReferencePathManager.Instance.ReferenceResourceName}");
}, "生成装配参考杆");
}
@@ -1391,14 +1395,16 @@ namespace NavisworksTransport.UI.WPF.ViewModels
CleanupAssemblyReferenceSelection();
AssemblyReferencePathManager.Instance.HideReferenceRod();
ClearAssemblyAnchorMarker();
+ ClearAssemblyCenterGuideLine();
+ ClearAssemblyReferenceLine();
AssemblyStartPointText = "未选择";
OnPropertyChanged(nameof(CanSelectAssemblyStartPoint));
- UpdateMainStatus("已隐藏直线装配参考杆");
- LogManager.Info("[直线装配] 已隐藏参考杆");
+ UpdateMainStatus("已隐藏直线装配参考线");
+ LogManager.Info("[直线装配] 已隐藏参考线");
}
catch (Exception ex)
{
- LogManager.Error($"[直线装配] 隐藏参考杆失败: {ex.Message}");
+ LogManager.Error($"[直线装配] 隐藏参考线失败: {ex.Message}");
}
}
@@ -1494,6 +1500,39 @@ namespace NavisworksTransport.UI.WPF.ViewModels
center.Z + up.Z * halfHeight * direction);
}
+ private AssemblyReferenceLine BuildAssemblyReferenceLine()
+ {
+ if (_assemblyTerminalObject == null || !ModelItemAnalysisHelper.IsModelItemValid(_assemblyTerminalObject))
+ {
+ throw new InvalidOperationException("终点箱体未设置或已失效,无法生成装配参考线");
+ }
+
+ BoundingBox3D bounds = _assemblyTerminalObject.BoundingBox();
+ Point3D centerPoint = bounds.Center;
+ Point3D endPoint = GetAssemblyTerminalAnchorPoint();
+ Vector3D direction = new Vector3D(centerPoint.X, centerPoint.Y, centerPoint.Z);
+ double directionLengthSquared = direction.X * direction.X + direction.Y * direction.Y + direction.Z * direction.Z;
+ if (directionLengthSquared < 1e-9)
+ {
+ throw new InvalidOperationException("箱体中心与世界原点重合,无法生成装配参考线方向");
+ }
+
+ direction = direction.Normalize();
+ double rodLength = UnitsConverter.ConvertFromMeters(AssemblyReferenceRodLengthInMeters);
+ Point3D startPoint = new Point3D(
+ endPoint.X + direction.X * rodLength,
+ endPoint.Y + direction.Y * rodLength,
+ endPoint.Z + direction.Z * rodLength);
+
+ LogManager.Info(
+ $"[直线装配] 参考线已计算,终点锚点=({endPoint.X:F2}, {endPoint.Y:F2}, {endPoint.Z:F2}), " +
+ $"箱体中心=({centerPoint.X:F2}, {centerPoint.Y:F2}, {centerPoint.Z:F2}), " +
+ $"参考线外端=({startPoint.X:F2}, {startPoint.Y:F2}, {startPoint.Z:F2}), " +
+ $"原点到箱体中心方向=({direction.X:F3}, {direction.Y:F3}, {direction.Z:F3})");
+
+ return new AssemblyReferenceLine(startPoint, endPoint, direction);
+ }
+
private void RefreshAssemblyTerminalObjectInfo(Point3D referenceStartPoint = null, Point3D referenceEndPoint = null)
{
if (_assemblyTerminalObject == null || !ModelItemAnalysisHelper.IsModelItemValid(_assemblyTerminalObject))
@@ -1551,21 +1590,17 @@ namespace NavisworksTransport.UI.WPF.ViewModels
try
{
- Point3D endPoint = GetAssemblyTerminalAnchorPoint();
- Vector3D forward = ModelItemTransformHelper.GetForwardDirectionFromTransform(_assemblyTerminalObject.Transform);
- double rodLength = UnitsConverter.ConvertFromMeters(AssemblyReferenceRodLengthInMeters);
- Point3D startPoint = new Point3D(
- endPoint.X - forward.X * rodLength,
- endPoint.Y - forward.Y * rodLength,
- endPoint.Z - forward.Z * rodLength);
+ AssemblyReferenceLine referenceLine = BuildAssemblyReferenceLine();
AssemblyReferencePathManager.Instance.CreateOrUpdateReferenceRod(
- startPoint,
- endPoint,
+ referenceLine.StartPoint,
+ referenceLine.EndPoint,
AssemblyReferenceRodDiameterInMeters);
- RefreshAssemblyTerminalObjectInfo(startPoint, endPoint);
+ RefreshAssemblyTerminalObjectInfo(referenceLine.StartPoint, referenceLine.EndPoint);
RenderAssemblyAnchorMarker();
+ RenderAssemblyCenterGuideLine();
+ RenderAssemblyReferenceLine(referenceLine);
OnPropertyChanged(nameof(CanSelectAssemblyStartPoint));
}
catch (Exception ex)
@@ -1574,6 +1609,82 @@ namespace NavisworksTransport.UI.WPF.ViewModels
}
}
+ private sealed class AssemblyReferenceLine
+ {
+ public AssemblyReferenceLine(Point3D startPoint, Point3D endPoint, Vector3D direction)
+ {
+ StartPoint = startPoint;
+ EndPoint = endPoint;
+ Direction = direction;
+ }
+
+ public Point3D StartPoint { get; }
+ public Point3D EndPoint { get; }
+ public Vector3D Direction { get; }
+ }
+
+ private static double CalculateDefaultAssemblyReferenceRodDiameterInMeters()
+ {
+ double standardRadiusInMeters = ConfigManager.Instance.Current.PathEditing.CellSizeMeters;
+ standardRadiusInMeters = Math.Max(0.1, Math.Min(0.5, standardRadiusInMeters));
+ return standardRadiusInMeters * 0.8;
+ }
+
+ private void FocusOnAssemblyReferenceArea(Point3D startPoint, Point3D endPoint)
+ {
+ try
+ {
+ double minX = Math.Min(startPoint.X, endPoint.X);
+ double minY = Math.Min(startPoint.Y, endPoint.Y);
+ double minZ = Math.Min(startPoint.Z, endPoint.Z);
+ double maxX = Math.Max(startPoint.X, endPoint.X);
+ double maxY = Math.Max(startPoint.Y, endPoint.Y);
+ double maxZ = Math.Max(startPoint.Z, endPoint.Z);
+
+ if (_assemblyTerminalObject != null && ModelItemAnalysisHelper.IsModelItemValid(_assemblyTerminalObject))
+ {
+ var terminalBounds = _assemblyTerminalObject.BoundingBox();
+ minX = Math.Min(minX, terminalBounds.Min.X);
+ minY = Math.Min(minY, terminalBounds.Min.Y);
+ minZ = Math.Min(minZ, terminalBounds.Min.Z);
+ maxX = Math.Max(maxX, terminalBounds.Max.X);
+ maxY = Math.Max(maxY, terminalBounds.Max.Y);
+ maxZ = Math.Max(maxZ, terminalBounds.Max.Z);
+ }
+
+ Point3D center = new Point3D(
+ (minX + maxX) / 2.0,
+ (minY + maxY) / 2.0,
+ (minZ + maxZ) / 2.0);
+
+ double targetSize = Math.Max(
+ maxX - minX,
+ Math.Max(maxY - minY, maxZ - minZ));
+
+ targetSize = Math.Max(targetSize, UnitsConverter.ConvertFromMeters(1.0));
+
+ ViewpointHelper.FocusOnPosition(center, targetSize, 45.0, 0.35);
+
+ var selection = new List();
+ if (_assemblyTerminalObject != null && ModelItemAnalysisHelper.IsModelItemValid(_assemblyTerminalObject))
+ {
+ selection.Add(_assemblyTerminalObject);
+ }
+
+ var referenceRod = AssemblyReferencePathManager.Instance.CurrentReferenceRod;
+ if (referenceRod != null)
+ {
+ selection.Add(referenceRod);
+ }
+
+ NavisworksSelectionHelper.SetModelSelection(selection);
+ }
+ catch (Exception ex)
+ {
+ LogManager.Warning($"[直线装配] 聚焦参考杆区域失败: {ex.Message}");
+ }
+ }
+
private void RenderAssemblyAnchorMarker()
{
if (_assemblyTerminalObject == null || !ModelItemAnalysisHelper.IsModelItemValid(_assemblyTerminalObject))
@@ -1588,6 +1699,8 @@ namespace NavisworksTransport.UI.WPF.ViewModels
}
Point3D anchorPoint = GetAssemblyTerminalAnchorPoint();
+ LogManager.Info(
+ $"[直线装配] 已渲染终点锚点标记: ({anchorPoint.X:F2}, {anchorPoint.Y:F2}, {anchorPoint.Z:F2})");
var markerRoute = new PathRoute("装配对接点")
{
Id = AssemblyAnchorMarkerPathId,
@@ -1597,6 +1710,47 @@ namespace NavisworksTransport.UI.WPF.ViewModels
renderPlugin.RenderPointOnly(markerRoute);
}
+ private void RenderAssemblyReferenceLine(AssemblyReferenceLine referenceLine)
+ {
+ var renderPlugin = PathPointRenderPlugin.Instance;
+ if (renderPlugin == null || referenceLine == null)
+ {
+ return;
+ }
+
+ renderPlugin.RenderRailBaseline(
+ AssemblyReferenceLinePathId,
+ new List { referenceLine.EndPoint, referenceLine.StartPoint });
+
+ LogManager.Info(
+ $"[直线装配] 已渲染参考线: 终点锚点=({referenceLine.EndPoint.X:F2}, {referenceLine.EndPoint.Y:F2}, {referenceLine.EndPoint.Z:F2}), " +
+ $"参考线外端=({referenceLine.StartPoint.X:F2}, {referenceLine.StartPoint.Y:F2}, {referenceLine.StartPoint.Z:F2})");
+ }
+
+ private void RenderAssemblyCenterGuideLine()
+ {
+ if (_assemblyTerminalObject == null || !ModelItemAnalysisHelper.IsModelItemValid(_assemblyTerminalObject))
+ {
+ return;
+ }
+
+ var renderPlugin = PathPointRenderPlugin.Instance;
+ if (renderPlugin == null)
+ {
+ return;
+ }
+
+ Point3D centerPoint = _assemblyTerminalObject.BoundingBox().Center;
+ renderPlugin.RenderRailBaseline(
+ AssemblyCenterGuideLinePathId,
+ new List { Point3D.Origin, centerPoint },
+ RenderStyleName.AssemblyGuideLine);
+
+ LogManager.Info(
+ $"[直线装配] 已渲染原点到箱体中心基准线: 原点=(0.00, 0.00, 0.00), " +
+ $"箱体中心=({centerPoint.X:F2}, {centerPoint.Y:F2}, {centerPoint.Z:F2})");
+ }
+
private void ClearAssemblyAnchorMarker()
{
var renderPlugin = PathPointRenderPlugin.Instance;
@@ -1608,6 +1762,28 @@ namespace NavisworksTransport.UI.WPF.ViewModels
renderPlugin.RemovePath(AssemblyAnchorMarkerPathId);
}
+ private void ClearAssemblyReferenceLine()
+ {
+ var renderPlugin = PathPointRenderPlugin.Instance;
+ if (renderPlugin == null)
+ {
+ return;
+ }
+
+ renderPlugin.ClearRailBaseline(AssemblyReferenceLinePathId);
+ }
+
+ private void ClearAssemblyCenterGuideLine()
+ {
+ var renderPlugin = PathPointRenderPlugin.Instance;
+ if (renderPlugin == null)
+ {
+ return;
+ }
+
+ renderPlugin.ClearRailBaseline(AssemblyCenterGuideLinePathId);
+ }
+
private void CleanupAssemblyReferenceSelection()
{
try
@@ -5071,6 +5247,15 @@ namespace NavisworksTransport.UI.WPF.ViewModels
LogManager.Warning($"清理直线装配对接点标记时发生异常: {ex.Message}");
}
+ try
+ {
+ ClearAssemblyReferenceLine();
+ }
+ catch (Exception ex)
+ {
+ LogManager.Warning($"清理直线装配参考线时发生异常: {ex.Message}");
+ }
+
// 确保停止任何活动的点击工具
try
{
diff --git a/src/Utils/ModelItemTransformHelper.cs b/src/Utils/ModelItemTransformHelper.cs
index 35af3a2..f878236 100644
--- a/src/Utils/ModelItemTransformHelper.cs
+++ b/src/Utils/ModelItemTransformHelper.cs
@@ -152,6 +152,50 @@ namespace NavisworksTransport.Utils
}
}
+ ///
+ /// 根据箱体局部尺寸和世界原点方向,推导“背离球心”的真实长轴方向。
+ /// 先选局部最长轴,再用箱体中心相对世界原点的方向决定正负。
+ ///
+ public static Vector3D GetLongestAxisDirectionAwayFromOrigin(BoundingBox3D bounds, Transform3D transform)
+ {
+ Vector3D localSize = EstimateLocalBoxSize(bounds, transform);
+
+ int longestAxisIndex = 0;
+ double longestAxisLength = localSize.X;
+ if (localSize.Y > longestAxisLength)
+ {
+ longestAxisIndex = 1;
+ longestAxisLength = localSize.Y;
+ }
+
+ if (localSize.Z > longestAxisLength)
+ {
+ longestAxisIndex = 2;
+ }
+
+ Vector3D axisDirection = GetAxisDirectionFromTransform(
+ transform,
+ longestAxisIndex,
+ longestAxisIndex == 0 ? new Vector3D(1, 0, 0) :
+ longestAxisIndex == 1 ? new Vector3D(0, 1, 0) :
+ new Vector3D(0, 0, 1));
+
+ Vector3D awayFromOrigin = new Vector3D(bounds.Center.X, bounds.Center.Y, bounds.Center.Z);
+ double awayLengthSquared = awayFromOrigin.X * awayFromOrigin.X + awayFromOrigin.Y * awayFromOrigin.Y + awayFromOrigin.Z * awayFromOrigin.Z;
+ if (awayLengthSquared < 1e-9)
+ {
+ return axisDirection;
+ }
+
+ double dot = axisDirection.X * awayFromOrigin.X + axisDirection.Y * awayFromOrigin.Y + axisDirection.Z * awayFromOrigin.Z;
+ if (dot < 0.0)
+ {
+ return new Vector3D(-axisDirection.X, -axisDirection.Y, -axisDirection.Z);
+ }
+
+ return axisDirection;
+ }
+
private static Vector3D GetAxisDirectionFromTransform(Transform3D transform, int axisIndex, Vector3D fallback)
{
if (transform == null)
@@ -317,6 +361,168 @@ namespace NavisworksTransport.Utils
ApplyAbsoluteTransform(item, targetPosition, targetRotation, preserveCurrentScale: true);
}
+ ///
+ /// 将物体移动到指定中心点和完整三维朝向,同时保留当前缩放。
+ /// 适用于参考杆、辅助几何等以几何中心作为定位基准的场景。
+ ///
+ public static void MoveItemToCenterAndRotationWithCurrentScale(ModelItem item, Point3D targetCenter, Rotation3D targetRotation)
+ {
+ if (item == null)
+ {
+ throw new ArgumentNullException(nameof(item));
+ }
+
+ var doc = Application.ActiveDocument;
+ var modelItems = new ModelItemCollection { item };
+
+ var currentComponents = item.Transform.Factor();
+ Vector3D currentScale = currentComponents.Scale;
+
+ doc.Models.ResetPermanentTransform(modelItems);
+
+ var originalBounds = item.BoundingBox();
+ Point3D originalCenter = originalBounds.Center;
+
+ var rotationTransform = new Transform3D(targetRotation);
+ var linear = rotationTransform.Linear;
+
+ var rotatedCenter = new Point3D(
+ linear.Get(0, 0) * originalCenter.X + linear.Get(0, 1) * originalCenter.Y + linear.Get(0, 2) * originalCenter.Z,
+ linear.Get(1, 0) * originalCenter.X + linear.Get(1, 1) * originalCenter.Y + linear.Get(1, 2) * originalCenter.Z,
+ linear.Get(2, 0) * originalCenter.X + linear.Get(2, 1) * originalCenter.Y + linear.Get(2, 2) * originalCenter.Z);
+
+ var identity = Transform3D.CreateTranslation(new Vector3D(0, 0, 0));
+ var components = identity.Factor();
+ components.Scale = currentScale;
+ components.Rotation = targetRotation;
+ components.Translation = new Vector3D(
+ targetCenter.X - rotatedCenter.X,
+ targetCenter.Y - rotatedCenter.Y,
+ targetCenter.Z - rotatedCenter.Z);
+
+ doc.Models.OverridePermanentTransform(modelItems, components.Combine(), false);
+ }
+
+ ///
+ /// 将物体移动到指定中心点和完整三维朝向。
+ /// 仅应用位置和旋转,不保留当前缩放。
+ /// 适用于缩放已经在 Model 层完成的参考杆等场景。
+ ///
+ public static void MoveItemToCenterAndRotation(ModelItem item, Point3D targetCenter, Rotation3D targetRotation)
+ {
+ if (item == null)
+ {
+ throw new ArgumentNullException(nameof(item));
+ }
+
+ var doc = Application.ActiveDocument;
+ var modelItems = new ModelItemCollection { item };
+
+ doc.Models.ResetPermanentTransform(modelItems);
+
+ var originalBounds = item.BoundingBox();
+ Point3D originalCenter = originalBounds.Center;
+
+ var rotationTransform = new Transform3D(targetRotation);
+ var linear = rotationTransform.Linear;
+
+ var rotatedCenter = new Point3D(
+ linear.Get(0, 0) * originalCenter.X + linear.Get(0, 1) * originalCenter.Y + linear.Get(0, 2) * originalCenter.Z,
+ linear.Get(1, 0) * originalCenter.X + linear.Get(1, 1) * originalCenter.Y + linear.Get(1, 2) * originalCenter.Z,
+ linear.Get(2, 0) * originalCenter.X + linear.Get(2, 1) * originalCenter.Y + linear.Get(2, 2) * originalCenter.Z);
+
+ var translation = new Vector3D(
+ targetCenter.X - rotatedCenter.X,
+ targetCenter.Y - rotatedCenter.Y,
+ targetCenter.Z - rotatedCenter.Z);
+
+ doc.Models.OverridePermanentTransform(modelItems, new Transform3D(targetRotation, translation), false);
+ }
+
+ ///
+ /// 将物体的局部正 X 端面中心对齐到目标点,并保留当前缩放。
+ /// 适用于单位立方体/单位圆柱体沿 +X 定义长度方向的参考杆资源。
+ ///
+ public static void MoveItemPositiveXEndToPointAndRotationWithCurrentScale(ModelItem item, Point3D targetPoint, Rotation3D targetRotation)
+ {
+ if (item == null)
+ {
+ throw new ArgumentNullException(nameof(item));
+ }
+
+ var doc = Application.ActiveDocument;
+ var modelItems = new ModelItemCollection { item };
+
+ var currentComponents = item.Transform.Factor();
+ Vector3D currentScale = currentComponents.Scale;
+
+ doc.Models.ResetPermanentTransform(modelItems);
+
+ var originalBounds = item.BoundingBox();
+ Point3D originalPositiveXEndCenter = new Point3D(
+ originalBounds.Max.X,
+ originalBounds.Center.Y,
+ originalBounds.Center.Z);
+
+ var rotationTransform = new Transform3D(targetRotation);
+ var linear = rotationTransform.Linear;
+
+ var rotatedPositiveXEndCenter = new Point3D(
+ linear.Get(0, 0) * originalPositiveXEndCenter.X + linear.Get(0, 1) * originalPositiveXEndCenter.Y + linear.Get(0, 2) * originalPositiveXEndCenter.Z,
+ linear.Get(1, 0) * originalPositiveXEndCenter.X + linear.Get(1, 1) * originalPositiveXEndCenter.Y + linear.Get(1, 2) * originalPositiveXEndCenter.Z,
+ linear.Get(2, 0) * originalPositiveXEndCenter.X + linear.Get(2, 1) * originalPositiveXEndCenter.Y + linear.Get(2, 2) * originalPositiveXEndCenter.Z);
+
+ var identity = Transform3D.CreateTranslation(new Vector3D(0, 0, 0));
+ var components = identity.Factor();
+ components.Scale = currentScale;
+ components.Rotation = targetRotation;
+ components.Translation = new Vector3D(
+ targetPoint.X - rotatedPositiveXEndCenter.X,
+ targetPoint.Y - rotatedPositiveXEndCenter.Y,
+ targetPoint.Z - rotatedPositiveXEndCenter.Z);
+
+ doc.Models.OverridePermanentTransform(modelItems, components.Combine(), false);
+ }
+
+ ///
+ /// 将物体的局部正 X 端面中心对齐到目标点。
+ /// 仅应用位置和旋转,不保留当前缩放。
+ /// 适用于缩放已经在 Model 层完成、Item 层只负责定位的场景。
+ ///
+ public static void MoveItemPositiveXEndToPointAndRotation(ModelItem item, Point3D targetPoint, Rotation3D targetRotation)
+ {
+ if (item == null)
+ {
+ throw new ArgumentNullException(nameof(item));
+ }
+
+ var doc = Application.ActiveDocument;
+ var modelItems = new ModelItemCollection { item };
+
+ doc.Models.ResetPermanentTransform(modelItems);
+
+ var originalBounds = item.BoundingBox();
+ Point3D originalPositiveXEndCenter = new Point3D(
+ originalBounds.Max.X,
+ originalBounds.Center.Y,
+ originalBounds.Center.Z);
+
+ var rotationTransform = new Transform3D(targetRotation);
+ var linear = rotationTransform.Linear;
+
+ var rotatedPositiveXEndCenter = new Point3D(
+ linear.Get(0, 0) * originalPositiveXEndCenter.X + linear.Get(0, 1) * originalPositiveXEndCenter.Y + linear.Get(0, 2) * originalPositiveXEndCenter.Z,
+ linear.Get(1, 0) * originalPositiveXEndCenter.X + linear.Get(1, 1) * originalPositiveXEndCenter.Y + linear.Get(1, 2) * originalPositiveXEndCenter.Z,
+ linear.Get(2, 0) * originalPositiveXEndCenter.X + linear.Get(2, 1) * originalPositiveXEndCenter.Y + linear.Get(2, 2) * originalPositiveXEndCenter.Z);
+
+ var translation = new Vector3D(
+ targetPoint.X - rotatedPositiveXEndCenter.X,
+ targetPoint.Y - rotatedPositiveXEndCenter.Y,
+ targetPoint.Z - rotatedPositiveXEndCenter.Z);
+
+ doc.Models.OverridePermanentTransform(modelItems, new Transform3D(targetRotation, translation), false);
+ }
+
private static void ApplyAbsoluteTransform(ModelItem item, Point3D targetPosition, Rotation3D targetRotation, bool preserveCurrentScale)
{
var doc = Application.ActiveDocument;
From 9adf9b5bafea2afe52d018d93a370ab2c65e63f2 Mon Sep 17 00:00:00 2001
From: tian <11429339@qq.com>
Date: Thu, 19 Mar 2026 23:12:49 +0800
Subject: [PATCH 03/87] Fix assembly reference rod and start-point path
creation
---
resources/unit_cylinder.nwc | Bin 1980 -> 2299 bytes
resources/unit_cylinder.obj | 201 ++++++++++++------
src/UI/WPF/ViewModels/PathEditingViewModel.cs | 68 ++++--
src/UI/WPF/Views/PathEditingView.xaml | 30 +++
4 files changed, 221 insertions(+), 78 deletions(-)
diff --git a/resources/unit_cylinder.nwc b/resources/unit_cylinder.nwc
index 540b4e6bcbcc8d9b3037500d27ab9fdabeaf179e..8fbca10ff7c38ea5d4e270902439dd0513b7584c 100644
GIT binary patch
delta 1406
zcmV-^1%dj!5Bm|2IDfH913?gmFIp&)LhY=4f*c5`u1y=F#ict~#=TKi=XRHwTf^ZC
zq|7V$1i@19bt1lnPI5^l_`|omEdR{^pJct}%A}~~f-F^uL=9FDqf|3^ak2>fEG2`N
z7`PoVI2tt}`Bu*wrDBwj*y<~J*BaZA^EGlF?w@q;k7|AX>3_C4uV;PUl`^%ng`Eaa
z=N+X-=L@n__cMb7gZ{oylA!4fh|xM!n-ul$c<(#tNV!(Fy(o{!4}lx-7IV^|+y)|u
zg4wBhYGtepmCb7TwOno1AK&k9m%DDaFxi{SE@E+$W%)mQ8KFfJfuX^coi&kNBY*q-
z0y`>bb^tBIVv{rhDt~>{Plysx7y$4W%@hQM&>lkZ;z6QI5O{EiH>R_*uxy~j1PfUb
zyA_CO`)}6GH3v7rLx|+1o9j^z9-@na(?vXZsE!_z31x2mxdkHF`^I^@55XRM&Trm)
z^Ue3>4Q?TX9+TK6ahJq%67P{U^q=xyeC>qm5Q}yo=4koH$A9k_p&aDsj75|Cgh^QQ
zjLE$@O!BOG#v*wRCeO3x8PjL!c2Glphsp1p=jS?1eQM~Z!}LQf&aa>KPc1?{d+j=7
zaiI@tnva=$%+$wBea!TOnSLS=-w!`{xcTVG)9n}Wm#_BJqhljrgbaDp
zSVP8|AsefPM9zc8JtOcYdz^WFls;4s_V;#opw=@ITO>ZrX0tn1^@JngaA?00hhpFW
z&iOCzKAQ=59NcRA>Wye$0V}mpMV6`pI8quG{9e@so`2?@TA5Eyz)o$NFOGWy@URdm
z#k#FFcvMkpzPeHa`|7Rss1hlGW8L0hSeWL)#c^`t)SMc)%&V?xepmqey;UJn8dbou
z)K{PqxDj>7TkTjE+)R1Wy+Lsd<}zkBIVtmCGs|UEm){Haq?@U#AWL9(99Gqcg8Par
zx#P_gxLzvwy`D6e0jsVuZ)VNxd=EB$6KP1h{dLLswrtGrzfR(^eGKQWUb^x&czPPX
zv9dxyrf!~)A0dZUFDwb}KiA+-=O6HNcdB@tV`N}pU<6_}CI*HM76t|Y1eO5TvwQ+5
z0Y*6BF8lkEeUT8<@@zdg|S+a8BB6nwCU(NzrHe|{;xv_}zoe@{!pFcbjr=>A-W3{i)m-gg#{o~&*PV;ids5f6*)+Zt?}
znr4ihcK7Dj(X(H{k7G@iS4ZP?fk&WyzrMWJWLcKg2_ZEV9j}*MUO3bXfNT-7&T#)C
z#dV^>)0q!ib!UDXcmz_+9UdhD(p0@KsW`QtrMMK4*?c9J0
z@VnfHRFk)o$*)=U8n)|GA(>=6HuXNxy;`nNkN%*(;K{Pyux#?`WOdHky7_eYgn2Z)
zW+15Wg1k>a|GeqEEkP#{#4+ETwsZfEeq?#w2Uc#e5N_fzFVsD%
zn%%eDE;@B>BObfz01-`bMtUu(>-*h7cQ=6XEqx3D%?sw+_vjnR)RRfU&%^1I6Iyp0
M2LJ#70001xG&%sXPXGV_
delta 1083
zcmV-B1jPIM5xftOIDaut12GUqHy{KB4RlnTAeNA*woMxaicQ5z*5DnGVb->0)-k_|mg2>(BguSPYsgld_%*veXq4wOB(+QqSPY%OdilR185<
z`7*XI)P|G{`32KIqDd-+$!D!H{>ROx~U|@&?;#45E0b(y8_5)(B>x-?qt}9!wdDOcb$li^_2GJln
zusAXsB*y^Pi(o_5SR<*oMl#EV6+11&V(a8xrN=;=*PKb>P4}S^t^ZmfUP+Z9n7!nzm
zoRM2pUgPf_;14k)4Tw{Kc-#N~|9OCPV-!fVe(Ak?i>en@gV;)njZuwJN{S#hNbKId
z`lSqDl}d_nEpaU%Hb@Ml2gC-60aZ6)gBOs&MPvY!1p^TvW`WW!KoA=0>cYSf5*h*I
z8#FMuGD0y!Fdz#Gj~OTr00oKI0(hKbWME)m1Y&L=k-)^j0006J0CBUr11JGWNN`sF
zZEpZ%umCZW0uVzbQi0eUh#7%64T#Oaq7C-gF?O)#s5;09hIAl~1!Au2i+3YoYZ%6*
zU)t)XeYMa(FGeUI0R2!JQj;hJnOf9;dq3VWYoa|4X(;$$52LFXy8rxAd})s=uHrzP
zeoXtA&{Q&^h<(6t7aJN)9KbXO3L22**{N(EGnLIl4=YN|W1+HntW-7+Q$0FOtUaXa
z!!!o~*0n7uvl|9X0e{a*!!Q^C@XXCql_CCN2fYb;^xzZNxhafwSd|?-7~5}aux)CZ
zG6o*JdG_Yx`2xOyP?Pnmqp`X`2(;g?-=8MW^Sn(6vD6-TPSQQIsTTlgkYb(U`9-Sh
zKy6P)-f1jPk65MOKrjFGb|!@C^_IcxJOtf#>0i*_8(#N;nR_gR%Xq>oc|XwkhTB4?
zu5H9)R}~P^6lbJYcfG#fO?2Zcm|W9`5YRXnZ{DMCA<>d2&6t#MU68LT;02Sx2s$@K
B{*V9w
diff --git a/resources/unit_cylinder.obj b/resources/unit_cylinder.obj
index f920599..3b4e1ca 100644
--- a/resources/unit_cylinder.obj
+++ b/resources/unit_cylinder.obj
@@ -1,58 +1,104 @@
# unit cylinder for assembly reference rod
-# dimensions: length 0.01m along +X, diameter 0.01m
+# dimensions: length 1.0 along +X, diameter 1.0
# centered at origin
+# when Navisworks imports OBJ as centimeters, generated NWC becomes 0.01m base size
o unit_cylinder
-v -0.005000 0.000000 0.000000
-v -0.005000 0.005000 0.000000
-v -0.005000 0.004330 0.002500
-v -0.005000 0.002500 0.004330
-v -0.005000 0.000000 0.005000
-v -0.005000 -0.002500 0.004330
-v -0.005000 -0.004330 0.002500
-v -0.005000 -0.005000 0.000000
-v -0.005000 -0.004330 -0.002500
-v -0.005000 -0.002500 -0.004330
-v -0.005000 0.000000 -0.005000
-v -0.005000 0.002500 -0.004330
-v -0.005000 0.004330 -0.002500
-v 0.005000 0.000000 0.000000
-v 0.005000 0.005000 0.000000
-v 0.005000 0.004330 0.002500
-v 0.005000 0.002500 0.004330
-v 0.005000 0.000000 0.005000
-v 0.005000 -0.002500 0.004330
-v 0.005000 -0.004330 0.002500
-v 0.005000 -0.005000 0.000000
-v 0.005000 -0.004330 -0.002500
-v 0.005000 -0.002500 -0.004330
-v 0.005000 0.000000 -0.005000
-v 0.005000 0.002500 -0.004330
-v 0.005000 0.004330 -0.002500
-f 2 15 16
-f 2 16 3
-f 3 16 17
-f 3 17 4
-f 4 17 18
-f 4 18 5
-f 5 18 19
-f 5 19 6
-f 6 19 20
-f 6 20 7
-f 7 20 21
-f 7 21 8
-f 8 21 22
-f 8 22 9
-f 9 22 23
-f 9 23 10
-f 10 23 24
-f 10 24 11
-f 11 24 25
-f 11 25 12
-f 12 25 26
-f 12 26 13
-f 13 26 15
-f 13 15 2
-f 1 3 2
+v -0.500000 0.000000 0.000000
+v 0.500000 0.000000 0.000000
+v -0.500000 0.500000 0.000000
+v -0.500000 0.482963 0.129410
+v -0.500000 0.433013 0.250000
+v -0.500000 0.353553 0.353553
+v -0.500000 0.250000 0.433013
+v -0.500000 0.129410 0.482963
+v -0.500000 0.000000 0.500000
+v -0.500000 -0.129410 0.482963
+v -0.500000 -0.250000 0.433013
+v -0.500000 -0.353553 0.353553
+v -0.500000 -0.433013 0.250000
+v -0.500000 -0.482963 0.129410
+v -0.500000 -0.500000 0.000000
+v -0.500000 -0.482963 -0.129410
+v -0.500000 -0.433013 -0.250000
+v -0.500000 -0.353553 -0.353553
+v -0.500000 -0.250000 -0.433013
+v -0.500000 -0.129410 -0.482963
+v -0.500000 0.000000 -0.500000
+v -0.500000 0.129410 -0.482963
+v -0.500000 0.250000 -0.433013
+v -0.500000 0.353553 -0.353553
+v -0.500000 0.433013 -0.250000
+v -0.500000 0.482963 -0.129410
+v 0.500000 0.500000 0.000000
+v 0.500000 0.482963 0.129410
+v 0.500000 0.433013 0.250000
+v 0.500000 0.353553 0.353553
+v 0.500000 0.250000 0.433013
+v 0.500000 0.129410 0.482963
+v 0.500000 0.000000 0.500000
+v 0.500000 -0.129410 0.482963
+v 0.500000 -0.250000 0.433013
+v 0.500000 -0.353553 0.353553
+v 0.500000 -0.433013 0.250000
+v 0.500000 -0.482963 0.129410
+v 0.500000 -0.500000 0.000000
+v 0.500000 -0.482963 -0.129410
+v 0.500000 -0.433013 -0.250000
+v 0.500000 -0.353553 -0.353553
+v 0.500000 -0.250000 -0.433013
+v 0.500000 -0.129410 -0.482963
+v 0.500000 0.000000 -0.500000
+v 0.500000 0.129410 -0.482963
+v 0.500000 0.250000 -0.433013
+v 0.500000 0.353553 -0.353553
+v 0.500000 0.433013 -0.250000
+v 0.500000 0.482963 -0.129410
+f 3 26 27
+f 3 27 4
+f 4 27 28
+f 4 28 5
+f 5 28 29
+f 5 29 6
+f 6 29 30
+f 6 30 7
+f 7 30 31
+f 7 31 8
+f 8 31 32
+f 8 32 9
+f 9 32 33
+f 9 33 10
+f 10 33 34
+f 10 34 11
+f 11 34 35
+f 11 35 12
+f 12 35 36
+f 12 36 13
+f 13 36 37
+f 13 37 14
+f 14 37 38
+f 14 38 15
+f 15 38 39
+f 15 39 16
+f 16 39 40
+f 16 40 17
+f 17 40 41
+f 17 41 18
+f 18 41 42
+f 18 42 19
+f 19 42 43
+f 19 43 20
+f 20 43 44
+f 20 44 21
+f 21 44 45
+f 21 45 22
+f 22 45 46
+f 22 46 23
+f 23 46 47
+f 23 47 24
+f 24 47 48
+f 24 48 25
+f 25 48 49
+f 25 49 26
f 1 4 3
f 1 5 4
f 1 6 5
@@ -63,16 +109,41 @@ f 1 10 9
f 1 11 10
f 1 12 11
f 1 13 12
-f 1 2 13
-f 14 15 16
-f 14 16 17
-f 14 17 18
-f 14 18 19
-f 14 19 20
-f 14 20 21
-f 14 21 22
-f 14 22 23
-f 14 23 24
-f 14 24 25
-f 14 25 26
-f 14 26 15
+f 1 14 13
+f 1 15 14
+f 1 16 15
+f 1 17 16
+f 1 18 17
+f 1 19 18
+f 1 20 19
+f 1 21 20
+f 1 22 21
+f 1 23 22
+f 1 24 23
+f 1 25 24
+f 1 26 25
+f 1 3 26
+f 2 27 28
+f 2 28 29
+f 2 29 30
+f 2 30 31
+f 2 31 32
+f 2 32 33
+f 2 33 34
+f 2 34 35
+f 2 35 36
+f 2 36 37
+f 2 37 38
+f 2 38 39
+f 2 39 40
+f 2 40 41
+f 2 41 42
+f 2 42 43
+f 2 43 44
+f 2 44 45
+f 2 45 46
+f 2 46 47
+f 2 47 48
+f 2 48 49
+f 2 49 26
+f 2 26 27
diff --git a/src/UI/WPF/ViewModels/PathEditingViewModel.cs b/src/UI/WPF/ViewModels/PathEditingViewModel.cs
index 37eb74a..240c311 100644
--- a/src/UI/WPF/ViewModels/PathEditingViewModel.cs
+++ b/src/UI/WPF/ViewModels/PathEditingViewModel.cs
@@ -122,10 +122,12 @@ namespace NavisworksTransport.UI.WPF.ViewModels
private ObservableCollection> _railPayloadAnchorModeOptions;
private const double DefaultAssemblyReferenceRodLengthInMeters = 20.0;
private static readonly double DefaultAssemblyReferenceRodDiameterInMeters = CalculateDefaultAssemblyReferenceRodDiameterInMeters();
+ private const double DefaultAssemblyAnchorVerticalOffsetInMeters = 0.0;
private string _assemblyTerminalObjectName = "未选择";
private string _assemblyTerminalObjectInfo = "请选择终点处已安装箱体";
private double _assemblyReferenceRodLengthInMeters = DefaultAssemblyReferenceRodLengthInMeters;
private double _assemblyReferenceRodDiameterInMeters = DefaultAssemblyReferenceRodDiameterInMeters;
+ private double _assemblyAnchorVerticalOffsetInMeters = DefaultAssemblyAnchorVerticalOffsetInMeters;
private string _assemblyStartPointText = "未选择";
private RailMountMode _assemblyMountMode = RailMountMode.UnderRail;
private RailPayloadAnchorMode _assemblyPayloadAnchorMode = RailPayloadAnchorMode.TopCenter;
@@ -374,6 +376,20 @@ namespace NavisworksTransport.UI.WPF.ViewModels
}
}
+ public double AssemblyAnchorVerticalOffsetInMeters
+ {
+ get => _assemblyAnchorVerticalOffsetInMeters;
+ set
+ {
+ if (SetProperty(ref _assemblyAnchorVerticalOffsetInMeters, value) && HasAssemblyTerminalObject)
+ {
+ RefreshAssemblyTerminalObjectInfo();
+ RenderAssemblyAnchorMarker();
+ RefreshAssemblyReferenceRodIfNeeded();
+ }
+ }
+ }
+
public bool HasAssemblyTerminalObject
{
get => _hasAssemblyTerminalObject;
@@ -1304,6 +1320,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
HasAssemblyTerminalObject = true;
AssemblyStartPointText = "未选择";
AssemblyTerminalObjectName = ModelItemAnalysisHelper.GetSafeDisplayName(selectedItem);
+ InitializeAssemblyAnchorVerticalOffsetFromTerminalObject();
RefreshAssemblyTerminalObjectInfo();
RenderAssemblyAnchorMarker();
OnPropertyChanged(nameof(CanGenerateAssemblyReferenceRod));
@@ -1420,6 +1437,9 @@ namespace NavisworksTransport.UI.WPF.ViewModels
Point3D projectedStartPoint = AssemblyReferencePathManager.Instance.ProjectPointToReferenceLine(pickResult.Point);
_assemblyStartPoint = projectedStartPoint;
AssemblyStartPointText = $"({projectedStartPoint.X:F2}, {projectedStartPoint.Y:F2}, {projectedStartPoint.Z:F2})";
+ LogManager.Info(
+ $"[直线装配] 点击点=({pickResult.Point.X:F2}, {pickResult.Point.Y:F2}, {pickResult.Point.Z:F2})," +
+ $"投影起点=({projectedStartPoint.X:F2}, {projectedStartPoint.Y:F2}, {projectedStartPoint.Z:F2})");
await SafeExecuteAsync(() =>
{
@@ -1450,7 +1470,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
RailPathDefinitionMode = RailPathDefinitionMode.RailCenterLine
};
- route.AddPoint(new PathPoint(_assemblyStartPoint, "装配起点", PathPointType.StartPoint));
+ route.AddPoint(new PathPoint(startPoint, "装配起点", PathPointType.StartPoint));
route.AddPoint(new PathPoint(endPoint, "装配终点", PathPointType.EndPoint));
if (!_pathPlanningManager.AddRoute(route))
@@ -1459,14 +1479,21 @@ namespace NavisworksTransport.UI.WPF.ViewModels
}
_pathPlanningManager.SetCurrentRoute(route);
- _pathPlanningManager.DrawRouteVisualization(route, isAutoPath: false);
- _pathPlanningManager.SavePathToDatabase(route);
-
if (!_pathPlanningManager.GeneratePath(route))
{
throw new InvalidOperationException("装配路径生成失败");
}
+ _pathPlanningManager.DrawRouteVisualization(route, isAutoPath: false);
+ RefreshPathRoutes();
+
+ var generatedRouteViewModel = PathRoutes.FirstOrDefault(p => p.Name == route.Name);
+ if (generatedRouteViewModel != null)
+ {
+ SelectedPathRoute = generatedRouteViewModel;
+ UpdatePassageSpaceVisualizationForPathType(route.PathType);
+ }
+
AssemblyTerminalObjectInfo = string.Format(
"对接终点=({0:F2}, {1:F2}, {2:F2}),装配起点={3},安装方式={4},对接基准={5}",
endPoint.X,
@@ -1477,7 +1504,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
AssemblyPayloadAnchorMode == RailPayloadAnchorMode.TopCenter ? "顶面对接" : "底面对接");
OnPropertyChanged(nameof(PathRoutes));
- LogManager.Info($"[直线装配] 已生成路径: {route.Name}");
+ LogManager.Info($"[直线装配] 已生成路径: {route.Name},起点=({startPoint.X:F2}, {startPoint.Y:F2}, {startPoint.Z:F2}),终点=({endPoint.X:F2}, {endPoint.Y:F2}, {endPoint.Z:F2})");
}
private Point3D GetAssemblyTerminalAnchorPoint()
@@ -1490,14 +1517,13 @@ namespace NavisworksTransport.UI.WPF.ViewModels
var bounds = _assemblyTerminalObject.BoundingBox();
var center = bounds.Center;
Vector3D up = ModelItemTransformHelper.GetUpDirectionFromTransform(_assemblyTerminalObject.Transform);
- Vector3D localSize = ModelItemTransformHelper.EstimateLocalBoxSize(bounds, _assemblyTerminalObject.Transform);
- double halfHeight = Math.Max(0.0, localSize.Z / 2.0);
double direction = AssemblyPayloadAnchorMode == RailPayloadAnchorMode.TopCenter ? 1.0 : -1.0;
+ double verticalOffset = UnitsConverter.ConvertFromMeters(AssemblyAnchorVerticalOffsetInMeters);
return new Point3D(
- center.X + up.X * halfHeight * direction,
- center.Y + up.Y * halfHeight * direction,
- center.Z + up.Z * halfHeight * direction);
+ center.X + up.X * verticalOffset * direction,
+ center.Y + up.Y * verticalOffset * direction,
+ center.Z + up.Z * verticalOffset * direction);
}
private AssemblyReferenceLine BuildAssemblyReferenceLine()
@@ -1548,7 +1574,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
if (referenceStartPoint != null && referenceEndPoint != null)
{
AssemblyTerminalObjectInfo = string.Format(
- "中心=({0:F2}, {1:F2}, {2:F2}),尺寸=({3:F2}, {4:F2}, {5:F2}),{6},{7}点=({8:F2}, {9:F2}, {10:F2}),参考杆起点=({11:F2}, {12:F2}, {13:F2})",
+ "中心=({0:F2}, {1:F2}, {2:F2}),尺寸=({3:F2}, {4:F2}, {5:F2}),{6},{7}点=({8:F2}, {9:F2}, {10:F2}),垂直偏移={11:F3}m,参考杆起点=({12:F2}, {13:F2}, {14:F2})",
bounds.Center.X,
bounds.Center.Y,
bounds.Center.Z,
@@ -1560,6 +1586,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
referenceEndPoint.X,
referenceEndPoint.Y,
referenceEndPoint.Z,
+ AssemblyAnchorVerticalOffsetInMeters,
referenceStartPoint.X,
referenceStartPoint.Y,
referenceStartPoint.Z);
@@ -1567,7 +1594,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
}
AssemblyTerminalObjectInfo = string.Format(
- "中心=({0:F2}, {1:F2}, {2:F2}),尺寸=({3:F2}, {4:F2}, {5:F2}),{6},{7}点=({8:F2}, {9:F2}, {10:F2})",
+ "中心=({0:F2}, {1:F2}, {2:F2}),尺寸=({3:F2}, {4:F2}, {5:F2}),{6},{7}点=({8:F2}, {9:F2}, {10:F2}),垂直偏移={11:F3}m",
bounds.Center.X,
bounds.Center.Y,
bounds.Center.Z,
@@ -1578,7 +1605,8 @@ namespace NavisworksTransport.UI.WPF.ViewModels
anchorText,
anchorPoint.X,
anchorPoint.Y,
- anchorPoint.Z);
+ anchorPoint.Z,
+ AssemblyAnchorVerticalOffsetInMeters);
}
private void RefreshAssemblyReferenceRodIfNeeded()
@@ -1609,6 +1637,20 @@ namespace NavisworksTransport.UI.WPF.ViewModels
}
}
+ private void InitializeAssemblyAnchorVerticalOffsetFromTerminalObject()
+ {
+ if (_assemblyTerminalObject == null || !ModelItemAnalysisHelper.IsModelItemValid(_assemblyTerminalObject))
+ {
+ return;
+ }
+
+ var bounds = _assemblyTerminalObject.BoundingBox();
+ Vector3D localSize = ModelItemTransformHelper.EstimateLocalBoxSize(bounds, _assemblyTerminalObject.Transform);
+ double suggestedOffset = UnitsConverter.ConvertToMeters(Math.Max(0.0, localSize.Z / 2.0));
+ _assemblyAnchorVerticalOffsetInMeters = suggestedOffset;
+ OnPropertyChanged(nameof(AssemblyAnchorVerticalOffsetInMeters));
+ }
+
private sealed class AssemblyReferenceLine
{
public AssemblyReferenceLine(Point3D startPoint, Point3D endPoint, Vector3D direction)
diff --git a/src/UI/WPF/Views/PathEditingView.xaml b/src/UI/WPF/Views/PathEditingView.xaml
index e8f87cb..e493651 100644
--- a/src/UI/WPF/Views/PathEditingView.xaml
+++ b/src/UI/WPF/Views/PathEditingView.xaml
@@ -332,6 +332,7 @@ NavisworksTransport 路径编辑页签视图 - 采用与动画控制和分层管
+
+
+
+
+
+
+
+
+
+
+
+
+
+
From 5847ba9ab3cda5e6342106b5a5f691f0627455ad Mon Sep 17 00:00:00 2001
From: tian <11429339@qq.com>
Date: Fri, 20 Mar 2026 11:13:43 +0800
Subject: [PATCH 04/87] Fix rail assembly pose and animation flow
---
deploy-plugin.bat | 2 +-
doc/design/2026/NavisworksAPI使用方法.md | 52 +++++
src/Core/Animation/PathAnimationManager.cs | 87 +++++++-
src/Core/PathDataManager.cs | 14 --
src/Core/PathDatabase.cs | 11 +-
src/Core/PathPlanningModels.cs | 29 +--
src/Core/VirtualObjectManager.cs | 138 ++++++++++++
src/UI/WPF/ViewModels/PathEditingViewModel.cs | 75 ++-----
src/UI/WPF/Views/PathEditingView.xaml | 40 ----
src/Utils/ModelItemTransformHelper.cs | 78 +++++++
src/Utils/RailPathPoseHelper.cs | 199 ++++++++++++++++--
11 files changed, 547 insertions(+), 178 deletions(-)
diff --git a/deploy-plugin.bat b/deploy-plugin.bat
index 1cce12f..96fa442 100644
--- a/deploy-plugin.bat
+++ b/deploy-plugin.bat
@@ -5,7 +5,7 @@ setlocal
taskkill /F /IM Roamer.exe 2>nul
if %errorlevel% == 0 (
echo Navisworks process terminated.
- timeout /t 1 /nobreak >nul
+ timeout /t 3 /nobreak >nul
)
set "TARGET_DIR=%PROGRAMDATA%\Autodesk\Navisworks Manage 2026\plugins\TransportPlugin"
diff --git a/doc/design/2026/NavisworksAPI使用方法.md b/doc/design/2026/NavisworksAPI使用方法.md
index a9d16fd..930dc03 100644
--- a/doc/design/2026/NavisworksAPI使用方法.md
+++ b/doc/design/2026/NavisworksAPI使用方法.md
@@ -727,6 +727,58 @@ var transform = components.Combine();
- 3 参数版本是“轴 + 两个参考向量”的构造,不是“三个基向量/三个坐标轴”
- 如果需求是“让物体本地某个轴对齐任意空间方向”,优先使用 **2 参数版本**
+#### 11.7.2.1.1 Rotation3D 四元数分量顺序的实测结论(2026-03-20 新增,重要)
+
+在 Rail 路径三维姿态调试中,对 `Rotation3D(Double, Double, Double, Double)` 和 `Rotation3D.A/B/C/D` 做了直接实测。
+
+**结论:**
+
+- `Rotation3D(a, b, c, d)` 的 4 个参数顺序是:**`x, y, z, w`**
+- 对应属性:
+ - `A = quaternion.x`
+ - `B = quaternion.y`
+ - `C = quaternion.z`
+ - `D = quaternion.w`
+
+实测验证方式:
+
+```csharp
+var rx = new Rotation3D(new UnitVector3D(1, 0, 0), Math.PI / 2.0);
+var ry = new Rotation3D(new UnitVector3D(0, 1, 0), Math.PI / 2.0);
+var rz = new Rotation3D(new UnitVector3D(0, 0, 1), Math.PI / 2.0);
+```
+
+日志结果:
+
+- `X90`: `A=0.707107, B=0.000000, C=0.000000, D=0.707107`
+- `Y90`: `A=0.000000, B=0.707107, C=0.000000, D=0.707107`
+- `Z90`: `A=0.000000, B=0.000000, C=0.707107, D=0.707107`
+
+并且将 `A/B/C/D` 原样重新传回:
+
+```csharp
+var reconstructed = new Rotation3D(rotation.A, rotation.B, rotation.C, rotation.D);
+var linear = new Transform3D(reconstructed).Linear;
+```
+
+读回的线性矩阵与对应的 `X90 / Y90 / Z90` 旋转完全一致。
+
+**因此:**
+
+- 当你已经算出了目标姿态的 quaternion `(qx, qy, qz, qw)` 时,正确写法是:
+
+```csharp
+var rotation = new Rotation3D(qx, qy, qz, qw);
+```
+
+- **不要**写成:
+
+```csharp
+var rotation = new Rotation3D(qw, qx, qy, qz); // ❌ 错误
+```
+
+这个结论已经在 Rail 三维姿态、参考杆三维定向的实际运行中验证通过。
+
#### 11.7.2.2 三维参考杆/任意向量对齐的正确用法
适用场景:
diff --git a/src/Core/Animation/PathAnimationManager.cs b/src/Core/Animation/PathAnimationManager.cs
index 3d05eef..8b0e72c 100644
--- a/src/Core/Animation/PathAnimationManager.cs
+++ b/src/Core/Animation/PathAnimationManager.cs
@@ -74,6 +74,8 @@ namespace NavisworksTransport.Core.Animation
public double YawRadians { get; set; } // 绕Z轴的偏航角(弧度)
public Rotation3D Rotation { get; set; } // 可选的完整三维姿态
public bool HasCustomRotation { get; set; } // 是否使用完整三维姿态
+ public Matrix3 LinearTransform { get; set; } // 可选的完整三维线性姿态
+ public bool HasCustomLinearTransform { get; set; }// 是否使用完整三维线性姿态
public List Collisions { get; set; } // 该帧的碰撞结果
public bool HasCollision => Collisions?.Count > 0;
@@ -83,6 +85,8 @@ namespace NavisworksTransport.Core.Animation
YawRadians = 0.0;
Rotation = Rotation3D.Identity;
HasCustomRotation = false;
+ LinearTransform = null;
+ HasCustomLinearTransform = false;
}
}
@@ -653,17 +657,35 @@ namespace NavisworksTransport.Core.Animation
Point3D previousPoint = _pathPoints[0];
Point3D nextPoint = _pathPoints.Count > 1 ? _pathPoints[1] : _pathPoints[0];
startPosition = RailPathPoseHelper.ResolveBottomPosition(_route, startPosition, previousPoint, nextPoint, objectHeight);
- LogManager.Debug($"[移动到起点] Rail路径调整: 参考点=({_pathPoints[0].X:F2},{_pathPoints[0].Y:F2},{_pathPoints[0].Z:F2}), 物体底面=({startPosition.X:F2},{startPosition.Y:F2},{startPosition.Z:F2}), 物体高度={objectHeight:F2}, 安装={_route.RailMountMode}, 对接={_route.RailPayloadAnchorMode}, 偏移={_route.RailReferenceToAnchorOffset:F2}");
+ LogManager.Debug($"[移动到起点] Rail路径调整: 参考点=({_pathPoints[0].X:F2},{_pathPoints[0].Y:F2},{_pathPoints[0].Z:F2}), 物体底面=({startPosition.X:F2},{startPosition.Y:F2},{startPosition.Z:F2}), 物体高度={objectHeight:F2}, 安装={_route.RailMountMode}, 对接={(PathRoute.IsTopPayloadAnchorForMountMode(_route.RailMountMode) ? "顶面对接" : "底面对接")}, 偏移={_route.RailReferenceToAnchorOffset:F2}");
- if (!_isVirtualObject && _animatedObject != null &&
- RailPathPoseHelper.TryCreateRailRotation(previousPoint, _pathPoints[0], nextPoint, out var railRotation))
+ if (RailPathPoseHelper.TryCreateRailRotation(previousPoint, _pathPoints[0], nextPoint, out var railRotation))
{
- ModelItemTransformHelper.MoveItemToPositionAndRotationWithCurrentScale(_animatedObject, startPosition, railRotation);
+ var railLinearTransform = new Transform3D(railRotation).Linear;
+ LogManager.Info(
+ $"[移动到起点] Rail旋转姿态: " +
+ $"X=({railLinearTransform.Get(0, 0):F4},{railLinearTransform.Get(1, 0):F4},{railLinearTransform.Get(2, 0):F4}), " +
+ $"Y=({railLinearTransform.Get(0, 1):F4},{railLinearTransform.Get(1, 1):F4},{railLinearTransform.Get(2, 1):F4}), " +
+ $"Z=({railLinearTransform.Get(0, 2):F4},{railLinearTransform.Get(1, 2):F4},{railLinearTransform.Get(2, 2):F4})");
+
+ if (_isVirtualObject)
+ {
+ VirtualObjectManager.Instance.MoveVirtualObject(startPosition, railRotation);
+ }
+ else if (_animatedObject != null)
+ {
+ ModelItemTransformHelper.MoveItemToPositionAndRotationWithCurrentScale(_animatedObject, startPosition, railRotation);
+ }
+ else
+ {
+ return false;
+ }
+
_currentPosition = startPosition;
_currentRotation = railRotation;
_hasCurrentRotation = true;
- _currentYaw = ModelItemTransformHelper.GetYawFromRotation(railRotation);
- LogManager.Debug("[移动到起点] Rail路径已应用完整三维姿态");
+ _currentYaw = Math.Atan2(railLinearTransform.Get(1, 0), railLinearTransform.Get(0, 0));
+ LogManager.Info("[移动到起点] Rail路径已应用完整三维旋转姿态");
return true;
}
}
@@ -924,7 +946,7 @@ namespace NavisworksTransport.Core.Animation
else if (_route.PathType == PathType.Rail)
{
framePosition = RailPathPoseHelper.ResolveBottomPosition(_route, framePosition, p1, p2, objectHeight);
- LogManager.Debug($"[Rail路径] 调整物体位置: 参考点=({p1.X:F2},{p1.Y:F2},{p1.Z:F2})->({p2.X:F2},{p2.Y:F2},{p2.Z:F2}), 物体底面=({framePosition.X:F2},{framePosition.Y:F2},{framePosition.Z:F2}), 安装={_route.RailMountMode}, 对接={_route.RailPayloadAnchorMode}");
+ LogManager.Debug($"[Rail路径] 调整物体位置: 参考点=({p1.X:F2},{p1.Y:F2},{p1.Z:F2})->({p2.X:F2},{p2.Y:F2},{p2.Z:F2}), 物体底面=({framePosition.X:F2},{framePosition.Y:F2},{framePosition.Z:F2}), 安装={_route.RailMountMode}, 对接={(PathRoute.IsTopPayloadAnchorForMountMode(_route.RailMountMode) ? "顶面对接" : "底面对接")}");
}
}
else
@@ -2156,12 +2178,19 @@ namespace NavisworksTransport.Core.Animation
{
try
{
- if (_animatedObject == null)
+ if (_isVirtualObject)
+ {
+ VirtualObjectManager.Instance.MoveVirtualObject(newPosition, newRotation);
+ }
+ else if (_animatedObject != null)
+ {
+ ModelItemTransformHelper.MoveItemToPositionAndRotationWithCurrentScale(_animatedObject, newPosition, newRotation);
+ }
+ else
{
return;
}
- ModelItemTransformHelper.MoveItemToPositionAndRotationWithCurrentScale(_animatedObject, newPosition, newRotation);
_currentPosition = newPosition;
_currentRotation = newRotation;
_hasCurrentRotation = true;
@@ -2173,6 +2202,34 @@ namespace NavisworksTransport.Core.Animation
}
}
+ private void UpdateObjectPosition(Point3D newPosition, Matrix3 newLinearTransform)
+ {
+ try
+ {
+ if (_isVirtualObject)
+ {
+ VirtualObjectManager.Instance.MoveVirtualObject(newPosition, newLinearTransform);
+ }
+ else if (_animatedObject != null)
+ {
+ ModelItemTransformHelper.MoveItemToPositionAndLinearTransformWithCurrentScale(_animatedObject, newPosition, newLinearTransform);
+ }
+ else
+ {
+ return;
+ }
+
+ _currentPosition = newPosition;
+ _currentRotation = Rotation3D.Identity;
+ _hasCurrentRotation = false;
+ _currentYaw = Math.Atan2(newLinearTransform.Get(1, 0), newLinearTransform.Get(0, 0));
+ }
+ catch (Exception ex)
+ {
+ LogManager.Error($"使用完整三维线性姿态更新对象位置失败: {ex.Message}");
+ }
+ }
+
///
/// 获取文档单位信息(用于日志记录)
///
@@ -2413,7 +2470,11 @@ namespace NavisworksTransport.Core.Animation
// 更新对象位置和朝向
var frameData = _animationFrames[_currentFrameIndex];
- if (frameData.HasCustomRotation && _route?.PathType == PathType.Rail)
+ if (frameData.HasCustomLinearTransform && _route?.PathType == PathType.Rail)
+ {
+ UpdateObjectPosition(frameData.Position, frameData.LinearTransform);
+ }
+ else if (frameData.HasCustomRotation && _route?.PathType == PathType.Rail)
{
UpdateObjectPosition(frameData.Position, frameData.Rotation);
}
@@ -3080,7 +3141,11 @@ namespace NavisworksTransport.Core.Animation
actualYaw += correctionRad;
}
- if (frameData.HasCustomRotation && _route?.PathType == PathType.Rail)
+ if (frameData.HasCustomLinearTransform && _route?.PathType == PathType.Rail)
+ {
+ UpdateObjectPosition(frameData.Position, frameData.LinearTransform);
+ }
+ else if (frameData.HasCustomRotation && _route?.PathType == PathType.Rail)
{
UpdateObjectPosition(frameData.Position, frameData.Rotation);
}
diff --git a/src/Core/PathDataManager.cs b/src/Core/PathDataManager.cs
index 31812b6..36df9f2 100644
--- a/src/Core/PathDataManager.cs
+++ b/src/Core/PathDataManager.cs
@@ -49,7 +49,6 @@ namespace NavisworksTransport
public string description { get; set; }
public string pathType { get; set; }
public string railMountMode { get; set; }
- public string railPayloadAnchorMode { get; set; }
public string railPathDefinitionMode { get; set; }
public double railReferenceToAnchorOffset { get; set; }
public double totalLength { get; set; }
@@ -280,7 +279,6 @@ namespace NavisworksTransport
description = route.Description ?? "",
pathType = route.PathType.ToString(),
railMountMode = route.RailMountMode.ToString(),
- railPayloadAnchorMode = route.RailPayloadAnchorMode.ToString(),
railPathDefinitionMode = route.RailPathDefinitionMode.ToString(),
railReferenceToAnchorOffset = Math.Round(route.RailReferenceToAnchorOffset, exportSettings?.Precision ?? 3),
totalLength = Math.Round(route.TotalLength, exportSettings?.Precision ?? 3),
@@ -526,12 +524,6 @@ namespace NavisworksTransport
route.RailMountMode = railMountMode;
}
- if (!string.IsNullOrEmpty(jsonRoute.railPayloadAnchorMode) &&
- Enum.TryParse(jsonRoute.railPayloadAnchorMode, out var railPayloadAnchorMode))
- {
- route.RailPayloadAnchorMode = railPayloadAnchorMode;
- }
-
if (!string.IsNullOrEmpty(jsonRoute.railPathDefinitionMode) &&
Enum.TryParse(jsonRoute.railPathDefinitionMode, out var railPathDefinitionMode))
{
@@ -1398,7 +1390,6 @@ namespace NavisworksTransport
routeElement.SetAttribute("description", route.Description ?? "");
routeElement.SetAttribute("pathType", route.PathType.ToString());
routeElement.SetAttribute("railMountMode", route.RailMountMode.ToString());
- routeElement.SetAttribute("railPayloadAnchorMode", route.RailPayloadAnchorMode.ToString());
routeElement.SetAttribute("railPathDefinitionMode", route.RailPathDefinitionMode.ToString());
routeElement.SetAttribute("railReferenceToAnchorOffset", route.RailReferenceToAnchorOffset.ToString("F3"));
routeElement.SetAttribute("totalLength", route.TotalLength.ToString("F3"));
@@ -1538,11 +1529,6 @@ namespace NavisworksTransport
route.RailMountMode = railMountMode;
}
- if (Enum.TryParse(routeNode.Attributes?["railPayloadAnchorMode"]?.Value, out var railPayloadAnchorMode))
- {
- route.RailPayloadAnchorMode = railPayloadAnchorMode;
- }
-
if (Enum.TryParse(routeNode.Attributes?["railPathDefinitionMode"]?.Value, out var railPathDefinitionMode))
{
route.RailPathDefinitionMode = railPathDefinitionMode;
diff --git a/src/Core/PathDatabase.cs b/src/Core/PathDatabase.cs
index b9d27e2..2688d96 100644
--- a/src/Core/PathDatabase.cs
+++ b/src/Core/PathDatabase.cs
@@ -100,7 +100,6 @@ namespace NavisworksTransport
PathType INTEGER,
LiftHeight REAL,
RailMountMode INTEGER,
- RailPayloadAnchorMode INTEGER,
RailPathDefinitionMode INTEGER,
RailReferenceToAnchorOffset REAL,
CreatedTime DATETIME,
@@ -460,8 +459,8 @@ namespace NavisworksTransport
// 路径长度由 PathRoute.TotalLength 计算属性实时从 Edges/Points 计算
var sql = @"
INSERT OR REPLACE INTO PathRoutes
- (Id, Name, EstimatedTime, TurnRadius, IsCurved, MaxObjectLength, MaxObjectWidth, MaxObjectHeight, SafetyMargin, GridSize, PathType, LiftHeight, RailMountMode, RailPayloadAnchorMode, RailPathDefinitionMode, RailReferenceToAnchorOffset, CreatedTime, LastModified)
- VALUES (@id, @name, @time, @turnRadius, @isCurved, @maxLength, @maxWidth, @maxHeight, @safetyMargin, @gridSize, @pathType, @liftHeightMeters, @railMountMode, @railPayloadAnchorMode, @railPathDefinitionMode, @railReferenceToAnchorOffset, @created, @modified)
+ (Id, Name, EstimatedTime, TurnRadius, IsCurved, MaxObjectLength, MaxObjectWidth, MaxObjectHeight, SafetyMargin, GridSize, PathType, LiftHeight, RailMountMode, RailPathDefinitionMode, RailReferenceToAnchorOffset, CreatedTime, LastModified)
+ VALUES (@id, @name, @time, @turnRadius, @isCurved, @maxLength, @maxWidth, @maxHeight, @safetyMargin, @gridSize, @pathType, @liftHeightMeters, @railMountMode, @railPathDefinitionMode, @railReferenceToAnchorOffset, @created, @modified)
";
using (var cmd = new SQLiteCommand(sql, _connection))
@@ -479,7 +478,6 @@ namespace NavisworksTransport
cmd.Parameters.AddWithValue("@pathType", (int)route.PathType);
cmd.Parameters.AddWithValue("@liftHeightMeters", route.LiftHeight);
cmd.Parameters.AddWithValue("@railMountMode", (int)route.RailMountMode);
- cmd.Parameters.AddWithValue("@railPayloadAnchorMode", (int)route.RailPayloadAnchorMode);
cmd.Parameters.AddWithValue("@railPathDefinitionMode", (int)route.RailPathDefinitionMode);
cmd.Parameters.AddWithValue("@railReferenceToAnchorOffset", route.RailReferenceToAnchorOffset);
cmd.Parameters.AddWithValue("@created", route.CreatedTime);
@@ -1525,7 +1523,6 @@ namespace NavisworksTransport
PathType = (PathType)Convert.ToInt32(reader["PathType"]),
LiftHeight = Convert.ToDouble(reader["LiftHeight"]),
RailMountMode = (RailMountMode)Convert.ToInt32(reader["RailMountMode"]),
- RailPayloadAnchorMode = (RailPayloadAnchorMode)Convert.ToInt32(reader["RailPayloadAnchorMode"]),
RailPathDefinitionMode = (RailPathDefinitionMode)Convert.ToInt32(reader["RailPathDefinitionMode"]),
RailReferenceToAnchorOffset = Convert.ToDouble(reader["RailReferenceToAnchorOffset"]),
CreatedTime = Convert.ToDateTime(reader["CreatedTime"]),
@@ -2709,7 +2706,7 @@ namespace NavisworksTransport
cmd.CommandText = @"
SELECT Id, Name, CreatedTime, LastModified,
TurnRadius, IsCurved, MaxObjectLength, MaxObjectWidth, MaxObjectHeight,
- SafetyMargin, GridSize, PathType, LiftHeight, RailMountMode, RailPayloadAnchorMode, RailPathDefinitionMode, RailReferenceToAnchorOffset
+ SafetyMargin, GridSize, PathType, LiftHeight, RailMountMode, RailPathDefinitionMode, RailReferenceToAnchorOffset
FROM PathRoutes
WHERE Id = @Id";
@@ -2735,7 +2732,6 @@ namespace NavisworksTransport
PathType = (PathType)Convert.ToInt32(reader["PathType"]),
LiftHeight = Convert.ToDouble(reader["LiftHeight"]),
RailMountMode = (RailMountMode)Convert.ToInt32(reader["RailMountMode"]),
- RailPayloadAnchorMode = (RailPayloadAnchorMode)Convert.ToInt32(reader["RailPayloadAnchorMode"]),
RailPathDefinitionMode = (RailPathDefinitionMode)Convert.ToInt32(reader["RailPathDefinitionMode"]),
RailReferenceToAnchorOffset = Convert.ToDouble(reader["RailReferenceToAnchorOffset"])
};
@@ -2774,7 +2770,6 @@ namespace NavisworksTransport
var columnsToAdd = new Dictionary
{
["railmountmode"] = "ALTER TABLE PathRoutes ADD COLUMN RailMountMode INTEGER DEFAULT 0",
- ["railpayloadanchormode"] = "ALTER TABLE PathRoutes ADD COLUMN RailPayloadAnchorMode INTEGER DEFAULT 0",
["railpathdefinitionmode"] = "ALTER TABLE PathRoutes ADD COLUMN RailPathDefinitionMode INTEGER DEFAULT 0",
["railreferencetoanchoroffset"] = "ALTER TABLE PathRoutes ADD COLUMN RailReferenceToAnchorOffset REAL DEFAULT 0"
};
diff --git a/src/Core/PathPlanningModels.cs b/src/Core/PathPlanningModels.cs
index 6c595e5..188f8a2 100644
--- a/src/Core/PathPlanningModels.cs
+++ b/src/Core/PathPlanningModels.cs
@@ -129,22 +129,6 @@ namespace NavisworksTransport
OverRail = 1
}
- ///
- /// Rail 路径中构件与安装头的对接方式
- ///
- public enum RailPayloadAnchorMode
- {
- ///
- /// 构件顶面中心与安装头对接
- ///
- TopCenter = 0,
-
- ///
- /// 构件底面中心与安装头对接
- ///
- BottomCenter = 1
- }
-
///
/// Rail 路径的参考线定义方式
///
@@ -558,6 +542,11 @@ namespace NavisworksTransport
[Serializable]
public class PathRoute
{
+ public static bool IsTopPayloadAnchorForMountMode(RailMountMode railMountMode)
+ {
+ return railMountMode == RailMountMode.UnderRail;
+ }
+
///
/// 路径点集合(控制点)
///
@@ -743,11 +732,6 @@ namespace NavisworksTransport
///
public RailMountMode RailMountMode { get; set; }
- ///
- /// Rail 路径中构件与安装头的对接方式(仅当 PathType == Rail 时有效)
- ///
- public RailPayloadAnchorMode RailPayloadAnchorMode { get; set; }
-
///
/// Rail 路径参考线定义方式(仅当 PathType == Rail 时有效)
///
@@ -799,7 +783,6 @@ namespace NavisworksTransport
TurnRadius = 0.0; // 0.0 表示未设置,实际使用时从配置获取默认值
IsCurved = false;
RailMountMode = RailMountMode.UnderRail;
- RailPayloadAnchorMode = RailPayloadAnchorMode.TopCenter;
RailPathDefinitionMode = RailPathDefinitionMode.LegacySuspensionPoint;
RailReferenceToAnchorOffset = 0.0;
}
@@ -823,7 +806,6 @@ namespace NavisworksTransport
LastModified = DateTime.Now;
Description = string.Empty;
RailMountMode = RailMountMode.UnderRail;
- RailPayloadAnchorMode = RailPayloadAnchorMode.TopCenter;
RailPathDefinitionMode = RailPathDefinitionMode.LegacySuspensionPoint;
RailReferenceToAnchorOffset = 0.0;
}
@@ -1060,7 +1042,6 @@ namespace NavisworksTransport
MaxObjectHeight = MaxObjectHeight,
SafetyMargin = SafetyMargin,
RailMountMode = RailMountMode,
- RailPayloadAnchorMode = RailPayloadAnchorMode,
RailPathDefinitionMode = RailPathDefinitionMode,
RailReferenceToAnchorOffset = RailReferenceToAnchorOffset
};
diff --git a/src/Core/VirtualObjectManager.cs b/src/Core/VirtualObjectManager.cs
index 4152169..f3d505d 100644
--- a/src/Core/VirtualObjectManager.cs
+++ b/src/Core/VirtualObjectManager.cs
@@ -249,6 +249,144 @@ namespace NavisworksTransport.Core
}
}
+ public void MoveVirtualObject(Point3D position, Rotation3D rotation)
+ {
+ if (_virtualObjectModelItem == null) return;
+
+ try
+ {
+ ModelItemTransformHelper.MoveItemToPositionAndRotation(_virtualObjectModelItem, position, rotation);
+ LogManager.Debug("虚拟物体已应用完整三维姿态");
+ }
+ catch (Exception ex)
+ {
+ LogManager.Error($"移动虚拟物体并应用完整三维姿态失败: {ex.Message}");
+ }
+ }
+
+ public void MoveVirtualObject(Point3D position, Matrix3 linearTransform)
+ {
+ if (_virtualObjectModel == null || _virtualObjectModelItem == null) return;
+
+ try
+ {
+ var doc = Application.ActiveDocument;
+ var modelItems = new ModelItemCollection { _virtualObjectModelItem };
+
+ // 虚拟物体的尺寸缩放在 Model 层完成。
+ // 对盒体而言,如果再把三维姿态放在 Item 层,会让非等比缩放和旋转分层,导致姿态看起来发歪。
+ // 因此这里直接在 Model 层一次性应用“缩放 + 旋转 + 平移”。
+ doc.Models.ResetPermanentTransform(modelItems);
+
+ var modelTransform = _virtualObjectModel.Transform;
+ var modelComponents = modelTransform.Factor();
+ var currentScale = modelComponents.Scale;
+ var originalBounds = _virtualObjectModelItem.BoundingBox();
+ var originalCenter = originalBounds.Center;
+ var originalGroundPos = new Point3D(originalBounds.Center.X, originalBounds.Center.Y, originalBounds.Min.Z);
+
+ var effectiveLinear = new Matrix3(
+ linearTransform.Get(0, 0) * currentScale.X, linearTransform.Get(0, 1) * currentScale.Y, linearTransform.Get(0, 2) * currentScale.Z,
+ linearTransform.Get(1, 0) * currentScale.X, linearTransform.Get(1, 1) * currentScale.Y, linearTransform.Get(1, 2) * currentScale.Z,
+ linearTransform.Get(2, 0) * currentScale.X, linearTransform.Get(2, 1) * currentScale.Y, linearTransform.Get(2, 2) * currentScale.Z);
+
+ LogMatrixDiagnostics(linearTransform, effectiveLinear);
+
+ var localGroundPos = originalGroundPos;
+ var transformedGroundPos = new Point3D(
+ effectiveLinear.Get(0, 0) * localGroundPos.X + effectiveLinear.Get(0, 1) * localGroundPos.Y + effectiveLinear.Get(0, 2) * localGroundPos.Z,
+ effectiveLinear.Get(1, 0) * localGroundPos.X + effectiveLinear.Get(1, 1) * localGroundPos.Y + effectiveLinear.Get(1, 2) * localGroundPos.Z,
+ effectiveLinear.Get(2, 0) * localGroundPos.X + effectiveLinear.Get(2, 1) * localGroundPos.Y + effectiveLinear.Get(2, 2) * localGroundPos.Z);
+
+ var translation = new Vector3D(
+ position.X - transformedGroundPos.X,
+ position.Y - transformedGroundPos.Y,
+ position.Z - transformedGroundPos.Z);
+
+ LogManager.Info(
+ $"[虚拟物体姿态] 应用前: 中心=({originalCenter.X:F3},{originalCenter.Y:F3},{originalCenter.Z:F3}), " +
+ $"底面中心=({originalGroundPos.X:F3},{originalGroundPos.Y:F3},{originalGroundPos.Z:F3}), " +
+ $"尺寸=({originalBounds.Size.X:F3},{originalBounds.Size.Y:F3},{originalBounds.Size.Z:F3}), " +
+ $"缩放=({currentScale.X:F3},{currentScale.Y:F3},{currentScale.Z:F3})");
+ LogManager.Info(
+ $"[虚拟物体姿态] 目标: 底面中心=({position.X:F3},{position.Y:F3},{position.Z:F3}), " +
+ $"线性矩阵=[[{linearTransform.Get(0, 0):F4},{linearTransform.Get(0, 1):F4},{linearTransform.Get(0, 2):F4}]," +
+ $"[{linearTransform.Get(1, 0):F4},{linearTransform.Get(1, 1):F4},{linearTransform.Get(1, 2):F4}]," +
+ $"[{linearTransform.Get(2, 0):F4},{linearTransform.Get(2, 1):F4},{linearTransform.Get(2, 2):F4}]]");
+ LogManager.Info(
+ $"[虚拟物体姿态] 组合后: 变换底面中心=({transformedGroundPos.X:F3},{transformedGroundPos.Y:F3},{transformedGroundPos.Z:F3}), " +
+ $"平移=({translation.X:F3},{translation.Y:F3},{translation.Z:F3})");
+
+ var newTransform = new Transform3D(effectiveLinear, translation);
+ doc.Models.SetModelUnitsAndTransform(_virtualObjectModel, _virtualObjectModel.Units, newTransform, false);
+
+ var appliedBounds = _virtualObjectModelItem.BoundingBox();
+ LogManager.Info(
+ $"[虚拟物体姿态] 应用后: 中心=({appliedBounds.Center.X:F3},{appliedBounds.Center.Y:F3},{appliedBounds.Center.Z:F3}), " +
+ $"底面中心=({appliedBounds.Center.X:F3},{appliedBounds.Center.Y:F3},{appliedBounds.Min.Z:F3}), " +
+ $"尺寸=({appliedBounds.Size.X:F3},{appliedBounds.Size.Y:F3},{appliedBounds.Size.Z:F3})");
+ }
+ catch (Exception ex)
+ {
+ LogManager.Error($"移动虚拟物体并应用完整三维线性姿态失败: {ex.Message}");
+ }
+ }
+
+ private void LogMatrixDiagnostics(Matrix3 sourceLinear, Matrix3 effectiveLinear)
+ {
+ if (sourceLinear == null || effectiveLinear == null)
+ {
+ return;
+ }
+
+ var sourceRow0 = new Vector3D(sourceLinear.Get(0, 0), sourceLinear.Get(0, 1), sourceLinear.Get(0, 2));
+ var sourceRow1 = new Vector3D(sourceLinear.Get(1, 0), sourceLinear.Get(1, 1), sourceLinear.Get(1, 2));
+ var sourceRow2 = new Vector3D(sourceLinear.Get(2, 0), sourceLinear.Get(2, 1), sourceLinear.Get(2, 2));
+ var sourceCol0 = new Vector3D(sourceLinear.Get(0, 0), sourceLinear.Get(1, 0), sourceLinear.Get(2, 0));
+ var sourceCol1 = new Vector3D(sourceLinear.Get(0, 1), sourceLinear.Get(1, 1), sourceLinear.Get(2, 1));
+ var sourceCol2 = new Vector3D(sourceLinear.Get(0, 2), sourceLinear.Get(1, 2), sourceLinear.Get(2, 2));
+
+ LogManager.Info(
+ $"[矩阵诊断] 源矩阵行: r0=({sourceRow0.X:F4},{sourceRow0.Y:F4},{sourceRow0.Z:F4}), " +
+ $"r1=({sourceRow1.X:F4},{sourceRow1.Y:F4},{sourceRow1.Z:F4}), " +
+ $"r2=({sourceRow2.X:F4},{sourceRow2.Y:F4},{sourceRow2.Z:F4})");
+ LogManager.Info(
+ $"[矩阵诊断] 源矩阵列: c0=({sourceCol0.X:F4},{sourceCol0.Y:F4},{sourceCol0.Z:F4}), " +
+ $"c1=({sourceCol1.X:F4},{sourceCol1.Y:F4},{sourceCol1.Z:F4}), " +
+ $"c2=({sourceCol2.X:F4},{sourceCol2.Y:F4},{sourceCol2.Z:F4})");
+
+ LogMatrixBasisMapping("[矩阵诊断] 有效矩阵-按当前乘法", effectiveLinear, transpose: false);
+ LogMatrixBasisMapping("[矩阵诊断] 有效矩阵-按转置乘法", effectiveLinear, transpose: true);
+ }
+
+ private void LogMatrixBasisMapping(string prefix, Matrix3 matrix, bool transpose)
+ {
+ var mappedX = TransformDirection(matrix, new Vector3D(1, 0, 0), transpose);
+ var mappedY = TransformDirection(matrix, new Vector3D(0, 1, 0), transpose);
+ var mappedZ = TransformDirection(matrix, new Vector3D(0, 0, 1), transpose);
+
+ LogManager.Info(
+ $"{prefix}: X->({mappedX.X:F4},{mappedX.Y:F4},{mappedX.Z:F4}), " +
+ $"Y->({mappedY.X:F4},{mappedY.Y:F4},{mappedY.Z:F4}), " +
+ $"Z->({mappedZ.X:F4},{mappedZ.Y:F4},{mappedZ.Z:F4})");
+ }
+
+ private Vector3D TransformDirection(Matrix3 matrix, Vector3D direction, bool transpose)
+ {
+ if (!transpose)
+ {
+ return new Vector3D(
+ matrix.Get(0, 0) * direction.X + matrix.Get(0, 1) * direction.Y + matrix.Get(0, 2) * direction.Z,
+ matrix.Get(1, 0) * direction.X + matrix.Get(1, 1) * direction.Y + matrix.Get(1, 2) * direction.Z,
+ matrix.Get(2, 0) * direction.X + matrix.Get(2, 1) * direction.Y + matrix.Get(2, 2) * direction.Z);
+ }
+
+ return new Vector3D(
+ matrix.Get(0, 0) * direction.X + matrix.Get(1, 0) * direction.Y + matrix.Get(2, 0) * direction.Z,
+ matrix.Get(0, 1) * direction.X + matrix.Get(1, 1) * direction.Y + matrix.Get(2, 1) * direction.Z,
+ matrix.Get(0, 2) * direction.X + matrix.Get(1, 2) * direction.Y + matrix.Get(2, 2) * direction.Z);
+ }
+
public void ResetToCADPosition()
{
if (_virtualObjectModelItem == null) return;
diff --git a/src/UI/WPF/ViewModels/PathEditingViewModel.cs b/src/UI/WPF/ViewModels/PathEditingViewModel.cs
index 240c311..877389a 100644
--- a/src/UI/WPF/ViewModels/PathEditingViewModel.cs
+++ b/src/UI/WPF/ViewModels/PathEditingViewModel.cs
@@ -119,7 +119,6 @@ namespace NavisworksTransport.UI.WPF.ViewModels
private PathStrategyOption _selectedPathStrategy;
private ObservableCollection _pathStrategyOptions;
private ObservableCollection> _railMountModeOptions;
- private ObservableCollection> _railPayloadAnchorModeOptions;
private const double DefaultAssemblyReferenceRodLengthInMeters = 20.0;
private static readonly double DefaultAssemblyReferenceRodDiameterInMeters = CalculateDefaultAssemblyReferenceRodDiameterInMeters();
private const double DefaultAssemblyAnchorVerticalOffsetInMeters = 0.0;
@@ -130,7 +129,6 @@ namespace NavisworksTransport.UI.WPF.ViewModels
private double _assemblyAnchorVerticalOffsetInMeters = DefaultAssemblyAnchorVerticalOffsetInMeters;
private string _assemblyStartPointText = "未选择";
private RailMountMode _assemblyMountMode = RailMountMode.UnderRail;
- private RailPayloadAnchorMode _assemblyPayloadAnchorMode = RailPayloadAnchorMode.TopCenter;
private bool _hasAssemblyTerminalObject;
private bool _isSelectingAssemblyStartPoint;
private ModelItem _assemblyTerminalObject;
@@ -236,7 +234,6 @@ namespace NavisworksTransport.UI.WPF.ViewModels
OnPropertyChanged(nameof(CanUsePathLines));
OnPropertyChanged(nameof(IsRailRouteSelected));
OnPropertyChanged(nameof(SelectedRailMountMode));
- OnPropertyChanged(nameof(SelectedRailPayloadAnchorMode));
if (!CanUsePathLines && ShowPathLines)
{
// 吊装和空轨路径不能使用路径线,自动关闭
@@ -301,11 +298,6 @@ namespace NavisworksTransport.UI.WPF.ViewModels
get => _railMountModeOptions;
}
- public ObservableCollection> RailPayloadAnchorModeOptions
- {
- get => _railPayloadAnchorModeOptions;
- }
-
public RailMountMode SelectedRailMountMode
{
get => GetSelectedCoreRoute()?.RailMountMode ?? RailMountMode.UnderRail;
@@ -323,23 +315,6 @@ namespace NavisworksTransport.UI.WPF.ViewModels
}
}
- public RailPayloadAnchorMode SelectedRailPayloadAnchorMode
- {
- get => GetSelectedCoreRoute()?.RailPayloadAnchorMode ?? RailPayloadAnchorMode.TopCenter;
- set
- {
- var coreRoute = GetSelectedCoreRoute();
- if (coreRoute == null || coreRoute.PathType != PathType.Rail || coreRoute.RailPayloadAnchorMode == value)
- {
- return;
- }
-
- coreRoute.RailPayloadAnchorMode = value;
- ApplyRailRouteConfigurationChange(coreRoute, $"对接基准已更新为 {value}");
- OnPropertyChanged(nameof(SelectedRailPayloadAnchorMode));
- }
- }
-
public string AssemblyTerminalObjectName
{
get => _assemblyTerminalObjectName;
@@ -402,19 +377,6 @@ namespace NavisworksTransport.UI.WPF.ViewModels
set => SetProperty(ref _assemblyStartPointText, value);
}
- public RailPayloadAnchorMode AssemblyPayloadAnchorMode
- {
- get => _assemblyPayloadAnchorMode;
- set
- {
- if (SetProperty(ref _assemblyPayloadAnchorMode, value) && HasAssemblyTerminalObject)
- {
- RefreshAssemblyTerminalObjectInfo();
- RefreshAssemblyReferenceRodIfNeeded();
- }
- }
- }
-
public RailMountMode AssemblyMountMode
{
get => _assemblyMountMode;
@@ -423,6 +385,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
if (SetProperty(ref _assemblyMountMode, value) && HasAssemblyTerminalObject)
{
RefreshAssemblyTerminalObjectInfo();
+ RefreshAssemblyReferenceRodIfNeeded();
}
}
}
@@ -1114,21 +1077,6 @@ namespace NavisworksTransport.UI.WPF.ViewModels
}
};
- _railPayloadAnchorModeOptions = new ObservableCollection>
- {
- new RailConfigOption
- {
- Value = RailPayloadAnchorMode.TopCenter,
- DisplayName = "顶面对接",
- Description = "以箱体顶面中心与安装头刚性对接"
- },
- new RailConfigOption
- {
- Value = RailPayloadAnchorMode.BottomCenter,
- DisplayName = "底面对接",
- Description = "以箱体底面中心与安装头刚性对接"
- }
- };
}
private PathRoute GetSelectedCoreRoute()
@@ -1466,7 +1414,6 @@ namespace NavisworksTransport.UI.WPF.ViewModels
Description = $"直线装配路径 - {AssemblyTerminalObjectName}",
PathType = PathType.Rail,
RailMountMode = AssemblyMountMode,
- RailPayloadAnchorMode = AssemblyPayloadAnchorMode,
RailPathDefinitionMode = RailPathDefinitionMode.RailCenterLine
};
@@ -1501,7 +1448,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
endPoint.Z,
AssemblyStartPointText,
AssemblyMountMode == RailMountMode.OverRail ? "轨上安装" : "轨下安装",
- AssemblyPayloadAnchorMode == RailPayloadAnchorMode.TopCenter ? "顶面对接" : "底面对接");
+ GetAssemblyAnchorText());
OnPropertyChanged(nameof(PathRoutes));
LogManager.Info($"[直线装配] 已生成路径: {route.Name},起点=({startPoint.X:F2}, {startPoint.Y:F2}, {startPoint.Z:F2}),终点=({endPoint.X:F2}, {endPoint.Y:F2}, {endPoint.Z:F2})");
@@ -1517,7 +1464,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
var bounds = _assemblyTerminalObject.BoundingBox();
var center = bounds.Center;
Vector3D up = ModelItemTransformHelper.GetUpDirectionFromTransform(_assemblyTerminalObject.Transform);
- double direction = AssemblyPayloadAnchorMode == RailPayloadAnchorMode.TopCenter ? 1.0 : -1.0;
+ double direction = GetAssemblyAnchorDirection();
double verticalOffset = UnitsConverter.ConvertFromMeters(AssemblyAnchorVerticalOffsetInMeters);
return new Point3D(
@@ -1568,7 +1515,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
var bounds = _assemblyTerminalObject.BoundingBox();
Point3D anchorPoint = GetAssemblyTerminalAnchorPoint();
- string anchorText = AssemblyPayloadAnchorMode == RailPayloadAnchorMode.TopCenter ? "顶面对接" : "底面对接";
+ string anchorText = GetAssemblyAnchorText();
string mountText = AssemblyMountMode == RailMountMode.OverRail ? "轨上安装" : "轨下安装";
if (referenceStartPoint != null && referenceEndPoint != null)
@@ -1651,6 +1598,20 @@ namespace NavisworksTransport.UI.WPF.ViewModels
OnPropertyChanged(nameof(AssemblyAnchorVerticalOffsetInMeters));
}
+ private double GetAssemblyAnchorDirection()
+ {
+ return PathRoute.IsTopPayloadAnchorForMountMode(AssemblyMountMode)
+ ? 1.0
+ : -1.0;
+ }
+
+ private string GetAssemblyAnchorText()
+ {
+ return PathRoute.IsTopPayloadAnchorForMountMode(AssemblyMountMode)
+ ? "顶面对接"
+ : "底面对接";
+ }
+
private sealed class AssemblyReferenceLine
{
public AssemblyReferenceLine(Point3D startPoint, Point3D endPoint, Vector3D direction)
diff --git a/src/UI/WPF/Views/PathEditingView.xaml b/src/UI/WPF/Views/PathEditingView.xaml
index e493651..5fdc00d 100644
--- a/src/UI/WPF/Views/PathEditingView.xaml
+++ b/src/UI/WPF/Views/PathEditingView.xaml
@@ -328,29 +328,6 @@ NavisworksTransport 路径编辑页签视图 - 采用与动画控制和分层管
DisplayMemberPath="DisplayName"
ToolTip="选择直线装配路径使用轨上安装还是轨下安装"/>
-
-
-
-
-
-
-
-
-
-
@@ -485,23 +462,6 @@ NavisworksTransport 路径编辑页签视图 - 采用与动画控制和分层管
DisplayMemberPath="DisplayName"
ToolTip="选择安装头位于双轨上方还是下方"/>
-
-
-
-
-
-
-
-
diff --git a/src/Utils/ModelItemTransformHelper.cs b/src/Utils/ModelItemTransformHelper.cs
index f878236..c66ad9d 100644
--- a/src/Utils/ModelItemTransformHelper.cs
+++ b/src/Utils/ModelItemTransformHelper.cs
@@ -361,6 +361,23 @@ namespace NavisworksTransport.Utils
ApplyAbsoluteTransform(item, targetPosition, targetRotation, preserveCurrentScale: true);
}
+ ///
+ /// 将物体移动到指定位置和完整三维线性姿态(先回到CAD原始位置,再移动)。
+ /// targetLinear 的列向量分别表示物体局部 X/Y/Z 轴在世界坐标中的方向。
+ ///
+ public static void MoveItemToPositionAndLinearTransform(ModelItem item, Point3D targetPosition, Matrix3 targetLinear)
+ {
+ ApplyAbsoluteLinearTransform(item, targetPosition, targetLinear, preserveCurrentScale: false);
+ }
+
+ ///
+ /// 将物体移动到指定位置和完整三维线性姿态,同时保留当前缩放。
+ ///
+ public static void MoveItemToPositionAndLinearTransformWithCurrentScale(ModelItem item, Point3D targetPosition, Matrix3 targetLinear)
+ {
+ ApplyAbsoluteLinearTransform(item, targetPosition, targetLinear, preserveCurrentScale: true);
+ }
+
///
/// 将物体移动到指定中心点和完整三维朝向,同时保留当前缩放。
/// 适用于参考杆、辅助几何等以几何中心作为定位基准的场景。
@@ -577,6 +594,67 @@ namespace NavisworksTransport.Utils
doc.Models.OverridePermanentTransform(modelItems, transform, false);
}
+ private static void ApplyAbsoluteLinearTransform(ModelItem item, Point3D targetPosition, Matrix3 targetLinear, bool preserveCurrentScale)
+ {
+ if (item == null)
+ {
+ throw new ArgumentNullException(nameof(item));
+ }
+
+ if (targetLinear == null)
+ {
+ throw new ArgumentNullException(nameof(targetLinear));
+ }
+
+ var doc = Application.ActiveDocument;
+ var modelItems = new ModelItemCollection { item };
+ Vector3D currentScale = new Vector3D(1, 1, 1);
+
+ if (preserveCurrentScale)
+ {
+ var currentComponents = item.Transform.Factor();
+ currentScale = currentComponents.Scale;
+ }
+
+ doc.Models.ResetPermanentTransform(modelItems);
+
+ var originalBounds = item.BoundingBox();
+ var originalGroundPos = new Point3D(
+ originalBounds.Center.X,
+ originalBounds.Center.Y,
+ originalBounds.Min.Z
+ );
+
+ Matrix3 effectiveLinear = preserveCurrentScale
+ ? BuildScaledLinearMatrix(targetLinear, currentScale)
+ : targetLinear;
+
+ Point3D transformedGroundPos = TransformPoint(effectiveLinear, originalGroundPos);
+ var translation = new Vector3D(
+ targetPosition.X - transformedGroundPos.X,
+ targetPosition.Y - transformedGroundPos.Y,
+ targetPosition.Z - transformedGroundPos.Z
+ );
+
+ doc.Models.OverridePermanentTransform(modelItems, new Transform3D(effectiveLinear, translation), false);
+ }
+
+ private static Matrix3 BuildScaledLinearMatrix(Matrix3 linear, Vector3D scale)
+ {
+ return new Matrix3(
+ linear.Get(0, 0) * scale.X, linear.Get(0, 1) * scale.Y, linear.Get(0, 2) * scale.Z,
+ linear.Get(1, 0) * scale.X, linear.Get(1, 1) * scale.Y, linear.Get(1, 2) * scale.Z,
+ linear.Get(2, 0) * scale.X, linear.Get(2, 1) * scale.Y, linear.Get(2, 2) * scale.Z);
+ }
+
+ private static Point3D TransformPoint(Matrix3 linear, Point3D point)
+ {
+ return new Point3D(
+ linear.Get(0, 0) * point.X + linear.Get(0, 1) * point.Y + linear.Get(0, 2) * point.Z,
+ linear.Get(1, 0) * point.X + linear.Get(1, 1) * point.Y + linear.Get(1, 2) * point.Z,
+ linear.Get(2, 0) * point.X + linear.Get(2, 1) * point.Y + linear.Get(2, 2) * point.Z);
+ }
+
///
/// 将物体移动到指定位置和朝向,同时保持缩放比例
/// 专为虚拟物体设计,避免缩放被覆盖
diff --git a/src/Utils/RailPathPoseHelper.cs b/src/Utils/RailPathPoseHelper.cs
index 24e5c3d..9bd81be 100644
--- a/src/Utils/RailPathPoseHelper.cs
+++ b/src/Utils/RailPathPoseHelper.cs
@@ -11,6 +11,7 @@ namespace NavisworksTransport.Utils
public static class RailPathPoseHelper
{
private const double TangentEpsilon = 1e-9;
+ private static bool _rotationConstructorConventionLogged;
///
/// 获取 Rail 参考点到构件对接点的有符号偏移(模型单位)。
@@ -45,7 +46,7 @@ namespace NavisworksTransport.Utils
return 0.0;
}
- if (route.RailPayloadAnchorMode == RailPayloadAnchorMode.TopCenter)
+ if (PathRoute.IsTopPayloadAnchorForMountMode(route.RailMountMode))
{
return anchorOffset - objectHeight;
}
@@ -55,7 +56,8 @@ namespace NavisworksTransport.Utils
///
/// 计算通行空间中心相对于 Rail 参考点的 Z 偏移(模型单位)。
- /// 对于当前渲染实现,top-center 对接对应正半高,bottom-center 对接对应负半高。
+ /// 顶面对接时,通行空间中心位于对接点下方半高;
+ /// 底面对接时,通行空间中心位于对接点上方半高。
///
public static double GetObjectSpaceCenterZOffset(PathRoute route, double objectSpaceHeight)
{
@@ -66,12 +68,12 @@ namespace NavisworksTransport.Utils
return 0.0;
}
- if (route.RailPayloadAnchorMode == RailPayloadAnchorMode.TopCenter)
+ if (PathRoute.IsTopPayloadAnchorForMountMode(route.RailMountMode))
{
- return anchorOffset + objectSpaceHeight / 2.0;
+ return anchorOffset - objectSpaceHeight / 2.0;
}
- return anchorOffset - objectSpaceHeight / 2.0;
+ return anchorOffset + objectSpaceHeight / 2.0;
}
///
@@ -149,16 +151,17 @@ namespace NavisworksTransport.Utils
}
///
- /// 根据轨道路径局部坐标系创建 Rail 构件姿态。
- /// 约定:局部 X 轴沿轨道切向,局部 Z 轴沿轨道法向,局部 Y 轴为横向。
+ /// 根据轨道路径切向和法向创建 Rail 构件完整三维姿态。
+ /// 约定:构件本地 X 轴为前进方向,本地 Z 轴为上方向。
+ /// 返回值是可直接用于 Transform3D 的线性矩阵。
///
- public static bool TryCreateRailRotation(
+ public static bool TryCreateRailLinearTransform(
Point3D previousPoint,
Point3D currentPoint,
Point3D nextPoint,
- out Rotation3D rotation)
+ out Matrix3 linearTransform)
{
- rotation = Rotation3D.Identity;
+ linearTransform = null;
var tangent = ResolveTangent(previousPoint, currentPoint, nextPoint);
double tangentLengthSquared = tangent.X * tangent.X + tangent.Y * tangent.Y + tangent.Z * tangent.Z;
@@ -167,29 +170,179 @@ namespace NavisworksTransport.Utils
return false;
}
- var normal = ResolveRailNormal(previousPoint, currentPoint, nextPoint);
- var lateral = Cross(normal, tangent);
+ tangent = Normalize(tangent);
+ var normal = Normalize(ResolveRailNormal(previousPoint, currentPoint, nextPoint));
+ var lateral = Normalize(Cross(normal, tangent));
+
double lateralLengthSquared = lateral.X * lateral.X + lateral.Y * lateral.Y + lateral.Z * lateral.Z;
if (lateralLengthSquared < TangentEpsilon)
+ {
+ Vector3D fallbackUp = Math.Abs(tangent.Z) < 0.9
+ ? new Vector3D(0, 0, 1)
+ : new Vector3D(0, 1, 0);
+ lateral = Normalize(Cross(fallbackUp, tangent));
+ lateralLengthSquared = lateral.X * lateral.X + lateral.Y * lateral.Y + lateral.Z * lateral.Z;
+ if (lateralLengthSquared < TangentEpsilon)
+ {
+ return false;
+ }
+ }
+
+ normal = Normalize(Cross(tangent, lateral));
+
+ // Navisworks 在 Transform3D(Matrix3, ...) 中按列读取局部基向量:
+ // 第 1 列 = 本地 X 轴在世界中的方向
+ // 第 2 列 = 本地 Y 轴在世界中的方向
+ // 第 3 列 = 本地 Z 轴在世界中的方向
+ linearTransform = new Matrix3(
+ tangent.X, lateral.X, normal.X,
+ tangent.Y, lateral.Y, normal.Y,
+ tangent.Z, lateral.Z, normal.Z);
+
+ LogManager.Info(
+ $"[Rail姿态] 切向=({tangent.X:F4},{tangent.Y:F4},{tangent.Z:F4}), " +
+ $"侧向=({lateral.X:F4},{lateral.Y:F4},{lateral.Z:F4}), " +
+ $"法向=({normal.X:F4},{normal.Y:F4},{normal.Z:F4})");
+ return true;
+ }
+
+ ///
+ /// 根据轨道路径切向和法向创建 Rail 构件完整三维旋转。
+ /// 约定:构件本地 X 轴为前进方向,本地 Z 轴为上方向。
+ ///
+ public static bool TryCreateRailRotation(
+ Point3D previousPoint,
+ Point3D currentPoint,
+ Point3D nextPoint,
+ out Rotation3D rotation)
+ {
+ rotation = Rotation3D.Identity;
+ LogRotationConstructorConventionOnce();
+
+ if (!TryCreateRailLinearTransform(previousPoint, currentPoint, nextPoint, out var linearTransform))
{
return false;
}
- tangent = Normalize(tangent);
- normal = Normalize(normal);
- lateral = Normalize(lateral);
+ rotation = CreateRotationFromLinearTransform(linearTransform);
+ var verifiedLinear = new Transform3D(rotation).Linear;
- rotation = new Rotation3D(
- new UnitVector3D(tangent.X, tangent.Y, tangent.Z),
- new UnitVector3D(lateral.X, lateral.Y, lateral.Z),
- new UnitVector3D(normal.X, normal.Y, normal.Z));
+ LogManager.Info(
+ $"[Rail旋转验证] X=({verifiedLinear.Get(0, 0):F4},{verifiedLinear.Get(1, 0):F4},{verifiedLinear.Get(2, 0):F4}), " +
+ $"Y=({verifiedLinear.Get(0, 1):F4},{verifiedLinear.Get(1, 1):F4},{verifiedLinear.Get(2, 1):F4}), " +
+ $"Z=({verifiedLinear.Get(0, 2):F4},{verifiedLinear.Get(1, 2):F4},{verifiedLinear.Get(2, 2):F4})");
return true;
}
+ private static Rotation3D CreateRotationFromLinearTransform(Matrix3 linearTransform)
+ {
+ double m00 = linearTransform.Get(0, 0);
+ double m01 = linearTransform.Get(0, 1);
+ double m02 = linearTransform.Get(0, 2);
+ double m10 = linearTransform.Get(1, 0);
+ double m11 = linearTransform.Get(1, 1);
+ double m12 = linearTransform.Get(1, 2);
+ double m20 = linearTransform.Get(2, 0);
+ double m21 = linearTransform.Get(2, 1);
+ double m22 = linearTransform.Get(2, 2);
+
+ double qx;
+ double qy;
+ double qz;
+ double qw;
+
+ double trace = m00 + m11 + m22;
+ if (trace > 0.0)
+ {
+ double s = Math.Sqrt(trace + 1.0) * 2.0;
+ qw = 0.25 * s;
+ qx = (m21 - m12) / s;
+ qy = (m02 - m20) / s;
+ qz = (m10 - m01) / s;
+ }
+ else if (m00 > m11 && m00 > m22)
+ {
+ double s = Math.Sqrt(1.0 + m00 - m11 - m22) * 2.0;
+ qw = (m21 - m12) / s;
+ qx = 0.25 * s;
+ qy = (m01 + m10) / s;
+ qz = (m02 + m20) / s;
+ }
+ else if (m11 > m22)
+ {
+ double s = Math.Sqrt(1.0 + m11 - m00 - m22) * 2.0;
+ qw = (m02 - m20) / s;
+ qx = (m01 + m10) / s;
+ qy = 0.25 * s;
+ qz = (m12 + m21) / s;
+ }
+ else
+ {
+ double s = Math.Sqrt(1.0 + m22 - m00 - m11) * 2.0;
+ qw = (m10 - m01) / s;
+ qx = (m02 + m20) / s;
+ qy = (m12 + m21) / s;
+ qz = 0.25 * s;
+ }
+
+ var rotation = new Rotation3D(qx, qy, qz, qw);
+ double error = CalculateLinearError(new Transform3D(rotation).Linear, linearTransform);
+ LogManager.Info(
+ $"[Rail旋转验证] quaternion候选=({qx:F4},{qy:F4},{qz:F4},{qw:F4}), 误差={error:F6}");
+ return rotation;
+ }
+
+ private static void LogRotationConstructorConventionOnce()
+ {
+ if (_rotationConstructorConventionLogged)
+ {
+ return;
+ }
+
+ _rotationConstructorConventionLogged = true;
+
+ LogKnownAxisRotation("X90", new UnitVector3D(1, 0, 0), Math.PI / 2.0);
+ LogKnownAxisRotation("Y90", new UnitVector3D(0, 1, 0), Math.PI / 2.0);
+ LogKnownAxisRotation("Z90", new UnitVector3D(0, 0, 1), Math.PI / 2.0);
+ }
+
+ private static void LogKnownAxisRotation(string name, UnitVector3D axis, double angle)
+ {
+ Rotation3D axisAngleRotation = new Rotation3D(axis, angle);
+ Rotation3D reconstructed = new Rotation3D(
+ axisAngleRotation.A,
+ axisAngleRotation.B,
+ axisAngleRotation.C,
+ axisAngleRotation.D);
+ Matrix3 linear = new Transform3D(reconstructed).Linear;
+
+ LogManager.Info(
+ $"[Rotation构造测试] {name}: " +
+ $"A={axisAngleRotation.A:F6}, B={axisAngleRotation.B:F6}, C={axisAngleRotation.C:F6}, D={axisAngleRotation.D:F6}, " +
+ $"LinearX=({linear.Get(0, 0):F4},{linear.Get(1, 0):F4},{linear.Get(2, 0):F4}), " +
+ $"LinearY=({linear.Get(0, 1):F4},{linear.Get(1, 1):F4},{linear.Get(2, 1):F4}), " +
+ $"LinearZ=({linear.Get(0, 2):F4},{linear.Get(1, 2):F4},{linear.Get(2, 2):F4})");
+ }
+
+ private static double CalculateLinearError(Matrix3 actual, Matrix3 expected)
+ {
+ double error = 0.0;
+ for (int row = 0; row < 3; row++)
+ {
+ for (int column = 0; column < 3; column++)
+ {
+ double delta = actual.Get(row, column) - expected.Get(row, column);
+ error += delta * delta;
+ }
+ }
+
+ return Math.Sqrt(error);
+ }
+
private static double GetBottomOffsetMagnitude(PathRoute route, double objectHeight)
{
double anchorOffset = GetAnchorOffset(route);
- return route.RailPayloadAnchorMode == RailPayloadAnchorMode.TopCenter
+ return PathRoute.IsTopPayloadAnchorForMountMode(route.RailMountMode)
? anchorOffset - objectHeight
: anchorOffset;
}
@@ -197,9 +350,9 @@ namespace NavisworksTransport.Utils
private static double GetObjectSpaceCenterOffsetMagnitude(PathRoute route, double objectSpaceHeight)
{
double anchorOffset = GetAnchorOffset(route);
- return route.RailPayloadAnchorMode == RailPayloadAnchorMode.TopCenter
- ? anchorOffset + objectSpaceHeight / 2.0
- : anchorOffset - objectSpaceHeight / 2.0;
+ return PathRoute.IsTopPayloadAnchorForMountMode(route.RailMountMode)
+ ? anchorOffset - objectSpaceHeight / 2.0
+ : anchorOffset + objectSpaceHeight / 2.0;
}
private static Vector3D ResolveRailNormal(Point3D previousPoint, Point3D currentPoint, Point3D nextPoint)
From 5e8c50e04347766b106252301ffd409ee30c7cb6 Mon Sep 17 00:00:00 2001
From: tian <11429339@qq.com>
Date: Fri, 20 Mar 2026 12:50:08 +0800
Subject: [PATCH 05/87] Stabilize rail 3D animation transforms
---
deploy-plugin.bat | 60 ++++-
doc/design/2026/NavisworksAPI使用方法.md | 45 ++++
src/Core/Animation/PathAnimationManager.cs | 132 ++++------
src/Core/VirtualObjectManager.cs | 123 ---------
src/Utils/CollisionSceneHelper.cs | 24 +-
src/Utils/ModelItemTransformHelper.cs | 292 ++++++++++++++++-----
6 files changed, 393 insertions(+), 283 deletions(-)
diff --git a/deploy-plugin.bat b/deploy-plugin.bat
index 96fa442..0d5d775 100644
--- a/deploy-plugin.bat
+++ b/deploy-plugin.bat
@@ -1,26 +1,70 @@
@echo off
setlocal
+set "TARGET_DIR=%PROGRAMDATA%\Autodesk\Navisworks Manage 2026\plugins\TransportPlugin"
+set "BUILD_DIR=bin\x64\Release"
+set "MAX_WAIT_SECONDS=15"
+set "COPY_RETRY_COUNT=5"
+
:: Stop Navisworks to release locked plugin DLLs.
taskkill /F /IM Roamer.exe 2>nul
if %errorlevel% == 0 (
echo Navisworks process terminated.
- timeout /t 3 /nobreak >nul
)
-set "TARGET_DIR=%PROGRAMDATA%\Autodesk\Navisworks Manage 2026\plugins\TransportPlugin"
-set "BUILD_DIR=bin\x64\Release"
+:: Wait until Roamer.exe fully exits. Fixed delay alone is not reliable.
+set /a WAIT_COUNT=0
+:WAIT_FOR_ROAMER_EXIT
+tasklist /FI "IMAGENAME eq Roamer.exe" | find /I "Roamer.exe" >nul
+if errorlevel 1 goto ROAMER_EXITED
+if %WAIT_COUNT% GEQ %MAX_WAIT_SECONDS% (
+ echo Warning: Roamer.exe still appears to be running after %MAX_WAIT_SECONDS% seconds.
+ goto ROAMER_EXITED
+)
+timeout /t 1 /nobreak >nul
+set /a WAIT_COUNT+=1
+goto WAIT_FOR_ROAMER_EXIT
+
+:ROAMER_EXITED
if not exist "%TARGET_DIR%" mkdir "%TARGET_DIR%"
+set /a COPY_ATTEMPT=0
+:COPY_DLLS
copy "%BUILD_DIR%\*.dll" "%TARGET_DIR%\" >nul
+if not errorlevel 1 goto DLLS_DEPLOYED
+set /a COPY_ATTEMPT+=1
+if %COPY_ATTEMPT% GEQ %COPY_RETRY_COUNT% (
+ echo Failed to deploy plugin DLLs after %COPY_RETRY_COUNT% attempts.
+ exit /b 1
+)
+timeout /t 1 /nobreak >nul
+goto COPY_DLLS
+
+:DLLS_DEPLOYED
echo Plugin DLLs deployed.
:: Copy bundled resources.
-if exist "%BUILD_DIR%\resources" (
- if not exist "%TARGET_DIR%\resources" mkdir "%TARGET_DIR%\resources"
- xcopy "%BUILD_DIR%\resources\*" "%TARGET_DIR%\resources\" /E /I /Y /Q >nul
- echo Resources folder deployed.
-)
+if exist "%BUILD_DIR%\resources" goto PREPARE_RESOURCES
+goto DEPLOY_DONE
+:PREPARE_RESOURCES
+if not exist "%TARGET_DIR%\resources" mkdir "%TARGET_DIR%\resources"
+set /a RESOURCE_COPY_ATTEMPT=0
+
+:COPY_RESOURCES
+xcopy "%BUILD_DIR%\resources\*" "%TARGET_DIR%\resources\" /E /I /Y /Q >nul
+if not errorlevel 1 goto RESOURCES_DEPLOYED
+set /a RESOURCE_COPY_ATTEMPT+=1
+if %RESOURCE_COPY_ATTEMPT% GEQ %COPY_RETRY_COUNT% (
+ echo Failed to deploy resources after %COPY_RETRY_COUNT% attempts.
+ exit /b 1
+)
+timeout /t 1 /nobreak >nul
+goto COPY_RESOURCES
+
+:RESOURCES_DEPLOYED
+echo Resources folder deployed.
+
+:DEPLOY_DONE
echo Plugin deployed successfully!
diff --git a/doc/design/2026/NavisworksAPI使用方法.md b/doc/design/2026/NavisworksAPI使用方法.md
index 930dc03..39f03e4 100644
--- a/doc/design/2026/NavisworksAPI使用方法.md
+++ b/doc/design/2026/NavisworksAPI使用方法.md
@@ -880,6 +880,51 @@ doc.Models.OverridePermanentTransform(modelItems, transform, false);
- 资源本地长度轴约定(例如是否沿本地 `+X`)
- 当前实际显示位置,优先看 `BoundingBox()`,不要只看 `ModelItem.Transform`
+#### 11.7.2.5 真实模型做三维旋转时,推荐使用“先移到原点,再旋转,再移到目标点”(2026-03-20 新增,重要)
+
+在 Rail 路径真实模型调试中,一个非常关键的经验是:
+
+- **不要**只在公式层面“猜一个补偿平移”
+- 对真实模型做完整三维姿态时,更稳的做法是显式拆成三步:
+
+```csharp
+// 1. 把当前参考点移到原点
+doc.Models.OverridePermanentTransform(
+ modelItems,
+ Transform3D.CreateTranslation(new Vector3D(-currentPoint.X, -currentPoint.Y, -currentPoint.Z)),
+ false);
+
+// 2. 在原点执行三维旋转
+var rotationComponents = Transform3D.CreateTranslation(new Vector3D(0, 0, 0)).Factor();
+rotationComponents.Rotation = deltaRotation;
+doc.Models.OverridePermanentTransform(modelItems, rotationComponents.Combine(), false);
+
+// 3. 再把参考点移到目标位置
+doc.Models.OverridePermanentTransform(
+ modelItems,
+ Transform3D.CreateTranslation(new Vector3D(targetPoint.X, targetPoint.Y, targetPoint.Z)),
+ false);
+```
+
+**适用场景:**
+
+- 真实模型不是“原点资源”,本身已经位于世界坐标某个远离原点的位置
+- 需要做完整三维旋转(不只是 yaw)
+- 目标是让物体绕某个固定参考点“自转”,而不是带着原始世界位置绕原点“公转”
+
+**核心理解:**
+
+- Navisworks 的旋转仍然是绕世界原点
+- 所以如果想绕“某个固定参考点”旋转,就先把这个参考点移到原点
+- 这个固定参考点不一定必须是中心,也可以是底面中心、安装点或其他局部固定点
+- 但在复杂三维姿态下,**几何中心通常是最稳定、最容易验证的参考点**
+
+**这条原则解决的问题:**
+
+- 虚拟物体因为本来就接近“原点资源”,旋转通常更容易成功
+- 真实模型自带原始世界位置时,如果直接旋转,很容易出现“方向对了,但位置跑很远”
+- 将参考点显式移到原点后再旋转,可以显著降低三维平移补偿出错的概率
+
#### 11.7.3 正确实现"绕物体中心旋转"
**解决方案:手动计算旋转导致的位置偏移并补偿**
diff --git a/src/Core/Animation/PathAnimationManager.cs b/src/Core/Animation/PathAnimationManager.cs
index 8b0e72c..e7b4c7b 100644
--- a/src/Core/Animation/PathAnimationManager.cs
+++ b/src/Core/Animation/PathAnimationManager.cs
@@ -74,8 +74,6 @@ namespace NavisworksTransport.Core.Animation
public double YawRadians { get; set; } // 绕Z轴的偏航角(弧度)
public Rotation3D Rotation { get; set; } // 可选的完整三维姿态
public bool HasCustomRotation { get; set; } // 是否使用完整三维姿态
- public Matrix3 LinearTransform { get; set; } // 可选的完整三维线性姿态
- public bool HasCustomLinearTransform { get; set; }// 是否使用完整三维线性姿态
public List Collisions { get; set; } // 该帧的碰撞结果
public bool HasCollision => Collisions?.Count > 0;
@@ -85,8 +83,6 @@ namespace NavisworksTransport.Core.Animation
YawRadians = 0.0;
Rotation = Rotation3D.Identity;
HasCustomRotation = false;
- LinearTransform = null;
- HasCustomLinearTransform = false;
}
}
@@ -425,15 +421,11 @@ namespace NavisworksTransport.Core.Animation
// 2. 重置内部跟踪变量(同步到原始状态)
// 注意:ResetPermanentTransform 后物体的 Transform 属性会自动变回原始值
_currentYaw = ModelItemTransformHelper.GetYawFromTransform(objectToRestore.Transform);
- _currentRotation = Rotation3D.Identity;
- _hasCurrentRotation = false;
+ _currentRotation = objectToRestore.Transform.Factor().Rotation;
+ _hasCurrentRotation = true;
var originalBoundingBox = objectToRestore.BoundingBox();
- _currentPosition = new Point3D(
- originalBoundingBox.Center.X,
- originalBoundingBox.Center.Y,
- originalBoundingBox.Min.Z
- );
+ _currentPosition = GetTrackedObjectPosition(objectToRestore, _route?.PathType == PathType.Rail && !isVirtual);
string objectName = isVirtual ? "虚拟物体" : objectToRestore.DisplayName;
LogManager.Info($"[归位] {objectName} 已彻底恢复到原始位置, yaw={_currentYaw:F3}");
@@ -596,10 +588,12 @@ namespace NavisworksTransport.Core.Animation
_animatedObject = animatedObject;
_originalTransform = animatedObject.Transform;
_originalCenter = animatedObject.BoundingBox().Center;
- _currentPosition = new Point3D(_originalCenter.X, _originalCenter.Y, animatedObject.BoundingBox().Min.Z);
+ _currentPosition = GetTrackedObjectPosition(animatedObject, _route?.PathType == PathType.Rail && !_isVirtualObject);
// 统一逻辑:从当前 Transform 中提取实际朝向(无论虚拟物体还是普通物体)
_currentYaw = ModelItemTransformHelper.GetYawFromTransform(_originalTransform);
+ _currentRotation = _originalTransform.Factor().Rotation;
+ _hasCurrentRotation = true;
}
if (pathPoints != null)
@@ -668,23 +662,7 @@ namespace NavisworksTransport.Core.Animation
$"Y=({railLinearTransform.Get(0, 1):F4},{railLinearTransform.Get(1, 1):F4},{railLinearTransform.Get(2, 1):F4}), " +
$"Z=({railLinearTransform.Get(0, 2):F4},{railLinearTransform.Get(1, 2):F4},{railLinearTransform.Get(2, 2):F4})");
- if (_isVirtualObject)
- {
- VirtualObjectManager.Instance.MoveVirtualObject(startPosition, railRotation);
- }
- else if (_animatedObject != null)
- {
- ModelItemTransformHelper.MoveItemToPositionAndRotationWithCurrentScale(_animatedObject, startPosition, railRotation);
- }
- else
- {
- return false;
- }
-
- _currentPosition = startPosition;
- _currentRotation = railRotation;
- _hasCurrentRotation = true;
- _currentYaw = Math.Atan2(railLinearTransform.Get(1, 0), railLinearTransform.Get(0, 0));
+ UpdateObjectPosition(startPosition, railRotation);
LogManager.Info("[移动到起点] Rail路径已应用完整三维旋转姿态");
return true;
}
@@ -2184,7 +2162,30 @@ namespace NavisworksTransport.Core.Animation
}
else if (_animatedObject != null)
{
- ModelItemTransformHelper.MoveItemToPositionAndRotationWithCurrentScale(_animatedObject, newPosition, newRotation);
+ var currentRotation = _hasCurrentRotation
+ ? _currentRotation
+ : _animatedObject.Transform.Factor().Rotation;
+ var currentLinear = new Transform3D(currentRotation).Linear;
+ var targetLinear = new Transform3D(newRotation).Linear;
+ LogManager.Info(
+ $"[动画姿态入口] {_animatedObject.DisplayName} 当前点=({_currentPosition.X:F3},{_currentPosition.Y:F3},{_currentPosition.Z:F3}), " +
+ $"目标点=({newPosition.X:F3},{newPosition.Y:F3},{newPosition.Z:F3})");
+ LogManager.Info(
+ $"[动画姿态入口] {_animatedObject.DisplayName} 当前姿态: " +
+ $"X=({currentLinear.Get(0, 0):F4},{currentLinear.Get(1, 0):F4},{currentLinear.Get(2, 0):F4}), " +
+ $"Y=({currentLinear.Get(0, 1):F4},{currentLinear.Get(1, 1):F4},{currentLinear.Get(2, 1):F4}), " +
+ $"Z=({currentLinear.Get(0, 2):F4},{currentLinear.Get(1, 2):F4},{currentLinear.Get(2, 2):F4})");
+ LogManager.Info(
+ $"[动画姿态入口] {_animatedObject.DisplayName} 目标姿态: " +
+ $"X=({targetLinear.Get(0, 0):F4},{targetLinear.Get(1, 0):F4},{targetLinear.Get(2, 0):F4}), " +
+ $"Y=({targetLinear.Get(0, 1):F4},{targetLinear.Get(1, 1):F4},{targetLinear.Get(2, 1):F4}), " +
+ $"Z=({targetLinear.Get(0, 2):F4},{targetLinear.Get(1, 2):F4},{targetLinear.Get(2, 2):F4})");
+ ModelItemTransformHelper.MoveItemIncrementallyToPositionAndRotation(
+ _animatedObject,
+ _currentPosition,
+ currentRotation,
+ newPosition,
+ newRotation);
}
else
{
@@ -2202,34 +2203,6 @@ namespace NavisworksTransport.Core.Animation
}
}
- private void UpdateObjectPosition(Point3D newPosition, Matrix3 newLinearTransform)
- {
- try
- {
- if (_isVirtualObject)
- {
- VirtualObjectManager.Instance.MoveVirtualObject(newPosition, newLinearTransform);
- }
- else if (_animatedObject != null)
- {
- ModelItemTransformHelper.MoveItemToPositionAndLinearTransformWithCurrentScale(_animatedObject, newPosition, newLinearTransform);
- }
- else
- {
- return;
- }
-
- _currentPosition = newPosition;
- _currentRotation = Rotation3D.Identity;
- _hasCurrentRotation = false;
- _currentYaw = Math.Atan2(newLinearTransform.Get(1, 0), newLinearTransform.Get(0, 0));
- }
- catch (Exception ex)
- {
- LogManager.Error($"使用完整三维线性姿态更新对象位置失败: {ex.Message}");
- }
- }
-
///
/// 获取文档单位信息(用于日志记录)
///
@@ -2319,12 +2292,7 @@ namespace NavisworksTransport.Core.Animation
return (null, 0);
}
- var bbox = obj.BoundingBox();
- var position = new Point3D(
- bbox.Center.X,
- bbox.Center.Y,
- bbox.Min.Z
- );
+ var position = _currentPosition;
// 🔥 关键:使用 _currentYaw(实际当前朝向),而不是 Transform 的 CAD 原始朝向
// Transform 返回的是 CAD 设计时的原始朝向,不是动画后的实际朝向
return (position, _currentYaw);
@@ -2380,8 +2348,7 @@ namespace NavisworksTransport.Core.Animation
var originalAnimatedObject = _animatedObject;
_animatedObject = obj;
- var bbox = obj.BoundingBox();
- _currentPosition = new Point3D(bbox.Center.X, bbox.Center.Y, bbox.Min.Z);
+ _currentPosition = _savedObjectPosition;
_currentYaw = _savedObjectYaw;
_currentRotation = _savedObjectRotation;
_hasCurrentRotation = _savedObjectHasCustomRotation;
@@ -2470,11 +2437,7 @@ namespace NavisworksTransport.Core.Animation
// 更新对象位置和朝向
var frameData = _animationFrames[_currentFrameIndex];
- if (frameData.HasCustomLinearTransform && _route?.PathType == PathType.Rail)
- {
- UpdateObjectPosition(frameData.Position, frameData.LinearTransform);
- }
- else if (frameData.HasCustomRotation && _route?.PathType == PathType.Rail)
+ if (frameData.HasCustomRotation && _route?.PathType == PathType.Rail)
{
UpdateObjectPosition(frameData.Position, frameData.Rotation);
}
@@ -2858,6 +2821,8 @@ namespace NavisworksTransport.Core.Animation
/// 用于保存物体当前状态(因为Transform返回的是CAD原始值)
///
public double CurrentYaw => _currentYaw;
+ public Rotation3D CurrentRotation => _currentRotation;
+ public bool HasCurrentRotation => _hasCurrentRotation;
///
/// 获取当前动画物体高度(模型单位)
@@ -2962,7 +2927,7 @@ namespace NavisworksTransport.Core.Animation
{
_originalTransform = animatedObject.Transform;
_originalCenter = animatedObject.BoundingBox().Center;
- _currentPosition = new Point3D(_originalCenter.X, _originalCenter.Y, animatedObject.BoundingBox().Min.Z);
+ _currentPosition = GetTrackedObjectPosition(animatedObject, _route?.PathType == PathType.Rail && !_isVirtualObject);
// 保持当前的 _currentYaw(因为物体可能已经被 MoveObjectToPathStart 旋转)
// 不要从 Transform 中提取,因为 Transform 返回的是原始值,不是当前值
@@ -2979,6 +2944,22 @@ namespace NavisworksTransport.Core.Animation
LogManager.Info($"[CreateAnimation] 动画已创建,状态设置为Ready");
}
+ private Point3D GetTrackedObjectPosition(ModelItem item, bool useStableRailAnchor)
+ {
+ if (item == null)
+ {
+ return new Point3D(0, 0, 0);
+ }
+
+ var bounds = item.BoundingBox();
+ if (useStableRailAnchor)
+ {
+ return ModelItemTransformHelper.GetStableBottomAnchorPoint(bounds, item.Transform);
+ }
+
+ return new Point3D(bounds.Center.X, bounds.Center.Y, bounds.Min.Z);
+ }
+
///
/// 设置物体角度修正值(度,顺时针)
///
@@ -3015,7 +2996,6 @@ namespace NavisworksTransport.Core.Animation
#region 动画实现方法
-
///
/// DispatcherTimer动画处理核心
///
@@ -3141,11 +3121,7 @@ namespace NavisworksTransport.Core.Animation
actualYaw += correctionRad;
}
- if (frameData.HasCustomLinearTransform && _route?.PathType == PathType.Rail)
- {
- UpdateObjectPosition(frameData.Position, frameData.LinearTransform);
- }
- else if (frameData.HasCustomRotation && _route?.PathType == PathType.Rail)
+ if (frameData.HasCustomRotation && _route?.PathType == PathType.Rail)
{
UpdateObjectPosition(frameData.Position, frameData.Rotation);
}
diff --git a/src/Core/VirtualObjectManager.cs b/src/Core/VirtualObjectManager.cs
index f3d505d..b1ea2dd 100644
--- a/src/Core/VirtualObjectManager.cs
+++ b/src/Core/VirtualObjectManager.cs
@@ -264,129 +264,6 @@ namespace NavisworksTransport.Core
}
}
- public void MoveVirtualObject(Point3D position, Matrix3 linearTransform)
- {
- if (_virtualObjectModel == null || _virtualObjectModelItem == null) return;
-
- try
- {
- var doc = Application.ActiveDocument;
- var modelItems = new ModelItemCollection { _virtualObjectModelItem };
-
- // 虚拟物体的尺寸缩放在 Model 层完成。
- // 对盒体而言,如果再把三维姿态放在 Item 层,会让非等比缩放和旋转分层,导致姿态看起来发歪。
- // 因此这里直接在 Model 层一次性应用“缩放 + 旋转 + 平移”。
- doc.Models.ResetPermanentTransform(modelItems);
-
- var modelTransform = _virtualObjectModel.Transform;
- var modelComponents = modelTransform.Factor();
- var currentScale = modelComponents.Scale;
- var originalBounds = _virtualObjectModelItem.BoundingBox();
- var originalCenter = originalBounds.Center;
- var originalGroundPos = new Point3D(originalBounds.Center.X, originalBounds.Center.Y, originalBounds.Min.Z);
-
- var effectiveLinear = new Matrix3(
- linearTransform.Get(0, 0) * currentScale.X, linearTransform.Get(0, 1) * currentScale.Y, linearTransform.Get(0, 2) * currentScale.Z,
- linearTransform.Get(1, 0) * currentScale.X, linearTransform.Get(1, 1) * currentScale.Y, linearTransform.Get(1, 2) * currentScale.Z,
- linearTransform.Get(2, 0) * currentScale.X, linearTransform.Get(2, 1) * currentScale.Y, linearTransform.Get(2, 2) * currentScale.Z);
-
- LogMatrixDiagnostics(linearTransform, effectiveLinear);
-
- var localGroundPos = originalGroundPos;
- var transformedGroundPos = new Point3D(
- effectiveLinear.Get(0, 0) * localGroundPos.X + effectiveLinear.Get(0, 1) * localGroundPos.Y + effectiveLinear.Get(0, 2) * localGroundPos.Z,
- effectiveLinear.Get(1, 0) * localGroundPos.X + effectiveLinear.Get(1, 1) * localGroundPos.Y + effectiveLinear.Get(1, 2) * localGroundPos.Z,
- effectiveLinear.Get(2, 0) * localGroundPos.X + effectiveLinear.Get(2, 1) * localGroundPos.Y + effectiveLinear.Get(2, 2) * localGroundPos.Z);
-
- var translation = new Vector3D(
- position.X - transformedGroundPos.X,
- position.Y - transformedGroundPos.Y,
- position.Z - transformedGroundPos.Z);
-
- LogManager.Info(
- $"[虚拟物体姿态] 应用前: 中心=({originalCenter.X:F3},{originalCenter.Y:F3},{originalCenter.Z:F3}), " +
- $"底面中心=({originalGroundPos.X:F3},{originalGroundPos.Y:F3},{originalGroundPos.Z:F3}), " +
- $"尺寸=({originalBounds.Size.X:F3},{originalBounds.Size.Y:F3},{originalBounds.Size.Z:F3}), " +
- $"缩放=({currentScale.X:F3},{currentScale.Y:F3},{currentScale.Z:F3})");
- LogManager.Info(
- $"[虚拟物体姿态] 目标: 底面中心=({position.X:F3},{position.Y:F3},{position.Z:F3}), " +
- $"线性矩阵=[[{linearTransform.Get(0, 0):F4},{linearTransform.Get(0, 1):F4},{linearTransform.Get(0, 2):F4}]," +
- $"[{linearTransform.Get(1, 0):F4},{linearTransform.Get(1, 1):F4},{linearTransform.Get(1, 2):F4}]," +
- $"[{linearTransform.Get(2, 0):F4},{linearTransform.Get(2, 1):F4},{linearTransform.Get(2, 2):F4}]]");
- LogManager.Info(
- $"[虚拟物体姿态] 组合后: 变换底面中心=({transformedGroundPos.X:F3},{transformedGroundPos.Y:F3},{transformedGroundPos.Z:F3}), " +
- $"平移=({translation.X:F3},{translation.Y:F3},{translation.Z:F3})");
-
- var newTransform = new Transform3D(effectiveLinear, translation);
- doc.Models.SetModelUnitsAndTransform(_virtualObjectModel, _virtualObjectModel.Units, newTransform, false);
-
- var appliedBounds = _virtualObjectModelItem.BoundingBox();
- LogManager.Info(
- $"[虚拟物体姿态] 应用后: 中心=({appliedBounds.Center.X:F3},{appliedBounds.Center.Y:F3},{appliedBounds.Center.Z:F3}), " +
- $"底面中心=({appliedBounds.Center.X:F3},{appliedBounds.Center.Y:F3},{appliedBounds.Min.Z:F3}), " +
- $"尺寸=({appliedBounds.Size.X:F3},{appliedBounds.Size.Y:F3},{appliedBounds.Size.Z:F3})");
- }
- catch (Exception ex)
- {
- LogManager.Error($"移动虚拟物体并应用完整三维线性姿态失败: {ex.Message}");
- }
- }
-
- private void LogMatrixDiagnostics(Matrix3 sourceLinear, Matrix3 effectiveLinear)
- {
- if (sourceLinear == null || effectiveLinear == null)
- {
- return;
- }
-
- var sourceRow0 = new Vector3D(sourceLinear.Get(0, 0), sourceLinear.Get(0, 1), sourceLinear.Get(0, 2));
- var sourceRow1 = new Vector3D(sourceLinear.Get(1, 0), sourceLinear.Get(1, 1), sourceLinear.Get(1, 2));
- var sourceRow2 = new Vector3D(sourceLinear.Get(2, 0), sourceLinear.Get(2, 1), sourceLinear.Get(2, 2));
- var sourceCol0 = new Vector3D(sourceLinear.Get(0, 0), sourceLinear.Get(1, 0), sourceLinear.Get(2, 0));
- var sourceCol1 = new Vector3D(sourceLinear.Get(0, 1), sourceLinear.Get(1, 1), sourceLinear.Get(2, 1));
- var sourceCol2 = new Vector3D(sourceLinear.Get(0, 2), sourceLinear.Get(1, 2), sourceLinear.Get(2, 2));
-
- LogManager.Info(
- $"[矩阵诊断] 源矩阵行: r0=({sourceRow0.X:F4},{sourceRow0.Y:F4},{sourceRow0.Z:F4}), " +
- $"r1=({sourceRow1.X:F4},{sourceRow1.Y:F4},{sourceRow1.Z:F4}), " +
- $"r2=({sourceRow2.X:F4},{sourceRow2.Y:F4},{sourceRow2.Z:F4})");
- LogManager.Info(
- $"[矩阵诊断] 源矩阵列: c0=({sourceCol0.X:F4},{sourceCol0.Y:F4},{sourceCol0.Z:F4}), " +
- $"c1=({sourceCol1.X:F4},{sourceCol1.Y:F4},{sourceCol1.Z:F4}), " +
- $"c2=({sourceCol2.X:F4},{sourceCol2.Y:F4},{sourceCol2.Z:F4})");
-
- LogMatrixBasisMapping("[矩阵诊断] 有效矩阵-按当前乘法", effectiveLinear, transpose: false);
- LogMatrixBasisMapping("[矩阵诊断] 有效矩阵-按转置乘法", effectiveLinear, transpose: true);
- }
-
- private void LogMatrixBasisMapping(string prefix, Matrix3 matrix, bool transpose)
- {
- var mappedX = TransformDirection(matrix, new Vector3D(1, 0, 0), transpose);
- var mappedY = TransformDirection(matrix, new Vector3D(0, 1, 0), transpose);
- var mappedZ = TransformDirection(matrix, new Vector3D(0, 0, 1), transpose);
-
- LogManager.Info(
- $"{prefix}: X->({mappedX.X:F4},{mappedX.Y:F4},{mappedX.Z:F4}), " +
- $"Y->({mappedY.X:F4},{mappedY.Y:F4},{mappedY.Z:F4}), " +
- $"Z->({mappedZ.X:F4},{mappedZ.Y:F4},{mappedZ.Z:F4})");
- }
-
- private Vector3D TransformDirection(Matrix3 matrix, Vector3D direction, bool transpose)
- {
- if (!transpose)
- {
- return new Vector3D(
- matrix.Get(0, 0) * direction.X + matrix.Get(0, 1) * direction.Y + matrix.Get(0, 2) * direction.Z,
- matrix.Get(1, 0) * direction.X + matrix.Get(1, 1) * direction.Y + matrix.Get(1, 2) * direction.Z,
- matrix.Get(2, 0) * direction.X + matrix.Get(2, 1) * direction.Y + matrix.Get(2, 2) * direction.Z);
- }
-
- return new Vector3D(
- matrix.Get(0, 0) * direction.X + matrix.Get(1, 0) * direction.Y + matrix.Get(2, 0) * direction.Z,
- matrix.Get(0, 1) * direction.X + matrix.Get(1, 1) * direction.Y + matrix.Get(2, 1) * direction.Z,
- matrix.Get(0, 2) * direction.X + matrix.Get(1, 2) * direction.Y + matrix.Get(2, 2) * direction.Z);
- }
-
public void ResetToCADPosition()
{
if (_virtualObjectModelItem == null) return;
diff --git a/src/Utils/CollisionSceneHelper.cs b/src/Utils/CollisionSceneHelper.cs
index 5465247..b2fc291 100644
--- a/src/Utils/CollisionSceneHelper.cs
+++ b/src/Utils/CollisionSceneHelper.cs
@@ -104,11 +104,21 @@ namespace NavisworksTransport.Utils
// 从PathAnimationManager获取当前实际朝向
var pam = PathAnimationManager.GetInstance();
var currentYaw = pam.CurrentYaw;
- var state = ModelItemTransformHelper.SaveObjectState(animatedObject, currentYaw);
+ var currentRotation = pam.CurrentRotation;
+ var hasCustomRotation = pam.HasCurrentRotation;
+ var currentState = pam.GetObjectCurrentPosition(animatedObject);
+ pam.SaveObjectState(animatedObject);
+
+ var state = ModelItemTransformHelper.SaveObjectState(
+ animatedObject,
+ currentYaw,
+ currentState.Position,
+ currentRotation,
+ hasCustomRotation);
- LogManager.Info(string.Format("已保存动画物体状态: {0}, 位置=({1:F2},{2:F2},{3:F2}), 朝向={4:F2}°",
+ LogManager.Info(string.Format("已保存动画物体状态: {0}, 位置=({1:F2},{2:F2},{3:F2}), 朝向={4:F2}°, customRotation={5}",
animatedObject.DisplayName, state.Position.X, state.Position.Y, state.Position.Z,
- currentYaw * 180 / Math.PI));
+ currentYaw * 180 / Math.PI, state.HasCustomRotation));
return state;
}
@@ -130,6 +140,14 @@ namespace NavisworksTransport.Utils
try
{
+ var pam = PathAnimationManager.GetInstance();
+ if (pam != null && ReferenceEquals(pam.AnimatedObject, animatedObject) && state.HasCustomRotation)
+ {
+ pam.RestoreAnimatedObjectState(animatedObject);
+ LogManager.Info(string.Format("动画物体已通过PathAnimationManager恢复三维姿态: {0}", animatedObject.DisplayName));
+ return;
+ }
+
// 检查是否是虚拟物体
bool isVirtual = VirtualObjectManager.Instance.IsVirtualObjectActive &&
VirtualObjectManager.Instance.CurrentVirtualObject == animatedObject;
diff --git a/src/Utils/ModelItemTransformHelper.cs b/src/Utils/ModelItemTransformHelper.cs
index c66ad9d..99c6a99 100644
--- a/src/Utils/ModelItemTransformHelper.cs
+++ b/src/Utils/ModelItemTransformHelper.cs
@@ -110,6 +110,32 @@ namespace NavisworksTransport.Utils
return GetAxisDirectionFromTransform(transform, 2, new Vector3D(0, 0, 1));
}
+ ///
+ /// 获取绑定在物体局部坐标中的稳定底面锚点。
+ /// 适用于带俯仰/侧倾的三维姿态对象,避免使用世界 AABB 底面中心导致锚点漂移。
+ ///
+ public static Point3D GetStableBottomAnchorPoint(BoundingBox3D bounds, Transform3D transform)
+ {
+ if (bounds == null)
+ {
+ return new Point3D(0, 0, 0);
+ }
+
+ if (transform == null)
+ {
+ return new Point3D(bounds.Center.X, bounds.Center.Y, bounds.Min.Z);
+ }
+
+ var up = GetUpDirectionFromTransform(transform);
+ var localSize = EstimateLocalBoxSize(bounds, transform);
+ double halfHeight = localSize.Z / 2.0;
+
+ return new Point3D(
+ bounds.Center.X - up.X * halfHeight,
+ bounds.Center.Y - up.Y * halfHeight,
+ bounds.Center.Z - up.Z * halfHeight);
+ }
+
///
/// 根据世界轴对齐包围盒和物体变换,估算物体在局部坐标系中的尺寸。
/// 适用于刚性箱体类对象,用于顶/底面对接点推导。
@@ -362,20 +388,121 @@ namespace NavisworksTransport.Utils
}
///
- /// 将物体移动到指定位置和完整三维线性姿态(先回到CAD原始位置,再移动)。
- /// targetLinear 的列向量分别表示物体局部 X/Y/Z 轴在世界坐标中的方向。
+ /// 基于物体当前实际姿态,增量移动到目标位置和完整三维朝向。
+ /// 适用于动画过程中的真实模型物体,避免每次回到 CAD 原始状态导致位置跑偏。
///
- public static void MoveItemToPositionAndLinearTransform(ModelItem item, Point3D targetPosition, Matrix3 targetLinear)
+ public static void MoveItemIncrementallyToPositionAndRotation(
+ ModelItem item,
+ Point3D currentPosition,
+ Rotation3D currentRotation,
+ Point3D targetPosition,
+ Rotation3D targetRotation)
{
- ApplyAbsoluteLinearTransform(item, targetPosition, targetLinear, preserveCurrentScale: false);
+ if (item == null)
+ {
+ throw new ArgumentNullException(nameof(item));
+ }
+
+ var doc = Application.ActiveDocument;
+ var modelItems = new ModelItemCollection { item };
+
+ Rotation3D deltaRotation = BuildDeltaRotation(currentRotation, targetRotation);
+ var deltaLinear = new Transform3D(deltaRotation).Linear;
+ var currentLinear = new Transform3D(currentRotation).Linear;
+ var targetLinear = new Transform3D(targetRotation).Linear;
+
+ var rotatedCurrentPosition = new Point3D(
+ deltaLinear.Get(0, 0) * currentPosition.X + deltaLinear.Get(0, 1) * currentPosition.Y + deltaLinear.Get(0, 2) * currentPosition.Z,
+ deltaLinear.Get(1, 0) * currentPosition.X + deltaLinear.Get(1, 1) * currentPosition.Y + deltaLinear.Get(1, 2) * currentPosition.Z,
+ deltaLinear.Get(2, 0) * currentPosition.X + deltaLinear.Get(2, 1) * currentPosition.Y + deltaLinear.Get(2, 2) * currentPosition.Z);
+
+ var translation = new Vector3D(
+ targetPosition.X - rotatedCurrentPosition.X,
+ targetPosition.Y - rotatedCurrentPosition.Y,
+ targetPosition.Z - rotatedCurrentPosition.Z);
+
+ LogManager.Info(
+ $"[模型增量姿态] {item.DisplayName} 当前=({currentPosition.X:F3},{currentPosition.Y:F3},{currentPosition.Z:F3}), " +
+ $"目标=({targetPosition.X:F3},{targetPosition.Y:F3},{targetPosition.Z:F3}), " +
+ $"平移=({translation.X:F3},{translation.Y:F3},{translation.Z:F3})");
+
+ LogManager.Info(
+ $"[模型增量姿态] {item.DisplayName} 当前旋转: " +
+ $"X=({currentLinear.Get(0, 0):F4},{currentLinear.Get(1, 0):F4},{currentLinear.Get(2, 0):F4}), " +
+ $"Y=({currentLinear.Get(0, 1):F4},{currentLinear.Get(1, 1):F4},{currentLinear.Get(2, 1):F4}), " +
+ $"Z=({currentLinear.Get(0, 2):F4},{currentLinear.Get(1, 2):F4},{currentLinear.Get(2, 2):F4})");
+
+ LogManager.Info(
+ $"[模型增量姿态] {item.DisplayName} 目标旋转: " +
+ $"X=({targetLinear.Get(0, 0):F4},{targetLinear.Get(1, 0):F4},{targetLinear.Get(2, 0):F4}), " +
+ $"Y=({targetLinear.Get(0, 1):F4},{targetLinear.Get(1, 1):F4},{targetLinear.Get(2, 1):F4}), " +
+ $"Z=({targetLinear.Get(0, 2):F4},{targetLinear.Get(1, 2):F4},{targetLinear.Get(2, 2):F4})");
+
+ LogManager.Info(
+ $"[模型增量姿态] {item.DisplayName} 增量旋转: " +
+ $"X=({deltaLinear.Get(0, 0):F4},{deltaLinear.Get(1, 0):F4},{deltaLinear.Get(2, 0):F4}), " +
+ $"Y=({deltaLinear.Get(0, 1):F4},{deltaLinear.Get(1, 1):F4},{deltaLinear.Get(2, 1):F4}), " +
+ $"Z=({deltaLinear.Get(0, 2):F4},{deltaLinear.Get(1, 2):F4},{deltaLinear.Get(2, 2):F4}), " +
+ $"旋后当前点=({rotatedCurrentPosition.X:F3},{rotatedCurrentPosition.Y:F3},{rotatedCurrentPosition.Z:F3})");
+
+ // 用显式三步法应用三维增量位姿:
+ // 1. 把当前锚点移到原点
+ // 2. 绕原点旋转到目标姿态
+ // 3. 再把锚点移到目标位置
+ // 这与旧 yaw 链路的补偿语义一致,只是从二维扩展到三维。
+ var toOrigin = Transform3D.CreateTranslation(new Vector3D(
+ -currentPosition.X,
+ -currentPosition.Y,
+ -currentPosition.Z));
+ doc.Models.OverridePermanentTransform(modelItems, toOrigin, false);
+
+ var rotationOnlyComponents = Transform3D.CreateTranslation(new Vector3D(0, 0, 0)).Factor();
+ rotationOnlyComponents.Rotation = deltaRotation;
+ var rotationOnly = rotationOnlyComponents.Combine();
+ doc.Models.OverridePermanentTransform(modelItems, rotationOnly, false);
+
+ var toTarget = Transform3D.CreateTranslation(new Vector3D(
+ targetPosition.X,
+ targetPosition.Y,
+ targetPosition.Z));
+ doc.Models.OverridePermanentTransform(modelItems, toTarget, false);
+
+ LogIncrementalTransformActual(item, targetPosition, targetRotation);
}
- ///
- /// 将物体移动到指定位置和完整三维线性姿态,同时保留当前缩放。
- ///
- public static void MoveItemToPositionAndLinearTransformWithCurrentScale(ModelItem item, Point3D targetPosition, Matrix3 targetLinear)
+ private static void LogIncrementalTransformActual(ModelItem item, Point3D targetPosition, Rotation3D targetRotation)
{
- ApplyAbsoluteLinearTransform(item, targetPosition, targetLinear, preserveCurrentScale: true);
+ try
+ {
+ var actualBounds = item.BoundingBox();
+ var actualTransform = item.Transform;
+ var actualRotation = actualTransform.Factor().Rotation;
+ var actualLinear = new Transform3D(actualRotation).Linear;
+ var targetLinear = new Transform3D(targetRotation).Linear;
+ var actualPosition = new Point3D(
+ actualBounds.Center.X,
+ actualBounds.Center.Y,
+ actualBounds.Min.Z);
+
+ LogManager.Info(
+ $"[模型增量姿态] {item.DisplayName} 应用后位置: " +
+ $"实际=({actualPosition.X:F3},{actualPosition.Y:F3},{actualPosition.Z:F3}), " +
+ $"期望=({targetPosition.X:F3},{targetPosition.Y:F3},{targetPosition.Z:F3}), " +
+ $"偏差=({actualPosition.X - targetPosition.X:F3},{actualPosition.Y - targetPosition.Y:F3},{actualPosition.Z - targetPosition.Z:F3})");
+
+ LogManager.Info(
+ $"[模型增量姿态] {item.DisplayName} 应用后旋转: " +
+ $"实际X=({actualLinear.Get(0, 0):F4},{actualLinear.Get(1, 0):F4},{actualLinear.Get(2, 0):F4}), " +
+ $"实际Y=({actualLinear.Get(0, 1):F4},{actualLinear.Get(1, 1):F4},{actualLinear.Get(2, 1):F4}), " +
+ $"实际Z=({actualLinear.Get(0, 2):F4},{actualLinear.Get(1, 2):F4},{actualLinear.Get(2, 2):F4}), " +
+ $"期望X=({targetLinear.Get(0, 0):F4},{targetLinear.Get(1, 0):F4},{targetLinear.Get(2, 0):F4}), " +
+ $"期望Y=({targetLinear.Get(0, 1):F4},{targetLinear.Get(1, 1):F4},{targetLinear.Get(2, 1):F4}), " +
+ $"期望Z=({targetLinear.Get(0, 2):F4},{targetLinear.Get(1, 2):F4},{targetLinear.Get(2, 2):F4})");
+ }
+ catch (Exception ex)
+ {
+ LogManager.Warning($"[模型增量姿态] 输出应用后日志失败: {ex.Message}");
+ }
}
///
@@ -554,14 +681,21 @@ namespace NavisworksTransport.Utils
doc.Models.ResetPermanentTransform(modelItems);
+ var originalTransform = item.Transform;
+ var originalComponents = originalTransform.Factor();
+ var originalRotation = originalComponents.Rotation;
var originalBounds = item.BoundingBox();
+ var originalUp = GetUpDirectionFromTransform(originalTransform);
+ var localSize = EstimateLocalBoxSize(originalBounds, originalTransform);
+ double halfHeight = localSize.Z / 2.0;
var originalGroundPos = new Point3D(
- originalBounds.Center.X,
- originalBounds.Center.Y,
- originalBounds.Min.Z
+ originalBounds.Center.X - originalUp.X * halfHeight,
+ originalBounds.Center.Y - originalUp.Y * halfHeight,
+ originalBounds.Center.Z - originalUp.Z * halfHeight
);
- var rotationTransform = new Transform3D(targetRotation);
+ Rotation3D deltaRotation = BuildDeltaRotation(originalRotation, targetRotation);
+ var rotationTransform = new Transform3D(deltaRotation);
var linear = rotationTransform.Linear;
var rotatedGroundPos = new Point3D(
@@ -582,77 +716,71 @@ namespace NavisworksTransport.Utils
var identity = Transform3D.CreateTranslation(new Vector3D(0, 0, 0));
var components = identity.Factor();
components.Scale = currentScale;
- components.Rotation = targetRotation;
+ components.Rotation = deltaRotation;
components.Translation = translation;
transform = components.Combine();
}
else
{
- transform = new Transform3D(targetRotation, translation);
+ transform = new Transform3D(deltaRotation, translation);
}
+ LogAbsoluteTransformDiagnostics(item, originalRotation, targetRotation, deltaRotation, originalGroundPos, targetPosition, translation, halfHeight, originalUp);
doc.Models.OverridePermanentTransform(modelItems, transform, false);
}
- private static void ApplyAbsoluteLinearTransform(ModelItem item, Point3D targetPosition, Matrix3 targetLinear, bool preserveCurrentScale)
+ private static Rotation3D BuildDeltaRotation(Rotation3D originalRotation, Rotation3D targetRotation)
{
- if (item == null)
- {
- throw new ArgumentNullException(nameof(item));
- }
-
- if (targetLinear == null)
- {
- throw new ArgumentNullException(nameof(targetLinear));
- }
-
- var doc = Application.ActiveDocument;
- var modelItems = new ModelItemCollection { item };
- Vector3D currentScale = new Vector3D(1, 1, 1);
-
- if (preserveCurrentScale)
- {
- var currentComponents = item.Transform.Factor();
- currentScale = currentComponents.Scale;
- }
-
- doc.Models.ResetPermanentTransform(modelItems);
-
- var originalBounds = item.BoundingBox();
- var originalGroundPos = new Point3D(
- originalBounds.Center.X,
- originalBounds.Center.Y,
- originalBounds.Min.Z
- );
-
- Matrix3 effectiveLinear = preserveCurrentScale
- ? BuildScaledLinearMatrix(targetLinear, currentScale)
- : targetLinear;
-
- Point3D transformedGroundPos = TransformPoint(effectiveLinear, originalGroundPos);
- var translation = new Vector3D(
- targetPosition.X - transformedGroundPos.X,
- targetPosition.Y - transformedGroundPos.Y,
- targetPosition.Z - transformedGroundPos.Z
- );
-
- doc.Models.OverridePermanentTransform(modelItems, new Transform3D(effectiveLinear, translation), false);
+ var originalInverse = originalRotation.Invert();
+ var deltaTransform = Transform3D.Multiply(
+ new Transform3D(targetRotation),
+ new Transform3D(originalInverse));
+ return deltaTransform.Factor().Rotation;
}
- private static Matrix3 BuildScaledLinearMatrix(Matrix3 linear, Vector3D scale)
+ private static void LogAbsoluteTransformDiagnostics(
+ ModelItem item,
+ Rotation3D originalRotation,
+ Rotation3D targetRotation,
+ Rotation3D deltaRotation,
+ Point3D originalGroundPos,
+ Point3D targetPosition,
+ Vector3D translation,
+ double halfHeight,
+ Vector3D originalUp)
{
- return new Matrix3(
- linear.Get(0, 0) * scale.X, linear.Get(0, 1) * scale.Y, linear.Get(0, 2) * scale.Z,
- linear.Get(1, 0) * scale.X, linear.Get(1, 1) * scale.Y, linear.Get(1, 2) * scale.Z,
- linear.Get(2, 0) * scale.X, linear.Get(2, 1) * scale.Y, linear.Get(2, 2) * scale.Z);
- }
+ try
+ {
+ var originalLinear = new Transform3D(originalRotation).Linear;
+ var targetLinear = new Transform3D(targetRotation).Linear;
+ var deltaLinear = new Transform3D(deltaRotation).Linear;
- private static Point3D TransformPoint(Matrix3 linear, Point3D point)
- {
- return new Point3D(
- linear.Get(0, 0) * point.X + linear.Get(0, 1) * point.Y + linear.Get(0, 2) * point.Z,
- linear.Get(1, 0) * point.X + linear.Get(1, 1) * point.Y + linear.Get(1, 2) * point.Z,
- linear.Get(2, 0) * point.X + linear.Get(2, 1) * point.Y + linear.Get(2, 2) * point.Z);
+ LogManager.Info(
+ $"[模型姿态] {item.DisplayName} 原始: " +
+ $"X=({originalLinear.Get(0, 0):F4},{originalLinear.Get(1, 0):F4},{originalLinear.Get(2, 0):F4}), " +
+ $"Y=({originalLinear.Get(0, 1):F4},{originalLinear.Get(1, 1):F4},{originalLinear.Get(2, 1):F4}), " +
+ $"Z=({originalLinear.Get(0, 2):F4},{originalLinear.Get(1, 2):F4},{originalLinear.Get(2, 2):F4})");
+
+ LogManager.Info(
+ $"[模型姿态] {item.DisplayName} 目标: " +
+ $"X=({targetLinear.Get(0, 0):F4},{targetLinear.Get(1, 0):F4},{targetLinear.Get(2, 0):F4}), " +
+ $"Y=({targetLinear.Get(0, 1):F4},{targetLinear.Get(1, 1):F4},{targetLinear.Get(2, 1):F4}), " +
+ $"Z=({targetLinear.Get(0, 2):F4},{targetLinear.Get(1, 2):F4},{targetLinear.Get(2, 2):F4})");
+
+ LogManager.Info(
+ $"[模型姿态] {item.DisplayName} 增量: " +
+ $"X=({deltaLinear.Get(0, 0):F4},{deltaLinear.Get(1, 0):F4},{deltaLinear.Get(2, 0):F4}), " +
+ $"Y=({deltaLinear.Get(0, 1):F4},{deltaLinear.Get(1, 1):F4},{deltaLinear.Get(2, 1):F4}), " +
+ $"Z=({deltaLinear.Get(0, 2):F4},{deltaLinear.Get(1, 2):F4},{deltaLinear.Get(2, 2):F4}), " +
+ $"原始Up=({originalUp.X:F4},{originalUp.Y:F4},{originalUp.Z:F4}), 半高={halfHeight:F3}, " +
+ $"原始底面中心=({originalGroundPos.X:F3},{originalGroundPos.Y:F3},{originalGroundPos.Z:F3}), " +
+ $"目标底面中心=({targetPosition.X:F3},{targetPosition.Y:F3},{targetPosition.Z:F3}), " +
+ $"平移=({translation.X:F3},{translation.Y:F3},{translation.Z:F3})");
+ }
+ catch (Exception ex)
+ {
+ LogManager.Warning($"[模型姿态] 输出诊断日志失败: {ex.Message}");
+ }
}
///
@@ -735,6 +863,8 @@ namespace NavisworksTransport.Utils
{
public Point3D Position { get; set; }
public double YawRadians { get; set; }
+ public Rotation3D Rotation { get; set; }
+ public bool HasCustomRotation { get; set; }
public Transform3D Transform { get; set; }
}
@@ -742,7 +872,12 @@ namespace NavisworksTransport.Utils
/// 保存物体当前状态
/// 注意:YawRadians 通过当前位置计算,因为 Transform 返回的是CAD原始值
///
- public static ObjectStateSnapshot SaveObjectState(ModelItem item, double? currentYaw = null)
+ public static ObjectStateSnapshot SaveObjectState(
+ ModelItem item,
+ double? currentYaw = null,
+ Point3D currentPosition = null,
+ Rotation3D currentRotation = null,
+ bool hasCustomRotation = false)
{
if (item == null) return null;
@@ -764,8 +899,10 @@ namespace NavisworksTransport.Utils
return new ObjectStateSnapshot
{
- Position = new Point3D(bounds.Center.X, bounds.Center.Y, bounds.Min.Z),
+ Position = currentPosition ?? new Point3D(bounds.Center.X, bounds.Center.Y, bounds.Min.Z),
YawRadians = yaw,
+ Rotation = currentRotation ?? Rotation3D.Identity,
+ HasCustomRotation = hasCustomRotation,
Transform = item.Transform
};
}
@@ -792,6 +929,19 @@ namespace NavisworksTransport.Utils
private static void RestoreObjectStateInternal(ModelItem item, ObjectStateSnapshot state, bool preserveScale)
{
if (item == null || state == null) return;
+
+ if (state.HasCustomRotation)
+ {
+ if (preserveScale)
+ {
+ MoveItemToPositionAndRotationWithCurrentScale(item, state.Position, state.Rotation);
+ }
+ else
+ {
+ MoveItemToPositionAndRotation(item, state.Position, state.Rotation);
+ }
+ return;
+ }
var doc = Application.ActiveDocument;
var modelItems = new ModelItemCollection { item };
From 015a2863ea5dd05c28f5f8ba801d659109585d14 Mon Sep 17 00:00:00 2001
From: tian <11429339@qq.com>
Date: Fri, 20 Mar 2026 14:59:41 +0800
Subject: [PATCH 06/87] Fix collision pose restore and deployment verification
---
deploy-plugin.bat | 104 ++++-------
.../GenerateCollisionReportCommand.cs | 17 +-
src/Core/Animation/PathAnimationManager.cs | 171 +++++++++++++++---
.../Collision/ClashDetectiveIntegration.cs | 124 ++++++++-----
src/Core/PathDatabase.cs | 29 ++-
src/Core/PathPointRenderPlugin.cs | 16 +-
.../ViewModels/AnimationControlViewModel.cs | 20 +-
src/Utils/CollisionSceneHelper.cs | 97 +++++++---
src/Utils/ModelItemTransformHelper.cs | 8 +-
9 files changed, 405 insertions(+), 181 deletions(-)
diff --git a/deploy-plugin.bat b/deploy-plugin.bat
index 0d5d775..eaf9e8a 100644
--- a/deploy-plugin.bat
+++ b/deploy-plugin.bat
@@ -2,69 +2,43 @@
setlocal
set "TARGET_DIR=%PROGRAMDATA%\Autodesk\Navisworks Manage 2026\plugins\TransportPlugin"
-set "BUILD_DIR=bin\x64\Release"
-set "MAX_WAIT_SECONDS=15"
-set "COPY_RETRY_COUNT=5"
+set "BUILD_DIR=%~dp0bin\x64\Release"
+powershell -NoProfile -ExecutionPolicy Bypass -Command ^
+ "$ErrorActionPreference = 'Stop';" ^
+ "$targetDir = [System.IO.Path]::GetFullPath('%TARGET_DIR%');" ^
+ "$buildDir = [System.IO.Path]::GetFullPath('%BUILD_DIR%');" ^
+ "" ^
+ "Get-Process Roamer -ErrorAction SilentlyContinue | Stop-Process -Force -ErrorAction SilentlyContinue;" ^
+ "$deadline = (Get-Date).AddSeconds(15);" ^
+ "while (Get-Process Roamer -ErrorAction SilentlyContinue) {" ^
+ " if ((Get-Date) -ge $deadline) { throw 'Roamer.exe still running after 15 seconds.' }" ^
+ " Start-Sleep -Milliseconds 500;" ^
+ "}" ^
+ "" ^
+ "New-Item -ItemType Directory -Force -Path $targetDir | Out-Null;" ^
+ "Copy-Item (Join-Path $buildDir '*.dll') $targetDir -Force;" ^
+ "if (Test-Path (Join-Path $buildDir 'resources')) {" ^
+ " New-Item -ItemType Directory -Force -Path (Join-Path $targetDir 'resources') | Out-Null;" ^
+ " Copy-Item (Join-Path $buildDir 'resources\\*') (Join-Path $targetDir 'resources') -Recurse -Force;" ^
+ "}" ^
+ "" ^
+ "$sourceFiles = @();" ^
+ "$sourceFiles += Get-ChildItem $buildDir -File -Filter '*.dll';" ^
+ "if (Test-Path (Join-Path $buildDir 'resources')) {" ^
+ " $sourceFiles += Get-ChildItem (Join-Path $buildDir 'resources') -File;" ^
+ "}" ^
+ "" ^
+ "foreach ($sourceFile in $sourceFiles) {" ^
+ " $targetFile = if ($sourceFile.DirectoryName -eq $buildDir) { Join-Path $targetDir $sourceFile.Name } else { Join-Path (Join-Path $targetDir 'resources') $sourceFile.Name };" ^
+ " if (-not (Test-Path $targetFile)) { throw ('Deployment verification failed: missing target file ' + $targetFile) }" ^
+ " $targetInfo = Get-Item $targetFile;" ^
+ " if ($sourceFile.Length -ne $targetInfo.Length -or $sourceFile.LastWriteTime -ne $targetInfo.LastWriteTime) {" ^
+ " throw ('Deployment verification failed for ' + $sourceFile.Name + ': source=' + $sourceFile.LastWriteTime.ToString('yyyy-MM-dd HH:mm:ss.fff') + ' (' + $sourceFile.Length + '), target=' + $targetInfo.LastWriteTime.ToString('yyyy-MM-dd HH:mm:ss.fff') + ' (' + $targetInfo.Length + ')');" ^
+ " }" ^
+ " Write-Host ('Verified ' + $sourceFile.Name + ': ' + $targetInfo.LastWriteTime.ToString('yyyy-MM-dd HH:mm:ss.fff'));" ^
+ "}" ^
+ "" ^
+ "Write-Host 'Plugin deployed successfully!';"
-:: Stop Navisworks to release locked plugin DLLs.
-taskkill /F /IM Roamer.exe 2>nul
-if %errorlevel% == 0 (
- echo Navisworks process terminated.
-)
-
-:: Wait until Roamer.exe fully exits. Fixed delay alone is not reliable.
-set /a WAIT_COUNT=0
-:WAIT_FOR_ROAMER_EXIT
-tasklist /FI "IMAGENAME eq Roamer.exe" | find /I "Roamer.exe" >nul
-if errorlevel 1 goto ROAMER_EXITED
-if %WAIT_COUNT% GEQ %MAX_WAIT_SECONDS% (
- echo Warning: Roamer.exe still appears to be running after %MAX_WAIT_SECONDS% seconds.
- goto ROAMER_EXITED
-)
-timeout /t 1 /nobreak >nul
-set /a WAIT_COUNT+=1
-goto WAIT_FOR_ROAMER_EXIT
-
-:ROAMER_EXITED
-
-if not exist "%TARGET_DIR%" mkdir "%TARGET_DIR%"
-
-set /a COPY_ATTEMPT=0
-:COPY_DLLS
-copy "%BUILD_DIR%\*.dll" "%TARGET_DIR%\" >nul
-if not errorlevel 1 goto DLLS_DEPLOYED
-set /a COPY_ATTEMPT+=1
-if %COPY_ATTEMPT% GEQ %COPY_RETRY_COUNT% (
- echo Failed to deploy plugin DLLs after %COPY_RETRY_COUNT% attempts.
- exit /b 1
-)
-timeout /t 1 /nobreak >nul
-goto COPY_DLLS
-
-:DLLS_DEPLOYED
-echo Plugin DLLs deployed.
-
-:: Copy bundled resources.
-if exist "%BUILD_DIR%\resources" goto PREPARE_RESOURCES
-goto DEPLOY_DONE
-
-:PREPARE_RESOURCES
-if not exist "%TARGET_DIR%\resources" mkdir "%TARGET_DIR%\resources"
-set /a RESOURCE_COPY_ATTEMPT=0
-
-:COPY_RESOURCES
-xcopy "%BUILD_DIR%\resources\*" "%TARGET_DIR%\resources\" /E /I /Y /Q >nul
-if not errorlevel 1 goto RESOURCES_DEPLOYED
-set /a RESOURCE_COPY_ATTEMPT+=1
-if %RESOURCE_COPY_ATTEMPT% GEQ %COPY_RETRY_COUNT% (
- echo Failed to deploy resources after %COPY_RETRY_COUNT% attempts.
- exit /b 1
-)
-timeout /t 1 /nobreak >nul
-goto COPY_RESOURCES
-
-:RESOURCES_DEPLOYED
-echo Resources folder deployed.
-
-:DEPLOY_DONE
-echo Plugin deployed successfully!
+if errorlevel 1 exit /b 1
+exit /b 0
diff --git a/src/Commands/GenerateCollisionReportCommand.cs b/src/Commands/GenerateCollisionReportCommand.cs
index 68cd8d8..8614b98 100644
--- a/src/Commands/GenerateCollisionReportCommand.cs
+++ b/src/Commands/GenerateCollisionReportCommand.cs
@@ -362,16 +362,16 @@ namespace NavisworksTransport.Commands
{
UpdateProgress(92, "生成默认路径截图...");
- // 记录截图前虚拟物体实际位置和朝向
+ // 记录截图前虚拟物体位置和动画管理器状态。
+ // 不读取 ModelItem.Transform 的“实际朝向”,因为 override 后该值不可靠,容易误导排查。
var virtualObject = VirtualObjectManager.Instance.CurrentVirtualObject;
if (virtualObject != null)
{
var bounds = virtualObject.BoundingBox();
- var actualYaw = ModelItemTransformHelper.GetYawFromTransform(virtualObject.Transform);
var pam = PathAnimationManager.GetInstance();
- LogManager.Info(string.Format("[默认截图前] 虚拟物体实际位置: ({0:F2},{1:F2},{2:F2}), 实际朝向: {3:F2}°, PAM记录朝向: {4:F2}°",
+ LogManager.Info(string.Format("[默认截图前] 虚拟物体位置: ({0:F2},{1:F2},{2:F2}), PAM记录朝向: {3:F2}°, customRotation={4}",
bounds.Center.X, bounds.Center.Y, bounds.Min.Z,
- actualYaw * 180 / Math.PI, pam.CurrentYaw * 180 / Math.PI));
+ pam.CurrentYaw * 180 / Math.PI, pam.HasCurrentRotation));
}
else
{
@@ -393,16 +393,15 @@ namespace NavisworksTransport.Commands
"overview"
);
- // 记录截图后虚拟物体实际位置和朝向
+ // 记录截图后虚拟物体位置和动画管理器状态。
virtualObject = VirtualObjectManager.Instance.CurrentVirtualObject;
if (virtualObject != null)
{
var bounds = virtualObject.BoundingBox();
- var actualYaw = ModelItemTransformHelper.GetYawFromTransform(virtualObject.Transform);
var pam = PathAnimationManager.GetInstance();
- LogManager.Info(string.Format("[默认截图后] 虚拟物体实际位置: ({0:F2},{1:F2},{2:F2}), 实际朝向: {3:F2}°, PAM记录朝向: {4:F2}°",
+ LogManager.Info(string.Format("[默认截图后] 虚拟物体位置: ({0:F2},{1:F2},{2:F2}), PAM记录朝向: {3:F2}°, customRotation={4}",
bounds.Center.X, bounds.Center.Y, bounds.Min.Z,
- actualYaw * 180 / Math.PI, pam.CurrentYaw * 180 / Math.PI));
+ pam.CurrentYaw * 180 / Math.PI, pam.HasCurrentRotation));
}
if (screenshotPath != null)
@@ -1097,4 +1096,4 @@ namespace NavisworksTransport.Commands
}
}
}
-}
\ No newline at end of file
+}
diff --git a/src/Core/Animation/PathAnimationManager.cs b/src/Core/Animation/PathAnimationManager.cs
index e7b4c7b..1f53517 100644
--- a/src/Core/Animation/PathAnimationManager.cs
+++ b/src/Core/Animation/PathAnimationManager.cs
@@ -1060,6 +1060,12 @@ namespace NavisworksTransport.Core.Animation
LogManager.Debug($"移动物体位置: {framePosition.X:F2},{framePosition.Y:F2},{framePosition.Z:F2}");
LogManager.Debug($"被撞物体位置:{GetObjectPosition(collider).X:F2},{GetObjectPosition(collider).Y:F2},{GetObjectPosition(collider).Z:F2}");
+ double actualYawRadians = yawRadians;
+ if (!frame.HasCustomRotation && _objectRotationCorrection != 0.0)
+ {
+ actualYawRadians += _objectRotationCorrection * Math.PI / 180.0;
+ }
+
var collisionResult = new CollisionResult
{
ClashGuid = Guid.NewGuid(),
@@ -1074,22 +1080,33 @@ namespace NavisworksTransport.Core.Animation
(virtualBoundingBox.Min.Y + virtualBoundingBox.Max.Y + colliderBox.Min.Y + colliderBox.Max.Y) / 4,
(virtualBoundingBox.Min.Z + virtualBoundingBox.Max.Z + colliderBox.Min.Z + colliderBox.Max.Z) / 4
),
- // 🔥 重要:记录虚拟物体的包围盒中心,而不是底面中心
- // 原因:
- // 1. framePosition是路径点,代表物体在地面上的位置(底面中心)
- // 2. ClashDetective移动逻辑使用包围盒中心进行定位
- // 3. 如果记录底面中心,ClashDetective会把包围盒中心移动到底面位置,导致物体下沉半个高度
- // 4. 记录包围盒中心可以确保ClashDetective移动后的位置与预计算时的虚拟物体位置一致
- Item1Position = new Point3D(
- framePosition.X,
- framePosition.Y,
- framePosition.Z + (virtualBoundingBox.Max.Z - virtualBoundingBox.Min.Z) / 2
- ),
+ // 碰撞结果中的 Item1Position 必须与后续恢复链路的基准点语义一致:
+ // 1. 二维 yaw 路径沿用成熟旧逻辑,记录包围盒中心;
+ // 2. 三维 customRotation 路径记录动画跟踪点(framePosition)。
+ Item1Position = frame.HasCustomRotation
+ ? new Point3D(
+ framePosition.X,
+ framePosition.Y,
+ framePosition.Z)
+ : new Point3D(
+ framePosition.X,
+ framePosition.Y,
+ framePosition.Z + (virtualBoundingBox.Max.Z - virtualBoundingBox.Min.Z) / 2),
Item2Position = GetObjectPosition(collider),
- Item1YawRadians = yawRadians, // 记录运动物体朝向
+ Item1YawRadians = actualYawRadians, // 记录运动物体实际播放朝向
+ Item1Rotation = frame.HasCustomRotation ? frame.Rotation : Rotation3D.Identity,
+ Item1HasCustomRotation = frame.HasCustomRotation,
HasPositionInfo = true
};
+ LogManager.Debug(
+ $"[保存碰撞结果] 帧={i}, " +
+ $"位置语义={(frame.HasCustomRotation ? "轨迹跟踪点" : "包围盒中心")}, " +
+ $"原始Yaw={yawRadians * 180.0 / Math.PI:F2}°, " +
+ $"实际Yaw={actualYawRadians * 180.0 / Math.PI:F2}°, " +
+ $"customRotation={frame.HasCustomRotation}, " +
+ $"保存位置=({collisionResult.Item1Position.X:F3}, {collisionResult.Item1Position.Y:F3}, {collisionResult.Item1Position.Z:F3})");
+
frame.Collisions.Add(collisionResult);
_allCollisionResults.Add(collisionResult);
}
@@ -1328,19 +1345,52 @@ namespace NavisworksTransport.Core.Animation
if (_animationFrames != null && _animationFrames.Count > 0)
{
var firstFrame = _animationFrames[0];
- var currentPos = _animatedObject.BoundingBox().Center;
- var distanceToStart = Math.Sqrt(
- Math.Pow(currentPos.X - firstFrame.Position.X, 2) +
- Math.Pow(currentPos.Y - firstFrame.Position.Y, 2)
- );
-
- // 如果距离起点超过0.1米,或者朝向不一致,需要重置
- var yawDiff = Math.Abs(_currentYaw - firstFrame.YawRadians);
- if (yawDiff > Math.PI) yawDiff = 2 * Math.PI - yawDiff; // 处理角度环绕
-
- if (distanceToStart > 0.1 || yawDiff > 0.01)
+
+ double distanceToStart;
+ bool needsReset;
+ string mismatchSummary;
+
+ if (firstFrame.HasCustomRotation && _route?.PathType == PathType.Rail)
{
- LogManager.Info($"[动画开始] 物体不在起点,重置到起点: 距离={distanceToStart:F2}m, 角度差={yawDiff * 180 / Math.PI:F2}°");
+ var dx = _currentPosition.X - firstFrame.Position.X;
+ var dy = _currentPosition.Y - firstFrame.Position.Y;
+ var dz = _currentPosition.Z - firstFrame.Position.Z;
+ distanceToStart = Math.Sqrt(dx * dx + dy * dy + dz * dz);
+
+ var currentLinear = new Transform3D(_currentRotation).Linear;
+ var targetLinear = new Transform3D(firstFrame.Rotation).Linear;
+ double rotationDiff =
+ Math.Abs(currentLinear.Get(0, 0) - targetLinear.Get(0, 0)) +
+ Math.Abs(currentLinear.Get(0, 1) - targetLinear.Get(0, 1)) +
+ Math.Abs(currentLinear.Get(0, 2) - targetLinear.Get(0, 2)) +
+ Math.Abs(currentLinear.Get(1, 0) - targetLinear.Get(1, 0)) +
+ Math.Abs(currentLinear.Get(1, 1) - targetLinear.Get(1, 1)) +
+ Math.Abs(currentLinear.Get(1, 2) - targetLinear.Get(1, 2)) +
+ Math.Abs(currentLinear.Get(2, 0) - targetLinear.Get(2, 0)) +
+ Math.Abs(currentLinear.Get(2, 1) - targetLinear.Get(2, 1)) +
+ Math.Abs(currentLinear.Get(2, 2) - targetLinear.Get(2, 2));
+
+ needsReset = distanceToStart > 0.1 || rotationDiff > 0.01;
+ mismatchSummary = $"距离={distanceToStart:F2}m, 三维姿态差={rotationDiff:F4}";
+ }
+ else
+ {
+ var currentPos = _animatedObject.BoundingBox().Center;
+ distanceToStart = Math.Sqrt(
+ Math.Pow(currentPos.X - firstFrame.Position.X, 2) +
+ Math.Pow(currentPos.Y - firstFrame.Position.Y, 2)
+ );
+
+ var yawDiff = Math.Abs(_currentYaw - firstFrame.YawRadians);
+ if (yawDiff > Math.PI) yawDiff = 2 * Math.PI - yawDiff;
+
+ needsReset = distanceToStart > 0.1 || yawDiff > 0.01;
+ mismatchSummary = $"距离={distanceToStart:F2}m, 角度差={yawDiff * 180 / Math.PI:F2}°";
+ }
+
+ if (needsReset)
+ {
+ LogManager.Info($"[动画开始] 物体不在起点,重置到起点: {mismatchSummary}");
MoveObjectToPathStart();
}
}
@@ -2371,6 +2421,79 @@ namespace NavisworksTransport.Core.Animation
}
}
+ ///
+ /// 判断给定对象是否由当前动画管理器控制。
+ /// 对虚拟物体,判断当前激活的虚拟物体;对真实模型,判断当前动画对象引用。
+ ///
+ public bool ControlsAnimatedObject(ModelItem obj)
+ {
+ if (obj == null)
+ {
+ return false;
+ }
+
+ if (_isVirtualObject)
+ {
+ return VirtualObjectManager.Instance.IsVirtualObjectActive &&
+ ReferenceEquals(VirtualObjectManager.Instance.CurrentVirtualObject, obj);
+ }
+
+ return ReferenceEquals(_animatedObject, obj);
+ }
+
+ ///
+ /// 使用当前动画链路将动画对象移动到指定姿态。
+ /// 适用于碰撞点定位、报告截图等场景,确保与动画播放使用同一套位置/姿态应用语义。
+ ///
+ public void MoveAnimatedObjectToPose(
+ ModelItem obj,
+ Point3D targetPosition,
+ double targetYawRadians,
+ Rotation3D targetRotation,
+ bool hasCustomRotation)
+ {
+ if (obj == null)
+ {
+ LogManager.Warning("MoveAnimatedObjectToPose: 对象为空");
+ return;
+ }
+
+ if (!ControlsAnimatedObject(obj))
+ {
+ LogManager.Warning($"MoveAnimatedObjectToPose: 对象不受当前动画管理器控制: {obj.DisplayName}");
+ return;
+ }
+
+ try
+ {
+ var originalAnimatedObject = _animatedObject;
+ if (!_isVirtualObject)
+ {
+ _animatedObject = obj;
+ }
+
+ if (hasCustomRotation)
+ {
+ UpdateObjectPosition(targetPosition, targetRotation);
+ }
+ else
+ {
+ UpdateObjectPosition(targetPosition, targetYawRadians);
+ }
+
+ _animatedObject = originalAnimatedObject;
+
+ LogManager.Info(
+ $"[动画姿态复用] {obj.DisplayName} 已移动到目标姿态: " +
+ $"位置=({targetPosition.X:F3},{targetPosition.Y:F3},{targetPosition.Z:F3}), customRotation={hasCustomRotation}");
+ }
+ catch (Exception ex)
+ {
+ LogManager.Error($"MoveAnimatedObjectToPose: 应用目标姿态失败: {ex.Message}", ex);
+ throw;
+ }
+ }
+
///
/// 获取播放方向(1=正向,-1=反向)
///
diff --git a/src/Core/Collision/ClashDetectiveIntegration.cs b/src/Core/Collision/ClashDetectiveIntegration.cs
index d168eda..68ea04d 100644
--- a/src/Core/Collision/ClashDetectiveIntegration.cs
+++ b/src/Core/Collision/ClashDetectiveIntegration.cs
@@ -177,6 +177,8 @@ namespace NavisworksTransport
public Point3D Item1Position { get; set; }
public Point3D Item2Position { get; set; }
public double Item1YawRadians { get; set; }
+ public Rotation3D Item1Rotation { get; set; }
+ public bool Item1HasCustomRotation { get; set; }
public bool HasPositionInfo { get; set; }
}
@@ -275,9 +277,14 @@ namespace NavisworksTransport
collisionRecord.Item1PosY = collision.Item1Position.Y;
collisionRecord.Item1PosZ = collision.Item1Position.Z;
collisionRecord.Item1YawRadians = collision.Item1YawRadians;
+ collisionRecord.Item1RotA = collision.Item1Rotation?.A;
+ collisionRecord.Item1RotB = collision.Item1Rotation?.B;
+ collisionRecord.Item1RotC = collision.Item1Rotation?.C;
+ collisionRecord.Item1RotD = collision.Item1Rotation?.D;
+ collisionRecord.Item1HasCustomRotation = collision.Item1HasCustomRotation;
collisionRecord.HasPositionInfo = true;
- LogManager.Debug($"[保存碰撞对象] 记录运动物体位置: ({collisionRecord.Item1PosX:F2}, {collisionRecord.Item1PosY:F2}, {collisionRecord.Item1PosZ:F2}), 朝向: {collisionRecord.Item1YawRadians:F2} rad");
+ LogManager.Debug($"[保存碰撞对象] 记录运动物体位置: ({collisionRecord.Item1PosX:F2}, {collisionRecord.Item1PosY:F2}, {collisionRecord.Item1PosZ:F2}), yaw={collisionRecord.Item1YawRadians:F2} rad, customRotation={collisionRecord.Item1HasCustomRotation}");
}
collisionObjects.Add(collisionRecord);
@@ -492,9 +499,22 @@ namespace NavisworksTransport
{
collisionResult.Item1Position = new Point3D(obj.Item1PosX.Value, obj.Item1PosY.Value, obj.Item1PosZ.Value);
collisionResult.Item1YawRadians = obj.Item1YawRadians ?? 0.0;
+ collisionResult.Item1HasCustomRotation = obj.Item1HasCustomRotation;
+ if (obj.Item1HasCustomRotation &&
+ obj.Item1RotA.HasValue &&
+ obj.Item1RotB.HasValue &&
+ obj.Item1RotC.HasValue &&
+ obj.Item1RotD.HasValue)
+ {
+ collisionResult.Item1Rotation = new Rotation3D(
+ obj.Item1RotA.Value,
+ obj.Item1RotB.Value,
+ obj.Item1RotC.Value,
+ obj.Item1RotD.Value);
+ }
collisionResult.HasPositionInfo = true;
- LogManager.Debug($"[加载碰撞结果] 恢复运动物体位置: ({obj.Item1PosX:F2}, {obj.Item1PosY:F2}, {obj.Item1PosZ:F2}), 朝向: {obj.Item1YawRadians:F2} rad");
+ LogManager.Debug($"[加载碰撞结果] 恢复运动物体位置: ({obj.Item1PosX:F2}, {obj.Item1PosY:F2}, {obj.Item1PosZ:F2}), yaw={obj.Item1YawRadians:F2} rad, customRotation={obj.Item1HasCustomRotation}");
}
results.Add(collisionResult);
@@ -778,56 +798,68 @@ namespace NavisworksTransport
var modelItems = new ModelItemCollection { testAnimatedObject };
var targetPosition = candidate.Item1Position;
var targetYaw = candidate.Item1YawRadians;
+ var targetRotation = candidate.Item1Rotation;
// 🔥 关键:为了最高精度,先回到CAD原始位置,再移动到目标位置
// 避免累积误差影响碰撞检测精度
doc.Models.ResetPermanentTransform(modelItems);
- // 获取CAD原始状态
- var originalBounds = testAnimatedObject.BoundingBox();
- var originalPos = new Point3D(
- (originalBounds.Min.X + originalBounds.Max.X) / 2,
- (originalBounds.Min.Y + originalBounds.Max.Y) / 2,
- (originalBounds.Min.Z + originalBounds.Max.Z) / 2
- );
- var originalYaw = ModelItemTransformHelper.GetYawFromTransform(testAnimatedObject.Transform);
-
- // 计算从CAD位置到目标位置的偏移和旋转
- var deltaPos = new Vector3D(
- targetPosition.X - originalPos.X,
- targetPosition.Y - originalPos.Y,
- targetPosition.Z - originalPos.Z
- );
- double deltaYaw = targetYaw - originalYaw;
-
- // 应用变换(包含旋转补偿)
- Transform3D transform;
- if (Math.Abs(deltaYaw) > 0.001)
+ if (candidate.Item1HasCustomRotation && targetRotation != null)
{
- // 计算绕原点旋转的补偿
- double cos = Math.Cos(deltaYaw);
- double sin = Math.Sin(deltaYaw);
- double rotatedX = originalPos.X * cos - originalPos.Y * sin;
- double rotatedY = originalPos.X * sin + originalPos.Y * cos;
-
- var compensatedTranslation = new Vector3D(
- targetPosition.X - rotatedX,
- targetPosition.Y - rotatedY,
- deltaPos.Z
- );
-
- var identity = Transform3D.CreateTranslation(new Vector3D(0, 0, 0));
- var components = identity.Factor();
- components.Rotation = new Rotation3D(new UnitVector3D(0, 0, 1), deltaYaw);
- components.Translation = compensatedTranslation;
- transform = components.Combine();
+ var originalBounds = testAnimatedObject.BoundingBox();
+ var originalCenter = originalBounds.Center;
+ var originalRotation = testAnimatedObject.Transform.Factor().Rotation;
+ ModelItemTransformHelper.MoveItemIncrementallyToPositionAndRotation(
+ testAnimatedObject,
+ originalCenter,
+ originalRotation,
+ targetPosition,
+ targetRotation);
}
else
{
- transform = Transform3D.CreateTranslation(deltaPos);
+ var originalBounds = testAnimatedObject.BoundingBox();
+ var originalPos = new Point3D(
+ (originalBounds.Min.X + originalBounds.Max.X) / 2,
+ (originalBounds.Min.Y + originalBounds.Max.Y) / 2,
+ (originalBounds.Min.Z + originalBounds.Max.Z) / 2
+ );
+ var originalYaw = ModelItemTransformHelper.GetYawFromTransform(testAnimatedObject.Transform);
+
+ var deltaPos = new Vector3D(
+ targetPosition.X - originalPos.X,
+ targetPosition.Y - originalPos.Y,
+ targetPosition.Z - originalPos.Z
+ );
+ double deltaYaw = targetYaw - originalYaw;
+
+ Transform3D transform;
+ if (Math.Abs(deltaYaw) > 0.001)
+ {
+ double cos = Math.Cos(deltaYaw);
+ double sin = Math.Sin(deltaYaw);
+ double rotatedX = originalPos.X * cos - originalPos.Y * sin;
+ double rotatedY = originalPos.X * sin + originalPos.Y * cos;
+
+ var compensatedTranslation = new Vector3D(
+ targetPosition.X - rotatedX,
+ targetPosition.Y - rotatedY,
+ deltaPos.Z
+ );
+
+ var identity = Transform3D.CreateTranslation(new Vector3D(0, 0, 0));
+ var components = identity.Factor();
+ components.Rotation = new Rotation3D(new UnitVector3D(0, 0, 1), deltaYaw);
+ components.Translation = compensatedTranslation;
+ transform = components.Combine();
+ }
+ else
+ {
+ transform = Transform3D.CreateTranslation(deltaPos);
+ }
+
+ doc.Models.OverridePermanentTransform(modelItems, transform, false);
}
-
- doc.Models.OverridePermanentTransform(modelItems, transform, false);
var tempTestName = $"临时验证_{confirmedCount + 1}_{i}_{DateTime.Now:HHmmss_fff}";
@@ -892,6 +924,8 @@ namespace NavisworksTransport
Item1Position = candidate.Item1Position,
Item2Position = candidate.Item2Position,
Item1YawRadians = candidate.Item1YawRadians,
+ Item1Rotation = candidate.Item1Rotation,
+ Item1HasCustomRotation = candidate.Item1HasCustomRotation,
HasPositionInfo = candidate.HasPositionInfo
};
LogManager.Debug($"[ClashDetective确认] 记录碰撞位置: {confirmedPositionKey}, 位置: ({candidate.Item1Position.X:F2}, {candidate.Item1Position.Y:F2}, {candidate.Item1Position.Z:F2})");
@@ -1006,6 +1040,8 @@ namespace NavisworksTransport
Item1Position = confirmedPosition?.Item1Position,
Item2Position = confirmedPosition?.Item2Position,
Item1YawRadians = confirmedPosition?.Item1YawRadians ?? 0,
+ Item1Rotation = confirmedPosition?.Item1Rotation,
+ Item1HasCustomRotation = confirmedPosition?.Item1HasCustomRotation ?? false,
HasPositionInfo = confirmedPosition != null
};
clashResults.Add(collisionResult);
@@ -2135,6 +2171,8 @@ namespace NavisworksTransport
public Point3D Item1Position { get; set; }
public Point3D Item2Position { get; set; }
public double Item1YawRadians { get; set; } // 运动物体朝向(弧度)
+ public Rotation3D Item1Rotation { get; set; } // 运动物体完整三维姿态
+ public bool Item1HasCustomRotation { get; set; }
public bool HasPositionInfo { get; set; }
// IEquatable 实现:基于碰撞对象进行去重
@@ -2199,4 +2237,4 @@ namespace NavisworksTransport
CollisionCount = collisionCount;
}
}
-}
\ No newline at end of file
+}
diff --git a/src/Core/PathDatabase.cs b/src/Core/PathDatabase.cs
index 2688d96..65d02e0 100644
--- a/src/Core/PathDatabase.cs
+++ b/src/Core/PathDatabase.cs
@@ -239,6 +239,11 @@ namespace NavisworksTransport
Item1PosY REAL,
Item1PosZ REAL,
Item1YawRadians REAL,
+ Item1RotA REAL,
+ Item1RotB REAL,
+ Item1RotC REAL,
+ Item1RotD REAL,
+ Item1HasCustomRotation INTEGER DEFAULT 0,
HasPositionInfo INTEGER DEFAULT 0,
FOREIGN KEY(DetectionRecordId) REFERENCES CollisionDetectionRecords(Id) ON DELETE CASCADE
)
@@ -1109,9 +1114,11 @@ namespace NavisworksTransport
var sql = @"
INSERT INTO ClashDetectiveCollisionObjects
(DetectionRecordId, ModelIndex, PathId, DisplayName, ObjectName,
- Item1PosX, Item1PosY, Item1PosZ, Item1YawRadians, HasPositionInfo)
+ Item1PosX, Item1PosY, Item1PosZ, Item1YawRadians,
+ Item1RotA, Item1RotB, Item1RotC, Item1RotD, Item1HasCustomRotation, HasPositionInfo)
VALUES (@detectionRecordId, @modelIndex, @pathId, @displayName, @objectName,
- @item1PosX, @item1PosY, @item1PosZ, @item1YawRadians, @hasPositionInfo)
+ @item1PosX, @item1PosY, @item1PosZ, @item1YawRadians,
+ @item1RotA, @item1RotB, @item1RotC, @item1RotD, @item1HasCustomRotation, @hasPositionInfo)
";
foreach (var obj in objects)
@@ -1127,6 +1134,11 @@ namespace NavisworksTransport
cmd.Parameters.AddWithValue("@item1PosY", obj.Item1PosY.HasValue ? (object)obj.Item1PosY.Value : DBNull.Value);
cmd.Parameters.AddWithValue("@item1PosZ", obj.Item1PosZ.HasValue ? (object)obj.Item1PosZ.Value : DBNull.Value);
cmd.Parameters.AddWithValue("@item1YawRadians", obj.Item1YawRadians.HasValue ? (object)obj.Item1YawRadians.Value : DBNull.Value);
+ cmd.Parameters.AddWithValue("@item1RotA", obj.Item1RotA.HasValue ? (object)obj.Item1RotA.Value : DBNull.Value);
+ cmd.Parameters.AddWithValue("@item1RotB", obj.Item1RotB.HasValue ? (object)obj.Item1RotB.Value : DBNull.Value);
+ cmd.Parameters.AddWithValue("@item1RotC", obj.Item1RotC.HasValue ? (object)obj.Item1RotC.Value : DBNull.Value);
+ cmd.Parameters.AddWithValue("@item1RotD", obj.Item1RotD.HasValue ? (object)obj.Item1RotD.Value : DBNull.Value);
+ cmd.Parameters.AddWithValue("@item1HasCustomRotation", obj.Item1HasCustomRotation ? 1 : 0);
cmd.Parameters.AddWithValue("@hasPositionInfo", obj.HasPositionInfo ? 1 : 0);
cmd.ExecuteNonQuery();
}
@@ -1154,7 +1166,8 @@ namespace NavisworksTransport
{
var sql = @"
SELECT Id, DetectionRecordId, ModelIndex, PathId, DisplayName, ObjectName,
- Item1PosX, Item1PosY, Item1PosZ, Item1YawRadians, HasPositionInfo
+ Item1PosX, Item1PosY, Item1PosZ, Item1YawRadians,
+ Item1RotA, Item1RotB, Item1RotC, Item1RotD, Item1HasCustomRotation, HasPositionInfo
FROM ClashDetectiveCollisionObjects
WHERE DetectionRecordId = @detectionRecordId
";
@@ -1178,6 +1191,11 @@ namespace NavisworksTransport
Item1PosY = reader["Item1PosY"] != DBNull.Value ? Convert.ToDouble(reader["Item1PosY"]) : (double?)null,
Item1PosZ = reader["Item1PosZ"] != DBNull.Value ? Convert.ToDouble(reader["Item1PosZ"]) : (double?)null,
Item1YawRadians = reader["Item1YawRadians"] != DBNull.Value ? Convert.ToDouble(reader["Item1YawRadians"]) : (double?)null,
+ Item1RotA = reader["Item1RotA"] != DBNull.Value ? Convert.ToDouble(reader["Item1RotA"]) : (double?)null,
+ Item1RotB = reader["Item1RotB"] != DBNull.Value ? Convert.ToDouble(reader["Item1RotB"]) : (double?)null,
+ Item1RotC = reader["Item1RotC"] != DBNull.Value ? Convert.ToDouble(reader["Item1RotC"]) : (double?)null,
+ Item1RotD = reader["Item1RotD"] != DBNull.Value ? Convert.ToDouble(reader["Item1RotD"]) : (double?)null,
+ Item1HasCustomRotation = reader["Item1HasCustomRotation"] != DBNull.Value && Convert.ToInt32(reader["Item1HasCustomRotation"]) == 1,
HasPositionInfo = reader["HasPositionInfo"] != DBNull.Value && Convert.ToInt32(reader["HasPositionInfo"]) == 1
});
}
@@ -3548,6 +3566,11 @@ namespace NavisworksTransport
public double? Item1PosY { get; set; }
public double? Item1PosZ { get; set; }
public double? Item1YawRadians { get; set; }
+ public double? Item1RotA { get; set; }
+ public double? Item1RotB { get; set; }
+ public double? Item1RotC { get; set; }
+ public double? Item1RotD { get; set; }
+ public bool Item1HasCustomRotation { get; set; }
public bool HasPositionInfo { get; set; }
}
diff --git a/src/Core/PathPointRenderPlugin.cs b/src/Core/PathPointRenderPlugin.cs
index 9355a2a..3bdc5f6 100644
--- a/src/Core/PathPointRenderPlugin.cs
+++ b/src/Core/PathPointRenderPlugin.cs
@@ -1501,15 +1501,15 @@ namespace NavisworksTransport
// 计算通行空间的垂直偏移(根据路径类型)
double verticalOffset = 0;
- if (_showObjectSpace)
- {
- if (visualization.PathRoute.PathType == NavisworksTransport.PathType.Ground)
- {
- // 地面路径:路径点是地面位置,通行空间底面在地面,中心需要向下偏移半个高度
- verticalOffset = -height / 2.0;
- }
- else if (visualization.PathRoute.PathType == NavisworksTransport.PathType.Rail)
+ if (_showObjectSpace)
{
+ if (visualization.PathRoute.PathType == NavisworksTransport.PathType.Ground)
+ {
+ // 地面路径:路径点是地面位置,通行空间底面在地面,中心需要向上偏移半个高度
+ verticalOffset = height / 2.0;
+ }
+ else if (visualization.PathRoute.PathType == NavisworksTransport.PathType.Rail)
+ {
verticalOffset = RailPathPoseHelper.GetObjectSpaceCenterZOffset(
visualization.PathRoute,
height);
diff --git a/src/UI/WPF/ViewModels/AnimationControlViewModel.cs b/src/UI/WPF/ViewModels/AnimationControlViewModel.cs
index f62e11c..871d8f2 100644
--- a/src/UI/WPF/ViewModels/AnimationControlViewModel.cs
+++ b/src/UI/WPF/ViewModels/AnimationControlViewModel.cs
@@ -181,6 +181,8 @@ namespace NavisworksTransport.UI.WPF.ViewModels
// 碰撞位置信息(用于还原碰撞场景)
public Point3D Item1Position { get; set; }
public double Item1YawRadians { get; set; }
+ public Rotation3D Item1Rotation { get; set; }
+ public bool Item1HasCustomRotation { get; set; }
public bool HasPositionInfo { get; set; }
}
@@ -2508,6 +2510,8 @@ namespace NavisworksTransport.UI.WPF.ViewModels
null,
collisionObject.Item1Position,
collisionObject.Item1YawRadians,
+ collisionObject.Item1Rotation,
+ collisionObject.Item1HasCustomRotation,
false);
return;
}
@@ -2546,6 +2550,8 @@ namespace NavisworksTransport.UI.WPF.ViewModels
animatedObject,
collisionObject.Item1Position,
collisionObject.Item1YawRadians,
+ collisionObject.Item1Rotation,
+ collisionObject.Item1HasCustomRotation,
collisionObject.HasPositionInfo);
// 🔥 虚拟物体缩放已通过MoveItemToPositionAndYawWithCurrentScale保留
@@ -3666,6 +3672,18 @@ namespace NavisworksTransport.UI.WPF.ViewModels
Item1Position = objRecord.HasPositionInfo && objRecord.Item1PosX.HasValue ?
new Point3D(objRecord.Item1PosX.Value, objRecord.Item1PosY.Value, objRecord.Item1PosZ.Value) : null,
Item1YawRadians = objRecord.Item1YawRadians ?? 0,
+ Item1Rotation = objRecord.Item1HasCustomRotation &&
+ objRecord.Item1RotA.HasValue &&
+ objRecord.Item1RotB.HasValue &&
+ objRecord.Item1RotC.HasValue &&
+ objRecord.Item1RotD.HasValue
+ ? new Rotation3D(
+ objRecord.Item1RotA.Value,
+ objRecord.Item1RotB.Value,
+ objRecord.Item1RotC.Value,
+ objRecord.Item1RotD.Value)
+ : Rotation3D.Identity,
+ Item1HasCustomRotation = objRecord.Item1HasCustomRotation,
HasPositionInfo = objRecord.HasPositionInfo
};
resultViewModel.CollisionObjects.Add(objViewModel);
@@ -5349,4 +5367,4 @@ namespace NavisworksTransport.UI.WPF.ViewModels
#endregion
}
-}
\ No newline at end of file
+}
diff --git a/src/Utils/CollisionSceneHelper.cs b/src/Utils/CollisionSceneHelper.cs
index b2fc291..a1e5327 100644
--- a/src/Utils/CollisionSceneHelper.cs
+++ b/src/Utils/CollisionSceneHelper.cs
@@ -20,7 +20,8 @@ namespace NavisworksTransport.Utils
{
if (collision == null) return;
MoveToCollisionAndFocus(collision.Item2 ?? collision.Item1, animatedObject,
- collision.Item1Position, collision.Item1YawRadians, collision.HasPositionInfo);
+ collision.Item1Position, collision.Item1YawRadians, collision.Item1Rotation,
+ collision.Item1HasCustomRotation, collision.HasPositionInfo);
}
///
@@ -30,9 +31,12 @@ namespace NavisworksTransport.Utils
/// 动画物体(运动物体)
/// 碰撞位置
/// 碰撞朝向
+ /// 碰撞完整姿态
+ /// 是否有完整姿态
/// 是否有位置信息
public static void MoveToCollisionAndFocus(ModelItem hitObject, ModelItem animatedObject,
- Point3D item1Position, double item1YawRadians, bool hasPositionInfo)
+ Point3D item1Position, double item1YawRadians, Rotation3D item1Rotation,
+ bool item1HasCustomRotation, bool hasPositionInfo)
{
try
{
@@ -53,35 +57,59 @@ namespace NavisworksTransport.Utils
// 移动动画物体到碰撞位置
if (animatedObject != null && hasPositionInfo && item1Position != null)
{
- // 计算目标底面位置(Item1Position存储的是包围盒中心,需要转换为底面)
- var bounds = animatedObject.BoundingBox();
- double halfHeight = (bounds.Max.Z - bounds.Min.Z) / 2.0;
- var targetGroundPosition = new Point3D(
- item1Position.X,
- item1Position.Y,
- item1Position.Z - halfHeight
- );
-
+ var pam = PathAnimationManager.GetInstance();
+ bool useAnimationPipeline = pam != null &&
+ pam.ControlsAnimatedObject(animatedObject) &&
+ item1HasCustomRotation &&
+ item1Rotation != null;
+
// 检查是否是虚拟物体
bool isVirtual = VirtualObjectManager.Instance.IsVirtualObjectActive &&
VirtualObjectManager.Instance.CurrentVirtualObject == animatedObject;
-
- // 使用工具方法从CAD原始状态移动到目标位置
- if (isVirtual)
+
+ if (useAnimationPipeline)
{
- // 虚拟物体:保留当前缩放
- ModelItemTransformHelper.MoveItemToPositionAndYawWithCurrentScale(
- animatedObject, targetGroundPosition, item1YawRadians);
+ pam.MoveAnimatedObjectToPose(
+ animatedObject,
+ item1Position,
+ item1YawRadians,
+ item1Rotation,
+ true);
+
+ LogManager.Info(string.Format(
+ "运动物体已通过动画链路移动到碰撞位置: 位置=({0:F2}, {1:F2}, {2:F2}), customRotation={3}",
+ item1Position.X, item1Position.Y, item1Position.Z,
+ true));
+ }
+ else if (item1HasCustomRotation && item1Rotation != null)
+ {
+ throw new InvalidOperationException(
+ $"三维碰撞点恢复必须复用动画主链路,当前对象未受 PathAnimationManager 控制: {animatedObject.DisplayName}");
}
else
{
- // 普通物体:标准移动
- ModelItemTransformHelper.MoveItemToPositionAndYaw(animatedObject, targetGroundPosition, item1YawRadians);
+ var bounds = animatedObject.BoundingBox();
+ double halfHeight = (bounds.Max.Z - bounds.Min.Z) / 2.0;
+ var targetGroundPosition = new Point3D(
+ item1Position.X,
+ item1Position.Y,
+ item1Position.Z - halfHeight
+ );
+
+ if (isVirtual)
+ {
+ ModelItemTransformHelper.MoveItemToPositionAndYawWithCurrentScale(
+ animatedObject, targetGroundPosition, item1YawRadians);
+ }
+ else
+ {
+ ModelItemTransformHelper.MoveItemToPositionAndYaw(animatedObject, targetGroundPosition, item1YawRadians);
+ }
+
+ LogManager.Info(string.Format("运动物体已移动到碰撞位置: ({0:F2}, {1:F2}, {2:F2}), 朝向: {3:F2}°",
+ targetGroundPosition.X, targetGroundPosition.Y, targetGroundPosition.Z,
+ item1YawRadians * 180 / Math.PI));
}
-
- LogManager.Info(string.Format("运动物体已移动到碰撞位置: ({0:F2}, {1:F2}, {2:F2}), 朝向: {3:F2}°",
- targetGroundPosition.X, targetGroundPosition.Y, targetGroundPosition.Z,
- item1YawRadians * 180 / Math.PI));
}
}
catch (Exception ex)
@@ -141,10 +169,27 @@ namespace NavisworksTransport.Utils
try
{
var pam = PathAnimationManager.GetInstance();
- if (pam != null && ReferenceEquals(pam.AnimatedObject, animatedObject) && state.HasCustomRotation)
+ if (state.HasCustomRotation)
{
- pam.RestoreAnimatedObjectState(animatedObject);
- LogManager.Info(string.Format("动画物体已通过PathAnimationManager恢复三维姿态: {0}", animatedObject.DisplayName));
+ if (pam != null && ReferenceEquals(pam.AnimatedObject, animatedObject))
+ {
+ pam.RestoreAnimatedObjectState(animatedObject);
+ LogManager.Info(string.Format("动画物体已通过PathAnimationManager恢复三维姿态: {0}", animatedObject.DisplayName));
+ return;
+ }
+
+ bool isVirtualWithCustomRotation = VirtualObjectManager.Instance.IsVirtualObjectActive &&
+ VirtualObjectManager.Instance.CurrentVirtualObject == animatedObject;
+ if (isVirtualWithCustomRotation)
+ {
+ ModelItemTransformHelper.RestoreObjectStateWithScale(animatedObject, state);
+ }
+ else
+ {
+ ModelItemTransformHelper.RestoreObjectState(animatedObject, state);
+ }
+
+ LogManager.Info(string.Format("动画物体已通过状态快照恢复三维姿态: {0}", animatedObject.DisplayName));
return;
}
diff --git a/src/Utils/ModelItemTransformHelper.cs b/src/Utils/ModelItemTransformHelper.cs
index 99c6a99..b49342a 100644
--- a/src/Utils/ModelItemTransformHelper.cs
+++ b/src/Utils/ModelItemTransformHelper.cs
@@ -507,7 +507,9 @@ namespace NavisworksTransport.Utils
///
/// 将物体移动到指定中心点和完整三维朝向,同时保留当前缩放。
- /// 适用于参考杆、辅助几何等以几何中心作为定位基准的场景。
+ /// 仅适用于“几何中心就是业务定位点”的实体,例如参考杆、辅助几何、虚拟包围盒。
+ /// 不适用于真实模型物体的动画、碰撞验证或场景恢复;真实模型应使用增量三步法,
+ /// 即先将固定参考点移到原点,再旋转,再移动到目标位置。
///
public static void MoveItemToCenterAndRotationWithCurrentScale(ModelItem item, Point3D targetCenter, Rotation3D targetRotation)
{
@@ -550,7 +552,9 @@ namespace NavisworksTransport.Utils
///
/// 将物体移动到指定中心点和完整三维朝向。
/// 仅应用位置和旋转,不保留当前缩放。
- /// 适用于缩放已经在 Model 层完成的参考杆等场景。
+ /// 仅适用于“几何中心就是业务定位点”的实体,例如缩放已经在 Model 层完成的参考杆。
+ /// 不适用于真实模型物体的动画、碰撞验证或场景恢复;真实模型应使用增量三步法,
+ /// 即先将固定参考点移到原点,再旋转,再移动到目标位置。
///
public static void MoveItemToCenterAndRotation(ModelItem item, Point3D targetCenter, Rotation3D targetRotation)
{
From c9a926356c5ddaff359bd71f58cd0215e80f8431 Mon Sep 17 00:00:00 2001
From: tian <11429339@qq.com>
Date: Fri, 20 Mar 2026 15:22:58 +0800
Subject: [PATCH 07/87] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E7=BB=88=E7=AB=AF?=
=?UTF-8?q?=E5=AE=89=E8=A3=85=E4=BB=BF=E7=9C=9FUI=EF=BC=8C=E5=8D=87?=
=?UTF-8?q?=E7=BA=A7=E6=95=B0=E6=8D=AE=E5=BA=93=E7=89=88=E6=9C=AC=E5=8F=B7?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/Core/PathDatabase.cs | 4 ++--
src/UI/WPF/ViewModels/PathEditingViewModel.cs | 24 +++++++++----------
src/UI/WPF/Views/PathEditingView.xaml | 14 ++++-------
3 files changed, 19 insertions(+), 23 deletions(-)
diff --git a/src/Core/PathDatabase.cs b/src/Core/PathDatabase.cs
index 65d02e0..9477166 100644
--- a/src/Core/PathDatabase.cs
+++ b/src/Core/PathDatabase.cs
@@ -300,8 +300,8 @@ namespace NavisworksTransport
// 12. 设置数据库版本(SQLite内置user_version)
// 版本号格式:主版本*10000 + 次版本*100 + 修订号
- // 例如:2.1.3 = 20103
- ExecuteNonQuery("PRAGMA user_version = 20103"); // v2.1.3 - 重命名DetectionToleranceMeters为DetectionTolerance
+ // 例如:2.1.4 = 20104
+ ExecuteNonQuery("PRAGMA user_version = 20104"); // v2.1.4 - ClashDetective碰撞对象位姿字段扩展与Rail三维恢复链路调整
// 创建索引
ExecuteNonQuery("CREATE INDEX IF NOT EXISTS idx_reports_route ON CollisionReports(RouteId)");
diff --git a/src/UI/WPF/ViewModels/PathEditingViewModel.cs b/src/UI/WPF/ViewModels/PathEditingViewModel.cs
index 877389a..cdbeba0 100644
--- a/src/UI/WPF/ViewModels/PathEditingViewModel.cs
+++ b/src/UI/WPF/ViewModels/PathEditingViewModel.cs
@@ -1302,7 +1302,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
RenderAssemblyReferenceLine(referenceLine);
FocusOnAssemblyReferenceArea(referenceLine.StartPoint, referenceLine.EndPoint);
- UpdateMainStatus("已生成直线装配参考杆,请在三维视图中确认终点锚点、方向、外端位置和杆体贴合情况");
+ UpdateMainStatus("已生成终端安装辅助线,请在三维视图中确认终点锚点、方向、外端位置和辅助线贴合情况");
OnPropertyChanged(nameof(CanSelectAssemblyStartPoint));
LogManager.Info(
$"[直线装配] 已生成参考杆: {AssemblyTerminalObjectName}, " +
@@ -1310,7 +1310,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
$"参考线外端=({referenceLine.StartPoint.X:F2}, {referenceLine.StartPoint.Y:F2}, {referenceLine.StartPoint.Z:F2}), " +
$"方向=({referenceLine.Direction.X:F3}, {referenceLine.Direction.Y:F3}, {referenceLine.Direction.Z:F3}), " +
$"资源={AssemblyReferencePathManager.Instance.ReferenceResourceName}");
- }, "生成装配参考杆");
+ }, "生成终端安装辅助线");
}
private async Task ExecuteSelectAssemblyStartPointAsync()
@@ -1329,7 +1329,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
if (!AssemblyReferencePathManager.Instance.HasReferenceLine)
{
- throw new InvalidOperationException("请先生成直线装配参考杆");
+ throw new InvalidOperationException("请先生成终端安装辅助线");
}
CleanupAssemblyReferenceSelection();
@@ -1348,9 +1348,9 @@ namespace NavisworksTransport.UI.WPF.ViewModels
}
_pathPlanningManager.StartClickTool(PathPointType.StartPoint);
- UpdateMainStatus("请在参考杆附近点击一个起点,系统会自动吸附到参考线上并生成直线路径");
+ UpdateMainStatus("请在辅助线附近点击一个起点,系统会自动吸附到辅助线上并生成直线路径");
LogManager.Info("[直线装配] 已进入起点拾取模式");
- }, "选择直线装配起点");
+ }, "选择终端安装起点");
}
private void ExecuteClearAssemblyReferenceRod()
@@ -1364,12 +1364,12 @@ namespace NavisworksTransport.UI.WPF.ViewModels
ClearAssemblyReferenceLine();
AssemblyStartPointText = "未选择";
OnPropertyChanged(nameof(CanSelectAssemblyStartPoint));
- UpdateMainStatus("已隐藏直线装配参考线");
- LogManager.Info("[直线装配] 已隐藏参考线");
+ UpdateMainStatus("已隐藏终端安装辅助线");
+ LogManager.Info("[直线装配] 已隐藏辅助线");
}
catch (Exception ex)
{
- LogManager.Error($"[直线装配] 隐藏参考线失败: {ex.Message}");
+ LogManager.Error($"[直线装配] 隐藏辅助线失败: {ex.Message}");
}
}
@@ -1393,13 +1393,13 @@ namespace NavisworksTransport.UI.WPF.ViewModels
{
CreateAssemblyLinearRoute(projectedStartPoint);
CleanupAssemblyReferenceSelection();
- UpdateMainStatus("已根据参考杆起点生成直线装配路径");
- }, "生成直线装配路径");
+ UpdateMainStatus("已根据辅助线起点生成终端安装路径");
+ }, "生成终端安装路径");
}
catch (Exception ex)
{
LogManager.Error($"[直线装配] 处理起点拾取失败: {ex.Message}", ex);
- UpdateMainStatus($"直线装配起点拾取失败: {ex.Message}");
+ UpdateMainStatus($"终端安装起点拾取失败: {ex.Message}");
CleanupAssemblyReferenceSelection();
}
}
@@ -1521,7 +1521,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
if (referenceStartPoint != null && referenceEndPoint != null)
{
AssemblyTerminalObjectInfo = string.Format(
- "中心=({0:F2}, {1:F2}, {2:F2}),尺寸=({3:F2}, {4:F2}, {5:F2}),{6},{7}点=({8:F2}, {9:F2}, {10:F2}),垂直偏移={11:F3}m,参考杆起点=({12:F2}, {13:F2}, {14:F2})",
+ "中心=({0:F2}, {1:F2}, {2:F2}),尺寸=({3:F2}, {4:F2}, {5:F2}),{6},{7}点=({8:F2}, {9:F2}, {10:F2}),垂直偏移={11:F3}m,辅助线外端=({12:F2}, {13:F2}, {14:F2})",
bounds.Center.X,
bounds.Center.Y,
bounds.Center.Z,
diff --git a/src/UI/WPF/Views/PathEditingView.xaml b/src/UI/WPF/Views/PathEditingView.xaml
index 5fdc00d..bc48783 100644
--- a/src/UI/WPF/Views/PathEditingView.xaml
+++ b/src/UI/WPF/Views/PathEditingView.xaml
@@ -305,10 +305,10 @@ NavisworksTransport 路径编辑页签视图 - 采用与动画控制和分层管
BorderBrush="#FFD4E7FF"
BorderThickness="1">
-
-
@@ -366,10 +366,6 @@ NavisworksTransport 路径编辑页签视图 - 采用与动画控制和分层管
Text="{Binding AssemblyTerminalObjectName}"
Style="{StaticResource ReadOnlyTextBoxStyle}"/>
-
@@ -394,7 +390,7 @@ NavisworksTransport 路径编辑页签视图 - 采用与动画控制和分层管
-
@@ -426,7 +422,7 @@ NavisworksTransport 路径编辑页签视图 - 采用与动画控制和分层管
Command="{Binding SelectAssemblyStartPointCommand}"
Style="{StaticResource ActionButtonStyle}"
IsEnabled="{Binding CanSelectAssemblyStartPoint}"/>
-
From a651b69459ed90d10238e7eefb5c6dffe81b817f Mon Sep 17 00:00:00 2001
From: tian <11429339@qq.com>
Date: Fri, 20 Mar 2026 15:42:16 +0800
Subject: [PATCH 08/87] Fix batch queue detection record snapshots
---
src/Core/BatchQueueManager.cs | 7 +-
.../ViewModels/AnimationControlViewModel.cs | 270 +++++++++---------
2 files changed, 140 insertions(+), 137 deletions(-)
diff --git a/src/Core/BatchQueueManager.cs b/src/Core/BatchQueueManager.cs
index 38b90f2..55ed6b0 100644
--- a/src/Core/BatchQueueManager.cs
+++ b/src/Core/BatchQueueManager.cs
@@ -391,6 +391,11 @@ namespace NavisworksTransport.Core
try
{
+ if (!item.DetectionRecordId.HasValue)
+ {
+ throw new InvalidOperationException("批处理队列项缺少 DetectionRecordId,无法保存完整碰撞结果和位姿数据。请重新生成动画后再加入批处理。");
+ }
+
var pathRoute = await _database.GetPathRouteAsync(item.RouteId);
if (pathRoute == null)
{
@@ -1072,4 +1077,4 @@ namespace NavisworksTransport.Core
{
public BatchQueueItem Item { get; set; }
}
-}
\ No newline at end of file
+}
diff --git a/src/UI/WPF/ViewModels/AnimationControlViewModel.cs b/src/UI/WPF/ViewModels/AnimationControlViewModel.cs
index 871d8f2..3308bf8 100644
--- a/src/UI/WPF/ViewModels/AnimationControlViewModel.cs
+++ b/src/UI/WPF/ViewModels/AnimationControlViewModel.cs
@@ -3134,126 +3134,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
LogManager.Info($"[检测记录] 用户选择重新生成,创建新记录");
}
- // 生成测试名称(将在ClashDetective完成后更新为正式名称)
- string testName = $"动画检测_{DateTime.Now:MMdd_HHmmssfff}";
-
- // 创建检测记录
- var record = new CollisionDetectionRecord
- {
- RouteId = routeId,
- CreatedTime = DateTime.Now,
- FrameRate = _animationFrameRate,
- DurationSeconds = AnimationDuration,
- // 检测容差:UI为米单位,内部使用模型单位
- DetectionTolerance = _detectionTolerance * UnitsConverter.GetMetersToUnitsConversionFactor(),
- IsVirtualObject = UseVirtualObject,
- AnimatedObjectName = UseVirtualObject ? "虚拟物体" : SelectedAnimatedObject?.DisplayName,
- DetectAllObjects = !IsManualCollisionTargetEnabled,
- ObjectRotationCorrection = _objectRotationCorrection,
- Description = $"动画生成于 {DateTime.Now:yyyy-MM-dd HH:mm:ss}",
- // 设置碰撞检测相关字段
- TestName = testName,
- TestTime = DateTime.Now,
- CollisionCount = 0, // 初始为0,将在ClashDetective完成后更新
- AnimationCollisionCount = 0
- };
-
- // 虚拟物体尺寸(米 → 模型单位,数据库存储模型单位)
- if (UseVirtualObject)
- {
- double metersToUnits = UnitsConverter.GetMetersToUnitsConversionFactor();
- record.VirtualObjectLength = VirtualObjectLengthInMeters * metersToUnits;
- record.VirtualObjectWidth = VirtualObjectWidthInMeters * metersToUnits;
- record.VirtualObjectHeight = VirtualObjectHeightInMeters * metersToUnits;
- }
- // 真实物体信息
- else if (SelectedAnimatedObject != null)
- {
- try
- {
- var pathId = Autodesk.Navisworks.Api.Application.ActiveDocument.Models.CreatePathId(SelectedAnimatedObject);
- record.ObjectModelIndex = pathId.ModelIndex;
- record.ObjectPathId = pathId.PathId;
- }
- catch (Exception ex)
- {
- LogManager.Warning($"[检测记录] 获取运动物体PathId失败: {ex.Message}");
- }
- }
-
- // 保存检测记录
- int recordId = pathDatabase.SaveCollisionDetectionRecord(record);
- LogManager.Info($"[检测记录] 已创建记录 (Id={recordId})");
-
- // 🔥 设置当前检测记录ID到PathAnimationManager(供碰撞结果保存时关联使用)
- _pathAnimationManager.CurrentDetectionRecordId = recordId;
- _pathAnimationManager.IsUsingHistoryRecord = false; // 新记录,不是历史记录
-
- // 保存排除列表
- var excludedObjects = _pathAnimationManager?.GetExcludedObjects();
- if (excludedObjects != null && excludedObjects.Count > 0)
- {
- var excludedRecords = new List();
- foreach (var obj in excludedObjects)
- {
- if (obj == null) continue;
- try
- {
- var pathId = Autodesk.Navisworks.Api.Application.ActiveDocument.Models.CreatePathId(obj);
- excludedRecords.Add(new CollisionDetectionExcludedObjectRecord
- {
- DetectionRecordId = recordId,
- ModelIndex = pathId.ModelIndex,
- PathId = pathId.PathId,
- DisplayName = obj.DisplayName,
- ObjectName = obj.DisplayName,
- Reason = "预计算碰撞分析排除"
- });
- }
- catch (Exception ex)
- {
- LogManager.Warning($"[检测记录] 准备排除对象记录失败: {obj.DisplayName}, {ex.Message}");
- }
- }
-
- if (excludedRecords.Count > 0)
- {
- pathDatabase.SaveCollisionDetectionExcludedObjects(recordId, excludedRecords);
- LogManager.Info($"[检测记录] 已保存 {excludedRecords.Count} 个排除对象");
- }
- }
-
- // 保存手工目标
- if (IsManualCollisionTargetEnabled && _manualCollisionTargets != null && _manualCollisionTargets.Count > 0)
- {
- var manualTargetRecords = new List();
- foreach (var target in _manualCollisionTargets)
- {
- if (target?.ModelItem == null) continue;
- try
- {
- var pathId = Autodesk.Navisworks.Api.Application.ActiveDocument.Models.CreatePathId(target.ModelItem);
- manualTargetRecords.Add(new CollisionDetectionManualTargetRecord
- {
- DetectionRecordId = recordId,
- ModelIndex = pathId.ModelIndex,
- PathId = pathId.PathId,
- DisplayName = target.ModelItem.DisplayName,
- ObjectName = target.ModelItem.DisplayName
- });
- }
- catch (Exception ex)
- {
- LogManager.Warning($"[检测记录] 准备手工目标记录失败: {target.DisplayName}, {ex.Message}");
- }
- }
-
- if (manualTargetRecords.Count > 0)
- {
- pathDatabase.SaveCollisionDetectionManualTargets(recordId, manualTargetRecords);
- LogManager.Info($"[检测记录] 已保存 {manualTargetRecords.Count} 个手工目标");
- }
- }
+ int recordId = CreateCollisionDetectionRecordSnapshot("动画生成", bindToAnimationManager: true);
// 🔥 注册动画配置哈希与检测记录ID的映射(用于后续检查重复配置)
try
@@ -3280,6 +3161,136 @@ namespace NavisworksTransport.UI.WPF.ViewModels
}
}
+ ///
+ /// 创建碰撞检测记录快照。
+ /// 生成动画和批处理都应该创建自己的配置快照,不依赖旧检测结果。
+ ///
+ private int CreateCollisionDetectionRecordSnapshot(string scenarioName, bool bindToAnimationManager)
+ {
+ var pathDatabase = _pathPlanningManager?.GetPathDatabase();
+ if (pathDatabase == null)
+ {
+ throw new InvalidOperationException("数据库未初始化,无法保存检测记录");
+ }
+
+ string routeId = CurrentPathRoute?.Id ?? string.Empty;
+ string testName = $"{scenarioName}_{DateTime.Now:MMdd_HHmmssfff}";
+ double metersToUnits = UnitsConverter.GetMetersToUnitsConversionFactor();
+
+ var record = new CollisionDetectionRecord
+ {
+ RouteId = routeId,
+ CreatedTime = DateTime.Now,
+ FrameRate = _animationFrameRate,
+ DurationSeconds = AnimationDuration,
+ DetectionTolerance = _detectionTolerance * metersToUnits,
+ IsVirtualObject = UseVirtualObject,
+ AnimatedObjectName = UseVirtualObject ? "虚拟物体" : SelectedAnimatedObject?.DisplayName,
+ DetectAllObjects = !IsManualCollisionTargetEnabled,
+ ObjectRotationCorrection = _objectRotationCorrection,
+ Description = $"{scenarioName}于 {DateTime.Now:yyyy-MM-dd HH:mm:ss}",
+ TestName = testName,
+ TestTime = DateTime.Now,
+ CollisionCount = 0,
+ AnimationCollisionCount = 0
+ };
+
+ if (UseVirtualObject)
+ {
+ record.VirtualObjectLength = VirtualObjectLengthInMeters * metersToUnits;
+ record.VirtualObjectWidth = VirtualObjectWidthInMeters * metersToUnits;
+ record.VirtualObjectHeight = VirtualObjectHeightInMeters * metersToUnits;
+ }
+ else if (SelectedAnimatedObject != null)
+ {
+ try
+ {
+ var pathId = Autodesk.Navisworks.Api.Application.ActiveDocument.Models.CreatePathId(SelectedAnimatedObject);
+ record.ObjectModelIndex = pathId.ModelIndex;
+ record.ObjectPathId = pathId.PathId;
+ }
+ catch (Exception ex)
+ {
+ LogManager.Warning($"[检测记录] 获取运动物体PathId失败: {ex.Message}");
+ }
+ }
+
+ int recordId = pathDatabase.SaveCollisionDetectionRecord(record);
+ LogManager.Info($"[检测记录] 已创建{scenarioName}记录 (Id={recordId})");
+
+ if (bindToAnimationManager)
+ {
+ _pathAnimationManager.CurrentDetectionRecordId = recordId;
+ _pathAnimationManager.IsUsingHistoryRecord = false;
+ }
+
+ var excludedObjects = _pathAnimationManager?.GetExcludedObjects();
+ if (excludedObjects != null && excludedObjects.Count > 0)
+ {
+ var excludedRecords = new List();
+ foreach (var obj in excludedObjects)
+ {
+ if (obj == null) continue;
+ try
+ {
+ var pathId = Autodesk.Navisworks.Api.Application.ActiveDocument.Models.CreatePathId(obj);
+ excludedRecords.Add(new CollisionDetectionExcludedObjectRecord
+ {
+ DetectionRecordId = recordId,
+ ModelIndex = pathId.ModelIndex,
+ PathId = pathId.PathId,
+ DisplayName = obj.DisplayName,
+ ObjectName = obj.DisplayName,
+ Reason = "预计算碰撞分析排除"
+ });
+ }
+ catch (Exception ex)
+ {
+ LogManager.Warning($"[检测记录] 准备排除对象记录失败: {obj.DisplayName}, {ex.Message}");
+ }
+ }
+
+ if (excludedRecords.Count > 0)
+ {
+ pathDatabase.SaveCollisionDetectionExcludedObjects(recordId, excludedRecords);
+ LogManager.Info($"[检测记录] 已保存 {excludedRecords.Count} 个排除对象");
+ }
+ }
+
+ if (IsManualCollisionTargetEnabled && _manualCollisionTargets != null && _manualCollisionTargets.Count > 0)
+ {
+ var manualTargetRecords = new List();
+ foreach (var target in _manualCollisionTargets)
+ {
+ if (target?.ModelItem == null) continue;
+ try
+ {
+ var pathId = Autodesk.Navisworks.Api.Application.ActiveDocument.Models.CreatePathId(target.ModelItem);
+ manualTargetRecords.Add(new CollisionDetectionManualTargetRecord
+ {
+ DetectionRecordId = recordId,
+ ModelIndex = pathId.ModelIndex,
+ PathId = pathId.PathId,
+ DisplayName = target.ModelItem.DisplayName,
+ ObjectName = target.ModelItem.DisplayName
+ });
+ }
+ catch (Exception ex)
+ {
+ LogManager.Warning($"[检测记录] 准备手工目标记录失败: {target.DisplayName}, {ex.Message}");
+ }
+ }
+
+ if (manualTargetRecords.Count > 0)
+ {
+ pathDatabase.SaveCollisionDetectionManualTargets(recordId, manualTargetRecords);
+ LogManager.Info($"[检测记录] 已保存 {manualTargetRecords.Count} 个手工目标");
+ }
+ }
+
+ return recordId;
+ }
+
///
/// 用户选择析析
///
@@ -5210,22 +5221,9 @@ namespace NavisworksTransport.UI.WPF.ViewModels
LogManager.Info($"[批处理] 添加路径到批处理队列: {CurrentPathRoute.Name}");
- // 🔥 获取该路径最新的检测记录ID
- int? detectionRecordId = null;
- var pathDatabase = _pathPlanningManager?.GetPathDatabase();
- if (pathDatabase != null)
- {
- var latestRecord = pathDatabase.GetLatestCollisionDetectionRecord(CurrentPathRoute.Id);
- detectionRecordId = latestRecord?.Id;
- if (detectionRecordId.HasValue)
- {
- LogManager.Info($"[批处理] 关联检测记录 (Id={detectionRecordId})");
- }
- else
- {
- LogManager.Warning("[批处理] 未找到检测记录,请先运行\"生成动画\"");
- }
- }
+ // 为批处理创建独立的检测记录快照,保存当前配置、排除列表和手工目标
+ int detectionRecordId = CreateCollisionDetectionRecordSnapshot("批处理任务创建", bindToAnimationManager: false);
+ LogManager.Info($"[批处理] 已创建批处理检测记录 (Id={detectionRecordId})");
// 创建批处理队列项
// 注意:虚拟物体尺寸需要从米转换为模型单位存入数据库
From 3fd184934bc72d2e59aeac6e7e5067b4cc3a6f9d Mon Sep 17 00:00:00 2001
From: tian <11429339@qq.com>
Date: Sat, 21 Mar 2026 06:10:34 +0800
Subject: [PATCH 09/87] Establish canonical coordinate framework with tests
---
NavisworksTransport.UnitTests.csproj | 69 +++
TransportPlugin.csproj | 7 +
TransportPlugin.sln | 10 +
.../CanonicalRailPoseBuilderTests.cs | 92 ++++
.../HostCoordinateAdapterTests.cs | 69 +++
.../ModelAxisConventionTests.cs | 87 +++
.../ProjectReferenceFrameTests.cs | 38 ++
UnitTests/Properties/AssemblyInfo.cs | 15 +
...oordinate-system-canonical-space-design.md | 503 ++++++++++++++++++
doc/guide/design_principles.md | 292 ++++++++++
...tem-canonical-space-implementation-plan.md | 362 +++++++++++++
run-unit-tests.bat | 50 ++
src/Core/Animation/PathAnimationManager.cs | 42 +-
src/Core/PathPointRenderPlugin.cs | 171 +++---
src/PathPlanning/ChannelBasedGridBuilder.cs | 6 +-
src/UI/WPF/ViewModels/PathEditingViewModel.cs | 114 +++-
src/UI/WPF/Views/PathEditingView.xaml | 66 ++-
.../CoordinateSystem/CanonicalBounds3.cs | 20 +
.../CanonicalRailPoseBuilder.cs | 83 +++
.../CoordinateSystemManager.cs | 35 ++
.../CoordinateSystem/HostCoordinateAdapter.cs | 274 ++++++++++
.../CoordinateSystem/LocalAxisDirection.cs | 18 +
.../CoordinateSystem/ModelAxisConvention.cs | 233 ++++++++
.../CoordinateSystem/ProjectReferenceFrame.cs | 97 ++++
src/Utils/ModelItemTransformHelper.cs | 53 ++
src/Utils/RailPathPoseHelper.cs | 179 +++++--
26 files changed, 2809 insertions(+), 176 deletions(-)
create mode 100644 NavisworksTransport.UnitTests.csproj
create mode 100644 UnitTests/CoordinateSystem/CanonicalRailPoseBuilderTests.cs
create mode 100644 UnitTests/CoordinateSystem/HostCoordinateAdapterTests.cs
create mode 100644 UnitTests/CoordinateSystem/ModelAxisConventionTests.cs
create mode 100644 UnitTests/CoordinateSystem/ProjectReferenceFrameTests.cs
create mode 100644 UnitTests/Properties/AssemblyInfo.cs
create mode 100644 doc/design/2026/coordinate-system-canonical-space-design.md
create mode 100644 doc/working/coordinate-system-canonical-space-implementation-plan.md
create mode 100644 run-unit-tests.bat
create mode 100644 src/Utils/CoordinateSystem/CanonicalBounds3.cs
create mode 100644 src/Utils/CoordinateSystem/CanonicalRailPoseBuilder.cs
create mode 100644 src/Utils/CoordinateSystem/HostCoordinateAdapter.cs
create mode 100644 src/Utils/CoordinateSystem/LocalAxisDirection.cs
create mode 100644 src/Utils/CoordinateSystem/ModelAxisConvention.cs
create mode 100644 src/Utils/CoordinateSystem/ProjectReferenceFrame.cs
diff --git a/NavisworksTransport.UnitTests.csproj b/NavisworksTransport.UnitTests.csproj
new file mode 100644
index 0000000..c58c383
--- /dev/null
+++ b/NavisworksTransport.UnitTests.csproj
@@ -0,0 +1,69 @@
+
+
+
+
+ Debug
+ x64
+ {7DDB41A4-A10B-4EA4-A658-0D5A9178A1F5}
+ Library
+ Properties
+ NavisworksTransport.UnitTests
+ NavisworksTransport.UnitTests
+ v4.8
+ 512
+ true
+
+
+ true
+ bin\x64\Debug\
+ DEBUG;TRACE
+ full
+ x64
+ 7.3
+ prompt
+
+
+ bin\x64\Release\
+ TRACE
+ true
+ pdbonly
+ x64
+ 7.3
+ prompt
+
+
+
+ ..\..\..\..\Program Files\Autodesk\Navisworks Manage 2026\Autodesk.Navisworks.Api.dll
+ True
+
+
+ packages\MSTest.TestFramework.3.0.4\lib\net462\Microsoft.VisualStudio.TestPlatform.TestFramework.dll
+
+
+ packages\MSTest.TestFramework.3.0.4\lib\net462\Microsoft.VisualStudio.TestPlatform.TestFramework.Extensions.dll
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {1A0124F6-3DEB-4153-8760-F568AD9393EE}
+ TransportPlugin
+
+
+
+
diff --git a/TransportPlugin.csproj b/TransportPlugin.csproj
index 79d391a..9db0354 100644
--- a/TransportPlugin.csproj
+++ b/TransportPlugin.csproj
@@ -63,6 +63,7 @@
+
packages\Newtonsoft.Json.13.0.3\lib\net45\Newtonsoft.Json.dll
@@ -330,6 +331,12 @@
+
+
+
+
+
+
diff --git a/TransportPlugin.sln b/TransportPlugin.sln
index 7b0594e..3f2571b 100644
--- a/TransportPlugin.sln
+++ b/TransportPlugin.sln
@@ -5,6 +5,8 @@ VisualStudioVersion = 17.14.36203.30
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TransportPlugin", "TransportPlugin.csproj", "{1A0124F6-3DEB-4153-8760-F568AD9393EE}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NavisworksTransport.UnitTests", "NavisworksTransport.UnitTests.csproj", "{7DDB41A4-A10B-4EA4-A658-0D5A9178A1F5}"
+EndProject
Project("{54435603-DBB4-11D2-8724-00A0C9A8B90C}") = "TransportPlugin.Setup", "..\TransportPlugin.Setup\TransportPlugin.Setup.vdproj", "{E1955F72-A686-9398-1C6A-936493D9211F}"
EndProject
Global
@@ -23,6 +25,14 @@ Global
{1A0124F6-3DEB-4153-8760-F568AD9393EE}.Release|Any CPU.Build.0 = Release|Any CPU
{1A0124F6-3DEB-4153-8760-F568AD9393EE}.Release|x64.ActiveCfg = Release|x64
{1A0124F6-3DEB-4153-8760-F568AD9393EE}.Release|x64.Build.0 = Release|x64
+ {7DDB41A4-A10B-4EA4-A658-0D5A9178A1F5}.Debug|Any CPU.ActiveCfg = Debug|x64
+ {7DDB41A4-A10B-4EA4-A658-0D5A9178A1F5}.Debug|Any CPU.Build.0 = Debug|x64
+ {7DDB41A4-A10B-4EA4-A658-0D5A9178A1F5}.Debug|x64.ActiveCfg = Debug|x64
+ {7DDB41A4-A10B-4EA4-A658-0D5A9178A1F5}.Debug|x64.Build.0 = Debug|x64
+ {7DDB41A4-A10B-4EA4-A658-0D5A9178A1F5}.Release|Any CPU.ActiveCfg = Release|x64
+ {7DDB41A4-A10B-4EA4-A658-0D5A9178A1F5}.Release|Any CPU.Build.0 = Release|x64
+ {7DDB41A4-A10B-4EA4-A658-0D5A9178A1F5}.Release|x64.ActiveCfg = Release|x64
+ {7DDB41A4-A10B-4EA4-A658-0D5A9178A1F5}.Release|x64.Build.0 = Release|x64
{E1955F72-A686-9398-1C6A-936493D9211F}.Debug|Any CPU.ActiveCfg = Debug
{E1955F72-A686-9398-1C6A-936493D9211F}.Debug|x64.ActiveCfg = Debug
{E1955F72-A686-9398-1C6A-936493D9211F}.Debug|x64.Build.0 = Debug
diff --git a/UnitTests/CoordinateSystem/CanonicalRailPoseBuilderTests.cs b/UnitTests/CoordinateSystem/CanonicalRailPoseBuilderTests.cs
new file mode 100644
index 0000000..b337260
--- /dev/null
+++ b/UnitTests/CoordinateSystem/CanonicalRailPoseBuilderTests.cs
@@ -0,0 +1,92 @@
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using NavisworksTransport.Utils.CoordinateSystem;
+using System.Numerics;
+
+namespace NavisworksTransport.UnitTests.CoordinateSystem
+{
+ [TestClass]
+ public class CanonicalRailPoseBuilderTests
+ {
+ [TestMethod]
+ public void StraightCanonicalPath_ZUpConvention_ShouldProduceIdentityLikeBasis()
+ {
+ bool ok = CanonicalRailPoseBuilder.TryCreateQuaternion(
+ new Vector3(0, 0, 0),
+ new Vector3(1, 0, 0),
+ new Vector3(2, 0, 0),
+ Vector3.UnitZ,
+ ModelAxisConvention.CreateDefaultForHost(CoordinateSystemType.ZUp),
+ out Quaternion rotation);
+
+ Assert.IsTrue(ok);
+ Matrix4x4 linear = Matrix4x4.CreateFromQuaternion(rotation);
+ AssertColumn(linear, 0, 1, 0, 0);
+ AssertColumn(linear, 1, 0, 1, 0);
+ AssertColumn(linear, 2, 0, 0, 1);
+ }
+
+ [TestMethod]
+ public void StraightCanonicalPath_YUpConvention_ShouldMapLocalYToCanonicalUp()
+ {
+ bool ok = CanonicalRailPoseBuilder.TryCreateQuaternion(
+ new Vector3(0, 0, 0),
+ new Vector3(1, 0, 0),
+ new Vector3(2, 0, 0),
+ Vector3.UnitZ,
+ ModelAxisConvention.CreateDefaultForHost(CoordinateSystemType.YUp),
+ out Quaternion rotation);
+
+ Assert.IsTrue(ok);
+ Matrix4x4 linear = Matrix4x4.CreateFromQuaternion(rotation);
+ AssertColumn(linear, 0, 1, 0, 0);
+ AssertColumn(linear, 1, 0, 0, 1);
+ AssertColumn(linear, 2, 0, -1, 0);
+ }
+
+ [TestMethod]
+ public void SlopedPath_ShouldKeepForwardAlignedWithPathAndUpOrthogonalized()
+ {
+ bool ok = CanonicalRailPoseBuilder.TryCreateBasis(
+ new Vector3(0, 0, 0),
+ new Vector3(1, 1, 1),
+ new Vector3(2, 2, 2),
+ Vector3.UnitZ,
+ out Vector3 forward,
+ out Vector3 lateral,
+ out Vector3 up);
+
+ Assert.IsTrue(ok);
+ Assert.AreEqual(1.0, forward.Length(), 1e-6);
+ Assert.AreEqual(1.0, lateral.Length(), 1e-6);
+ Assert.AreEqual(1.0, up.Length(), 1e-6);
+ Assert.AreEqual(0.0, Vector3.Dot(forward, up), 1e-6);
+ Assert.AreEqual(0.0, Vector3.Dot(forward, lateral), 1e-6);
+ Assert.AreEqual(0.0, Vector3.Dot(lateral, up), 1e-6);
+ }
+
+ private static void AssertColumn(Matrix4x4 matrix, int column, double x, double y, double z)
+ {
+ switch (column)
+ {
+ case 0:
+ Assert.AreEqual(x, matrix.M11, 1e-6);
+ Assert.AreEqual(y, matrix.M21, 1e-6);
+ Assert.AreEqual(z, matrix.M31, 1e-6);
+ break;
+ case 1:
+ Assert.AreEqual(x, matrix.M12, 1e-6);
+ Assert.AreEqual(y, matrix.M22, 1e-6);
+ Assert.AreEqual(z, matrix.M32, 1e-6);
+ break;
+ case 2:
+ Assert.AreEqual(x, matrix.M13, 1e-6);
+ Assert.AreEqual(y, matrix.M23, 1e-6);
+ Assert.AreEqual(z, matrix.M33, 1e-6);
+ break;
+ default:
+ Assert.Fail("Only first 3 columns are valid.");
+ break;
+ }
+ }
+ }
+}
diff --git a/UnitTests/CoordinateSystem/HostCoordinateAdapterTests.cs b/UnitTests/CoordinateSystem/HostCoordinateAdapterTests.cs
new file mode 100644
index 0000000..0a97569
--- /dev/null
+++ b/UnitTests/CoordinateSystem/HostCoordinateAdapterTests.cs
@@ -0,0 +1,69 @@
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using NavisworksTransport.Utils.CoordinateSystem;
+using System.Numerics;
+
+namespace NavisworksTransport.UnitTests.CoordinateSystem
+{
+ [TestClass]
+ public class HostCoordinateAdapterTests
+ {
+ [TestMethod]
+ public void ZUp_PointRoundTrip_ShouldRemainUnchanged()
+ {
+ var adapter = new HostCoordinateAdapter(CoordinateSystemType.ZUp);
+ var hostPoint = new Vector3(1.5f, -2.5f, 3.5f);
+
+ Vector3 canonicalPoint = adapter.ToCanonicalPoint3(hostPoint);
+ Vector3 roundTripPoint = adapter.FromCanonicalPoint3(canonicalPoint);
+
+ AssertPoint(roundTripPoint, 1.5, -2.5, 3.5);
+ }
+
+ [TestMethod]
+ public void YUp_PointAndVectorRoundTrip_ShouldMatchExpectedMapping()
+ {
+ var adapter = new HostCoordinateAdapter(CoordinateSystemType.YUp);
+ var hostPoint = new Vector3(1.0f, 2.0f, 3.0f);
+ var hostVector = new Vector3(4.0f, 5.0f, 6.0f);
+
+ Vector3 canonicalPoint = adapter.ToCanonicalPoint3(hostPoint);
+ Vector3 canonicalVector = adapter.ToCanonicalVector3(hostVector);
+
+ AssertPoint(canonicalPoint, 1.0, -3.0, 2.0);
+ AssertVector(canonicalVector, 4.0, -6.0, 5.0);
+
+ Vector3 restoredPoint = adapter.FromCanonicalPoint3(canonicalPoint);
+ Vector3 restoredVector = adapter.FromCanonicalVector3(canonicalVector);
+
+ AssertPoint(restoredPoint, 1.0, 2.0, 3.0);
+ AssertVector(restoredVector, 4.0, 5.0, 6.0);
+ }
+
+ [TestMethod]
+ public void YUp_BoundsRoundTrip_ShouldPreserveBounds()
+ {
+ var adapter = new HostCoordinateAdapter(CoordinateSystemType.YUp);
+ var hostBounds = new CanonicalBounds3(new Vector3(-2.0f, 1.0f, 3.0f), new Vector3(5.0f, 7.0f, 11.0f));
+
+ CanonicalBounds3 canonicalBounds = adapter.ToCanonicalBounds3(hostBounds);
+ CanonicalBounds3 restoredBounds = adapter.FromCanonicalBounds3(canonicalBounds);
+
+ AssertPoint(restoredBounds.Min, -2.0, 1.0, 3.0);
+ AssertPoint(restoredBounds.Max, 5.0, 7.0, 11.0);
+ }
+
+ private static void AssertPoint(Vector3 point, double x, double y, double z)
+ {
+ Assert.AreEqual(x, point.X, 1e-9);
+ Assert.AreEqual(y, point.Y, 1e-9);
+ Assert.AreEqual(z, point.Z, 1e-9);
+ }
+
+ private static void AssertVector(Vector3 vector, double x, double y, double z)
+ {
+ Assert.AreEqual(x, vector.X, 1e-9);
+ Assert.AreEqual(y, vector.Y, 1e-9);
+ Assert.AreEqual(z, vector.Z, 1e-9);
+ }
+ }
+}
diff --git a/UnitTests/CoordinateSystem/ModelAxisConventionTests.cs b/UnitTests/CoordinateSystem/ModelAxisConventionTests.cs
new file mode 100644
index 0000000..74f16e0
--- /dev/null
+++ b/UnitTests/CoordinateSystem/ModelAxisConventionTests.cs
@@ -0,0 +1,87 @@
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using NavisworksTransport.Utils.CoordinateSystem;
+using System.Numerics;
+
+namespace NavisworksTransport.UnitTests.CoordinateSystem
+{
+ [TestClass]
+ public class ModelAxisConventionTests
+ {
+ [TestMethod]
+ public void DefaultForYUp_ShouldUseXForwardAndYUp()
+ {
+ ModelAxisConvention convention = ModelAxisConvention.CreateDefaultForHost(CoordinateSystemType.YUp);
+
+ Assert.AreEqual(LocalAxisDirection.PositiveX, convention.ForwardAxis);
+ Assert.AreEqual(LocalAxisDirection.PositiveY, convention.UpAxis);
+ }
+
+ [TestMethod]
+ public void DefaultForZUp_ShouldUseXForwardAndZUp()
+ {
+ ModelAxisConvention convention = ModelAxisConvention.CreateDefaultForHost(CoordinateSystemType.ZUp);
+
+ Assert.AreEqual(LocalAxisDirection.PositiveX, convention.ForwardAxis);
+ Assert.AreEqual(LocalAxisDirection.PositiveZ, convention.UpAxis);
+ }
+
+ [TestMethod]
+ public void YUpConvention_CreateLinearTransform_ShouldMapLocalYToWorldUp()
+ {
+ var convention = new ModelAxisConvention(LocalAxisDirection.PositiveX, LocalAxisDirection.PositiveY);
+ Matrix4x4 linear = convention.CreateLinearTransform3(new Vector3(1, 0, 0), new Vector3(0, 0, 1));
+
+ AssertColumn(linear, 0, 1, 0, 0);
+ AssertColumn(linear, 1, 0, 0, 1);
+ AssertColumn(linear, 2, 0, -1, 0);
+ }
+
+ [TestMethod]
+ public void ZUpConvention_CreateLinearTransform_ShouldMapLocalZToWorldUp()
+ {
+ var convention = new ModelAxisConvention(LocalAxisDirection.PositiveX, LocalAxisDirection.PositiveZ);
+ Matrix4x4 linear = convention.CreateLinearTransform3(new Vector3(1, 0, 0), new Vector3(0, 0, 1));
+
+ AssertColumn(linear, 0, 1, 0, 0);
+ AssertColumn(linear, 1, 0, 1, 0);
+ AssertColumn(linear, 2, 0, 0, 1);
+ }
+
+ [TestMethod]
+ public void YUpConvention_CreateRotation_ShouldAlignLocalAxesToRequestedWorldAxes()
+ {
+ var convention = new ModelAxisConvention(LocalAxisDirection.PositiveX, LocalAxisDirection.PositiveY);
+ Quaternion rotation = convention.CreateQuaternion(new Vector3(0, 1, 0), new Vector3(0, 0, 1));
+ Matrix4x4 linear = Matrix4x4.CreateFromQuaternion(rotation);
+
+ AssertColumn(linear, 0, 0, 1, 0);
+ AssertColumn(linear, 1, 0, 0, 1);
+ AssertColumn(linear, 2, 1, 0, 0);
+ }
+
+ private static void AssertColumn(Matrix4x4 matrix, int column, double x, double y, double z)
+ {
+ switch (column)
+ {
+ case 0:
+ Assert.AreEqual(x, matrix.M11, 1e-6);
+ Assert.AreEqual(y, matrix.M21, 1e-6);
+ Assert.AreEqual(z, matrix.M31, 1e-6);
+ break;
+ case 1:
+ Assert.AreEqual(x, matrix.M12, 1e-6);
+ Assert.AreEqual(y, matrix.M22, 1e-6);
+ Assert.AreEqual(z, matrix.M32, 1e-6);
+ break;
+ case 2:
+ Assert.AreEqual(x, matrix.M13, 1e-6);
+ Assert.AreEqual(y, matrix.M23, 1e-6);
+ Assert.AreEqual(z, matrix.M33, 1e-6);
+ break;
+ default:
+ Assert.Fail("Only first 3 columns are valid.");
+ break;
+ }
+ }
+ }
+}
diff --git a/UnitTests/CoordinateSystem/ProjectReferenceFrameTests.cs b/UnitTests/CoordinateSystem/ProjectReferenceFrameTests.cs
new file mode 100644
index 0000000..755de5f
--- /dev/null
+++ b/UnitTests/CoordinateSystem/ProjectReferenceFrameTests.cs
@@ -0,0 +1,38 @@
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using NavisworksTransport.Utils.CoordinateSystem;
+using System.Numerics;
+
+namespace NavisworksTransport.UnitTests.CoordinateSystem
+{
+ [TestClass]
+ public class ProjectReferenceFrameTests
+ {
+ [TestMethod]
+ public void DefaultForYUp_ShouldUseCanonicalUpAndYUpModelConvention()
+ {
+ ProjectReferenceFrame frame = ProjectReferenceFrame.CreateDefault(CoordinateSystemType.YUp);
+
+ Assert.AreEqual(0.0, frame.SphereCenterInCanonical3.X, 1e-9);
+ Assert.AreEqual(0.0, frame.SphereCenterInCanonical3.Y, 1e-9);
+ Assert.AreEqual(0.0, frame.SphereCenterInCanonical3.Z, 1e-9);
+ Assert.AreEqual(0.0, frame.ProjectUpInCanonical3.X, 1e-9);
+ Assert.AreEqual(0.0, frame.ProjectUpInCanonical3.Y, 1e-9);
+ Assert.AreEqual(1.0, frame.ProjectUpInCanonical3.Z, 1e-9);
+ Assert.AreEqual(LocalAxisDirection.PositiveX, frame.DefaultModelAxisConvention.ForwardAxis);
+ Assert.AreEqual(LocalAxisDirection.PositiveY, frame.DefaultModelAxisConvention.UpAxis);
+ }
+
+ [TestMethod]
+ public void Constructor_ShouldNormalizeProjectUp()
+ {
+ var frame = new ProjectReferenceFrame(
+ new Vector3(1, 2, 3),
+ new Vector3(0, 0, 10),
+ ModelAxisConvention.CreateDefaultForHost(CoordinateSystemType.ZUp));
+
+ Assert.AreEqual(0.0, frame.ProjectUpInCanonical3.X, 1e-9);
+ Assert.AreEqual(0.0, frame.ProjectUpInCanonical3.Y, 1e-9);
+ Assert.AreEqual(1.0, frame.ProjectUpInCanonical3.Z, 1e-9);
+ }
+ }
+}
diff --git a/UnitTests/Properties/AssemblyInfo.cs b/UnitTests/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000..06b09da
--- /dev/null
+++ b/UnitTests/Properties/AssemblyInfo.cs
@@ -0,0 +1,15 @@
+using System.Reflection;
+using System.Runtime.InteropServices;
+
+[assembly: AssemblyTitle("NavisworksTransport.UnitTests")]
+[assembly: AssemblyDescription("")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("")]
+[assembly: AssemblyProduct("NavisworksTransport.UnitTests")]
+[assembly: AssemblyCopyright("Copyright © 2026")]
+[assembly: AssemblyTrademark("")]
+[assembly: AssemblyCulture("")]
+[assembly: ComVisible(false)]
+[assembly: Guid("8f8a2fd7-c7e5-4185-95a4-740a2bf6130f")]
+[assembly: AssemblyVersion("1.0.0.0")]
+[assembly: AssemblyFileVersion("1.0.0.0")]
diff --git a/doc/design/2026/coordinate-system-canonical-space-design.md b/doc/design/2026/coordinate-system-canonical-space-design.md
new file mode 100644
index 0000000..4d15f77
--- /dev/null
+++ b/doc/design/2026/coordinate-system-canonical-space-design.md
@@ -0,0 +1,503 @@
+# 坐标系统一架构设计方案
+
+## 1. 背景
+
+当前项目运行在 Navisworks 宿主环境中,程序直接读取和写入的都是 Navisworks 世界坐标:
+
+- `Point3D`
+- `Vector3D`
+- `BoundingBox3D`
+- `Transform3D`
+- 鼠标点击拾取点
+- `Document.UpVector`
+
+这些坐标对程序来说属于**外部坐标**。
+
+项目目前的问题不是“完全没有坐标系抽象”,而是:
+
+- 有一部分代码已经开始抽象 `Y-up / Z-up`
+- 另一部分代码仍然直接写死世界 `Z`
+- 还有一部分代码把世界原点直接当作业务球心
+
+结果是三种语义混在一起:
+
+1. Navisworks 外部坐标语义
+2. 程序内部几何计算语义
+3. 工程业务基准语义(球心、安装基准、轨道参考面)
+
+这会导致:
+
+- `Y-up` / `Z-up` 项目切换后功能不一致
+- 终端安装仿真把世界原点误当成球心
+- Rail 姿态和渲染层偷用世界 `Z`
+- 不同模块对同一个点的解释不一致
+
+因此,需要建立一套更成熟、更清晰的坐标系架构。
+
+## 2. 设计目标
+
+本方案的目标不是“到处加 `if (isYUp)`”,而是建立一个标准的分层架构:
+
+- Navisworks 世界坐标统一视为**外部坐标**
+- 程序内部统一使用一套**规范内部坐标**
+- 工程语义使用独立的**业务基准坐标**
+- 坐标转换只发生在少数边界入口
+- 业务逻辑禁止直接依赖宿主坐标语义
+
+## 3. 总体方案
+
+### 3.1 内部统一坐标
+
+程序内部统一使用:
+
+- **Canonical Space(规范内部坐标)**
+- **固定为 `Z-up`**
+
+选择 `Z-up` 的原因:
+
+1. 当前项目大量成熟逻辑本身就是按 `Z-up` 语义构建的
+2. 动画、渲染、yaw 语义、很多 helper 都更接近 `Z-up`
+3. 以 `Z-up` 作为内部标准,改造成本低于整体改成 `Y-up`
+
+注意:
+
+- 这只是程序内部选择
+- 不代表客户模型必须是 `Z-up`
+- 客户仍可继续使用 `Y-up` 项目
+
+### 3.2 三层坐标语义
+
+```mermaid
+flowchart LR
+ A["Navisworks Host Space\n外部坐标"] --> B["Host Coordinate Adapter\n边界适配层"]
+ B --> C["Canonical Space (Z-up)\n内部统一坐标"]
+ C --> D["Project Reference Frame\n业务基准坐标"]
+
+ C --> E["路径规划/几何计算"]
+ C --> F["Rail 姿态/动画"]
+ C --> G["碰撞检测/结果恢复"]
+ C --> H["渲染/辅助线/通行空间"]
+
+ D --> I["球心"]
+ D --> J["终端安装基准"]
+ D --> K["轨道参考面"]
+
+ C --> B
+ B --> L["Navisworks 输出\n渲染/移动/截图恢复"]
+```
+
+三层职责如下:
+
+#### A. Navisworks Host Space(外部坐标)
+
+宿主 API 直接提供的坐标。
+
+特点:
+
+- 是程序的输入/输出坐标
+- 可能来自 `Y-up` 项目,也可能来自 `Z-up` 项目
+- 不应直接当成内部计算坐标
+
+#### B. Canonical Space(内部统一坐标)
+
+程序内部唯一允许进行几何计算、路径计算、姿态计算的坐标空间。
+
+特点:
+
+- 固定为 `Z-up`
+- 只解决坐标轴和方向语义统一问题
+- 不承载业务基准含义
+
+#### C. Project Reference Frame(业务基准坐标)
+
+建立在内部统一坐标基础上的工程语义层。
+
+负责表达:
+
+- 球心
+- 项目 up 方向的业务解释
+- 终端安装参考面
+- 轨道参考面
+- 业务锚点
+
+这层不能偷用世界原点,也不能偷用宿主世界轴。
+
+### 3.3 坐标系定义必须包含的语义
+
+在本项目中,`Y-up` / `Z-up` 不能只被理解成“点坐标怎么换算”。
+
+一个完整的坐标系定义,至少必须显式包含:
+
+- `UpAxis`
+- `ElevationAxis`
+- `HorizontalPlane`
+- 必要时的 `Handedness`
+
+因此:
+
+- `Y-up` 的含义是:
+ - `Y` 为 up 轴
+ - `Y` 为高程轴
+ - `XZ` 为水平平面
+- `Z-up` 的含义是:
+ - `Z` 为 up 轴
+ - `Z` 为高程轴
+ - `XY` 为水平平面
+
+后续凡是出现:
+
+- 高度
+- 底面
+- 顶面
+- 通行空间高度轴
+- 俯仰/法向
+
+都必须基于这组定义来解释,不能继续偷用“世界 Z 就是 up”的旧假设。
+
+### 3.4 模型局部轴约定是独立层
+
+除了宿主坐标系和内部统一坐标系,还必须区分**模型局部轴约定**。
+
+这是另一层独立定义,至少包含:
+
+- `LocalForwardAxis`
+- `LocalUpAxis`
+
+例如:
+
+- 虚拟物体资源通常按程序约定构建,可能是 `Local X = Forward, Local Z = Up`
+- 真实模型如果来自 `Y-up` 项目,则很可能是 `Local X = Forward, Local Y = Up`
+
+这意味着:
+
+- 即使宿主坐标系转换已经正确
+- 如果模型局部轴约定没有显式处理
+- 动画和姿态仍然会出现“路径对了、通行空间对了、模型自己站歪了”的现象
+
+因此,后续姿态系统必须显式区分:
+
+1. 宿主坐标系定义
+2. 内部统一坐标系定义
+3. 业务基准定义
+4. 模型局部轴约定
+
+## 4. 核心设计原则
+
+### 4.1 Navisworks 世界坐标统一视为外部坐标
+
+这是本方案的第一条硬规则。
+
+对程序而言,以下数据统一视为外部输入:
+
+- `Point3D`
+- `Vector3D`
+- `BoundingBox3D`
+- `Transform3D`
+- `ModelItem.BoundingBox()`
+- `Document.UpVector`
+- 鼠标点击拾取点
+
+无论当前 NWD/NWC 是客户原始模型,还是预先转换保存后的模型,
+**只要是从 Navisworks API 读出来的,它对程序来说就是外部坐标。**
+
+### 4.1.1 外部坐标不等于内部语义
+
+外部坐标虽然来自 Navisworks,但不能直接拿来推导内部业务语义。
+
+特别是以下概念必须先经过坐标定义解释:
+
+- 哪个轴是 up
+- 哪个轴是高程
+- 哪个平面是水平面
+- 一个 `BoundingBox` 的“底面”到底是哪一面
+
+也就是说:
+
+- 外部坐标是输入
+- 坐标定义决定如何解释这个输入
+- 业务代码不得跳过这一步
+
+### 4.2 程序内部一律只认 Canonical Space
+
+业务层不得直接消费 Navisworks 坐标。
+
+禁止这样做:
+
+```csharp
+// ❌ 错误:直接拿宿主包围盒结果开始做业务计算
+var bounds = item.BoundingBox();
+var center = bounds.Center;
+var direction = new Vector3D(center.X, center.Y, center.Z);
+```
+
+正确做法应当是:
+
+```csharp
+// ✅ 正确:先通过边界适配层转换到内部统一坐标
+var hostBounds = item.BoundingBox();
+var bounds = adapter.ToCanonicalBounds(hostBounds);
+var center = bounds.Center;
+```
+
+### 4.3 坐标系层只解决轴变换,不解决业务语义
+
+坐标适配层只负责:
+
+- 外部坐标到内部坐标的变换
+- 内部坐标到外部坐标的逆变换
+- up 轴、高程轴、水平面定义
+
+不负责:
+
+- 球心是否在 `(0,0,0)`
+- 终端安装是否指向球心
+- 顶面/底面对接如何定义
+- 轨道参考面在哪里
+
+这些都属于业务基准层。
+
+### 4.4 项目基准点必须显式配置或显式求解
+
+像“球心”这类工程点不属于坐标系本身。
+
+不能因为某一批模型里球心恰好在世界原点,就长期把它写死。
+
+应使用以下来源之一:
+
+- 项目配置
+- 明确的设计资料
+- 多条向心轴线拟合
+- 其他明确的业务求解方式
+
+### 4.5 不在业务代码中散落 `Y-up / Z-up` 分支
+
+不推荐:
+
+```csharp
+// ❌ 错误
+if (isYUp)
+{
+ ...
+}
+else
+{
+ ...
+}
+```
+
+推荐:
+
+- 边界适配层统一处理 `Host -> Canonical`
+- 业务层只消费 Canonical 数据
+
+### 4.6 渲染几何也必须完成坐标转换
+
+坐标系改造不能只停留在:
+
+- 点坐标转换
+- 中心点偏移
+- 业务锚点转换
+
+对于以下可视化对象,还必须同步改造它们的**局部几何轴构造**:
+
+- 通行空间长方体
+- 辅助线杆体
+- 圆形/圆柱标记
+- 切向、法向、侧向相关的渲染面片
+
+否则会出现一种典型错误:
+
+- 对象中心点已经在正确位置
+- 但渲染几何仍然按世界 `Z-up` 去构造 `right/up/height`
+- 最终看起来仍然像 `Z-up`,即使业务点位已经是对的
+
+本项目已经出现过这一类问题:
+
+- `Y-up` 模型中,通行空间中心点偏移已经正确
+- 但长方体本体仍然按世界 `XY + Z` 构造
+- 导致通行空间整体看起来仍是 `Z-up`
+
+因此,渲染层必须遵守以下规则:
+
+1. 凡是依赖 `up/right/forward/normal` 的渲染几何,必须基于统一坐标语义构造局部轴。
+2. 不允许只修改“中心点/偏移量”而保留旧的世界轴构造公式。
+3. 如果渲染输出面向 Navisworks 宿主,则应先在 Canonical Space 中完成几何语义计算,再转换回宿主坐标输出。
+4. 对长方体、圆柱体这类实体渲染,必须同时验证:
+ - 中心点是否正确
+ - 局部高度轴是否正确
+ - 局部侧向轴是否正确
+ - 法向/截面方向是否正确
+
+## 5. 推荐的技术方案
+
+### 5.1 使用完整变换矩阵作为适配基础
+
+不推荐继续停留在:
+
+- `GetElevation()`
+- `GetHorizontalCoords()`
+- `CreatePoint()`
+
+这类偏二维、局部的接口上。
+
+对三维动画、Rail 姿态、碰撞恢复来说,更成熟的方案是:
+
+- 使用完整的空间变换对象
+- 以矩阵/旋转+平移为核心
+
+即:
+
+- `ExternalToCanonical`
+- `CanonicalToExternal`
+
+这两套变换应成为边界层的基础能力。
+
+### 5.2 建议新增的核心组件
+
+#### 1. `HostCoordinateAdapter`
+
+职责:
+
+- 统一处理 Navisworks 外部坐标到 Canonical Space 的转换
+- 统一处理 Canonical Space 到 Navisworks 外部坐标的反向转换
+
+建议能力:
+
+- `ToCanonicalPoint(...)`
+- `ToCanonicalVector(...)`
+- `ToCanonicalBounds(...)`
+- `ToCanonicalTransform(...)`
+- `FromCanonicalPoint(...)`
+- `FromCanonicalVector(...)`
+- `FromCanonicalBounds(...)`
+- `FromCanonicalTransform(...)`
+
+#### 2. `CanonicalTransform`
+
+职责:
+
+- 封装 `ExternalToCanonical / CanonicalToExternal`
+- 提供点、向量、姿态、包围盒的统一变换
+
+#### 3. `ProjectReferenceFrame`
+
+职责:
+
+- 管理业务基准语义
+
+建议包含:
+
+- `SphereCenterInCanonical`
+- `ProjectUpInCanonical`
+- `RailReferencePlane`
+- `AssemblyReferenceFrame`
+
+### 5.3 业务层只消费 Canonical 对象
+
+终端安装、Rail、动画、碰撞、渲染层不应直接使用:
+
+- Navisworks `Point3D`
+- Navisworks `BoundingBox3D`
+- Navisworks `Transform3D`
+
+而应通过适配层先转成内部统一对象后再计算。
+
+## 6. 输入输出边界
+
+### 6.1 必须拦截的输入入口
+
+以下都是必须拦截的宿主输入点:
+
+- 鼠标点击获取路径点
+- `ModelItem.BoundingBox()`
+- `ModelItem.Transform`
+- `Geometry.BoundingBox`
+- `Document.UpVector`
+- `PathClickToolPlugin` 等交互工具返回的点
+
+这些点一旦进入业务逻辑,就应先转换到 Canonical Space。
+
+### 6.2 必须反向转换的输出入口
+
+以下都是必须回写到宿主时做反向转换的输出点:
+
+- 3D 渲染点、线、法向
+- 动画对象位置与姿态
+- 碰撞点恢复
+- 碰撞报告截图定位
+- 辅助线/参考杆/通行空间可视化
+
+## 7. 当前项目中的优先改造模块
+
+以下模块优先级最高,因为它们既参与三维姿态,又直接暴露了世界坐标假设问题:
+
+### 7.1 必改
+
+- [PathEditingViewModel.cs](/C:/Users/Tellme/apps/NavisworksTransport-rail-mount-modes/src/UI/WPF/ViewModels/PathEditingViewModel.cs)
+ - 当前终端安装仿真里仍把世界原点当球心
+- [RailPathPoseHelper.cs](/C:/Users/Tellme/apps/NavisworksTransport-rail-mount-modes/src/Utils/RailPathPoseHelper.cs)
+ - 当前仍把世界 `Z` 当 `worldUp`
+- [PathPointRenderPlugin.cs](/C:/Users/Tellme/apps/NavisworksTransport-rail-mount-modes/src/Core/PathPointRenderPlugin.cs)
+ - 当前大量渲染法向仍写死 `(0,0,1)`
+- [PathAnimationManager.cs](/C:/Users/Tellme/apps/NavisworksTransport-rail-mount-modes/src/Core/Animation/PathAnimationManager.cs)
+ - 需要逐步统一输入输出边界
+- [CollisionSceneHelper.cs](/C:/Users/Tellme/apps/NavisworksTransport-rail-mount-modes/src/Utils/CollisionSceneHelper.cs)
+ - 需要确保碰撞恢复只使用内部语义
+
+### 7.2 次改
+
+- 自动路径规划相关的高度、坡度、网格构建
+- 历史二维 `yaw` 辅助逻辑
+- 旧视图辅助和截图辅助
+
+这些部分可以后续逐步纳入统一架构,不必阻塞当前终端安装与 Rail 主线。
+
+## 8. 迁移策略
+
+不建议一次性全项目重构。
+
+建议按以下顺序推进:
+
+### 阶段 1
+
+建立边界层:
+
+- `HostCoordinateAdapter`
+- `CanonicalTransform`
+- `ProjectReferenceFrame`
+
+### 阶段 2
+
+先接入以下主线功能:
+
+- 终端安装仿真
+- Rail 姿态
+- 动画播放
+- 碰撞恢复
+- 辅助线/通行空间渲染
+
+### 阶段 3
+
+再逐步改造:
+
+- 自动路径规划
+- 高度检测
+- 坡度分析
+- 旧二维路径辅助逻辑
+
+## 9. 关键结论
+
+本项目如果要长期稳定支持 `Y-up` 和 `Z-up` 项目,正确方向不是:
+
+- 到处散落 `if (isYUp)`
+- 强行要求客户把模型先转成 `Z-up`
+- 继续混用世界原点和业务球心
+
+而应该是:
+
+- **Navisworks 世界坐标统一视为外部坐标**
+- **程序内部统一使用 Canonical Space(Z-up)**
+- **业务基准点单独建模**
+- **坐标转换只发生在边界层**
+
+这是一条更接近业界三维软件成熟做法的路线。
diff --git a/doc/guide/design_principles.md b/doc/guide/design_principles.md
index f05e188..a88d76a 100644
--- a/doc/guide/design_principles.md
+++ b/doc/guide/design_principles.md
@@ -259,11 +259,303 @@ private static void TryRecoverComponents()
### 适用场景
+## 18. 坐标系分层原则
+
+### 问题描述
+
+项目已经同时面对以下三类不同语义的坐标:
+
+- Navisworks 世界坐标
+- 程序内部计算坐标
+- 工程业务基准坐标(如球心、安装基准、轨道参考面)
+
+如果这三层语义混在一起使用,就会出现典型问题:
+
+- 把世界原点误当成业务球心
+- 把世界 `Z` 误当成项目唯一的 up 方向
+- 一部分代码按源模型坐标算,一部分代码按转换后坐标算
+- Y-up / Z-up 项目切换后,路径、姿态、渲染结果彼此不一致
+
+### 设计原则
+
+#### 18.1 必须明确区分三层坐标语义
+
+```mermaid
+flowchart LR
+ A["Navisworks 世界坐标\n(模型原始坐标, 可能是 Y-up 或 Z-up 语义)"] --> B["内部统一坐标\n(程序所有计算都使用)"]
+ B --> C["业务基准坐标\n(球心、安装方向、轨道参考面)"]
+
+ C --> D["终端安装仿真"]
+ C --> E["Rail 姿态/动画"]
+ C --> F["碰撞检测/结果恢复"]
+
+ B --> G["路径规划/网格/几何运算"]
+ B --> H["渲染/可视化"]
+
+ I["UI 输入/日志/坐标显示"] <-->|按需转换| B
+```
+
+三层语义的职责如下:
+
+- **Navisworks 世界坐标**
+ - 这是 API 直接返回的坐标、包围盒、变换矩阵
+ - 反映模型当前在 Navisworks 场景中的真实位置
+ - 不能自动等同于业务上的球心、安装参考面或统一 up 方向
+
+- **内部统一坐标**
+ - 程序内部所有几何计算、路径规划、姿态计算应尽量统一使用这一层
+ - 这一层负责消化源模型的 Y-up / Z-up 差异
+ - 这一层只处理坐标轴和方向语义,不处理业务规则
+
+- **业务基准坐标**
+ - 用来表达球心、安装基准点、轨道参考面等工程语义
+ - 这一层不应偷用世界原点或世界某个固定轴
+ - 业务基准点应显式配置或显式计算,不得隐式假设
+
+#### 18.2 坐标系层只负责轴语义转换,不负责业务解释
+
+坐标系抽象层应只回答以下问题:
+
+- 当前项目 up 方向是什么
+- 高程轴是哪一轴
+- 水平面由哪两轴组成
+- 点和向量如何在源模型坐标与内部统一坐标之间转换
+
+坐标系层不应负责以下业务问题:
+
+- 球心是否在 `(0,0,0)`
+- 终端安装参考线是否指向球心
+- 顶面/底面对接如何定义
+- 轨道参考面偏移量是多少
+
+这些都属于业务基准层。
+
+#### 18.3 业务逻辑不得直接写死世界原点和世界 Z
+
+以下写法都应视为高风险设计:
+
+```csharp
+// ❌ 错误:把世界原点直接当成业务球心
+Vector3D direction = new Vector3D(centerPoint.X, centerPoint.Y, centerPoint.Z);
+
+// ❌ 错误:把世界Z直接当成项目up
+var worldUp = new Vector3D(0, 0, 1);
+
+// ❌ 错误:把Min.Z / Max.Z直接当成统一的高程语义
+double top = bounds.Max.Z;
+double bottom = bounds.Min.Z;
+```
+
+更合理的写法应当是:
+
+```csharp
+// ✅ 正确:球心来自业务基准配置或业务计算
+Vector3D direction = centerPoint - sphereCenter;
+
+// ✅ 正确:up方向来自坐标系抽象或对象自身姿态
+Vector3D worldUp = CoordinateSystemManager.Instance.Current.UpVector;
+
+// ✅ 正确:高程语义来自坐标系抽象
+double elevation = coordinateSystem.GetElevation(point);
+```
+
+#### 18.4 程序内部应优先统一计算坐标,再按需转换回用户语义
+
+当客户项目坚持使用 Y-up 坐标时,不应要求客户先把模型硬转成 Z-up 再继续使用。
+
+更合理的做法是:
+
+- 保留客户熟悉的输入输出坐标语义
+- 程序内部统一转换到内部计算坐标
+- 计算完成后,再把需要展示给用户的数据映射回客户语义
+
+也就是说:
+
+- **客户层**可以是 Y-up
+- **内部计算层**可以统一成程序更容易处理的一套坐标
+- **显示/日志层**再根据需要转回客户语义
+
+#### 18.5 不要在全项目到处散落 Y-up / Z-up 条件分支
+
+不推荐这种扩散式兼容写法:
+
+```csharp
+// ❌ 错误:在业务逻辑中到处散落坐标系分支
+if (isYUp)
+{
+ // ...
+}
+else
+{
+ // ...
+}
+```
+
+更推荐的方式是:
+
+- 在坐标系层统一封装 `UpVector`、`GetElevation()`、`GetHorizontalCoords()` 等能力
+- 在业务层统一消费抽象结果
+- 让路径、姿态、渲染尽量只面对“内部统一坐标”
+
+#### 18.6 业务基准点必须单独配置或显式求解
+
+像“球心”这类点,不属于坐标系本身的一部分,必须单独处理。
+
+例如:
+
+- 球心可以来自项目配置
+- 或来自多条已知向心轴线的拟合结果
+- 或来自设计资料中的明确基准点
+
+但不能因为过去某批模型里球心正好在世界原点,就长期把它写死。
+
+### 适用场景
+
+- 终端安装仿真
+- Rail 三维姿态与动画
+- 路径规划中的高程与水平平面计算
+- 多坐标系项目的输入、显示与日志输出
+
- 所有对外接口方法
- 事件处理器
- 后台任务执行
- Navisworks API调用
+## 19. 框架先行与测试先行原则
+
+### 问题描述
+
+当功能涉及到底层几何语义、坐标系语义、局部轴约定或姿态矩阵时,如果直接在业务链路里一边试一边改,通常会出现这些问题:
+
+- 虚拟物体和真实模型的语义被混用
+- 一处补丁修好,另一条链路被带坏
+- 日志越来越多,但问题边界越来越模糊
+- 业务代码里充满临时补偿,最终难以维护
+
+这类问题的根因往往不是业务流程本身,而是底层框架语义没有先被验证。
+
+### 设计原则
+
+#### 19.1 先抽离框架,再接业务
+
+对于以下类型的问题,不应先改业务代码:
+
+- 坐标系转换
+- 局部轴约定
+- 姿态构造
+- 纯几何补偿
+- 宿主坐标与内部统一坐标之间的映射
+
+正确顺序应当是:
+
+1. 先抽出纯框架层
+2. 用最小测试验证框架层
+3. 通过后再接入业务模块
+
+错误示例:
+
+```csharp
+// ❌ 错误:尚未验证坐标/姿态语义,直接改动画和渲染链路
+if (isYUp)
+{
+ rotation = routeRotation * someCorrection;
+}
+else
+{
+ rotation = routeRotation;
+}
+```
+
+正确示例:
+
+```csharp
+// ✅ 正确:先让纯框架层定义并验证语义
+Quaternion rotation = canonicalPoseBuilder.CreateQuaternion(
+ canonicalForward,
+ canonicalUp,
+ modelAxisConvention);
+
+// 业务层只消费已验证的结果
+frame.Rotation = ToNavisworksRotation(rotation);
+```
+
+#### 19.2 可测试的核心框架不得直接依赖宿主 API
+
+凡是需要单元测试验证的核心几何框架,不应直接绑定 Navisworks `Point3D`、`Vector3D`、`BoundingBox3D` 这类宿主类型。
+
+原因是:
+
+- 宿主 API 在脱离 Navisworks 进程时通常无法初始化
+- 会导致测试只能在宿主环境里间接验证
+- 这样测试粒度太粗,定位问题非常慢
+
+更合理的方式是:
+
+- 框架核心使用纯数学类型
+ - 例如 `System.Numerics.Vector3`
+ - 例如 `System.Numerics.Matrix4x4`
+ - 例如 `System.Numerics.Quaternion`
+- 只有边界适配层才接触 Navisworks API
+
+也就是说:
+
+- **纯数学层**:可单元测试
+- **宿主适配层**:负责包装和转换
+- **业务层**:消费已验证结果
+
+#### 19.3 先验证“语义”,再验证“效果”
+
+这类框架测试的重点不是先看动画画面,而是先验证最小语义:
+
+- Y-up 到 Canonical Z-up 的点/向量转换是否正确
+- 本地 `X-forward / Y-up` 与 `X-forward / Z-up` 的轴约定是否正确
+- 给定世界前进方向和上方向,生成的姿态矩阵列向量是否正确
+- 包围盒和参考点经过往返转换是否保持一致
+
+只有这些最小语义先对,业务画面才值得继续看。
+
+#### 19.4 业务接入应尽量只做“选择约定”,不做“临时补偿”
+
+当框架层已经定义了:
+
+- 宿主坐标系
+- 内部统一坐标
+- 模型局部轴约定
+
+那么业务层最理想的职责应该只是:
+
+- 选择当前对象使用哪一种约定
+- 调用框架生成姿态或坐标
+
+而不是在业务层继续做:
+
+- `+90°`
+- `-90°`
+- “如果 Y-up 就补一下”
+- “如果真实模型就再扭一下”
+
+这些都属于典型的补丁式开发,会掩盖框架问题。
+
+#### 19.5 一旦测试表明框架不纯,必须先回到框架层
+
+如果在测试阶段发现:
+
+- 纯框架层还依赖宿主 API
+- 测试无法脱离 Navisworks 运行
+- 同一个姿态语义在框架和业务里各算一遍
+
+就不应该继续改业务代码,而应立即回到框架层收口。
+
+这比继续在业务链路里加日志、加补偿更重要。
+
+### 适用场景
+
+- 坐标系改造
+- Y-up / Z-up 兼容
+- Rail 三维姿态
+- 真实模型与虚拟物体局部轴约定统一
+- 碰撞恢复姿态语义
+
## 3. 内存管理与性能优化
### 问题描述
diff --git a/doc/working/coordinate-system-canonical-space-implementation-plan.md b/doc/working/coordinate-system-canonical-space-implementation-plan.md
new file mode 100644
index 0000000..6e67f8c
--- /dev/null
+++ b/doc/working/coordinate-system-canonical-space-implementation-plan.md
@@ -0,0 +1,362 @@
+# 坐标系统一架构实施清单
+
+## 1. 文档目的
+
+本文档基于
+[coordinate-system-canonical-space-design.md](/C:/Users/Tellme/apps/NavisworksTransport-rail-mount-modes/doc/design/2026/coordinate-system-canonical-space-design.md)
+中的方案,拆解出一份可执行的实施清单。
+
+目标不是一次性全项目重构,而是:
+
+- 先建立“外部坐标 -> 内部统一坐标 -> 业务基准坐标”的边界
+- 再优先改造当前最重要的非自动路径规划功能
+- 逐步把历史 `Z-up` 假设收口
+
+## 2. 总体原则
+
+### 2.1 本轮范围
+
+当前优先范围:
+
+- 终端安装仿真
+- Rail 三维姿态
+- 动画播放
+- 碰撞检测与碰撞恢复
+- 辅助线、通行空间、碰撞点相关渲染
+
+当前明确**不优先**处理:
+
+- 自动路径规划
+- 高度检测
+- 坡度分析
+- 旧网格高度/通道高度相关算法
+
+### 2.2 内部统一坐标
+
+当前实施目标固定为:
+
+- **内部统一坐标 = Canonical Space = Z-up**
+
+### 2.3 外部坐标定义
+
+当前实施中,统一定义:
+
+- **Navisworks API 返回的世界坐标 = 外部坐标**
+
+即:
+
+- `Point3D`
+- `Vector3D`
+- `BoundingBox3D`
+- `Transform3D`
+- `Document.UpVector`
+- `ModelItem.BoundingBox()`
+- 鼠标点击拾取点
+
+都先视为外部输入。
+
+## 3. 里程碑划分
+
+### M1. 建立坐标边界层
+
+目标:
+
+- 引入统一的宿主坐标适配器
+- 不再在业务代码里直接解释 Navisworks 坐标
+
+### M2. 接入终端安装仿真
+
+目标:
+
+- 终端安装仿真不再直接依赖世界原点和世界 `Z`
+- 球心与项目 up 都走统一边界层/业务基准层
+
+### M3. 接入 Rail 三维姿态
+
+目标:
+
+- Rail 姿态和动画只认内部统一坐标
+- 不再在姿态 helper 中写死 `(0,0,1)`
+
+### M4. 接入碰撞恢复和可视化
+
+目标:
+
+- 碰撞报告、碰撞点恢复、自动截图、辅助线渲染统一使用同一套内部语义
+
+## 4. 任务清单
+
+## 4.1 M1 建立坐标边界层
+
+### Task 1. 新增 `HostCoordinateAdapter`
+
+目标:
+
+- 建立 Navisworks 外部坐标与内部统一坐标之间的唯一适配入口
+
+建议职责:
+
+- `ToCanonicalPoint(...)`
+- `ToCanonicalVector(...)`
+- `ToCanonicalBounds(...)`
+- `ToCanonicalTransform(...)`
+- `FromCanonicalPoint(...)`
+- `FromCanonicalVector(...)`
+- `FromCanonicalBounds(...)`
+- `FromCanonicalTransform(...)`
+
+建议要求:
+
+- 统一使用完整三维变换语义
+- 不再停留在 `GetElevation()/GetHorizontalCoords()` 这种偏二维接口
+- 点、向量、包围盒、旋转、变换都必须有明确变换规则
+- 坐标定义中必须显式暴露:
+ - `UpAxis`
+ - `ElevationAxis`
+ - `HorizontalPlane`
+
+验收:
+
+- 对同一组 Navisworks 输入,适配结果在 Y-up 和 Z-up 项目中都能稳定输出 Canonical Space 数据
+
+### Task 2. 新增 `ProjectReferenceFrame`
+
+目标:
+
+- 承载业务基准点,不再把业务基准混进坐标系层
+
+建议职责:
+
+- `SphereCenterInCanonical`
+- `ProjectUpInCanonical`
+- 终端安装参考方向
+- 轨道参考面
+
+验收:
+
+- 程序内不再把世界原点直接当球心使用
+
+### Task 3. 约束边界:禁止业务代码直接解释宿主坐标
+
+目标:
+
+- 收口“谁可以直接碰 Navisworks 坐标”
+
+需要收口的入口:
+
+- 鼠标点击取点
+- `ModelItem.BoundingBox()`
+- `Transform3D`
+- `Document.UpVector`
+- 3D 渲染输入
+- 动画对象位姿输出
+
+验收:
+
+- 新功能主链路中,业务代码不再直接从 `BoundingBox().Center` 开始做业务推导
+
+## 4.2 M2 接入终端安装仿真
+
+### Task 4. 改造 `PathEditingViewModel`
+
+文件:
+
+- [PathEditingViewModel.cs](/C:/Users/Tellme/apps/NavisworksTransport-rail-mount-modes/src/UI/WPF/ViewModels/PathEditingViewModel.cs)
+
+目标:
+
+- 终端安装辅助线全部改为基于 Canonical Space + ProjectReferenceFrame
+
+重点修改点:
+
+- `BuildAssemblyReferenceLine()`
+- `GetAssemblyTerminalAnchorPoint()`
+- 与辅助线、终点锚点、参考方向相关的计算
+
+必须消除:
+
+- `Vector3D direction = new Vector3D(centerPoint.X, centerPoint.Y, centerPoint.Z);`
+- 直接把世界原点当球心
+
+验收:
+
+- Y-up 项目中,若球心配置正确,辅助线方向与业务预期一致
+- Z-up 项目中,行为保持不退化
+
+### Task 5. 明确 UI 和日志的坐标语义
+
+目标:
+
+- 终端安装相关 UI、日志输出不混淆内部坐标和外部坐标
+
+建议:
+
+- 内部计算使用 Canonical
+- 对用户显示时,明确是否转回 Navisworks 外部坐标
+
+验收:
+
+- 日志中坐标语义一致,不再出现“内部算的是一种,打印的是另一种”
+
+## 4.3 M3 接入 Rail 三维姿态
+
+### Task 6. 改造 `RailPathPoseHelper`
+
+文件:
+
+- [RailPathPoseHelper.cs](/C:/Users/Tellme/apps/NavisworksTransport-rail-mount-modes/src/Utils/RailPathPoseHelper.cs)
+
+目标:
+
+- Rail 姿态计算统一基于 Canonical Space
+
+必须消除:
+
+- `new Vector3D(0, 0, 1)`
+- `worldUp = new Vector3D(0, 0, 1)`
+
+替换方式:
+
+- 改用适配层提供的 Canonical up
+- 或由 `ProjectReferenceFrame` 提供项目参考 up
+
+验收:
+
+- Y-up 项目与 Z-up 项目中,Rail 参考方向一致、俯仰/侧倾逻辑一致
+
+### Task 7. 清理 Rail 动画输入边界
+
+涉及文件:
+
+- [PathAnimationManager.cs](/C:/Users/Tellme/apps/NavisworksTransport-rail-mount-modes/src/Core/Animation/PathAnimationManager.cs)
+- [VirtualObjectManager.cs](/C:/Users/Tellme/apps/NavisworksTransport-rail-mount-modes/src/Core/VirtualObjectManager.cs)
+- [ModelItemTransformHelper.cs](/C:/Users/Tellme/apps/NavisworksTransport-rail-mount-modes/src/Utils/ModelItemTransformHelper.cs)
+
+目标:
+
+- 动画内部只消费 Canonical 姿态
+- 输出到 Navisworks 时再做反向转换
+
+验收:
+
+- Rail 虚拟物体与真实模型在 Y-up / Z-up 项目中的姿态语义一致
+
+### Task 7.1 明确模型局部轴约定
+
+目标:
+
+- 将“模型本地哪个轴代表 forward/up”从隐含假设提升为显式定义
+
+需要覆盖:
+
+- 虚拟物体资源
+- 终端安装辅助杆资源
+- 真实模型在 `Y-up` / `Z-up` 项目中的默认局部轴约定
+
+最低要求:
+
+- 至少明确 `LocalForwardAxis`
+- 至少明确 `LocalUpAxis`
+
+验收:
+
+- 不再出现“路径方向和通行空间都正确,但真实模型仍按错误本地 up 站立”的现象
+- `Y-up` 真实模型与 `Z-up` 真实模型都能通过显式局部轴约定接入 Rail 姿态链路
+
+## 4.4 M4 接入碰撞恢复和渲染
+
+### Task 8. 改造碰撞恢复链路
+
+涉及文件:
+
+- [CollisionSceneHelper.cs](/C:/Users/Tellme/apps/NavisworksTransport-rail-mount-modes/src/Utils/CollisionSceneHelper.cs)
+- [GenerateCollisionReportCommand.cs](/C:/Users/Tellme/apps/NavisworksTransport-rail-mount-modes/src/Commands/GenerateCollisionReportCommand.cs)
+- [ClashDetectiveIntegration.cs](/C:/Users/Tellme/apps/NavisworksTransport-rail-mount-modes/src/Core/Collision/ClashDetectiveIntegration.cs)
+
+目标:
+
+- 碰撞点恢复、报告截图、碰撞点回看全部使用统一的内部姿态语义
+
+验收:
+
+- Rail 碰撞报告恢复位置和动画实际姿态一致
+- 二维路径不被三维逻辑污染
+
+### Task 9. 改造渲染层
+
+文件:
+
+- [PathPointRenderPlugin.cs](/C:/Users/Tellme/apps/NavisworksTransport-rail-mount-modes/src/Core/PathPointRenderPlugin.cs)
+- [AssemblyReferencePathManager.cs](/C:/Users/Tellme/apps/NavisworksTransport-rail-mount-modes/src/Core/AssemblyReferencePathManager.cs)
+
+目标:
+
+- 辅助线、通行空间、点标记、碰撞点渲染,不再直接写死 `Normal = (0,0,1)`
+- 渲染层不仅要改“中心点/偏移量”,还要统一渲染几何的局部轴语义
+
+验收:
+
+- Y-up 项目中,渲染法向和可视化朝向不再依赖世界 Z
+- Y-up 项目中,默认通行空间和 `SP` 模式通行空间的高度轴与宿主 up 一致
+- 不再出现“中心点位置正确,但长方体/杆体本体仍按 Z-up 构造”的现象
+
+实施提示:
+
+- 长方体、圆柱体、辅助杆这类几何渲染,必须同时检查:
+ - 中心点是否正确
+ - `right/up/height` 局部轴是否正确
+ - `Normal` 是否仍偷用世界 `Z`
+- 不能只修改 `ApplyVerticalOffset(...)` 或中心点偏移,而保留旧的 `XY + Z` 轴构造公式
+
+## 5. 已知问题与注意事项
+
+### 5.1 当前不应优先展开的模块
+
+以下模块虽然也存在坐标系写死问题,但暂不作为本轮主线:
+
+- [ChannelHeightDetector.cs](/C:/Users/Tellme/apps/NavisworksTransport-rail-mount-modes/src/PathPlanning/ChannelHeightDetector.cs)
+- [SlopeAnalyzer.cs](/C:/Users/Tellme/apps/NavisworksTransport-rail-mount-modes/src/PathPlanning/SlopeAnalyzer.cs)
+- [OptimizedHeightCalculator.cs](/C:/Users/Tellme/apps/NavisworksTransport-rail-mount-modes/src/PathPlanning/OptimizedHeightCalculator.cs)
+
+原因:
+
+- 它们主要服务于自动路径规划
+- 当前项目主线优先级不在这里
+
+### 5.2 不能依赖 fallback 掩盖问题
+
+实施过程中禁止:
+
+- 三维路径恢复失败时偷偷退回 `yaw`
+- 外部坐标转换失败时偷偷按世界 `Z` 继续算
+- 业务基准点缺失时默认球心为 `(0,0,0)`
+
+必须让错误显式暴露。
+
+## 6. 当前实施优先顺序
+
+建议实际推进顺序如下:
+
+1. `HostCoordinateAdapter`
+2. `ProjectReferenceFrame`
+3. `PathEditingViewModel`
+4. `RailPathPoseHelper`
+5. `PathAnimationManager`
+6. `CollisionSceneHelper`
+7. `PathPointRenderPlugin`
+
+## 7. 完成判定
+
+当以下条件同时满足时,可认为本轮非自动路径规划坐标系统一改造达到阶段目标:
+
+- 终端安装仿真在 Y-up / Z-up 项目中语义一致
+- Rail 三维姿态在 Y-up / Z-up 项目中语义一致
+- 动画播放与碰撞恢复姿态一致
+- 碰撞报告截图与实际动画姿态一致
+- 主链路业务代码中不再直接写死世界原点和世界 `Z`
+
+## 8. 备注
+
+本清单服务于“先稳定非自动路径规划主线”的目标,不代表自动路径规划相关模块不重要。
+
+后续如要继续推进全项目坐标系统一,应单独启动自动路径规划相关的第二阶段清理计划。
diff --git a/run-unit-tests.bat b/run-unit-tests.bat
new file mode 100644
index 0000000..b5df301
--- /dev/null
+++ b/run-unit-tests.bat
@@ -0,0 +1,50 @@
+@echo off
+setlocal
+
+echo Building NavisworksTransport.UnitTests...
+
+set MSBUILD_PATH="C:\Program Files\Microsoft Visual Studio\2022\Community\MSBuild\Current\Bin\MSBuild.exe"
+if not exist %MSBUILD_PATH% (
+ set MSBUILD_PATH="C:\Program Files\Microsoft Visual Studio\2022\Professional\MSBuild\Current\Bin\MSBuild.exe"
+)
+if not exist %MSBUILD_PATH% (
+ echo MSBuild not found. Please install Visual Studio 2022 or Build Tools.
+ exit /b 1
+)
+
+set VSTEST_PATH="C:\Program Files\Microsoft Visual Studio\2022\Community\Common7\IDE\Extensions\TestPlatform\vstest.console.exe"
+if not exist %VSTEST_PATH% (
+ set VSTEST_PATH="C:\Program Files\Microsoft Visual Studio\2022\Professional\Common7\IDE\Extensions\TestPlatform\vstest.console.exe"
+)
+if not exist %VSTEST_PATH% (
+ echo vstest.console.exe not found. Please install Visual Studio 2022 test tools.
+ exit /b 1
+)
+
+set TEST_ADAPTER_PATH="packages\MSTest.TestAdapter.3.0.4\build\net462"
+if not exist %TEST_ADAPTER_PATH% (
+ echo MSTest adapter path not found: %TEST_ADAPTER_PATH%
+ exit /b 1
+)
+
+set NAVISWORKS_PATH=C:\Program Files\Autodesk\Navisworks Manage 2026
+if not exist "%NAVISWORKS_PATH%\Autodesk.Navisworks.Api.dll" (
+ echo Navisworks API path not found: %NAVISWORKS_PATH%
+ exit /b 1
+)
+
+set PATH=%NAVISWORKS_PATH%;%PATH%
+
+%MSBUILD_PATH% "NavisworksTransport.UnitTests.csproj" /p:Configuration=Release /p:Platform=x64 /verbosity:minimal
+if errorlevel 1 (
+ echo Unit test build failed!
+ exit /b 1
+)
+
+%VSTEST_PATH% "bin\x64\Release\NavisworksTransport.UnitTests.dll" /Platform:x64 /TestAdapterPath:%TEST_ADAPTER_PATH%
+if errorlevel 1 (
+ echo Unit tests failed!
+ exit /b 1
+)
+
+echo Unit tests passed!
diff --git a/src/Core/Animation/PathAnimationManager.cs b/src/Core/Animation/PathAnimationManager.cs
index 1f53517..5f2a693 100644
--- a/src/Core/Animation/PathAnimationManager.cs
+++ b/src/Core/Animation/PathAnimationManager.cs
@@ -8,6 +8,7 @@ using Autodesk.Navisworks.Api.Clash;
using NavisworksTransport.Core.Config;
using NavisworksTransport.Core.Spatial;
using NavisworksTransport.Utils;
+using NavisworksTransport.Utils.CoordinateSystem;
using NavisApplication = Autodesk.Navisworks.Api.Application;
namespace NavisworksTransport.Core.Animation
@@ -653,7 +654,12 @@ namespace NavisworksTransport.Core.Animation
startPosition = RailPathPoseHelper.ResolveBottomPosition(_route, startPosition, previousPoint, nextPoint, objectHeight);
LogManager.Debug($"[移动到起点] Rail路径调整: 参考点=({_pathPoints[0].X:F2},{_pathPoints[0].Y:F2},{_pathPoints[0].Z:F2}), 物体底面=({startPosition.X:F2},{startPosition.Y:F2},{startPosition.Z:F2}), 物体高度={objectHeight:F2}, 安装={_route.RailMountMode}, 对接={(PathRoute.IsTopPayloadAnchorForMountMode(_route.RailMountMode) ? "顶面对接" : "底面对接")}, 偏移={_route.RailReferenceToAnchorOffset:F2}");
- if (RailPathPoseHelper.TryCreateRailRotation(previousPoint, _pathPoints[0], nextPoint, out var railRotation))
+ if (RailPathPoseHelper.TryCreateRailRotation(
+ previousPoint,
+ _pathPoints[0],
+ nextPoint,
+ GetCurrentRailModelAxisConvention(),
+ out var railRotation))
{
var railLinearTransform = new Transform3D(railRotation).Linear;
LogManager.Info(
@@ -966,7 +972,12 @@ namespace NavisworksTransport.Core.Animation
};
if (_route.PathType == PathType.Rail &&
- RailPathPoseHelper.TryCreateRailRotation(previousFramePoint, framePosition, nextFramePoint, out var railRotation))
+ RailPathPoseHelper.TryCreateRailRotation(
+ previousFramePoint,
+ framePosition,
+ nextFramePoint,
+ GetCurrentRailModelAxisConvention(),
+ out var railRotation))
{
frame.Rotation = railRotation;
frame.HasCustomRotation = true;
@@ -2963,7 +2974,8 @@ namespace NavisworksTransport.Core.Animation
}
var boundingBox = _animatedObject.BoundingBox();
- return boundingBox.Max.Z - boundingBox.Min.Z;
+ var adapter = CoordinateSystemManager.Instance.CreateHostAdapter();
+ return ModelItemTransformHelper.GetHostHeight(boundingBox, adapter);
}
///
@@ -3077,12 +3089,34 @@ namespace NavisworksTransport.Core.Animation
var bounds = item.BoundingBox();
if (useStableRailAnchor)
{
- return ModelItemTransformHelper.GetStableBottomAnchorPoint(bounds, item.Transform);
+ var adapter = CoordinateSystemManager.Instance.CreateHostAdapter();
+ return ModelItemTransformHelper.GetHostBottomAnchorPoint(bounds, adapter);
}
return new Point3D(bounds.Center.X, bounds.Center.Y, bounds.Min.Z);
}
+ ///
+ /// Rail 路径的目标姿态以“本地 X 为前进方向、本地 Z 为上方向”为约定。
+ /// 真实模型在 Y-up 宿主里通常仍保持“本地 Y 为上方向”的资源语义,
+ /// 因此需要补一个基准修正:绕本地 X 轴 +90°,使模型的 Y-up 语义映射到 Rail 的 Z-up 期望语义。
+ /// 虚拟物体资源本身按 Z-up 构建,不需要此修正。
+ ///
+ private ModelAxisConvention GetCurrentRailModelAxisConvention()
+ {
+ if (_isVirtualObject)
+ {
+ return ModelAxisConvention.CreateDefaultForHost(CoordinateSystemType.ZUp);
+ }
+
+ var adapter = CoordinateSystemManager.Instance.CreateHostAdapter();
+ var convention = ModelAxisConvention.CreateDefaultForHost(adapter.HostType);
+ LogManager.Info(
+ $"[Rail姿态修正] Host={adapter.HostType}, 虚拟物体={_isVirtualObject}, " +
+ $"Forward={convention.ForwardAxis}, Up={convention.UpAxis}");
+ return convention;
+ }
+
///
/// 设置物体角度修正值(度,顺时针)
///
diff --git a/src/Core/PathPointRenderPlugin.cs b/src/Core/PathPointRenderPlugin.cs
index 3bdc5f6..ba45fb2 100644
--- a/src/Core/PathPointRenderPlugin.cs
+++ b/src/Core/PathPointRenderPlugin.cs
@@ -5,6 +5,7 @@ using Autodesk.Navisworks.Api;
using Autodesk.Navisworks.Api.Plugins;
using NavisworksTransport.Core.Config;
using NavisworksTransport.Utils;
+using NavisworksTransport.Utils.CoordinateSystem;
using static NavisworksTransport.Core.Config.ConfigManager;
namespace NavisworksTransport
@@ -938,7 +939,7 @@ namespace NavisworksTransport
return new CircleMarker
{
Center = marker.Position,
- Normal = new Vector3D(0, 0, 1),
+ Normal = GetHostUpVector(),
Radius = GetRadiusForGridVisualization(),
Color = style.Color,
Alpha = style.Alpha,
@@ -1373,7 +1374,7 @@ namespace NavisworksTransport
var grayEndMarker = new CircleMarker
{
Center = originalEndPoint,
- Normal = new Vector3D(0, 0, 1),
+ Normal = GetHostUpVector(),
Radius = GetRadiusForPointType(PathPointType.EndPoint),
Color = unreachedStyle.Color, // 未到达终点颜色
Alpha = unreachedStyle.Alpha, // 透明度,统一管理
@@ -1615,17 +1616,10 @@ namespace NavisworksTransport
}
else if (_showObjectSpace && Math.Abs(verticalOffset) > 0.001)
{
- // 通行空间模式且有垂直偏移时,沿Z轴平移(确保只有垂直偏移,没有水平偏移)
- adjustedStartPoint = new Point3D(
- startPoint.X,
- startPoint.Y,
- startPoint.Z + verticalOffset
- );
- adjustedEndPoint = new Point3D(
- endPoint.X,
- endPoint.Y,
- endPoint.Z + verticalOffset
- );
+ // 非 Rail 通行空间必须沿宿主 up 偏移,不能写死世界 Z。
+ var hostUp = GetHostUpVector();
+ adjustedStartPoint = ApplyVerticalOffset(startPoint, hostUp, verticalOffset);
+ adjustedEndPoint = ApplyVerticalOffset(endPoint, hostUp, verticalOffset);
}
else
{
@@ -1718,29 +1712,18 @@ namespace NavisworksTransport
}
else if (_showObjectSpace && Math.Abs(verticalOffset) > 0.001)
{
- // 通行空间模式且有垂直偏移时,沿Z轴平移(确保只有垂直偏移,没有水平偏移)
- adjustedStartPoint = new Point3D(
- startPoint.X,
- startPoint.Y,
- startPoint.Z + verticalOffset
- );
- adjustedEndPoint = new Point3D(
- endPoint.X,
- endPoint.Y,
- endPoint.Z + verticalOffset
- );
+ // 非 Rail 通行空间必须沿宿主 up 偏移,不能写死世界 Z。
+ var hostUp = GetHostUpVector();
+ adjustedStartPoint = ApplyVerticalOffset(startPoint, hostUp, verticalOffset);
+ adjustedEndPoint = ApplyVerticalOffset(endPoint, hostUp, verticalOffset);
- // 对 SampledPoints 应用垂直偏移(沿Z轴)
+ // 对 SampledPoints 应用同一宿主 up 方向偏移。
adjustedSampledPoints = new List();
if (edge.SampledPoints != null)
{
foreach (var point in edge.SampledPoints)
{
- adjustedSampledPoints.Add(new Point3D(
- point.X,
- point.Y,
- point.Z + verticalOffset
- ));
+ adjustedSampledPoints.Add(ApplyVerticalOffset(point, hostUp, verticalOffset));
}
}
}
@@ -1810,20 +1793,26 @@ namespace NavisworksTransport
segmentDirection.Z / segmentLength
);
- // 计算垂直向量(宽度方向)
- // 使用方向向量与Z轴的叉积来计算right向量,确保right向量在水平面上
- var right = new Vector3D(normalizedDirection.Y, -normalizedDirection.X, 0);
+ // 使用宿主 up 方向构造水平 right 向量,避免把世界 Z 写死到 Y-up 项目里。
+ var hostUp = GetHostUpVector();
+ var right = Cross(normalizedDirection, hostUp);
- // 如果方向向量垂直于XY平面(吊装路径),则使用水平段的方向向量来计算right
- if (Math.Abs(normalizedDirection.Z) > 0.9)
+ // 如果方向向量接近宿主 up(垂直段),则使用水平段方向重新构造 right。
+ double parallelToUp = Math.Abs(
+ normalizedDirection.X * hostUp.X +
+ normalizedDirection.Y * hostUp.Y +
+ normalizedDirection.Z * hostUp.Z);
+ if (parallelToUp > 0.9)
{
if (horizontalDirection != null)
{
- right = new Vector3D(horizontalDirection.Y, -horizontalDirection.X, 0);
+ right = Cross(horizontalDirection, hostUp);
}
else
{
- right = new Vector3D(1, 0, 0);
+ right = Math.Abs(hostUp.X) < 0.9
+ ? Normalize(Cross(hostUp, new Vector3D(1, 0, 0)))
+ : Normalize(Cross(hostUp, new Vector3D(0, 1, 0)));
}
}
@@ -1973,18 +1962,10 @@ namespace NavisworksTransport
}
else if (_showObjectSpace && Math.Abs(verticalOffset) > 0.001)
{
- // 通行空间模式且有垂直偏移时
- // 所有空中路径:统一沿Z轴向下偏移,顶面中心对齐路径点
- adjustedStartPoint = new Point3D(
- startPoint.Position.X,
- startPoint.Position.Y,
- startPoint.Position.Z + verticalOffset
- );
- adjustedEndPoint = new Point3D(
- endPoint.Position.X,
- endPoint.Position.Y,
- endPoint.Position.Z + verticalOffset
- );
+ // 非 Rail 通行空间统一沿宿主 up 方向偏移,不能再默认世界 Z。
+ var hostUp = GetHostUpVector();
+ adjustedStartPoint = ApplyVerticalOffset(startPoint.Position, hostUp, verticalOffset);
+ adjustedEndPoint = ApplyVerticalOffset(endPoint.Position, hostUp, verticalOffset);
}
else
{
@@ -2068,35 +2049,43 @@ namespace NavisworksTransport
return point;
}
- // 归一化up向量
- var upLength = Math.Sqrt(up.X * up.X + up.Y * up.Y + up.Z * up.Z);
- if (upLength < 0.001)
+ var normalizedUp = Normalize(up);
+ if (normalizedUp.X == 0.0 && normalizedUp.Y == 0.0 && normalizedUp.Z == 0.0)
{
return point;
}
- var normalizedUp = new Vector3D(up.X / upLength, up.Y / upLength, up.Z / upLength);
- // 计算up向量在垂直平面上的投影(只保留Z分量)
- // 这样可以确保偏移只在垂直方向上,不会产生水平偏移
- var verticalComponent = new Vector3D(0, 0, normalizedUp.Z);
+ return new Point3D(
+ point.X + normalizedUp.X * verticalOffset,
+ point.Y + normalizedUp.Y * verticalOffset,
+ point.Z + normalizedUp.Z * verticalOffset
+ );
+ }
- // 计算垂直分量的长度
- var verticalComponentLength = Math.Abs(verticalComponent.Z);
+ private static Vector3D GetHostUpVector()
+ {
+ var adapter = CoordinateSystemManager.Instance.CreateHostAdapter();
+ return Normalize(adapter.FromCanonicalVector(HostCoordinateAdapter.CanonicalUp));
+ }
- if (verticalComponentLength < 0.001)
+ private static Vector3D Cross(Vector3D a, Vector3D b)
+ {
+ return new Vector3D(
+ a.Y * b.Z - a.Z * b.Y,
+ a.Z * b.X - a.X * b.Z,
+ a.X * b.Y - a.Y * b.X);
+ }
+
+ private static Vector3D Normalize(Vector3D vector)
+ {
+ double lengthSquared = vector.X * vector.X + vector.Y * vector.Y + vector.Z * vector.Z;
+ if (lengthSquared < 1e-9)
{
- // 如果up向量几乎是水平的,则直接使用纯Z方向
- return new Point3D(point.X, point.Y, point.Z + verticalOffset);
+ return new Vector3D(0, 0, 0);
}
- // 沿着垂直分量方向平移
- // 需要调整偏移量,因为垂直分量的长度可能不是1
- var adjustedOffset = verticalOffset * (Math.Sign(verticalComponent.Z) / verticalComponentLength);
- return new Point3D(
- point.X,
- point.Y,
- point.Z + adjustedOffset
- );
+ double length = Math.Sqrt(lengthSquared);
+ return new Vector3D(vector.X / length, vector.Y / length, vector.Z / length);
}
///
@@ -2125,7 +2114,7 @@ namespace NavisworksTransport
var tsMarker = new SquareMarker
{
Center = edge.Trajectory.Ts,
- Normal = new Vector3D(0, 0, 1),
+ Normal = GetHostUpVector(),
Size = tangentRadius * 2, // 直径
Height = tangentHeight, // 高度
Color = tangentColor,
@@ -2139,7 +2128,7 @@ namespace NavisworksTransport
var teMarker = new SquareMarker
{
Center = edge.Trajectory.Te,
- Normal = new Vector3D(0, 0, 1),
+ Normal = GetHostUpVector(),
Size = tangentRadius * 2, // 直径
Height = tangentHeight, // 高度
Color = tangentColor,
@@ -2222,7 +2211,7 @@ namespace NavisworksTransport
return new CircleMarker
{
Center = point.Position,
- Normal = new Vector3D(0, 0, 1),
+ Normal = GetHostUpVector(),
Radius = isGridVisualization ? GetRadiusForGridVisualization() : GetRadiusForPointType(point.Type),
Color = isGridVisualization ? gridStyle.Color : GetColorForPointType(point.Type),
Alpha = isGridVisualization ? gridStyle.Alpha : 1.0,
@@ -2745,21 +2734,24 @@ namespace NavisworksTransport
// 归一化方向向量
direction = new Vector3D(direction.X / length, direction.Y / length, direction.Z / length);
- // 计算垂直向量(在XY平面内垂直于方向向量)
- var right = new Vector3D(-direction.Y, direction.X, 0);
+ var hostUp = GetHostUpVector();
+ var right = Cross(direction, hostUp);
- // 如果方向向量垂直于XY平面(吊装路径),则使用水平段的方向向量来计算right
- if (Math.Abs(direction.Z) > 0.9)
+ double parallelToUp = Math.Abs(
+ direction.X * hostUp.X +
+ direction.Y * hostUp.Y +
+ direction.Z * hostUp.Z);
+ if (parallelToUp > 0.9)
{
if (horizontalDirection != null)
{
- // 垂直段的 right 向量应该垂直于水平段的方向向量
- right = new Vector3D(horizontalDirection.Y, -horizontalDirection.X, 0);
+ right = Cross(horizontalDirection, hostUp);
}
else
{
- // 如果没有提供水平段方向向量,使用默认值
- right = new Vector3D(1, 0, 0);
+ right = Math.Abs(hostUp.X) < 0.9
+ ? Normalize(Cross(hostUp, new Vector3D(1, 0, 0)))
+ : Normalize(Cross(hostUp, new Vector3D(0, 1, 0)));
}
}
@@ -2837,21 +2829,24 @@ namespace NavisworksTransport
direction = new Vector3D(direction.X / directionLength, direction.Y / directionLength, direction.Z / directionLength);
- // 计算垂直向量(在XY平面内垂直于方向向量)
- var right = new Vector3D(-direction.Y, direction.X, 0);
+ var hostUp = GetHostUpVector();
+ var right = Cross(direction, hostUp);
- // 如果方向向量垂直于XY平面(吊装路径),则使用水平段的方向向量来计算right
- if (Math.Abs(direction.Z) > 0.9)
+ double parallelToUp = Math.Abs(
+ direction.X * hostUp.X +
+ direction.Y * hostUp.Y +
+ direction.Z * hostUp.Z);
+ if (parallelToUp > 0.9)
{
if (horizontalDirection != null)
{
- // 垂直段的 right 向量应该垂直于水平段的方向向量
- right = new Vector3D(-horizontalDirection.Y, horizontalDirection.X, 0);
+ right = Cross(horizontalDirection, hostUp);
}
else
{
- // 如果没有提供水平段方向向量,使用默认值
- right = new Vector3D(1, 0, 0);
+ right = Math.Abs(hostUp.X) < 0.9
+ ? Normalize(Cross(hostUp, new Vector3D(1, 0, 0)))
+ : Normalize(Cross(hostUp, new Vector3D(0, 1, 0)));
}
}
diff --git a/src/PathPlanning/ChannelBasedGridBuilder.cs b/src/PathPlanning/ChannelBasedGridBuilder.cs
index 3d82c76..4c2a29c 100644
--- a/src/PathPlanning/ChannelBasedGridBuilder.cs
+++ b/src/PathPlanning/ChannelBasedGridBuilder.cs
@@ -101,7 +101,9 @@ namespace NavisworksTransport.PathPlanning
LogManager.Info($"[通道网格构建器] 通道总边界: {FormatBounds(totalBounds)}");
// 3. 创建网格地图
- var gridMap = new GridMap(totalBounds, gridSize);
+ // 必须继续传递当前构建器绑定的坐标系,避免同一轮流程里
+ // ChannelBasedGridBuilder、GridMap、ChannelHeightDetector 各自重新抓取 Current。
+ var gridMap = new GridMap(totalBounds, gridSize, _coordinateSystem);
// 4. 为每个通道生成精确投影
var processedChannels = new List();
@@ -397,7 +399,7 @@ namespace NavisworksTransport.PathPlanning
var emptyBounds = new BoundingBox3D(new Point3D(0, 0, 0), new Point3D(0, 0, 0));
return new ChannelCoverage
{
- GridMap = new GridMap(emptyBounds, 1.0),
+ GridMap = new GridMap(emptyBounds, 1.0, _coordinateSystem),
ChannelItems = new List(),
TotalBounds = emptyBounds
};
diff --git a/src/UI/WPF/ViewModels/PathEditingViewModel.cs b/src/UI/WPF/ViewModels/PathEditingViewModel.cs
index cdbeba0..ce55ad8 100644
--- a/src/UI/WPF/ViewModels/PathEditingViewModel.cs
+++ b/src/UI/WPF/ViewModels/PathEditingViewModel.cs
@@ -18,6 +18,7 @@ using NavisworksTransport.Commands;
using NavisworksTransport.UI.WPF.Views;
using NavisworksTransport.UI.WPF.Models;
using NavisworksTransport.Utils;
+using NavisworksTransport.Utils.CoordinateSystem;
using NavisworksTransport;
namespace NavisworksTransport.UI.WPF.ViewModels
@@ -122,11 +123,17 @@ namespace NavisworksTransport.UI.WPF.ViewModels
private const double DefaultAssemblyReferenceRodLengthInMeters = 20.0;
private static readonly double DefaultAssemblyReferenceRodDiameterInMeters = CalculateDefaultAssemblyReferenceRodDiameterInMeters();
private const double DefaultAssemblyAnchorVerticalOffsetInMeters = 0.0;
+ private const double DefaultAssemblySphereCenterX = 0.0;
+ private const double DefaultAssemblySphereCenterY = 0.0;
+ private const double DefaultAssemblySphereCenterZ = 0.0;
private string _assemblyTerminalObjectName = "未选择";
private string _assemblyTerminalObjectInfo = "请选择终点处已安装箱体";
private double _assemblyReferenceRodLengthInMeters = DefaultAssemblyReferenceRodLengthInMeters;
private double _assemblyReferenceRodDiameterInMeters = DefaultAssemblyReferenceRodDiameterInMeters;
private double _assemblyAnchorVerticalOffsetInMeters = DefaultAssemblyAnchorVerticalOffsetInMeters;
+ private double _assemblySphereCenterX = DefaultAssemblySphereCenterX;
+ private double _assemblySphereCenterY = DefaultAssemblySphereCenterY;
+ private double _assemblySphereCenterZ = DefaultAssemblySphereCenterZ;
private string _assemblyStartPointText = "未选择";
private RailMountMode _assemblyMountMode = RailMountMode.UnderRail;
private bool _hasAssemblyTerminalObject;
@@ -365,6 +372,45 @@ namespace NavisworksTransport.UI.WPF.ViewModels
}
}
+ public double AssemblySphereCenterX
+ {
+ get => _assemblySphereCenterX;
+ set
+ {
+ if (SetProperty(ref _assemblySphereCenterX, value) && HasAssemblyTerminalObject)
+ {
+ RefreshAssemblyTerminalObjectInfo();
+ RefreshAssemblyReferenceRodIfNeeded();
+ }
+ }
+ }
+
+ public double AssemblySphereCenterY
+ {
+ get => _assemblySphereCenterY;
+ set
+ {
+ if (SetProperty(ref _assemblySphereCenterY, value) && HasAssemblyTerminalObject)
+ {
+ RefreshAssemblyTerminalObjectInfo();
+ RefreshAssemblyReferenceRodIfNeeded();
+ }
+ }
+ }
+
+ public double AssemblySphereCenterZ
+ {
+ get => _assemblySphereCenterZ;
+ set
+ {
+ if (SetProperty(ref _assemblySphereCenterZ, value) && HasAssemblyTerminalObject)
+ {
+ RefreshAssemblyTerminalObjectInfo();
+ RefreshAssemblyReferenceRodIfNeeded();
+ }
+ }
+ }
+
public bool HasAssemblyTerminalObject
{
get => _hasAssemblyTerminalObject;
@@ -1461,16 +1507,21 @@ namespace NavisworksTransport.UI.WPF.ViewModels
throw new InvalidOperationException("终点箱体未设置或已失效,无法计算对接点");
}
+ HostCoordinateAdapter adapter = CoordinateSystemManager.Instance.CreateHostAdapter();
+
var bounds = _assemblyTerminalObject.BoundingBox();
- var center = bounds.Center;
- Vector3D up = ModelItemTransformHelper.GetUpDirectionFromTransform(_assemblyTerminalObject.Transform);
+ Point3D canonicalCenter = adapter.ToCanonicalPoint(bounds.Center);
+ Vector3D canonicalUp = adapter.ToCanonicalVector(
+ ModelItemTransformHelper.GetUpDirectionFromTransform(_assemblyTerminalObject.Transform));
double direction = GetAssemblyAnchorDirection();
double verticalOffset = UnitsConverter.ConvertFromMeters(AssemblyAnchorVerticalOffsetInMeters);
- return new Point3D(
- center.X + up.X * verticalOffset * direction,
- center.Y + up.Y * verticalOffset * direction,
- center.Z + up.Z * verticalOffset * direction);
+ Point3D canonicalAnchorPoint = new Point3D(
+ canonicalCenter.X + canonicalUp.X * verticalOffset * direction,
+ canonicalCenter.Y + canonicalUp.Y * verticalOffset * direction,
+ canonicalCenter.Z + canonicalUp.Z * verticalOffset * direction);
+
+ return adapter.FromCanonicalPoint(canonicalAnchorPoint);
}
private AssemblyReferenceLine BuildAssemblyReferenceLine()
@@ -1480,28 +1531,38 @@ namespace NavisworksTransport.UI.WPF.ViewModels
throw new InvalidOperationException("终点箱体未设置或已失效,无法生成装配参考线");
}
+ HostCoordinateAdapter adapter = CoordinateSystemManager.Instance.CreateHostAdapter();
+ ProjectReferenceFrame projectFrame = CreateAssemblyProjectReferenceFrame(adapter);
+
BoundingBox3D bounds = _assemblyTerminalObject.BoundingBox();
Point3D centerPoint = bounds.Center;
Point3D endPoint = GetAssemblyTerminalAnchorPoint();
- Vector3D direction = new Vector3D(centerPoint.X, centerPoint.Y, centerPoint.Z);
+
+ Point3D canonicalCenterPoint = adapter.ToCanonicalPoint(centerPoint);
+ Point3D canonicalEndPoint = adapter.ToCanonicalPoint(endPoint);
+ Vector3D direction = new Vector3D(
+ canonicalCenterPoint.X - projectFrame.SphereCenterInCanonical.X,
+ canonicalCenterPoint.Y - projectFrame.SphereCenterInCanonical.Y,
+ canonicalCenterPoint.Z - projectFrame.SphereCenterInCanonical.Z);
double directionLengthSquared = direction.X * direction.X + direction.Y * direction.Y + direction.Z * direction.Z;
if (directionLengthSquared < 1e-9)
{
- throw new InvalidOperationException("箱体中心与世界原点重合,无法生成装配参考线方向");
+ throw new InvalidOperationException("箱体中心与项目球心重合,无法生成装配参考线方向");
}
direction = direction.Normalize();
double rodLength = UnitsConverter.ConvertFromMeters(AssemblyReferenceRodLengthInMeters);
- Point3D startPoint = new Point3D(
- endPoint.X + direction.X * rodLength,
- endPoint.Y + direction.Y * rodLength,
- endPoint.Z + direction.Z * rodLength);
+ Point3D canonicalStartPoint = new Point3D(
+ canonicalEndPoint.X + direction.X * rodLength,
+ canonicalEndPoint.Y + direction.Y * rodLength,
+ canonicalEndPoint.Z + direction.Z * rodLength);
+ Point3D startPoint = adapter.FromCanonicalPoint(canonicalStartPoint);
LogManager.Info(
$"[直线装配] 参考线已计算,终点锚点=({endPoint.X:F2}, {endPoint.Y:F2}, {endPoint.Z:F2}), " +
$"箱体中心=({centerPoint.X:F2}, {centerPoint.Y:F2}, {centerPoint.Z:F2}), " +
$"参考线外端=({startPoint.X:F2}, {startPoint.Y:F2}, {startPoint.Z:F2}), " +
- $"原点到箱体中心方向=({direction.X:F3}, {direction.Y:F3}, {direction.Z:F3})");
+ $"球心到箱体中心方向(内部坐标)=({direction.X:F3}, {direction.Y:F3}, {direction.Z:F3})");
return new AssemblyReferenceLine(startPoint, endPoint, direction);
}
@@ -1743,17 +1804,40 @@ namespace NavisworksTransport.UI.WPF.ViewModels
return;
}
+ HostCoordinateAdapter adapter = CoordinateSystemManager.Instance.CreateHostAdapter();
+ ProjectReferenceFrame projectFrame = CreateAssemblyProjectReferenceFrame(adapter);
+
Point3D centerPoint = _assemblyTerminalObject.BoundingBox().Center;
+ Point3D sphereCenterPoint = adapter.FromCanonicalPoint(projectFrame.SphereCenterInCanonical);
renderPlugin.RenderRailBaseline(
AssemblyCenterGuideLinePathId,
- new List { Point3D.Origin, centerPoint },
+ new List { sphereCenterPoint, centerPoint },
RenderStyleName.AssemblyGuideLine);
LogManager.Info(
- $"[直线装配] 已渲染原点到箱体中心基准线: 原点=(0.00, 0.00, 0.00), " +
+ $"[直线装配] 已渲染球心到箱体中心基准线: 球心=({sphereCenterPoint.X:F2}, {sphereCenterPoint.Y:F2}, {sphereCenterPoint.Z:F2}), " +
$"箱体中心=({centerPoint.X:F2}, {centerPoint.Y:F2}, {centerPoint.Z:F2})");
}
+ private ProjectReferenceFrame CreateAssemblyProjectReferenceFrame(HostCoordinateAdapter adapter)
+ {
+ if (adapter == null)
+ {
+ throw new ArgumentNullException(nameof(adapter));
+ }
+
+ Point3D hostSphereCenter = new Point3D(
+ AssemblySphereCenterX,
+ AssemblySphereCenterY,
+ AssemblySphereCenterZ);
+
+ Point3D canonicalSphereCenter = adapter.ToCanonicalPoint(hostSphereCenter);
+ return new ProjectReferenceFrame(
+ canonicalSphereCenter,
+ HostCoordinateAdapter.CanonicalUp,
+ ModelAxisConvention.CreateDefaultForHost(adapter.HostType));
+ }
+
private void ClearAssemblyAnchorMarker()
{
var renderPlugin = PathPointRenderPlugin.Instance;
diff --git a/src/UI/WPF/Views/PathEditingView.xaml b/src/UI/WPF/Views/PathEditingView.xaml
index bc48783..6c14832 100644
--- a/src/UI/WPF/Views/PathEditingView.xaml
+++ b/src/UI/WPF/Views/PathEditingView.xaml
@@ -328,11 +328,11 @@ NavisworksTransport 路径编辑页签视图 - 采用与动画控制和分层管
DisplayMemberPath="DisplayName"
ToolTip="选择直线装配路径使用轨上安装还是轨下安装"/>
-
-
-
-
-
+
+
+
+
+
-
-
-
-
-
+ Style="{StaticResource StatusTextStyle}"
+ VerticalAlignment="Center"
+ TextWrapping="Wrap"/>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ public struct CanonicalBounds3
+ {
+ public Vector3 Min { get; }
+ public Vector3 Max { get; }
+
+ public CanonicalBounds3(Vector3 min, Vector3 max)
+ {
+ Min = Vector3.Min(min, max);
+ Max = Vector3.Max(min, max);
+ }
+ }
+}
diff --git a/src/Utils/CoordinateSystem/CanonicalRailPoseBuilder.cs b/src/Utils/CoordinateSystem/CanonicalRailPoseBuilder.cs
new file mode 100644
index 0000000..a4f82b3
--- /dev/null
+++ b/src/Utils/CoordinateSystem/CanonicalRailPoseBuilder.cs
@@ -0,0 +1,83 @@
+using System;
+using System.Numerics;
+
+namespace NavisworksTransport.Utils.CoordinateSystem
+{
+ ///
+ /// 在内部 Canonical Space 中构造 Rail 姿态。
+ /// 这一层只处理纯数学切向/法向/局部轴约定,不依赖 Navisworks API。
+ ///
+ public static class CanonicalRailPoseBuilder
+ {
+ private const float TangentEpsilon = 1e-6f;
+
+ public static bool TryCreateBasis(
+ Vector3 previousPoint,
+ Vector3 currentPoint,
+ Vector3 nextPoint,
+ Vector3 canonicalUp,
+ out Vector3 forward,
+ out Vector3 lateral,
+ out Vector3 up)
+ {
+ forward = default;
+ lateral = default;
+ up = default;
+
+ Vector3 tangent = nextPoint - previousPoint;
+ if (tangent.LengthSquared() < TangentEpsilon)
+ {
+ tangent = nextPoint - currentPoint;
+ }
+
+ if (tangent.LengthSquared() < TangentEpsilon)
+ {
+ return false;
+ }
+
+ forward = Vector3.Normalize(tangent);
+ Vector3 projectedUp = canonicalUp - Vector3.Dot(canonicalUp, forward) * forward;
+ if (projectedUp.LengthSquared() < TangentEpsilon)
+ {
+ projectedUp = Math.Abs(forward.Z) < 0.9f
+ ? Vector3.UnitZ
+ : Vector3.UnitY;
+ projectedUp = projectedUp - Vector3.Dot(projectedUp, forward) * forward;
+ }
+
+ if (projectedUp.LengthSquared() < TangentEpsilon)
+ {
+ return false;
+ }
+
+ up = Vector3.Normalize(projectedUp);
+ lateral = Vector3.Normalize(Vector3.Cross(up, forward));
+ up = Vector3.Normalize(Vector3.Cross(forward, lateral));
+ return true;
+ }
+
+ public static bool TryCreateQuaternion(
+ Vector3 previousPoint,
+ Vector3 currentPoint,
+ Vector3 nextPoint,
+ Vector3 canonicalUp,
+ ModelAxisConvention convention,
+ out Quaternion rotation)
+ {
+ rotation = Quaternion.Identity;
+
+ if (convention == null)
+ {
+ throw new ArgumentNullException(nameof(convention));
+ }
+
+ if (!TryCreateBasis(previousPoint, currentPoint, nextPoint, canonicalUp, out var forward, out _, out var up))
+ {
+ return false;
+ }
+
+ rotation = convention.CreateQuaternion(forward, up);
+ return true;
+ }
+ }
+}
diff --git a/src/Utils/CoordinateSystem/CoordinateSystemManager.cs b/src/Utils/CoordinateSystem/CoordinateSystemManager.cs
index 9779940..05655d9 100644
--- a/src/Utils/CoordinateSystem/CoordinateSystemManager.cs
+++ b/src/Utils/CoordinateSystem/CoordinateSystemManager.cs
@@ -37,6 +37,12 @@ namespace NavisworksTransport.Utils.CoordinateSystem
///
public CoordinateSystemType ConfiguredType => _configuredType;
+ ///
+ /// 解析后的宿主坐标系类型。
+ /// AutoDetect 模式下返回当前实际检测结果。
+ ///
+ public CoordinateSystemType ResolvedType => GetResolvedType();
+
#endregion
#region 构造函数
@@ -123,6 +129,14 @@ namespace NavisworksTransport.Utils.CoordinateSystem
return $"类型: {_current.Type}, 向上向量: ({_current.UpVector.X:F2}, {_current.UpVector.Y:F2}, {_current.UpVector.Z:F2})";
}
+ ///
+ /// 获取当前宿主坐标到 Canonical Space 的统一适配器。
+ ///
+ public HostCoordinateAdapter CreateHostAdapter()
+ {
+ return new HostCoordinateAdapter(GetResolvedType());
+ }
+
#endregion
#region 私有方法
@@ -193,6 +207,27 @@ namespace NavisworksTransport.Utils.CoordinateSystem
}
}
+ ///
+ /// 获取当前解析后的宿主坐标系类型。
+ ///
+ public CoordinateSystemType GetResolvedType()
+ {
+ if (_current == null)
+ {
+ return CoordinateSystemType.ZUp;
+ }
+
+ switch (_current.Type)
+ {
+ case CoordinateSystemType.YUp:
+ return CoordinateSystemType.YUp;
+ case CoordinateSystemType.ZUp:
+ return CoordinateSystemType.ZUp;
+ default:
+ return CoordinateSystemType.ZUp;
+ }
+ }
+
#endregion
}
}
diff --git a/src/Utils/CoordinateSystem/HostCoordinateAdapter.cs b/src/Utils/CoordinateSystem/HostCoordinateAdapter.cs
new file mode 100644
index 0000000..4fb862f
--- /dev/null
+++ b/src/Utils/CoordinateSystem/HostCoordinateAdapter.cs
@@ -0,0 +1,274 @@
+using System;
+using System.Collections.Generic;
+using System.Numerics;
+using Autodesk.Navisworks.Api;
+
+namespace NavisworksTransport.Utils.CoordinateSystem
+{
+ ///
+ /// Navisworks 外部坐标与内部 Canonical Space 之间的统一适配器。
+ ///
+ /// 约定:
+ /// - 外部坐标 = Navisworks API 返回的世界坐标
+ /// - 内部坐标 = Canonical Space,固定为 Z-up
+ ///
+ /// 当前仅支持:
+ /// - Host Z-up -> Canonical Z-up(恒等)
+ /// - Host Y-up -> Canonical Z-up(绕 X 轴 +90° 的等价变换)
+ ///
+ public sealed class HostCoordinateAdapter
+ {
+ ///
+ /// Canonical Space 固定为 Z-up。
+ ///
+ public static readonly Vector3 CanonicalUpVector3 = new Vector3(0f, 0f, 1f);
+ public static readonly Vector3D CanonicalUp = new Vector3D(0, 0, 1);
+
+ ///
+ /// 当前宿主坐标系类型。
+ ///
+ public CoordinateSystemType HostType { get; }
+
+ ///
+ /// 宿主坐标系中“向上轴”的索引。
+ /// Y-up => 1, Z-up => 2。
+ ///
+ public int HostUpAxisIndex => HostType == CoordinateSystemType.YUp ? 1 : 2;
+
+ ///
+ /// 宿主坐标系中的单位 up 向量。
+ ///
+ public Vector3 HostUpVector3 => FromCanonicalVector3(CanonicalUpVector3);
+ public Vector3D HostUpVector => FromCanonicalVector(CanonicalUp);
+
+ public HostCoordinateAdapter(CoordinateSystemType hostType)
+ {
+ if (hostType == CoordinateSystemType.AutoDetect)
+ {
+ throw new ArgumentException("HostCoordinateAdapter 不接受 AutoDetect,必须传入明确的宿主坐标系。", nameof(hostType));
+ }
+
+ HostType = hostType;
+ }
+
+ ///
+ /// 从当前坐标系管理器创建适配器。
+ ///
+ public static HostCoordinateAdapter CreateFromManager()
+ {
+ var hostType = CoordinateSystemManager.Instance.ConfiguredType == CoordinateSystemType.AutoDetect
+ ? CoordinateSystemManager.Instance.GetResolvedType()
+ : CoordinateSystemManager.Instance.ConfiguredType;
+
+ return new HostCoordinateAdapter(hostType);
+ }
+
+ public Point3D ToCanonicalPoint(Point3D hostPoint)
+ {
+ if (hostPoint == null)
+ {
+ throw new ArgumentNullException(nameof(hostPoint));
+ }
+
+ return ToPoint3D(ToCanonicalPoint3(ToVector3(hostPoint)));
+ }
+
+ public Point3D FromCanonicalPoint(Point3D canonicalPoint)
+ {
+ if (canonicalPoint == null)
+ {
+ throw new ArgumentNullException(nameof(canonicalPoint));
+ }
+
+ return ToPoint3D(FromCanonicalPoint3(ToVector3(canonicalPoint)));
+ }
+
+ public Vector3D ToCanonicalVector(Vector3D hostVector)
+ {
+ if (hostVector == null)
+ {
+ throw new ArgumentNullException(nameof(hostVector));
+ }
+
+ return ToVector3D(ToCanonicalVector3(ToNumericsVector(hostVector)));
+ }
+
+ public Vector3D FromCanonicalVector(Vector3D canonicalVector)
+ {
+ if (canonicalVector == null)
+ {
+ throw new ArgumentNullException(nameof(canonicalVector));
+ }
+
+ return ToVector3D(FromCanonicalVector3(ToNumericsVector(canonicalVector)));
+ }
+
+ public BoundingBox3D ToCanonicalBounds(BoundingBox3D hostBounds)
+ {
+ if (hostBounds == null)
+ {
+ throw new ArgumentNullException(nameof(hostBounds));
+ }
+
+ CanonicalBounds3 canonicalBounds = ToCanonicalBounds3(new CanonicalBounds3(
+ ToVector3(hostBounds.Min),
+ ToVector3(hostBounds.Max)));
+ return new BoundingBox3D(ToPoint3D(canonicalBounds.Min), ToPoint3D(canonicalBounds.Max));
+ }
+
+ public BoundingBox3D FromCanonicalBounds(BoundingBox3D canonicalBounds)
+ {
+ if (canonicalBounds == null)
+ {
+ throw new ArgumentNullException(nameof(canonicalBounds));
+ }
+
+ CanonicalBounds3 hostBounds = FromCanonicalBounds3(new CanonicalBounds3(
+ ToVector3(canonicalBounds.Min),
+ ToVector3(canonicalBounds.Max)));
+ return new BoundingBox3D(ToPoint3D(hostBounds.Min), ToPoint3D(hostBounds.Max));
+ }
+
+ public Vector3 ToCanonicalPoint3(Vector3 hostPoint)
+ {
+ switch (HostType)
+ {
+ case CoordinateSystemType.ZUp:
+ return hostPoint;
+ case CoordinateSystemType.YUp:
+ return new Vector3(hostPoint.X, -hostPoint.Z, hostPoint.Y);
+ default:
+ throw new InvalidOperationException($"不支持的宿主坐标系: {HostType}");
+ }
+ }
+
+ public Vector3 FromCanonicalPoint3(Vector3 canonicalPoint)
+ {
+ switch (HostType)
+ {
+ case CoordinateSystemType.ZUp:
+ return canonicalPoint;
+ case CoordinateSystemType.YUp:
+ return new Vector3(canonicalPoint.X, canonicalPoint.Z, -canonicalPoint.Y);
+ default:
+ throw new InvalidOperationException($"不支持的宿主坐标系: {HostType}");
+ }
+ }
+
+ public Vector3 ToCanonicalVector3(Vector3 hostVector)
+ {
+ return ToCanonicalPoint3(hostVector);
+ }
+
+ public Vector3 FromCanonicalVector3(Vector3 canonicalVector)
+ {
+ return FromCanonicalPoint3(canonicalVector);
+ }
+
+ public CanonicalBounds3 ToCanonicalBounds3(CanonicalBounds3 hostBounds)
+ {
+ return RebuildBounds3(hostBounds, ToCanonicalPoint3);
+ }
+
+ public CanonicalBounds3 FromCanonicalBounds3(CanonicalBounds3 canonicalBounds)
+ {
+ return RebuildBounds3(canonicalBounds, FromCanonicalPoint3);
+ }
+
+ private static BoundingBox3D RebuildBounds(BoundingBox3D sourceBounds, Func pointTransform)
+ {
+ var corners = GetCorners(sourceBounds);
+ var transformedCorners = new List(corners.Count);
+
+ foreach (var corner in corners)
+ {
+ transformedCorners.Add(pointTransform(corner));
+ }
+
+ double minX = double.MaxValue;
+ double minY = double.MaxValue;
+ double minZ = double.MaxValue;
+ double maxX = double.MinValue;
+ double maxY = double.MinValue;
+ double maxZ = double.MinValue;
+
+ foreach (var point in transformedCorners)
+ {
+ minX = Math.Min(minX, point.X);
+ minY = Math.Min(minY, point.Y);
+ minZ = Math.Min(minZ, point.Z);
+ maxX = Math.Max(maxX, point.X);
+ maxY = Math.Max(maxY, point.Y);
+ maxZ = Math.Max(maxZ, point.Z);
+ }
+
+ return new BoundingBox3D(new Point3D(minX, minY, minZ), new Point3D(maxX, maxY, maxZ));
+ }
+
+ private static CanonicalBounds3 RebuildBounds3(CanonicalBounds3 sourceBounds, Func pointTransform)
+ {
+ var corners = GetCorners3(sourceBounds);
+ Vector3 min = new Vector3(float.MaxValue, float.MaxValue, float.MaxValue);
+ Vector3 max = new Vector3(float.MinValue, float.MinValue, float.MinValue);
+
+ foreach (var corner in corners)
+ {
+ Vector3 point = pointTransform(corner);
+ min = Vector3.Min(min, point);
+ max = Vector3.Max(max, point);
+ }
+
+ return new CanonicalBounds3(min, max);
+ }
+
+ private static List GetCorners(BoundingBox3D bounds)
+ {
+ return new List
+ {
+ new Point3D(bounds.Min.X, bounds.Min.Y, bounds.Min.Z),
+ new Point3D(bounds.Max.X, bounds.Min.Y, bounds.Min.Z),
+ new Point3D(bounds.Min.X, bounds.Max.Y, bounds.Min.Z),
+ new Point3D(bounds.Max.X, bounds.Max.Y, bounds.Min.Z),
+ new Point3D(bounds.Min.X, bounds.Min.Y, bounds.Max.Z),
+ new Point3D(bounds.Max.X, bounds.Min.Y, bounds.Max.Z),
+ new Point3D(bounds.Min.X, bounds.Max.Y, bounds.Max.Z),
+ new Point3D(bounds.Max.X, bounds.Max.Y, bounds.Max.Z)
+ };
+ }
+
+ private static List GetCorners3(CanonicalBounds3 bounds)
+ {
+ return new List
+ {
+ new Vector3(bounds.Min.X, bounds.Min.Y, bounds.Min.Z),
+ new Vector3(bounds.Max.X, bounds.Min.Y, bounds.Min.Z),
+ new Vector3(bounds.Min.X, bounds.Max.Y, bounds.Min.Z),
+ new Vector3(bounds.Max.X, bounds.Max.Y, bounds.Min.Z),
+ new Vector3(bounds.Min.X, bounds.Min.Y, bounds.Max.Z),
+ new Vector3(bounds.Max.X, bounds.Min.Y, bounds.Max.Z),
+ new Vector3(bounds.Min.X, bounds.Max.Y, bounds.Max.Z),
+ new Vector3(bounds.Max.X, bounds.Max.Y, bounds.Max.Z)
+ };
+ }
+
+ private static Vector3 ToVector3(Point3D point)
+ {
+ return new Vector3((float)point.X, (float)point.Y, (float)point.Z);
+ }
+
+ private static Vector3 ToNumericsVector(Vector3D vector)
+ {
+ return new Vector3((float)vector.X, (float)vector.Y, (float)vector.Z);
+ }
+
+ private static Point3D ToPoint3D(Vector3 point)
+ {
+ return new Point3D(point.X, point.Y, point.Z);
+ }
+
+ private static Vector3D ToVector3D(Vector3 vector)
+ {
+ return new Vector3D(vector.X, vector.Y, vector.Z);
+ }
+ }
+}
diff --git a/src/Utils/CoordinateSystem/LocalAxisDirection.cs b/src/Utils/CoordinateSystem/LocalAxisDirection.cs
new file mode 100644
index 0000000..79e061a
--- /dev/null
+++ b/src/Utils/CoordinateSystem/LocalAxisDirection.cs
@@ -0,0 +1,18 @@
+using Autodesk.Navisworks.Api;
+
+namespace NavisworksTransport.Utils.CoordinateSystem
+{
+ ///
+ /// 模型局部轴方向定义。
+ /// 用于显式表达本地哪个轴代表 forward/up。
+ ///
+ public enum LocalAxisDirection
+ {
+ PositiveX,
+ PositiveY,
+ PositiveZ,
+ NegativeX,
+ NegativeY,
+ NegativeZ
+ }
+}
diff --git a/src/Utils/CoordinateSystem/ModelAxisConvention.cs b/src/Utils/CoordinateSystem/ModelAxisConvention.cs
new file mode 100644
index 0000000..824979d
--- /dev/null
+++ b/src/Utils/CoordinateSystem/ModelAxisConvention.cs
@@ -0,0 +1,233 @@
+using System;
+using System.Numerics;
+using Autodesk.Navisworks.Api;
+
+namespace NavisworksTransport.Utils.CoordinateSystem
+{
+ ///
+ /// 模型局部轴约定。
+ /// 显式定义模型本地哪个轴是前进方向、哪个轴是上方向。
+ ///
+ public sealed class ModelAxisConvention
+ {
+ private readonly Vector3 _forwardUnitVector;
+ private readonly Vector3 _upUnitVector;
+
+ public LocalAxisDirection ForwardAxis { get; }
+ public LocalAxisDirection UpAxis { get; }
+
+ public ModelAxisConvention(LocalAxisDirection forwardAxis, LocalAxisDirection upAxis)
+ {
+ ForwardAxis = forwardAxis;
+ UpAxis = upAxis;
+
+ _forwardUnitVector = GetAxisVector3(forwardAxis);
+ _upUnitVector = GetAxisVector3(upAxis);
+ float dot = Vector3.Dot(_forwardUnitVector, _upUnitVector);
+ if (Math.Abs(dot) > 1e-6)
+ {
+ throw new ArgumentException("ForwardAxis 和 UpAxis 不能平行或重合。");
+ }
+ }
+
+ public Vector3 ForwardUnitVector => _forwardUnitVector;
+ public Vector3 UpUnitVector => _upUnitVector;
+ public Vector3D ForwardVector => ToNavVector(_forwardUnitVector);
+ public Vector3D UpVector => ToNavVector(_upUnitVector);
+
+ ///
+ /// 根据模型局部轴约定,直接构造“本地 forward/up 对齐到目标世界 forward/up”的线性姿态。
+ /// 返回矩阵的 3 列分别表示模型本地 +X/+Y/+Z 在世界中的方向。
+ ///
+ public Matrix3 CreateLinearTransform(Vector3D worldForward, Vector3D worldUp)
+ {
+ Matrix4x4 linear = CreateLinearTransform3(ToNumericsVector(worldForward), ToNumericsVector(worldUp));
+ return ToNavMatrix3(linear);
+ }
+
+ public Rotation3D CreateRotation(Vector3D worldForward, Vector3D worldUp)
+ {
+ Quaternion quaternion = CreateQuaternion(ToNumericsVector(worldForward), ToNumericsVector(worldUp));
+ return new Rotation3D(quaternion.X, quaternion.Y, quaternion.Z, quaternion.W);
+ }
+
+ public Matrix4x4 CreateLinearTransform3(Vector3 worldForward, Vector3 worldUp)
+ {
+ Vector3 normalizedWorldForward = Normalize(worldForward);
+ Vector3 normalizedWorldUp = Normalize(worldUp);
+ Vector3 worldRemaining = Normalize(Vector3.Cross(normalizedWorldForward, normalizedWorldUp));
+
+ Vector3 localRemaining = Normalize(Vector3.Cross(_forwardUnitVector, _upUnitVector));
+
+ Vector3 worldX = ResolveWorldAxisForLocalPositiveAxis(
+ 0,
+ normalizedWorldForward,
+ normalizedWorldUp,
+ worldRemaining,
+ localRemaining);
+ Vector3 worldY = ResolveWorldAxisForLocalPositiveAxis(
+ 1,
+ normalizedWorldForward,
+ normalizedWorldUp,
+ worldRemaining,
+ localRemaining);
+ Vector3 worldZ = ResolveWorldAxisForLocalPositiveAxis(
+ 2,
+ normalizedWorldForward,
+ normalizedWorldUp,
+ worldRemaining,
+ localRemaining);
+
+ return new Matrix4x4(
+ worldX.X, worldY.X, worldZ.X, 0f,
+ worldX.Y, worldY.Y, worldZ.Y, 0f,
+ worldX.Z, worldY.Z, worldZ.Z, 0f,
+ 0f, 0f, 0f, 1f);
+ }
+
+ public Quaternion CreateQuaternion(Vector3 worldForward, Vector3 worldUp)
+ {
+ return Quaternion.CreateFromRotationMatrix(CreateLinearTransform3(worldForward, worldUp));
+ }
+
+ ///
+ /// 当前项目里,真实模型默认遵循宿主 up 语义:
+ /// - Z-up 宿主 => 本地 X 前进,本地 Z 向上
+ /// - Y-up 宿主 => 本地 X 前进,本地 Y 向上
+ ///
+ public static ModelAxisConvention CreateDefaultForHost(CoordinateSystemType hostType)
+ {
+ switch (hostType)
+ {
+ case CoordinateSystemType.YUp:
+ return new ModelAxisConvention(LocalAxisDirection.PositiveX, LocalAxisDirection.PositiveY);
+ case CoordinateSystemType.ZUp:
+ default:
+ return new ModelAxisConvention(LocalAxisDirection.PositiveX, LocalAxisDirection.PositiveZ);
+ }
+ }
+
+ private static Vector3 GetAxisVector3(LocalAxisDirection axis)
+ {
+ switch (axis)
+ {
+ case LocalAxisDirection.PositiveX: return new Vector3(1f, 0f, 0f);
+ case LocalAxisDirection.PositiveY: return new Vector3(0f, 1f, 0f);
+ case LocalAxisDirection.PositiveZ: return new Vector3(0f, 0f, 1f);
+ case LocalAxisDirection.NegativeX: return new Vector3(-1f, 0f, 0f);
+ case LocalAxisDirection.NegativeY: return new Vector3(0f, -1f, 0f);
+ case LocalAxisDirection.NegativeZ: return new Vector3(0f, 0f, -1f);
+ default:
+ throw new ArgumentOutOfRangeException(nameof(axis), axis, null);
+ }
+ }
+
+ private Vector3 ResolveWorldAxisForLocalPositiveAxis(
+ int localAxisIndex,
+ Vector3 worldForward,
+ Vector3 worldUp,
+ Vector3 worldRemaining,
+ Vector3 localRemaining)
+ {
+ if (MatchesAxis(ForwardAxis, localAxisIndex, out int forwardSign))
+ {
+ return Scale(worldForward, forwardSign);
+ }
+
+ if (MatchesAxis(UpAxis, localAxisIndex, out int upSign))
+ {
+ return Scale(worldUp, upSign);
+ }
+
+ LocalAxisDirection remainingAxis = GetAxisDirection(localRemaining);
+ if (MatchesAxis(remainingAxis, localAxisIndex, out int remainingSign))
+ {
+ return Scale(worldRemaining, remainingSign);
+ }
+
+ throw new InvalidOperationException("无法为模型局部轴构造世界姿态矩阵。");
+ }
+
+ private static bool MatchesAxis(LocalAxisDirection axis, int localAxisIndex, out int sign)
+ {
+ switch (axis)
+ {
+ case LocalAxisDirection.PositiveX:
+ sign = 1;
+ return localAxisIndex == 0;
+ case LocalAxisDirection.NegativeX:
+ sign = -1;
+ return localAxisIndex == 0;
+ case LocalAxisDirection.PositiveY:
+ sign = 1;
+ return localAxisIndex == 1;
+ case LocalAxisDirection.NegativeY:
+ sign = -1;
+ return localAxisIndex == 1;
+ case LocalAxisDirection.PositiveZ:
+ sign = 1;
+ return localAxisIndex == 2;
+ case LocalAxisDirection.NegativeZ:
+ sign = -1;
+ return localAxisIndex == 2;
+ default:
+ throw new ArgumentOutOfRangeException(nameof(axis), axis, null);
+ }
+ }
+
+ private static LocalAxisDirection GetAxisDirection(Vector3 vector)
+ {
+ if (IsApproximately(vector, 1, 0, 0)) return LocalAxisDirection.PositiveX;
+ if (IsApproximately(vector, -1, 0, 0)) return LocalAxisDirection.NegativeX;
+ if (IsApproximately(vector, 0, 1, 0)) return LocalAxisDirection.PositiveY;
+ if (IsApproximately(vector, 0, -1, 0)) return LocalAxisDirection.NegativeY;
+ if (IsApproximately(vector, 0, 0, 1)) return LocalAxisDirection.PositiveZ;
+ if (IsApproximately(vector, 0, 0, -1)) return LocalAxisDirection.NegativeZ;
+
+ throw new InvalidOperationException("模型局部第三轴不是标准正交轴,无法确定局部轴约定。");
+ }
+
+ private static bool IsApproximately(Vector3 vector, double x, double y, double z)
+ {
+ return Math.Abs(vector.X - x) < 1e-6 &&
+ Math.Abs(vector.Y - y) < 1e-6 &&
+ Math.Abs(vector.Z - z) < 1e-6;
+ }
+
+ private static Vector3 Scale(Vector3 vector, int sign)
+ {
+ return sign == 1
+ ? vector
+ : new Vector3(-vector.X, -vector.Y, -vector.Z);
+ }
+
+ private static Vector3 Normalize(Vector3 vector)
+ {
+ float lengthSquared = vector.LengthSquared();
+ if (lengthSquared < 1e-12f)
+ {
+ throw new InvalidOperationException("无法为模型局部轴构造有效的正交侧向。");
+ }
+
+ return Vector3.Normalize(vector);
+ }
+
+ private static Matrix3 ToNavMatrix3(Matrix4x4 matrix)
+ {
+ return new Matrix3(
+ matrix.M11, matrix.M12, matrix.M13,
+ matrix.M21, matrix.M22, matrix.M23,
+ matrix.M31, matrix.M32, matrix.M33);
+ }
+
+ private static Vector3 ToNumericsVector(Vector3D vector)
+ {
+ return new Vector3((float)vector.X, (float)vector.Y, (float)vector.Z);
+ }
+
+ private static Vector3D ToNavVector(Vector3 vector)
+ {
+ return new Vector3D(vector.X, vector.Y, vector.Z);
+ }
+ }
+}
diff --git a/src/Utils/CoordinateSystem/ProjectReferenceFrame.cs b/src/Utils/CoordinateSystem/ProjectReferenceFrame.cs
new file mode 100644
index 0000000..e144c6d
--- /dev/null
+++ b/src/Utils/CoordinateSystem/ProjectReferenceFrame.cs
@@ -0,0 +1,97 @@
+using System;
+using System.Numerics;
+using Autodesk.Navisworks.Api;
+
+namespace NavisworksTransport.Utils.CoordinateSystem
+{
+ ///
+ /// 工程业务基准框架。
+ ///
+ /// 该对象建立在 Canonical Space 之上,用来承载:
+ /// - 球心
+ /// - 项目 up 方向
+ /// - 后续的终端安装基准和轨道参考面
+ ///
+ /// 注意:
+ /// - 这不是宿主坐标系定义
+ /// - 这不是 Navisworks 世界坐标
+ /// - 这是业务语义层
+ ///
+ public sealed class ProjectReferenceFrame
+ {
+ private readonly Vector3 _sphereCenterInCanonical;
+ private readonly Vector3 _projectUpInCanonical;
+
+ ///
+ /// Canonical Space 中的球心坐标。
+ ///
+ public Vector3 SphereCenterInCanonical3 => _sphereCenterInCanonical;
+ public Point3D SphereCenterInCanonical => new Point3D(_sphereCenterInCanonical.X, _sphereCenterInCanonical.Y, _sphereCenterInCanonical.Z);
+
+ ///
+ /// Canonical Space 中的项目上方向。
+ /// 当前固定与 Canonical Space 一致,为 (0,0,1)。
+ ///
+ public Vector3 ProjectUpInCanonical3 => _projectUpInCanonical;
+ public Vector3D ProjectUpInCanonical => new Vector3D(_projectUpInCanonical.X, _projectUpInCanonical.Y, _projectUpInCanonical.Z);
+
+ ///
+ /// 真实模型默认局部轴约定。
+ ///
+ public ModelAxisConvention DefaultModelAxisConvention { get; }
+
+ public ProjectReferenceFrame(
+ Point3D sphereCenterInCanonical,
+ Vector3D projectUpInCanonical,
+ ModelAxisConvention defaultModelAxisConvention)
+ : this(
+ new Vector3((float)sphereCenterInCanonical.X, (float)sphereCenterInCanonical.Y, (float)sphereCenterInCanonical.Z),
+ new Vector3((float)projectUpInCanonical.X, (float)projectUpInCanonical.Y, (float)projectUpInCanonical.Z),
+ defaultModelAxisConvention)
+ {
+ }
+
+ public ProjectReferenceFrame(
+ Vector3 sphereCenterInCanonical,
+ Vector3 projectUpInCanonical,
+ ModelAxisConvention defaultModelAxisConvention)
+ {
+ if (projectUpInCanonical.LengthSquared() < 1e-12f)
+ {
+ throw new ArgumentException("项目 up 方向不能为零向量。", nameof(projectUpInCanonical));
+ }
+
+ if (defaultModelAxisConvention == null)
+ {
+ throw new ArgumentNullException(nameof(defaultModelAxisConvention));
+ }
+
+ _sphereCenterInCanonical = sphereCenterInCanonical;
+ _projectUpInCanonical = Vector3.Normalize(projectUpInCanonical);
+ DefaultModelAxisConvention = defaultModelAxisConvention;
+ }
+
+ ///
+ /// 创建默认业务基准框架。
+ ///
+ /// 说明:
+ /// - 当前只是 M1 骨架,先显式使用原点和 CanonicalUp
+ /// - 后续应改为来自项目配置或显式求解,不应长期依赖该默认值
+ ///
+ public static ProjectReferenceFrame CreateDefault()
+ {
+ return CreateDefault(CoordinateSystemType.ZUp);
+ }
+
+ ///
+ /// 按宿主坐标系创建默认业务基准框架。
+ ///
+ public static ProjectReferenceFrame CreateDefault(CoordinateSystemType hostType)
+ {
+ return new ProjectReferenceFrame(
+ Vector3.Zero,
+ HostCoordinateAdapter.CanonicalUpVector3,
+ ModelAxisConvention.CreateDefaultForHost(hostType));
+ }
+ }
+}
diff --git a/src/Utils/ModelItemTransformHelper.cs b/src/Utils/ModelItemTransformHelper.cs
index b49342a..e32b4db 100644
--- a/src/Utils/ModelItemTransformHelper.cs
+++ b/src/Utils/ModelItemTransformHelper.cs
@@ -1,5 +1,6 @@
using System;
using Autodesk.Navisworks.Api;
+using NavisworksTransport.Utils.CoordinateSystem;
namespace NavisworksTransport.Utils
{
@@ -136,6 +137,58 @@ namespace NavisworksTransport.Utils
bounds.Center.Z - up.Z * halfHeight);
}
+ ///
+ /// 使用宿主坐标系的 up 轴,从世界 AABB 中提取“底部锚点”。
+ /// 适用于真实模型仍保持宿主坐标语义(如 Y-up 模型)的场景。
+ ///
+ public static Point3D GetHostBottomAnchorPoint(BoundingBox3D bounds, HostCoordinateAdapter adapter)
+ {
+ if (bounds == null)
+ {
+ return new Point3D(0, 0, 0);
+ }
+
+ if (adapter == null)
+ {
+ throw new ArgumentNullException(nameof(adapter));
+ }
+
+ switch (adapter.HostUpAxisIndex)
+ {
+ case 1:
+ return new Point3D(bounds.Center.X, bounds.Min.Y, bounds.Center.Z);
+ case 2:
+ default:
+ return new Point3D(bounds.Center.X, bounds.Center.Y, bounds.Min.Z);
+ }
+ }
+
+ ///
+ /// 使用宿主坐标系的 up 轴,从世界 AABB 中提取高度。
+ /// 适用于真实模型仍保持宿主坐标语义(如 Y-up 模型)的场景。
+ ///
+ public static double GetHostHeight(BoundingBox3D bounds, HostCoordinateAdapter adapter)
+ {
+ if (bounds == null)
+ {
+ return 0.0;
+ }
+
+ if (adapter == null)
+ {
+ throw new ArgumentNullException(nameof(adapter));
+ }
+
+ switch (adapter.HostUpAxisIndex)
+ {
+ case 1:
+ return bounds.Max.Y - bounds.Min.Y;
+ case 2:
+ default:
+ return bounds.Max.Z - bounds.Min.Z;
+ }
+ }
+
///
/// 根据世界轴对齐包围盒和物体变换,估算物体在局部坐标系中的尺寸。
/// 适用于刚性箱体类对象,用于顶/底面对接点推导。
diff --git a/src/Utils/RailPathPoseHelper.cs b/src/Utils/RailPathPoseHelper.cs
index 9bd81be..378b09f 100644
--- a/src/Utils/RailPathPoseHelper.cs
+++ b/src/Utils/RailPathPoseHelper.cs
@@ -1,5 +1,7 @@
using System;
+using System.Numerics;
using Autodesk.Navisworks.Api;
+using NavisworksTransport.Utils.CoordinateSystem;
namespace NavisworksTransport.Utils
{
@@ -81,11 +83,17 @@ namespace NavisworksTransport.Utils
///
public static Point3D ResolveBottomPosition(PathRoute route, Point3D referencePoint, double objectHeight)
{
- double bottomOffset = GetBottomZOffset(route, objectHeight);
- return new Point3D(
- referencePoint.X,
- referencePoint.Y,
- referencePoint.Z + bottomOffset);
+ var adapter = CoordinateSystemManager.Instance.CreateHostAdapter();
+ var canonicalReferencePoint = adapter.ToCanonicalPoint(referencePoint);
+ var canonicalUp = HostCoordinateAdapter.CanonicalUp;
+ double bottomOffset = GetBottomOffsetMagnitude(route, objectHeight);
+
+ var canonicalBottomPoint = new Point3D(
+ canonicalReferencePoint.X + canonicalUp.X * bottomOffset,
+ canonicalReferencePoint.Y + canonicalUp.Y * bottomOffset,
+ canonicalReferencePoint.Z + canonicalUp.Z * bottomOffset);
+
+ return adapter.FromCanonicalPoint(canonicalBottomPoint);
}
///
@@ -109,13 +117,17 @@ namespace NavisworksTransport.Utils
return ResolveBottomPosition(route, referencePoint, objectHeight);
}
+ var adapter = CoordinateSystemManager.Instance.CreateHostAdapter();
+ var canonicalReferencePoint = adapter.ToCanonicalPoint(referencePoint);
var normal = ResolveRailNormal(previousPoint, referencePoint, nextPoint);
double bottomOffset = GetBottomOffsetMagnitude(route, objectHeight);
- return new Point3D(
- referencePoint.X + normal.X * bottomOffset,
- referencePoint.Y + normal.Y * bottomOffset,
- referencePoint.Z + normal.Z * bottomOffset);
+ var canonicalBottomPoint = new Point3D(
+ canonicalReferencePoint.X + normal.X * bottomOffset,
+ canonicalReferencePoint.Y + normal.Y * bottomOffset,
+ canonicalReferencePoint.Z + normal.Z * bottomOffset);
+
+ return adapter.FromCanonicalPoint(canonicalBottomPoint);
}
///
@@ -137,17 +149,23 @@ namespace NavisworksTransport.Utils
if (route.RailPathDefinitionMode == RailPathDefinitionMode.LegacySuspensionPoint)
{
- return new Point3D(
- referencePoint.X,
- referencePoint.Y,
- referencePoint.Z + centerOffset);
+ var adapter = CoordinateSystemManager.Instance.CreateHostAdapter();
+ var canonicalReferencePoint = adapter.ToCanonicalPoint(referencePoint);
+ var canonicalCenterPoint = new Point3D(
+ canonicalReferencePoint.X,
+ canonicalReferencePoint.Y,
+ canonicalReferencePoint.Z + centerOffset);
+ return adapter.FromCanonicalPoint(canonicalCenterPoint);
}
+ var adapterForRail = CoordinateSystemManager.Instance.CreateHostAdapter();
+ var canonicalReferencePointForRail = adapterForRail.ToCanonicalPoint(referencePoint);
var normal = ResolveRailNormal(previousPoint, referencePoint, nextPoint);
- return new Point3D(
- referencePoint.X + normal.X * centerOffset,
- referencePoint.Y + normal.Y * centerOffset,
- referencePoint.Z + normal.Z * centerOffset);
+ var canonicalCenterPointForRail = new Point3D(
+ canonicalReferencePointForRail.X + normal.X * centerOffset,
+ canonicalReferencePointForRail.Y + normal.Y * centerOffset,
+ canonicalReferencePointForRail.Z + normal.Z * centerOffset);
+ return adapterForRail.FromCanonicalPoint(canonicalCenterPointForRail);
}
///
@@ -160,49 +178,72 @@ namespace NavisworksTransport.Utils
Point3D currentPoint,
Point3D nextPoint,
out Matrix3 linearTransform)
+ {
+ return TryCreateRailLinearTransform(
+ previousPoint,
+ currentPoint,
+ nextPoint,
+ ModelAxisConvention.CreateDefaultForHost(CoordinateSystemType.ZUp),
+ out linearTransform);
+ }
+
+ ///
+ /// 根据轨道路径切向和法向创建 Rail 构件完整三维姿态。
+ /// 通过模型局部轴约定显式指定本地哪个轴代表 forward/up。
+ ///
+ public static bool TryCreateRailLinearTransform(
+ Point3D previousPoint,
+ Point3D currentPoint,
+ Point3D nextPoint,
+ ModelAxisConvention axisConvention,
+ out Matrix3 linearTransform)
{
linearTransform = null;
- var tangent = ResolveTangent(previousPoint, currentPoint, nextPoint);
- double tangentLengthSquared = tangent.X * tangent.X + tangent.Y * tangent.Y + tangent.Z * tangent.Z;
- if (tangentLengthSquared < TangentEpsilon)
+ if (axisConvention == null)
+ {
+ throw new ArgumentNullException(nameof(axisConvention));
+ }
+
+ var adapter = CoordinateSystemManager.Instance.CreateHostAdapter();
+ var canonicalPreviousPoint = ToNumerics(adapter.ToCanonicalPoint(previousPoint));
+ var canonicalCurrentPoint = ToNumerics(adapter.ToCanonicalPoint(currentPoint));
+ var canonicalNextPoint = ToNumerics(adapter.ToCanonicalPoint(nextPoint));
+
+ if (!CanonicalRailPoseBuilder.TryCreateBasis(
+ canonicalPreviousPoint,
+ canonicalCurrentPoint,
+ canonicalNextPoint,
+ HostCoordinateAdapter.CanonicalUpVector3,
+ out var canonicalForward,
+ out var canonicalLateral,
+ out var canonicalUp))
{
return false;
}
- tangent = Normalize(tangent);
- var normal = Normalize(ResolveRailNormal(previousPoint, currentPoint, nextPoint));
- var lateral = Normalize(Cross(normal, tangent));
+ Quaternion canonicalRotation = axisConvention.CreateQuaternion(canonicalForward, canonicalUp);
+ Matrix4x4 canonicalLinear = Matrix4x4.CreateFromQuaternion(canonicalRotation);
- double lateralLengthSquared = lateral.X * lateral.X + lateral.Y * lateral.Y + lateral.Z * lateral.Z;
- if (lateralLengthSquared < TangentEpsilon)
- {
- Vector3D fallbackUp = Math.Abs(tangent.Z) < 0.9
- ? new Vector3D(0, 0, 1)
- : new Vector3D(0, 1, 0);
- lateral = Normalize(Cross(fallbackUp, tangent));
- lateralLengthSquared = lateral.X * lateral.X + lateral.Y * lateral.Y + lateral.Z * lateral.Z;
- if (lateralLengthSquared < TangentEpsilon)
- {
- return false;
- }
- }
-
- normal = Normalize(Cross(tangent, lateral));
+ var hostXAxis = Normalize(adapter.FromCanonicalVector(ToNavVector(new Vector3(canonicalLinear.M11, canonicalLinear.M21, canonicalLinear.M31))));
+ var hostYAxis = Normalize(adapter.FromCanonicalVector(ToNavVector(new Vector3(canonicalLinear.M12, canonicalLinear.M22, canonicalLinear.M32))));
+ var hostZAxis = Normalize(adapter.FromCanonicalVector(ToNavVector(new Vector3(canonicalLinear.M13, canonicalLinear.M23, canonicalLinear.M33))));
// Navisworks 在 Transform3D(Matrix3, ...) 中按列读取局部基向量:
// 第 1 列 = 本地 X 轴在世界中的方向
// 第 2 列 = 本地 Y 轴在世界中的方向
// 第 3 列 = 本地 Z 轴在世界中的方向
linearTransform = new Matrix3(
- tangent.X, lateral.X, normal.X,
- tangent.Y, lateral.Y, normal.Y,
- tangent.Z, lateral.Z, normal.Z);
+ hostXAxis.X, hostYAxis.X, hostZAxis.X,
+ hostXAxis.Y, hostYAxis.Y, hostZAxis.Y,
+ hostXAxis.Z, hostYAxis.Z, hostZAxis.Z);
LogManager.Info(
- $"[Rail姿态] 切向=({tangent.X:F4},{tangent.Y:F4},{tangent.Z:F4}), " +
- $"侧向=({lateral.X:F4},{lateral.Y:F4},{lateral.Z:F4}), " +
- $"法向=({normal.X:F4},{normal.Y:F4},{normal.Z:F4})");
+ $"[Rail姿态] Canonical切向=({canonicalForward.X:F4},{canonicalForward.Y:F4},{canonicalForward.Z:F4}), " +
+ $"Canonical侧向=({canonicalLateral.X:F4},{canonicalLateral.Y:F4},{canonicalLateral.Z:F4}), " +
+ $"Canonical法向=({canonicalUp.X:F4},{canonicalUp.Y:F4},{canonicalUp.Z:F4}), " +
+ $"Host法向=({hostZAxis.X:F4},{hostZAxis.Y:F4},{hostZAxis.Z:F4}), " +
+ $"模型Forward={axisConvention.ForwardAxis}, 模型Up={axisConvention.UpAxis}");
return true;
}
@@ -215,11 +256,26 @@ namespace NavisworksTransport.Utils
Point3D currentPoint,
Point3D nextPoint,
out Rotation3D rotation)
+ {
+ return TryCreateRailRotation(
+ previousPoint,
+ currentPoint,
+ nextPoint,
+ ModelAxisConvention.CreateDefaultForHost(CoordinateSystemType.ZUp),
+ out rotation);
+ }
+
+ public static bool TryCreateRailRotation(
+ Point3D previousPoint,
+ Point3D currentPoint,
+ Point3D nextPoint,
+ ModelAxisConvention axisConvention,
+ out Rotation3D rotation)
{
rotation = Rotation3D.Identity;
LogRotationConstructorConventionOnce();
- if (!TryCreateRailLinearTransform(previousPoint, currentPoint, nextPoint, out var linearTransform))
+ if (!TryCreateRailLinearTransform(previousPoint, currentPoint, nextPoint, axisConvention, out var linearTransform))
{
return false;
}
@@ -362,7 +418,7 @@ namespace NavisworksTransport.Utils
if (tangentLengthSquared < TangentEpsilon)
{
- return new Vector3D(0, 0, 1);
+ return HostCoordinateAdapter.CanonicalUp;
}
tangent = new Vector3D(
@@ -370,7 +426,7 @@ namespace NavisworksTransport.Utils
tangent.Y / Math.Sqrt(tangentLengthSquared),
tangent.Z / Math.Sqrt(tangentLengthSquared));
- var worldUp = new Vector3D(0, 0, 1);
+ var worldUp = HostCoordinateAdapter.CanonicalUp;
double projection = worldUp.X * tangent.X + worldUp.Y * tangent.Y + worldUp.Z * tangent.Z;
var normal = new Vector3D(
worldUp.X - projection * tangent.X,
@@ -380,7 +436,7 @@ namespace NavisworksTransport.Utils
double normalLengthSquared = normal.X * normal.X + normal.Y * normal.Y + normal.Z * normal.Z;
if (normalLengthSquared < TangentEpsilon)
{
- return new Vector3D(0, 0, 1);
+ return HostCoordinateAdapter.CanonicalUp;
}
double normalLength = Math.Sqrt(normalLengthSquared);
@@ -392,18 +448,23 @@ namespace NavisworksTransport.Utils
private static Vector3D ResolveTangent(Point3D previousPoint, Point3D currentPoint, Point3D nextPoint)
{
+ var adapter = CoordinateSystemManager.Instance.CreateHostAdapter();
+ var canonicalPreviousPoint = adapter.ToCanonicalPoint(previousPoint);
+ var canonicalCurrentPoint = adapter.ToCanonicalPoint(currentPoint);
+ var canonicalNextPoint = adapter.ToCanonicalPoint(nextPoint);
+
var tangent = new Vector3D(
- nextPoint.X - previousPoint.X,
- nextPoint.Y - previousPoint.Y,
- nextPoint.Z - previousPoint.Z);
+ canonicalNextPoint.X - canonicalPreviousPoint.X,
+ canonicalNextPoint.Y - canonicalPreviousPoint.Y,
+ canonicalNextPoint.Z - canonicalPreviousPoint.Z);
double tangentLengthSquared = tangent.X * tangent.X + tangent.Y * tangent.Y + tangent.Z * tangent.Z;
if (tangentLengthSquared < TangentEpsilon)
{
tangent = new Vector3D(
- nextPoint.X - currentPoint.X,
- nextPoint.Y - currentPoint.Y,
- nextPoint.Z - currentPoint.Z);
+ canonicalNextPoint.X - canonicalCurrentPoint.X,
+ canonicalNextPoint.Y - canonicalCurrentPoint.Y,
+ canonicalNextPoint.Z - canonicalCurrentPoint.Z);
}
return tangent;
@@ -428,5 +489,15 @@ namespace NavisworksTransport.Utils
a.Z * b.X - a.X * b.Z,
a.X * b.Y - a.Y * b.X);
}
+
+ private static Vector3 ToNumerics(Point3D point)
+ {
+ return new Vector3((float)point.X, (float)point.Y, (float)point.Z);
+ }
+
+ private static Vector3D ToNavVector(Vector3 vector)
+ {
+ return new Vector3D(vector.X, vector.Y, vector.Z);
+ }
}
}
From c53db7a6fdd59944987a3ae325fa65ebc05fe6ab Mon Sep 17 00:00:00 2001
From: tian <11429339@qq.com>
Date: Sat, 21 Mar 2026 19:19:13 +0800
Subject: [PATCH 10/87] Finalize canonical rail coordinate integration
---
...oordinate-system-canonical-space-design.md | 48 ++++
doc/guide/design_principles.md | 86 +++++++
...tem-canonical-space-implementation-plan.md | 56 +++++
doc/working/test_path_import.json | 117 ---------
.../GenerateCollisionReportCommand.cs | 30 ++-
src/Core/Animation/PathAnimationManager.cs | 229 +++++++++++++-----
.../Collision/ClashDetectiveIntegration.cs | 112 +++++----
src/Core/PathAnalysisEngine.cs | 8 +-
src/Core/PathDatabase.cs | 92 +++----
.../ViewModels/AnimationControlViewModel.cs | 103 ++++----
.../ViewModels/CollisionReportViewModel.cs | 4 +-
src/UI/WPF/ViewModels/PathEditingViewModel.cs | 11 +-
src/Utils/CollisionSceneHelper.cs | 88 +++----
.../CoordinateSystem/ModelAxisConvention.cs | 34 +++
src/Utils/ModelItemTransformHelper.cs | 74 +++++-
15 files changed, 699 insertions(+), 393 deletions(-)
delete mode 100644 doc/working/test_path_import.json
diff --git a/doc/design/2026/coordinate-system-canonical-space-design.md b/doc/design/2026/coordinate-system-canonical-space-design.md
index 4d15f77..4ab4395 100644
--- a/doc/design/2026/coordinate-system-canonical-space-design.md
+++ b/doc/design/2026/coordinate-system-canonical-space-design.md
@@ -476,6 +476,33 @@ else
- 碰撞恢复
- 辅助线/通行空间渲染
+阶段 2 的两个重要约束:
+
+1. 真实物体物理尺寸必须固定
+
+- 起点贴合、动画帧生成、通行空间尺寸、碰撞恢复
+- 必须共用同一份固定物理尺寸
+- 不允许在对象已经旋转后,再从当前世界 AABB 重新推导“真实高度”
+
+否则会产生典型错误:
+
+- 起点贴合正确
+- 动画第一帧立刻出现固定间隙
+
+2. 渲染与业务计算必须同时改
+
+- 不能只改参考点、中心点、偏移量
+- 还必须同步改渲染几何局部轴:
+ - `right`
+ - `up`
+ - `normal`
+ - `height axis`
+
+否则会出现:
+
+- 业务点位正确
+- 但通行空间/辅助杆/长方体仍按旧 `Z-up` 轴构造
+
### 阶段 3
再逐步改造:
@@ -485,6 +512,27 @@ else
- 坡度分析
- 旧二维路径辅助逻辑
+### 部署约束
+
+WPF 插件的最终部署,必须依赖完整主项目构建产物,而不应默认复用测试顺带生成的程序集。
+
+原因:
+
+- DLL 时间戳正确,不代表插件可运行
+- 如果 `TransportPlugin.g.resources` 不完整,Navisworks 在创建面板时仍会因缺少 `.baml` 崩溃
+
+必须保证:
+
+- 主项目完整构建成功
+- 关键视图资源已经编入程序集
+
+最低检查集:
+
+- `LogisticsControlPanel.baml`
+- `PathEditingView.baml`
+- `AnimationControlView.baml`
+- `LayerManagementView.baml`
+
## 9. 关键结论
本项目如果要长期稳定支持 `Y-up` 和 `Z-up` 项目,正确方向不是:
diff --git a/doc/guide/design_principles.md b/doc/guide/design_principles.md
index a88d76a..33a66bd 100644
--- a/doc/guide/design_principles.md
+++ b/doc/guide/design_principles.md
@@ -2437,4 +2437,90 @@ DialogHelper.ShowDialog(new MyDialog());
---
+## 18. 基础框架改造的开发顺序
+
+### 原则
+
+当功能涉及以下任一类基础语义时,禁止直接在业务代码里试错式修补:
+
+- 坐标系
+- 三维姿态
+- 几何局部轴
+- 单位系统
+- 动画/碰撞恢复的定位基准
+
+正确顺序必须是:
+
+1. 先抽出框架层/数学层
+2. 先写最小可验证测试
+3. 测试通过后再接回业务模块
+
+### 原因
+
+这类问题一旦直接在业务链路里反复修补,最容易出现:
+
+- 虚拟物体正确、真实物体错误
+- 起点正确、动画第一帧错误
+- 动画正确、碰撞恢复错误
+- 中心点正确、渲染几何局部轴错误
+
+根因通常不是“又差一个补偿”,而是底层语义没有先被验证。
+
+### 本项目实证经验
+
+本项目在 Rail 三维姿态与坐标系改造中,采用以下顺序后明显更稳:
+
+1. 先建立 `HostCoordinateAdapter / ModelAxisConvention / CanonicalRailPoseBuilder`
+2. 再写单元测试验证 `Y-up / Z-up`、局部 `forward/up`、四元数/线性姿态
+3. 最后再接入:
+ - 终端安装仿真
+ - Rail 姿态
+ - 动画播放
+ - 碰撞恢复
+
+结论:
+
+- 对基础语义改造,**测试先行接业务** 是推荐流程
+- 没有测试支撑时,不应在业务代码里靠补偿和 fallback 猜结果
+
+### 额外经验 1:物体物理尺寸必须固定
+
+对于真实物体动画,物体的物理尺寸一旦确定,就必须固定存储并重复使用:
+
+- 起点贴合使用的尺寸
+- 动画帧预计算使用的尺寸
+- 通行空间尺寸语义
+- 碰撞恢复使用的尺寸
+
+这些必须来自**同一份固定物理尺寸**,不能在动画过程中反复从“当前已旋转的 AABB”重新推导。
+
+否则会出现典型问题:
+
+- 起点贴合正确
+- 动画第一帧立刻拉开固定间隙
+
+### 额外经验 2:部署必须校验 WPF 资源完整性
+
+对 WPF 插件来说,仅校验 DLL 被复制成功还不够。
+
+必须确认构建产物包含完整的 `.g.resources / .baml` 资源;否则会出现:
+
+- 插件 DLL 存在
+- Navisworks 能加载程序集
+- 但一打开面板就因缺少 `*.baml` 崩溃
+
+本项目已实际遇到:
+
+- `TransportPlugin.dll` 成功部署
+- 但缺少 `LogisticsControlPanel.baml / PathEditingView.baml / AnimationControlView.baml`
+- 最终表现为“插件布局丢失”或“手工打开插件窗口即崩溃”
+
+因此经验是:
+
+- 单元测试顺带产出的 DLL 不能默认用于最终部署
+- 最终部署前应至少确认主项目完整构建成功
+- 必要时校验关键 WPF 视图资源是否真的编入程序集
+
+---
+
*本文档将随着项目发展持续更新,确保设计指导的有效性和实用性。*
diff --git a/doc/working/coordinate-system-canonical-space-implementation-plan.md b/doc/working/coordinate-system-canonical-space-implementation-plan.md
index 6e67f8c..fc7c3f9 100644
--- a/doc/working/coordinate-system-canonical-space-implementation-plan.md
+++ b/doc/working/coordinate-system-canonical-space-implementation-plan.md
@@ -240,6 +240,8 @@
验收:
- Rail 虚拟物体与真实模型在 Y-up / Z-up 项目中的姿态语义一致
+- 真实物体起点贴合和动画第一帧使用同一份固定物理尺寸语义
+- 不再允许从“已旋转后的当前 AABB”重新推导 Rail 物体高度
### Task 7.1 明确模型局部轴约定
@@ -282,6 +284,11 @@
- Rail 碰撞报告恢复位置和动画实际姿态一致
- 二维路径不被三维逻辑污染
+实施提示:
+
+- 对真实物体,碰撞恢复和动画播放必须共用同一份固定物理尺寸语义
+- 不允许恢复链路偷偷退回“重新读取当前 AABB 再算高度/底面”
+
### Task 9. 改造渲染层
文件:
@@ -308,6 +315,29 @@
- `Normal` 是否仍偷用世界 `Z`
- 不能只修改 `ApplyVerticalOffset(...)` 或中心点偏移,而保留旧的 `XY + Z` 轴构造公式
+### Task 9.1 通行空间与物体贴合语义统一
+
+涉及文件:
+
+- [AnimationControlViewModel.cs](/C:/Users/Tellme/apps/NavisworksTransport-rail-mount-modes/src/UI/WPF/ViewModels/AnimationControlViewModel.cs)
+- [PathAnimationManager.cs](/C:/Users/Tellme/apps/NavisworksTransport-rail-mount-modes/src/Core/Animation/PathAnimationManager.cs)
+
+目标:
+
+- 通行空间显示、起点贴合、动画帧贴合使用统一尺寸语义
+- Rail 轨上/轨下模式下,贴合面与留缝面保持一致
+
+经验约束:
+
+- `轨下安装`:顶面贴路径,底面留单侧间隙
+- `轨上安装`:底面贴路径,顶面留单侧间隙
+- 真实物体尺寸必须在选择物体时固定下来,并传入动画管理器
+
+验收:
+
+- 起点贴合正确后,动画第一帧不再出现固定间隙
+- 通行空间与真实物体的贴合面语义一致
+
## 5. 已知问题与注意事项
### 5.1 当前不应优先展开的模块
@@ -333,6 +363,32 @@
必须让错误显式暴露。
+### 5.3 不能使用不完整构建产物部署 WPF 插件
+
+实施过程中必须区分:
+
+- 单元测试所需的程序集产物
+- 可用于 Navisworks 运行的完整 WPF 插件产物
+
+对于本项目:
+
+- 仅有 `TransportPlugin.dll` 被复制成功,不代表插件可用
+- 还必须确保 `TransportPlugin.g.resources` 中包含完整的主视图 `.baml`
+
+最低要求:
+
+- 主项目构建成功后再部署
+- 如遇异常,应检查关键资源是否存在:
+ - `LogisticsControlPanel.baml`
+ - `PathEditingView.baml`
+ - `AnimationControlView.baml`
+ - `LayerManagementView.baml`
+
+否则可能出现:
+
+- 插件布局恢复失败
+- 手工打开插件窗口即崩溃
+
## 6. 当前实施优先顺序
建议实际推进顺序如下:
diff --git a/doc/working/test_path_import.json b/doc/working/test_path_import.json
deleted file mode 100644
index 63dfbc8..0000000
--- a/doc/working/test_path_import.json
+++ /dev/null
@@ -1,117 +0,0 @@
-{
- "PathPlanningData": {
- "version": "1.0",
- "generator": "NavisworksTransport",
- "timestamp": "2025-11-07T15:30:00",
- "ProjectInfo": {
- "name": "测试物流路径",
- "description": "用于测试JSON导入导出功能",
- "units": "meters",
- "coordinateSystem": "Global"
- },
- "Routes": [
- {
- "id": "route001",
- "name": "测试路径1",
- "description": "从入口到仓库的路径",
- "totalLength": 45.6,
- "objectLimits": {
- "maxLength": 2.0,
- "maxWidth": 1.5,
- "maxHeight": 2.0,
- "safetyMargin": 0.25
- },
- "gridSize": 0.5,
- "created": "2025-11-07T10:00:00",
- "points": [
- {
- "id": "point001",
- "name": "起点",
- "type": "StartPoint",
- "index": 0,
- "x": 0.0,
- "y": 0.0,
- "z": 0.0,
- "created": "2025-11-07T10:00:00"
- },
- {
- "id": "point002",
- "name": "路径点1",
- "type": "WayPoint",
- "index": 1,
- "x": 10.0,
- "y": 0.0,
- "z": 0.0,
- "created": "2025-11-07T10:00:01"
- },
- {
- "id": "point003",
- "name": "路径点2",
- "type": "WayPoint",
- "index": 2,
- "x": 20.0,
- "y": 5.0,
- "z": 0.0,
- "created": "2025-11-07T10:00:02"
- },
- {
- "id": "point004",
- "name": "终点",
- "type": "EndPoint",
- "index": 3,
- "x": 30.0,
- "y": 10.0,
- "z": 0.0,
- "created": "2025-11-07T10:00:03"
- }
- ]
- },
- {
- "id": "route002",
- "name": "测试路径2",
- "description": "从仓库到出口的路径",
- "totalLength": 32.4,
- "objectLimits": {
- "maxLength": 2.0,
- "maxWidth": 1.5,
- "maxHeight": 2.0,
- "safetyMargin": 0.25
- },
- "gridSize": 0.5,
- "created": "2025-11-07T10:05:00",
- "points": [
- {
- "id": "point005",
- "name": "起点",
- "type": "StartPoint",
- "index": 0,
- "x": 30.0,
- "y": 10.0,
- "z": 0.0,
- "created": "2025-11-07T10:05:00"
- },
- {
- "id": "point006",
- "name": "路径点1",
- "type": "WayPoint",
- "index": 1,
- "x": 25.0,
- "y": 8.0,
- "z": 0.0,
- "created": "2025-11-07T10:05:01"
- },
- {
- "id": "point007",
- "name": "终点",
- "type": "EndPoint",
- "index": 2,
- "x": 20.0,
- "y": 5.0,
- "z": 0.0,
- "created": "2025-11-07T10:05:02"
- }
- ]
- }
- ]
- }
-}
diff --git a/src/Commands/GenerateCollisionReportCommand.cs b/src/Commands/GenerateCollisionReportCommand.cs
index 8614b98..9f39b72 100644
--- a/src/Commands/GenerateCollisionReportCommand.cs
+++ b/src/Commands/GenerateCollisionReportCommand.cs
@@ -9,6 +9,7 @@ using Autodesk.Navisworks.Api.Clash;
using NavisworksTransport.Core;
using NavisworksTransport.Core.Animation;
using NavisworksTransport.Utils;
+using NavisworksTransport.Utils.CoordinateSystem;
using NavisworksTransport.UI.WPF.Views;
namespace NavisworksTransport.Commands
@@ -367,11 +368,11 @@ namespace NavisworksTransport.Commands
var virtualObject = VirtualObjectManager.Instance.CurrentVirtualObject;
if (virtualObject != null)
{
- var bounds = virtualObject.BoundingBox();
var pam = PathAnimationManager.GetInstance();
+ var position = GetAnimatedObjectReportedPosition(virtualObject, pam);
LogManager.Info(string.Format("[默认截图前] 虚拟物体位置: ({0:F2},{1:F2},{2:F2}), PAM记录朝向: {3:F2}°, customRotation={4}",
- bounds.Center.X, bounds.Center.Y, bounds.Min.Z,
- pam.CurrentYaw * 180 / Math.PI, pam.HasCurrentRotation));
+ position.X, position.Y, position.Z,
+ pam.CurrentYaw * 180 / Math.PI, pam.HasTrackedRotation));
}
else
{
@@ -397,11 +398,11 @@ namespace NavisworksTransport.Commands
virtualObject = VirtualObjectManager.Instance.CurrentVirtualObject;
if (virtualObject != null)
{
- var bounds = virtualObject.BoundingBox();
var pam = PathAnimationManager.GetInstance();
+ var position = GetAnimatedObjectReportedPosition(virtualObject, pam);
LogManager.Info(string.Format("[默认截图后] 虚拟物体位置: ({0:F2},{1:F2},{2:F2}), PAM记录朝向: {3:F2}°, customRotation={4}",
- bounds.Center.X, bounds.Center.Y, bounds.Min.Z,
- pam.CurrentYaw * 180 / Math.PI, pam.HasCurrentRotation));
+ position.X, position.Y, position.Z,
+ pam.CurrentYaw * 180 / Math.PI, pam.HasTrackedRotation));
}
if (screenshotPath != null)
@@ -609,6 +610,23 @@ namespace NavisworksTransport.Commands
return PathPlanningResult.Success(result, message);
}
+ private static Point3D GetAnimatedObjectReportedPosition(ModelItem animatedObject, PathAnimationManager pam)
+ {
+ if (animatedObject == null)
+ {
+ return new Point3D(0, 0, 0);
+ }
+
+ if (pam != null && pam.ControlsAnimatedObject(animatedObject))
+ {
+ return pam.GetObjectCurrentPosition(animatedObject).Position;
+ }
+
+ var bounds = animatedObject.BoundingBox();
+ var adapter = CoordinateSystemManager.Instance.CreateHostAdapter();
+ return ModelItemTransformHelper.GetHostBottomAnchorPoint(bounds, adapter);
+ }
+
///
diff --git a/src/Core/Animation/PathAnimationManager.cs b/src/Core/Animation/PathAnimationManager.cs
index 5f2a693..e25d6c8 100644
--- a/src/Core/Animation/PathAnimationManager.cs
+++ b/src/Core/Animation/PathAnimationManager.cs
@@ -96,6 +96,9 @@ namespace NavisworksTransport.Core.Animation
private double _virtualObjectLength = 0; // 虚拟物体长度(模型单位)
private double _virtualObjectWidth = 0; // 虚拟物体宽度(模型单位)
private double _virtualObjectHeight = 0; // 虚拟物体高度(模型单位)
+ private double _realObjectLength = 0; // 真实物体长度(模型单位,固定物理尺寸)
+ private double _realObjectWidth = 0; // 真实物体宽度(模型单位,固定物理尺寸)
+ private double _realObjectHeight = 0; // 真实物体高度(模型单位,固定物理尺寸)
private List _pathPoints;
private List _manualCollisionTargets = new List();
private bool _manualCollisionOverrideEnabled = false;
@@ -157,12 +160,12 @@ namespace NavisworksTransport.Core.Animation
private Transform3D _originalTransform;
private Point3D _originalCenter; // 存储部件的原始中心位置
- private Point3D _currentPosition; // 存储部件的当前位置
+ private Point3D _trackedPosition; // 动画管理器内部跟踪的位置状态,不等于宿主即时读回位置
private AnimationState _currentState = AnimationState.Idle;
private double _pausedProgress = 0.0; // 暂停时的进度(0-1之间)
private double _currentYaw = 0.0; // 当前偏航角(弧度)
- private Rotation3D _currentRotation = Rotation3D.Identity; // 当前完整姿态
- private bool _hasCurrentRotation = false; // 当前是否使用完整姿态
+ private Rotation3D _trackedRotation = Rotation3D.Identity; // 动画管理器内部跟踪的完整姿态
+ private bool _hasTrackedRotation = false; // 当前是否使用完整姿态
// TimeLiner 集成
private TimeLinerIntegrationManager _timeLinerManager;
@@ -422,11 +425,11 @@ namespace NavisworksTransport.Core.Animation
// 2. 重置内部跟踪变量(同步到原始状态)
// 注意:ResetPermanentTransform 后物体的 Transform 属性会自动变回原始值
_currentYaw = ModelItemTransformHelper.GetYawFromTransform(objectToRestore.Transform);
- _currentRotation = objectToRestore.Transform.Factor().Rotation;
- _hasCurrentRotation = true;
+ _trackedRotation = objectToRestore.Transform.Factor().Rotation;
+ _hasTrackedRotation = true;
var originalBoundingBox = objectToRestore.BoundingBox();
- _currentPosition = GetTrackedObjectPosition(objectToRestore, _route?.PathType == PathType.Rail && !isVirtual);
+ _trackedPosition = GetTrackedObjectPosition(objectToRestore, _route?.PathType == PathType.Rail && !isVirtual);
string objectName = isVirtual ? "虚拟物体" : objectToRestore.DisplayName;
LogManager.Info($"[归位] {objectName} 已彻底恢复到原始位置, yaw={_currentYaw:F3}");
@@ -469,6 +472,18 @@ namespace NavisworksTransport.Core.Animation
_virtualObjectHeight = height; // 模型单位
}
+ ///
+ /// 设置真实物体的固定物理尺寸(模型单位)。
+ /// 一旦动画开始生成,后续 Rail 帧和起点贴合应使用同一份尺寸语义,
+ /// 不能再从已经旋转后的当前 AABB 重新推导高度。
+ ///
+ public void SetRealObjectDimensions(double length, double width, double height)
+ {
+ _realObjectLength = length;
+ _realObjectWidth = width;
+ _realObjectHeight = height;
+ }
+
///
/// 设置动画参数(批处理专用)
///
@@ -589,12 +604,12 @@ namespace NavisworksTransport.Core.Animation
_animatedObject = animatedObject;
_originalTransform = animatedObject.Transform;
_originalCenter = animatedObject.BoundingBox().Center;
- _currentPosition = GetTrackedObjectPosition(animatedObject, _route?.PathType == PathType.Rail && !_isVirtualObject);
+ _trackedPosition = GetTrackedObjectPosition(animatedObject, _route?.PathType == PathType.Rail && !_isVirtualObject);
// 统一逻辑:从当前 Transform 中提取实际朝向(无论虚拟物体还是普通物体)
_currentYaw = ModelItemTransformHelper.GetYawFromTransform(_originalTransform);
- _currentRotation = _originalTransform.Factor().Rotation;
- _hasCurrentRotation = true;
+ _trackedRotation = _originalTransform.Factor().Rotation;
+ _hasTrackedRotation = true;
}
if (pathPoints != null)
@@ -835,12 +850,18 @@ namespace NavisworksTransport.Core.Animation
}
// 3. 生成每一帧
- double distancePerFrameInModelUnits = totalLengthInModelUnits / totalFrames;
+ double distancePerFrameInModelUnits = totalFrames > 1
+ ? totalLengthInModelUnits / (totalFrames - 1)
+ : 0.0;
LogManager.Info($"总帧数: {totalFrames}, 总长度: {totalLengthInModelUnits / metersToModelUnits:F2}米, 每帧移动距离: {distancePerFrameInModelUnits / metersToModelUnits:F4}米");
for (int i = 0; i < totalFrames; i++)
{
- double targetDistance = i * distancePerFrameInModelUnits; // 按距离采样(模型单位),不是按进度
+ double targetDistance = i * distancePerFrameInModelUnits; // 按距离采样(模型单位),最后一帧应精确到终点
+ if (targetDistance > totalLengthInModelUnits)
+ {
+ targetDistance = totalLengthInModelUnits;
+ }
Point3D framePosition;
Point3D previousFramePoint = Point3D.Origin;
@@ -1091,10 +1112,10 @@ namespace NavisworksTransport.Core.Animation
(virtualBoundingBox.Min.Y + virtualBoundingBox.Max.Y + colliderBox.Min.Y + colliderBox.Max.Y) / 4,
(virtualBoundingBox.Min.Z + virtualBoundingBox.Max.Z + colliderBox.Min.Z + colliderBox.Max.Z) / 4
),
- // 碰撞结果中的 Item1Position 必须与后续恢复链路的基准点语义一致:
+ // 碰撞结果中的 AnimatedObjectTrackedPosition 必须与后续恢复链路的动画跟踪点语义一致:
// 1. 二维 yaw 路径沿用成熟旧逻辑,记录包围盒中心;
// 2. 三维 customRotation 路径记录动画跟踪点(framePosition)。
- Item1Position = frame.HasCustomRotation
+ AnimatedObjectTrackedPosition = frame.HasCustomRotation
? new Point3D(
framePosition.X,
framePosition.Y,
@@ -1104,9 +1125,9 @@ namespace NavisworksTransport.Core.Animation
framePosition.Y,
framePosition.Z + (virtualBoundingBox.Max.Z - virtualBoundingBox.Min.Z) / 2),
Item2Position = GetObjectPosition(collider),
- Item1YawRadians = actualYawRadians, // 记录运动物体实际播放朝向
- Item1Rotation = frame.HasCustomRotation ? frame.Rotation : Rotation3D.Identity,
- Item1HasCustomRotation = frame.HasCustomRotation,
+ AnimatedObjectTrackedYawRadians = actualYawRadians, // 记录动画跟踪点对应的运动物体实际播放朝向
+ AnimatedObjectTrackedRotation = frame.HasCustomRotation ? frame.Rotation : Rotation3D.Identity,
+ AnimatedObjectHasTrackedRotation = frame.HasCustomRotation,
HasPositionInfo = true
};
@@ -1116,7 +1137,7 @@ namespace NavisworksTransport.Core.Animation
$"原始Yaw={yawRadians * 180.0 / Math.PI:F2}°, " +
$"实际Yaw={actualYawRadians * 180.0 / Math.PI:F2}°, " +
$"customRotation={frame.HasCustomRotation}, " +
- $"保存位置=({collisionResult.Item1Position.X:F3}, {collisionResult.Item1Position.Y:F3}, {collisionResult.Item1Position.Z:F3})");
+ $"保存位置=({collisionResult.AnimatedObjectTrackedPosition.X:F3}, {collisionResult.AnimatedObjectTrackedPosition.Y:F3}, {collisionResult.AnimatedObjectTrackedPosition.Z:F3})");
frame.Collisions.Add(collisionResult);
_allCollisionResults.Add(collisionResult);
@@ -1130,7 +1151,7 @@ namespace NavisworksTransport.Core.Animation
var framesWithCollision = _animationFrames.Count(f => f.HasCollision);
var totalCollisions = _animationFrames.Sum(f => f.Collisions.Count);
// 检查碰撞结果是否包含位置信息
- var collisionsWithPosition = _allCollisionResults.Count(c => c.HasPositionInfo && c.Item1Position != null);
+ var collisionsWithPosition = _allCollisionResults.Count(c => c.HasPositionInfo && c.AnimatedObjectTrackedPosition != null);
LogManager.Info($"=== 预计算完成 ===");
LogManager.Info($"总帧数: {_animationFrames.Count}");
@@ -1363,12 +1384,12 @@ namespace NavisworksTransport.Core.Animation
if (firstFrame.HasCustomRotation && _route?.PathType == PathType.Rail)
{
- var dx = _currentPosition.X - firstFrame.Position.X;
- var dy = _currentPosition.Y - firstFrame.Position.Y;
- var dz = _currentPosition.Z - firstFrame.Position.Z;
+ var dx = _trackedPosition.X - firstFrame.Position.X;
+ var dy = _trackedPosition.Y - firstFrame.Position.Y;
+ var dz = _trackedPosition.Z - firstFrame.Position.Z;
distanceToStart = Math.Sqrt(dx * dx + dy * dy + dz * dz);
- var currentLinear = new Transform3D(_currentRotation).Linear;
+ var currentLinear = new Transform3D(_trackedRotation).Linear;
var targetLinear = new Transform3D(firstFrame.Rotation).Linear;
double rotationDiff =
Math.Abs(currentLinear.Get(0, 0) - targetLinear.Get(0, 0)) +
@@ -1701,6 +1722,7 @@ namespace NavisworksTransport.Core.Animation
// 动画自然结束时保持物体在最终位置(不移回起点)
LogManager.Info("动画播放自然结束,物体保持在最终位置");
+ LogRailEndAlignmentDiagnostics();
// 直接设置为完成状态,避免中间状态切换
SetState(AnimationState.Finished);
@@ -1774,6 +1796,85 @@ namespace NavisworksTransport.Core.Animation
}
}
+ ///
+ /// 输出 Rail 路径动画终点对齐诊断。
+ /// 只做日志,不改变业务行为,用于确认:
+ /// 1. 路径终点锚点
+ /// 2. 最后一帧动画跟踪点
+ /// 3. 动画结束时的实际跟踪点
+ /// 4. 沿路径前进方向的终点偏差
+ ///
+ private void LogRailEndAlignmentDiagnostics()
+ {
+ try
+ {
+ if (_route == null || _route.PathType != PathType.Rail || _pathPoints == null || _pathPoints.Count < 2 || _animationFrames == null || _animationFrames.Count == 0)
+ {
+ return;
+ }
+
+ Point3D terminalAnchorPoint = _pathPoints[_pathPoints.Count - 1];
+ Point3D previousAnchorPoint = _pathPoints[_pathPoints.Count - 2];
+ var lastFrame = _animationFrames[_animationFrames.Count - 1];
+ Point3D finalTrackedPoint = _trackedPosition;
+
+ double objectHeight = GetAnimatedObjectHeight();
+ Point3D expectedTrackedEndPoint = RailPathPoseHelper.ResolveBottomPosition(
+ _route,
+ terminalAnchorPoint,
+ previousAnchorPoint,
+ terminalAnchorPoint,
+ objectHeight);
+
+ Vector3D forward = new Vector3D(
+ terminalAnchorPoint.X - previousAnchorPoint.X,
+ terminalAnchorPoint.Y - previousAnchorPoint.Y,
+ terminalAnchorPoint.Z - previousAnchorPoint.Z);
+
+ double forwardLength = Math.Sqrt(forward.X * forward.X + forward.Y * forward.Y + forward.Z * forward.Z);
+ if (forwardLength < 1e-9)
+ {
+ return;
+ }
+
+ Vector3D normalizedForward = new Vector3D(
+ forward.X / forwardLength,
+ forward.Y / forwardLength,
+ forward.Z / forwardLength);
+
+ Vector3D frameDelta = new Vector3D(
+ lastFrame.Position.X - expectedTrackedEndPoint.X,
+ lastFrame.Position.Y - expectedTrackedEndPoint.Y,
+ lastFrame.Position.Z - expectedTrackedEndPoint.Z);
+ Vector3D finalDelta = new Vector3D(
+ finalTrackedPoint.X - expectedTrackedEndPoint.X,
+ finalTrackedPoint.Y - expectedTrackedEndPoint.Y,
+ finalTrackedPoint.Z - expectedTrackedEndPoint.Z);
+
+ double frameForwardDelta = Dot(frameDelta, normalizedForward);
+ double finalForwardDelta = Dot(finalDelta, normalizedForward);
+
+ LogManager.Info(
+ $"[Rail终点诊断] 终点锚点=({terminalAnchorPoint.X:F3},{terminalAnchorPoint.Y:F3},{terminalAnchorPoint.Z:F3}), " +
+ $"期望跟踪点=({expectedTrackedEndPoint.X:F3},{expectedTrackedEndPoint.Y:F3},{expectedTrackedEndPoint.Z:F3}), " +
+ $"最后一帧跟踪点=({lastFrame.Position.X:F3},{lastFrame.Position.Y:F3},{lastFrame.Position.Z:F3}), " +
+ $"动画结束跟踪点=({finalTrackedPoint.X:F3},{finalTrackedPoint.Y:F3},{finalTrackedPoint.Z:F3})");
+ LogManager.Info(
+ $"[Rail终点诊断] 前进方向=({normalizedForward.X:F4},{normalizedForward.Y:F4},{normalizedForward.Z:F4}), " +
+ $"最后一帧偏差=({frameDelta.X:F3},{frameDelta.Y:F3},{frameDelta.Z:F3}), 沿前进轴={frameForwardDelta:F3}, " +
+ $"结束偏差=({finalDelta.X:F3},{finalDelta.Y:F3},{finalDelta.Z:F3}), 沿前进轴={finalForwardDelta:F3}");
+ }
+ catch (Exception ex)
+ {
+ LogManager.Warning($"[Rail终点诊断] 输出失败: {ex.Message}");
+ }
+ }
+
+ private static double Dot(Vector3D a, Vector3D b)
+ {
+ return a.X * b.X + a.Y * b.Y + a.Z * b.Z;
+ }
+
///
/// 暂停动画
///
@@ -2147,9 +2248,9 @@ namespace NavisworksTransport.Core.Animation
// 计算平移和旋转的增量
var deltaPos = new Vector3D(
- newPosition.X - _currentPosition.X,
- newPosition.Y - _currentPosition.Y,
- newPosition.Z - _currentPosition.Z
+ newPosition.X - _trackedPosition.X,
+ newPosition.Y - _trackedPosition.Y,
+ newPosition.Z - _trackedPosition.Z
);
Transform3D incrementalTransform;
@@ -2164,18 +2265,18 @@ namespace NavisworksTransport.Core.Animation
//LogManager.Debug($"[UpdateObjectPosition] 当前yaw={_currentYaw * 180 / Math.PI:F2}度, 目标yaw={newYaw * 180 / Math.PI:F2}度, 旋转增量deltaYaw={deltaYaw * 180 / Math.PI:F2}度");
// 计算绕当前位置旋转的等效变换:
- // 1. 如果绕原点旋转deltaYaw,当前位置_currentPosition会移动到哪里?
+ // 1. 如果绕原点旋转deltaYaw,当前位置_trackedPosition会移动到哪里?
double cos = Math.Cos(deltaYaw);
double sin = Math.Sin(deltaYaw);
- double rotatedX = _currentPosition.X * cos - _currentPosition.Y * sin;
- double rotatedY = _currentPosition.X * sin + _currentPosition.Y * cos;
+ double rotatedX = _trackedPosition.X * cos - _trackedPosition.Y * sin;
+ double rotatedY = _trackedPosition.X * sin + _trackedPosition.Y * cos;
// 2. 但我们希望物体绕自己旋转,位置移动到newPosition
// 所以需要的平移 = newPosition - (旋转后的位置)
var compensatedTranslation = new Vector3D(
newPosition.X - rotatedX,
newPosition.Y - rotatedY,
- newPosition.Z - _currentPosition.Z // Z保持deltaPos
+ newPosition.Z - _trackedPosition.Z // Z保持deltaPos
);
// 3. 组合:先旋转(绕原点),再平移(补偿+目标位置)
@@ -2187,8 +2288,8 @@ namespace NavisworksTransport.Core.Animation
incrementalTransform = components.Combine();
_currentYaw = newYaw;
- _currentRotation = Rotation3D.Identity;
- _hasCurrentRotation = false;
+ _trackedRotation = Rotation3D.Identity;
+ _hasTrackedRotation = false;
}
else
{
@@ -2200,8 +2301,8 @@ namespace NavisworksTransport.Core.Animation
// 应用增量变换(false = 增量模式)
doc.Models.OverridePermanentTransform(modelItems, incrementalTransform, false);
- // 更新当前位置
- _currentPosition = newPosition;
+ // 更新跟踪位置
+ _trackedPosition = newPosition;
}
catch (Exception ex)
{
@@ -2223,16 +2324,28 @@ namespace NavisworksTransport.Core.Animation
}
else if (_animatedObject != null)
{
- var currentRotation = _hasCurrentRotation
- ? _currentRotation
+ Point3D currentPositionForTransform = _trackedPosition;
+ var currentRotation = _hasTrackedRotation
+ ? _trackedRotation
: _animatedObject.Transform.Factor().Rotation;
+ try
+ {
+ var actualHostPosition = GetTrackedObjectPosition(_animatedObject, _route?.PathType == PathType.Rail && !_isVirtualObject);
+ currentPositionForTransform = actualHostPosition;
+ LogManager.Info(
+ $"[动画姿态入口] {_animatedObject.DisplayName} 宿主即时读回点=({actualHostPosition.X:F3},{actualHostPosition.Y:F3},{actualHostPosition.Z:F3})");
+ }
+ catch (Exception ex)
+ {
+ LogManager.Warning($"[动画姿态入口] 读取宿主实际状态失败: {ex.Message}");
+ }
var currentLinear = new Transform3D(currentRotation).Linear;
var targetLinear = new Transform3D(newRotation).Linear;
LogManager.Info(
- $"[动画姿态入口] {_animatedObject.DisplayName} 当前点=({_currentPosition.X:F3},{_currentPosition.Y:F3},{_currentPosition.Z:F3}), " +
+ $"[动画姿态入口] {_animatedObject.DisplayName} 跟踪点=({_trackedPosition.X:F3},{_trackedPosition.Y:F3},{_trackedPosition.Z:F3}), " +
$"目标点=({newPosition.X:F3},{newPosition.Y:F3},{newPosition.Z:F3})");
LogManager.Info(
- $"[动画姿态入口] {_animatedObject.DisplayName} 当前姿态: " +
+ $"[动画姿态入口] {_animatedObject.DisplayName} 跟踪姿态: " +
$"X=({currentLinear.Get(0, 0):F4},{currentLinear.Get(1, 0):F4},{currentLinear.Get(2, 0):F4}), " +
$"Y=({currentLinear.Get(0, 1):F4},{currentLinear.Get(1, 1):F4},{currentLinear.Get(2, 1):F4}), " +
$"Z=({currentLinear.Get(0, 2):F4},{currentLinear.Get(1, 2):F4},{currentLinear.Get(2, 2):F4})");
@@ -2243,7 +2356,7 @@ namespace NavisworksTransport.Core.Animation
$"Z=({targetLinear.Get(0, 2):F4},{targetLinear.Get(1, 2):F4},{targetLinear.Get(2, 2):F4})");
ModelItemTransformHelper.MoveItemIncrementallyToPositionAndRotation(
_animatedObject,
- _currentPosition,
+ currentPositionForTransform,
currentRotation,
newPosition,
newRotation);
@@ -2253,9 +2366,9 @@ namespace NavisworksTransport.Core.Animation
return;
}
- _currentPosition = newPosition;
- _currentRotation = newRotation;
- _hasCurrentRotation = true;
+ _trackedPosition = newPosition;
+ _trackedRotation = newRotation;
+ _hasTrackedRotation = true;
_currentYaw = ModelItemTransformHelper.GetYawFromRotation(newRotation);
}
catch (Exception ex)
@@ -2353,7 +2466,7 @@ namespace NavisworksTransport.Core.Animation
return (null, 0);
}
- var position = _currentPosition;
+ var position = _trackedPosition;
// 🔥 关键:使用 _currentYaw(实际当前朝向),而不是 Transform 的 CAD 原始朝向
// Transform 返回的是 CAD 设计时的原始朝向,不是动画后的实际朝向
return (position, _currentYaw);
@@ -2375,8 +2488,8 @@ namespace NavisworksTransport.Core.Animation
var (position, yaw) = GetObjectCurrentPosition(obj);
_savedObjectPosition = position;
_savedObjectYaw = yaw;
- _savedObjectRotation = _currentRotation;
- _savedObjectHasCustomRotation = _hasCurrentRotation;
+ _savedObjectRotation = _trackedRotation;
+ _savedObjectHasCustomRotation = _hasTrackedRotation;
_hasSavedObjectState = true;
LogManager.Info($"已保存物体状态: pos=({position.X:F2},{position.Y:F2},{position.Z:F2}), yaw={yaw * 180 / Math.PI:F2}度, customRotation={_savedObjectHasCustomRotation}");
}
@@ -2409,10 +2522,10 @@ namespace NavisworksTransport.Core.Animation
var originalAnimatedObject = _animatedObject;
_animatedObject = obj;
- _currentPosition = _savedObjectPosition;
+ _trackedPosition = _savedObjectPosition;
_currentYaw = _savedObjectYaw;
- _currentRotation = _savedObjectRotation;
- _hasCurrentRotation = _savedObjectHasCustomRotation;
+ _trackedRotation = _savedObjectRotation;
+ _hasTrackedRotation = _savedObjectHasCustomRotation;
if (_savedObjectHasCustomRotation)
{
@@ -2753,7 +2866,7 @@ namespace NavisworksTransport.Core.Animation
// 清空对象引用
_animatedObject = null;
_pathPoints?.Clear();
- _currentPosition = new Point3D(0, 0, 0);
+ _trackedPosition = new Point3D(0, 0, 0);
_originalCenter = new Point3D(0, 0, 0);
LogManager.Info("[PathAnimationManager] 对象引用已清理");
@@ -2955,8 +3068,8 @@ namespace NavisworksTransport.Core.Animation
/// 用于保存物体当前状态(因为Transform返回的是CAD原始值)
///
public double CurrentYaw => _currentYaw;
- public Rotation3D CurrentRotation => _currentRotation;
- public bool HasCurrentRotation => _hasCurrentRotation;
+ public Rotation3D TrackedRotation => _trackedRotation;
+ public bool HasTrackedRotation => _hasTrackedRotation;
///
/// 获取当前动画物体高度(模型单位)
@@ -2968,6 +3081,11 @@ namespace NavisworksTransport.Core.Animation
return _virtualObjectHeight;
}
+ if (_realObjectHeight > 0.0)
+ {
+ return _realObjectHeight;
+ }
+
if (_animatedObject == null)
{
throw new InvalidOperationException("动画对象为空,无法获取物体高度");
@@ -3062,7 +3180,7 @@ namespace NavisworksTransport.Core.Animation
{
_originalTransform = animatedObject.Transform;
_originalCenter = animatedObject.BoundingBox().Center;
- _currentPosition = GetTrackedObjectPosition(animatedObject, _route?.PathType == PathType.Rail && !_isVirtualObject);
+ _trackedPosition = GetTrackedObjectPosition(animatedObject, _route?.PathType == PathType.Rail && !_isVirtualObject);
// 保持当前的 _currentYaw(因为物体可能已经被 MoveObjectToPathStart 旋转)
// 不要从 Transform 中提取,因为 Transform 返回的是原始值,不是当前值
@@ -3087,13 +3205,8 @@ namespace NavisworksTransport.Core.Animation
}
var bounds = item.BoundingBox();
- if (useStableRailAnchor)
- {
- var adapter = CoordinateSystemManager.Instance.CreateHostAdapter();
- return ModelItemTransformHelper.GetHostBottomAnchorPoint(bounds, adapter);
- }
-
- return new Point3D(bounds.Center.X, bounds.Center.Y, bounds.Min.Z);
+ var adapter = CoordinateSystemManager.Instance.CreateHostAdapter();
+ return ModelItemTransformHelper.GetHostBottomAnchorPoint(bounds, adapter);
}
///
diff --git a/src/Core/Collision/ClashDetectiveIntegration.cs b/src/Core/Collision/ClashDetectiveIntegration.cs
index 68ea04d..1e52bdb 100644
--- a/src/Core/Collision/ClashDetectiveIntegration.cs
+++ b/src/Core/Collision/ClashDetectiveIntegration.cs
@@ -7,6 +7,7 @@ using Autodesk.Navisworks.Api.Clash;
using NavisworksTransport.Core;
using NavisworksTransport.Core.Animation;
using NavisworksTransport.Utils;
+using NavisworksTransport.Utils.CoordinateSystem;
namespace NavisworksTransport
{
@@ -174,11 +175,11 @@ namespace NavisworksTransport
///
private class CollisionPositionInfo
{
- public Point3D Item1Position { get; set; }
+ public Point3D AnimatedObjectTrackedPosition { get; set; }
public Point3D Item2Position { get; set; }
- public double Item1YawRadians { get; set; }
- public Rotation3D Item1Rotation { get; set; }
- public bool Item1HasCustomRotation { get; set; }
+ public double AnimatedObjectTrackedYawRadians { get; set; }
+ public Rotation3D AnimatedObjectTrackedRotation { get; set; }
+ public bool AnimatedObjectHasTrackedRotation { get; set; }
public bool HasPositionInfo { get; set; }
}
@@ -271,20 +272,20 @@ namespace NavisworksTransport
};
// 保存碰撞时运动物体的位置和朝向(如果可用)
- if (collision.HasPositionInfo && collision.Item1Position != null)
+ if (collision.HasPositionInfo && collision.AnimatedObjectTrackedPosition != null)
{
- collisionRecord.Item1PosX = collision.Item1Position.X;
- collisionRecord.Item1PosY = collision.Item1Position.Y;
- collisionRecord.Item1PosZ = collision.Item1Position.Z;
- collisionRecord.Item1YawRadians = collision.Item1YawRadians;
- collisionRecord.Item1RotA = collision.Item1Rotation?.A;
- collisionRecord.Item1RotB = collision.Item1Rotation?.B;
- collisionRecord.Item1RotC = collision.Item1Rotation?.C;
- collisionRecord.Item1RotD = collision.Item1Rotation?.D;
- collisionRecord.Item1HasCustomRotation = collision.Item1HasCustomRotation;
+ collisionRecord.AnimatedObjectTrackedPosX = collision.AnimatedObjectTrackedPosition.X;
+ collisionRecord.AnimatedObjectTrackedPosY = collision.AnimatedObjectTrackedPosition.Y;
+ collisionRecord.AnimatedObjectTrackedPosZ = collision.AnimatedObjectTrackedPosition.Z;
+ collisionRecord.AnimatedObjectTrackedYawRadians = collision.AnimatedObjectTrackedYawRadians;
+ collisionRecord.AnimatedObjectTrackedRotA = collision.AnimatedObjectTrackedRotation?.A;
+ collisionRecord.AnimatedObjectTrackedRotB = collision.AnimatedObjectTrackedRotation?.B;
+ collisionRecord.AnimatedObjectTrackedRotC = collision.AnimatedObjectTrackedRotation?.C;
+ collisionRecord.AnimatedObjectTrackedRotD = collision.AnimatedObjectTrackedRotation?.D;
+ collisionRecord.AnimatedObjectHasTrackedRotation = collision.AnimatedObjectHasTrackedRotation;
collisionRecord.HasPositionInfo = true;
-
- LogManager.Debug($"[保存碰撞对象] 记录运动物体位置: ({collisionRecord.Item1PosX:F2}, {collisionRecord.Item1PosY:F2}, {collisionRecord.Item1PosZ:F2}), yaw={collisionRecord.Item1YawRadians:F2} rad, customRotation={collisionRecord.Item1HasCustomRotation}");
+
+ LogManager.Debug($"[保存碰撞对象] 记录运动物体位置: ({collisionRecord.AnimatedObjectTrackedPosX:F2}, {collisionRecord.AnimatedObjectTrackedPosY:F2}, {collisionRecord.AnimatedObjectTrackedPosZ:F2}), yaw={collisionRecord.AnimatedObjectTrackedYawRadians:F2} rad, customRotation={collisionRecord.AnimatedObjectHasTrackedRotation}");
}
collisionObjects.Add(collisionRecord);
@@ -495,26 +496,26 @@ namespace NavisworksTransport
};
// 恢复碰撞时运动物体的位置和朝向(如果可用)
- if (obj.HasPositionInfo && obj.Item1PosX.HasValue && obj.Item1PosY.HasValue && obj.Item1PosZ.HasValue)
+ if (obj.HasPositionInfo && obj.AnimatedObjectTrackedPosX.HasValue && obj.AnimatedObjectTrackedPosY.HasValue && obj.AnimatedObjectTrackedPosZ.HasValue)
{
- collisionResult.Item1Position = new Point3D(obj.Item1PosX.Value, obj.Item1PosY.Value, obj.Item1PosZ.Value);
- collisionResult.Item1YawRadians = obj.Item1YawRadians ?? 0.0;
- collisionResult.Item1HasCustomRotation = obj.Item1HasCustomRotation;
- if (obj.Item1HasCustomRotation &&
- obj.Item1RotA.HasValue &&
- obj.Item1RotB.HasValue &&
- obj.Item1RotC.HasValue &&
- obj.Item1RotD.HasValue)
+ collisionResult.AnimatedObjectTrackedPosition = new Point3D(obj.AnimatedObjectTrackedPosX.Value, obj.AnimatedObjectTrackedPosY.Value, obj.AnimatedObjectTrackedPosZ.Value);
+ collisionResult.AnimatedObjectTrackedYawRadians = obj.AnimatedObjectTrackedYawRadians ?? 0.0;
+ collisionResult.AnimatedObjectHasTrackedRotation = obj.AnimatedObjectHasTrackedRotation;
+ if (obj.AnimatedObjectHasTrackedRotation &&
+ obj.AnimatedObjectTrackedRotA.HasValue &&
+ obj.AnimatedObjectTrackedRotB.HasValue &&
+ obj.AnimatedObjectTrackedRotC.HasValue &&
+ obj.AnimatedObjectTrackedRotD.HasValue)
{
- collisionResult.Item1Rotation = new Rotation3D(
- obj.Item1RotA.Value,
- obj.Item1RotB.Value,
- obj.Item1RotC.Value,
- obj.Item1RotD.Value);
+ collisionResult.AnimatedObjectTrackedRotation = new Rotation3D(
+ obj.AnimatedObjectTrackedRotA.Value,
+ obj.AnimatedObjectTrackedRotB.Value,
+ obj.AnimatedObjectTrackedRotC.Value,
+ obj.AnimatedObjectTrackedRotD.Value);
}
collisionResult.HasPositionInfo = true;
-
- LogManager.Debug($"[加载碰撞结果] 恢复运动物体位置: ({obj.Item1PosX:F2}, {obj.Item1PosY:F2}, {obj.Item1PosZ:F2}), yaw={obj.Item1YawRadians:F2} rad, customRotation={obj.Item1HasCustomRotation}");
+
+ LogManager.Debug($"[加载碰撞结果] 恢复运动物体位置: ({obj.AnimatedObjectTrackedPosX:F2}, {obj.AnimatedObjectTrackedPosY:F2}, {obj.AnimatedObjectTrackedPosZ:F2}), yaw={obj.AnimatedObjectTrackedYawRadians:F2} rad, customRotation={obj.AnimatedObjectHasTrackedRotation}");
}
results.Add(collisionResult);
@@ -796,22 +797,27 @@ namespace NavisworksTransport
{
var testAnimatedObject = candidate.Item1;
var modelItems = new ModelItemCollection { testAnimatedObject };
- var targetPosition = candidate.Item1Position;
- var targetYaw = candidate.Item1YawRadians;
- var targetRotation = candidate.Item1Rotation;
+ var targetPosition = candidate.AnimatedObjectTrackedPosition;
+ var targetYaw = candidate.AnimatedObjectTrackedYawRadians;
+ var targetRotation = candidate.AnimatedObjectTrackedRotation;
// 🔥 关键:为了最高精度,先回到CAD原始位置,再移动到目标位置
// 避免累积误差影响碰撞检测精度
doc.Models.ResetPermanentTransform(modelItems);
- if (candidate.Item1HasCustomRotation && targetRotation != null)
+ if (candidate.AnimatedObjectHasTrackedRotation && targetRotation != null)
{
+ var adapter = CoordinateSystemManager.Instance.CreateHostAdapter();
var originalBounds = testAnimatedObject.BoundingBox();
- var originalCenter = originalBounds.Center;
+ var originalTrackedPosition = ModelItemTransformHelper.GetHostBottomAnchorPoint(originalBounds, adapter);
var originalRotation = testAnimatedObject.Transform.Factor().Rotation;
+ LogManager.Info(
+ $"[ClashDetective验证] 三维候选恢复: " +
+ $"当前跟踪点=({originalTrackedPosition.X:F3},{originalTrackedPosition.Y:F3},{originalTrackedPosition.Z:F3}), " +
+ $"目标跟踪点=({targetPosition.X:F3},{targetPosition.Y:F3},{targetPosition.Z:F3})");
ModelItemTransformHelper.MoveItemIncrementallyToPositionAndRotation(
testAnimatedObject,
- originalCenter,
+ originalTrackedPosition,
originalRotation,
targetPosition,
targetRotation);
@@ -921,14 +927,14 @@ namespace NavisworksTransport
var confirmedPositionKey = GetCollisionObjectKey(candidate.Item1, candidate.Item2);
_confirmedCollisionPositions[confirmedPositionKey] = new CollisionPositionInfo
{
- Item1Position = candidate.Item1Position,
+ AnimatedObjectTrackedPosition = candidate.AnimatedObjectTrackedPosition,
Item2Position = candidate.Item2Position,
- Item1YawRadians = candidate.Item1YawRadians,
- Item1Rotation = candidate.Item1Rotation,
- Item1HasCustomRotation = candidate.Item1HasCustomRotation,
+ AnimatedObjectTrackedYawRadians = candidate.AnimatedObjectTrackedYawRadians,
+ AnimatedObjectTrackedRotation = candidate.AnimatedObjectTrackedRotation,
+ AnimatedObjectHasTrackedRotation = candidate.AnimatedObjectHasTrackedRotation,
HasPositionInfo = candidate.HasPositionInfo
};
- LogManager.Debug($"[ClashDetective确认] 记录碰撞位置: {confirmedPositionKey}, 位置: ({candidate.Item1Position.X:F2}, {candidate.Item1Position.Y:F2}, {candidate.Item1Position.Z:F2})");
+ LogManager.Debug($"[ClashDetective确认] 记录碰撞位置: {confirmedPositionKey}, 位置: ({candidate.AnimatedObjectTrackedPosition.X:F2}, {candidate.AnimatedObjectTrackedPosition.Y:F2}, {candidate.AnimatedObjectTrackedPosition.Z:F2})");
int subResultIndex = 1;
// 🔥 优化:预获取运动物体名称,避免在循环中重复获取
@@ -1037,11 +1043,11 @@ namespace NavisworksTransport
Distance = clashResult.Distance,
CreatedTime = DateTime.Now,
// 🔥 使用ClashDetective确认时的位置信息
- Item1Position = confirmedPosition?.Item1Position,
+ AnimatedObjectTrackedPosition = confirmedPosition?.AnimatedObjectTrackedPosition,
Item2Position = confirmedPosition?.Item2Position,
- Item1YawRadians = confirmedPosition?.Item1YawRadians ?? 0,
- Item1Rotation = confirmedPosition?.Item1Rotation,
- Item1HasCustomRotation = confirmedPosition?.Item1HasCustomRotation ?? false,
+ AnimatedObjectTrackedYawRadians = confirmedPosition?.AnimatedObjectTrackedYawRadians ?? 0,
+ AnimatedObjectTrackedRotation = confirmedPosition?.AnimatedObjectTrackedRotation,
+ AnimatedObjectHasTrackedRotation = confirmedPosition?.AnimatedObjectHasTrackedRotation ?? false,
HasPositionInfo = confirmedPosition != null
};
clashResults.Add(collisionResult);
@@ -1056,7 +1062,7 @@ namespace NavisworksTransport
.ToList();
// 🔥 日志:统计位置信息保留情况
- var withPositionCount = finalClashResults.Count(c => c.HasPositionInfo && c.Item1Position != null);
+ var withPositionCount = finalClashResults.Count(c => c.HasPositionInfo && c.AnimatedObjectTrackedPosition != null);
LogManager.Info($"[最终去重] ClashDetective结果去重: {clashResults.Count} 个碰撞 -> {finalClashResults.Count} 个唯一碰撞对,其中 {withPositionCount} 个包含位置信息");
// 🔥 优化:在去重后为结果设置详细的 DisplayName
@@ -1132,7 +1138,7 @@ namespace NavisworksTransport
LogManager.Info($"[预计算数据] 共有 {precomputedCollisions.Count} 个碰撞记录");
// 检查预计算碰撞结果是否包含位置信息
- var collisionsWithPosition = precomputedCollisions.Count(c => c.HasPositionInfo && c.Item1Position != null);
+ var collisionsWithPosition = precomputedCollisions.Count(c => c.HasPositionInfo && c.AnimatedObjectTrackedPosition != null);
LogManager.Info($"[预计算数据] 包含位置信息的碰撞: {collisionsWithPosition}/{precomputedCollisions.Count}");
// 直接使用所有预计算结果,只过滤有效对象(不去重)
@@ -2168,11 +2174,11 @@ namespace NavisworksTransport
public DateTime CreatedTime { get; set; }
// 位置和朝向信息用于还原碰撞场景
- public Point3D Item1Position { get; set; }
+ public Point3D AnimatedObjectTrackedPosition { get; set; }
public Point3D Item2Position { get; set; }
- public double Item1YawRadians { get; set; } // 运动物体朝向(弧度)
- public Rotation3D Item1Rotation { get; set; } // 运动物体完整三维姿态
- public bool Item1HasCustomRotation { get; set; }
+ public double AnimatedObjectTrackedYawRadians { get; set; } // 动画跟踪点对应的运动物体朝向(弧度)
+ public Rotation3D AnimatedObjectTrackedRotation { get; set; } // 动画跟踪点对应的运动物体完整三维姿态
+ public bool AnimatedObjectHasTrackedRotation { get; set; }
public bool HasPositionInfo { get; set; }
// IEquatable 实现:基于碰撞对象进行去重
diff --git a/src/Core/PathAnalysisEngine.cs b/src/Core/PathAnalysisEngine.cs
index 8bacd87..01b34fb 100644
--- a/src/Core/PathAnalysisEngine.cs
+++ b/src/Core/PathAnalysisEngine.cs
@@ -653,14 +653,14 @@ namespace NavisworksTransport.Core
var collisions = new List();
foreach (var obj in collisionObjects)
{
- if (obj.Item1PosX.HasValue && obj.Item1PosY.HasValue && obj.Item1PosZ.HasValue)
+ if (obj.AnimatedObjectTrackedPosX.HasValue && obj.AnimatedObjectTrackedPosY.HasValue && obj.AnimatedObjectTrackedPosZ.HasValue)
{
collisions.Add(new CollisionResult
{
Center = new Point3D(
- obj.Item1PosX.Value,
- obj.Item1PosY.Value,
- obj.Item1PosZ.Value),
+ obj.AnimatedObjectTrackedPosX.Value,
+ obj.AnimatedObjectTrackedPosY.Value,
+ obj.AnimatedObjectTrackedPosZ.Value),
Item2 = ModelItemProxyHelper.CreateProxy(obj.DisplayName)
});
}
diff --git a/src/Core/PathDatabase.cs b/src/Core/PathDatabase.cs
index 9477166..5b03432 100644
--- a/src/Core/PathDatabase.cs
+++ b/src/Core/PathDatabase.cs
@@ -234,16 +234,16 @@ namespace NavisworksTransport
PathId TEXT,
DisplayName TEXT,
ObjectName TEXT,
- -- 碰撞时运动物体的位置和朝向(用于还原碰撞场景)
- Item1PosX REAL,
- Item1PosY REAL,
- Item1PosZ REAL,
- Item1YawRadians REAL,
- Item1RotA REAL,
- Item1RotB REAL,
- Item1RotC REAL,
- Item1RotD REAL,
- Item1HasCustomRotation INTEGER DEFAULT 0,
+ -- 碰撞时运动物体的动画跟踪点位置和姿态(用于还原碰撞场景)
+ AnimatedObjectTrackedPosX REAL,
+ AnimatedObjectTrackedPosY REAL,
+ AnimatedObjectTrackedPosZ REAL,
+ AnimatedObjectTrackedYawRadians REAL,
+ AnimatedObjectTrackedRotA REAL,
+ AnimatedObjectTrackedRotB REAL,
+ AnimatedObjectTrackedRotC REAL,
+ AnimatedObjectTrackedRotD REAL,
+ AnimatedObjectHasTrackedRotation INTEGER DEFAULT 0,
HasPositionInfo INTEGER DEFAULT 0,
FOREIGN KEY(DetectionRecordId) REFERENCES CollisionDetectionRecords(Id) ON DELETE CASCADE
)
@@ -300,8 +300,8 @@ namespace NavisworksTransport
// 12. 设置数据库版本(SQLite内置user_version)
// 版本号格式:主版本*10000 + 次版本*100 + 修订号
- // 例如:2.1.4 = 20104
- ExecuteNonQuery("PRAGMA user_version = 20104"); // v2.1.4 - ClashDetective碰撞对象位姿字段扩展与Rail三维恢复链路调整
+ // 例如:2.1.5 = 20105
+ ExecuteNonQuery("PRAGMA user_version = 20105"); // v2.1.5 - 碰撞对象位姿字段命名统一为动画跟踪点语义
// 创建索引
ExecuteNonQuery("CREATE INDEX IF NOT EXISTS idx_reports_route ON CollisionReports(RouteId)");
@@ -1114,11 +1114,11 @@ namespace NavisworksTransport
var sql = @"
INSERT INTO ClashDetectiveCollisionObjects
(DetectionRecordId, ModelIndex, PathId, DisplayName, ObjectName,
- Item1PosX, Item1PosY, Item1PosZ, Item1YawRadians,
- Item1RotA, Item1RotB, Item1RotC, Item1RotD, Item1HasCustomRotation, HasPositionInfo)
+ AnimatedObjectTrackedPosX, AnimatedObjectTrackedPosY, AnimatedObjectTrackedPosZ, AnimatedObjectTrackedYawRadians,
+ AnimatedObjectTrackedRotA, AnimatedObjectTrackedRotB, AnimatedObjectTrackedRotC, AnimatedObjectTrackedRotD, AnimatedObjectHasTrackedRotation, HasPositionInfo)
VALUES (@detectionRecordId, @modelIndex, @pathId, @displayName, @objectName,
- @item1PosX, @item1PosY, @item1PosZ, @item1YawRadians,
- @item1RotA, @item1RotB, @item1RotC, @item1RotD, @item1HasCustomRotation, @hasPositionInfo)
+ @trackedPosX, @trackedPosY, @trackedPosZ, @trackedYawRadians,
+ @trackedRotA, @trackedRotB, @trackedRotC, @trackedRotD, @hasTrackedRotation, @hasPositionInfo)
";
foreach (var obj in objects)
@@ -1130,15 +1130,15 @@ namespace NavisworksTransport
cmd.Parameters.AddWithValue("@pathId", obj.PathId ?? (object)DBNull.Value);
cmd.Parameters.AddWithValue("@displayName", obj.DisplayName ?? "");
cmd.Parameters.AddWithValue("@objectName", obj.ObjectName ?? "");
- cmd.Parameters.AddWithValue("@item1PosX", obj.Item1PosX.HasValue ? (object)obj.Item1PosX.Value : DBNull.Value);
- cmd.Parameters.AddWithValue("@item1PosY", obj.Item1PosY.HasValue ? (object)obj.Item1PosY.Value : DBNull.Value);
- cmd.Parameters.AddWithValue("@item1PosZ", obj.Item1PosZ.HasValue ? (object)obj.Item1PosZ.Value : DBNull.Value);
- cmd.Parameters.AddWithValue("@item1YawRadians", obj.Item1YawRadians.HasValue ? (object)obj.Item1YawRadians.Value : DBNull.Value);
- cmd.Parameters.AddWithValue("@item1RotA", obj.Item1RotA.HasValue ? (object)obj.Item1RotA.Value : DBNull.Value);
- cmd.Parameters.AddWithValue("@item1RotB", obj.Item1RotB.HasValue ? (object)obj.Item1RotB.Value : DBNull.Value);
- cmd.Parameters.AddWithValue("@item1RotC", obj.Item1RotC.HasValue ? (object)obj.Item1RotC.Value : DBNull.Value);
- cmd.Parameters.AddWithValue("@item1RotD", obj.Item1RotD.HasValue ? (object)obj.Item1RotD.Value : DBNull.Value);
- cmd.Parameters.AddWithValue("@item1HasCustomRotation", obj.Item1HasCustomRotation ? 1 : 0);
+ cmd.Parameters.AddWithValue("@trackedPosX", obj.AnimatedObjectTrackedPosX.HasValue ? (object)obj.AnimatedObjectTrackedPosX.Value : DBNull.Value);
+ cmd.Parameters.AddWithValue("@trackedPosY", obj.AnimatedObjectTrackedPosY.HasValue ? (object)obj.AnimatedObjectTrackedPosY.Value : DBNull.Value);
+ cmd.Parameters.AddWithValue("@trackedPosZ", obj.AnimatedObjectTrackedPosZ.HasValue ? (object)obj.AnimatedObjectTrackedPosZ.Value : DBNull.Value);
+ cmd.Parameters.AddWithValue("@trackedYawRadians", obj.AnimatedObjectTrackedYawRadians.HasValue ? (object)obj.AnimatedObjectTrackedYawRadians.Value : DBNull.Value);
+ cmd.Parameters.AddWithValue("@trackedRotA", obj.AnimatedObjectTrackedRotA.HasValue ? (object)obj.AnimatedObjectTrackedRotA.Value : DBNull.Value);
+ cmd.Parameters.AddWithValue("@trackedRotB", obj.AnimatedObjectTrackedRotB.HasValue ? (object)obj.AnimatedObjectTrackedRotB.Value : DBNull.Value);
+ cmd.Parameters.AddWithValue("@trackedRotC", obj.AnimatedObjectTrackedRotC.HasValue ? (object)obj.AnimatedObjectTrackedRotC.Value : DBNull.Value);
+ cmd.Parameters.AddWithValue("@trackedRotD", obj.AnimatedObjectTrackedRotD.HasValue ? (object)obj.AnimatedObjectTrackedRotD.Value : DBNull.Value);
+ cmd.Parameters.AddWithValue("@hasTrackedRotation", obj.AnimatedObjectHasTrackedRotation ? 1 : 0);
cmd.Parameters.AddWithValue("@hasPositionInfo", obj.HasPositionInfo ? 1 : 0);
cmd.ExecuteNonQuery();
}
@@ -1166,8 +1166,8 @@ namespace NavisworksTransport
{
var sql = @"
SELECT Id, DetectionRecordId, ModelIndex, PathId, DisplayName, ObjectName,
- Item1PosX, Item1PosY, Item1PosZ, Item1YawRadians,
- Item1RotA, Item1RotB, Item1RotC, Item1RotD, Item1HasCustomRotation, HasPositionInfo
+ AnimatedObjectTrackedPosX, AnimatedObjectTrackedPosY, AnimatedObjectTrackedPosZ, AnimatedObjectTrackedYawRadians,
+ AnimatedObjectTrackedRotA, AnimatedObjectTrackedRotB, AnimatedObjectTrackedRotC, AnimatedObjectTrackedRotD, AnimatedObjectHasTrackedRotation, HasPositionInfo
FROM ClashDetectiveCollisionObjects
WHERE DetectionRecordId = @detectionRecordId
";
@@ -1187,15 +1187,15 @@ namespace NavisworksTransport
PathId = reader["PathId"] != DBNull.Value ? reader["PathId"].ToString() : null,
DisplayName = reader["DisplayName"]?.ToString(),
ObjectName = reader["ObjectName"]?.ToString(),
- Item1PosX = reader["Item1PosX"] != DBNull.Value ? Convert.ToDouble(reader["Item1PosX"]) : (double?)null,
- Item1PosY = reader["Item1PosY"] != DBNull.Value ? Convert.ToDouble(reader["Item1PosY"]) : (double?)null,
- Item1PosZ = reader["Item1PosZ"] != DBNull.Value ? Convert.ToDouble(reader["Item1PosZ"]) : (double?)null,
- Item1YawRadians = reader["Item1YawRadians"] != DBNull.Value ? Convert.ToDouble(reader["Item1YawRadians"]) : (double?)null,
- Item1RotA = reader["Item1RotA"] != DBNull.Value ? Convert.ToDouble(reader["Item1RotA"]) : (double?)null,
- Item1RotB = reader["Item1RotB"] != DBNull.Value ? Convert.ToDouble(reader["Item1RotB"]) : (double?)null,
- Item1RotC = reader["Item1RotC"] != DBNull.Value ? Convert.ToDouble(reader["Item1RotC"]) : (double?)null,
- Item1RotD = reader["Item1RotD"] != DBNull.Value ? Convert.ToDouble(reader["Item1RotD"]) : (double?)null,
- Item1HasCustomRotation = reader["Item1HasCustomRotation"] != DBNull.Value && Convert.ToInt32(reader["Item1HasCustomRotation"]) == 1,
+ AnimatedObjectTrackedPosX = reader["AnimatedObjectTrackedPosX"] != DBNull.Value ? Convert.ToDouble(reader["AnimatedObjectTrackedPosX"]) : (double?)null,
+ AnimatedObjectTrackedPosY = reader["AnimatedObjectTrackedPosY"] != DBNull.Value ? Convert.ToDouble(reader["AnimatedObjectTrackedPosY"]) : (double?)null,
+ AnimatedObjectTrackedPosZ = reader["AnimatedObjectTrackedPosZ"] != DBNull.Value ? Convert.ToDouble(reader["AnimatedObjectTrackedPosZ"]) : (double?)null,
+ AnimatedObjectTrackedYawRadians = reader["AnimatedObjectTrackedYawRadians"] != DBNull.Value ? Convert.ToDouble(reader["AnimatedObjectTrackedYawRadians"]) : (double?)null,
+ AnimatedObjectTrackedRotA = reader["AnimatedObjectTrackedRotA"] != DBNull.Value ? Convert.ToDouble(reader["AnimatedObjectTrackedRotA"]) : (double?)null,
+ AnimatedObjectTrackedRotB = reader["AnimatedObjectTrackedRotB"] != DBNull.Value ? Convert.ToDouble(reader["AnimatedObjectTrackedRotB"]) : (double?)null,
+ AnimatedObjectTrackedRotC = reader["AnimatedObjectTrackedRotC"] != DBNull.Value ? Convert.ToDouble(reader["AnimatedObjectTrackedRotC"]) : (double?)null,
+ AnimatedObjectTrackedRotD = reader["AnimatedObjectTrackedRotD"] != DBNull.Value ? Convert.ToDouble(reader["AnimatedObjectTrackedRotD"]) : (double?)null,
+ AnimatedObjectHasTrackedRotation = reader["AnimatedObjectHasTrackedRotation"] != DBNull.Value && Convert.ToInt32(reader["AnimatedObjectHasTrackedRotation"]) == 1,
HasPositionInfo = reader["HasPositionInfo"] != DBNull.Value && Convert.ToInt32(reader["HasPositionInfo"]) == 1
});
}
@@ -3561,16 +3561,16 @@ namespace NavisworksTransport
public string DisplayName { get; set; }
public string ObjectName { get; set; }
- // 碰撞时运动物体的位置和朝向(用于还原碰撞场景)
- public double? Item1PosX { get; set; }
- public double? Item1PosY { get; set; }
- public double? Item1PosZ { get; set; }
- public double? Item1YawRadians { get; set; }
- public double? Item1RotA { get; set; }
- public double? Item1RotB { get; set; }
- public double? Item1RotC { get; set; }
- public double? Item1RotD { get; set; }
- public bool Item1HasCustomRotation { get; set; }
+ // 碰撞时运动物体的动画跟踪点位置和姿态(用于还原碰撞场景)
+ public double? AnimatedObjectTrackedPosX { get; set; }
+ public double? AnimatedObjectTrackedPosY { get; set; }
+ public double? AnimatedObjectTrackedPosZ { get; set; }
+ public double? AnimatedObjectTrackedYawRadians { get; set; }
+ public double? AnimatedObjectTrackedRotA { get; set; }
+ public double? AnimatedObjectTrackedRotB { get; set; }
+ public double? AnimatedObjectTrackedRotC { get; set; }
+ public double? AnimatedObjectTrackedRotD { get; set; }
+ public bool AnimatedObjectHasTrackedRotation { get; set; }
public bool HasPositionInfo { get; set; }
}
diff --git a/src/UI/WPF/ViewModels/AnimationControlViewModel.cs b/src/UI/WPF/ViewModels/AnimationControlViewModel.cs
index 3308bf8..e22a601 100644
--- a/src/UI/WPF/ViewModels/AnimationControlViewModel.cs
+++ b/src/UI/WPF/ViewModels/AnimationControlViewModel.cs
@@ -18,6 +18,7 @@ using NavisworksTransport.Commands;
using NavisworksTransport.UI.WPF.Collections;
using NavisworksTransport.UI.WPF.Views;
using NavisworksTransport.Utils;
+using NavisworksTransport.Utils.CoordinateSystem;
namespace NavisworksTransport.UI.WPF.ViewModels
{
@@ -178,11 +179,11 @@ namespace NavisworksTransport.UI.WPF.ViewModels
public ICommand HighlightCommand { get; set; }
public ICommand ClearHighlightCommand { get; set; }
- // 碰撞位置信息(用于还原碰撞场景)
- public Point3D Item1Position { get; set; }
- public double Item1YawRadians { get; set; }
- public Rotation3D Item1Rotation { get; set; }
- public bool Item1HasCustomRotation { get; set; }
+ // 动画跟踪点位置信息(用于还原碰撞场景)
+ public Point3D AnimatedObjectTrackedPosition { get; set; }
+ public double AnimatedObjectTrackedYawRadians { get; set; }
+ public Rotation3D AnimatedObjectTrackedRotation { get; set; }
+ public bool AnimatedObjectHasTrackedRotation { get; set; }
public bool HasPositionInfo { get; set; }
}
@@ -1700,9 +1701,15 @@ namespace NavisworksTransport.UI.WPF.ViewModels
// 2. 先保存物体原始尺寸(在设置SelectedAnimatedObject之前)
var bbox = newObject.BoundingBox();
double metersToModelUnits = UnitsConverter.GetMetersToUnitsConversionFactor();
- _objectOriginalLength = (bbox.Max.X - bbox.Min.X) / metersToModelUnits;
- _objectOriginalWidth = (bbox.Max.Y - bbox.Min.Y) / metersToModelUnits;
- _objectOriginalHeight = (bbox.Max.Z - bbox.Min.Z) / metersToModelUnits;
+ var adapter = CoordinateSystemManager.Instance.CreateHostAdapter();
+ var axisConvention = ModelAxisConvention.CreateDefaultForHost(adapter.HostType);
+ double objectLength = axisConvention.GetForwardSize(bbox);
+ double objectWidth = axisConvention.GetSideSize(bbox);
+ double objectHeight = axisConvention.GetUpSize(bbox);
+ _objectOriginalLength = objectLength / metersToModelUnits;
+ _objectOriginalWidth = objectWidth / metersToModelUnits;
+ _objectOriginalHeight = objectHeight / metersToModelUnits;
+ _pathAnimationManager?.SetRealObjectDimensions(objectLength, objectWidth, objectHeight);
LogManager.Debug($"[选择物体] 保存原始尺寸: 长度={_objectOriginalLength:F2}m, 宽度={_objectOriginalWidth:F2}m, 高度={_objectOriginalHeight:F2}m");
// 3. 设置新物体(会触发UpdatePassageSpaceVisualization)
@@ -2508,10 +2515,10 @@ namespace NavisworksTransport.UI.WPF.ViewModels
CollisionSceneHelper.MoveToCollisionAndFocus(
collisionObject.ModelItem,
null,
- collisionObject.Item1Position,
- collisionObject.Item1YawRadians,
- collisionObject.Item1Rotation,
- collisionObject.Item1HasCustomRotation,
+ collisionObject.AnimatedObjectTrackedPosition,
+ collisionObject.AnimatedObjectTrackedYawRadians,
+ collisionObject.AnimatedObjectTrackedRotation,
+ collisionObject.AnimatedObjectHasTrackedRotation,
false);
return;
}
@@ -2548,10 +2555,10 @@ namespace NavisworksTransport.UI.WPF.ViewModels
CollisionSceneHelper.MoveToCollisionAndFocus(
collisionObject.ModelItem,
animatedObject,
- collisionObject.Item1Position,
- collisionObject.Item1YawRadians,
- collisionObject.Item1Rotation,
- collisionObject.Item1HasCustomRotation,
+ collisionObject.AnimatedObjectTrackedPosition,
+ collisionObject.AnimatedObjectTrackedYawRadians,
+ collisionObject.AnimatedObjectTrackedRotation,
+ collisionObject.AnimatedObjectHasTrackedRotation,
collisionObject.HasPositionInfo);
// 🔥 虚拟物体缩放已通过MoveItemToPositionAndYawWithCurrentScale保留
@@ -3680,21 +3687,21 @@ namespace NavisworksTransport.UI.WPF.ViewModels
ModelItem = modelItem,
HighlightCommand = new RelayCommand(() => resultViewModel.ExecuteHighlightCollisionObject(objViewModel)),
// 填充碰撞位置信息
- Item1Position = objRecord.HasPositionInfo && objRecord.Item1PosX.HasValue ?
- new Point3D(objRecord.Item1PosX.Value, objRecord.Item1PosY.Value, objRecord.Item1PosZ.Value) : null,
- Item1YawRadians = objRecord.Item1YawRadians ?? 0,
- Item1Rotation = objRecord.Item1HasCustomRotation &&
- objRecord.Item1RotA.HasValue &&
- objRecord.Item1RotB.HasValue &&
- objRecord.Item1RotC.HasValue &&
- objRecord.Item1RotD.HasValue
+ AnimatedObjectTrackedPosition = objRecord.HasPositionInfo && objRecord.AnimatedObjectTrackedPosX.HasValue ?
+ new Point3D(objRecord.AnimatedObjectTrackedPosX.Value, objRecord.AnimatedObjectTrackedPosY.Value, objRecord.AnimatedObjectTrackedPosZ.Value) : null,
+ AnimatedObjectTrackedYawRadians = objRecord.AnimatedObjectTrackedYawRadians ?? 0,
+ AnimatedObjectTrackedRotation = objRecord.AnimatedObjectHasTrackedRotation &&
+ objRecord.AnimatedObjectTrackedRotA.HasValue &&
+ objRecord.AnimatedObjectTrackedRotB.HasValue &&
+ objRecord.AnimatedObjectTrackedRotC.HasValue &&
+ objRecord.AnimatedObjectTrackedRotD.HasValue
? new Rotation3D(
- objRecord.Item1RotA.Value,
- objRecord.Item1RotB.Value,
- objRecord.Item1RotC.Value,
- objRecord.Item1RotD.Value)
+ objRecord.AnimatedObjectTrackedRotA.Value,
+ objRecord.AnimatedObjectTrackedRotB.Value,
+ objRecord.AnimatedObjectTrackedRotC.Value,
+ objRecord.AnimatedObjectTrackedRotD.Value)
: Rotation3D.Identity,
- Item1HasCustomRotation = objRecord.Item1HasCustomRotation,
+ AnimatedObjectHasTrackedRotation = objRecord.AnimatedObjectHasTrackedRotation,
HasPositionInfo = objRecord.HasPositionInfo
};
resultViewModel.CollisionObjects.Add(objViewModel);
@@ -4410,9 +4417,12 @@ namespace NavisworksTransport.UI.WPF.ViewModels
else // Rail
{
// 空轨路径:物体的长度方向(X轴,前进方向)朝向路径方向
- // 通行空间沿路径方向 = X方向尺寸(长度),垂直于路径方向 = Y方向尺寸(宽度),高度上下都加间隙(在空中)
+ // 通行空间沿路径方向 = X方向尺寸(长度),垂直于路径方向 = Y方向尺寸(宽度)
+ // 法线方向只在远离轨道的一侧留间隙:
+ // - 轨下安装:顶面贴路径,底面留间隙
+ // - 轨上安装:底面贴路径,顶面留间隙
passageAcrossPath = effectiveWidth + 2 * safetyMargin; // 旋转后宽度 + 2*间隙(垂直于路径方向)
- passageNormalToPath = virtualObjectHeight + 2 * safetyMargin; // Z方向(高度)+ 2*间隙(法线方向,上下都加)
+ passageNormalToPath = virtualObjectHeight + safetyMargin; // 法线方向单侧留间隙
passageAlongPath = effectiveLength; // 旋转后长度(沿路径方向)
// 空轨路径不需要分段参数
passageNormalToPathVertical = passageNormalToPath;
@@ -4422,13 +4432,13 @@ namespace NavisworksTransport.UI.WPF.ViewModels
}
else if (SelectedAnimatedObject != null)
{
- // 使用选择物体的包围盒尺寸(模型单位)
+ // 使用选择物体的局部轴语义尺寸(模型单位)
var bbox = SelectedAnimatedObject.BoundingBox();
-
- // 包围盒尺寸已经是模型单位
- double sizeX = bbox.Max.X - bbox.Min.X; // X方向 = 长度(前进方向)
- double sizeY = bbox.Max.Y - bbox.Min.Y; // Y方向 = 宽度(侧面)
- double sizeZ = bbox.Max.Z - bbox.Min.Z; // Z方向 = 高度
+ var adapter = CoordinateSystemManager.Instance.CreateHostAdapter();
+ var axisConvention = ModelAxisConvention.CreateDefaultForHost(adapter.HostType);
+ double sizeAlongPath = axisConvention.GetForwardSize(bbox);
+ double sizeAcrossPath = axisConvention.GetSideSize(bbox);
+ double sizeNormalToPath = axisConvention.GetUpSize(bbox);
// 根据路径类型确定通行空间的尺寸
if (CurrentPathRoute.PathType == NavisworksTransport.PathType.Rail || CurrentPathRoute.PathType == NavisworksTransport.PathType.Hoisting)
@@ -4436,12 +4446,12 @@ namespace NavisworksTransport.UI.WPF.ViewModels
// 空中路径(空轨或吊装):
// 高度上下都加间隙
passageAcrossPath = effectiveWidth + 2 * safetyMargin; // 旋转后宽度 + 2*间隙(垂直于路径方向)
- passageNormalToPath = sizeZ + 2 * safetyMargin; // Z方向(高度)+ 2*间隙(法线方向)
+ passageNormalToPath = sizeNormalToPath + 2 * safetyMargin; // 局部up方向高度 + 2*间隙(法线方向)
passageAlongPath = effectiveLength; // 旋转后长度(沿路径方向)
// 吊装路径分段参数
passageNormalToPathVertical = effectiveLength + 2 * safetyMargin; // 垂直段:物体长度 + 2*间隙
- passageNormalToPathHorizontal = sizeZ + 2 * safetyMargin; // 水平段:物体高度 + 2*间隙
- LogManager.Debug($"[通行空间可视化] 空中路径使用物体尺寸 ({SelectedAnimatedObject.DisplayName}): 有效长度={effectiveLength / metersToUnitsPassage:F2}m, 有效宽度={effectiveWidth / metersToUnitsPassage:F2}m, 高度={sizeZ / metersToUnitsPassage:F2}m, 通行空间沿路径={passageAlongPath / metersToUnitsPassage:F2}m, 垂直路径={passageAcrossPath / metersToUnitsPassage:F2}m, 法线={passageNormalToPath / metersToUnitsPassage:F2}m");
+ passageNormalToPathHorizontal = sizeNormalToPath + 2 * safetyMargin; // 水平段:物体高度 + 2*间隙
+ LogManager.Debug($"[通行空间可视化] 空中路径使用物体尺寸 ({SelectedAnimatedObject.DisplayName}): 局部长度={sizeAlongPath / metersToUnitsPassage:F2}m, 局部宽度={sizeAcrossPath / metersToUnitsPassage:F2}m, 局部高度={sizeNormalToPath / metersToUnitsPassage:F2}m, 有效长度={effectiveLength / metersToUnitsPassage:F2}m, 有效宽度={effectiveWidth / metersToUnitsPassage:F2}m, 通行空间沿路径={passageAlongPath / metersToUnitsPassage:F2}m, 垂直路径={passageAcrossPath / metersToUnitsPassage:F2}m, 法线={passageNormalToPath / metersToUnitsPassage:F2}m");
}
else if (CurrentPathRoute.PathType == NavisworksTransport.PathType.Ground)
{
@@ -4449,24 +4459,27 @@ namespace NavisworksTransport.UI.WPF.ViewModels
// 物体的长度方向(X轴,前进方向)朝向路径方向
// 通行空间沿路径方向 = X方向尺寸(长度),垂直于路径方向 = Y方向尺寸(宽度),高度上方加间隙(下方不需要,因为有地面)
passageAcrossPath = effectiveWidth + 2 * safetyMargin; // 旋转后宽度 + 2*间隙(垂直于路径方向)
- passageNormalToPath = sizeZ + safetyMargin; // Z方向(高度)+ 间隙(法线方向,只有上方)
+ passageNormalToPath = sizeNormalToPath + safetyMargin; // 局部up方向高度 + 间隙(法线方向,只有上方)
passageAlongPath = effectiveLength; // 旋转后长度(沿路径方向)
// 地面路径不需要分段参数
passageNormalToPathVertical = passageNormalToPath;
passageNormalToPathHorizontal = passageNormalToPath;
- LogManager.Debug($"[通行空间可视化] 地面路径使用物体尺寸 ({SelectedAnimatedObject.DisplayName}): 有效长度={effectiveLength / metersToUnitsPassage:F2}m, 有效宽度={effectiveWidth / metersToUnitsPassage:F2}m, 通行空间沿路径={passageAlongPath / metersToUnitsPassage:F2}m, 垂直路径={passageAcrossPath / metersToUnitsPassage:F2}m, 法线={passageNormalToPath / metersToUnitsPassage:F2}m");
+ LogManager.Debug($"[通行空间可视化] 地面路径使用物体尺寸 ({SelectedAnimatedObject.DisplayName}): 局部长度={sizeAlongPath / metersToUnitsPassage:F2}m, 局部宽度={sizeAcrossPath / metersToUnitsPassage:F2}m, 局部高度={sizeNormalToPath / metersToUnitsPassage:F2}m, 有效长度={effectiveLength / metersToUnitsPassage:F2}m, 有效宽度={effectiveWidth / metersToUnitsPassage:F2}m, 通行空间沿路径={passageAlongPath / metersToUnitsPassage:F2}m, 垂直路径={passageAcrossPath / metersToUnitsPassage:F2}m, 法线={passageNormalToPath / metersToUnitsPassage:F2}m");
}
else // Rail
{
// 空轨路径(可能有坡度):
// 物体的长度方向(X轴,前进方向)朝向路径方向
- // 通行空间沿路径方向 = X方向尺寸(长度),垂直于路径方向 = Y方向尺寸(宽度),高度上下都加间隙(在空中)
+ // 通行空间沿路径方向 = X方向尺寸(长度),垂直于路径方向 = Y方向尺寸(宽度)
+ // 法线方向只在远离轨道的一侧留间隙:
+ // - 轨下安装:顶面贴路径,底面留间隙
+ // - 轨上安装:底面贴路径,顶面留间隙
passageAcrossPath = effectiveWidth + 2 * safetyMargin; // 旋转后宽度 + 2*间隙(垂直于路径方向)
- passageNormalToPath = sizeZ + 2 * safetyMargin; // Z方向(高度)+ 2*间隙(法线方向,上下都加)
+ passageNormalToPath = sizeNormalToPath + safetyMargin; // 局部up方向高度 + 单侧间隙(法线方向)
passageAlongPath = effectiveLength; // 旋转后长度(沿路径方向)
passageNormalToPathVertical = passageNormalToPath;
passageNormalToPathHorizontal = passageNormalToPath;
- LogManager.Debug($"[通行空间可视化] 空轨路径使用物体尺寸 ({SelectedAnimatedObject.DisplayName}): 有效长度={effectiveLength / metersToUnitsPassage:F2}m, 有效宽度={effectiveWidth / metersToUnitsPassage:F2}m, 通行空间沿路径={passageAlongPath / metersToUnitsPassage:F2}m, 垂直路径={passageAcrossPath / metersToUnitsPassage:F2}m, 法线={passageNormalToPath / metersToUnitsPassage:F2}m");
+ LogManager.Debug($"[通行空间可视化] 空轨路径使用物体尺寸 ({SelectedAnimatedObject.DisplayName}): 局部长度={sizeAlongPath / metersToUnitsPassage:F2}m, 局部宽度={sizeAcrossPath / metersToUnitsPassage:F2}m, 局部高度={sizeNormalToPath / metersToUnitsPassage:F2}m, 有效长度={effectiveLength / metersToUnitsPassage:F2}m, 有效宽度={effectiveWidth / metersToUnitsPassage:F2}m, 通行空间沿路径={passageAlongPath / metersToUnitsPassage:F2}m, 垂直路径={passageAcrossPath / metersToUnitsPassage:F2}m, 法线={passageNormalToPath / metersToUnitsPassage:F2}m");
}
}
else
diff --git a/src/UI/WPF/ViewModels/CollisionReportViewModel.cs b/src/UI/WPF/ViewModels/CollisionReportViewModel.cs
index 37e97bc..32e95f5 100644
--- a/src/UI/WPF/ViewModels/CollisionReportViewModel.cs
+++ b/src/UI/WPF/ViewModels/CollisionReportViewModel.cs
@@ -1389,7 +1389,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
var collisionData = collisionItem.CollisionData;
// 首次查看碰撞时保存运动物体状态
- if (collisionData.Item1 != null && collisionData.HasPositionInfo && collisionData.Item1Position != null)
+ if (collisionData.Item1 != null && collisionData.HasPositionInfo && collisionData.AnimatedObjectTrackedPosition != null)
{
if (_savedAnimatedObjectState == null || _currentAnimatedObject != collisionData.Item1)
{
@@ -1432,4 +1432,4 @@ namespace NavisworksTransport.UI.WPF.ViewModels
#endregion
}
-}
\ No newline at end of file
+}
diff --git a/src/UI/WPF/ViewModels/PathEditingViewModel.cs b/src/UI/WPF/ViewModels/PathEditingViewModel.cs
index ce55ad8..88fb975 100644
--- a/src/UI/WPF/ViewModels/PathEditingViewModel.cs
+++ b/src/UI/WPF/ViewModels/PathEditingViewModel.cs
@@ -1508,11 +1508,14 @@ namespace NavisworksTransport.UI.WPF.ViewModels
}
HostCoordinateAdapter adapter = CoordinateSystemManager.Instance.CreateHostAdapter();
+ ProjectReferenceFrame projectFrame = CreateAssemblyProjectReferenceFrame(adapter);
var bounds = _assemblyTerminalObject.BoundingBox();
Point3D canonicalCenter = adapter.ToCanonicalPoint(bounds.Center);
Vector3D canonicalUp = adapter.ToCanonicalVector(
- ModelItemTransformHelper.GetUpDirectionFromTransform(_assemblyTerminalObject.Transform));
+ ModelItemTransformHelper.GetDirectionFromTransform(
+ _assemblyTerminalObject.Transform,
+ projectFrame.DefaultModelAxisConvention.UpAxis));
double direction = GetAssemblyAnchorDirection();
double verticalOffset = UnitsConverter.ConvertFromMeters(AssemblyAnchorVerticalOffsetInMeters);
@@ -1652,9 +1655,11 @@ namespace NavisworksTransport.UI.WPF.ViewModels
return;
}
+ HostCoordinateAdapter adapter = CoordinateSystemManager.Instance.CreateHostAdapter();
+ ProjectReferenceFrame projectFrame = CreateAssemblyProjectReferenceFrame(adapter);
var bounds = _assemblyTerminalObject.BoundingBox();
- Vector3D localSize = ModelItemTransformHelper.EstimateLocalBoxSize(bounds, _assemblyTerminalObject.Transform);
- double suggestedOffset = UnitsConverter.ConvertToMeters(Math.Max(0.0, localSize.Z / 2.0));
+ double suggestedOffset = UnitsConverter.ConvertToMeters(
+ Math.Max(0.0, projectFrame.DefaultModelAxisConvention.GetUpSize(bounds) / 2.0));
_assemblyAnchorVerticalOffsetInMeters = suggestedOffset;
OnPropertyChanged(nameof(AssemblyAnchorVerticalOffsetInMeters));
}
diff --git a/src/Utils/CollisionSceneHelper.cs b/src/Utils/CollisionSceneHelper.cs
index a1e5327..ffe839b 100644
--- a/src/Utils/CollisionSceneHelper.cs
+++ b/src/Utils/CollisionSceneHelper.cs
@@ -2,6 +2,7 @@ using System;
using Autodesk.Navisworks.Api;
using NavisworksTransport.Core;
using NavisworksTransport.Core.Animation;
+using NavisworksTransport.Utils.CoordinateSystem;
namespace NavisworksTransport.Utils
{
@@ -20,8 +21,8 @@ namespace NavisworksTransport.Utils
{
if (collision == null) return;
MoveToCollisionAndFocus(collision.Item2 ?? collision.Item1, animatedObject,
- collision.Item1Position, collision.Item1YawRadians, collision.Item1Rotation,
- collision.Item1HasCustomRotation, collision.HasPositionInfo);
+ collision.AnimatedObjectTrackedPosition, collision.AnimatedObjectTrackedYawRadians, collision.AnimatedObjectTrackedRotation,
+ collision.AnimatedObjectHasTrackedRotation, collision.HasPositionInfo);
}
///
@@ -29,14 +30,14 @@ namespace NavisworksTransport.Utils
///
/// 被撞对象
/// 动画物体(运动物体)
- /// 碰撞位置
- /// 碰撞朝向
- /// 碰撞完整姿态
- /// 是否有完整姿态
+ /// 动画跟踪点位置
+ /// 动画跟踪点对应的碰撞朝向
+ /// 动画跟踪点对应的碰撞完整姿态
+ /// 是否有完整姿态
/// 是否有位置信息
public static void MoveToCollisionAndFocus(ModelItem hitObject, ModelItem animatedObject,
- Point3D item1Position, double item1YawRadians, Rotation3D item1Rotation,
- bool item1HasCustomRotation, bool hasPositionInfo)
+ Point3D animatedObjectTrackedPosition, double animatedObjectTrackedYawRadians, Rotation3D animatedObjectTrackedRotation,
+ bool animatedObjectHasTrackedRotation, bool hasPositionInfo)
{
try
{
@@ -55,60 +56,30 @@ namespace NavisworksTransport.Utils
}
// 移动动画物体到碰撞位置
- if (animatedObject != null && hasPositionInfo && item1Position != null)
+ if (animatedObject != null && hasPositionInfo && animatedObjectTrackedPosition != null)
{
var pam = PathAnimationManager.GetInstance();
bool useAnimationPipeline = pam != null &&
- pam.ControlsAnimatedObject(animatedObject) &&
- item1HasCustomRotation &&
- item1Rotation != null;
-
- // 检查是否是虚拟物体
- bool isVirtual = VirtualObjectManager.Instance.IsVirtualObjectActive &&
- VirtualObjectManager.Instance.CurrentVirtualObject == animatedObject;
+ pam.ControlsAnimatedObject(animatedObject);
if (useAnimationPipeline)
{
pam.MoveAnimatedObjectToPose(
animatedObject,
- item1Position,
- item1YawRadians,
- item1Rotation,
- true);
+ animatedObjectTrackedPosition,
+ animatedObjectTrackedYawRadians,
+ animatedObjectTrackedRotation ?? Rotation3D.Identity,
+ animatedObjectHasTrackedRotation && animatedObjectTrackedRotation != null);
LogManager.Info(string.Format(
"运动物体已通过动画链路移动到碰撞位置: 位置=({0:F2}, {1:F2}, {2:F2}), customRotation={3}",
- item1Position.X, item1Position.Y, item1Position.Z,
- true));
- }
- else if (item1HasCustomRotation && item1Rotation != null)
- {
- throw new InvalidOperationException(
- $"三维碰撞点恢复必须复用动画主链路,当前对象未受 PathAnimationManager 控制: {animatedObject.DisplayName}");
+ animatedObjectTrackedPosition.X, animatedObjectTrackedPosition.Y, animatedObjectTrackedPosition.Z,
+ animatedObjectHasTrackedRotation && animatedObjectTrackedRotation != null));
}
else
{
- var bounds = animatedObject.BoundingBox();
- double halfHeight = (bounds.Max.Z - bounds.Min.Z) / 2.0;
- var targetGroundPosition = new Point3D(
- item1Position.X,
- item1Position.Y,
- item1Position.Z - halfHeight
- );
-
- if (isVirtual)
- {
- ModelItemTransformHelper.MoveItemToPositionAndYawWithCurrentScale(
- animatedObject, targetGroundPosition, item1YawRadians);
- }
- else
- {
- ModelItemTransformHelper.MoveItemToPositionAndYaw(animatedObject, targetGroundPosition, item1YawRadians);
- }
-
- LogManager.Info(string.Format("运动物体已移动到碰撞位置: ({0:F2}, {1:F2}, {2:F2}), 朝向: {3:F2}°",
- targetGroundPosition.X, targetGroundPosition.Y, targetGroundPosition.Z,
- item1YawRadians * 180 / Math.PI));
+ throw new InvalidOperationException(
+ $"碰撞点恢复必须复用动画主链路,当前对象未受 PathAnimationManager 控制: {animatedObject.DisplayName}");
}
}
}
@@ -132,8 +103,8 @@ namespace NavisworksTransport.Utils
// 从PathAnimationManager获取当前实际朝向
var pam = PathAnimationManager.GetInstance();
var currentYaw = pam.CurrentYaw;
- var currentRotation = pam.CurrentRotation;
- var hasCustomRotation = pam.HasCurrentRotation;
+ var currentRotation = pam.TrackedRotation;
+ var hasCustomRotation = pam.HasTrackedRotation;
var currentState = pam.GetObjectCurrentPosition(animatedObject);
pam.SaveObjectState(animatedObject);
@@ -169,15 +140,18 @@ namespace NavisworksTransport.Utils
try
{
var pam = PathAnimationManager.GetInstance();
+ if (pam != null && pam.ControlsAnimatedObject(animatedObject))
+ {
+ pam.RestoreAnimatedObjectState(animatedObject);
+ LogManager.Info(string.Format(
+ "动画物体已通过PathAnimationManager恢复状态: {0}, customRotation={1}",
+ animatedObject.DisplayName,
+ state.HasCustomRotation));
+ return;
+ }
+
if (state.HasCustomRotation)
{
- if (pam != null && ReferenceEquals(pam.AnimatedObject, animatedObject))
- {
- pam.RestoreAnimatedObjectState(animatedObject);
- LogManager.Info(string.Format("动画物体已通过PathAnimationManager恢复三维姿态: {0}", animatedObject.DisplayName));
- return;
- }
-
bool isVirtualWithCustomRotation = VirtualObjectManager.Instance.IsVirtualObjectActive &&
VirtualObjectManager.Instance.CurrentVirtualObject == animatedObject;
if (isVirtualWithCustomRotation)
diff --git a/src/Utils/CoordinateSystem/ModelAxisConvention.cs b/src/Utils/CoordinateSystem/ModelAxisConvention.cs
index 824979d..7affc79 100644
--- a/src/Utils/CoordinateSystem/ModelAxisConvention.cs
+++ b/src/Utils/CoordinateSystem/ModelAxisConvention.cs
@@ -34,6 +34,7 @@ namespace NavisworksTransport.Utils.CoordinateSystem
public Vector3 UpUnitVector => _upUnitVector;
public Vector3D ForwardVector => ToNavVector(_forwardUnitVector);
public Vector3D UpVector => ToNavVector(_upUnitVector);
+ public LocalAxisDirection SideAxis => GetAxisDirection(Normalize(Vector3.Cross(_forwardUnitVector, _upUnitVector)));
///
/// 根据模型局部轴约定,直接构造“本地 forward/up 对齐到目标世界 forward/up”的线性姿态。
@@ -107,6 +108,21 @@ namespace NavisworksTransport.Utils.CoordinateSystem
}
}
+ public double GetForwardSize(BoundingBox3D bounds)
+ {
+ return GetAxisExtent(bounds, ForwardAxis);
+ }
+
+ public double GetUpSize(BoundingBox3D bounds)
+ {
+ return GetAxisExtent(bounds, UpAxis);
+ }
+
+ public double GetSideSize(BoundingBox3D bounds)
+ {
+ return GetAxisExtent(bounds, SideAxis);
+ }
+
private static Vector3 GetAxisVector3(LocalAxisDirection axis)
{
switch (axis)
@@ -229,5 +245,23 @@ namespace NavisworksTransport.Utils.CoordinateSystem
{
return new Vector3D(vector.X, vector.Y, vector.Z);
}
+
+ private static double GetAxisExtent(BoundingBox3D bounds, LocalAxisDirection axis)
+ {
+ switch (axis)
+ {
+ case LocalAxisDirection.PositiveX:
+ case LocalAxisDirection.NegativeX:
+ return bounds.Max.X - bounds.Min.X;
+ case LocalAxisDirection.PositiveY:
+ case LocalAxisDirection.NegativeY:
+ return bounds.Max.Y - bounds.Min.Y;
+ case LocalAxisDirection.PositiveZ:
+ case LocalAxisDirection.NegativeZ:
+ return bounds.Max.Z - bounds.Min.Z;
+ default:
+ throw new ArgumentOutOfRangeException(nameof(axis), axis, null);
+ }
+ }
}
}
diff --git a/src/Utils/ModelItemTransformHelper.cs b/src/Utils/ModelItemTransformHelper.cs
index e32b4db..0ad8a0f 100644
--- a/src/Utils/ModelItemTransformHelper.cs
+++ b/src/Utils/ModelItemTransformHelper.cs
@@ -111,6 +111,22 @@ namespace NavisworksTransport.Utils
return GetAxisDirectionFromTransform(transform, 2, new Vector3D(0, 0, 1));
}
+ ///
+ /// 根据模型局部轴约定,获取指定局部轴在世界坐标中的方向。
+ ///
+ public static Vector3D GetDirectionFromTransform(
+ Transform3D transform,
+ LocalAxisDirection axisDirection)
+ {
+ GetAxisIndexAndSign(axisDirection, out int axisIndex, out int sign);
+ Vector3D fallback = GetFallbackAxis(axisDirection);
+ Vector3D direction = GetAxisDirectionFromTransform(transform, axisIndex, fallback);
+
+ return sign >= 0
+ ? direction
+ : new Vector3D(-direction.X, -direction.Y, -direction.Z);
+ }
+
///
/// 获取绑定在物体局部坐标中的稳定底面锚点。
/// 适用于带俯仰/侧倾的三维姿态对象,避免使用世界 AABB 底面中心导致锚点漂移。
@@ -305,6 +321,60 @@ namespace NavisworksTransport.Utils
}
}
+ private static void GetAxisIndexAndSign(LocalAxisDirection axisDirection, out int axisIndex, out int sign)
+ {
+ switch (axisDirection)
+ {
+ case LocalAxisDirection.PositiveX:
+ axisIndex = 0;
+ sign = 1;
+ break;
+ case LocalAxisDirection.NegativeX:
+ axisIndex = 0;
+ sign = -1;
+ break;
+ case LocalAxisDirection.PositiveY:
+ axisIndex = 1;
+ sign = 1;
+ break;
+ case LocalAxisDirection.NegativeY:
+ axisIndex = 1;
+ sign = -1;
+ break;
+ case LocalAxisDirection.PositiveZ:
+ axisIndex = 2;
+ sign = 1;
+ break;
+ case LocalAxisDirection.NegativeZ:
+ axisIndex = 2;
+ sign = -1;
+ break;
+ default:
+ throw new ArgumentOutOfRangeException(nameof(axisDirection), axisDirection, null);
+ }
+ }
+
+ private static Vector3D GetFallbackAxis(LocalAxisDirection axisDirection)
+ {
+ switch (axisDirection)
+ {
+ case LocalAxisDirection.PositiveX:
+ return new Vector3D(1, 0, 0);
+ case LocalAxisDirection.NegativeX:
+ return new Vector3D(-1, 0, 0);
+ case LocalAxisDirection.PositiveY:
+ return new Vector3D(0, 1, 0);
+ case LocalAxisDirection.NegativeY:
+ return new Vector3D(0, -1, 0);
+ case LocalAxisDirection.PositiveZ:
+ return new Vector3D(0, 0, 1);
+ case LocalAxisDirection.NegativeZ:
+ return new Vector3D(0, 0, -1);
+ default:
+ throw new ArgumentOutOfRangeException(nameof(axisDirection), axisDirection, null);
+ }
+ }
+
private static double[] SolveLinearSystem3x3(double[,] matrix, double[] values)
{
double determinant =
@@ -538,13 +608,13 @@ namespace NavisworksTransport.Utils
actualBounds.Min.Z);
LogManager.Info(
- $"[模型增量姿态] {item.DisplayName} 应用后位置: " +
+ $"[模型增量姿态] {item.DisplayName} 立即读回位置(可能滞后): " +
$"实际=({actualPosition.X:F3},{actualPosition.Y:F3},{actualPosition.Z:F3}), " +
$"期望=({targetPosition.X:F3},{targetPosition.Y:F3},{targetPosition.Z:F3}), " +
$"偏差=({actualPosition.X - targetPosition.X:F3},{actualPosition.Y - targetPosition.Y:F3},{actualPosition.Z - targetPosition.Z:F3})");
LogManager.Info(
- $"[模型增量姿态] {item.DisplayName} 应用后旋转: " +
+ $"[模型增量姿态] {item.DisplayName} 立即读回旋转(可能滞后/不反映override): " +
$"实际X=({actualLinear.Get(0, 0):F4},{actualLinear.Get(1, 0):F4},{actualLinear.Get(2, 0):F4}), " +
$"实际Y=({actualLinear.Get(0, 1):F4},{actualLinear.Get(1, 1):F4},{actualLinear.Get(2, 1):F4}), " +
$"实际Z=({actualLinear.Get(0, 2):F4},{actualLinear.Get(1, 2):F4},{actualLinear.Get(2, 2):F4}), " +
From 7ccce8cf5a4c410a587f97d63d167a91d83cc17c Mon Sep 17 00:00:00 2001
From: tian <11429339@qq.com>
Date: Sat, 21 Mar 2026 21:58:11 +0800
Subject: [PATCH 11/87] Stabilize rail collision pose recovery and canonical
tracking
---
NavisworksTransport.UnitTests.csproj | 2 +
TransportPlugin.csproj | 3 +
.../CanonicalRailOffsetResolverTests.cs | 101 +++++++
.../CanonicalRailPoseBuilderTests.cs | 18 ++
.../CanonicalTrackedPositionResolverTests.cs | 65 +++++
deploy-plugin.bat | 23 ++
...oordinate-system-canonical-space-design.md | 53 ++++
doc/guide/design_principles.md | 24 ++
...tem-canonical-space-implementation-plan.md | 42 +++
.../GenerateCollisionReportCommand.cs | 33 ++-
src/Core/Animation/PathAnimationManager.cs | 249 ++++++++++++------
.../Collision/ClashDetectiveIntegration.cs | 35 +--
src/Core/PathDataManager.cs | 10 -
src/Core/PathDatabase.cs | 19 +-
src/Core/PathPlanningModels.cs | 12 +-
.../ViewModels/AnimationControlViewModel.cs | 5 +-
src/Utils/CollisionSceneHelper.cs | 31 +++
.../CanonicalRailOffsetResolver.cs | 48 ++++
.../CanonicalRailPoseBuilder.cs | 25 ++
.../CanonicalTrackedPositionResolver.cs | 43 +++
src/Utils/CoordinateSystem/RailLocalFrame.cs | 33 +++
src/Utils/ModelItemTransformHelper.cs | 127 ++++-----
src/Utils/RailPathPoseHelper.cs | 118 ++++-----
23 files changed, 837 insertions(+), 282 deletions(-)
create mode 100644 UnitTests/CoordinateSystem/CanonicalRailOffsetResolverTests.cs
create mode 100644 UnitTests/CoordinateSystem/CanonicalTrackedPositionResolverTests.cs
create mode 100644 src/Utils/CoordinateSystem/CanonicalRailOffsetResolver.cs
create mode 100644 src/Utils/CoordinateSystem/CanonicalTrackedPositionResolver.cs
create mode 100644 src/Utils/CoordinateSystem/RailLocalFrame.cs
diff --git a/NavisworksTransport.UnitTests.csproj b/NavisworksTransport.UnitTests.csproj
index c58c383..7832f72 100644
--- a/NavisworksTransport.UnitTests.csproj
+++ b/NavisworksTransport.UnitTests.csproj
@@ -55,6 +55,8 @@
+
+
diff --git a/TransportPlugin.csproj b/TransportPlugin.csproj
index 9db0354..adb5d43 100644
--- a/TransportPlugin.csproj
+++ b/TransportPlugin.csproj
@@ -332,11 +332,14 @@
+
+
+
diff --git a/UnitTests/CoordinateSystem/CanonicalRailOffsetResolverTests.cs b/UnitTests/CoordinateSystem/CanonicalRailOffsetResolverTests.cs
new file mode 100644
index 0000000..ca843f2
--- /dev/null
+++ b/UnitTests/CoordinateSystem/CanonicalRailOffsetResolverTests.cs
@@ -0,0 +1,101 @@
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using NavisworksTransport.Core;
+using NavisworksTransport.Utils.CoordinateSystem;
+using System.Numerics;
+
+namespace NavisworksTransport.UnitTests.CoordinateSystem
+{
+ [TestClass]
+ public class CanonicalRailOffsetResolverTests
+ {
+ [TestMethod]
+ public void OverRail_ShouldOffsetTrackedCenterAlongNormalByHalfHeight()
+ {
+ PathRoute route = new PathRoute
+ {
+ PathType = PathType.Rail,
+ RailMountMode = RailMountMode.OverRail,
+ RailPathDefinitionMode = RailPathDefinitionMode.RailCenterLine
+ };
+
+ RailLocalFrame frame = new RailLocalFrame(Vector3.UnitX, Vector3.UnitY, Vector3.UnitZ);
+ Vector3 trackedCenter = CanonicalRailOffsetResolver.ResolveTrackedCenter(
+ route,
+ Vector3.Zero,
+ frame,
+ 4.0);
+
+ AssertVector(trackedCenter, 0.0, 0.0, 2.0);
+ }
+
+ [TestMethod]
+ public void UnderRail_ShouldOffsetTrackedCenterOppositeToNormalByHalfHeight()
+ {
+ PathRoute route = new PathRoute
+ {
+ PathType = PathType.Rail,
+ RailMountMode = RailMountMode.UnderRail,
+ RailPathDefinitionMode = RailPathDefinitionMode.RailCenterLine
+ };
+
+ RailLocalFrame frame = new RailLocalFrame(Vector3.UnitX, Vector3.UnitY, Vector3.UnitZ);
+ Vector3 trackedCenter = CanonicalRailOffsetResolver.ResolveTrackedCenter(
+ route,
+ Vector3.Zero,
+ frame,
+ 4.0);
+
+ AssertVector(trackedCenter, 0.0, 0.0, -2.0);
+ }
+
+ [TestMethod]
+ public void SlopedNormal_ShouldMoveTrackedCenterAlongRailNormalInsteadOfWorldUp()
+ {
+ PathRoute route = new PathRoute
+ {
+ PathType = PathType.Rail,
+ RailMountMode = RailMountMode.OverRail,
+ RailPathDefinitionMode = RailPathDefinitionMode.RailCenterLine
+ };
+
+ Vector3 normal = Vector3.Normalize(new Vector3(0f, 1f, 1f));
+ RailLocalFrame frame = new RailLocalFrame(Vector3.UnitX, Vector3.UnitY, normal);
+
+ Vector3 trackedCenter = CanonicalRailOffsetResolver.ResolveTrackedCenter(
+ route,
+ new Vector3(10f, 20f, 30f),
+ frame,
+ 4.0);
+
+ Vector3 expected = new Vector3(10f, 20f, 30f) + normal * 2f;
+ AssertVector(trackedCenter, expected.X, expected.Y, expected.Z);
+ }
+
+ [TestMethod]
+ public void AnchorSemantic_ShouldOnlyUseHalfHeightOffset_WhenReferencePointIsAlreadyAnchor()
+ {
+ PathRoute route = new PathRoute
+ {
+ PathType = PathType.Rail,
+ RailMountMode = RailMountMode.OverRail,
+ RailPathDefinitionMode = RailPathDefinitionMode.RailCenterLine
+ };
+
+ RailLocalFrame frame = new RailLocalFrame(Vector3.UnitX, Vector3.UnitY, Vector3.UnitZ);
+ Vector3 trackedCenter = CanonicalRailOffsetResolver.ResolveTrackedCenter(
+ route,
+ Vector3.Zero,
+ frame,
+ 4.0);
+
+ AssertVector(trackedCenter, 0.0, 0.0, 2.0);
+ }
+
+ private static void AssertVector(Vector3 actual, double x, double y, double z)
+ {
+ Assert.AreEqual(x, actual.X, 1e-6);
+ Assert.AreEqual(y, actual.Y, 1e-6);
+ Assert.AreEqual(z, actual.Z, 1e-6);
+ }
+ }
+}
diff --git a/UnitTests/CoordinateSystem/CanonicalRailPoseBuilderTests.cs b/UnitTests/CoordinateSystem/CanonicalRailPoseBuilderTests.cs
index b337260..3e2fca1 100644
--- a/UnitTests/CoordinateSystem/CanonicalRailPoseBuilderTests.cs
+++ b/UnitTests/CoordinateSystem/CanonicalRailPoseBuilderTests.cs
@@ -64,6 +64,24 @@ namespace NavisworksTransport.UnitTests.CoordinateSystem
Assert.AreEqual(0.0, Vector3.Dot(lateral, up), 1e-6);
}
+ [TestMethod]
+ public void SlopedPath_ShouldCreateRailLocalFrameWithStableNormalCloseToCanonicalUp()
+ {
+ bool ok = CanonicalRailPoseBuilder.TryCreateLocalFrame(
+ new Vector3(0, 0, 0),
+ new Vector3(10, 0, 1),
+ new Vector3(20, 0, 2),
+ Vector3.UnitZ,
+ out RailLocalFrame frame);
+
+ Assert.IsTrue(ok);
+ Assert.IsTrue(Vector3.Dot(frame.Forward, Vector3.UnitX) > 0.99f);
+ Assert.IsTrue(Vector3.Dot(frame.Normal, Vector3.UnitZ) > 0.99f);
+ Assert.AreEqual(0.0, Vector3.Dot(frame.Forward, frame.Normal), 1e-6);
+ Assert.AreEqual(0.0, Vector3.Dot(frame.Forward, frame.Lateral), 1e-6);
+ Assert.AreEqual(0.0, Vector3.Dot(frame.Lateral, frame.Normal), 1e-6);
+ }
+
private static void AssertColumn(Matrix4x4 matrix, int column, double x, double y, double z)
{
switch (column)
diff --git a/UnitTests/CoordinateSystem/CanonicalTrackedPositionResolverTests.cs b/UnitTests/CoordinateSystem/CanonicalTrackedPositionResolverTests.cs
new file mode 100644
index 0000000..674e192
--- /dev/null
+++ b/UnitTests/CoordinateSystem/CanonicalTrackedPositionResolverTests.cs
@@ -0,0 +1,65 @@
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using NavisworksTransport.Utils.CoordinateSystem;
+using System.Numerics;
+
+namespace NavisworksTransport.UnitTests.CoordinateSystem
+{
+ [TestClass]
+ public class CanonicalTrackedPositionResolverTests
+ {
+ [TestMethod]
+ public void GroundReference_ShouldOffsetHalfHeightAlongUp()
+ {
+ Vector3 result = CanonicalTrackedPositionResolver.ResolveGroundTrackedCenter(
+ new Vector3(10f, 20f, 30f),
+ Vector3.UnitZ,
+ 4.0);
+
+ AssertVector(result, 10.0, 20.0, 32.0);
+ }
+
+ [TestMethod]
+ public void TopSuspensionReference_ShouldOffsetNegativeHalfHeightAlongUp()
+ {
+ Vector3 result = CanonicalTrackedPositionResolver.ResolveTopSuspensionTrackedCenter(
+ new Vector3(10f, 20f, 30f),
+ Vector3.UnitZ,
+ 4.0);
+
+ AssertVector(result, 10.0, 20.0, 28.0);
+ }
+
+ [TestMethod]
+ public void ArbitraryNormalReference_ShouldOffsetAlongProvidedNormal()
+ {
+ Vector3 normal = Vector3.Normalize(new Vector3(0f, 1f, 1f));
+ Vector3 result = CanonicalTrackedPositionResolver.ResolveCenterFromContactReference(
+ new Vector3(5f, 6f, 7f),
+ normal,
+ 4.0,
+ 0.0);
+
+ Vector3 expected = new Vector3(5f, 6f, 7f) + normal * 2f;
+ AssertVector(result, expected.X, expected.Y, expected.Z);
+ }
+
+ [TestMethod]
+ public void MidSurfaceFactor_ShouldProduceNoOffset()
+ {
+ Vector3 result = CanonicalTrackedPositionResolver.ResolveCenterFromContactReference(
+ new Vector3(1f, 2f, 3f),
+ Vector3.UnitZ,
+ 8.0,
+ 0.5);
+
+ AssertVector(result, 1.0, 2.0, 3.0);
+ }
+
+ private static void AssertVector(Vector3 actual, double x, double y, double z)
+ {
+ Assert.AreEqual(x, actual.X, 1e-6);
+ Assert.AreEqual(y, actual.Y, 1e-6);
+ Assert.AreEqual(z, actual.Z, 1e-6);
+ }
+ }
+}
diff --git a/deploy-plugin.bat b/deploy-plugin.bat
index eaf9e8a..9c39165 100644
--- a/deploy-plugin.bat
+++ b/deploy-plugin.bat
@@ -7,6 +7,22 @@ powershell -NoProfile -ExecutionPolicy Bypass -Command ^
"$ErrorActionPreference = 'Stop';" ^
"$targetDir = [System.IO.Path]::GetFullPath('%TARGET_DIR%');" ^
"$buildDir = [System.IO.Path]::GetFullPath('%BUILD_DIR%');" ^
+ "function Test-FileUnlocked([string]$path) {" ^
+ " if (-not (Test-Path $path)) { return $true }" ^
+ " try {" ^
+ " $stream = [System.IO.File]::Open($path, [System.IO.FileMode]::Open, [System.IO.FileAccess]::ReadWrite, [System.IO.FileShare]::None);" ^
+ " $stream.Close();" ^
+ " return $true;" ^
+ " } catch {" ^
+ " return $false;" ^
+ " }" ^
+ "}" ^
+ "function Wait-FileUnlocked([string]$path, [datetime]$deadline) {" ^
+ " while (-not (Test-FileUnlocked $path)) {" ^
+ " if ((Get-Date) -ge $deadline) { throw ('Target file still locked: ' + $path) }" ^
+ " Start-Sleep -Milliseconds 500;" ^
+ " }" ^
+ "}" ^
"" ^
"Get-Process Roamer -ErrorAction SilentlyContinue | Stop-Process -Force -ErrorAction SilentlyContinue;" ^
"$deadline = (Get-Date).AddSeconds(15);" ^
@@ -14,6 +30,13 @@ powershell -NoProfile -ExecutionPolicy Bypass -Command ^
" if ((Get-Date) -ge $deadline) { throw 'Roamer.exe still running after 15 seconds.' }" ^
" Start-Sleep -Milliseconds 500;" ^
"}" ^
+ "$keyTargetFiles = @(" ^
+ " (Join-Path $targetDir 'TransportPlugin.dll')," ^
+ " (Join-Path $targetDir 'geometry4Sharp.dll')," ^
+ " (Join-Path (Join-Path $targetDir 'resources') 'unit_cube.nwc')," ^
+ " (Join-Path (Join-Path $targetDir 'resources') 'unit_cylinder.nwc')" ^
+ ");" ^
+ "foreach ($keyFile in $keyTargetFiles) { Wait-FileUnlocked $keyFile $deadline }" ^
"" ^
"New-Item -ItemType Directory -Force -Path $targetDir | Out-Null;" ^
"Copy-Item (Join-Path $buildDir '*.dll') $targetDir -Force;" ^
diff --git a/doc/design/2026/coordinate-system-canonical-space-design.md b/doc/design/2026/coordinate-system-canonical-space-design.md
index 4ab4395..7aa02af 100644
--- a/doc/design/2026/coordinate-system-canonical-space-design.md
+++ b/doc/design/2026/coordinate-system-canonical-space-design.md
@@ -181,6 +181,59 @@ flowchart LR
3. 业务基准定义
4. 模型局部轴约定
+### 3.5 Rail 局部坐标系与动画跟踪点
+
+`Rail` 路径不能再只理解成“沿世界 up 做上下偏移”。
+
+对 `Rail` 来说,必须显式建立一套局部坐标系:
+
+- `Forward`
+ - 沿轨道前进方向
+- `Normal`
+ - 安装法向
+ - 可以是任意空间角度
+ - 但应尽量贴近项目 up
+- `Lateral`
+ - 由 `Normal x Forward` 或正交化后确定
+
+后续所有 `Rail` 相关计算都应建立在这套 `RailLocalFrame` 之上:
+
+- 物体姿态
+- 通行空间姿态
+- 路径参考点到动画跟踪点的偏移
+- 终点贴合语义
+
+### 3.6 动画跟踪点统一语义
+
+动画系统内部不应混用:
+
+- 底面中心
+- 顶面中心
+- 包围盒中心
+- 路径参考点
+
+统一规则应为:
+
+- **动画跟踪点 = 当前动画主链路使用的唯一位置语义**
+- 当前阶段建议统一为:**几何中心**
+
+这样做的原因是:
+
+- 中心点对三维旋转最中性
+- 不依赖“当前哪一面朝上”
+- 不依赖宿主 `Y-up / Z-up`
+- 物体姿态与碰撞恢复、截图回放更容易共用同一套语义
+
+不同路径的业务需求通过“参考点 -> 跟踪中心点”的显式偏移来表达:
+
+- 地面/吊装:通常沿 `+up / -up`
+- Rail:沿 `RailLocalFrame.Normal`
+
+这样:
+
+- 业务层只表达“路径参考点”和“法向偏移”
+- 动画层只认“中心点 + 姿态”
+
## 4. 核心设计原则
### 4.1 Navisworks 世界坐标统一视为外部坐标
diff --git a/doc/guide/design_principles.md b/doc/guide/design_principles.md
index 33a66bd..d3961d1 100644
--- a/doc/guide/design_principles.md
+++ b/doc/guide/design_principles.md
@@ -2521,6 +2521,30 @@ DialogHelper.ShowDialog(new MyDialog());
- 最终部署前应至少确认主项目完整构建成功
- 必要时校验关键 WPF 视图资源是否真的编入程序集
+### 额外经验 3:三维碰撞验证前禁止先重置宿主姿态
+
+对受 `PathAnimationManager` 控制的三维动画对象,`ClashDetective` 候选验证前不能先调用:
+
+- `doc.Models.ResetPermanentTransform(modelItems)`
+
+原因:
+
+- `PathAnimationManager` 的内部跟踪姿态记录的是“当前动画目标姿态”
+- 一旦先把宿主物体重置回 CAD 原始姿态,宿主真实姿态就会变成单位姿态
+- 如果随后仍复用 `PathAnimationManager.MoveAnimatedObjectToPose(...)`
+- 则它会把“缓存姿态”误当成当前姿态,导致增量旋转退化成单位旋转,只剩平移
+
+实际后果:
+
+- 动画结束后,碰撞检测汇总一启动,真实物体会被摆平或姿态跳变
+- 后续所有 `ClashDetective` 验证都在错误姿态上进行
+
+正确做法:
+
+- 对三维 `PAM` 主链路对象,直接复用 `MoveAnimatedObjectToPose(...)`
+- 不再在验证入口额外执行 `ResetPermanentTransform`
+- `ResetPermanentTransform` 只保留给旧二维/非 `PAM` 恢复分支
+
---
*本文档将随着项目发展持续更新,确保设计指导的有效性和实用性。*
diff --git a/doc/working/coordinate-system-canonical-space-implementation-plan.md b/doc/working/coordinate-system-canonical-space-implementation-plan.md
index fc7c3f9..cf3928a 100644
--- a/doc/working/coordinate-system-canonical-space-implementation-plan.md
+++ b/doc/working/coordinate-system-canonical-space-implementation-plan.md
@@ -242,6 +242,48 @@
- Rail 虚拟物体与真实模型在 Y-up / Z-up 项目中的姿态语义一致
- 真实物体起点贴合和动画第一帧使用同一份固定物理尺寸语义
- 不再允许从“已旋转后的当前 AABB”重新推导 Rail 物体高度
+- Rail 动画不再直接在业务层手写 `forward/up/offset`,而是复用基础工具
+- `ClashDetective` 三维候选验证必须直接复用 `PathAnimationManager` 主链路恢复,不允许先 `ResetPermanentTransform` 再复用 `PAM`
+
+### Task 7.2 新增 `RailLocalFrame`
+
+目标:
+
+- 将 `Rail` 局部坐标系正式提升为基础框架对象
+
+最低要求:
+
+- `Forward`
+- `Normal`
+- `Lateral`
+
+实施要求:
+
+- 先在纯数学层构建并测试
+- 再由 `RailPathPoseHelper`、动画、通行空间复用
+
+验收:
+
+- 不再由业务代码自己拼切向/法向/侧向
+- `Rail` 的姿态、法向偏移、通行空间统一使用同一套局部坐标系
+
+### Task 7.3 新增“参考点 -> 跟踪中心点”偏移解析器
+
+目标:
+
+- 将路径参考点到动画跟踪中心点的偏移,从业务代码中抽离成基础工具
+
+实施要求:
+
+- 先以几何中心为统一动画跟踪点
+- 地面/吊装路径使用宿主 up 语义
+- Rail 路径使用 `RailLocalFrame.Normal`
+- 先补单测再接业务
+
+验收:
+
+- `PathAnimationManager` 不再直接散落手写半高偏移
+- `AnimatedObjectTrackedPosition` 明确表示动画跟踪中心点
### Task 7.1 明确模型局部轴约定
diff --git a/src/Commands/GenerateCollisionReportCommand.cs b/src/Commands/GenerateCollisionReportCommand.cs
index 9f39b72..1eb3f4e 100644
--- a/src/Commands/GenerateCollisionReportCommand.cs
+++ b/src/Commands/GenerateCollisionReportCommand.cs
@@ -471,8 +471,11 @@ namespace NavisworksTransport.Commands
int successCount = 0;
// 保存动画物体原始状态(用于截图后恢复)
+ // 对受 PathAnimationManager 控制的对象,直接复用与 ClashDetective 相同的保存/恢复主链路,
+ // 避免报告生成额外引入第二套状态语义。
ModelItem animatedObject = null;
ModelItemTransformHelper.ObjectStateSnapshot savedObjectState = null;
+ bool restoreViaAnimationManager = false;
// 找到第一个有位置信息的碰撞来获取动画物体
foreach (var c in collisionData.AllCollisions)
@@ -480,7 +483,18 @@ namespace NavisworksTransport.Commands
if (c.Item1 != null && c.HasPositionInfo)
{
animatedObject = c.Item1;
- savedObjectState = CollisionSceneHelper.SaveAnimatedObjectState(animatedObject);
+ var pam = PathAnimationManager.GetInstance();
+ restoreViaAnimationManager = pam != null && pam.ControlsAnimatedObject(animatedObject);
+
+ if (restoreViaAnimationManager)
+ {
+ pam.SaveObjectState(animatedObject);
+ LogManager.Info(string.Format("已通过动画主链路保存物体状态: {0}", animatedObject.DisplayName));
+ }
+ else
+ {
+ savedObjectState = CollisionSceneHelper.SaveAnimatedObjectState(animatedObject);
+ }
break;
}
}
@@ -544,7 +558,19 @@ namespace NavisworksTransport.Commands
}
// 恢复动画物体到原始状态
- CollisionSceneHelper.RestoreAnimatedObjectState(animatedObject, savedObjectState);
+ if (animatedObject != null)
+ {
+ var pam = PathAnimationManager.GetInstance();
+ if (restoreViaAnimationManager && pam != null && pam.ControlsAnimatedObject(animatedObject))
+ {
+ pam.RestoreAnimatedObjectState(animatedObject);
+ LogManager.Info(string.Format("已通过动画主链路恢复物体状态: {0}", animatedObject.DisplayName));
+ }
+ else
+ {
+ CollisionSceneHelper.RestoreAnimatedObjectState(animatedObject, savedObjectState);
+ }
+ }
LogManager.Info(string.Format("已生成 {0}/{1} 个碰撞对视角截图", successCount, collisionData.AllCollisions.Count));
}
@@ -623,8 +649,7 @@ namespace NavisworksTransport.Commands
}
var bounds = animatedObject.BoundingBox();
- var adapter = CoordinateSystemManager.Instance.CreateHostAdapter();
- return ModelItemTransformHelper.GetHostBottomAnchorPoint(bounds, adapter);
+ return bounds?.Center ?? new Point3D(0, 0, 0);
}
diff --git a/src/Core/Animation/PathAnimationManager.cs b/src/Core/Animation/PathAnimationManager.cs
index e25d6c8..b1f5246 100644
--- a/src/Core/Animation/PathAnimationManager.cs
+++ b/src/Core/Animation/PathAnimationManager.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using System.Numerics;
using System.Text;
using System.Windows.Threading;
using Autodesk.Navisworks.Api;
@@ -429,7 +430,7 @@ namespace NavisworksTransport.Core.Animation
_hasTrackedRotation = true;
var originalBoundingBox = objectToRestore.BoundingBox();
- _trackedPosition = GetTrackedObjectPosition(objectToRestore, _route?.PathType == PathType.Rail && !isVirtual);
+ _trackedPosition = GetTrackedObjectPosition(objectToRestore);
string objectName = isVirtual ? "虚拟物体" : objectToRestore.DisplayName;
LogManager.Info($"[归位] {objectName} 已彻底恢复到原始位置, yaw={_currentYaw:F3}");
@@ -604,7 +605,7 @@ namespace NavisworksTransport.Core.Animation
_animatedObject = animatedObject;
_originalTransform = animatedObject.Transform;
_originalCenter = animatedObject.BoundingBox().Center;
- _trackedPosition = GetTrackedObjectPosition(animatedObject, _route?.PathType == PathType.Rail && !_isVirtualObject);
+ _trackedPosition = GetTrackedObjectPosition(animatedObject);
// 统一逻辑:从当前 Transform 中提取实际朝向(无论虚拟物体还是普通物体)
_currentYaw = ModelItemTransformHelper.GetYawFromTransform(_originalTransform);
@@ -657,17 +658,17 @@ namespace NavisworksTransport.Core.Animation
Point3D startPosition = _pathPoints[0];
if (_route.PathType == PathType.Hoisting)
{
- // 吊装路径:第一个路径点(起吊点)是地面位置,物体底面应该在这里
- // 不需要向下移动物体高度
- LogManager.Debug($"[移动到起点] 吊装路径:起吊点是地面位置,物体底面Z={startPosition.Z:F2}");
+ // 吊装路径:第一个路径点(起吊点)是地面位置,动画跟踪点统一使用几何中心
+ startPosition = ResolveGroundTrackedCenter(startPosition, GetAnimatedObjectHeight());
+ LogManager.Debug($"[移动到起点] 吊装路径:起吊点已转换为物体中心=({startPosition.X:F2},{startPosition.Y:F2},{startPosition.Z:F2})");
}
else if (_route.PathType == PathType.Rail)
{
double objectHeight = GetAnimatedObjectHeight();
Point3D previousPoint = _pathPoints[0];
Point3D nextPoint = _pathPoints.Count > 1 ? _pathPoints[1] : _pathPoints[0];
- startPosition = RailPathPoseHelper.ResolveBottomPosition(_route, startPosition, previousPoint, nextPoint, objectHeight);
- LogManager.Debug($"[移动到起点] Rail路径调整: 参考点=({_pathPoints[0].X:F2},{_pathPoints[0].Y:F2},{_pathPoints[0].Z:F2}), 物体底面=({startPosition.X:F2},{startPosition.Y:F2},{startPosition.Z:F2}), 物体高度={objectHeight:F2}, 安装={_route.RailMountMode}, 对接={(PathRoute.IsTopPayloadAnchorForMountMode(_route.RailMountMode) ? "顶面对接" : "底面对接")}, 偏移={_route.RailReferenceToAnchorOffset:F2}");
+ startPosition = RailPathPoseHelper.ResolveObjectSpaceCenterPosition(_route, startPosition, previousPoint, nextPoint, objectHeight);
+ LogManager.Debug($"[移动到起点] Rail路径调整: 参考点=({_pathPoints[0].X:F2},{_pathPoints[0].Y:F2},{_pathPoints[0].Z:F2}), 物体中心=({startPosition.X:F2},{startPosition.Y:F2},{startPosition.Z:F2}), 物体高度={objectHeight:F2}, 安装={_route.RailMountMode}, 对接={(PathRoute.IsTopPayloadAnchorForMountMode(_route.RailMountMode) ? "顶面对接" : "底面对接")}");
if (RailPathPoseHelper.TryCreateRailRotation(
previousPoint,
@@ -690,8 +691,9 @@ namespace NavisworksTransport.Core.Animation
}
else
{
- // 地面路径:points[0] 是地面位置,物体底面应该在这里
- LogManager.Debug($"[移动到起点] 地面路径: pos=({startPosition.X:F2},{startPosition.Y:F2},{startPosition.Z:F2})");
+ // 地面路径:路径点在接触面上,动画跟踪点统一使用几何中心
+ startPosition = ResolveGroundTrackedCenter(startPosition, GetAnimatedObjectHeight());
+ LogManager.Debug($"[移动到起点] 地面路径中心点=({startPosition.X:F2},{startPosition.Y:F2},{startPosition.Z:F2})");
}
// 使用 UpdateObjectPosition 统一处理移动和旋转
@@ -925,33 +927,25 @@ namespace NavisworksTransport.Core.Animation
if (segmentIndex == firstSegment)
{
- // 起吊段:从地面逐渐上升到悬挂点-物体高度
- // 进度0时(地面):物体底面在地面,不向下移动
- // 进度1时(悬挂点):物体顶面在悬挂点,物体底面=悬挂点-物体高度
- double heightOffset = segmentProgress * objectHeight;
- framePosition = new Point3D(framePosition.X, framePosition.Y, framePosition.Z - heightOffset);
- LogManager.Debug($"[吊装路径-起吊段] 进度={segmentProgress:F2}, 高度偏移={heightOffset:F2}, 物体底面Z={framePosition.Z:F2}");
+ framePosition = ResolveHoistingTrackedCenter(framePosition, objectHeight, segmentProgress);
+ LogManager.Debug($"[吊装路径-起吊段] 进度={segmentProgress:F2}, 物体中心=({framePosition.X:F2},{framePosition.Y:F2},{framePosition.Z:F2})");
}
else if (segmentIndex > firstSegment && segmentIndex < lastSegment)
{
- // 平移段(中间的所有线段):全程都是悬挂点,物体顶面在悬挂点
- framePosition = new Point3D(framePosition.X, framePosition.Y, framePosition.Z - objectHeight);
- LogManager.Debug($"[吊装路径-平移段] 线段{segmentIndex}, 进度={segmentProgress:F2}, 物体底面Z={framePosition.Z:F2}");
+ // 平移段(中间的所有线段):参考点位于顶部悬挂点
+ framePosition = ResolveHoistingTrackedCenter(framePosition, objectHeight, 1.0);
+ LogManager.Debug($"[吊装路径-平移段] 线段{segmentIndex}, 进度={segmentProgress:F2}, 物体中心=({framePosition.X:F2},{framePosition.Y:F2},{framePosition.Z:F2})");
}
else if (segmentIndex == lastSegment)
{
- // 下降段:从悬挂点-物体高度逐渐下降到地面
- // 进度0时(悬挂点):物体顶面在悬挂点,物体底面=悬挂点-物体高度
- // 进度1时(地面):物体底面在地面,不向下移动
- double heightOffset = (1 - segmentProgress) * objectHeight;
- framePosition = new Point3D(framePosition.X, framePosition.Y, framePosition.Z - heightOffset);
- LogManager.Debug($"[吊装路径-下降段] 进度={segmentProgress:F2}, 高度偏移={heightOffset:F2}, 物体底面Z={framePosition.Z:F2}");
+ framePosition = ResolveHoistingTrackedCenter(framePosition, objectHeight, 1.0 - segmentProgress);
+ LogManager.Debug($"[吊装路径-下降段] 进度={segmentProgress:F2}, 物体中心=({framePosition.X:F2},{framePosition.Y:F2},{framePosition.Z:F2})");
}
}
else if (_route.PathType == PathType.Rail)
{
- framePosition = RailPathPoseHelper.ResolveBottomPosition(_route, framePosition, p1, p2, objectHeight);
- LogManager.Debug($"[Rail路径] 调整物体位置: 参考点=({p1.X:F2},{p1.Y:F2},{p1.Z:F2})->({p2.X:F2},{p2.Y:F2},{p2.Z:F2}), 物体底面=({framePosition.X:F2},{framePosition.Y:F2},{framePosition.Z:F2}), 安装={_route.RailMountMode}, 对接={(PathRoute.IsTopPayloadAnchorForMountMode(_route.RailMountMode) ? "顶面对接" : "底面对接")}");
+ framePosition = RailPathPoseHelper.ResolveObjectSpaceCenterPosition(_route, framePosition, p1, p2, objectHeight);
+ LogManager.Debug($"[Rail路径] 调整物体位置: 参考点=({p1.X:F2},{p1.Y:F2},{p1.Z:F2})->({p2.X:F2},{p2.Y:F2},{p2.Z:F2}), 物体中心=({framePosition.X:F2},{framePosition.Y:F2},{framePosition.Z:F2}), 安装={_route.RailMountMode}, 对接={(PathRoute.IsTopPayloadAnchorForMountMode(_route.RailMountMode) ? "顶面对接" : "底面对接")}");
}
}
else
@@ -977,6 +971,8 @@ namespace NavisworksTransport.Core.Animation
framePosition = InterpolateOnArcEdge(edge, edgeProgress);
}
+ framePosition = ResolveGroundTrackedCenter(framePosition, GetAnimatedObjectHeight());
+
yawRadians = ComputeYawOnPath(i, allSampledPoints, edgeIndex, edgeProgress);
previousFramePoint = framePosition;
nextFramePoint = framePosition;
@@ -1023,19 +1019,19 @@ namespace NavisworksTransport.Core.Animation
// 计算偏移量(当前包围盒中心到framePosition的偏移)
var offsetX = framePosition.X - originalCenter.X;
var offsetY = framePosition.Y - originalCenter.Y;
- var offsetZ = framePosition.Z - currentObjectBoundingBox.Min.Z; // Z方向:framePosition是底面,originalCenter是中心
+ var offsetZ = framePosition.Z - originalCenter.Z;
- // 创建新的包围盒(将当前包围盒移动到framePosition)
+ // 创建新的包围盒(将当前包围盒中心移动到framePosition)
var virtualBoundingBox = new BoundingBox3D(
new Point3D(
currentObjectBoundingBox.Min.X + offsetX,
currentObjectBoundingBox.Min.Y + offsetY,
- framePosition.Z // 底面Z坐标
+ currentObjectBoundingBox.Min.Z + offsetZ
),
new Point3D(
currentObjectBoundingBox.Max.X + offsetX,
currentObjectBoundingBox.Max.Y + offsetY,
- framePosition.Z + (currentObjectBoundingBox.Max.Z - currentObjectBoundingBox.Min.Z) // 保持高度
+ currentObjectBoundingBox.Max.Z + offsetZ
)
);
@@ -1112,18 +1108,12 @@ namespace NavisworksTransport.Core.Animation
(virtualBoundingBox.Min.Y + virtualBoundingBox.Max.Y + colliderBox.Min.Y + colliderBox.Max.Y) / 4,
(virtualBoundingBox.Min.Z + virtualBoundingBox.Max.Z + colliderBox.Min.Z + colliderBox.Max.Z) / 4
),
- // 碰撞结果中的 AnimatedObjectTrackedPosition 必须与后续恢复链路的动画跟踪点语义一致:
- // 1. 二维 yaw 路径沿用成熟旧逻辑,记录包围盒中心;
- // 2. 三维 customRotation 路径记录动画跟踪点(framePosition)。
- AnimatedObjectTrackedPosition = frame.HasCustomRotation
- ? new Point3D(
- framePosition.X,
- framePosition.Y,
- framePosition.Z)
- : new Point3D(
- framePosition.X,
- framePosition.Y,
- framePosition.Z + (virtualBoundingBox.Max.Z - virtualBoundingBox.Min.Z) / 2),
+ // AnimatedObjectTrackedPosition 统一定义为动画主链路的跟踪点位置。
+ // 当前动画主链路已统一使用几何中心作为跟踪点,因此所有路径都直接记录 framePosition。
+ AnimatedObjectTrackedPosition = new Point3D(
+ framePosition.X,
+ framePosition.Y,
+ framePosition.Z),
Item2Position = GetObjectPosition(collider),
AnimatedObjectTrackedYawRadians = actualYawRadians, // 记录动画跟踪点对应的运动物体实际播放朝向
AnimatedObjectTrackedRotation = frame.HasCustomRotation ? frame.Rotation : Rotation3D.Identity,
@@ -1133,7 +1123,7 @@ namespace NavisworksTransport.Core.Animation
LogManager.Debug(
$"[保存碰撞结果] 帧={i}, " +
- $"位置语义={(frame.HasCustomRotation ? "轨迹跟踪点" : "包围盒中心")}, " +
+ $"位置语义=动画跟踪中心点, " +
$"原始Yaw={yawRadians * 180.0 / Math.PI:F2}°, " +
$"实际Yaw={actualYawRadians * 180.0 / Math.PI:F2}°, " +
$"customRotation={frame.HasCustomRotation}, " +
@@ -1152,6 +1142,15 @@ namespace NavisworksTransport.Core.Animation
var totalCollisions = _animationFrames.Sum(f => f.Collisions.Count);
// 检查碰撞结果是否包含位置信息
var collisionsWithPosition = _allCollisionResults.Count(c => c.HasPositionInfo && c.AnimatedObjectTrackedPosition != null);
+
+ if (_route.PathType == PathType.Rail && _animationFrames.Count > 0)
+ {
+ AnimationFrame firstRailFrame = _animationFrames[0];
+ AnimationFrame lastRailFrame = _animationFrames[_animationFrames.Count - 1];
+ LogManager.Info(
+ $"[Rail帧诊断] 首帧跟踪中心=({firstRailFrame.Position.X:F3},{firstRailFrame.Position.Y:F3},{firstRailFrame.Position.Z:F3}), " +
+ $"末帧跟踪中心=({lastRailFrame.Position.X:F3},{lastRailFrame.Position.Y:F3},{lastRailFrame.Position.Z:F3}), 总帧数={_animationFrames.Count}");
+ }
LogManager.Info($"=== 预计算完成 ===");
LogManager.Info($"总帧数: {_animationFrames.Count}");
@@ -1724,9 +1723,6 @@ namespace NavisworksTransport.Core.Animation
LogManager.Info("动画播放自然结束,物体保持在最终位置");
LogRailEndAlignmentDiagnostics();
- // 直接设置为完成状态,避免中间状态切换
- SetState(AnimationState.Finished);
-
// 清除所有高亮(包括物体和碰撞)
ModelHighlightHelper.ClearAllHighlights();
LogManager.Info("动画完成:已清除所有高亮");
@@ -1735,12 +1731,6 @@ namespace NavisworksTransport.Core.Animation
var actualTotalFrames = _animationFrames?.Count ?? 0;
LogManager.Info($"动画播放完成,总帧数: {actualTotalFrames}, 平均FPS: {_actualFPS:F1}");
- // 更新 TimeLiner 任务状态
- if (_timeLinerManager != null && !string.IsNullOrEmpty(_currentTaskId))
- {
- _timeLinerManager.UpdateTaskProgress(_currentTaskId, 1.0, AnimationState.Finished);
- }
-
// 🔥 检查是否使用历史记录,如果是则跳过ClashDetective测试
if (_isUsingHistoryRecord)
{
@@ -1789,6 +1779,16 @@ namespace NavisworksTransport.Core.Animation
LogManager.Info("碰撞检测被取消");
}
}
+
+ // 后处理全部完成后再进入 Finished。
+ // 否则 UI 会在碰撞汇总仍在运行时启动报告生成,和当前物体恢复链路并发,导致结束后姿态被再次改写。
+ SetState(AnimationState.Finished);
+
+ // 更新 TimeLiner 任务状态
+ if (_timeLinerManager != null && !string.IsNullOrEmpty(_currentTaskId))
+ {
+ _timeLinerManager.UpdateTaskProgress(_currentTaskId, 1.0, AnimationState.Finished);
+ }
}
catch (Exception ex)
{
@@ -1819,7 +1819,7 @@ namespace NavisworksTransport.Core.Animation
Point3D finalTrackedPoint = _trackedPosition;
double objectHeight = GetAnimatedObjectHeight();
- Point3D expectedTrackedEndPoint = RailPathPoseHelper.ResolveBottomPosition(
+ Point3D expectedTrackedEndPoint = RailPathPoseHelper.ResolveObjectSpaceCenterPosition(
_route,
terminalAnchorPoint,
previousAnchorPoint,
@@ -2318,6 +2318,7 @@ namespace NavisworksTransport.Core.Animation
{
try
{
+ bool isRailRealObject = _route?.PathType == PathType.Rail && !_isVirtualObject && _animatedObject != null;
if (_isVirtualObject)
{
VirtualObjectManager.Instance.MoveVirtualObject(newPosition, newRotation);
@@ -2330,10 +2331,10 @@ namespace NavisworksTransport.Core.Animation
: _animatedObject.Transform.Factor().Rotation;
try
{
- var actualHostPosition = GetTrackedObjectPosition(_animatedObject, _route?.PathType == PathType.Rail && !_isVirtualObject);
+ var actualHostPosition = GetTrackedObjectPosition(_animatedObject);
currentPositionForTransform = actualHostPosition;
- LogManager.Info(
- $"[动画姿态入口] {_animatedObject.DisplayName} 宿主即时读回点=({actualHostPosition.X:F3},{actualHostPosition.Y:F3},{actualHostPosition.Z:F3})");
+ LogManager.Debug(
+ $"[动画姿态入口] {_animatedObject.DisplayName} 宿主即时读回点=({actualHostPosition.X:F3},{actualHostPosition.Y:F3},{actualHostPosition.Z:F3})");
}
catch (Exception ex)
{
@@ -2341,19 +2342,19 @@ namespace NavisworksTransport.Core.Animation
}
var currentLinear = new Transform3D(currentRotation).Linear;
var targetLinear = new Transform3D(newRotation).Linear;
- LogManager.Info(
- $"[动画姿态入口] {_animatedObject.DisplayName} 跟踪点=({_trackedPosition.X:F3},{_trackedPosition.Y:F3},{_trackedPosition.Z:F3}), " +
- $"目标点=({newPosition.X:F3},{newPosition.Y:F3},{newPosition.Z:F3})");
- LogManager.Info(
- $"[动画姿态入口] {_animatedObject.DisplayName} 跟踪姿态: " +
- $"X=({currentLinear.Get(0, 0):F4},{currentLinear.Get(1, 0):F4},{currentLinear.Get(2, 0):F4}), " +
- $"Y=({currentLinear.Get(0, 1):F4},{currentLinear.Get(1, 1):F4},{currentLinear.Get(2, 1):F4}), " +
- $"Z=({currentLinear.Get(0, 2):F4},{currentLinear.Get(1, 2):F4},{currentLinear.Get(2, 2):F4})");
- LogManager.Info(
- $"[动画姿态入口] {_animatedObject.DisplayName} 目标姿态: " +
- $"X=({targetLinear.Get(0, 0):F4},{targetLinear.Get(1, 0):F4},{targetLinear.Get(2, 0):F4}), " +
- $"Y=({targetLinear.Get(0, 1):F4},{targetLinear.Get(1, 1):F4},{targetLinear.Get(2, 1):F4}), " +
- $"Z=({targetLinear.Get(0, 2):F4},{targetLinear.Get(1, 2):F4},{targetLinear.Get(2, 2):F4})");
+ LogManager.Debug(
+ $"[动画姿态入口] {_animatedObject.DisplayName} 跟踪点=({_trackedPosition.X:F3},{_trackedPosition.Y:F3},{_trackedPosition.Z:F3}), " +
+ $"目标点=({newPosition.X:F3},{newPosition.Y:F3},{newPosition.Z:F3})");
+ LogManager.Debug(
+ $"[动画姿态入口] {_animatedObject.DisplayName} 跟踪姿态: " +
+ $"X=({currentLinear.Get(0, 0):F4},{currentLinear.Get(1, 0):F4},{currentLinear.Get(2, 0):F4}), " +
+ $"Y=({currentLinear.Get(0, 1):F4},{currentLinear.Get(1, 1):F4},{currentLinear.Get(2, 1):F4}), " +
+ $"Z=({currentLinear.Get(0, 2):F4},{currentLinear.Get(1, 2):F4},{currentLinear.Get(2, 2):F4})");
+ LogManager.Debug(
+ $"[动画姿态入口] {_animatedObject.DisplayName} 目标姿态: " +
+ $"X=({targetLinear.Get(0, 0):F4},{targetLinear.Get(1, 0):F4},{targetLinear.Get(2, 0):F4}), " +
+ $"Y=({targetLinear.Get(0, 1):F4},{targetLinear.Get(1, 1):F4},{targetLinear.Get(2, 1):F4}), " +
+ $"Z=({targetLinear.Get(0, 2):F4},{targetLinear.Get(1, 2):F4},{targetLinear.Get(2, 2):F4})");
ModelItemTransformHelper.MoveItemIncrementallyToPositionAndRotation(
_animatedObject,
currentPositionForTransform,
@@ -2370,6 +2371,15 @@ namespace NavisworksTransport.Core.Animation
_trackedRotation = newRotation;
_hasTrackedRotation = true;
_currentYaw = ModelItemTransformHelper.GetYawFromRotation(newRotation);
+
+ if (isRailRealObject)
+ {
+ Point3D hostActualAfter = GetTrackedObjectPosition(_animatedObject);
+ LogManager.Debug(
+ $"[Rail姿态应用] 宿主最终跟踪中心=({hostActualAfter.X:F3},{hostActualAfter.Y:F3},{hostActualAfter.Z:F3}), " +
+ $"目标跟踪中心=({newPosition.X:F3},{newPosition.Y:F3},{newPosition.Z:F3}), " +
+ $"偏差=({hostActualAfter.X - newPosition.X:F3},{hostActualAfter.Y - newPosition.Y:F3},{hostActualAfter.Z - newPosition.Z:F3})");
+ }
}
catch (Exception ex)
{
@@ -2492,6 +2502,16 @@ namespace NavisworksTransport.Core.Animation
_savedObjectHasCustomRotation = _hasTrackedRotation;
_hasSavedObjectState = true;
LogManager.Info($"已保存物体状态: pos=({position.X:F2},{position.Y:F2},{position.Z:F2}), yaw={yaw * 180 / Math.PI:F2}度, customRotation={_savedObjectHasCustomRotation}");
+ if (_savedObjectHasCustomRotation)
+ {
+ var linear = new Transform3D(_savedObjectRotation).Linear;
+ LogManager.Info(
+ $"[PAM保存姿态] {obj.DisplayName} 保存目标姿态: " +
+ $"位置=({_savedObjectPosition.X:F3},{_savedObjectPosition.Y:F3},{_savedObjectPosition.Z:F3}), " +
+ $"X=({linear.Get(0, 0):F4},{linear.Get(1, 0):F4},{linear.Get(2, 0):F4}), " +
+ $"Y=({linear.Get(0, 1):F4},{linear.Get(1, 1):F4},{linear.Get(2, 1):F4}), " +
+ $"Z=({linear.Get(0, 2):F4},{linear.Get(1, 2):F4},{linear.Get(2, 2):F4})");
+ }
}
catch (Exception ex)
{
@@ -2522,10 +2542,18 @@ namespace NavisworksTransport.Core.Animation
var originalAnimatedObject = _animatedObject;
_animatedObject = obj;
- _trackedPosition = _savedObjectPosition;
- _currentYaw = _savedObjectYaw;
- _trackedRotation = _savedObjectRotation;
- _hasTrackedRotation = _savedObjectHasCustomRotation;
+ LogHostActualPoseAxes(obj, "[PAM恢复前宿主姿态]", false);
+
+ if (_savedObjectHasCustomRotation)
+ {
+ var linear = new Transform3D(_savedObjectRotation).Linear;
+ LogManager.Info(
+ $"[PAM恢复姿态] {obj.DisplayName} 恢复目标姿态: " +
+ $"位置=({_savedObjectPosition.X:F3},{_savedObjectPosition.Y:F3},{_savedObjectPosition.Z:F3}), " +
+ $"X=({linear.Get(0, 0):F4},{linear.Get(1, 0):F4},{linear.Get(2, 0):F4}), " +
+ $"Y=({linear.Get(0, 1):F4},{linear.Get(1, 1):F4},{linear.Get(2, 1):F4}), " +
+ $"Z=({linear.Get(0, 2):F4},{linear.Get(1, 2):F4},{linear.Get(2, 2):F4})");
+ }
if (_savedObjectHasCustomRotation)
{
@@ -2536,6 +2564,13 @@ namespace NavisworksTransport.Core.Animation
UpdateObjectPosition(_savedObjectPosition, _savedObjectYaw);
}
+ _trackedPosition = _savedObjectPosition;
+ _currentYaw = _savedObjectYaw;
+ _trackedRotation = _savedObjectRotation;
+ _hasTrackedRotation = _savedObjectHasCustomRotation;
+
+ LogHostActualPoseAxes(obj, "[PAM恢复后宿主姿态]", false);
+
_animatedObject = originalAnimatedObject;
LogManager.Info($"已恢复物体状态: pos=({_savedObjectPosition.X:F2},{_savedObjectPosition.Y:F2},{_savedObjectPosition.Z:F2}), yaw={_savedObjectYaw * 180 / Math.PI:F2}度, customRotation={_savedObjectHasCustomRotation}");
}
@@ -2596,6 +2631,8 @@ namespace NavisworksTransport.Core.Animation
_animatedObject = obj;
}
+ LogHostActualPoseAxes(obj, "[动画姿态复用前宿主姿态]", false);
+
if (hasCustomRotation)
{
UpdateObjectPosition(targetPosition, targetRotation);
@@ -2605,6 +2642,8 @@ namespace NavisworksTransport.Core.Animation
UpdateObjectPosition(targetPosition, targetYawRadians);
}
+ LogHostActualPoseAxes(obj, "[动画姿态复用后宿主姿态]", false);
+
_animatedObject = originalAnimatedObject;
LogManager.Info(
@@ -2618,6 +2657,37 @@ namespace NavisworksTransport.Core.Animation
}
}
+ private void LogHostActualPoseAxes(ModelItem obj, string prefix, bool important)
+ {
+ if (obj == null)
+ {
+ return;
+ }
+
+ try
+ {
+ Transform3D transform = obj.Transform;
+ Matrix3 linear = transform.Linear;
+ string message =
+ $"{prefix} {obj.DisplayName}: " +
+ $"X=({linear.Get(0, 0):F4},{linear.Get(1, 0):F4},{linear.Get(2, 0):F4}), " +
+ $"Y=({linear.Get(0, 1):F4},{linear.Get(1, 1):F4},{linear.Get(2, 1):F4}), " +
+ $"Z=({linear.Get(0, 2):F4},{linear.Get(1, 2):F4},{linear.Get(2, 2):F4})";
+ if (important)
+ {
+ LogManager.Info(message);
+ }
+ else
+ {
+ LogManager.Debug(message);
+ }
+ }
+ catch (Exception ex)
+ {
+ LogManager.Warning($"{prefix} 读取失败: {ex.Message}");
+ }
+ }
+
///
/// 获取播放方向(1=正向,-1=反向)
///
@@ -3180,7 +3250,7 @@ namespace NavisworksTransport.Core.Animation
{
_originalTransform = animatedObject.Transform;
_originalCenter = animatedObject.BoundingBox().Center;
- _trackedPosition = GetTrackedObjectPosition(animatedObject, _route?.PathType == PathType.Rail && !_isVirtualObject);
+ _trackedPosition = GetTrackedObjectPosition(animatedObject);
// 保持当前的 _currentYaw(因为物体可能已经被 MoveObjectToPathStart 旋转)
// 不要从 Transform 中提取,因为 Transform 返回的是原始值,不是当前值
@@ -3197,7 +3267,7 @@ namespace NavisworksTransport.Core.Animation
LogManager.Info($"[CreateAnimation] 动画已创建,状态设置为Ready");
}
- private Point3D GetTrackedObjectPosition(ModelItem item, bool useStableRailAnchor)
+ private Point3D GetTrackedObjectPosition(ModelItem item)
{
if (item == null)
{
@@ -3205,8 +3275,35 @@ namespace NavisworksTransport.Core.Animation
}
var bounds = item.BoundingBox();
+ return bounds?.Center ?? new Point3D(0, 0, 0);
+ }
+
+ private Point3D ResolveGroundTrackedCenter(Point3D groundContactPoint, double objectHeight)
+ {
var adapter = CoordinateSystemManager.Instance.CreateHostAdapter();
- return ModelItemTransformHelper.GetHostBottomAnchorPoint(bounds, adapter);
+ var canonicalReferencePoint = ToNumerics(adapter.ToCanonicalPoint(groundContactPoint));
+ var canonicalCenter = CanonicalTrackedPositionResolver.ResolveGroundTrackedCenter(
+ canonicalReferencePoint,
+ HostCoordinateAdapter.CanonicalUpVector3,
+ objectHeight);
+ return adapter.FromCanonicalPoint(new Point3D(canonicalCenter.X, canonicalCenter.Y, canonicalCenter.Z));
+ }
+
+ private Point3D ResolveHoistingTrackedCenter(Point3D referencePoint, double objectHeight, double referenceContactFactor)
+ {
+ var adapter = CoordinateSystemManager.Instance.CreateHostAdapter();
+ var canonicalReferencePoint = ToNumerics(adapter.ToCanonicalPoint(referencePoint));
+ var canonicalCenter = CanonicalTrackedPositionResolver.ResolveCenterFromContactReference(
+ canonicalReferencePoint,
+ HostCoordinateAdapter.CanonicalUpVector3,
+ objectHeight,
+ referenceContactFactor);
+ return adapter.FromCanonicalPoint(new Point3D(canonicalCenter.X, canonicalCenter.Y, canonicalCenter.Z));
+ }
+
+ private static Vector3 ToNumerics(Point3D point)
+ {
+ return new Vector3((float)point.X, (float)point.Y, (float)point.Z);
}
///
diff --git a/src/Core/Collision/ClashDetectiveIntegration.cs b/src/Core/Collision/ClashDetectiveIntegration.cs
index 1e52bdb..ac693d4 100644
--- a/src/Core/Collision/ClashDetectiveIntegration.cs
+++ b/src/Core/Collision/ClashDetectiveIntegration.cs
@@ -801,29 +801,34 @@ namespace NavisworksTransport
var targetYaw = candidate.AnimatedObjectTrackedYawRadians;
var targetRotation = candidate.AnimatedObjectTrackedRotation;
- // 🔥 关键:为了最高精度,先回到CAD原始位置,再移动到目标位置
- // 避免累积误差影响碰撞检测精度
- doc.Models.ResetPermanentTransform(modelItems);
-
if (candidate.AnimatedObjectHasTrackedRotation && targetRotation != null)
{
- var adapter = CoordinateSystemManager.Instance.CreateHostAdapter();
- var originalBounds = testAnimatedObject.BoundingBox();
- var originalTrackedPosition = ModelItemTransformHelper.GetHostBottomAnchorPoint(originalBounds, adapter);
- var originalRotation = testAnimatedObject.Transform.Factor().Rotation;
LogManager.Info(
$"[ClashDetective验证] 三维候选恢复: " +
- $"当前跟踪点=({originalTrackedPosition.X:F3},{originalTrackedPosition.Y:F3},{originalTrackedPosition.Z:F3}), " +
+ $"对象={testAnimatedObject.DisplayName}, " +
$"目标跟踪点=({targetPosition.X:F3},{targetPosition.Y:F3},{targetPosition.Z:F3})");
- ModelItemTransformHelper.MoveItemIncrementallyToPositionAndRotation(
- testAnimatedObject,
- originalTrackedPosition,
- originalRotation,
- targetPosition,
- targetRotation);
+
+ var pam = PathAnimationManager.GetInstance();
+ if (pam != null && pam.ControlsAnimatedObject(testAnimatedObject))
+ {
+ pam.MoveAnimatedObjectToPose(
+ testAnimatedObject,
+ targetPosition,
+ targetYaw,
+ targetRotation,
+ true);
+ }
+ else
+ {
+ throw new InvalidOperationException(
+ $"ClashDetective三维候选恢复失败:对象 {testAnimatedObject.DisplayName} 不受当前动画管理器控制。");
+ }
}
else
{
+ // 旧二维候选验证仍沿用“回到 CAD 原始位置再应用增量”的成熟语义。
+ doc.Models.ResetPermanentTransform(modelItems);
+
var originalBounds = testAnimatedObject.BoundingBox();
var originalPos = new Point3D(
(originalBounds.Min.X + originalBounds.Max.X) / 2,
diff --git a/src/Core/PathDataManager.cs b/src/Core/PathDataManager.cs
index 36df9f2..17036f3 100644
--- a/src/Core/PathDataManager.cs
+++ b/src/Core/PathDataManager.cs
@@ -50,7 +50,6 @@ namespace NavisworksTransport
public string pathType { get; set; }
public string railMountMode { get; set; }
public string railPathDefinitionMode { get; set; }
- public double railReferenceToAnchorOffset { get; set; }
public double totalLength { get; set; }
public JsonObjectLimits objectLimits { get; set; }
public double gridSize { get; set; }
@@ -280,7 +279,6 @@ namespace NavisworksTransport
pathType = route.PathType.ToString(),
railMountMode = route.RailMountMode.ToString(),
railPathDefinitionMode = route.RailPathDefinitionMode.ToString(),
- railReferenceToAnchorOffset = Math.Round(route.RailReferenceToAnchorOffset, exportSettings?.Precision ?? 3),
totalLength = Math.Round(route.TotalLength, exportSettings?.Precision ?? 3),
objectLimits = new
{
@@ -530,8 +528,6 @@ namespace NavisworksTransport
route.RailPathDefinitionMode = railPathDefinitionMode;
}
- route.RailReferenceToAnchorOffset = jsonRoute.railReferenceToAnchorOffset;
-
// 解析路径点
var points = new List();
foreach (var jsonPoint in jsonRoute.points)
@@ -1391,7 +1387,6 @@ namespace NavisworksTransport
routeElement.SetAttribute("pathType", route.PathType.ToString());
routeElement.SetAttribute("railMountMode", route.RailMountMode.ToString());
routeElement.SetAttribute("railPathDefinitionMode", route.RailPathDefinitionMode.ToString());
- routeElement.SetAttribute("railReferenceToAnchorOffset", route.RailReferenceToAnchorOffset.ToString("F3"));
routeElement.SetAttribute("totalLength", route.TotalLength.ToString("F3"));
routeElement.SetAttribute("maxObjectLength", route.MaxObjectLength.ToString("F3"));
routeElement.SetAttribute("maxObjectWidth", route.MaxObjectWidth.ToString("F3"));
@@ -1534,11 +1529,6 @@ namespace NavisworksTransport
route.RailPathDefinitionMode = railPathDefinitionMode;
}
- if (double.TryParse(routeNode.Attributes?["railReferenceToAnchorOffset"]?.Value, out double railReferenceToAnchorOffset))
- {
- route.RailReferenceToAnchorOffset = railReferenceToAnchorOffset;
- }
-
// 解析创建时间
if (DateTime.TryParse(routeNode.Attributes?["created"]?.Value, out DateTime createdTime))
{
diff --git a/src/Core/PathDatabase.cs b/src/Core/PathDatabase.cs
index 5b03432..44af0c6 100644
--- a/src/Core/PathDatabase.cs
+++ b/src/Core/PathDatabase.cs
@@ -101,7 +101,6 @@ namespace NavisworksTransport
LiftHeight REAL,
RailMountMode INTEGER,
RailPathDefinitionMode INTEGER,
- RailReferenceToAnchorOffset REAL,
CreatedTime DATETIME,
LastModified DATETIME
)
@@ -300,8 +299,8 @@ namespace NavisworksTransport
// 12. 设置数据库版本(SQLite内置user_version)
// 版本号格式:主版本*10000 + 次版本*100 + 修订号
- // 例如:2.1.5 = 20105
- ExecuteNonQuery("PRAGMA user_version = 20105"); // v2.1.5 - 碰撞对象位姿字段命名统一为动画跟踪点语义
+ // 例如: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)");
@@ -464,8 +463,8 @@ namespace NavisworksTransport
// 路径长度由 PathRoute.TotalLength 计算属性实时从 Edges/Points 计算
var sql = @"
INSERT OR REPLACE INTO PathRoutes
- (Id, Name, EstimatedTime, TurnRadius, IsCurved, MaxObjectLength, MaxObjectWidth, MaxObjectHeight, SafetyMargin, GridSize, PathType, LiftHeight, RailMountMode, RailPathDefinitionMode, RailReferenceToAnchorOffset, CreatedTime, LastModified)
- VALUES (@id, @name, @time, @turnRadius, @isCurved, @maxLength, @maxWidth, @maxHeight, @safetyMargin, @gridSize, @pathType, @liftHeightMeters, @railMountMode, @railPathDefinitionMode, @railReferenceToAnchorOffset, @created, @modified)
+ (Id, Name, EstimatedTime, TurnRadius, IsCurved, MaxObjectLength, MaxObjectWidth, MaxObjectHeight, SafetyMargin, GridSize, PathType, LiftHeight, RailMountMode, RailPathDefinitionMode, CreatedTime, LastModified)
+ VALUES (@id, @name, @time, @turnRadius, @isCurved, @maxLength, @maxWidth, @maxHeight, @safetyMargin, @gridSize, @pathType, @liftHeightMeters, @railMountMode, @railPathDefinitionMode, @created, @modified)
";
using (var cmd = new SQLiteCommand(sql, _connection))
@@ -484,7 +483,6 @@ namespace NavisworksTransport
cmd.Parameters.AddWithValue("@liftHeightMeters", route.LiftHeight);
cmd.Parameters.AddWithValue("@railMountMode", (int)route.RailMountMode);
cmd.Parameters.AddWithValue("@railPathDefinitionMode", (int)route.RailPathDefinitionMode);
- cmd.Parameters.AddWithValue("@railReferenceToAnchorOffset", route.RailReferenceToAnchorOffset);
cmd.Parameters.AddWithValue("@created", route.CreatedTime);
cmd.Parameters.AddWithValue("@modified", DateTime.Now);
cmd.ExecuteNonQuery();
@@ -1542,7 +1540,6 @@ namespace NavisworksTransport
LiftHeight = Convert.ToDouble(reader["LiftHeight"]),
RailMountMode = (RailMountMode)Convert.ToInt32(reader["RailMountMode"]),
RailPathDefinitionMode = (RailPathDefinitionMode)Convert.ToInt32(reader["RailPathDefinitionMode"]),
- RailReferenceToAnchorOffset = Convert.ToDouble(reader["RailReferenceToAnchorOffset"]),
CreatedTime = Convert.ToDateTime(reader["CreatedTime"]),
LastModified = Convert.ToDateTime(reader["LastModified"])
};
@@ -2724,7 +2721,7 @@ namespace NavisworksTransport
cmd.CommandText = @"
SELECT Id, Name, CreatedTime, LastModified,
TurnRadius, IsCurved, MaxObjectLength, MaxObjectWidth, MaxObjectHeight,
- SafetyMargin, GridSize, PathType, LiftHeight, RailMountMode, RailPathDefinitionMode, RailReferenceToAnchorOffset
+ SafetyMargin, GridSize, PathType, LiftHeight, RailMountMode, RailPathDefinitionMode
FROM PathRoutes
WHERE Id = @Id";
@@ -2750,8 +2747,7 @@ namespace NavisworksTransport
PathType = (PathType)Convert.ToInt32(reader["PathType"]),
LiftHeight = Convert.ToDouble(reader["LiftHeight"]),
RailMountMode = (RailMountMode)Convert.ToInt32(reader["RailMountMode"]),
- RailPathDefinitionMode = (RailPathDefinitionMode)Convert.ToInt32(reader["RailPathDefinitionMode"]),
- RailReferenceToAnchorOffset = Convert.ToDouble(reader["RailReferenceToAnchorOffset"])
+ RailPathDefinitionMode = (RailPathDefinitionMode)Convert.ToInt32(reader["RailPathDefinitionMode"])
};
// 加载路径点和路径边
@@ -2788,8 +2784,7 @@ namespace NavisworksTransport
var columnsToAdd = new Dictionary
{
["railmountmode"] = "ALTER TABLE PathRoutes ADD COLUMN RailMountMode INTEGER DEFAULT 0",
- ["railpathdefinitionmode"] = "ALTER TABLE PathRoutes ADD COLUMN RailPathDefinitionMode INTEGER DEFAULT 0",
- ["railreferencetoanchoroffset"] = "ALTER TABLE PathRoutes ADD COLUMN RailReferenceToAnchorOffset REAL DEFAULT 0"
+ ["railpathdefinitionmode"] = "ALTER TABLE PathRoutes ADD COLUMN RailPathDefinitionMode INTEGER DEFAULT 0"
};
foreach (var column in columnsToAdd)
diff --git a/src/Core/PathPlanningModels.cs b/src/Core/PathPlanningModels.cs
index 188f8a2..e3ba6bb 100644
--- a/src/Core/PathPlanningModels.cs
+++ b/src/Core/PathPlanningModels.cs
@@ -737,13 +737,6 @@ namespace NavisworksTransport
///
public RailPathDefinitionMode RailPathDefinitionMode { get; set; }
- ///
- /// Rail 参考线到构件对接点的偏移距离(模型单位)
- /// 在 LegacySuspensionPoint 模式下通常为 0。
- /// 在 RailCenterLine 模式下,结合 RailMountMode 解释为相对轨道中心线的上下偏移。
- ///
- public double RailReferenceToAnchorOffset { get; set; }
-
// 数据库分析相关属性
///
/// 碰撞数量(从数据库加载)
@@ -784,7 +777,6 @@ namespace NavisworksTransport
IsCurved = false;
RailMountMode = RailMountMode.UnderRail;
RailPathDefinitionMode = RailPathDefinitionMode.LegacySuspensionPoint;
- RailReferenceToAnchorOffset = 0.0;
}
///
@@ -807,7 +799,6 @@ namespace NavisworksTransport
Description = string.Empty;
RailMountMode = RailMountMode.UnderRail;
RailPathDefinitionMode = RailPathDefinitionMode.LegacySuspensionPoint;
- RailReferenceToAnchorOffset = 0.0;
}
///
@@ -1042,8 +1033,7 @@ namespace NavisworksTransport
MaxObjectHeight = MaxObjectHeight,
SafetyMargin = SafetyMargin,
RailMountMode = RailMountMode,
- RailPathDefinitionMode = RailPathDefinitionMode,
- RailReferenceToAnchorOffset = RailReferenceToAnchorOffset
+ RailPathDefinitionMode = RailPathDefinitionMode
};
// 克隆所有路径点
diff --git a/src/UI/WPF/ViewModels/AnimationControlViewModel.cs b/src/UI/WPF/ViewModels/AnimationControlViewModel.cs
index e22a601..59c3aa1 100644
--- a/src/UI/WPF/ViewModels/AnimationControlViewModel.cs
+++ b/src/UI/WPF/ViewModels/AnimationControlViewModel.cs
@@ -1885,12 +1885,9 @@ namespace NavisworksTransport.UI.WPF.ViewModels
_clashIntegration.ClearWasLastTestCanceled();
break;
}
-
- // 先清除所有碰撞高亮(包括预计算和ClashDetective)
+
ModelHighlightHelper.ClearCollisionHighlights();
- // 生成碰撞报告(无论有无碰撞)并等待完成
await GenerateAndSaveCollisionReport();
- // 高亮ClashDetective结果(只在动画自然完成时进行,确保报告生成完成)
HighlightClashDetectiveResults();
break;
case NavisworksTransport.Core.Animation.AnimationState.Stopped:
diff --git a/src/Utils/CollisionSceneHelper.cs b/src/Utils/CollisionSceneHelper.cs
index ffe839b..2d2e7b4 100644
--- a/src/Utils/CollisionSceneHelper.cs
+++ b/src/Utils/CollisionSceneHelper.cs
@@ -118,6 +118,21 @@ namespace NavisworksTransport.Utils
LogManager.Info(string.Format("已保存动画物体状态: {0}, 位置=({1:F2},{2:F2},{3:F2}), 朝向={4:F2}°, customRotation={5}",
animatedObject.DisplayName, state.Position.X, state.Position.Y, state.Position.Z,
currentYaw * 180 / Math.PI, state.HasCustomRotation));
+ if (state.HasCustomRotation && state.Rotation != null)
+ {
+ var linear = new Transform3D(state.Rotation).Linear;
+ LogManager.Info(string.Format(
+ "[报告姿态保存] {0} 保存姿态: " +
+ "位置=({1:F3},{2:F3},{3:F3}), " +
+ "X=({4:F4},{5:F4},{6:F4}), " +
+ "Y=({7:F4},{8:F4},{9:F4}), " +
+ "Z=({10:F4},{11:F4},{12:F4})",
+ animatedObject.DisplayName,
+ state.Position.X, state.Position.Y, state.Position.Z,
+ linear.Get(0, 0), linear.Get(1, 0), linear.Get(2, 0),
+ linear.Get(0, 1), linear.Get(1, 1), linear.Get(2, 1),
+ linear.Get(0, 2), linear.Get(1, 2), linear.Get(2, 2)));
+ }
return state;
}
@@ -139,6 +154,22 @@ namespace NavisworksTransport.Utils
try
{
+ if (state.HasCustomRotation && state.Rotation != null)
+ {
+ var linear = new Transform3D(state.Rotation).Linear;
+ LogManager.Info(string.Format(
+ "[报告姿态恢复] {0} 恢复目标姿态: " +
+ "位置=({1:F3},{2:F3},{3:F3}), " +
+ "X=({4:F4},{5:F4},{6:F4}), " +
+ "Y=({7:F4},{8:F4},{9:F4}), " +
+ "Z=({10:F4},{11:F4},{12:F4})",
+ animatedObject.DisplayName,
+ state.Position.X, state.Position.Y, state.Position.Z,
+ linear.Get(0, 0), linear.Get(1, 0), linear.Get(2, 0),
+ linear.Get(0, 1), linear.Get(1, 1), linear.Get(2, 1),
+ linear.Get(0, 2), linear.Get(1, 2), linear.Get(2, 2)));
+ }
+
var pam = PathAnimationManager.GetInstance();
if (pam != null && pam.ControlsAnimatedObject(animatedObject))
{
diff --git a/src/Utils/CoordinateSystem/CanonicalRailOffsetResolver.cs b/src/Utils/CoordinateSystem/CanonicalRailOffsetResolver.cs
new file mode 100644
index 0000000..1b6e757
--- /dev/null
+++ b/src/Utils/CoordinateSystem/CanonicalRailOffsetResolver.cs
@@ -0,0 +1,48 @@
+using System;
+using System.Numerics;
+using NavisworksTransport.Core;
+
+namespace NavisworksTransport.Utils.CoordinateSystem
+{
+ ///
+ /// 负责在 Canonical Space 中,将 Rail 路径参考点转换成动画跟踪中心点。
+ /// 这层只处理纯数学偏移,不依赖 Navisworks API。
+ ///
+ public static class CanonicalRailOffsetResolver
+ {
+ public static double GetCenterTrackedOffset(PathRoute route, double objectHeight)
+ {
+ // 路径点本身已经是安装参考点(终点锚点)语义。
+ // 动画跟踪中心只需要相对安装参考点沿轨道法向偏移半个高度,
+ // 不能叠加任何“参考线 -> 锚点”旧语义偏移,否则会把终点/逐帧参考点重复平移。
+ return PathRoute.IsTopPayloadAnchorForMountMode(route.RailMountMode)
+ ? -objectHeight / 2.0
+ : objectHeight / 2.0;
+ }
+
+ public static Vector3 ResolveTrackedCenter(
+ PathRoute route,
+ Vector3 canonicalReferencePoint,
+ RailLocalFrame localFrame,
+ double objectHeight)
+ {
+ if (route == null || route.PathType != PathType.Rail)
+ {
+ return canonicalReferencePoint;
+ }
+
+ if (localFrame == null)
+ {
+ throw new ArgumentNullException(nameof(localFrame));
+ }
+
+ double centerOffset = GetCenterTrackedOffset(route, objectHeight);
+ double referenceContactFactor = 0.5 - (centerOffset / objectHeight);
+ return CanonicalTrackedPositionResolver.ResolveCenterFromContactReference(
+ canonicalReferencePoint,
+ localFrame.Normal,
+ objectHeight,
+ referenceContactFactor);
+ }
+ }
+}
diff --git a/src/Utils/CoordinateSystem/CanonicalRailPoseBuilder.cs b/src/Utils/CoordinateSystem/CanonicalRailPoseBuilder.cs
index a4f82b3..405f01f 100644
--- a/src/Utils/CoordinateSystem/CanonicalRailPoseBuilder.cs
+++ b/src/Utils/CoordinateSystem/CanonicalRailPoseBuilder.cs
@@ -11,6 +11,31 @@ namespace NavisworksTransport.Utils.CoordinateSystem
{
private const float TangentEpsilon = 1e-6f;
+ public static bool TryCreateLocalFrame(
+ Vector3 previousPoint,
+ Vector3 currentPoint,
+ Vector3 nextPoint,
+ Vector3 canonicalUp,
+ out RailLocalFrame frame)
+ {
+ frame = null;
+
+ if (!TryCreateBasis(
+ previousPoint,
+ currentPoint,
+ nextPoint,
+ canonicalUp,
+ out var forward,
+ out var lateral,
+ out var up))
+ {
+ return false;
+ }
+
+ frame = new RailLocalFrame(forward, lateral, up);
+ return true;
+ }
+
public static bool TryCreateBasis(
Vector3 previousPoint,
Vector3 currentPoint,
diff --git a/src/Utils/CoordinateSystem/CanonicalTrackedPositionResolver.cs b/src/Utils/CoordinateSystem/CanonicalTrackedPositionResolver.cs
new file mode 100644
index 0000000..fb43c6c
--- /dev/null
+++ b/src/Utils/CoordinateSystem/CanonicalTrackedPositionResolver.cs
@@ -0,0 +1,43 @@
+using System.Numerics;
+
+namespace NavisworksTransport.Utils.CoordinateSystem
+{
+ ///
+ /// 在 Canonical Space 中,将路径参考点解析为动画跟踪中心点。
+ /// 统一表达“参考点位于物体法向哪一侧接触面”的语义。
+ ///
+ public static class CanonicalTrackedPositionResolver
+ {
+ ///
+ /// 将任意法向上的参考点解析为几何中心。
+ /// referenceContactFactor 的语义:
+ /// 0 = 参考点位于底部/负法向接触面
+ /// 1 = 参考点位于顶部/正法向接触面
+ /// 中心点偏移 = normal * height * (0.5 - referenceContactFactor)
+ ///
+ public static Vector3 ResolveCenterFromContactReference(
+ Vector3 referencePoint,
+ Vector3 normal,
+ double objectHeight,
+ double referenceContactFactor)
+ {
+ return referencePoint + Vector3.Normalize(normal) * (float)(objectHeight * (0.5 - referenceContactFactor));
+ }
+
+ public static Vector3 ResolveGroundTrackedCenter(
+ Vector3 groundContactPoint,
+ Vector3 canonicalUp,
+ double objectHeight)
+ {
+ return ResolveCenterFromContactReference(groundContactPoint, canonicalUp, objectHeight, 0.0);
+ }
+
+ public static Vector3 ResolveTopSuspensionTrackedCenter(
+ Vector3 suspensionReferencePoint,
+ Vector3 canonicalUp,
+ double objectHeight)
+ {
+ return ResolveCenterFromContactReference(suspensionReferencePoint, canonicalUp, objectHeight, 1.0);
+ }
+ }
+}
diff --git a/src/Utils/CoordinateSystem/RailLocalFrame.cs b/src/Utils/CoordinateSystem/RailLocalFrame.cs
new file mode 100644
index 0000000..5306db3
--- /dev/null
+++ b/src/Utils/CoordinateSystem/RailLocalFrame.cs
@@ -0,0 +1,33 @@
+using System;
+using System.Numerics;
+
+namespace NavisworksTransport.Utils.CoordinateSystem
+{
+ ///
+ /// Rail 路径在 Canonical Space 中的局部坐标系。
+ /// Forward 沿路径前进方向,Normal 表示安装法向,Lateral 由二者叉乘确定。
+ ///
+ public sealed class RailLocalFrame
+ {
+ public RailLocalFrame(Vector3 forward, Vector3 lateral, Vector3 normal)
+ {
+ Forward = Normalize(forward, nameof(forward));
+ Lateral = Normalize(lateral, nameof(lateral));
+ Normal = Normalize(normal, nameof(normal));
+ }
+
+ public Vector3 Forward { get; }
+ public Vector3 Lateral { get; }
+ public Vector3 Normal { get; }
+
+ private static Vector3 Normalize(Vector3 value, string name)
+ {
+ if (value.LengthSquared() < 1e-12f)
+ {
+ throw new ArgumentException("局部坐标轴长度不能为零。", name);
+ }
+
+ return Vector3.Normalize(value);
+ }
+ }
+}
diff --git a/src/Utils/ModelItemTransformHelper.cs b/src/Utils/ModelItemTransformHelper.cs
index 0ad8a0f..1e6d59f 100644
--- a/src/Utils/ModelItemTransformHelper.cs
+++ b/src/Utils/ModelItemTransformHelper.cs
@@ -436,7 +436,7 @@ namespace NavisworksTransport.Utils
/// 适用于碰撞位置还原等场景
///
/// 要移动的物体
- /// 目标位置(地面位置,即包围盒底面)
+ /// 目标位置(动画跟踪点,当前统一为几何中心)
/// 目标朝向(弧度,绕Z轴)
public static void MoveItemToPositionAndYaw(ModelItem item, Point3D targetPosition, double targetYaw)
{
@@ -448,18 +448,14 @@ namespace NavisworksTransport.Utils
// 获取CAD原始状态
var originalBounds = item.BoundingBox();
- var originalGroundPos = new Point3D(
- originalBounds.Center.X,
- originalBounds.Center.Y,
- originalBounds.Min.Z
- );
+ var originalCenterPos = originalBounds.Center;
var originalYaw = GetYawFromTransform(item.Transform);
// 计算从CAD原始位置到目标位置的增量
var deltaPos = new Vector3D(
- targetPosition.X - originalGroundPos.X,
- targetPosition.Y - originalGroundPos.Y,
- targetPosition.Z - originalGroundPos.Z
+ targetPosition.X - originalCenterPos.X,
+ targetPosition.Y - originalCenterPos.Y,
+ targetPosition.Z - originalCenterPos.Z
);
double deltaYaw = targetYaw - originalYaw;
@@ -470,13 +466,13 @@ namespace NavisworksTransport.Utils
// 有旋转:需要补偿绕原点旋转带来的位置偏移
double cos = Math.Cos(deltaYaw);
double sin = Math.Sin(deltaYaw);
- double rotatedX = originalGroundPos.X * cos - originalGroundPos.Y * sin;
- double rotatedY = originalGroundPos.X * sin + originalGroundPos.Y * cos;
+ double rotatedX = originalCenterPos.X * cos - originalCenterPos.Y * sin;
+ double rotatedY = originalCenterPos.X * sin + originalCenterPos.Y * cos;
var compensatedTranslation = new Vector3D(
targetPosition.X - rotatedX,
targetPosition.Y - rotatedY,
- deltaPos.Z
+ targetPosition.Z - originalCenterPos.Z
);
var identity = Transform3D.CreateTranslation(new Vector3D(0, 0, 0));
@@ -544,24 +540,24 @@ namespace NavisworksTransport.Utils
targetPosition.Y - rotatedCurrentPosition.Y,
targetPosition.Z - rotatedCurrentPosition.Z);
- LogManager.Info(
+ LogManager.Debug(
$"[模型增量姿态] {item.DisplayName} 当前=({currentPosition.X:F3},{currentPosition.Y:F3},{currentPosition.Z:F3}), " +
$"目标=({targetPosition.X:F3},{targetPosition.Y:F3},{targetPosition.Z:F3}), " +
$"平移=({translation.X:F3},{translation.Y:F3},{translation.Z:F3})");
- LogManager.Info(
+ LogManager.Debug(
$"[模型增量姿态] {item.DisplayName} 当前旋转: " +
$"X=({currentLinear.Get(0, 0):F4},{currentLinear.Get(1, 0):F4},{currentLinear.Get(2, 0):F4}), " +
$"Y=({currentLinear.Get(0, 1):F4},{currentLinear.Get(1, 1):F4},{currentLinear.Get(2, 1):F4}), " +
$"Z=({currentLinear.Get(0, 2):F4},{currentLinear.Get(1, 2):F4},{currentLinear.Get(2, 2):F4})");
- LogManager.Info(
+ LogManager.Debug(
$"[模型增量姿态] {item.DisplayName} 目标旋转: " +
$"X=({targetLinear.Get(0, 0):F4},{targetLinear.Get(1, 0):F4},{targetLinear.Get(2, 0):F4}), " +
$"Y=({targetLinear.Get(0, 1):F4},{targetLinear.Get(1, 1):F4},{targetLinear.Get(2, 1):F4}), " +
$"Z=({targetLinear.Get(0, 2):F4},{targetLinear.Get(1, 2):F4},{targetLinear.Get(2, 2):F4})");
- LogManager.Info(
+ LogManager.Debug(
$"[模型增量姿态] {item.DisplayName} 增量旋转: " +
$"X=({deltaLinear.Get(0, 0):F4},{deltaLinear.Get(1, 0):F4},{deltaLinear.Get(2, 0):F4}), " +
$"Y=({deltaLinear.Get(0, 1):F4},{deltaLinear.Get(1, 1):F4},{deltaLinear.Get(2, 1):F4}), " +
@@ -602,18 +598,15 @@ namespace NavisworksTransport.Utils
var actualRotation = actualTransform.Factor().Rotation;
var actualLinear = new Transform3D(actualRotation).Linear;
var targetLinear = new Transform3D(targetRotation).Linear;
- var actualPosition = new Point3D(
- actualBounds.Center.X,
- actualBounds.Center.Y,
- actualBounds.Min.Z);
+ var actualPosition = actualBounds.Center;
- LogManager.Info(
+ LogManager.Debug(
$"[模型增量姿态] {item.DisplayName} 立即读回位置(可能滞后): " +
$"实际=({actualPosition.X:F3},{actualPosition.Y:F3},{actualPosition.Z:F3}), " +
$"期望=({targetPosition.X:F3},{targetPosition.Y:F3},{targetPosition.Z:F3}), " +
$"偏差=({actualPosition.X - targetPosition.X:F3},{actualPosition.Y - targetPosition.Y:F3},{actualPosition.Z - targetPosition.Z:F3})");
- LogManager.Info(
+ LogManager.Debug(
$"[模型增量姿态] {item.DisplayName} 立即读回旋转(可能滞后/不反映override): " +
$"实际X=({actualLinear.Get(0, 0):F4},{actualLinear.Get(1, 0):F4},{actualLinear.Get(2, 0):F4}), " +
$"实际Y=({actualLinear.Get(0, 1):F4},{actualLinear.Get(1, 1):F4},{actualLinear.Get(2, 1):F4}), " +
@@ -812,29 +805,22 @@ namespace NavisworksTransport.Utils
var originalComponents = originalTransform.Factor();
var originalRotation = originalComponents.Rotation;
var originalBounds = item.BoundingBox();
- var originalUp = GetUpDirectionFromTransform(originalTransform);
- var localSize = EstimateLocalBoxSize(originalBounds, originalTransform);
- double halfHeight = localSize.Z / 2.0;
- var originalGroundPos = new Point3D(
- originalBounds.Center.X - originalUp.X * halfHeight,
- originalBounds.Center.Y - originalUp.Y * halfHeight,
- originalBounds.Center.Z - originalUp.Z * halfHeight
- );
+ var originalCenterPos = originalBounds.Center;
Rotation3D deltaRotation = BuildDeltaRotation(originalRotation, targetRotation);
var rotationTransform = new Transform3D(deltaRotation);
var linear = rotationTransform.Linear;
- var rotatedGroundPos = new Point3D(
- linear.Get(0, 0) * originalGroundPos.X + linear.Get(0, 1) * originalGroundPos.Y + linear.Get(0, 2) * originalGroundPos.Z,
- linear.Get(1, 0) * originalGroundPos.X + linear.Get(1, 1) * originalGroundPos.Y + linear.Get(1, 2) * originalGroundPos.Z,
- linear.Get(2, 0) * originalGroundPos.X + linear.Get(2, 1) * originalGroundPos.Y + linear.Get(2, 2) * originalGroundPos.Z
+ var rotatedCenterPos = new Point3D(
+ linear.Get(0, 0) * originalCenterPos.X + linear.Get(0, 1) * originalCenterPos.Y + linear.Get(0, 2) * originalCenterPos.Z,
+ linear.Get(1, 0) * originalCenterPos.X + linear.Get(1, 1) * originalCenterPos.Y + linear.Get(1, 2) * originalCenterPos.Z,
+ linear.Get(2, 0) * originalCenterPos.X + linear.Get(2, 1) * originalCenterPos.Y + linear.Get(2, 2) * originalCenterPos.Z
);
var translation = new Vector3D(
- targetPosition.X - rotatedGroundPos.X,
- targetPosition.Y - rotatedGroundPos.Y,
- targetPosition.Z - rotatedGroundPos.Z
+ targetPosition.X - rotatedCenterPos.X,
+ targetPosition.Y - rotatedCenterPos.Y,
+ targetPosition.Z - rotatedCenterPos.Z
);
Transform3D transform;
@@ -852,7 +838,7 @@ namespace NavisworksTransport.Utils
transform = new Transform3D(deltaRotation, translation);
}
- LogAbsoluteTransformDiagnostics(item, originalRotation, targetRotation, deltaRotation, originalGroundPos, targetPosition, translation, halfHeight, originalUp);
+ LogAbsoluteTransformDiagnostics(item, originalRotation, targetRotation, deltaRotation, originalCenterPos, targetPosition, translation);
doc.Models.OverridePermanentTransform(modelItems, transform, false);
}
@@ -870,11 +856,9 @@ namespace NavisworksTransport.Utils
Rotation3D originalRotation,
Rotation3D targetRotation,
Rotation3D deltaRotation,
- Point3D originalGroundPos,
+ Point3D originalTrackedPosition,
Point3D targetPosition,
- Vector3D translation,
- double halfHeight,
- Vector3D originalUp)
+ Vector3D translation)
{
try
{
@@ -899,9 +883,8 @@ namespace NavisworksTransport.Utils
$"X=({deltaLinear.Get(0, 0):F4},{deltaLinear.Get(1, 0):F4},{deltaLinear.Get(2, 0):F4}), " +
$"Y=({deltaLinear.Get(0, 1):F4},{deltaLinear.Get(1, 1):F4},{deltaLinear.Get(2, 1):F4}), " +
$"Z=({deltaLinear.Get(0, 2):F4},{deltaLinear.Get(1, 2):F4},{deltaLinear.Get(2, 2):F4}), " +
- $"原始Up=({originalUp.X:F4},{originalUp.Y:F4},{originalUp.Z:F4}), 半高={halfHeight:F3}, " +
- $"原始底面中心=({originalGroundPos.X:F3},{originalGroundPos.Y:F3},{originalGroundPos.Z:F3}), " +
- $"目标底面中心=({targetPosition.X:F3},{targetPosition.Y:F3},{targetPosition.Z:F3}), " +
+ $"原始跟踪点=({originalTrackedPosition.X:F3},{originalTrackedPosition.Y:F3},{originalTrackedPosition.Z:F3}), " +
+ $"目标跟踪点=({targetPosition.X:F3},{targetPosition.Y:F3},{targetPosition.Z:F3}), " +
$"平移=({translation.X:F3},{translation.Y:F3},{translation.Z:F3})");
}
catch (Exception ex)
@@ -931,18 +914,14 @@ namespace NavisworksTransport.Utils
// 获取CAD原始状态
var originalBounds = item.BoundingBox();
- var originalGroundPos = new Point3D(
- originalBounds.Center.X,
- originalBounds.Center.Y,
- originalBounds.Min.Z
- );
+ var originalCenterPos = originalBounds.Center;
var originalYaw = GetYawFromTransform(item.Transform);
// 计算从CAD原始位置到目标位置的增量
var deltaPos = new Vector3D(
- targetPosition.X - originalGroundPos.X,
- targetPosition.Y - originalGroundPos.Y,
- targetPosition.Z - originalGroundPos.Z
+ targetPosition.X - originalCenterPos.X,
+ targetPosition.Y - originalCenterPos.Y,
+ targetPosition.Z - originalCenterPos.Z
);
double deltaYaw = targetYaw - originalYaw;
@@ -964,13 +943,13 @@ namespace NavisworksTransport.Utils
{
double cos = Math.Cos(deltaYaw);
double sin = Math.Sin(deltaYaw);
- double rotatedX = originalGroundPos.X * cos - originalGroundPos.Y * sin;
- double rotatedY = originalGroundPos.X * sin + originalGroundPos.Y * cos;
+ double rotatedX = originalCenterPos.X * cos - originalCenterPos.Y * sin;
+ double rotatedY = originalCenterPos.X * sin + originalCenterPos.Y * cos;
components.Translation = new Vector3D(
targetPosition.X - rotatedX,
targetPosition.Y - rotatedY,
- deltaPos.Z
+ targetPosition.Z - originalCenterPos.Z
);
}
else
@@ -1079,18 +1058,14 @@ namespace NavisworksTransport.Utils
// 2. 从CAD原始位置移动到保存的位置
// 获取CAD原始状态
var originalBounds = item.BoundingBox();
- var originalGroundPos = new Point3D(
- originalBounds.Center.X,
- originalBounds.Center.Y,
- originalBounds.Min.Z
- );
+ var originalCenterPos = originalBounds.Center;
var originalYaw = GetYawFromTransform(item.Transform);
// 计算到保存位置的增量
var deltaPos = new Vector3D(
- state.Position.X - originalGroundPos.X,
- state.Position.Y - originalGroundPos.Y,
- state.Position.Z - originalGroundPos.Z
+ state.Position.X - originalCenterPos.X,
+ state.Position.Y - originalCenterPos.Y,
+ state.Position.Z - originalCenterPos.Z
);
double deltaYaw = state.YawRadians - originalYaw;
@@ -1100,13 +1075,13 @@ namespace NavisworksTransport.Utils
{
double cos = Math.Cos(deltaYaw);
double sin = Math.Sin(deltaYaw);
- double rotatedX = originalGroundPos.X * cos - originalGroundPos.Y * sin;
- double rotatedY = originalGroundPos.X * sin + originalGroundPos.Y * cos;
+ double rotatedX = originalCenterPos.X * cos - originalCenterPos.Y * sin;
+ double rotatedY = originalCenterPos.X * sin + originalCenterPos.Y * cos;
var compensatedTranslation = new Vector3D(
state.Position.X - rotatedX,
state.Position.Y - rotatedY,
- deltaPos.Z
+ state.Position.Z - originalCenterPos.Z
);
var identity = Transform3D.CreateTranslation(new Vector3D(0, 0, 0));
@@ -1141,18 +1116,14 @@ namespace NavisworksTransport.Utils
// 获取CAD原始状态
var originalBounds = item.BoundingBox();
- var originalGroundPos = new Point3D(
- originalBounds.Center.X,
- originalBounds.Center.Y,
- originalBounds.Min.Z
- );
+ var originalCenterPos = originalBounds.Center;
var originalYaw = GetYawFromTransform(item.Transform);
// 计算从CAD原始位置到目标位置的增量
var deltaPos = new Vector3D(
- targetPosition.X - originalGroundPos.X,
- targetPosition.Y - originalGroundPos.Y,
- targetPosition.Z - originalGroundPos.Z
+ targetPosition.X - originalCenterPos.X,
+ targetPosition.Y - originalCenterPos.Y,
+ targetPosition.Z - originalCenterPos.Z
);
double deltaYaw = targetYaw - originalYaw;
@@ -1171,13 +1142,13 @@ namespace NavisworksTransport.Utils
{
double cos = Math.Cos(deltaYaw);
double sin = Math.Sin(deltaYaw);
- double rotatedX = originalGroundPos.X * cos - originalGroundPos.Y * sin;
- double rotatedY = originalGroundPos.X * sin + originalGroundPos.Y * cos;
+ double rotatedX = originalCenterPos.X * cos - originalCenterPos.Y * sin;
+ double rotatedY = originalCenterPos.X * sin + originalCenterPos.Y * cos;
components.Translation = new Vector3D(
targetPosition.X - rotatedX,
targetPosition.Y - rotatedY,
- deltaPos.Z
+ targetPosition.Z - originalCenterPos.Z
);
}
else
diff --git a/src/Utils/RailPathPoseHelper.cs b/src/Utils/RailPathPoseHelper.cs
index 378b09f..c668a4d 100644
--- a/src/Utils/RailPathPoseHelper.cs
+++ b/src/Utils/RailPathPoseHelper.cs
@@ -15,34 +15,11 @@ namespace NavisworksTransport.Utils
private const double TangentEpsilon = 1e-9;
private static bool _rotationConstructorConventionLogged;
- ///
- /// 获取 Rail 参考点到构件对接点的有符号偏移(模型单位)。
- /// LegacySuspensionPoint 模式下,路径点本身就是构件对接点,因此偏移为 0。
- ///
- public static double GetAnchorOffset(PathRoute route)
- {
- if (route == null || route.PathType != PathType.Rail)
- {
- return 0.0;
- }
-
- if (route.RailPathDefinitionMode == RailPathDefinitionMode.LegacySuspensionPoint)
- {
- return 0.0;
- }
-
- return route.RailMountMode == RailMountMode.OverRail
- ? route.RailReferenceToAnchorOffset
- : -route.RailReferenceToAnchorOffset;
- }
-
///
/// 计算构件底面中心相对于 Rail 参考点的 Z 偏移(模型单位)。
///
public static double GetBottomZOffset(PathRoute route, double objectHeight)
{
- double anchorOffset = GetAnchorOffset(route);
-
if (route == null || route.PathType != PathType.Rail)
{
return 0.0;
@@ -50,10 +27,10 @@ namespace NavisworksTransport.Utils
if (PathRoute.IsTopPayloadAnchorForMountMode(route.RailMountMode))
{
- return anchorOffset - objectHeight;
+ return -objectHeight;
}
- return anchorOffset;
+ return 0.0;
}
///
@@ -63,8 +40,6 @@ namespace NavisworksTransport.Utils
///
public static double GetObjectSpaceCenterZOffset(PathRoute route, double objectSpaceHeight)
{
- double anchorOffset = GetAnchorOffset(route);
-
if (route == null || route.PathType != PathType.Rail)
{
return 0.0;
@@ -72,10 +47,10 @@ namespace NavisworksTransport.Utils
if (PathRoute.IsTopPayloadAnchorForMountMode(route.RailMountMode))
{
- return anchorOffset - objectSpaceHeight / 2.0;
+ return -objectSpaceHeight / 2.0;
}
- return anchorOffset + objectSpaceHeight / 2.0;
+ return objectSpaceHeight / 2.0;
}
///
@@ -160,11 +135,21 @@ namespace NavisworksTransport.Utils
var adapterForRail = CoordinateSystemManager.Instance.CreateHostAdapter();
var canonicalReferencePointForRail = adapterForRail.ToCanonicalPoint(referencePoint);
- var normal = ResolveRailNormal(previousPoint, referencePoint, nextPoint);
- var canonicalCenterPointForRail = new Point3D(
- canonicalReferencePointForRail.X + normal.X * centerOffset,
- canonicalReferencePointForRail.Y + normal.Y * centerOffset,
- canonicalReferencePointForRail.Z + normal.Z * centerOffset);
+ if (!TryCreateCanonicalLocalFrame(previousPoint, referencePoint, nextPoint, out RailLocalFrame frame))
+ {
+ var canonicalCenterPointForRailFallback = new Point3D(
+ canonicalReferencePointForRail.X,
+ canonicalReferencePointForRail.Y,
+ canonicalReferencePointForRail.Z + centerOffset);
+ return adapterForRail.FromCanonicalPoint(canonicalCenterPointForRailFallback);
+ }
+
+ Vector3 trackedCenter = CanonicalRailOffsetResolver.ResolveTrackedCenter(
+ route,
+ ToNumerics(canonicalReferencePointForRail),
+ frame,
+ objectSpaceHeight);
+ var canonicalCenterPointForRail = new Point3D(trackedCenter.X, trackedCenter.Y, trackedCenter.Z);
return adapterForRail.FromCanonicalPoint(canonicalCenterPointForRail);
}
@@ -210,18 +195,19 @@ namespace NavisworksTransport.Utils
var canonicalCurrentPoint = ToNumerics(adapter.ToCanonicalPoint(currentPoint));
var canonicalNextPoint = ToNumerics(adapter.ToCanonicalPoint(nextPoint));
- if (!CanonicalRailPoseBuilder.TryCreateBasis(
+ if (!CanonicalRailPoseBuilder.TryCreateLocalFrame(
canonicalPreviousPoint,
canonicalCurrentPoint,
canonicalNextPoint,
HostCoordinateAdapter.CanonicalUpVector3,
- out var canonicalForward,
- out var canonicalLateral,
- out var canonicalUp))
+ out RailLocalFrame frame))
{
return false;
}
+ var canonicalForward = frame.Forward;
+ var canonicalLateral = frame.Lateral;
+ var canonicalUp = frame.Normal;
Quaternion canonicalRotation = axisConvention.CreateQuaternion(canonicalForward, canonicalUp);
Matrix4x4 canonicalLinear = Matrix4x4.CreateFromQuaternion(canonicalRotation);
@@ -397,53 +383,26 @@ namespace NavisworksTransport.Utils
private static double GetBottomOffsetMagnitude(PathRoute route, double objectHeight)
{
- double anchorOffset = GetAnchorOffset(route);
return PathRoute.IsTopPayloadAnchorForMountMode(route.RailMountMode)
- ? anchorOffset - objectHeight
- : anchorOffset;
+ ? -objectHeight
+ : 0.0;
}
private static double GetObjectSpaceCenterOffsetMagnitude(PathRoute route, double objectSpaceHeight)
{
- double anchorOffset = GetAnchorOffset(route);
return PathRoute.IsTopPayloadAnchorForMountMode(route.RailMountMode)
- ? anchorOffset - objectSpaceHeight / 2.0
- : anchorOffset + objectSpaceHeight / 2.0;
+ ? -objectSpaceHeight / 2.0
+ : objectSpaceHeight / 2.0;
}
private static Vector3D ResolveRailNormal(Point3D previousPoint, Point3D currentPoint, Point3D nextPoint)
{
- var tangent = ResolveTangent(previousPoint, currentPoint, nextPoint);
- double tangentLengthSquared = tangent.X * tangent.X + tangent.Y * tangent.Y + tangent.Z * tangent.Z;
-
- if (tangentLengthSquared < TangentEpsilon)
+ if (TryCreateCanonicalLocalFrame(previousPoint, currentPoint, nextPoint, out RailLocalFrame frame))
{
- return HostCoordinateAdapter.CanonicalUp;
+ return ToNavVector(frame.Normal);
}
- tangent = new Vector3D(
- tangent.X / Math.Sqrt(tangentLengthSquared),
- tangent.Y / Math.Sqrt(tangentLengthSquared),
- tangent.Z / Math.Sqrt(tangentLengthSquared));
-
- var worldUp = HostCoordinateAdapter.CanonicalUp;
- double projection = worldUp.X * tangent.X + worldUp.Y * tangent.Y + worldUp.Z * tangent.Z;
- var normal = new Vector3D(
- worldUp.X - projection * tangent.X,
- worldUp.Y - projection * tangent.Y,
- worldUp.Z - projection * tangent.Z);
-
- double normalLengthSquared = normal.X * normal.X + normal.Y * normal.Y + normal.Z * normal.Z;
- if (normalLengthSquared < TangentEpsilon)
- {
- return HostCoordinateAdapter.CanonicalUp;
- }
-
- double normalLength = Math.Sqrt(normalLengthSquared);
- return new Vector3D(
- normal.X / normalLength,
- normal.Y / normalLength,
- normal.Z / normalLength);
+ return HostCoordinateAdapter.CanonicalUp;
}
private static Vector3D ResolveTangent(Point3D previousPoint, Point3D currentPoint, Point3D nextPoint)
@@ -470,6 +429,21 @@ namespace NavisworksTransport.Utils
return tangent;
}
+ private static bool TryCreateCanonicalLocalFrame(
+ Point3D previousPoint,
+ Point3D currentPoint,
+ Point3D nextPoint,
+ out RailLocalFrame frame)
+ {
+ var adapter = CoordinateSystemManager.Instance.CreateHostAdapter();
+ return CanonicalRailPoseBuilder.TryCreateLocalFrame(
+ ToNumerics(adapter.ToCanonicalPoint(previousPoint)),
+ ToNumerics(adapter.ToCanonicalPoint(currentPoint)),
+ ToNumerics(adapter.ToCanonicalPoint(nextPoint)),
+ HostCoordinateAdapter.CanonicalUpVector3,
+ out frame);
+ }
+
private static Vector3D Normalize(Vector3D vector)
{
double lengthSquared = vector.X * vector.X + vector.Y * vector.Y + vector.Z * vector.Z;
From 042b9a2804c1be6207483310d072300b1d84b2f8 Mon Sep 17 00:00:00 2001
From: tian <11429339@qq.com>
Date: Sun, 22 Mar 2026 00:00:16 +0800
Subject: [PATCH 12/87] Refine canonical pose flow and handoff notes
---
NavisworksTransport.UnitTests.csproj | 1 +
TransportPlugin.csproj | 1 +
.../CanonicalPlanarPoseBuilderTests.cs | 83 +++++++
.../HostCoordinateAdapterTests.cs | 91 ++++++++
doc/working/current-engineering-state.md | 117 ++++++++++
resources/unit_cube.nwc | Bin 1762 -> 1783 bytes
src/Core/Animation/PathAnimationManager.cs | 214 ++++++++++++++++--
src/Core/VirtualObjectManager.cs | 37 +++
.../CanonicalPlanarPoseBuilder.cs | 57 +++++
.../CoordinateSystem/HostCoordinateAdapter.cs | 70 ++++++
10 files changed, 656 insertions(+), 15 deletions(-)
create mode 100644 UnitTests/CoordinateSystem/CanonicalPlanarPoseBuilderTests.cs
create mode 100644 doc/working/current-engineering-state.md
create mode 100644 src/Utils/CoordinateSystem/CanonicalPlanarPoseBuilder.cs
diff --git a/NavisworksTransport.UnitTests.csproj b/NavisworksTransport.UnitTests.csproj
index 7832f72..619c514 100644
--- a/NavisworksTransport.UnitTests.csproj
+++ b/NavisworksTransport.UnitTests.csproj
@@ -55,6 +55,7 @@
+
diff --git a/TransportPlugin.csproj b/TransportPlugin.csproj
index adb5d43..f1e1a5c 100644
--- a/TransportPlugin.csproj
+++ b/TransportPlugin.csproj
@@ -332,6 +332,7 @@
+
diff --git a/UnitTests/CoordinateSystem/CanonicalPlanarPoseBuilderTests.cs b/UnitTests/CoordinateSystem/CanonicalPlanarPoseBuilderTests.cs
new file mode 100644
index 0000000..b0b6b0a
--- /dev/null
+++ b/UnitTests/CoordinateSystem/CanonicalPlanarPoseBuilderTests.cs
@@ -0,0 +1,83 @@
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using NavisworksTransport.Utils.CoordinateSystem;
+using System.Numerics;
+
+namespace NavisworksTransport.UnitTests.CoordinateSystem
+{
+ [TestClass]
+ public class CanonicalPlanarPoseBuilderTests
+ {
+ [TestMethod]
+ public void StraightForward_ZUpConvention_ShouldKeepLocalZAsWorldUp()
+ {
+ bool ok = CanonicalPlanarPoseBuilder.TryCreateQuaternionFromForward(
+ new Vector3(5, 0, 0),
+ Vector3.UnitZ,
+ ModelAxisConvention.CreateDefaultForHost(CoordinateSystemType.ZUp),
+ out Quaternion rotation);
+
+ Assert.IsTrue(ok);
+ Matrix4x4 linear = Matrix4x4.CreateFromQuaternion(rotation);
+ AssertColumn(linear, 0, 1, 0, 0);
+ AssertColumn(linear, 1, 0, 1, 0);
+ AssertColumn(linear, 2, 0, 0, 1);
+ }
+
+ [TestMethod]
+ public void StraightForward_YUpConvention_ShouldMapLocalYToWorldUp()
+ {
+ bool ok = CanonicalPlanarPoseBuilder.TryCreateQuaternionFromForward(
+ new Vector3(5, 0, 0),
+ Vector3.UnitZ,
+ ModelAxisConvention.CreateDefaultForHost(CoordinateSystemType.YUp),
+ out Quaternion rotation);
+
+ Assert.IsTrue(ok);
+ Matrix4x4 linear = Matrix4x4.CreateFromQuaternion(rotation);
+ AssertColumn(linear, 0, 1, 0, 0);
+ AssertColumn(linear, 1, 0, 0, 1);
+ AssertColumn(linear, 2, 0, -1, 0);
+ }
+
+ [TestMethod]
+ public void ForwardWithVerticalComponent_ShouldProjectToHorizontalPlane()
+ {
+ bool ok = CanonicalPlanarPoseBuilder.TryCreateQuaternionFromForward(
+ new Vector3(10, 0, 3),
+ Vector3.UnitZ,
+ ModelAxisConvention.CreateDefaultForHost(CoordinateSystemType.ZUp),
+ out Quaternion rotation);
+
+ Assert.IsTrue(ok);
+ Matrix4x4 linear = Matrix4x4.CreateFromQuaternion(rotation);
+ Assert.AreEqual(0.0, linear.M31, 1e-6);
+ Assert.AreEqual(0.0, linear.M32, 1e-6);
+ Assert.AreEqual(1.0, linear.M33, 1e-6);
+ }
+
+ private static void AssertColumn(Matrix4x4 matrix, int column, double x, double y, double z)
+ {
+ switch (column)
+ {
+ case 0:
+ Assert.AreEqual(x, matrix.M11, 1e-6);
+ Assert.AreEqual(y, matrix.M21, 1e-6);
+ Assert.AreEqual(z, matrix.M31, 1e-6);
+ break;
+ case 1:
+ Assert.AreEqual(x, matrix.M12, 1e-6);
+ Assert.AreEqual(y, matrix.M22, 1e-6);
+ Assert.AreEqual(z, matrix.M32, 1e-6);
+ break;
+ case 2:
+ Assert.AreEqual(x, matrix.M13, 1e-6);
+ Assert.AreEqual(y, matrix.M23, 1e-6);
+ Assert.AreEqual(z, matrix.M33, 1e-6);
+ break;
+ default:
+ Assert.Fail("Only first 3 columns are valid.");
+ break;
+ }
+ }
+ }
+}
diff --git a/UnitTests/CoordinateSystem/HostCoordinateAdapterTests.cs b/UnitTests/CoordinateSystem/HostCoordinateAdapterTests.cs
index 0a97569..3d54d44 100644
--- a/UnitTests/CoordinateSystem/HostCoordinateAdapterTests.cs
+++ b/UnitTests/CoordinateSystem/HostCoordinateAdapterTests.cs
@@ -1,6 +1,7 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using NavisworksTransport.Utils.CoordinateSystem;
using System.Numerics;
+using Autodesk.Navisworks.Api;
namespace NavisworksTransport.UnitTests.CoordinateSystem
{
@@ -52,6 +53,96 @@ namespace NavisworksTransport.UnitTests.CoordinateSystem
AssertPoint(restoredBounds.Max, 5.0, 7.0, 11.0);
}
+ [TestMethod]
+ public void YUp_CanonicalRotation_ShouldConvertToHostYUpBasis()
+ {
+ var adapter = new HostCoordinateAdapter(CoordinateSystemType.YUp);
+ var convention = ModelAxisConvention.CreateDefaultForHost(CoordinateSystemType.ZUp);
+ Quaternion canonicalRotation = convention.CreateQuaternion(new Vector3(1, 0, 0), Vector3.UnitZ);
+
+ Matrix4x4 hostLinear = adapter.FromCanonicalLinearTransform(Matrix4x4.CreateFromQuaternion(canonicalRotation));
+
+ Assert.AreEqual(1.0, hostLinear.M11, 1e-6);
+ Assert.AreEqual(0.0, hostLinear.M21, 1e-6);
+ Assert.AreEqual(0.0, hostLinear.M31, 1e-6);
+
+ Assert.AreEqual(0.0, hostLinear.M12, 1e-6);
+ Assert.AreEqual(0.0, hostLinear.M22, 1e-6);
+ Assert.AreEqual(-1.0, hostLinear.M32, 1e-6);
+
+ Assert.AreEqual(0.0, hostLinear.M13, 1e-6);
+ Assert.AreEqual(1.0, hostLinear.M23, 1e-6);
+ Assert.AreEqual(0.0, hostLinear.M33, 1e-6);
+ }
+
+ [TestMethod]
+ public void YUp_CanonicalRotation_WithPlanarForward_ShouldProduceExpectedHostAxes()
+ {
+ var adapter = new HostCoordinateAdapter(CoordinateSystemType.YUp);
+ var convention = ModelAxisConvention.CreateDefaultForHost(CoordinateSystemType.ZUp);
+
+ Vector3 hostForward = Vector3.Normalize(new Vector3(-0.997320f, 0.0f, 0.073164f));
+ Vector3 canonicalForward = adapter.ToCanonicalVector3(hostForward);
+
+ bool ok = CanonicalPlanarPoseBuilder.TryCreateQuaternionFromForward(
+ canonicalForward,
+ HostCoordinateAdapter.CanonicalUpVector3,
+ convention,
+ out Quaternion canonicalRotation);
+
+ Assert.IsTrue(ok);
+
+ Matrix4x4 canonicalLinear = Matrix4x4.CreateFromQuaternion(canonicalRotation);
+ Matrix4x4 hostLinear = adapter.FromCanonicalLinearTransform(canonicalLinear);
+ Quaternion hostQuaternion = Quaternion.CreateFromRotationMatrix(hostLinear);
+ Matrix4x4 reconstructedHostLinear = Matrix4x4.CreateFromQuaternion(hostQuaternion);
+
+ Assert.AreEqual(hostForward.X, reconstructedHostLinear.M11, 1e-3);
+ Assert.AreEqual(hostForward.Y, reconstructedHostLinear.M21, 1e-3);
+ Assert.AreEqual(hostForward.Z, reconstructedHostLinear.M31, 1e-3);
+
+ Assert.AreEqual(0.0, reconstructedHostLinear.M13, 1e-3);
+ Assert.AreEqual(1.0, reconstructedHostLinear.M23, 1e-3);
+ Assert.AreEqual(0.0, reconstructedHostLinear.M33, 1e-3);
+ }
+
+ [TestMethod]
+ public void YUp_HostLinear_FromActualGroundPathPoints_ShouldMatchExpectedHostAxes()
+ {
+ var adapter = new HostCoordinateAdapter(CoordinateSystemType.YUp);
+ var convention = ModelAxisConvention.CreateDefaultForHost(CoordinateSystemType.ZUp);
+
+ Vector3 previousPoint = new Vector3(-181.82637032765f, 14.8333330154419f, 3.38710203888167f);
+ Vector3 currentPoint = new Vector3(-181.82637032765f, 14.8333330154419f, 3.38710203888167f);
+ Vector3 nextPoint = new Vector3(-202.828908853644f, 16.3772966612769f, 14.304117291825f);
+
+ Vector3 canonicalPrevious = adapter.ToCanonicalPoint3(previousPoint);
+ Vector3 canonicalCurrent = adapter.ToCanonicalPoint3(currentPoint);
+ Vector3 canonicalNext = adapter.ToCanonicalPoint3(nextPoint);
+
+ Vector3 canonicalForward = canonicalNext - canonicalPrevious;
+ Assert.IsTrue(CanonicalPlanarPoseBuilder.TryCreateQuaternionFromForward(
+ canonicalForward,
+ HostCoordinateAdapter.CanonicalUpVector3,
+ convention,
+ out Quaternion canonicalRotation));
+
+ Matrix4x4 canonicalLinear = Matrix4x4.CreateFromQuaternion(canonicalRotation);
+ Matrix4x4 hostLinear = adapter.FromCanonicalLinearTransform(canonicalLinear);
+
+ Assert.AreEqual(-0.8873, hostLinear.M11, 1e-3);
+ Assert.AreEqual(0.0000, hostLinear.M21, 1e-3);
+ Assert.AreEqual(0.4612, hostLinear.M31, 1e-3);
+
+ Assert.AreEqual(0.4612, hostLinear.M12, 1e-3);
+ Assert.AreEqual(0.0000, hostLinear.M22, 1e-3);
+ Assert.AreEqual(0.8873, hostLinear.M32, 1e-3);
+
+ Assert.AreEqual(0.0000, hostLinear.M13, 1e-3);
+ Assert.AreEqual(1.0000, hostLinear.M23, 1e-3);
+ Assert.AreEqual(0.0000, hostLinear.M33, 1e-3);
+ }
+
private static void AssertPoint(Vector3 point, double x, double y, double z)
{
Assert.AreEqual(x, point.X, 1e-9);
diff --git a/doc/working/current-engineering-state.md b/doc/working/current-engineering-state.md
new file mode 100644
index 0000000..f6a1035
--- /dev/null
+++ b/doc/working/current-engineering-state.md
@@ -0,0 +1,117 @@
+# 当前工程状态
+
+更新时间:2026-03-21
+
+## 1. 当前稳定状态
+
+- `Rail` 路径主链路已稳定:
+ - `ZUp` 模型下,轨上/轨下路径的真实物体与虚拟物体起点、动画、终点贴合正确。
+ - `YUp` 模型下,终端安装仿真、`Rail` 姿态、真实物体与虚拟物体通行空间、起点与动画主链路已基本跑通。
+- 地面路径在 `YUp` 模型下:
+ - 虚拟物体起点姿态、动画姿态、转弯姿态已恢复正常。
+ - 当前已禁止地面/吊装路径偷偷退回旧 `yaw` 链路。
+- 碰撞检测/恢复主链路已稳定:
+ - `ClashDetective` 三维恢复不能再先 `ResetPermanentTransform`。
+ - 碰撞恢复、自动报告、自动截图已重新对齐到动画主链路。
+- 虚拟物体资源问题已确认并修复:
+ - 旧 `unit_cube.nwc` 局部几何中心不在原点,会导致虚拟物体中心偏差。
+ - 新 `unit_cube.nwc` 已替换为几何中心在原点的版本。
+
+## 2. 当前坐标系架构
+
+- 外部坐标:
+ - `Navisworks` 世界坐标,统一视为外部输入坐标。
+- 内部统一坐标:
+ - `Canonical Space`,固定 `Z-up`。
+- 业务基准层:
+ - `ProjectReferenceFrame`
+ - 球心、项目 `up`、默认模型轴约定
+- 模型局部轴约定:
+ - `ModelAxisConvention`
+ - 明确 `ForwardAxis / UpAxis`
+- `Rail` 局部坐标系:
+ - `RailLocalFrame`
+ - `Forward / Normal / Lateral`
+
+## 3. 当前关键基础工具
+
+- `src/Utils/CoordinateSystem/HostCoordinateAdapter.cs`
+- `src/Utils/CoordinateSystem/ProjectReferenceFrame.cs`
+- `src/Utils/CoordinateSystem/ModelAxisConvention.cs`
+- `src/Utils/CoordinateSystem/CanonicalRailPoseBuilder.cs`
+- `src/Utils/CoordinateSystem/RailLocalFrame.cs`
+- `src/Utils/CoordinateSystem/CanonicalRailOffsetResolver.cs`
+- `src/Utils/CoordinateSystem/CanonicalTrackedPositionResolver.cs`
+- `src/Utils/CoordinateSystem/CanonicalPlanarPoseBuilder.cs`
+
+## 4. 当前必须记住的根因与规则
+
+### 4.1 Rotation / 矩阵语义
+
+- `Rotation3D(a, b, c, d)` 的参数顺序是四元数 `x, y, z, w`。
+- 不要再随意在 `System.Numerics` 矩阵、四元数、Navisworks `Rotation3D` 之间猜行列语义。
+- 新的姿态构造应优先复用已经验证过的坐标框架工具,不要临时手拼矩阵。
+
+### 4.2 碰撞恢复
+
+- 对受 `PathAnimationManager` 控制的对象,`ClashDetective` 验证前绝不能先 `ResetPermanentTransform`。
+- 否则宿主姿态会被清回单位姿态,后续增量恢复会错误地认为“不需要再旋转”。
+
+### 4.3 动画跟踪点语义
+
+- `AnimatedObjectTrackedPosition` 的唯一语义:
+ - 当前动画主链路使用的跟踪点位置。
+- 当前主链路已经切到:
+ - 几何中心跟踪。
+- 如果后续动画跟踪点再变,数据库和碰撞结果语义必须同步更新。
+
+### 4.4 虚拟物体资源
+
+- 虚拟物体资源的局部几何中心必须在原点。
+- 如果资源局部原点不对,上层姿态和坐标框架再正确,也会表现为固定偏差。
+- 日志里优先看:
+ - 包围盒中心
+ - 目标中心
+ - 应用后偏差
+- 不要再用虚拟物体 `Transform` 的即时读回值判断 override 后的真实姿态。
+
+### 4.5 地面/吊装路径
+
+- 地面/吊装路径已经开始走完整姿态链路,不再允许悄悄退回旧 `yaw` 方案。
+- 如果完整姿态生成失败,应直接暴露错误,而不是 fallback。
+
+## 5. 当前保留的日志策略
+
+- 保留:
+ - 终点诊断
+ - 保存/恢复姿态
+ - 关键碰撞恢复
+ - 虚拟物体应用后中心/偏差
+- 已降级或删除:
+ - 大量重复逐帧宿主姿态轴日志
+ - 虚拟物体 `Transform` 即时读回日志(容易误导)
+
+## 6. 当前还值得继续观察的点
+
+- 空轨路径里仍有一个旧 warning:
+ - `[空轨] 双轨几何中心线提取失败,退回 OBB 主轴中线`
+- 这与当前 `YUp` 地面路径问题无关,但后续可以单独处理。
+
+## 7. 下一步建议
+
+- 继续按“先框架、先测试、后接业务”的方式推进。
+- 新问题优先:
+ 1. 先看日志
+ 2. 若日志不足,先补日志
+ 3. 再补针对性的回归测试
+ 4. 最后再改业务代码
+
+## 8. 当前阶段的工作边界
+
+- 不要回到“直接补旧 `yaw` 链路”的方式。
+- 不要再增加隐藏错误的 fallback。
+- 不要再混淆:
+ - 外部宿主坐标
+ - 内部 `Canonical` 坐标
+ - 业务参考点
+ - 模型局部轴约定
diff --git a/resources/unit_cube.nwc b/resources/unit_cube.nwc
index e6027ce47495797ebe1ff8ec9c4142c716e11cbd..81352deb7d52fddb0e9bad05eb41fd6d221d17cf 100644
GIT binary patch
delta 1094
zcmV-M1iAa-4fhR@IDfH913?gm@3hduMmsB?AP0gr*QO0o;p`5U$?il}=WdsoTSML;
zWnRH25KBRPn~0BKr*mFX3I6cyEX%+D{0G^f8)aP9w}LEng+vY35Tn#nc=WOe{2&!W
zD=~04V(>I-LTWob9ZJn8A+gn4YJF#Hnqd)U_nP^w>hmw(-hX{Rzw%+b%d_qQE&(@4*d7Vv01fsU
zc26xCp*#RvQV%+KoMUBRU|Q7oMU8w0000G00MZN3uKd311f*f
zElSMH(ap^-%_{+tDXGOVMXAO4rA5g=R%u>lNqll?QmS5Fc{0$<UPhcpy?u!qrA4BdZzDZaEv6<2W}PCur7
zOlT^ZP{cl9xQh*qCJtbl0|gDp^6ZoK10Mu3oi0z47zA*C%?iRW5QOtAdu|1_T7x23xg5h+b6XJx%NbaTzs>X9!r5=+CR=|HO*(hC)6Y1
zDFs1<7gT-b=)+~_?Fll8fTc<4-qD>e`!8-xQu@2@fm!BnXuebP{fzo<%EkA6=$g&*
z-Kjap&s`4~EXHI98GEd}gFgP(+gX_qvezpL(~}TX+m(Mof2VoX2UhOU5YE_`*Xr)I
z%AII#9i6Nt3tFhmC3a$MQuP
M1poj50001xG^|Vd9RL6T
delta 1032
zcmV+j1o!**4dM-uIDautLop17U+Dk?OWByXK`JB`-h|Y(Dh!V;QsgEEr6ezr{X*3n
zbnF#4078r$sEAvD>1!t>e(94X`}c1jSG{h8Nv-c0iFJb^MAVQo>lr+IUM70thM?t~
zsMCa;3Rz@bTHhI4=BUzq@hy5CU;E#@@vzT57p8W$w9^C*>3=SiN8i@L1}6(L8ypz)
zbtDum%=|Njr-Uj>3IKyVodE&;(sAh-YoAUQT5W`WWSE-o%0
z3IH>yD)xAsV`N}pURV_x8%g~7sk&v!>dWa!|;q9G@+>kbYy
z^KBaHX|$s`=#u53PAF-~G9e0UuHw1*n!LN6e4G4d
zw!gf;QFbs?BTQpekpv=>KLss+6mG;crfPc7n+MOvFA$}gU|Yo)4<=?^rezgavd}b~
zdhuZ5&-gLkJ$d&JaJLL9E80yao4`C~XV|hV%Z_0fwhWIB)1yO&&=8Cjqgn^Je;CcR
zBZI2U8>I%bpbtETToewEQvq2f--~7wV=Zbjfo-p5%(UsAE)14m@0l|O=
zkSX$t&erN}>25?s&T__+s@@yAXU#S0kuNk7JYCegU#;>wKh9~l&Bx;tMoDzSKoCL0
zy3YcAwCudDAd?u9JgeM0y0c~f#ZO7je%IYMtNabkcN@MxWr3e_@qHg$vwgnX4L1x5
zpF#qQF*)Hhd8qw4`rCg~y)zX;_Ikx&b{v6fyY?^WL(Qu`u(Hb{I8Da9QMYGRJJH-a
zI(cnlp7` 0 &&
+ (Math.Abs(previousFramePoint.X - nextFramePoint.X) > 1e-6 ||
+ Math.Abs(previousFramePoint.Y - nextFramePoint.Y) > 1e-6 ||
+ Math.Abs(previousFramePoint.Z - nextFramePoint.Z) > 1e-6))
+ {
+ LogManager.Debug(
+ $"[平面转弯诊断] 帧={i}, edge={edgeIndex}, " +
+ $"prev=({previousFramePoint.X:F3},{previousFramePoint.Y:F3},{previousFramePoint.Z:F3}), " +
+ $"cur=({framePosition.X:F3},{framePosition.Y:F3},{framePosition.Z:F3}), " +
+ $"next=({nextFramePoint.X:F3},{nextFramePoint.Y:F3},{nextFramePoint.Z:F3}), " +
+ $"yaw={yawRadians * 180.0 / Math.PI:F2}°");
+ }
}
// 创建帧并检测碰撞
@@ -999,6 +1042,17 @@ namespace NavisworksTransport.Core.Animation
frame.Rotation = railRotation;
frame.HasCustomRotation = true;
}
+ else if ((_route.PathType == PathType.Ground || _route.PathType == PathType.Hoisting) &&
+ TryCreatePlanarPathRotationForFrame(previousFramePoint, framePosition, nextFramePoint, out var planarRotation))
+ {
+ frame.Rotation = planarRotation;
+ frame.HasCustomRotation = true;
+ }
+ else if (_route.PathType == PathType.Ground || _route.PathType == PathType.Hoisting)
+ {
+ throw new InvalidOperationException(
+ $"平面路径第 {i} 帧未能生成完整姿态,已禁止退回旧 yaw 链路。");
+ }
// 记录第一帧的角度(用于调试)
if (i == 0)
@@ -1381,7 +1435,7 @@ namespace NavisworksTransport.Core.Animation
bool needsReset;
string mismatchSummary;
- if (firstFrame.HasCustomRotation && _route?.PathType == PathType.Rail)
+ if (firstFrame.HasCustomRotation)
{
var dx = _trackedPosition.X - firstFrame.Position.X;
var dy = _trackedPosition.Y - firstFrame.Position.Y;
@@ -2380,6 +2434,10 @@ namespace NavisworksTransport.Core.Animation
$"目标跟踪中心=({newPosition.X:F3},{newPosition.Y:F3},{newPosition.Z:F3}), " +
$"偏差=({hostActualAfter.X - newPosition.X:F3},{hostActualAfter.Y - newPosition.Y:F3},{hostActualAfter.Z - newPosition.Z:F3})");
}
+ else if (_route?.PathType == PathType.Ground || _route?.PathType == PathType.Hoisting)
+ {
+ LogHostActualPoseAxes(_isVirtualObject ? VirtualObjectManager.Instance.CurrentVirtualObject : _animatedObject, "[平面姿态应用后宿主姿态]", false);
+ }
}
catch (Exception ex)
{
@@ -2754,7 +2812,7 @@ namespace NavisworksTransport.Core.Animation
// 更新对象位置和朝向
var frameData = _animationFrames[_currentFrameIndex];
- if (frameData.HasCustomRotation && _route?.PathType == PathType.Rail)
+ if (frameData.HasCustomRotation)
{
UpdateObjectPosition(frameData.Position, frameData.Rotation);
}
@@ -3306,12 +3364,6 @@ namespace NavisworksTransport.Core.Animation
return new Vector3((float)point.X, (float)point.Y, (float)point.Z);
}
- ///
- /// Rail 路径的目标姿态以“本地 X 为前进方向、本地 Z 为上方向”为约定。
- /// 真实模型在 Y-up 宿主里通常仍保持“本地 Y 为上方向”的资源语义,
- /// 因此需要补一个基准修正:绕本地 X 轴 +90°,使模型的 Y-up 语义映射到 Rail 的 Z-up 期望语义。
- /// 虚拟物体资源本身按 Z-up 构建,不需要此修正。
- ///
private ModelAxisConvention GetCurrentRailModelAxisConvention()
{
if (_isVirtualObject)
@@ -3327,6 +3379,133 @@ namespace NavisworksTransport.Core.Animation
return convention;
}
+ private ModelAxisConvention GetCurrentModelAxisConvention()
+ {
+ if (_isVirtualObject)
+ {
+ return ModelAxisConvention.CreateDefaultForHost(CoordinateSystemType.ZUp);
+ }
+
+ var adapter = CoordinateSystemManager.Instance.CreateHostAdapter();
+ return ModelAxisConvention.CreateDefaultForHost(adapter.HostType);
+ }
+
+ private bool TryCreatePlanarPathRotationAtStart(out Rotation3D rotation)
+ {
+ rotation = Rotation3D.Identity;
+
+ if (_pathPoints == null || _pathPoints.Count < 2)
+ {
+ return false;
+ }
+
+ if (_route?.PathType == PathType.Hoisting)
+ {
+ if (_pathPoints.Count < 3)
+ {
+ return false;
+ }
+
+ return TryCreatePlanarPathRotationFromHostForward(
+ new Vector3D(
+ _pathPoints[2].X - _pathPoints[1].X,
+ _pathPoints[2].Y - _pathPoints[1].Y,
+ _pathPoints[2].Z - _pathPoints[1].Z),
+ out rotation);
+ }
+
+ return TryCreatePlanarPathRotationFromHostPoints(_pathPoints[0], _pathPoints[0], _pathPoints[1], out rotation);
+ }
+
+ private bool TryCreatePlanarPathRotationForFrame(Point3D previousPoint, Point3D currentPoint, Point3D nextPoint, out Rotation3D rotation)
+ {
+ rotation = Rotation3D.Identity;
+
+ if (_route?.PathType == PathType.Hoisting)
+ {
+ if (_pathPoints == null || _pathPoints.Count < 3)
+ {
+ return false;
+ }
+
+ return TryCreatePlanarPathRotationFromHostForward(
+ new Vector3D(
+ _pathPoints[2].X - _pathPoints[1].X,
+ _pathPoints[2].Y - _pathPoints[1].Y,
+ _pathPoints[2].Z - _pathPoints[1].Z),
+ out rotation);
+ }
+
+ return TryCreatePlanarPathRotationFromHostPoints(previousPoint, currentPoint, nextPoint, out rotation);
+ }
+
+ private bool TryCreatePlanarPathRotationFromHostPoints(Point3D previousPoint, Point3D currentPoint, Point3D nextPoint, out Rotation3D rotation)
+ {
+ rotation = Rotation3D.Identity;
+
+ var adapter = CoordinateSystemManager.Instance.CreateHostAdapter();
+ var convention = GetCurrentModelAxisConvention();
+ Vector3 canonicalPrevious = ToNumerics(adapter.ToCanonicalPoint(previousPoint));
+ Vector3 canonicalCurrent = ToNumerics(adapter.ToCanonicalPoint(currentPoint));
+ Vector3 canonicalNext = ToNumerics(adapter.ToCanonicalPoint(nextPoint));
+
+ Vector3 forward = canonicalNext - canonicalPrevious;
+ if (forward.LengthSquared() < 1e-6f)
+ {
+ forward = canonicalNext - canonicalCurrent;
+ }
+
+ forward = ApplyPlanarRotationCorrection(forward);
+
+ if (!CanonicalPlanarPoseBuilder.TryCreateQuaternionFromForward(
+ forward,
+ HostCoordinateAdapter.CanonicalUpVector3,
+ convention,
+ out var quaternion))
+ {
+ return false;
+ }
+
+ rotation = adapter.FromCanonicalRotation(quaternion);
+ return true;
+ }
+
+ private bool TryCreatePlanarPathRotationFromHostForward(Vector3D hostForward, out Rotation3D rotation)
+ {
+ rotation = Rotation3D.Identity;
+
+ var adapter = CoordinateSystemManager.Instance.CreateHostAdapter();
+ var convention = GetCurrentModelAxisConvention();
+ Vector3 canonicalForward = adapter.ToCanonicalVector3(new Vector3((float)hostForward.X, (float)hostForward.Y, (float)hostForward.Z));
+ canonicalForward = ApplyPlanarRotationCorrection(canonicalForward);
+
+ if (!CanonicalPlanarPoseBuilder.TryCreateQuaternionFromForward(
+ canonicalForward,
+ HostCoordinateAdapter.CanonicalUpVector3,
+ convention,
+ out var quaternion))
+ {
+ return false;
+ }
+
+ rotation = adapter.FromCanonicalRotation(quaternion);
+ return true;
+ }
+
+ private Vector3 ApplyPlanarRotationCorrection(Vector3 canonicalForward)
+ {
+ if (Math.Abs(_objectRotationCorrection) < 1e-9)
+ {
+ return canonicalForward;
+ }
+
+ float correctionRadians = (float)(_objectRotationCorrection * Math.PI / 180.0);
+ Quaternion correction = Quaternion.CreateFromAxisAngle(
+ Vector3.Normalize(HostCoordinateAdapter.CanonicalUpVector3),
+ correctionRadians);
+ return Vector3.Transform(canonicalForward, correction);
+ }
+
///
/// 设置物体角度修正值(度,顺时针)
///
@@ -3488,10 +3667,15 @@ namespace NavisworksTransport.Core.Animation
actualYaw += correctionRad;
}
- if (frameData.HasCustomRotation && _route?.PathType == PathType.Rail)
+ if (frameData.HasCustomRotation)
{
UpdateObjectPosition(frameData.Position, frameData.Rotation);
}
+ else if (_route?.PathType == PathType.Ground || _route?.PathType == PathType.Hoisting)
+ {
+ throw new InvalidOperationException(
+ $"平面路径播放帧 {_currentFrameIndex} 缺少完整姿态,禁止退回旧 yaw 链路。");
+ }
else
{
UpdateObjectPosition(frameData.Position, actualYaw);
diff --git a/src/Core/VirtualObjectManager.cs b/src/Core/VirtualObjectManager.cs
index b1ea2dd..2809375 100644
--- a/src/Core/VirtualObjectManager.cs
+++ b/src/Core/VirtualObjectManager.cs
@@ -119,6 +119,8 @@ namespace NavisworksTransport.Core
_virtualObjectModelItem = geometryItem;
}
+ LogVirtualObjectGeometry("[虚拟物体创建] Append后缩放前");
+
_currentLengthMeters = lengthMeters;
_currentWidthMeters = widthMeters;
_currentHeightMeters = heightMeters;
@@ -255,7 +257,15 @@ namespace NavisworksTransport.Core
try
{
+ LogVirtualObjectGeometry("[虚拟物体姿态] 应用前");
ModelItemTransformHelper.MoveItemToPositionAndRotation(_virtualObjectModelItem, position, rotation);
+ var actualBounds = _virtualObjectModelItem.BoundingBox();
+ Point3D actualCenter = actualBounds?.Center ?? new Point3D(0, 0, 0);
+ LogManager.Info(
+ $"[虚拟物体姿态] 应用后中心=({actualCenter.X:F3},{actualCenter.Y:F3},{actualCenter.Z:F3}), " +
+ $"目标中心=({position.X:F3},{position.Y:F3},{position.Z:F3}), " +
+ $"偏差=({actualCenter.X - position.X:F3},{actualCenter.Y - position.Y:F3},{actualCenter.Z - position.Z:F3})");
+ LogVirtualObjectGeometry("[虚拟物体姿态] 应用后");
LogManager.Debug("虚拟物体已应用完整三维姿态");
}
catch (Exception ex)
@@ -370,6 +380,33 @@ namespace NavisworksTransport.Core
Transform3D newTransform = transformComponents.Combine();
doc.Models.SetModelUnitsAndTransform(_virtualObjectModel, _virtualObjectModel.Units, newTransform, false);
+
+ LogVirtualObjectGeometry("[虚拟物体缩放] 完成后");
+ }
+
+ private void LogVirtualObjectGeometry(string prefix)
+ {
+ if (_virtualObjectModelItem == null)
+ {
+ return;
+ }
+
+ try
+ {
+ var itemBounds = _virtualObjectModelItem.BoundingBox();
+
+ if (itemBounds != null)
+ {
+ LogManager.Info(
+ $"{prefix} Item包围盒: Min=({itemBounds.Min.X:F3},{itemBounds.Min.Y:F3},{itemBounds.Min.Z:F3}), " +
+ $"Max=({itemBounds.Max.X:F3},{itemBounds.Max.Y:F3},{itemBounds.Max.Z:F3}), " +
+ $"Center=({itemBounds.Center.X:F3},{itemBounds.Center.Y:F3},{itemBounds.Center.Z:F3})");
+ }
+ }
+ catch (Exception ex)
+ {
+ LogManager.Warning($"{prefix} 记录虚拟物体几何失败: {ex.Message}");
+ }
}
///
diff --git a/src/Utils/CoordinateSystem/CanonicalPlanarPoseBuilder.cs b/src/Utils/CoordinateSystem/CanonicalPlanarPoseBuilder.cs
new file mode 100644
index 0000000..1d87f49
--- /dev/null
+++ b/src/Utils/CoordinateSystem/CanonicalPlanarPoseBuilder.cs
@@ -0,0 +1,57 @@
+using System;
+using System.Numerics;
+
+namespace NavisworksTransport.Utils.CoordinateSystem
+{
+ ///
+ /// 在内部 Canonical Space 中,为地面/吊装这类“法向取项目 up”的路径构造姿态。
+ /// 这一层只处理纯数学的前进方向和上方向,不依赖 Navisworks API。
+ ///
+ public static class CanonicalPlanarPoseBuilder
+ {
+ private const float ForwardEpsilon = 1e-6f;
+
+ public static bool TryCreateQuaternionFromPoints(
+ Vector3 previousPoint,
+ Vector3 currentPoint,
+ Vector3 nextPoint,
+ Vector3 canonicalUp,
+ ModelAxisConvention convention,
+ out Quaternion rotation)
+ {
+ rotation = Quaternion.Identity;
+
+ Vector3 tangent = nextPoint - previousPoint;
+ if (tangent.LengthSquared() < ForwardEpsilon)
+ {
+ tangent = nextPoint - currentPoint;
+ }
+
+ return TryCreateQuaternionFromForward(tangent, canonicalUp, convention, out rotation);
+ }
+
+ public static bool TryCreateQuaternionFromForward(
+ Vector3 desiredForward,
+ Vector3 canonicalUp,
+ ModelAxisConvention convention,
+ out Quaternion rotation)
+ {
+ if (convention == null)
+ {
+ throw new ArgumentNullException(nameof(convention));
+ }
+
+ rotation = Quaternion.Identity;
+
+ Vector3 projectedForward = desiredForward - Vector3.Dot(desiredForward, canonicalUp) * canonicalUp;
+ if (projectedForward.LengthSquared() < ForwardEpsilon)
+ {
+ return false;
+ }
+
+ Vector3 normalizedForward = Vector3.Normalize(projectedForward);
+ rotation = convention.CreateQuaternion(normalizedForward, Vector3.Normalize(canonicalUp));
+ return true;
+ }
+ }
+}
diff --git a/src/Utils/CoordinateSystem/HostCoordinateAdapter.cs b/src/Utils/CoordinateSystem/HostCoordinateAdapter.cs
index 4fb862f..fd1f814 100644
--- a/src/Utils/CoordinateSystem/HostCoordinateAdapter.cs
+++ b/src/Utils/CoordinateSystem/HostCoordinateAdapter.cs
@@ -165,6 +165,76 @@ namespace NavisworksTransport.Utils.CoordinateSystem
return FromCanonicalPoint3(canonicalVector);
}
+ public Matrix4x4 FromCanonicalLinearTransform(Matrix4x4 canonicalLinear)
+ {
+ Vector3 hostX = FromCanonicalVector3(new Vector3(canonicalLinear.M11, canonicalLinear.M21, canonicalLinear.M31));
+ Vector3 hostY = FromCanonicalVector3(new Vector3(canonicalLinear.M12, canonicalLinear.M22, canonicalLinear.M32));
+ Vector3 hostZ = FromCanonicalVector3(new Vector3(canonicalLinear.M13, canonicalLinear.M23, canonicalLinear.M33));
+
+ return new Matrix4x4(
+ hostX.X, hostY.X, hostZ.X, 0f,
+ hostX.Y, hostY.Y, hostZ.Y, 0f,
+ hostX.Z, hostY.Z, hostZ.Z, 0f,
+ 0f, 0f, 0f, 1f);
+ }
+
+ public Rotation3D FromCanonicalRotation(Quaternion canonicalRotation)
+ {
+ Matrix4x4 canonicalLinear = Matrix4x4.CreateFromQuaternion(canonicalRotation);
+ Matrix4x4 hostLinear = FromCanonicalLinearTransform(canonicalLinear);
+ return CreateRotationFromLinearComponents(
+ hostLinear.M11, hostLinear.M12, hostLinear.M13,
+ hostLinear.M21, hostLinear.M22, hostLinear.M23,
+ hostLinear.M31, hostLinear.M32, hostLinear.M33);
+ }
+
+ private static Rotation3D CreateRotationFromLinearComponents(
+ double m00, double m01, double m02,
+ double m10, double m11, double m12,
+ double m20, double m21, double m22)
+ {
+ double qx;
+ double qy;
+ double qz;
+ double qw;
+
+ double trace = m00 + m11 + m22;
+ if (trace > 0.0)
+ {
+ double s = Math.Sqrt(trace + 1.0) * 2.0;
+ qw = 0.25 * s;
+ qx = (m21 - m12) / s;
+ qy = (m02 - m20) / s;
+ qz = (m10 - m01) / s;
+ }
+ else if (m00 > m11 && m00 > m22)
+ {
+ double s = Math.Sqrt(1.0 + m00 - m11 - m22) * 2.0;
+ qw = (m21 - m12) / s;
+ qx = 0.25 * s;
+ qy = (m01 + m10) / s;
+ qz = (m02 + m20) / s;
+ }
+ else if (m11 > m22)
+ {
+ double s = Math.Sqrt(1.0 + m11 - m00 - m22) * 2.0;
+ qw = (m02 - m20) / s;
+ qx = (m01 + m10) / s;
+ qy = 0.25 * s;
+ qz = (m12 + m21) / s;
+ }
+ else
+ {
+ double s = Math.Sqrt(1.0 + m22 - m00 - m11) * 2.0;
+ qw = (m10 - m01) / s;
+ qx = (m02 + m20) / s;
+ qy = (m12 + m21) / s;
+ qz = 0.25 * s;
+ }
+
+ return new Rotation3D(qx, qy, qz, qw);
+ }
+
public CanonicalBounds3 ToCanonicalBounds3(CanonicalBounds3 hostBounds)
{
return RebuildBounds3(hostBounds, ToCanonicalPoint3);
From 8f55b5e9a1dcb301dddb248e29868044f4da374b Mon Sep 17 00:00:00 2001
From: tian <11429339@qq.com>
Date: Sun, 22 Mar 2026 12:21:25 +0800
Subject: [PATCH 13/87] Fix YUp hoisting path rendering and finish sync
---
NavisworksTransport.UnitTests.csproj | 1 +
TransportPlugin.csproj | 1 +
.../HoistingCoordinateHelperTests.cs | 146 ++++++++++++
doc/working/current-engineering-state.md | 16 +-
src/Core/PathPlanningManager.cs | 214 +++++++++++-------
src/Core/PathPointRenderPlugin.cs | 86 ++++---
src/Core/UIStateManager.cs | 77 ++++---
src/PathPlanning/AerialPathGenerator.cs | 58 +++--
src/UI/WPF/ViewModels/PathEditingViewModel.cs | 180 ++++++++-------
.../HoistingCoordinateHelper.cs | 205 +++++++++++++++++
10 files changed, 713 insertions(+), 271 deletions(-)
create mode 100644 UnitTests/CoordinateSystem/HoistingCoordinateHelperTests.cs
create mode 100644 src/Utils/CoordinateSystem/HoistingCoordinateHelper.cs
diff --git a/NavisworksTransport.UnitTests.csproj b/NavisworksTransport.UnitTests.csproj
index 619c514..8cd57ca 100644
--- a/NavisworksTransport.UnitTests.csproj
+++ b/NavisworksTransport.UnitTests.csproj
@@ -58,6 +58,7 @@
+
diff --git a/TransportPlugin.csproj b/TransportPlugin.csproj
index f1e1a5c..9ab3261 100644
--- a/TransportPlugin.csproj
+++ b/TransportPlugin.csproj
@@ -337,6 +337,7 @@
+
diff --git a/UnitTests/CoordinateSystem/HoistingCoordinateHelperTests.cs b/UnitTests/CoordinateSystem/HoistingCoordinateHelperTests.cs
new file mode 100644
index 0000000..c660ac8
--- /dev/null
+++ b/UnitTests/CoordinateSystem/HoistingCoordinateHelperTests.cs
@@ -0,0 +1,146 @@
+using Autodesk.Navisworks.Api;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using NavisworksTransport.Utils.CoordinateSystem;
+using System.Numerics;
+
+namespace NavisworksTransport.UnitTests.CoordinateSystem
+{
+ [TestClass]
+ public class HoistingCoordinateHelperTests
+ {
+ [TestMethod]
+ public void OffsetAlongUp_YUp_ShouldChangeHostYOnly()
+ {
+ var adapter = new HostCoordinateAdapter(CoordinateSystemType.YUp);
+ var startPoint = new Vector3(1.0f, 2.0f, 3.0f);
+
+ Vector3 liftedPoint = HoistingCoordinateHelper.OffsetAlongUp3(startPoint, 10.0, adapter);
+
+ AssertPoint(liftedPoint, 1.0, 12.0, 3.0);
+ }
+
+ [TestMethod]
+ public void ProjectToAerialElevation_YUp_ShouldKeepGroundHorizontalCoords()
+ {
+ var adapter = new HostCoordinateAdapter(CoordinateSystemType.YUp);
+ var groundPoint = new Vector3(4.0f, 5.0f, 6.0f);
+ var aerialReferencePoint = new Vector3(1.0f, 12.0f, 3.0f);
+
+ Vector3 projectedPoint = HoistingCoordinateHelper.ProjectToAerialElevation3(groundPoint, aerialReferencePoint, adapter);
+
+ AssertPoint(projectedPoint, 4.0, 12.0, 6.0);
+ }
+
+ [TestMethod]
+ public void RelativeHeight_YUp_ShouldUseHostYDifference()
+ {
+ var adapter = new HostCoordinateAdapter(CoordinateSystemType.YUp);
+ var groundPoint = new Vector3(-10.0f, -30.0f, 8.0f);
+ var aerialPoint = new Vector3(-10.0f, 35.0f, 8.0f);
+
+ double relativeHeight = HoistingCoordinateHelper.GetRelativeHeight3(groundPoint, aerialPoint, adapter);
+
+ Assert.AreEqual(65.0, relativeHeight, 1e-9);
+ }
+
+ [TestMethod]
+ public void SetElevation_YUp_ShouldRestoreClickedGroundElevationWithoutChangingHorizontalProjection()
+ {
+ var adapter = new HostCoordinateAdapter(CoordinateSystemType.YUp);
+ var aerialPoint = new Vector3(-180.50f, 28.17f, 14.83f);
+ var clickedGroundPoint = new Vector3(-180.50f, -12.40f, 14.83f);
+
+ Vector3 landingPoint = HoistingCoordinateHelper.SetElevation3(
+ aerialPoint,
+ HoistingCoordinateHelper.GetElevation(clickedGroundPoint, adapter),
+ adapter);
+
+ AssertPoint(landingPoint, -180.50, -12.40, 14.83);
+ }
+
+ [TestMethod]
+ public void CreateHorizontalTurnPoint_YUp_ShouldOperateInHostXZPlane()
+ {
+ var adapter = new HostCoordinateAdapter(CoordinateSystemType.YUp);
+ var currentPoint = new Vector3(-168.70f, 28.17f, -30.01f);
+ var nextPoint = new Vector3(-180.50f, 28.17f, 14.83f);
+
+ Vector3 turnPointKeepCurrentX = HoistingCoordinateHelper.CreateHorizontalTurnPoint3(
+ currentPoint,
+ nextPoint,
+ keepCurrentFirstHorizontalAxis: true,
+ adapter: adapter);
+ Vector3 turnPointKeepCurrentZ = HoistingCoordinateHelper.CreateHorizontalTurnPoint3(
+ currentPoint,
+ nextPoint,
+ keepCurrentFirstHorizontalAxis: false,
+ adapter: adapter);
+
+ AssertPoint(turnPointKeepCurrentX, -168.70, 28.17, 14.83);
+ AssertPoint(turnPointKeepCurrentZ, -180.50, 28.17, -30.01);
+ }
+
+ [TestMethod]
+ public void GetHorizontalDirection_YUp_ShouldProjectToHostXZPlane()
+ {
+ var adapter = new HostCoordinateAdapter(CoordinateSystemType.YUp);
+ var fromPoint = new Vector3(-164.81f, 45.45f, 74.69f);
+ var toPoint = new Vector3(-176.90f, 45.45f, 40.48f);
+
+ Vector3 horizontalDirection = HoistingCoordinateHelper.GetHorizontalDirection3(fromPoint, toPoint, adapter);
+
+ Assert.AreEqual(0.0, horizontalDirection.Y, 1e-5);
+ Assert.AreEqual(1.0, horizontalDirection.Length(), 1e-5);
+ Assert.IsTrue(horizontalDirection.X < 0.0f);
+ Assert.IsTrue(horizontalDirection.Z < 0.0f);
+ }
+
+ [TestMethod]
+ public void ExtendVerticalTransitionForObjectSpace_YUp_ShouldMoveLowerPointAlongHostY()
+ {
+ var adapter = new HostCoordinateAdapter(CoordinateSystemType.YUp);
+ var lowerPoint = new Vector3(-191.56f, 29.05f, -2.63f);
+
+ Vector3 extendedPoint = HoistingCoordinateHelper.ExtendVerticalTransitionForObjectSpace3(
+ lowerPoint,
+ 4.0,
+ isLowerPoint: true,
+ adapter: adapter);
+
+ AssertPoint(extendedPoint, -191.56, 25.05, -2.63);
+ }
+
+ [TestMethod]
+ public void ExtendVerticalTransitionForObjectSpace_YUp_ShouldNotChangeHigherPoint()
+ {
+ var adapter = new HostCoordinateAdapter(CoordinateSystemType.YUp);
+ var higherPoint = new Vector3(-191.56f, 45.45f, -2.63f);
+
+ Vector3 unchangedPoint = HoistingCoordinateHelper.ExtendVerticalTransitionForObjectSpace3(
+ higherPoint,
+ 4.0,
+ isLowerPoint: false,
+ adapter: adapter);
+
+ AssertPoint(unchangedPoint, -191.56, 45.45, -2.63);
+ }
+
+ [TestMethod]
+ public void OffsetAlongUp_ZUp_ShouldChangeHostZOnly()
+ {
+ var adapter = new HostCoordinateAdapter(CoordinateSystemType.ZUp);
+ var startPoint = new Vector3(1.0f, 2.0f, 3.0f);
+
+ Vector3 liftedPoint = HoistingCoordinateHelper.OffsetAlongUp3(startPoint, 10.0, adapter);
+
+ AssertPoint(liftedPoint, 1.0, 2.0, 13.0);
+ }
+
+ private static void AssertPoint(Vector3 actual, double x, double y, double z)
+ {
+ Assert.AreEqual(x, actual.X, 1e-5);
+ Assert.AreEqual(y, actual.Y, 1e-5);
+ Assert.AreEqual(z, actual.Z, 1e-5);
+ }
+ }
+}
diff --git a/doc/working/current-engineering-state.md b/doc/working/current-engineering-state.md
index f6a1035..af66461 100644
--- a/doc/working/current-engineering-state.md
+++ b/doc/working/current-engineering-state.md
@@ -1,6 +1,6 @@
# 当前工程状态
-更新时间:2026-03-21
+更新时间:2026-03-22
## 1. 当前稳定状态
@@ -79,6 +79,20 @@
- 地面/吊装路径已经开始走完整姿态链路,不再允许悄悄退回旧 `yaw` 方案。
- 如果完整姿态生成失败,应直接暴露错误,而不是 fallback。
+- `YUp` 吊装路径创建主链路已补齐到宿主坐标适配架构:
+ - 提升、水平移动、下降、终点落地都不能再把世界 `Z` 硬编码成“向上”。
+ - 终点必须使用用户最后一次点击的地面点,不能回填起点地面高程。
+- 吊装路径“设为终点直接结束”如果出现:
+ - 核心路径点数正确,但路径列表/路径点列表仍显示为 `0`
+ - 切到别的路径再切回来后才恢复正常
+ - 这类问题优先检查 `UIStateManager` 的 UI 队列消费,而不是先怀疑路径数据或坐标系。
+- 本次真实根因已经确认:
+ - `FinishEditing()` 期间会连续追加 `CurrentRouteChanged / PathPointsListUpdated / RouteGenerated` 等 UI 事件。
+ - 旧的 `UIStateManager.ProcessQueuedUpdates()` 只消费“当前这一批”队列项,执行过程中新增的后续事件会滞留。
+ - 所以路径数据其实已经完成,只是 UI 直到下一次路径切换时才把滞留事件顺带处理掉。
+- 当前修复原则:
+ - `UIStateManager` 必须持续消费后续批次,直到 UI 队列真正为空。
+ - 不要用“手动切换路径”“先选空再选回”“强行补 OnPropertyChanged”这类 UI 和稀泥方式掩盖队列问题。
## 5. 当前保留的日志策略
diff --git a/src/Core/PathPlanningManager.cs b/src/Core/PathPlanningManager.cs
index f4b23f4..ed6dfd4 100644
--- a/src/Core/PathPlanningManager.cs
+++ b/src/Core/PathPlanningManager.cs
@@ -9,6 +9,7 @@ using NavisworksTransport.Utils;
using NavisworksTransport.Core;
using NavisworksTransport.Core.Config;
using NavisworksTransport.Commands;
+using NavisworksTransport.Utils.CoordinateSystem;
namespace NavisworksTransport
{
@@ -1708,18 +1709,22 @@ namespace NavisworksTransport
// 计算下一个索引
int nextIndex = CurrentRoute.Points.Count;
- // 创建下降点:在落地点正上方,Z坐标为最后一个路径点的Z(保持当前高度)
+ // 创建下降点:在落地点正上方,保持当前空中高程(沿宿主 up)
var descendPoint = new PathPoint(
- new Point3D(lastPoint.Position.X, lastPoint.Position.Y, lastPoint.Position.Z),
+ lastPoint.Position,
"下降点",
PathPointType.WayPoint);
descendPoint.Direction = HoistingPointDirection.Vertical;
descendPoint.Index = nextIndex++;
LogManager.Info($"[吊装路径] 已生成下降点(在终点正上方): ({descendPoint.Position.X:F2}, {descendPoint.Position.Y:F2}, {descendPoint.Position.Z:F2})");
- // 创建落地点(终点):使用最后一个路径点的XY,使用起点Z作为地面高度
+ Point3D finalGroundPoint = _hoistingGroundIntermediatePoints.Count > 0
+ ? _hoistingGroundIntermediatePoints[_hoistingGroundIntermediatePoints.Count - 1]
+ : SetHoistingElevation(lastPoint.Position, GetHoistingElevation(startPoint.Position));
+
+ // 创建落地点(终点):使用用户最后一次点击的地面点,避免把终点高程错误吸回起点
var endPoint = new PathPoint(
- new Point3D(lastPoint.Position.X, lastPoint.Position.Y, startPoint.Position.Z),
+ finalGroundPoint,
"落地点",
PathPointType.EndPoint);
endPoint.Direction = HoistingPointDirection.Vertical;
@@ -1796,6 +1801,9 @@ namespace NavisworksTransport
// 触发路径点列表更新事件
RaisePathPointsListUpdated(CurrentRoute, "编辑完成");
+ // 结束编辑后主动再同步一次当前路径,确保UI中的占位路径能回填完整点列表
+ SetCurrentRouteInternal(CurrentRoute, triggerEvent: true);
+
// 触发路径生成事件
RaiseRouteGenerated(CurrentRoute, RouteGenerationMethod.Manual);
@@ -1910,12 +1918,12 @@ namespace NavisworksTransport
{
var startPoint = points[0];
var liftPoint = points[1];
- double newAerialZ = startPoint.Position.Z + route.LiftHeight;
- liftPoint.Position = new Point3D(startPoint.Position.X, startPoint.Position.Y, newAerialZ);
+ double newAerialElevation = GetHoistingElevation(startPoint.Position) + route.LiftHeight;
+ liftPoint.Position = SetHoistingElevation(startPoint.Position, newAerialElevation);
// 同步所有其他空中点Z坐标(索引2到n-2)
for (int i = 2; i <= points.Count - 2; i++)
{
- points[i].Position = new Point3D(points[i].Position.X, points[i].Position.Y, newAerialZ);
+ points[i].Position = SetHoistingElevation(points[i].Position, newAerialElevation);
}
LogManager.Debug($"[吊装路径] 修改起点,更新提升点位置: ({liftPoint.Position.X:F2}, {liftPoint.Position.Y:F2}, {liftPoint.Position.Z:F2}),同步所有空中点Z坐标");
}
@@ -1924,7 +1932,7 @@ namespace NavisworksTransport
{
var endPoint = points[modifiedPointIndex];
var descendPoint = points[modifiedPointIndex - 1];
- descendPoint.Position = new Point3D(endPoint.Position.X, endPoint.Position.Y, descendPoint.Position.Z);
+ descendPoint.Position = SetHoistingElevation(endPoint.Position, GetHoistingElevation(descendPoint.Position));
LogManager.Debug($"[吊装路径] 修改终点,更新下降点位置: ({descendPoint.Position.X:F2}, {descendPoint.Position.Y:F2}, {descendPoint.Position.Z:F2})");
}
}
@@ -1951,9 +1959,12 @@ namespace NavisworksTransport
LogManager.Debug($"[斜线处理] 当前点 [{startIndex}]: {currentPoint.Name}, 位置: ({currentPoint.Position.X:F2}, {currentPoint.Position.Y:F2}, {currentPoint.Position.Z:F2})");
LogManager.Debug($"[斜线处理] 下一点 [{startIndex + 1}]: {nextPoint.Name}, 位置: ({nextPoint.Position.X:F2}, {nextPoint.Position.Y:F2}, {nextPoint.Position.Z:F2})");
- double deltaX = Math.Abs(nextPoint.Position.X - currentPoint.Position.X);
- double deltaY = Math.Abs(nextPoint.Position.Y - currentPoint.Position.Y);
- double deltaZ = Math.Abs(nextPoint.Position.Z - currentPoint.Position.Z);
+ Point3D canonicalCurrent = ToHoistingCanonicalPoint(currentPoint.Position);
+ Point3D canonicalNext = ToHoistingCanonicalPoint(nextPoint.Position);
+
+ double deltaX = Math.Abs(canonicalNext.X - canonicalCurrent.X);
+ double deltaY = Math.Abs(canonicalNext.Y - canonicalCurrent.Y);
+ double deltaZ = Math.Abs(canonicalNext.Z - canonicalCurrent.Z);
LogManager.Debug($"[斜线处理] deltaX={deltaX:F2}, deltaY={deltaY:F2}, deltaZ={deltaZ:F2}");
@@ -2010,7 +2021,7 @@ namespace NavisworksTransport
// 不修改用户点击的坐标,只插入转折点
// 转折点:X用新点的X,Y用旧点的Y(先X方向移动,再Y方向移动)
return InsertIntermediatePoint(points, startIndex, currentPoint, nextPoint,
- new Point3D(nextPoint.Position.X, currentPoint.Position.Y, currentPoint.Position.Z),
+ CreateHorizontalTurnPoint(currentPoint.Position, nextPoint.Position, HoistingPointDirection.Longitudinal),
HoistingPointDirection.Longitudinal);
}
@@ -2024,7 +2035,7 @@ namespace NavisworksTransport
// 插入中间点:先水平移动(到新X,旧Z),目标点自然就是(新X,新Z)
return InsertIntermediatePoint(points, startIndex, currentPoint, nextPoint,
- new Point3D(nextPoint.Position.X, currentPoint.Position.Y, currentPoint.Position.Z),
+ CreateHorizontalTurnPoint(currentPoint.Position, nextPoint.Position, HoistingPointDirection.Longitudinal),
HoistingPointDirection.Longitudinal);
}
@@ -2038,7 +2049,7 @@ namespace NavisworksTransport
// 插入中间点:先水平移动(到新Y,旧Z),目标点自然就是(新Y,新Z)
return InsertIntermediatePoint(points, startIndex, currentPoint, nextPoint,
- new Point3D(currentPoint.Position.X, nextPoint.Position.Y, currentPoint.Position.Z),
+ CreateHorizontalTurnPoint(currentPoint.Position, nextPoint.Position, HoistingPointDirection.Lateral),
HoistingPointDirection.Lateral);
}
@@ -2060,7 +2071,7 @@ namespace NavisworksTransport
{
// 插入中间点:先X方向移动(保持Y),到目标X后再处理Y和Z
return InsertIntermediatePoint(points, startIndex, currentPoint, nextPoint,
- new Point3D(nextPoint.Position.X, currentPoint.Position.Y, currentPoint.Position.Z),
+ CreateHorizontalTurnPoint(currentPoint.Position, nextPoint.Position, HoistingPointDirection.Longitudinal),
HoistingPointDirection.Longitudinal);
}
else
@@ -2070,13 +2081,13 @@ namespace NavisworksTransport
if (deltaX > deltaY)
{
return InsertIntermediatePoint(points, startIndex, currentPoint, nextPoint,
- new Point3D(nextPoint.Position.X, currentPoint.Position.Y, currentPoint.Position.Z),
+ CreateHorizontalTurnPoint(currentPoint.Position, nextPoint.Position, HoistingPointDirection.Longitudinal),
HoistingPointDirection.Longitudinal);
}
else
{
return InsertIntermediatePoint(points, startIndex, currentPoint, nextPoint,
- new Point3D(currentPoint.Position.X, nextPoint.Position.Y, currentPoint.Position.Z),
+ CreateHorizontalTurnPoint(currentPoint.Position, nextPoint.Position, HoistingPointDirection.Lateral),
HoistingPointDirection.Lateral);
}
}
@@ -2182,10 +2193,12 @@ namespace NavisworksTransport
continue;
}
- // 判断三个点是否在同一直线上(需要在3D空间中共线)
- // 首先检查Z坐标是否相同(在同一平面上)
- double deltaZ1 = Math.Abs(currentPoint.Position.Z - prevPoint.Position.Z);
- double deltaZ2 = Math.Abs(nextPoint.Position.Z - currentPoint.Position.Z);
+ // 判断三个点是否在同一直线上(统一在 Canonical 水平面上判断)
+ Point3D canonicalPrevPoint = ToHoistingCanonicalPoint(prevPoint.Position);
+ Point3D canonicalCurrentPoint = ToHoistingCanonicalPoint(currentPoint.Position);
+ Point3D canonicalNextPoint = ToHoistingCanonicalPoint(nextPoint.Position);
+ double deltaZ1 = Math.Abs(canonicalCurrentPoint.Z - canonicalPrevPoint.Z);
+ double deltaZ2 = Math.Abs(canonicalNextPoint.Z - canonicalCurrentPoint.Z);
bool sameZ = deltaZ1 < tolerance && deltaZ2 < tolerance;
// 如果Z不同,这三个点一定不共线(一个是垂直过渡点)
@@ -2196,10 +2209,10 @@ namespace NavisworksTransport
}
// 在同一平面上,检查XY方向是否共线
- bool sameX = Math.Abs(prevPoint.Position.X - currentPoint.Position.X) < tolerance &&
- Math.Abs(currentPoint.Position.X - nextPoint.Position.X) < tolerance;
- bool sameY = Math.Abs(prevPoint.Position.Y - currentPoint.Position.Y) < tolerance &&
- Math.Abs(currentPoint.Position.Y - nextPoint.Position.Y) < tolerance;
+ bool sameX = Math.Abs(canonicalPrevPoint.X - canonicalCurrentPoint.X) < tolerance &&
+ Math.Abs(canonicalCurrentPoint.X - canonicalNextPoint.X) < tolerance;
+ bool sameY = Math.Abs(canonicalPrevPoint.Y - canonicalCurrentPoint.Y) < tolerance &&
+ Math.Abs(canonicalCurrentPoint.Y - canonicalNextPoint.Y) < tolerance;
bool isCollinear = sameX || sameY;
@@ -2267,7 +2280,7 @@ namespace NavisworksTransport
var layers = new Dictionary>();
for (int i = 1; i < points.Count - 1; i++) // 跳过起点(0)和终点(Count-1)
{
- double z = points[i].Position.Z;
+ double z = GetHoistingElevation(points[i].Position);
// 查找是否已有相同Z坐标的层(考虑浮点数精度)
bool found = false;
@@ -2352,7 +2365,7 @@ namespace NavisworksTransport
}
bool hasChanges = false;
- double layerZ = points[layerIndices[0]].Position.Z;
+ double layerZ = GetHoistingElevation(points[layerIndices[0]].Position);
// 在该层内进行环路检测
// n和m是layerIndices列表中的索引位置
@@ -2374,10 +2387,10 @@ namespace NavisworksTransport
var p4 = points[idx4].Position;
// 确保所有点都在同一层(水平线段,Z坐标严格相同,只考虑浮点数精度)
- if (Math.Abs(p1.Z - layerZ) > epsilon ||
- Math.Abs(p2.Z - layerZ) > epsilon ||
- Math.Abs(p3.Z - layerZ) > epsilon ||
- Math.Abs(p4.Z - layerZ) > epsilon)
+ if (Math.Abs(GetHoistingElevation(p1) - layerZ) > epsilon ||
+ Math.Abs(GetHoistingElevation(p2) - layerZ) > epsilon ||
+ Math.Abs(GetHoistingElevation(p3) - layerZ) > epsilon ||
+ Math.Abs(GetHoistingElevation(p4) - layerZ) > epsilon)
{
m++;
if (n + m + 1 >= layerIndices.Count)
@@ -2476,14 +2489,19 @@ namespace NavisworksTransport
private bool GetOrthogonalSegmentIntersection(Point3D a1, Point3D a2, Point3D b1, Point3D b2, out Point3D intersection)
{
intersection = new Point3D(0, 0, 0);
-
- // 判断线段A是水平还是垂直
- bool aIsHorizontal = Math.Abs(a1.Y - a2.Y) < 0.001;
- bool aIsVertical = Math.Abs(a1.X - a2.X) < 0.001;
-
+
+ Point3D canonicalA1 = ToHoistingCanonicalPoint(a1);
+ Point3D canonicalA2 = ToHoistingCanonicalPoint(a2);
+ Point3D canonicalB1 = ToHoistingCanonicalPoint(b1);
+ Point3D canonicalB2 = ToHoistingCanonicalPoint(b2);
+
+ // 判断线段A是水平还是垂直(在 Canonical 水平面 XY 中)
+ bool aIsHorizontal = Math.Abs(canonicalA1.Y - canonicalA2.Y) < 0.001;
+ bool aIsVertical = Math.Abs(canonicalA1.X - canonicalA2.X) < 0.001;
+
// 判断线段B是水平还是垂直
- bool bIsHorizontal = Math.Abs(b1.Y - b2.Y) < 0.001;
- bool bIsVertical = Math.Abs(b1.X - b2.X) < 0.001;
+ bool bIsHorizontal = Math.Abs(canonicalB1.Y - canonicalB2.Y) < 0.001;
+ bool bIsVertical = Math.Abs(canonicalB1.X - canonicalB2.X) < 0.001;
// 必须一条水平一条垂直才可能形成矩形环路
if ((aIsHorizontal && bIsHorizontal) || (aIsVertical && bIsVertical))
@@ -2495,9 +2513,9 @@ namespace NavisworksTransport
if (aIsVertical && bIsHorizontal)
{
// 交换
- var temp1 = a1; var temp2 = a2;
- a1 = b1; a2 = b2;
- b1 = temp1; b2 = temp2;
+ var temp1 = canonicalA1; var temp2 = canonicalA2;
+ canonicalA1 = canonicalB1; canonicalA2 = canonicalB2;
+ canonicalB1 = temp1; canonicalB2 = temp2;
aIsHorizontal = true; aIsVertical = false;
bIsHorizontal = false; bIsVertical = true;
}
@@ -2510,12 +2528,12 @@ namespace NavisworksTransport
// 水平线段A: y = a1.Y, x范围 [min(a1.X,a2.X), max(a1.X,a2.X)]
// 垂直线段B: x = b1.X, y范围 [min(b1.Y,b2.Y), max(b1.Y,b2.Y)]
- double aY = a1.Y;
- double bX = b1.X;
- double aMinX = Math.Min(a1.X, a2.X);
- double aMaxX = Math.Max(a1.X, a2.X);
- double bMinY = Math.Min(b1.Y, b2.Y);
- double bMaxY = Math.Max(b1.Y, b2.Y);
+ double aY = canonicalA1.Y;
+ double bX = canonicalB1.X;
+ double aMinX = Math.Min(canonicalA1.X, canonicalA2.X);
+ double aMaxX = Math.Max(canonicalA1.X, canonicalA2.X);
+ double bMinY = Math.Min(canonicalB1.Y, canonicalB2.Y);
+ double bMaxY = Math.Max(canonicalB1.Y, canonicalB2.Y);
// 检查垂直线段的X是否在水平线段的X范围内(不包括端点)
// 且水平线段的Y是否在垂直线段的Y范围内(不包括端点)
@@ -2526,7 +2544,10 @@ namespace NavisworksTransport
if (xInRange && yInRange)
{
- intersection = new Point3D(bX, aY, a1.Z);
+ intersection = GetHoistingHostAdapter().FromCanonicalPoint(new Point3D(
+ bX,
+ aY,
+ canonicalA1.Z));
return true;
}
@@ -2642,16 +2663,12 @@ namespace NavisworksTransport
if (isLiftPoint || isDescendPoint)
{
// 修改提升点或下降点的Z坐标,同步更新所有空中路径点的Z坐标
- double newHoistingHeight = newPosition.Z;
- double originalZ = pointToUpdate.Position.Z;
+ double newHoistingHeight = GetHoistingElevation(newPosition);
+ double originalZ = GetHoistingElevation(pointToUpdate.Position);
double heightDelta = newHoistingHeight - originalZ;
- // 更新当前点的XY和Z坐标
- pointToUpdate.Position = new Point3D(
- newPosition.X,
- newPosition.Y,
- newHoistingHeight
- );
+ // 更新当前点的宿主水平位置与高程
+ pointToUpdate.Position = SetHoistingElevation(newPosition, newHoistingHeight);
// 同步更新所有空中路径点的Z坐标
foreach (var point in route.Points)
@@ -2660,11 +2677,7 @@ namespace NavisworksTransport
// 跳过当前正在修改的点,避免重复更新
if (point.Name != "起吊点" && point.Name != "落地点" && point != pointToUpdate)
{
- point.Position = new Point3D(
- point.Position.X,
- point.Position.Y,
- point.Position.Z + heightDelta
- );
+ point.Position = OffsetPointAlongHoistingUp(point.Position, heightDelta);
}
}
@@ -2679,12 +2692,8 @@ namespace NavisworksTransport
// 空中路径点(非提升点、下降点):只更新XY坐标,保持原有吊装高度
else if (!isStartPoint && !isEndPoint)
{
- double originalZ = pointToUpdate.Position.Z;
- pointToUpdate.Position = new Point3D(
- newPosition.X,
- newPosition.Y,
- originalZ // 保留原有Z坐标(吊装高度)
- );
+ double originalZ = GetHoistingElevation(pointToUpdate.Position);
+ pointToUpdate.Position = SetHoistingElevation(newPosition, originalZ);
LogManager.Info($"[更新路径点] 吊装路径空中点 {pointToUpdate.Name},保留原有高度: {originalZ:F3}");
}
// 起吊点/落地点:更新完整位置
@@ -2739,6 +2748,41 @@ namespace NavisworksTransport
///
/// 吊装路径
/// 提升高度(从起吊点到提升点的高度差)
+ private HostCoordinateAdapter GetHoistingHostAdapter()
+ {
+ return CoordinateSystemManager.Instance.CreateHostAdapter();
+ }
+
+ private Point3D ToHoistingCanonicalPoint(Point3D hostPoint)
+ {
+ return GetHoistingHostAdapter().ToCanonicalPoint(hostPoint);
+ }
+
+ private double GetHoistingElevation(Point3D point)
+ {
+ return HoistingCoordinateHelper.GetElevation(point, GetHoistingHostAdapter());
+ }
+
+ private Point3D SetHoistingElevation(Point3D point, double elevation)
+ {
+ return HoistingCoordinateHelper.SetElevation(point, elevation, GetHoistingHostAdapter());
+ }
+
+ private Point3D OffsetPointAlongHoistingUp(Point3D point, double offset)
+ {
+ return HoistingCoordinateHelper.OffsetAlongUp(point, offset, GetHoistingHostAdapter());
+ }
+
+ private Point3D CreateHorizontalTurnPoint(Point3D currentPoint, Point3D nextPoint, HoistingPointDirection direction)
+ {
+ bool keepCurrentFirstHorizontalAxis = direction == HoistingPointDirection.Lateral;
+ return HoistingCoordinateHelper.CreateHorizontalTurnPoint(
+ currentPoint,
+ nextPoint,
+ keepCurrentFirstHorizontalAxis,
+ GetHoistingHostAdapter());
+ }
+
private double CalculateHoistingHeight(PathRoute route)
{
if (route == null || route.PathType != PathType.Hoisting || route.Points.Count < 2)
@@ -2755,8 +2799,8 @@ namespace NavisworksTransport
return 0.0;
}
- // 计算高度差(提升点Z - 起吊点Z)
- return liftPoint.Position.Z - startPoint.Position.Z;
+ // 计算高度差(沿宿主 up)
+ return GetHoistingElevation(liftPoint.Position) - GetHoistingElevation(startPoint.Position);
}
///
@@ -3209,13 +3253,14 @@ namespace NavisworksTransport
double liftHeightModelUnits = CurrentRoute.LiftHeight > 0
? CurrentRoute.LiftHeight
: UnitsConverter.ConvertFromMeters(3.0);
+ double targetElevation = GetHoistingElevation(CurrentRoute.Points[0].Position) + liftHeightModelUnits;
// 使用 AerialPathGenerator 生成空中路径点
var generator = new AerialPathGenerator();
confirmPoint = generator.GenerateSmartHoistingPoint(
previousPoint,
_previewPoint.Position, // 用户点击的地面位置
- liftHeightModelUnits, // 直接使用模型单位
+ targetElevation,
CurrentRoute.Points.Count > 0 ? CurrentRoute.Points.Count : 1);
LogManager.Info($"[预览点-吊装路径] 已生成空中路径点: {confirmPoint.Name}, 位置: ({confirmPoint.Position.X:F2}, {confirmPoint.Position.Y:F2}, {confirmPoint.Position.Z:F2})");
@@ -3908,7 +3953,7 @@ namespace NavisworksTransport
// 使用已转换的模型单位高度
var liftPoint = new PathPoint(
- new Point3D(_hoistingStartPoint.X, _hoistingStartPoint.Y, _hoistingStartPoint.Z + liftHeightModelUnits),
+ OffsetPointAlongHoistingUp(_hoistingStartPoint, liftHeightModelUnits),
"提升点",
PathPointType.WayPoint);
liftPoint.Direction = HoistingPointDirection.Vertical;
@@ -3995,30 +4040,26 @@ namespace NavisworksTransport
{
previousPoint = CurrentRoute.Points.Last().Position;
var lastPoint = CurrentRoute.Points.Last();
- LogManager.Info($"[吊装模式] 上一路径点: {lastPoint.Name}, Z={lastPoint.Position.Z:F2}");
+ LogManager.Info($"[吊装模式] 上一路径点: {lastPoint.Name}, Elevation={GetHoistingElevation(lastPoint.Position):F2}");
}
else
{
- previousPoint = new Point3D(
- _hoistingStartPoint.X,
- _hoistingStartPoint.Y,
- _hoistingStartPoint.Z + liftHeightModelUnits);
- LogManager.Info($"[吊装模式] 无上一路径点,使用起点+高度: Z={previousPoint.Z:F2}");
+ previousPoint = OffsetPointAlongHoistingUp(_hoistingStartPoint, liftHeightModelUnits);
+ LogManager.Info($"[吊装模式] 无上一路径点,使用起点+高度: Elevation={GetHoistingElevation(previousPoint):F2}");
}
// 记录原始地面点击位置
_hoistingGroundIntermediatePoints.Add(groundPoint);
LogManager.Debug($"[吊装模式] 已记录地面点击点: ({groundPoint.X:F2}, {groundPoint.Y:F2}, {groundPoint.Z:F2})");
- // 检查高度是否变化(比较绝对高度)
- // liftHeightModelUnits 是相对地面的高度,需要加上起点Z得到绝对高度
- double newAbsoluteHeight = _hoistingStartPoint.Z + liftHeightModelUnits;
- double heightDiff = Math.Abs(newAbsoluteHeight - previousPoint.Z);
+ // 检查高度是否变化(比较沿宿主 up 的绝对高程)
+ double newAbsoluteHeight = GetHoistingElevation(_hoistingStartPoint) + liftHeightModelUnits;
+ double heightDiff = Math.Abs(newAbsoluteHeight - GetHoistingElevation(previousPoint));
bool heightChanged = heightDiff > 0.001;
LogManager.Info($"[吊装模式] 起点=({_hoistingStartPoint.X:F2}, {_hoistingStartPoint.Y:F2}, {_hoistingStartPoint.Z:F2})");
LogManager.Info($"[吊装模式] 相对高度(米)={liftHeightMetersInput:F2}, 相对高度(模型)={liftHeightModelUnits:F2}");
- LogManager.Info($"[吊装模式] 新绝对高度={newAbsoluteHeight:F2}, 上一位置Z={previousPoint.Z:F2}");
+ LogManager.Info($"[吊装模式] 新绝对高度={newAbsoluteHeight:F2}, 上一位置Elevation={GetHoistingElevation(previousPoint):F2}");
// 步骤1:水平移动点(在旧高度上移动到新XY)
// 使用 AerialPathGenerator 生成水平路径点
@@ -4031,7 +4072,7 @@ namespace NavisworksTransport
var aerialPoint = generator.GenerateSmartHoistingPoint(
previousPoint,
groundPoint,
- previousPoint.Z, // 保持旧高度
+ GetHoistingElevation(previousPoint),
CurrentRoute.Points.Count > 0 ? CurrentRoute.Points.Count : 1);
if (aerialPoint != null)
@@ -4053,12 +4094,9 @@ namespace NavisworksTransport
if (heightChanged)
{
// 高度变化:垂直上升到新高度
- LogManager.Info($"[吊装模式] 高度变化,插入垂直过渡点: Z={previousPoint.Z:F2} -> Z={newAbsoluteHeight:F2}");
+ LogManager.Info($"[吊装模式] 高度变化,插入垂直过渡点: Elevation={GetHoistingElevation(previousPoint):F2} -> Elevation={newAbsoluteHeight:F2}");
- var verticalTransitionPoint = new Point3D(
- previousPoint.X,
- previousPoint.Y,
- newAbsoluteHeight);
+ var verticalTransitionPoint = SetHoistingElevation(previousPoint, newAbsoluteHeight);
var verticalPoint = new PathPoint(
verticalTransitionPoint,
$"高度过渡{CurrentRoute.Points.Count}",
diff --git a/src/Core/PathPointRenderPlugin.cs b/src/Core/PathPointRenderPlugin.cs
index ba45fb2..04a1fbc 100644
--- a/src/Core/PathPointRenderPlugin.cs
+++ b/src/Core/PathPointRenderPlugin.cs
@@ -1859,16 +1859,11 @@ namespace NavisworksTransport
if (visualization.PathRoute.PathType == NavisworksTransport.PathType.Hoisting &&
sortedPoints.Count >= 3)
{
- // 吊装路径:线段1是水平段(从点1到点2)
- var horizontalStart = sortedPoints[1].Position;
- var horizontalEnd = sortedPoints[2].Position;
- var dx = horizontalEnd.X - horizontalStart.X;
- var dy = horizontalEnd.Y - horizontalStart.Y;
- var length = Math.Sqrt(dx * dx + dy * dy);
- if (length > 0.001)
- {
- horizontalDirection = new Vector3D(dx / length, dy / length, 0);
- }
+ var adapter = CoordinateSystemManager.Instance.CreateHostAdapter();
+ horizontalDirection = ToVector3D(HoistingCoordinateHelper.GetHorizontalDirection3(
+ ToVector3(sortedPoints[1].Position),
+ ToVector3(sortedPoints[2].Position),
+ adapter));
}
// 直接使用路径点构建直线连线
@@ -1899,13 +1894,16 @@ namespace NavisworksTransport
int firstSegment = 0; // 起吊段
int lastSegment = totalSegments - 1; // 下降段
- // 检测Z方向变化来判断垂直段
- double dz = endPoint.Position.Z - startPoint.Position.Z;
+ // 沿宿主 up 判断垂直段,避免把世界 Z 误当成向上轴
+ var hostUp = GetHostUpVector();
double dx = endPoint.Position.X - startPoint.Position.X;
double dy = endPoint.Position.Y - startPoint.Position.Y;
- double horizontalDist = Math.Sqrt(dx * dx + dy * dy);
- // 如果Z变化占主导(垂直段),或者是第一个/最后一个线段
- bool isZDominant = Math.Abs(dz) > horizontalDist * 2.0; // 垂直变化是水平变化的2倍以上
+ double dz = endPoint.Position.Z - startPoint.Position.Z;
+ double upDelta = dx * hostUp.X + dy * hostUp.Y + dz * hostUp.Z;
+ double segmentLength = Math.Sqrt(dx * dx + dy * dy + dz * dz);
+ double horizontalDist = Math.Sqrt(Math.Max(0.0, segmentLength * segmentLength - upDelta * upDelta));
+ // 如果沿宿主 up 的变化占主导(垂直段),或者是第一个/最后一个线段
+ bool isZDominant = Math.Abs(upDelta) > horizontalDist * 2.0;
// 第一个线段和最后一个线段是起吊/下降段
// 中间的垂直段是层间过渡
@@ -1920,15 +1918,22 @@ namespace NavisworksTransport
bool isTurn90Horizontal = false;
if (!isVerticalSegment && visualization.PathRoute.PathType == NavisworksTransport.PathType.Hoisting && horizontalDirection != null)
{
- // 计算当前水平段方向
- var currentDx = endPoint.Position.X - startPoint.Position.X;
- var currentDy = endPoint.Position.Y - startPoint.Position.Y;
- var currentLength = Math.Sqrt(currentDx * currentDx + currentDy * currentDy);
+ var adapter = CoordinateSystemManager.Instance.CreateHostAdapter();
+ var currentDirection = ToVector3D(HoistingCoordinateHelper.GetHorizontalDirection3(
+ ToVector3(startPoint.Position),
+ ToVector3(endPoint.Position),
+ adapter));
+ var currentLength = Math.Sqrt(
+ currentDirection.X * currentDirection.X +
+ currentDirection.Y * currentDirection.Y +
+ currentDirection.Z * currentDirection.Z);
if (currentLength > 0.001)
{
- var currentDirection = new Vector3D(currentDx / currentLength, currentDy / currentLength, 0);
// 计算与初始水平段方向的点积
- double dotProduct = currentDirection.X * horizontalDirection.X + currentDirection.Y * horizontalDirection.Y;
+ double dotProduct =
+ currentDirection.X * horizontalDirection.X +
+ currentDirection.Y * horizontalDirection.Y +
+ currentDirection.Z * horizontalDirection.Z;
// 点积接近0表示垂直(90度转折)
isTurn90Horizontal = Math.Abs(dotProduct) < 0.1;
}
@@ -1991,27 +1996,26 @@ namespace NavisworksTransport
{
// 层间垂直过渡:通行空间总高度 = 层间路径高度 + 物体高度
// 顶面中心 = 路径高点,底面中心 = 路径低点向下物体高度
- bool isStartHigher = adjustedStartPoint.Z > adjustedEndPoint.Z;
+ var adapter = CoordinateSystemManager.Instance.CreateHostAdapter();
+ bool isStartHigher =
+ HoistingCoordinateHelper.GetElevation(adjustedStartPoint, adapter) >
+ HoistingCoordinateHelper.GetElevation(adjustedEndPoint, adapter);
if (isStartHigher)
{
- // 起点是高点,终点是低点
- // 顶面在起点,底面在终点向下延伸物体高度
- adjustedEndPoint = new Point3D(
- adjustedEndPoint.X,
- adjustedEndPoint.Y,
- adjustedEndPoint.Z - _objectHeight
- );
+ adjustedEndPoint = HoistingCoordinateHelper.ExtendVerticalTransitionForObjectSpace(
+ adjustedEndPoint,
+ _objectHeight,
+ isLowerPoint: true,
+ adapter: adapter);
}
else
{
- // 终点是高点,起点是低点
- // 顶面在终点,底面在起点向下延伸物体高度
- adjustedStartPoint = new Point3D(
- adjustedStartPoint.X,
- adjustedStartPoint.Y,
- adjustedStartPoint.Z - _objectHeight
- );
+ adjustedStartPoint = HoistingCoordinateHelper.ExtendVerticalTransitionForObjectSpace(
+ adjustedStartPoint,
+ _objectHeight,
+ isLowerPoint: true,
+ adapter: adapter);
}
}
// 起吊/下降段不需要延伸,因为底面对齐地面
@@ -2062,6 +2066,16 @@ namespace NavisworksTransport
);
}
+ private static System.Numerics.Vector3 ToVector3(Point3D point)
+ {
+ return new System.Numerics.Vector3((float)point.X, (float)point.Y, (float)point.Z);
+ }
+
+ private static Vector3D ToVector3D(System.Numerics.Vector3 vector)
+ {
+ return new Vector3D(vector.X, vector.Y, vector.Z);
+ }
+
private static Vector3D GetHostUpVector()
{
var adapter = CoordinateSystemManager.Instance.CreateHostAdapter();
diff --git a/src/Core/UIStateManager.cs b/src/Core/UIStateManager.cs
index 6fe44f6..826dd0b 100644
--- a/src/Core/UIStateManager.cs
+++ b/src/Core/UIStateManager.cs
@@ -778,46 +778,51 @@ namespace NavisworksTransport.Core
try
{
- var operations = new List();
-
- // 收集当前队列中的所有操作
- while (_updateQueue.TryDequeue(out var operation))
+ while (true)
{
- operations.Add(operation);
- }
+ var operations = new List();
- if (operations.Count == 0)
- {
- return;
- }
+ // 收集当前队列中的所有操作
+ while (_updateQueue.TryDequeue(out var operation))
+ {
+ operations.Add(operation);
+ }
- // 按优先级排序
- operations = operations.OrderByDescending(op => op.Priority).ToList();
+ if (operations.Count == 0)
+ {
+ break;
+ }
- LogManager.Info($"开始处理{operations.Count}个排队的UI更新操作,强制同步: {forcedSync}");
+ // 按优先级排序
+ operations = operations.OrderByDescending(op => op.Priority).ToList();
- // 批量执行操作
- var actions = operations.Select(op => op.Operation).ToArray();
-
- if (forcedSync)
- {
- // 使用同步版本的批量更新
- LogManager.Info("使用同步方式处理UI更新队列");
- ExecuteBatchUIUpdateSync(actions);
- LogManager.Info($"同步处理{operations.Count}个UI更新操作完成");
- }
- else
- {
- // 使用异步版本的批量更新
- LogManager.Info("使用异步方式处理UI更新队列");
- await ExecuteBatchUIUpdate(actions);
- LogManager.Info($"异步处理{operations.Count}个UI更新操作完成");
- }
-
- // 处理完成后,如果队列为空,停止保底定时器
- if (_updateQueue.IsEmpty)
- {
- StopIdleProcessor();
+ LogManager.Info($"开始处理{operations.Count}个排队的UI更新操作,强制同步: {forcedSync}");
+
+ // 批量执行操作
+ var actions = operations.Select(op => op.Operation).ToArray();
+
+ if (forcedSync)
+ {
+ // 使用同步版本的批量更新
+ LogManager.Info("使用同步方式处理UI更新队列");
+ ExecuteBatchUIUpdateSync(actions);
+ LogManager.Info($"同步处理{operations.Count}个UI更新操作完成");
+ }
+ else
+ {
+ // 使用异步版本的批量更新
+ LogManager.Info("使用异步方式处理UI更新队列");
+ await ExecuteBatchUIUpdate(actions);
+ LogManager.Info($"异步处理{operations.Count}个UI更新操作完成");
+ }
+
+ if (_updateQueue.IsEmpty)
+ {
+ StopIdleProcessor();
+ break;
+ }
+
+ LogManager.Info($"检测到执行过程中新增 {_updateQueue.Count} 个UI更新操作,继续处理后续批次");
}
}
catch (Exception ex)
@@ -952,4 +957,4 @@ namespace NavisworksTransport.Core
}
#endregion
-}
\ No newline at end of file
+}
diff --git a/src/PathPlanning/AerialPathGenerator.cs b/src/PathPlanning/AerialPathGenerator.cs
index 8130002..4867e9b 100644
--- a/src/PathPlanning/AerialPathGenerator.cs
+++ b/src/PathPlanning/AerialPathGenerator.cs
@@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using Autodesk.Navisworks.Api;
using NavisworksTransport.Utils;
+using NavisworksTransport.Utils.CoordinateSystem;
namespace NavisworksTransport.PathPlanning
{
@@ -12,12 +13,24 @@ namespace NavisworksTransport.PathPlanning
///
public class AerialPathGenerator
{
+ private readonly HostCoordinateAdapter _adapter;
+
///
/// 方向判断容差(米)
/// 当两个方向的距离差小于此值时,默认选择纵向
///
private const double DIRECTION_TOLERANCE = 0.001;
+ public AerialPathGenerator()
+ : this(HostCoordinateAdapter.CreateFromManager())
+ {
+ }
+
+ public AerialPathGenerator(HostCoordinateAdapter adapter)
+ {
+ _adapter = adapter ?? throw new ArgumentNullException(nameof(adapter));
+ }
+
///
/// 生成吊装路径(起点吊起 → 平移 → 终点吊下)
/// 保留原有方法以保持向后兼容
@@ -49,10 +62,7 @@ namespace NavisworksTransport.PathPlanning
pathPoints.Add(startPointPath);
// 点2:提升点(起点正上方,达到提升高度)
- var liftPoint = new Point3D(
- startPoint.X,
- startPoint.Y,
- startPoint.Z + liftHeightModelUnits);
+ var liftPoint = HoistingCoordinateHelper.OffsetAlongUp(startPoint, liftHeightModelUnits, _adapter);
var liftPointPath = new PathPoint(
liftPoint,
"提升点",
@@ -61,10 +71,7 @@ namespace NavisworksTransport.PathPlanning
pathPoints.Add(liftPointPath);
// 点3:平移终点(终点正上方,保持提升高度)
- var moveEndPoint = new Point3D(
- endPoint.X,
- endPoint.Y,
- liftPoint.Z); // 使用提升点的Z坐标,保持提升高度一致
+ var moveEndPoint = HoistingCoordinateHelper.ProjectToAerialElevation(endPoint, liftPoint, _adapter);
var moveEndPointPath = new PathPoint(
moveEndPoint,
"平移终点",
@@ -109,10 +116,10 @@ namespace NavisworksTransport.PathPlanning
double deltaY = Math.Abs(clickedGroundPoint.Y - previousPoint.Y);
// 使用目标高度作为Z坐标,XY使用地面点击位置
- Point3D newPoint = new Point3D(
- clickedGroundPoint.X,
- clickedGroundPoint.Y,
- targetHeightModelUnits); // 使用传入的目标高度
+ Point3D newPoint = HoistingCoordinateHelper.SetElevation(
+ clickedGroundPoint,
+ targetHeightModelUnits,
+ _adapter);
// 判断移动方向(仅用于记录,不影响位置)
HoistingPointDirection direction;
@@ -165,6 +172,7 @@ namespace NavisworksTransport.PathPlanning
var pathPoints = new List();
double liftHeightModelUnits = UnitsConverter.ConvertFromMeters(liftHeightMeters);
+ double liftElevation = HoistingCoordinateHelper.GetElevation(startPoint, _adapter) + liftHeightModelUnits;
// 点1:起吊点(地面起点)
var startPathPoint = new PathPoint(
@@ -175,10 +183,7 @@ namespace NavisworksTransport.PathPlanning
pathPoints.Add(startPathPoint);
// 点2:提升点(起点正上方,吊装高度)
- var liftPoint = new Point3D(
- startPoint.X,
- startPoint.Y,
- startPoint.Z + liftHeightModelUnits);
+ var liftPoint = HoistingCoordinateHelper.OffsetAlongUp(startPoint, liftHeightModelUnits, _adapter);
var liftPathPoint = new PathPoint(
liftPoint,
"提升点",
@@ -194,7 +199,7 @@ namespace NavisworksTransport.PathPlanning
var newPoint = GenerateSmartHoistingPoint(
previousPoint,
groundIntermediatePoints[i],
- liftHeightModelUnits, // 使用模型单位的高度
+ liftElevation,
i + 1);
pathPoints.Add(newPoint);
@@ -258,13 +263,10 @@ namespace NavisworksTransport.PathPlanning
{
var level = levels[i];
double levelHeightModelUnits = UnitsConverter.ConvertFromMeters(level.HeightInMeters);
- double currentHeight = startPoint.Z + levelHeightModelUnits;
+ double currentHeight = HoistingCoordinateHelper.GetElevation(startPoint, _adapter) + levelHeightModelUnits;
// 垂直移动点(上升或下降到该层级高度)
- var verticalPoint = new Point3D(
- currentPoint.X,
- currentPoint.Y,
- currentHeight);
+ var verticalPoint = HoistingCoordinateHelper.SetElevation(currentPoint, currentHeight, _adapter);
string verticalPointName = level.IsRise ? $"提升点{i + 1}" : $"下降点{i + 1}";
var verticalPathPoint = new PathPoint(
@@ -275,10 +277,7 @@ namespace NavisworksTransport.PathPlanning
pathPoints.Add(verticalPathPoint);
// 水平移动点(移动到该层级的目标位置,保持高度)
- var horizontalPoint = new Point3D(
- level.HorizontalTarget.X,
- level.HorizontalTarget.Y,
- currentHeight);
+ var horizontalPoint = HoistingCoordinateHelper.SetElevation(level.HorizontalTarget, currentHeight, _adapter);
string horizontalPointName = $"平移点{i + 1}";
var horizontalPathPoint = new PathPoint(
@@ -293,10 +292,7 @@ namespace NavisworksTransport.PathPlanning
}
// 最后:垂直下降到地面终点
- var descendPoint = new Point3D(
- currentPoint.X,
- currentPoint.Y,
- endPoint.Z);
+ var descendPoint = HoistingCoordinateHelper.ProjectToAerialElevation(endPoint, currentPoint, _adapter);
var descendPathPoint = new PathPoint(
descendPoint,
"最终下降点",
@@ -315,4 +311,4 @@ namespace NavisworksTransport.PathPlanning
return pathPoints;
}
}
-}
\ No newline at end of file
+}
diff --git a/src/UI/WPF/ViewModels/PathEditingViewModel.cs b/src/UI/WPF/ViewModels/PathEditingViewModel.cs
index 88fb975..daddc0c 100644
--- a/src/UI/WPF/ViewModels/PathEditingViewModel.cs
+++ b/src/UI/WPF/ViewModels/PathEditingViewModel.cs
@@ -2872,6 +2872,79 @@ namespace NavisworksTransport.UI.WPF.ViewModels
return points;
}
+ private bool SyncPathViewModelFromCoreRoute(PathRouteViewModel pathViewModel, PathRoute coreRoute, bool preserveSelection = true)
+ {
+ if (pathViewModel == null || coreRoute == null)
+ {
+ return false;
+ }
+
+ bool needsUpdate = !ReferenceEquals(pathViewModel.Route, coreRoute) ||
+ pathViewModel.Points.Count != coreRoute.Points.Count;
+
+ if (!needsUpdate && pathViewModel.Points.Count == coreRoute.Points.Count)
+ {
+ for (int i = 0; i < pathViewModel.Points.Count; i++)
+ {
+ var uiPoint = pathViewModel.Points[i];
+ var corePoint = coreRoute.Points[i];
+
+ if (uiPoint.Id != corePoint.Id ||
+ uiPoint.Name != corePoint.Name ||
+ uiPoint.Type != corePoint.Type)
+ {
+ needsUpdate = true;
+ break;
+ }
+
+ const double tolerance = 1e-6;
+ if (Math.Abs(uiPoint.X - corePoint.Position.X) > tolerance ||
+ Math.Abs(uiPoint.Y - corePoint.Position.Y) > tolerance ||
+ Math.Abs(uiPoint.Z - corePoint.Position.Z) > tolerance)
+ {
+ needsUpdate = true;
+ break;
+ }
+ }
+ }
+
+ pathViewModel.Route = coreRoute;
+ pathViewModel.SetTimeInfo(coreRoute.CreatedTime, coreRoute.LastModified);
+
+ if (!needsUpdate)
+ {
+ pathViewModel.NotifyPropertyChanged(nameof(PathRouteViewModel.TotalLength));
+ pathViewModel.NotifyPropertyChanged(nameof(PathRouteViewModel.LastModifiedTime));
+ return false;
+ }
+
+ string selectedPointId = preserveSelection ? SelectedPathPoint?.Id : null;
+ bool hadSelection = !string.IsNullOrEmpty(selectedPointId);
+ PathPointViewModel newSelectedPoint = null;
+
+ pathViewModel.Points.Clear();
+ foreach (var pointViewModel in CreatePathPointViewModelsFromCoreRoute(coreRoute))
+ {
+ pathViewModel.Points.Add(pointViewModel);
+ if (hadSelection && pointViewModel.Id == selectedPointId)
+ {
+ newSelectedPoint = pointViewModel;
+ }
+ }
+
+ pathViewModel.NotifyPropertyChanged(nameof(PathRouteViewModel.PointCount));
+ pathViewModel.NotifyPropertyChanged(nameof(PathRouteViewModel.TotalLength));
+ pathViewModel.NotifyPropertyChanged(nameof(PathRouteViewModel.SummaryInfo));
+ pathViewModel.NotifyPropertyChanged(nameof(PathRouteViewModel.LastModifiedTime));
+
+ if (hadSelection)
+ {
+ SelectedPathPoint = newSelectedPoint;
+ }
+
+ return true;
+ }
+
///
/// 更新吊装路径的关联点(UI层)
///
@@ -4825,90 +4898,15 @@ namespace NavisworksTransport.UI.WPF.ViewModels
var pathViewModel = PathRoutes.FirstOrDefault(p => p.Name == e.Route.Name);
if (pathViewModel != null)
{
- // 检查是否需要更新:数量变化或名称/类型/坐标变化
- bool needsUpdate = pathViewModel.Points.Count != e.Route.Points.Count;
-
- if (!needsUpdate && pathViewModel.Points.Count == e.Route.Points.Count)
- {
- // 数量相同时,检查名称、类型和坐标是否有变化
- for (int i = 0; i < pathViewModel.Points.Count; i++)
- {
- var uiPoint = pathViewModel.Points[i];
- var corePoint = e.Route.Points[i];
-
- // 检查名称和类型变化
- if (uiPoint.Name != corePoint.Name || uiPoint.Type != corePoint.Type)
- {
- needsUpdate = true;
- LogManager.Info($"检测到路径点属性变化: {uiPoint.Name}({uiPoint.Type}) -> {corePoint.Name}({corePoint.Type})");
- break;
- }
-
- // 检查坐标变化(使用小数位精度比较)
- const double tolerance = 1e-6; // 1微米精度
- if (Math.Abs(uiPoint.X - corePoint.Position.X) > tolerance ||
- Math.Abs(uiPoint.Y - corePoint.Position.Y) > tolerance ||
- Math.Abs(uiPoint.Z - corePoint.Position.Z) > tolerance)
- {
- needsUpdate = true;
- LogManager.Info($"检测到路径点坐标变化: {uiPoint.Name} " +
- $"({uiPoint.X:F3}, {uiPoint.Y:F3}, {uiPoint.Z:F3}) -> " +
- $"({corePoint.Position.X:F3}, {corePoint.Position.Y:F3}, {corePoint.Position.Z:F3})");
- break;
- }
- }
- }
-
- if (!needsUpdate)
+ bool updated = SyncPathViewModelFromCoreRoute(pathViewModel, e.Route, preserveSelection: true);
+ if (!updated)
{
LogManager.Info($"路径点数据无变化({pathViewModel.Points.Count}),跳过更新");
return;
}
- // 保存当前选中点的Id,以便在重新创建后恢复选中状态
- string selectedPointId = SelectedPathPoint?.Id;
- bool hadSelection = !string.IsNullOrEmpty(selectedPointId);
-
- // 同步路径点数据
- pathViewModel.Points.Clear();
- PathPointViewModel newSelectedPoint = null;
- for (int i = 0; i < e.Route.Points.Count; i++)
- {
- var point = e.Route.Points[i];
- var pointViewModel = new PathPointViewModel
- {
- Id = point.Id,
- Name = point.Name,
- X = point.Position.X,
- Y = point.Position.Y,
- Z = point.Position.Z,
- Type = point.Type,
- Index = i
- };
- pathViewModel.Points.Add(pointViewModel);
- LogManager.Debug($"[路径点列表更新] 索引{i}: {point.Name}, Id={point.Id}, 位置=({point.Position.X:F2}, {point.Position.Y:F2}, {point.Position.Z:F2})");
-
- // 如果是之前选中的点,保存引用
- if (hadSelection && point.Id == selectedPointId)
- {
- newSelectedPoint = pointViewModel;
- }
- }
LogManager.Info($"已更新路径点列表,当前点数: {pathViewModel.Points.Count}");
- // 恢复选中状态(使用新的ViewModel实例)
- if (hadSelection && newSelectedPoint != null)
- {
- SelectedPathPoint = newSelectedPoint;
- LogManager.Info($"[路径点列表更新] 恢复选中状态: {newSelectedPoint.Name}, 索引: {newSelectedPoint.Index}");
- }
- else if (hadSelection && newSelectedPoint == null)
- {
- // 之前选中的点已被删除
- SelectedPathPoint = null;
- LogManager.Info("[路径点列表更新] 之前选中的点已被删除,清空选中状态");
- }
-
// 更新真实路径可视化
UpdatePathVisualization();
}
@@ -4941,6 +4939,12 @@ namespace NavisworksTransport.UI.WPF.ViewModels
var pathViewModel = PathRoutes.FirstOrDefault(p => p.Name == e.NewRoute.Name);
if (pathViewModel != null)
{
+ bool synced = SyncPathViewModelFromCoreRoute(pathViewModel, e.NewRoute, preserveSelection: true);
+ if (synced)
+ {
+ LogManager.Info($"当前路径切换时已同步路径点列表: {e.NewRoute.Name}, 点数: {pathViewModel.Points.Count}");
+ }
+
// 只在真的不同时才设置,避免重复触发
if (SelectedPathRoute != pathViewModel)
{
@@ -5121,8 +5125,26 @@ namespace NavisworksTransport.UI.WPF.ViewModels
}
else if (e.GenerationMethod == RouteGenerationMethod.Manual)
{
- // 手工路径生成(如果需要特殊处理)
- LogManager.Info($"手工路径生成完成: {e.Route.Name}");
+ LogManager.Info($"手工路径生成完成: {e.Route.Name},开始刷新路径列表");
+ var existingPath = PathRoutes.FirstOrDefault(p => p.Name == e.Route.Name);
+ if (existingPath == null)
+ {
+ existingPath = new PathRouteViewModel(isFromDatabase: true)
+ {
+ Route = e.Route,
+ IsActive = true
+ };
+ PathRoutes.Add(existingPath);
+ LogManager.Info($"手工路径生成后补建UI路径壳: {e.Route.Name}");
+ }
+
+ SyncPathViewModelFromCoreRoute(existingPath, e.Route, preserveSelection: true);
+ SelectedPathRoute = existingPath;
+ LogManager.Info($"手工路径已同步并重新选中: {e.Route.Name},点数: {existingPath.Points.Count}");
+
+ OnPropertyChanged(nameof(PathRoutes));
+ OnPropertyChanged(nameof(CanExecuteStartEdit));
+ OnPropertyChanged(nameof(CanExecuteEndEdit));
}
else if (e.GenerationMethod == RouteGenerationMethod.DatabaseLoad)
{
diff --git a/src/Utils/CoordinateSystem/HoistingCoordinateHelper.cs b/src/Utils/CoordinateSystem/HoistingCoordinateHelper.cs
new file mode 100644
index 0000000..c1fb295
--- /dev/null
+++ b/src/Utils/CoordinateSystem/HoistingCoordinateHelper.cs
@@ -0,0 +1,205 @@
+using System;
+using System.Numerics;
+using Autodesk.Navisworks.Api;
+
+namespace NavisworksTransport.Utils.CoordinateSystem
+{
+ ///
+ /// 吊装路径在宿主坐标与 Canonical Space 之间的坐标语义辅助。
+ /// 统一表达“沿宿主 up 的高程”和“宿主水平面投影”,避免把世界 Z 误当成向上轴。
+ ///
+ public static class HoistingCoordinateHelper
+ {
+ public static double GetElevation(Point3D hostPoint, HostCoordinateAdapter adapter)
+ {
+ if (hostPoint == null)
+ {
+ throw new ArgumentNullException(nameof(hostPoint));
+ }
+
+ return GetElevation(ToVector3(hostPoint), adapter);
+ }
+
+ public static double GetElevation(Vector3 hostPoint, HostCoordinateAdapter adapter)
+ {
+ if (adapter == null)
+ {
+ throw new ArgumentNullException(nameof(adapter));
+ }
+
+ return adapter.ToCanonicalPoint3(hostPoint).Z;
+ }
+
+ public static Point3D SetElevation(Point3D hostPoint, double elevation, HostCoordinateAdapter adapter)
+ {
+ if (hostPoint == null)
+ {
+ throw new ArgumentNullException(nameof(hostPoint));
+ }
+
+ return ToPoint3D(SetElevation3(ToVector3(hostPoint), elevation, adapter));
+ }
+
+ public static Vector3 SetElevation3(Vector3 hostPoint, double elevation, HostCoordinateAdapter adapter)
+ {
+ if (adapter == null)
+ {
+ throw new ArgumentNullException(nameof(adapter));
+ }
+
+ Vector3 canonicalPoint = adapter.ToCanonicalPoint3(hostPoint);
+ Vector3 canonicalResult = new Vector3(canonicalPoint.X, canonicalPoint.Y, (float)elevation);
+ return adapter.FromCanonicalPoint3(canonicalResult);
+ }
+
+ public static Point3D OffsetAlongUp(Point3D hostPoint, double offset, HostCoordinateAdapter adapter)
+ {
+ return ToPoint3D(OffsetAlongUp3(ToVector3(hostPoint), offset, adapter));
+ }
+
+ public static Vector3 OffsetAlongUp3(Vector3 hostPoint, double offset, HostCoordinateAdapter adapter)
+ {
+ return SetElevation3(hostPoint, GetElevation(hostPoint, adapter) + offset, adapter);
+ }
+
+ public static Point3D ProjectToAerialElevation(Point3D groundPoint, Point3D aerialReferencePoint, HostCoordinateAdapter adapter)
+ {
+ return ToPoint3D(ProjectToAerialElevation3(ToVector3(groundPoint), ToVector3(aerialReferencePoint), adapter));
+ }
+
+ public static Vector3 ProjectToAerialElevation3(Vector3 groundPoint, Vector3 aerialReferencePoint, HostCoordinateAdapter adapter)
+ {
+ return SetElevation3(groundPoint, GetElevation(aerialReferencePoint, adapter), adapter);
+ }
+
+ public static double GetRelativeHeight(Point3D groundPoint, Point3D aerialPoint, HostCoordinateAdapter adapter)
+ {
+ return GetRelativeHeight3(ToVector3(groundPoint), ToVector3(aerialPoint), adapter);
+ }
+
+ public static double GetRelativeHeight3(Vector3 groundPoint, Vector3 aerialPoint, HostCoordinateAdapter adapter)
+ {
+ return GetElevation(aerialPoint, adapter) - GetElevation(groundPoint, adapter);
+ }
+
+ public static Point3D CreateHorizontalTurnPoint(
+ Point3D currentPoint,
+ Point3D nextPoint,
+ bool keepCurrentFirstHorizontalAxis,
+ HostCoordinateAdapter adapter)
+ {
+ if (currentPoint == null)
+ {
+ throw new ArgumentNullException(nameof(currentPoint));
+ }
+
+ if (nextPoint == null)
+ {
+ throw new ArgumentNullException(nameof(nextPoint));
+ }
+
+ if (adapter == null)
+ {
+ throw new ArgumentNullException(nameof(adapter));
+ }
+
+ return ToPoint3D(CreateHorizontalTurnPoint3(
+ ToVector3(currentPoint),
+ ToVector3(nextPoint),
+ keepCurrentFirstHorizontalAxis,
+ adapter));
+ }
+
+ public static Vector3 CreateHorizontalTurnPoint3(
+ Vector3 currentPoint,
+ Vector3 nextPoint,
+ bool keepCurrentFirstHorizontalAxis,
+ HostCoordinateAdapter adapter)
+ {
+ if (adapter == null)
+ {
+ throw new ArgumentNullException(nameof(adapter));
+ }
+
+ Vector3 canonicalCurrent = adapter.ToCanonicalPoint3(currentPoint);
+ Vector3 canonicalNext = adapter.ToCanonicalPoint3(nextPoint);
+
+ Vector3 canonicalIntermediate = keepCurrentFirstHorizontalAxis
+ ? new Vector3(canonicalCurrent.X, canonicalNext.Y, canonicalCurrent.Z)
+ : new Vector3(canonicalNext.X, canonicalCurrent.Y, canonicalCurrent.Z);
+
+ return adapter.FromCanonicalPoint3(canonicalIntermediate);
+ }
+
+ public static Vector3 GetHorizontalDirection3(Vector3 fromPoint, Vector3 toPoint, HostCoordinateAdapter adapter)
+ {
+ if (adapter == null)
+ {
+ throw new ArgumentNullException(nameof(adapter));
+ }
+
+ Vector3 canonicalFrom = adapter.ToCanonicalPoint3(fromPoint);
+ Vector3 canonicalTo = adapter.ToCanonicalPoint3(toPoint);
+ Vector3 canonicalHorizontal = new Vector3(
+ canonicalTo.X - canonicalFrom.X,
+ canonicalTo.Y - canonicalFrom.Y,
+ 0.0f);
+
+ float horizontalLength = canonicalHorizontal.Length();
+ if (horizontalLength < 1e-6f)
+ {
+ return Vector3.Zero;
+ }
+
+ Vector3 canonicalDirection = canonicalHorizontal / horizontalLength;
+ return adapter.FromCanonicalVector3(canonicalDirection);
+ }
+
+ public static Point3D ExtendVerticalTransitionForObjectSpace(
+ Point3D hostPoint,
+ double objectHeight,
+ bool isLowerPoint,
+ HostCoordinateAdapter adapter)
+ {
+ if (hostPoint == null)
+ {
+ throw new ArgumentNullException(nameof(hostPoint));
+ }
+
+ return ToPoint3D(ExtendVerticalTransitionForObjectSpace3(
+ ToVector3(hostPoint),
+ objectHeight,
+ isLowerPoint,
+ adapter));
+ }
+
+ public static Vector3 ExtendVerticalTransitionForObjectSpace3(
+ Vector3 hostPoint,
+ double objectHeight,
+ bool isLowerPoint,
+ HostCoordinateAdapter adapter)
+ {
+ if (adapter == null)
+ {
+ throw new ArgumentNullException(nameof(adapter));
+ }
+
+ if (!isLowerPoint || Math.Abs(objectHeight) < 1e-9)
+ {
+ return hostPoint;
+ }
+
+ return OffsetAlongUp3(hostPoint, -objectHeight, adapter);
+ }
+
+ private static Vector3 ToVector3(Point3D point)
+ {
+ return new Vector3((float)point.X, (float)point.Y, (float)point.Z);
+ }
+
+ private static Point3D ToPoint3D(Vector3 point)
+ {
+ return new Point3D(point.X, point.Y, point.Z);
+ }
+ }
+}
From 5e7f9bdf510fd0377b398f2f9250aafb27f0a3a5 Mon Sep 17 00:00:00 2001
From: tian <11429339@qq.com>
Date: Sun, 22 Mar 2026 13:06:08 +0800
Subject: [PATCH 14/87] Finish assembly route generation cleanup
---
src/UI/WPF/ViewModels/PathEditingViewModel.cs | 49 ++++++++++++++-----
1 file changed, 37 insertions(+), 12 deletions(-)
diff --git a/src/UI/WPF/ViewModels/PathEditingViewModel.cs b/src/UI/WPF/ViewModels/PathEditingViewModel.cs
index daddc0c..1d04ecd 100644
--- a/src/UI/WPF/ViewModels/PathEditingViewModel.cs
+++ b/src/UI/WPF/ViewModels/PathEditingViewModel.cs
@@ -1403,15 +1403,10 @@ namespace NavisworksTransport.UI.WPF.ViewModels
{
try
{
- CleanupAssemblyReferenceSelection();
- AssemblyReferencePathManager.Instance.HideReferenceRod();
- ClearAssemblyAnchorMarker();
- ClearAssemblyCenterGuideLine();
- ClearAssemblyReferenceLine();
- AssemblyStartPointText = "未选择";
- OnPropertyChanged(nameof(CanSelectAssemblyStartPoint));
- UpdateMainStatus("已隐藏终端安装辅助线");
- LogManager.Info("[直线装配] 已隐藏辅助线");
+ HideAssemblyReferenceVisuals(
+ resetStartPointText: false,
+ statusMessage: "已隐藏终端安装辅助线",
+ logMessage: "[直线装配] 已隐藏辅助线");
}
catch (Exception ex)
{
@@ -1477,8 +1472,10 @@ namespace NavisworksTransport.UI.WPF.ViewModels
throw new InvalidOperationException("装配路径生成失败");
}
- _pathPlanningManager.DrawRouteVisualization(route, isAutoPath: false);
- RefreshPathRoutes();
+ if (!_pathPlanningManager.FinishEditing())
+ {
+ throw new InvalidOperationException("装配路径完成编辑失败");
+ }
var generatedRouteViewModel = PathRoutes.FirstOrDefault(p => p.Name == route.Name);
if (generatedRouteViewModel != null)
@@ -1487,6 +1484,11 @@ namespace NavisworksTransport.UI.WPF.ViewModels
UpdatePassageSpaceVisualizationForPathType(route.PathType);
}
+ HideAssemblyReferenceVisuals(
+ resetStartPointText: false,
+ statusMessage: null,
+ logMessage: "[直线装配] 路径生成完成后已隐藏辅助线可视化");
+
AssemblyTerminalObjectInfo = string.Format(
"对接终点=({0:F2}, {1:F2}, {2:F2}),装配起点={3},安装方式={4},对接基准={5}",
endPoint.X,
@@ -1497,7 +1499,30 @@ namespace NavisworksTransport.UI.WPF.ViewModels
GetAssemblyAnchorText());
OnPropertyChanged(nameof(PathRoutes));
- LogManager.Info($"[直线装配] 已生成路径: {route.Name},起点=({startPoint.X:F2}, {startPoint.Y:F2}, {startPoint.Z:F2}),终点=({endPoint.X:F2}, {endPoint.Y:F2}, {endPoint.Z:F2})");
+ LogManager.Info($"[直线装配] 已生成并完成路径: {route.Name},起点=({startPoint.X:F2}, {startPoint.Y:F2}, {startPoint.Z:F2}),终点=({endPoint.X:F2}, {endPoint.Y:F2}, {endPoint.Z:F2})");
+ }
+
+ private void HideAssemblyReferenceVisuals(bool resetStartPointText, string statusMessage, string logMessage)
+ {
+ CleanupAssemblyReferenceSelection();
+ AssemblyReferencePathManager.Instance.HideReferenceRod();
+ ClearAssemblyAnchorMarker();
+ ClearAssemblyCenterGuideLine();
+ ClearAssemblyReferenceLine();
+ if (resetStartPointText)
+ {
+ AssemblyStartPointText = "未选择";
+ }
+ OnPropertyChanged(nameof(CanSelectAssemblyStartPoint));
+ if (!string.IsNullOrWhiteSpace(statusMessage))
+ {
+ UpdateMainStatus(statusMessage);
+ }
+
+ if (!string.IsNullOrWhiteSpace(logMessage))
+ {
+ LogManager.Info(logMessage);
+ }
}
private Point3D GetAssemblyTerminalAnchorPoint()
From 1802eda971df80447d9e647ed4da89e1973c3e25 Mon Sep 17 00:00:00 2001
From: tian <11429339@qq.com>
Date: Sun, 22 Mar 2026 13:30:35 +0800
Subject: [PATCH 15/87] Separate asset axis conventions from host coordinates
---
.../ModelAxisConventionTests.cs | 34 +++++++
src/Core/Animation/PathAnimationManager.cs | 4 +-
src/Core/AssemblyReferencePathManager.cs | 10 +-
src/Core/VirtualObjectManager.cs | 91 +++----------------
.../CoordinateSystem/ModelAxisConvention.cs | 64 +++++++++++++
src/Utils/RailPathPoseHelper.cs | 4 +-
6 files changed, 123 insertions(+), 84 deletions(-)
diff --git a/UnitTests/CoordinateSystem/ModelAxisConventionTests.cs b/UnitTests/CoordinateSystem/ModelAxisConventionTests.cs
index 74f16e0..8526f21 100644
--- a/UnitTests/CoordinateSystem/ModelAxisConventionTests.cs
+++ b/UnitTests/CoordinateSystem/ModelAxisConventionTests.cs
@@ -59,6 +59,40 @@ namespace NavisworksTransport.UnitTests.CoordinateSystem
AssertColumn(linear, 2, 1, 0, 0);
}
+ [TestMethod]
+ public void ReferenceRodAssetConvention_ShouldUseXForwardAndZUp()
+ {
+ ModelAxisConvention convention = ModelAxisConvention.CreateReferenceRodAssetConvention();
+
+ Assert.AreEqual(LocalAxisDirection.PositiveX, convention.ForwardAxis);
+ Assert.AreEqual(LocalAxisDirection.PositiveZ, convention.UpAxis);
+ Assert.AreEqual(LocalAxisDirection.NegativeY, convention.SideAxis);
+ }
+
+ [TestMethod]
+ public void VirtualObjectAssetConvention_CreateScaleVector_ShouldMapLengthWidthHeightToLocalAxes()
+ {
+ ModelAxisConvention convention = ModelAxisConvention.CreateVirtualObjectAssetConvention();
+
+ var scale = convention.CreateScaleVector3(12.0, 5.0, 7.0);
+
+ Assert.AreEqual(12.0, scale.X, 1e-6);
+ Assert.AreEqual(5.0, scale.Y, 1e-6);
+ Assert.AreEqual(7.0, scale.Z, 1e-6);
+ }
+
+ [TestMethod]
+ public void YUpConvention_CreateScaleVector_ShouldMapUpSizeToLocalY()
+ {
+ ModelAxisConvention convention = ModelAxisConvention.CreateDefaultForHost(CoordinateSystemType.YUp);
+
+ var scale = convention.CreateScaleVector3(12.0, 5.0, 7.0);
+
+ Assert.AreEqual(12.0, scale.X, 1e-6);
+ Assert.AreEqual(7.0, scale.Y, 1e-6);
+ Assert.AreEqual(5.0, scale.Z, 1e-6);
+ }
+
private static void AssertColumn(Matrix4x4 matrix, int column, double x, double y, double z)
{
switch (column)
diff --git a/src/Core/Animation/PathAnimationManager.cs b/src/Core/Animation/PathAnimationManager.cs
index 9f75699..dc5978a 100644
--- a/src/Core/Animation/PathAnimationManager.cs
+++ b/src/Core/Animation/PathAnimationManager.cs
@@ -3368,7 +3368,7 @@ namespace NavisworksTransport.Core.Animation
{
if (_isVirtualObject)
{
- return ModelAxisConvention.CreateDefaultForHost(CoordinateSystemType.ZUp);
+ return ModelAxisConvention.CreateVirtualObjectAssetConvention();
}
var adapter = CoordinateSystemManager.Instance.CreateHostAdapter();
@@ -3383,7 +3383,7 @@ namespace NavisworksTransport.Core.Animation
{
if (_isVirtualObject)
{
- return ModelAxisConvention.CreateDefaultForHost(CoordinateSystemType.ZUp);
+ return ModelAxisConvention.CreateVirtualObjectAssetConvention();
}
var adapter = CoordinateSystemManager.Instance.CreateHostAdapter();
diff --git a/src/Core/AssemblyReferencePathManager.cs b/src/Core/AssemblyReferencePathManager.cs
index 7191415..986604f 100644
--- a/src/Core/AssemblyReferencePathManager.cs
+++ b/src/Core/AssemblyReferencePathManager.cs
@@ -4,6 +4,7 @@ using System.Linq;
using System.Reflection;
using Autodesk.Navisworks.Api;
using NavisworksTransport.Utils;
+using NavisworksTransport.Utils.CoordinateSystem;
namespace NavisworksTransport.Core
{
@@ -15,6 +16,7 @@ namespace NavisworksTransport.Core
public class AssemblyReferencePathManager
{
private const double ReferenceRodBaseSizeInMeters = 0.01;
+ private static readonly ModelAxisConvention ReferenceRodAxisConvention = ModelAxisConvention.CreateReferenceRodAssetConvention();
private static AssemblyReferencePathManager _instance;
private static readonly object _lock = new object();
@@ -220,7 +222,7 @@ namespace NavisworksTransport.Core
var currentTransform = _referenceRodModel.Transform;
var components = currentTransform.Factor();
- components.Scale = new Vector3D(
+ components.Scale = ReferenceRodAxisConvention.CreateScaleVector(
lengthInMeters / ReferenceRodBaseSizeInMeters,
diameterInMeters / ReferenceRodBaseSizeInMeters,
diameterInMeters / ReferenceRodBaseSizeInMeters);
@@ -265,11 +267,11 @@ namespace NavisworksTransport.Core
endPoint.Y - startPoint.Y,
endPoint.Z - startPoint.Z));
- // 参考杆资源约定:几何中心在原点,长度轴沿局部 +X。
+ // 参考杆资源约定:几何中心在原点,长度轴沿本地 ForwardAxis。
// Navisworks API 文档中 Rotation3D(vector1, vector2) 的语义
- // 是“将 vector1 旋转到与 vector2 同方向”,这里直接将本地 X 轴对齐参考线方向。
+ // 是“将 vector1 旋转到与 vector2 同方向”,这里直接将资源本地 forward 轴对齐参考线方向。
return new Rotation3D(
- new UnitVector3D(1, 0, 0),
+ new UnitVector3D(ReferenceRodAxisConvention.ForwardVector),
new UnitVector3D(tangent));
}
diff --git a/src/Core/VirtualObjectManager.cs b/src/Core/VirtualObjectManager.cs
index 2809375..f8e7647 100644
--- a/src/Core/VirtualObjectManager.cs
+++ b/src/Core/VirtualObjectManager.cs
@@ -4,6 +4,7 @@ using System.Linq;
using System.Reflection;
using Autodesk.Navisworks.Api;
using NavisworksTransport.Utils;
+using NavisworksTransport.Utils.CoordinateSystem;
namespace NavisworksTransport.Core
{
@@ -17,6 +18,7 @@ namespace NavisworksTransport.Core
///
public class VirtualObjectManager
{
+ private static readonly ModelAxisConvention VirtualObjectAxisConvention = ModelAxisConvention.CreateVirtualObjectAssetConvention();
private static VirtualObjectManager _instance;
private static readonly object _lock = new object();
@@ -188,62 +190,15 @@ namespace NavisworksTransport.Core
try
{
- var doc = Application.ActiveDocument;
- var modelItems = new ModelItemCollection { _virtualObjectModelItem };
-
- // 获取当前缩放
- var currentTransform = _virtualObjectModelItem.Transform;
- var currentComponents = currentTransform.Factor();
- var currentScale = currentComponents.Scale;
-
- // 重置到CAD原始状态
- doc.Models.ResetPermanentTransform(modelItems);
-
- // 获取CAD原始状态
- var originalBounds = _virtualObjectModelItem.BoundingBox();
- var originalGroundPos = new Point3D(
- originalBounds.Center.X,
- originalBounds.Center.Y,
- originalBounds.Min.Z
- );
- var originalYaw = GetYawFromTransform(_virtualObjectModelItem.Transform);
-
- // 计算增量
- var deltaPos = new Vector3D(
- position.X - originalGroundPos.X,
- position.Y - originalGroundPos.Y,
- position.Z - originalGroundPos.Z
- );
- double deltaYaw = yawRadians - originalYaw;
-
- // 构建变换,保留当前缩放
- var identity = Transform3D.CreateTranslation(new Vector3D(0, 0, 0));
- var components = identity.Factor();
- components.Scale = currentScale;
-
- if (Math.Abs(deltaYaw) > 0.001)
- {
- double cos = Math.Cos(deltaYaw);
- double sin = Math.Sin(deltaYaw);
- double rotatedX = originalGroundPos.X * cos - originalGroundPos.Y * sin;
- double rotatedY = originalGroundPos.X * sin + originalGroundPos.Y * cos;
-
- components.Rotation = new Rotation3D(new UnitVector3D(0, 0, 1), deltaYaw);
- components.Translation = new Vector3D(
- position.X - rotatedX,
- position.Y - rotatedY,
- deltaPos.Z
- );
- }
- else
- {
- components.Translation = deltaPos;
- }
-
- Transform3D transform = components.Combine();
- doc.Models.OverridePermanentTransform(modelItems, transform, false);
-
- LogManager.Debug($"虚拟物体已移动");
+ var adapter = CoordinateSystemManager.Instance.CreateHostAdapter();
+ var hostForward = adapter.FromCanonicalVector(new Vector3D(Math.Cos(yawRadians), Math.Sin(yawRadians), 0));
+ var hostUp = adapter.FromCanonicalVector(new Vector3D(
+ HostCoordinateAdapter.CanonicalUpVector3.X,
+ HostCoordinateAdapter.CanonicalUpVector3.Y,
+ HostCoordinateAdapter.CanonicalUpVector3.Z));
+ var rotation = VirtualObjectAxisConvention.CreateRotation(hostForward, hostUp);
+ MoveVirtualObject(position, rotation);
+ LogManager.Debug($"虚拟物体已按Canonical平面yaw应用完整姿态");
}
catch (Exception ex)
{
@@ -367,16 +322,14 @@ namespace NavisworksTransport.Core
if (_virtualObjectModel == null || _virtualObjectModelItem == null) return;
var doc = Application.ActiveDocument;
- double metersToUnits = Utils.UnitsConverter.GetMetersToUnitsConversionFactor(doc.Units);
-
double baseSizeMeters = 0.01;
- double scaleX = lengthMeters / baseSizeMeters;
- double scaleY = widthMeters / baseSizeMeters;
- double scaleZ = heightMeters / baseSizeMeters;
var currentTransform = _virtualObjectModel.Transform;
var transformComponents = currentTransform.Factor();
- transformComponents.Scale = new Vector3D(scaleX, scaleY, scaleZ);
+ transformComponents.Scale = VirtualObjectAxisConvention.CreateScaleVector(
+ lengthMeters / baseSizeMeters,
+ widthMeters / baseSizeMeters,
+ heightMeters / baseSizeMeters);
Transform3D newTransform = transformComponents.Combine();
doc.Models.SetModelUnitsAndTransform(_virtualObjectModel, _virtualObjectModel.Units, newTransform, false);
@@ -409,20 +362,6 @@ namespace NavisworksTransport.Core
}
}
- ///
- /// 从Transform中提取Yaw角度(简化版本)
- ///
- private double GetYawFromTransform(Transform3D transform)
- {
- try
- {
- // 使用ModelItemTransformHelper中的方法
- return ModelItemTransformHelper.GetYawFromTransform(transform);
- }
- catch { }
- return 0.0;
- }
-
///
/// 清理虚拟物体(用于程序退出或切换文档时)
///
diff --git a/src/Utils/CoordinateSystem/ModelAxisConvention.cs b/src/Utils/CoordinateSystem/ModelAxisConvention.cs
index 7affc79..f1c9fb4 100644
--- a/src/Utils/CoordinateSystem/ModelAxisConvention.cs
+++ b/src/Utils/CoordinateSystem/ModelAxisConvention.cs
@@ -108,6 +108,33 @@ namespace NavisworksTransport.Utils.CoordinateSystem
}
}
+ ///
+ /// 虚拟物体资源的本地轴约定。
+ /// 该约定仅描述资源文件本身的 local forward/up,不代表当前宿主文档的坐标系。
+ ///
+ public static ModelAxisConvention CreateVirtualObjectAssetConvention()
+ {
+ return new ModelAxisConvention(LocalAxisDirection.PositiveX, LocalAxisDirection.PositiveZ);
+ }
+
+ ///
+ /// 参考杆资源的本地轴约定。
+ /// 当前资源约定长度轴沿本地 +X,截面 up 沿本地 +Z。
+ ///
+ public static ModelAxisConvention CreateReferenceRodAssetConvention()
+ {
+ return new ModelAxisConvention(LocalAxisDirection.PositiveX, LocalAxisDirection.PositiveZ);
+ }
+
+ ///
+ /// Rail 资源的本地轴约定。
+ /// 当前资源约定前进方向沿本地 +X,向上沿本地 +Z。
+ ///
+ public static ModelAxisConvention CreateRailAssetConvention()
+ {
+ return new ModelAxisConvention(LocalAxisDirection.PositiveX, LocalAxisDirection.PositiveZ);
+ }
+
public double GetForwardSize(BoundingBox3D bounds)
{
return GetAxisExtent(bounds, ForwardAxis);
@@ -123,6 +150,23 @@ namespace NavisworksTransport.Utils.CoordinateSystem
return GetAxisExtent(bounds, SideAxis);
}
+ ///
+ /// 根据 forward/side/up 语义尺寸,构造资源本地 X/Y/Z 的缩放向量。
+ ///
+ public Vector3D CreateScaleVector(double forwardSize, double sideSize, double upSize)
+ {
+ Vector3 scale = CreateScaleVector3(forwardSize, sideSize, upSize);
+ return new Vector3D(scale.X, scale.Y, scale.Z);
+ }
+
+ public Vector3 CreateScaleVector3(double forwardSize, double sideSize, double upSize)
+ {
+ float scaleX = (float)ResolveLocalAxisScale(0, forwardSize, sideSize, upSize);
+ float scaleY = (float)ResolveLocalAxisScale(1, forwardSize, sideSize, upSize);
+ float scaleZ = (float)ResolveLocalAxisScale(2, forwardSize, sideSize, upSize);
+ return new Vector3(scaleX, scaleY, scaleZ);
+ }
+
private static Vector3 GetAxisVector3(LocalAxisDirection axis)
{
switch (axis)
@@ -203,6 +247,26 @@ namespace NavisworksTransport.Utils.CoordinateSystem
throw new InvalidOperationException("模型局部第三轴不是标准正交轴,无法确定局部轴约定。");
}
+ private double ResolveLocalAxisScale(int localAxisIndex, double forwardSize, double sideSize, double upSize)
+ {
+ if (MatchesAxis(ForwardAxis, localAxisIndex, out _))
+ {
+ return forwardSize;
+ }
+
+ if (MatchesAxis(UpAxis, localAxisIndex, out _))
+ {
+ return upSize;
+ }
+
+ if (MatchesAxis(SideAxis, localAxisIndex, out _))
+ {
+ return sideSize;
+ }
+
+ throw new InvalidOperationException("无法为模型局部轴解析缩放尺寸。");
+ }
+
private static bool IsApproximately(Vector3 vector, double x, double y, double z)
{
return Math.Abs(vector.X - x) < 1e-6 &&
diff --git a/src/Utils/RailPathPoseHelper.cs b/src/Utils/RailPathPoseHelper.cs
index c668a4d..82b507c 100644
--- a/src/Utils/RailPathPoseHelper.cs
+++ b/src/Utils/RailPathPoseHelper.cs
@@ -168,7 +168,7 @@ namespace NavisworksTransport.Utils
previousPoint,
currentPoint,
nextPoint,
- ModelAxisConvention.CreateDefaultForHost(CoordinateSystemType.ZUp),
+ ModelAxisConvention.CreateRailAssetConvention(),
out linearTransform);
}
@@ -247,7 +247,7 @@ namespace NavisworksTransport.Utils
previousPoint,
currentPoint,
nextPoint,
- ModelAxisConvention.CreateDefaultForHost(CoordinateSystemType.ZUp),
+ ModelAxisConvention.CreateRailAssetConvention(),
out rotation);
}
From b1d41703341eaaac7338c31cc7e718467755ad36 Mon Sep 17 00:00:00 2001
From: tian <11429339@qq.com>
Date: Sun, 22 Mar 2026 14:45:50 +0800
Subject: [PATCH 16/87] Fix animation end alignment and YUp viewpoint focus
---
doc/working/current-engineering-state.md | 45 ++++++++
src/Core/Animation/PathAnimationManager.cs | 115 ++++++++++++++-------
src/Utils/ViewpointHelper.cs | 20 +++-
3 files changed, 139 insertions(+), 41 deletions(-)
diff --git a/doc/working/current-engineering-state.md b/doc/working/current-engineering-state.md
index af66461..73e6ff1 100644
--- a/doc/working/current-engineering-state.md
+++ b/doc/working/current-engineering-state.md
@@ -82,6 +82,22 @@
- `YUp` 吊装路径创建主链路已补齐到宿主坐标适配架构:
- 提升、水平移动、下降、终点落地都不能再把世界 `Z` 硬编码成“向上”。
- 终点必须使用用户最后一次点击的地面点,不能回填起点地面高程。
+- 吊装路径相关的当前稳定约束:
+ - 路径创建、终点补点、路径点修改、斜线正交化、通行空间识别,必须共用同一套“宿主坐标 -> Canonical -> 宿主坐标”的高度语义。
+ - `高度过渡点` 这类自动生成点,本质上仍是吊装路径点,不能在渲染或可视化阶段再退回旧 `ZUp` 假设。
+ - 通行空间如果遇到“纯垂直段却从前方空中偏出去一截、变成斜的”,优先检查:
+ - 垂直段识别是否仍在比较 `dz`
+ - 低点补物体高度是否仍在直接改 `Z`
+ - 吊装水平参考方向是否仍在固定 `XY` 平面求
+- 吊装路径这轮重构后,相关职责已经收束到:
+ - `HoistingCoordinateHelper`
+ - `PathPlanningManager`
+ - `AerialPathGenerator`
+ - `PathPointRenderPlugin`
+- 当前规则:
+ - 吊装“向上”只能通过宿主 `up` 语义表达,不能直接写 `point.Z +/- height`
+ - 通行空间垂直延伸必须沿宿主 down 方向补物体高度
+ - 吊装水平参考方向必须先剔除宿主 up 分量,再在宿主水平面内求方向
- 吊装路径“设为终点直接结束”如果出现:
- 核心路径点数正确,但路径列表/路径点列表仍显示为 `0`
- 切到别的路径再切回来后才恢复正常
@@ -93,6 +109,35 @@
- 当前修复原则:
- `UIStateManager` 必须持续消费后续批次,直到 UI 队列真正为空。
- 不要用“手动切换路径”“先选空再选回”“强行补 OnPropertyChanged”这类 UI 和稀泥方式掩盖队列问题。
+ - “设为终点直接结束”和“点结束按钮完成路径”都必须走到同样完整的收尾语义:
+ - 路径数据完成
+ - 当前路径切换完成
+ - 路径列表与路径点列表同步完成
+- 动画自然结束时,必须先显式应用最后一帧姿态,再进入后续碰撞检测/恢复链路。
+ - 否则会出现:
+ - 播放停下时视觉上离终点还差一点
+ - 碰撞检测开始前又被后续姿态应用“往前补一下”
+- 当前播放链路已经统一:
+ - 逐帧播放和动画结束落点都复用同一套姿态应用入口
+ - 地面/吊装路径如果缺少完整姿态,应直接报错,不能回退旧 `yaw`
+- `Rail / Ground / Hoisting` 终点诊断日志的比较基准必须是:
+ - 该路径类型真实使用的“期望跟踪点/几何中心”
+ - 不能再把地面接触点或终点锚点直接拿来和动画跟踪中心比较
+
+### 4.6 聚焦 / 视点
+
+- `ViewpointHelper` 的聚焦/俯视逻辑不能再把世界 `Z` 硬编码成“上方向”。
+- 在 `YUp` 模型里:
+ - 相机位置必须沿宿主 `HostUpVector` 偏移
+ - 屏幕上方向也必须通过宿主坐标适配得到
+- 如果继续写死:
+ - `cameraPosition = (x, y, z + distance)`
+ - `upVector = (0, 1, 0)`
+ - 那么 `YUp` 模型下聚焦就会跑到侧视图。
+- 当前原则:
+ - 聚焦输入/输出使用宿主坐标
+ - 方向语义通过 `HostCoordinateAdapter` 明确适配
+ - 不要在视点辅助逻辑里直接假设宿主一定是 `ZUp`
## 5. 当前保留的日志策略
diff --git a/src/Core/Animation/PathAnimationManager.cs b/src/Core/Animation/PathAnimationManager.cs
index dc5978a..2bccc0e 100644
--- a/src/Core/Animation/PathAnimationManager.cs
+++ b/src/Core/Animation/PathAnimationManager.cs
@@ -1767,6 +1767,23 @@ namespace NavisworksTransport.Core.Animation
// 重置暂停进度
_pausedProgress = 0.0;
+ // 显式落到最后一帧,避免播放结束时视觉上停在倒数第二帧,
+ // 但后续碰撞链又按最终姿态重新摆正,造成“结束后又往前窜一点”。
+ if (_animationFrames != null && _animationFrames.Count > 0)
+ {
+ _currentFrameIndex = _animationFrames.Count - 1;
+ ApplyAnimationFramePose(_animationFrames[_currentFrameIndex], _currentFrameIndex);
+ ProgressChanged?.Invoke(this, 100.0);
+ try
+ {
+ NavisApplication.ActiveDocument?.ActiveView?.RequestDelayedRedraw(ViewRedrawRequests.All);
+ }
+ catch (Exception redrawEx)
+ {
+ LogManager.Warning($"动画结束时请求最终重绘失败: {redrawEx.Message}");
+ }
+ }
+
// 使用预计算的所有碰撞结果进行高亮显示(需要去重以避免重复高亮)
var allCollisionResults = _allCollisionResults
.GroupBy(c => new { c.Item1, c.Item2 })
@@ -1775,7 +1792,7 @@ namespace NavisworksTransport.Core.Animation
// 动画自然结束时保持物体在最终位置(不移回起点)
LogManager.Info("动画播放自然结束,物体保持在最终位置");
- LogRailEndAlignmentDiagnostics();
+ LogEndAlignmentDiagnostics();
// 清除所有高亮(包括物体和碰撞)
ModelHighlightHelper.ClearAllHighlights();
@@ -1851,18 +1868,18 @@ namespace NavisworksTransport.Core.Animation
}
///
- /// 输出 Rail 路径动画终点对齐诊断。
+ /// 输出路径动画终点对齐诊断。
/// 只做日志,不改变业务行为,用于确认:
/// 1. 路径终点锚点
/// 2. 最后一帧动画跟踪点
/// 3. 动画结束时的实际跟踪点
/// 4. 沿路径前进方向的终点偏差
///
- private void LogRailEndAlignmentDiagnostics()
+ private void LogEndAlignmentDiagnostics()
{
try
{
- if (_route == null || _route.PathType != PathType.Rail || _pathPoints == null || _pathPoints.Count < 2 || _animationFrames == null || _animationFrames.Count == 0)
+ if (_route == null || _pathPoints == null || _pathPoints.Count < 2 || _animationFrames == null || _animationFrames.Count == 0)
{
return;
}
@@ -1872,13 +1889,30 @@ namespace NavisworksTransport.Core.Animation
var lastFrame = _animationFrames[_animationFrames.Count - 1];
Point3D finalTrackedPoint = _trackedPosition;
- double objectHeight = GetAnimatedObjectHeight();
- Point3D expectedTrackedEndPoint = RailPathPoseHelper.ResolveObjectSpaceCenterPosition(
- _route,
- terminalAnchorPoint,
- previousAnchorPoint,
- terminalAnchorPoint,
- objectHeight);
+ Point3D expectedTrackedEndPoint;
+ if (_route.PathType == PathType.Rail)
+ {
+ double objectHeight = GetAnimatedObjectHeight();
+ expectedTrackedEndPoint = RailPathPoseHelper.ResolveObjectSpaceCenterPosition(
+ _route,
+ terminalAnchorPoint,
+ previousAnchorPoint,
+ terminalAnchorPoint,
+ objectHeight);
+ }
+ else if (_route.PathType == PathType.Hoisting)
+ {
+ expectedTrackedEndPoint = ResolveHoistingTrackedCenter(
+ terminalAnchorPoint,
+ GetAnimatedObjectHeight(),
+ 0.0);
+ }
+ else
+ {
+ expectedTrackedEndPoint = ResolveGroundTrackedCenter(
+ terminalAnchorPoint,
+ GetAnimatedObjectHeight());
+ }
Vector3D forward = new Vector3D(
terminalAnchorPoint.X - previousAnchorPoint.X,
@@ -1908,19 +1942,20 @@ namespace NavisworksTransport.Core.Animation
double frameForwardDelta = Dot(frameDelta, normalizedForward);
double finalForwardDelta = Dot(finalDelta, normalizedForward);
+ string pathLabel = _route.PathType.GetDisplayName();
LogManager.Info(
- $"[Rail终点诊断] 终点锚点=({terminalAnchorPoint.X:F3},{terminalAnchorPoint.Y:F3},{terminalAnchorPoint.Z:F3}), " +
+ $"[{pathLabel}终点诊断] 终点锚点=({terminalAnchorPoint.X:F3},{terminalAnchorPoint.Y:F3},{terminalAnchorPoint.Z:F3}), " +
$"期望跟踪点=({expectedTrackedEndPoint.X:F3},{expectedTrackedEndPoint.Y:F3},{expectedTrackedEndPoint.Z:F3}), " +
$"最后一帧跟踪点=({lastFrame.Position.X:F3},{lastFrame.Position.Y:F3},{lastFrame.Position.Z:F3}), " +
$"动画结束跟踪点=({finalTrackedPoint.X:F3},{finalTrackedPoint.Y:F3},{finalTrackedPoint.Z:F3})");
LogManager.Info(
- $"[Rail终点诊断] 前进方向=({normalizedForward.X:F4},{normalizedForward.Y:F4},{normalizedForward.Z:F4}), " +
+ $"[{pathLabel}终点诊断] 前进方向=({normalizedForward.X:F4},{normalizedForward.Y:F4},{normalizedForward.Z:F4}), " +
$"最后一帧偏差=({frameDelta.X:F3},{frameDelta.Y:F3},{frameDelta.Z:F3}), 沿前进轴={frameForwardDelta:F3}, " +
$"结束偏差=({finalDelta.X:F3},{finalDelta.Y:F3},{finalDelta.Z:F3}), 沿前进轴={finalForwardDelta:F3}");
}
catch (Exception ex)
{
- LogManager.Warning($"[Rail终点诊断] 输出失败: {ex.Message}");
+ LogManager.Warning($"[终点诊断] 输出失败: {ex.Message}");
}
}
@@ -3658,28 +3693,7 @@ namespace NavisworksTransport.Core.Animation
if (_currentFrameIndex < _animationFrames.Count)
{
var frameData = _animationFrames[_currentFrameIndex];
-
- // 计算实际朝向 = 路径方向 + 角度修正
- double actualYaw = frameData.YawRadians;
- if (_objectRotationCorrection != 0.0)
- {
- double correctionRad = _objectRotationCorrection * Math.PI / 180.0;
- actualYaw += correctionRad;
- }
-
- if (frameData.HasCustomRotation)
- {
- UpdateObjectPosition(frameData.Position, frameData.Rotation);
- }
- else if (_route?.PathType == PathType.Ground || _route?.PathType == PathType.Hoisting)
- {
- throw new InvalidOperationException(
- $"平面路径播放帧 {_currentFrameIndex} 缺少完整姿态,禁止退回旧 yaw 链路。");
- }
- else
- {
- UpdateObjectPosition(frameData.Position, actualYaw);
- }
+ ApplyAnimationFramePose(frameData, _currentFrameIndex);
// 更新碰撞高亮(基于预计算结果)
UpdateCollisionHighlightFromFrame();
@@ -3706,6 +3720,35 @@ namespace NavisworksTransport.Core.Animation
UpdateFPSCounterAndCheckSwitch();
}
+ private void ApplyAnimationFramePose(AnimationFrame frameData, int frameIndex)
+ {
+ if (frameData == null)
+ {
+ return;
+ }
+
+ double actualYaw = frameData.YawRadians;
+ if (_objectRotationCorrection != 0.0)
+ {
+ double correctionRad = _objectRotationCorrection * Math.PI / 180.0;
+ actualYaw += correctionRad;
+ }
+
+ if (frameData.HasCustomRotation)
+ {
+ UpdateObjectPosition(frameData.Position, frameData.Rotation);
+ }
+ else if (_route?.PathType == PathType.Ground || _route?.PathType == PathType.Hoisting)
+ {
+ throw new InvalidOperationException(
+ $"平面路径播放帧 {frameIndex} 缺少完整姿态,禁止退回旧 yaw 链路。");
+ }
+ else
+ {
+ UpdateObjectPosition(frameData.Position, actualYaw);
+ }
+ }
+
///
/// 更新FPS计数器(DispatcherTimer模式)
diff --git a/src/Utils/ViewpointHelper.cs b/src/Utils/ViewpointHelper.cs
index 4ce8656..bc9a030 100644
--- a/src/Utils/ViewpointHelper.cs
+++ b/src/Utils/ViewpointHelper.cs
@@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using Autodesk.Navisworks.Api;
+using NavisworksTransport.Utils.CoordinateSystem;
namespace NavisworksTransport.Utils
{
@@ -82,6 +83,10 @@ namespace NavisworksTransport.Utils
throw new InvalidOperationException("没有活动的文档");
}
+ var adapter = CoordinateSystemManager.Instance.CreateHostAdapter();
+ Vector3D hostUp = adapter.HostUpVector;
+ Vector3D screenUp = adapter.FromCanonicalVector(new Vector3D(0, 1, 0));
+
// 1. 计算聚焦包围盒中心(相机指向的位置)
Point3D focusCenter = new Point3D(
(focusBoundingBox.Min.X + focusBoundingBox.Max.X) / 2,
@@ -93,7 +98,10 @@ namespace NavisworksTransport.Utils
// 将基准尺寸(米)转换为模型单位
double baseDimensionInModelUnits = UnitsConverter.ConvertFromMeters(baseDimension);
double cameraDistance = baseDimensionInModelUnits; // 相机距离设置为基准尺寸的1倍(可调整比例以获得更好视野)
- Point3D cameraPosition = new Point3D(focusCenter.X, focusCenter.Y, focusCenter.Z + cameraDistance);
+ Point3D cameraPosition = new Point3D(
+ focusCenter.X + hostUp.X * cameraDistance,
+ focusCenter.Y + hostUp.Y * cameraDistance,
+ focusCenter.Z + hostUp.Z * cameraDistance);
// 将相机距离转换回米用于日志显示
double cameraDistanceMeters = UnitsConverter.ConvertToMeters(cameraDistance);
@@ -105,10 +113,12 @@ namespace NavisworksTransport.Utils
double expansionMargin = boxWidth * (expansionFactor - 1) / 2;
// 4. 应用视角(使用通用方法,带 ZoomBox)
- Vector3D upVector = new Vector3D(0, 1, 0); // Y轴向上
- ApplyViewpointWithZoomBox(cameraPosition, focusCenter, upVector, viewBoundingBox, expansionMargin);
+ ApplyViewpointWithZoomBox(cameraPosition, focusCenter, screenUp, viewBoundingBox, expansionMargin);
- LogManager.Info($"视角已调整完成: 俯视图(已摆正), 基准尺寸={baseDimension:F2}米, 相机距离={cameraDistanceMeters:F2}米, FOV=自动, 包围盒扩展系数={VIEW_BOUNDING_BOX_EXPANSION_FACTOR:F1}, 扩展边距={expansionMargin:F2}米");
+ LogManager.Info(
+ $"视角已调整完成: 俯视图(已摆正), HostUp=({hostUp.X:F2},{hostUp.Y:F2},{hostUp.Z:F2}), " +
+ $"ScreenUp=({screenUp.X:F2},{screenUp.Y:F2},{screenUp.Z:F2}), 基准尺寸={baseDimension:F2}米, " +
+ $"相机距离={cameraDistanceMeters:F2}米, FOV=自动, 包围盒扩展系数={VIEW_BOUNDING_BOX_EXPANSION_FACTOR:F1}, 扩展边距={expansionMargin:F2}米");
}
///
@@ -518,4 +528,4 @@ namespace NavisworksTransport.Utils
#endregion
}
-}
\ No newline at end of file
+}
From 70aaff10bfcb3fa326bd70a88d6d4684fea60d60 Mon Sep 17 00:00:00 2001
From: tian <11429339@qq.com>
Date: Sun, 22 Mar 2026 19:45:09 +0800
Subject: [PATCH 17/87] Add assembly end-face guided optical axis
---
NavisworksTransport.UnitTests.csproj | 1 +
TransportPlugin.csproj | 1 +
.../AssemblyEndFaceAnalyzerTests.cs | 113 ++++++
src/UI/WPF/ViewModels/PathEditingViewModel.cs | 340 ++++++++++++++++-
src/UI/WPF/Views/PathEditingView.xaml | 4 +
src/Utils/Assembly/AssemblyEndFaceAnalyzer.cs | 342 ++++++++++++++++++
6 files changed, 786 insertions(+), 15 deletions(-)
create mode 100644 UnitTests/CoordinateSystem/AssemblyEndFaceAnalyzerTests.cs
create mode 100644 src/Utils/Assembly/AssemblyEndFaceAnalyzer.cs
diff --git a/NavisworksTransport.UnitTests.csproj b/NavisworksTransport.UnitTests.csproj
index 8cd57ca..eb50839 100644
--- a/NavisworksTransport.UnitTests.csproj
+++ b/NavisworksTransport.UnitTests.csproj
@@ -61,6 +61,7 @@
+
diff --git a/TransportPlugin.csproj b/TransportPlugin.csproj
index 9ab3261..bc1bef4 100644
--- a/TransportPlugin.csproj
+++ b/TransportPlugin.csproj
@@ -139,6 +139,7 @@
+
diff --git a/UnitTests/CoordinateSystem/AssemblyEndFaceAnalyzerTests.cs b/UnitTests/CoordinateSystem/AssemblyEndFaceAnalyzerTests.cs
new file mode 100644
index 0000000..4824dfc
--- /dev/null
+++ b/UnitTests/CoordinateSystem/AssemblyEndFaceAnalyzerTests.cs
@@ -0,0 +1,113 @@
+using System;
+using System.Collections.Generic;
+using System.Numerics;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using NavisworksTransport.Utils.GeometryAnalysis;
+
+namespace NavisworksTransport.UnitTests.CoordinateSystem
+{
+ [TestClass]
+ public class AssemblyEndFaceAnalyzerTests
+ {
+ [TestMethod]
+ public void Analyze_RectangularFaceWithSideNoise_ShouldReturnFaceCenter()
+ {
+ var triangles = new List();
+ triangles.AddRange(CreateRectangleFace(-2.0, 2.0, -1.0, 1.0, 10.0));
+ triangles.AddRange(CreateRectangleFace(-2.0, 2.0, -1.0, 1.0, 8.0));
+ triangles.Add(new AnalysisTriangle3(
+ new Vector3(-2.0f, -1.0f, 10.0f),
+ new Vector3(-2.0f, -1.0f, 9.0f),
+ new Vector3(-2.0f, 1.0f, 9.0f)));
+
+ EndFaceAnalysisResult result = AssemblyEndFaceAnalyzer.Analyze(
+ triangles,
+ new Vector3(-1.5f, -0.7f, 10.0f),
+ new Vector3(1.4f, -0.5f, 10.0f),
+ new Vector3(1.1f, 0.8f, 10.0f));
+
+ Assert.IsTrue(result.IsReliable, result.DiagnosticMessage);
+ AssertPoint(result.Center, 0.0, 0.0, 10.0);
+ Assert.AreEqual(2, result.CandidateTriangleCount);
+ }
+
+ [TestMethod]
+ public void Analyze_SquareRingFace_ShouldReturnRingCenter()
+ {
+ var triangles = new List();
+ triangles.AddRange(CreateRingFace(4.0, 2.0, 5.0));
+
+ EndFaceAnalysisResult result = AssemblyEndFaceAnalyzer.Analyze(
+ triangles,
+ new Vector3(-3.5f, -3.5f, 5.0f),
+ new Vector3(3.5f, -3.5f, 5.0f),
+ new Vector3(3.5f, 3.5f, 5.0f));
+
+ Assert.IsTrue(result.IsReliable, result.DiagnosticMessage);
+ AssertPoint(result.Center, 0.0, 0.0, 5.0);
+ Assert.IsTrue(result.CandidateTriangleCount >= 8);
+ }
+
+ [TestMethod]
+ public void Analyze_NearlyCollinearSeedPoints_ShouldFail()
+ {
+ var triangles = new List();
+ triangles.AddRange(CreateRectangleFace(-1.0, 1.0, -1.0, 1.0, 0.0));
+
+ EndFaceAnalysisResult result = AssemblyEndFaceAnalyzer.Analyze(
+ triangles,
+ new Vector3(0.0f, 0.0f, 0.0f),
+ new Vector3(1.0f, 0.0f, 0.0f),
+ new Vector3(2.0f, 0.0f, 0.0f));
+
+ Assert.IsFalse(result.IsReliable);
+ }
+
+ private static IEnumerable CreateRectangleFace(double minX, double maxX, double minY, double maxY, double z)
+ {
+ yield return new AnalysisTriangle3(
+ new Vector3((float)minX, (float)minY, (float)z),
+ new Vector3((float)maxX, (float)minY, (float)z),
+ new Vector3((float)maxX, (float)maxY, (float)z));
+ yield return new AnalysisTriangle3(
+ new Vector3((float)minX, (float)minY, (float)z),
+ new Vector3((float)maxX, (float)maxY, (float)z),
+ new Vector3((float)minX, (float)maxY, (float)z));
+ }
+
+ private static IEnumerable CreateRingFace(double outerHalfSize, double innerHalfSize, double z)
+ {
+ if (innerHalfSize >= outerHalfSize)
+ {
+ throw new ArgumentOutOfRangeException(nameof(innerHalfSize));
+ }
+
+ foreach (AnalysisTriangle3 triangle in CreateRectangleFace(-outerHalfSize, outerHalfSize, innerHalfSize, outerHalfSize, z))
+ {
+ yield return triangle;
+ }
+
+ foreach (AnalysisTriangle3 triangle in CreateRectangleFace(-outerHalfSize, outerHalfSize, -outerHalfSize, -innerHalfSize, z))
+ {
+ yield return triangle;
+ }
+
+ foreach (AnalysisTriangle3 triangle in CreateRectangleFace(-outerHalfSize, -innerHalfSize, -innerHalfSize, innerHalfSize, z))
+ {
+ yield return triangle;
+ }
+
+ foreach (AnalysisTriangle3 triangle in CreateRectangleFace(innerHalfSize, outerHalfSize, -innerHalfSize, innerHalfSize, z))
+ {
+ yield return triangle;
+ }
+ }
+
+ private static void AssertPoint(Vector3 actual, double x, double y, double z)
+ {
+ Assert.AreEqual(x, actual.X, 1e-5);
+ Assert.AreEqual(y, actual.Y, 1e-5);
+ Assert.AreEqual(z, actual.Z, 1e-5);
+ }
+ }
+}
diff --git a/src/UI/WPF/ViewModels/PathEditingViewModel.cs b/src/UI/WPF/ViewModels/PathEditingViewModel.cs
index 1d04ecd..ef84caa 100644
--- a/src/UI/WPF/ViewModels/PathEditingViewModel.cs
+++ b/src/UI/WPF/ViewModels/PathEditingViewModel.cs
@@ -8,6 +8,7 @@ using System.Windows.Input;
using System.Threading.Tasks;
using System.Linq;
using System.IO;
+using System.Numerics;
using Microsoft.Win32;
using Autodesk.Navisworks.Api;
using Autodesk.Navisworks.Api.Plugins;
@@ -19,6 +20,7 @@ using NavisworksTransport.UI.WPF.Views;
using NavisworksTransport.UI.WPF.Models;
using NavisworksTransport.Utils;
using NavisworksTransport.Utils.CoordinateSystem;
+using NavisworksTransport.Utils.GeometryAnalysis;
using NavisworksTransport;
namespace NavisworksTransport.UI.WPF.ViewModels
@@ -138,11 +140,19 @@ namespace NavisworksTransport.UI.WPF.ViewModels
private RailMountMode _assemblyMountMode = RailMountMode.UnderRail;
private bool _hasAssemblyTerminalObject;
private bool _isSelectingAssemblyStartPoint;
+ private bool _isSelectingAssemblyEndFacePoints;
+ private bool _hasAssemblyEndFaceAnalysis;
private ModelItem _assemblyTerminalObject;
private Point3D _assemblyStartPoint;
+ private Point3D _assemblyEndFaceCenterPoint;
+ private Vector3 _assemblyEndFaceNormal;
+ private readonly List _assemblyEndFaceSeedPoints = new List();
private const string AssemblyAnchorMarkerPathId = "assembly_anchor_marker";
private const string AssemblyCenterGuideLinePathId = "assembly_center_guide_line";
private const string AssemblyReferenceLinePathId = "assembly_reference_line";
+ private const string AssemblyEndFaceSeedPathId = "assembly_end_face_seed_points";
+ private const string AssemblyEndFaceCenterPathId = "assembly_end_face_center";
+ private const string AssemblyEndFaceNormalPathId = "assembly_end_face_normal";
// 自动路径起点和终点的路径对象引用(用于正确的ID管理)
private PathRoute _autoPathStartPointRoute = null;
@@ -960,6 +970,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
public ICommand GenerateAssemblyReferenceRodCommand { get; private set; }
public ICommand SelectAssemblyStartPointCommand { get; private set; }
public ICommand ClearAssemblyReferenceRodCommand { get; private set; }
+ public ICommand AnalyzeAssemblyTerminalFaceCommand { get; private set; }
// 多层吊装命令
public ICommand SelectMultiLevelStartPointCommand { get; private set; }
@@ -1002,6 +1013,12 @@ namespace NavisworksTransport.UI.WPF.ViewModels
_pathPlanningManager != null &&
AssemblyReferencePathManager.Instance.HasReferenceLine &&
!IsSelectingAssemblyStartPoint;
+ public bool CanAnalyzeAssemblyTerminalFace => HasAssemblyTerminalObject &&
+ _pathPlanningManager != null &&
+ !IsSelectingAssemblyStartPoint &&
+ !IsSelectingAssemblyEndFacePoints;
+
+ public bool IsSelectingAssemblyEndFacePoints => _isSelectingAssemblyEndFacePoints;
public bool CanExecuteModifyPoint => SelectedPathPoint != null &&
_pathPlanningManager?.PathEditState != PathEditState.EditingPoint;
@@ -1285,6 +1302,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
GenerateAssemblyReferenceRodCommand = new RelayCommand(async () => await ExecuteGenerateAssemblyReferenceRodAsync(), () => CanGenerateAssemblyReferenceRod);
SelectAssemblyStartPointCommand = new RelayCommand(async () => await ExecuteSelectAssemblyStartPointAsync(), () => CanSelectAssemblyStartPoint);
ClearAssemblyReferenceRodCommand = new RelayCommand(() => ExecuteClearAssemblyReferenceRod());
+ AnalyzeAssemblyTerminalFaceCommand = new RelayCommand(async () => await ExecuteAnalyzeAssemblyTerminalFaceAsync(), () => CanAnalyzeAssemblyTerminalFace);
}
#endregion
@@ -1317,8 +1335,10 @@ namespace NavisworksTransport.UI.WPF.ViewModels
InitializeAssemblyAnchorVerticalOffsetFromTerminalObject();
RefreshAssemblyTerminalObjectInfo();
RenderAssemblyAnchorMarker();
+ ClearAssemblyEndFaceAnalysisVisuals();
OnPropertyChanged(nameof(CanGenerateAssemblyReferenceRod));
OnPropertyChanged(nameof(CanSelectAssemblyStartPoint));
+ OnPropertyChanged(nameof(CanAnalyzeAssemblyTerminalFace));
UpdateMainStatus($"已捕获终点箱体: {AssemblyTerminalObjectName}");
LogManager.Info($"[直线装配] 已捕获终点箱体: {AssemblyTerminalObjectName}");
}, "捕获终点箱体");
@@ -1414,6 +1434,44 @@ namespace NavisworksTransport.UI.WPF.ViewModels
}
}
+ private async Task ExecuteAnalyzeAssemblyTerminalFaceAsync()
+ {
+ await SafeExecuteAsync(() =>
+ {
+ if (_pathPlanningManager == null)
+ {
+ throw new InvalidOperationException("路径规划管理器未初始化,无法分析端面。");
+ }
+
+ if (_assemblyTerminalObject == null || !ModelItemAnalysisHelper.IsModelItemValid(_assemblyTerminalObject))
+ {
+ throw new InvalidOperationException("终点箱体未设置或已失效,请先捕获终点箱体。");
+ }
+
+ CleanupAssemblyReferenceSelection();
+ CleanupAssemblyEndFaceSelection(clearVisuals: false);
+ ClearAssemblyEndFaceAnalysisVisuals();
+ _assemblyEndFaceSeedPoints.Clear();
+
+ _pathPlanningManager.DisableMouseHandling();
+ _isSelectingAssemblyEndFacePoints = true;
+ OnPropertyChanged(nameof(CanAnalyzeAssemblyTerminalFace));
+ OnPropertyChanged(nameof(CanSelectAssemblyStartPoint));
+
+ PathClickToolPlugin.MouseClicked -= OnAssemblyEndFaceMouseClicked;
+ PathClickToolPlugin.MouseClicked += OnAssemblyEndFaceMouseClicked;
+
+ if (!ForceReinitializeToolPlugin(subscribeToEvents: false))
+ {
+ CleanupAssemblyEndFaceSelection(clearVisuals: false);
+ throw new InvalidOperationException("ToolPlugin 初始化失败,请重试。");
+ }
+
+ UpdateMainStatus("请在同一个端面平面上连续点击 3 个点,系统将分析端面中心。");
+ LogManager.Info("[直线装配] 已进入端面三点分析模式");
+ }, "分析终端端面");
+ }
+
private async void OnAssemblyReferenceMouseClicked(object sender, PickItemResult pickResult)
{
try
@@ -1445,6 +1503,47 @@ namespace NavisworksTransport.UI.WPF.ViewModels
}
}
+ private async void OnAssemblyEndFaceMouseClicked(object sender, PickItemResult pickResult)
+ {
+ try
+ {
+ if (!_isSelectingAssemblyEndFacePoints || pickResult == null)
+ {
+ return;
+ }
+
+ await SafeExecuteAsync(() =>
+ {
+ if (!IsPickOnAssemblyTerminalObject(pickResult))
+ {
+ UpdateMainStatus("请点击当前终点箱体的同一个端面,不要点到其他对象。");
+ return;
+ }
+
+ _assemblyEndFaceSeedPoints.Add(pickResult.Point);
+ RenderAssemblyEndFaceSeedPoints();
+
+ int pickedCount = _assemblyEndFaceSeedPoints.Count;
+ LogManager.Info($"[直线装配] 已记录端面种子点 {pickedCount}: ({pickResult.Point.X:F3}, {pickResult.Point.Y:F3}, {pickResult.Point.Z:F3})");
+
+ if (pickedCount < 3)
+ {
+ UpdateMainStatus($"已记录端面点 {pickedCount}/3,请继续在同一端面平面上点击。");
+ return;
+ }
+
+ AnalyzeCurrentAssemblyEndFace();
+ CleanupAssemblyEndFaceSelection(clearVisuals: false);
+ }, "处理端面三点拾取");
+ }
+ catch (Exception ex)
+ {
+ LogManager.Error($"[直线装配] 端面三点分析失败: {ex.Message}", ex);
+ UpdateMainStatus($"端面三点分析失败: {ex.Message}");
+ CleanupAssemblyEndFaceSelection(clearVisuals: false);
+ }
+ }
+
private void CreateAssemblyLinearRoute(Point3D startPoint)
{
Point3D endPoint = AssemblyReferencePathManager.Instance.ReferenceLineEnd;
@@ -1505,15 +1604,18 @@ namespace NavisworksTransport.UI.WPF.ViewModels
private void HideAssemblyReferenceVisuals(bool resetStartPointText, string statusMessage, string logMessage)
{
CleanupAssemblyReferenceSelection();
+ CleanupAssemblyEndFaceSelection(clearVisuals: false);
AssemblyReferencePathManager.Instance.HideReferenceRod();
ClearAssemblyAnchorMarker();
ClearAssemblyCenterGuideLine();
ClearAssemblyReferenceLine();
+ ClearAssemblyEndFaceAnalysisVisuals();
if (resetStartPointText)
{
AssemblyStartPointText = "未选择";
}
OnPropertyChanged(nameof(CanSelectAssemblyStartPoint));
+ OnPropertyChanged(nameof(CanAnalyzeAssemblyTerminalFace));
if (!string.IsNullOrWhiteSpace(statusMessage))
{
UpdateMainStatus(statusMessage);
@@ -1552,6 +1654,21 @@ namespace NavisworksTransport.UI.WPF.ViewModels
return adapter.FromCanonicalPoint(canonicalAnchorPoint);
}
+ private Point3D GetAssemblyOpticalAxisReferencePoint()
+ {
+ if (_hasAssemblyEndFaceAnalysis && _assemblyEndFaceCenterPoint != null)
+ {
+ return _assemblyEndFaceCenterPoint;
+ }
+
+ if (_assemblyTerminalObject == null || !ModelItemAnalysisHelper.IsModelItemValid(_assemblyTerminalObject))
+ {
+ throw new InvalidOperationException("终点箱体未设置或已失效,无法计算光轴参考点");
+ }
+
+ return _assemblyTerminalObject.BoundingBox().Center;
+ }
+
private AssemblyReferenceLine BuildAssemblyReferenceLine()
{
if (_assemblyTerminalObject == null || !ModelItemAnalysisHelper.IsModelItemValid(_assemblyTerminalObject))
@@ -1562,20 +1679,19 @@ namespace NavisworksTransport.UI.WPF.ViewModels
HostCoordinateAdapter adapter = CoordinateSystemManager.Instance.CreateHostAdapter();
ProjectReferenceFrame projectFrame = CreateAssemblyProjectReferenceFrame(adapter);
- BoundingBox3D bounds = _assemblyTerminalObject.BoundingBox();
- Point3D centerPoint = bounds.Center;
+ Point3D opticalAxisReferencePoint = GetAssemblyOpticalAxisReferencePoint();
Point3D endPoint = GetAssemblyTerminalAnchorPoint();
- Point3D canonicalCenterPoint = adapter.ToCanonicalPoint(centerPoint);
+ Point3D canonicalAxisReferencePoint = adapter.ToCanonicalPoint(opticalAxisReferencePoint);
Point3D canonicalEndPoint = adapter.ToCanonicalPoint(endPoint);
Vector3D direction = new Vector3D(
- canonicalCenterPoint.X - projectFrame.SphereCenterInCanonical.X,
- canonicalCenterPoint.Y - projectFrame.SphereCenterInCanonical.Y,
- canonicalCenterPoint.Z - projectFrame.SphereCenterInCanonical.Z);
+ canonicalAxisReferencePoint.X - projectFrame.SphereCenterInCanonical.X,
+ canonicalAxisReferencePoint.Y - projectFrame.SphereCenterInCanonical.Y,
+ canonicalAxisReferencePoint.Z - projectFrame.SphereCenterInCanonical.Z);
double directionLengthSquared = direction.X * direction.X + direction.Y * direction.Y + direction.Z * direction.Z;
if (directionLengthSquared < 1e-9)
{
- throw new InvalidOperationException("箱体中心与项目球心重合,无法生成装配参考线方向");
+ throw new InvalidOperationException("光轴参考点与项目球心重合,无法生成装配参考线方向");
}
direction = direction.Normalize();
@@ -1588,9 +1704,9 @@ namespace NavisworksTransport.UI.WPF.ViewModels
LogManager.Info(
$"[直线装配] 参考线已计算,终点锚点=({endPoint.X:F2}, {endPoint.Y:F2}, {endPoint.Z:F2}), " +
- $"箱体中心=({centerPoint.X:F2}, {centerPoint.Y:F2}, {centerPoint.Z:F2}), " +
+ $"光轴参考点=({opticalAxisReferencePoint.X:F2}, {opticalAxisReferencePoint.Y:F2}, {opticalAxisReferencePoint.Z:F2}), " +
$"参考线外端=({startPoint.X:F2}, {startPoint.Y:F2}, {startPoint.Z:F2}), " +
- $"球心到箱体中心方向(内部坐标)=({direction.X:F3}, {direction.Y:F3}, {direction.Z:F3})");
+ $"球心到光轴参考点方向(内部坐标)=({direction.X:F3}, {direction.Y:F3}, {direction.Z:F3})");
return new AssemblyReferenceLine(startPoint, endPoint, direction);
}
@@ -1606,11 +1722,15 @@ namespace NavisworksTransport.UI.WPF.ViewModels
Point3D anchorPoint = GetAssemblyTerminalAnchorPoint();
string anchorText = GetAssemblyAnchorText();
string mountText = AssemblyMountMode == RailMountMode.OverRail ? "轨上安装" : "轨下安装";
+ Point3D opticalAxisReferencePoint = GetAssemblyOpticalAxisReferencePoint();
+ string opticalAxisText = _hasAssemblyEndFaceAnalysis
+ ? string.Format("端面中心=({0:F2}, {1:F2}, {2:F2})", opticalAxisReferencePoint.X, opticalAxisReferencePoint.Y, opticalAxisReferencePoint.Z)
+ : string.Format("箱体中心=({0:F2}, {1:F2}, {2:F2})", opticalAxisReferencePoint.X, opticalAxisReferencePoint.Y, opticalAxisReferencePoint.Z);
if (referenceStartPoint != null && referenceEndPoint != null)
{
AssemblyTerminalObjectInfo = string.Format(
- "中心=({0:F2}, {1:F2}, {2:F2}),尺寸=({3:F2}, {4:F2}, {5:F2}),{6},{7}点=({8:F2}, {9:F2}, {10:F2}),垂直偏移={11:F3}m,辅助线外端=({12:F2}, {13:F2}, {14:F2})",
+ "中心=({0:F2}, {1:F2}, {2:F2}),尺寸=({3:F2}, {4:F2}, {5:F2}),{6},{7}点=({8:F2}, {9:F2}, {10:F2}),{11},垂直偏移={12:F3}m,辅助线外端=({13:F2}, {14:F2}, {15:F2})",
bounds.Center.X,
bounds.Center.Y,
bounds.Center.Z,
@@ -1622,6 +1742,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
referenceEndPoint.X,
referenceEndPoint.Y,
referenceEndPoint.Z,
+ opticalAxisText,
AssemblyAnchorVerticalOffsetInMeters,
referenceStartPoint.X,
referenceStartPoint.Y,
@@ -1630,7 +1751,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
}
AssemblyTerminalObjectInfo = string.Format(
- "中心=({0:F2}, {1:F2}, {2:F2}),尺寸=({3:F2}, {4:F2}, {5:F2}),{6},{7}点=({8:F2}, {9:F2}, {10:F2}),垂直偏移={11:F3}m",
+ "中心=({0:F2}, {1:F2}, {2:F2}),尺寸=({3:F2}, {4:F2}, {5:F2}),{6},{7}点=({8:F2}, {9:F2}, {10:F2}),{11},垂直偏移={12:F3}m",
bounds.Center.X,
bounds.Center.Y,
bounds.Center.Z,
@@ -1642,6 +1763,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
anchorPoint.X,
anchorPoint.Y,
anchorPoint.Z,
+ opticalAxisText,
AssemblyAnchorVerticalOffsetInMeters);
}
@@ -1800,7 +1922,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
Id = AssemblyAnchorMarkerPathId,
Description = "直线装配终点对接标记"
};
- markerRoute.AddPoint(new PathPoint(anchorPoint, "对接点", PathPointType.EndPoint));
+ AddVisualizationPoint(markerRoute, anchorPoint, "对接点", PathPointType.EndPoint);
renderPlugin.RenderPointOnly(markerRoute);
}
@@ -1837,7 +1959,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
HostCoordinateAdapter adapter = CoordinateSystemManager.Instance.CreateHostAdapter();
ProjectReferenceFrame projectFrame = CreateAssemblyProjectReferenceFrame(adapter);
- Point3D centerPoint = _assemblyTerminalObject.BoundingBox().Center;
+ Point3D centerPoint = GetAssemblyOpticalAxisReferencePoint();
Point3D sphereCenterPoint = adapter.FromCanonicalPoint(projectFrame.SphereCenterInCanonical);
renderPlugin.RenderRailBaseline(
AssemblyCenterGuideLinePathId,
@@ -1845,8 +1967,90 @@ namespace NavisworksTransport.UI.WPF.ViewModels
RenderStyleName.AssemblyGuideLine);
LogManager.Info(
- $"[直线装配] 已渲染球心到箱体中心基准线: 球心=({sphereCenterPoint.X:F2}, {sphereCenterPoint.Y:F2}, {sphereCenterPoint.Z:F2}), " +
- $"箱体中心=({centerPoint.X:F2}, {centerPoint.Y:F2}, {centerPoint.Z:F2})");
+ $"[直线装配] 已渲染球心到光轴参考点基准线: 球心=({sphereCenterPoint.X:F2}, {sphereCenterPoint.Y:F2}, {sphereCenterPoint.Z:F2}), " +
+ $"光轴参考点=({centerPoint.X:F2}, {centerPoint.Y:F2}, {centerPoint.Z:F2})");
+ }
+
+ private void RenderAssemblyEndFaceSeedPoints()
+ {
+ var renderPlugin = PathPointRenderPlugin.Instance;
+ if (renderPlugin == null)
+ {
+ return;
+ }
+
+ renderPlugin.RemovePath(AssemblyEndFaceSeedPathId);
+
+ if (_assemblyEndFaceSeedPoints.Count == 0)
+ {
+ return;
+ }
+
+ var markerRoute = new PathRoute("端面三点")
+ {
+ Id = AssemblyEndFaceSeedPathId,
+ Description = "终端安装端面三点"
+ };
+
+ for (int i = 0; i < _assemblyEndFaceSeedPoints.Count; i++)
+ {
+ AddVisualizationPoint(markerRoute, _assemblyEndFaceSeedPoints[i], $"端面点{i + 1}", PathPointType.WayPoint);
+ }
+
+ renderPlugin.RenderPointOnly(markerRoute);
+ }
+
+ private void RenderAssemblyEndFaceCenter(Point3D centerPoint)
+ {
+ var renderPlugin = PathPointRenderPlugin.Instance;
+ if (renderPlugin == null)
+ {
+ return;
+ }
+
+ renderPlugin.RemovePath(AssemblyEndFaceCenterPathId);
+
+ var markerRoute = new PathRoute("端面中心")
+ {
+ Id = AssemblyEndFaceCenterPathId,
+ Description = "终端安装端面中心"
+ };
+ AddVisualizationPoint(markerRoute, centerPoint, "端面中心", PathPointType.EndPoint);
+ renderPlugin.RenderPointOnly(markerRoute);
+ }
+
+ private static void AddVisualizationPoint(PathRoute route, Point3D position, string name, PathPointType type)
+ {
+ if (route == null)
+ {
+ throw new ArgumentNullException(nameof(route));
+ }
+
+ route.Points.Add(new PathPoint(position, name, type)
+ {
+ Index = route.Points.Count
+ });
+ }
+
+ private void RenderAssemblyEndFaceNormal(Point3D centerPoint, Vector3 normal)
+ {
+ var renderPlugin = PathPointRenderPlugin.Instance;
+ if (renderPlugin == null)
+ {
+ return;
+ }
+
+ renderPlugin.ClearRailBaseline(AssemblyEndFaceNormalPathId);
+
+ double lineLength = UnitsConverter.ConvertFromMeters(Math.Max(0.5, AssemblyReferenceRodDiameterInMeters * 4.0));
+ Point3D normalEndPoint = new Point3D(
+ centerPoint.X + normal.X * (float)lineLength,
+ centerPoint.Y + normal.Y * (float)lineLength,
+ centerPoint.Z + normal.Z * (float)lineLength);
+ renderPlugin.RenderRailBaseline(
+ AssemblyEndFaceNormalPathId,
+ new List { centerPoint, normalEndPoint },
+ RenderStyleName.AssemblyGuideLine);
}
private ProjectReferenceFrame CreateAssemblyProjectReferenceFrame(HostCoordinateAdapter adapter)
@@ -1901,6 +2105,22 @@ namespace NavisworksTransport.UI.WPF.ViewModels
renderPlugin.ClearRailBaseline(AssemblyCenterGuideLinePathId);
}
+ private void ClearAssemblyEndFaceAnalysisVisuals()
+ {
+ var renderPlugin = PathPointRenderPlugin.Instance;
+ if (renderPlugin == null)
+ {
+ return;
+ }
+
+ renderPlugin.RemovePath(AssemblyEndFaceSeedPathId);
+ renderPlugin.RemovePath(AssemblyEndFaceCenterPathId);
+ renderPlugin.ClearRailBaseline(AssemblyEndFaceNormalPathId);
+ _hasAssemblyEndFaceAnalysis = false;
+ _assemblyEndFaceCenterPoint = null;
+ _assemblyEndFaceNormal = default(Vector3);
+ }
+
private void CleanupAssemblyReferenceSelection()
{
try
@@ -1917,6 +2137,87 @@ namespace NavisworksTransport.UI.WPF.ViewModels
}
}
+ private void CleanupAssemblyEndFaceSelection(bool clearVisuals)
+ {
+ try
+ {
+ PathClickToolPlugin.MouseClicked -= OnAssemblyEndFaceMouseClicked;
+ _isSelectingAssemblyEndFacePoints = false;
+ _pathPlanningManager?.EnableMouseHandling();
+ _pathPlanningManager?.StopClickTool();
+ if (clearVisuals)
+ {
+ _assemblyEndFaceSeedPoints.Clear();
+ ClearAssemblyEndFaceAnalysisVisuals();
+ }
+ OnPropertyChanged(nameof(CanAnalyzeAssemblyTerminalFace));
+ OnPropertyChanged(nameof(CanSelectAssemblyStartPoint));
+ }
+ catch (Exception ex)
+ {
+ LogManager.Error($"[直线装配] 清理端面三点拾取状态失败: {ex.Message}", ex);
+ }
+ }
+
+ private bool IsPickOnAssemblyTerminalObject(PickItemResult pickResult)
+ {
+ if (pickResult?.ModelItem == null || _assemblyTerminalObject == null)
+ {
+ return false;
+ }
+
+ if (ModelItemAnalysisHelper.ModelItemEquals(pickResult.ModelItem, _assemblyTerminalObject))
+ {
+ return true;
+ }
+
+ return pickResult.ModelItem.AncestorsAndSelf.Any(ancestor => ModelItemAnalysisHelper.ModelItemEquals(ancestor, _assemblyTerminalObject));
+ }
+
+ private void AnalyzeCurrentAssemblyEndFace()
+ {
+ if (_assemblyEndFaceSeedPoints.Count != 3)
+ {
+ throw new InvalidOperationException("端面分析需要恰好 3 个种子点。");
+ }
+
+ var triangles = GeometryHelper.ExtractTriangles(new[] { _assemblyTerminalObject })
+ .Select(triangle => new AnalysisTriangle3(
+ new Vector3((float)triangle.Point1.X, (float)triangle.Point1.Y, (float)triangle.Point1.Z),
+ new Vector3((float)triangle.Point2.X, (float)triangle.Point2.Y, (float)triangle.Point2.Z),
+ new Vector3((float)triangle.Point3.X, (float)triangle.Point3.Y, (float)triangle.Point3.Z)))
+ .ToList();
+
+ EndFaceAnalysisResult result = AssemblyEndFaceAnalyzer.Analyze(
+ triangles,
+ new Vector3((float)_assemblyEndFaceSeedPoints[0].X, (float)_assemblyEndFaceSeedPoints[0].Y, (float)_assemblyEndFaceSeedPoints[0].Z),
+ new Vector3((float)_assemblyEndFaceSeedPoints[1].X, (float)_assemblyEndFaceSeedPoints[1].Y, (float)_assemblyEndFaceSeedPoints[1].Z),
+ new Vector3((float)_assemblyEndFaceSeedPoints[2].X, (float)_assemblyEndFaceSeedPoints[2].Y, (float)_assemblyEndFaceSeedPoints[2].Z));
+
+ if (!result.IsReliable)
+ {
+ throw new InvalidOperationException(result.DiagnosticMessage);
+ }
+
+ Point3D centerPoint = AssemblyEndFaceAnalyzer.ToPoint3D(result.Center);
+ _hasAssemblyEndFaceAnalysis = true;
+ _assemblyEndFaceCenterPoint = centerPoint;
+ _assemblyEndFaceNormal = result.Normal;
+ RenderAssemblyEndFaceSeedPoints();
+ RenderAssemblyEndFaceCenter(centerPoint);
+ RenderAssemblyEndFaceNormal(centerPoint, result.Normal);
+ RefreshAssemblyTerminalObjectInfo();
+ RefreshAssemblyReferenceRodIfNeeded();
+
+ AssemblyTerminalObjectInfo =
+ $"{AssemblyTerminalObjectInfo} | 端面中心=({centerPoint.X:F2}, {centerPoint.Y:F2}, {centerPoint.Z:F2}),候选三角形={result.CandidateTriangleCount},偏差={result.MaxPlaneDeviation:F6}";
+ UpdateMainStatus($"端面分析完成:中心=({centerPoint.X:F2}, {centerPoint.Y:F2}, {centerPoint.Z:F2}),候选三角形={result.CandidateTriangleCount}");
+ LogManager.Info(
+ $"[直线装配] 端面分析完成: 中心=({centerPoint.X:F3}, {centerPoint.Y:F3}, {centerPoint.Z:F3}), " +
+ $"法向=({result.Normal.X:F4}, {result.Normal.Y:F4}, {result.Normal.Z:F4}), " +
+ $"三角形={result.CandidateTriangleCount}, 顶点={result.CandidateVertexCount}, 偏差={result.MaxPlaneDeviation:F6}");
+ }
+
private async Task ExecuteNewPathAsync()
{
await SafeExecuteAsync(() =>
@@ -5377,6 +5678,15 @@ namespace NavisworksTransport.UI.WPF.ViewModels
LogManager.Warning($"清理直线装配事件订阅时发生异常: {ex.Message}");
}
+ try
+ {
+ CleanupAssemblyEndFaceSelection(clearVisuals: true);
+ }
+ catch (Exception ex)
+ {
+ LogManager.Warning($"清理端面三点分析事件订阅时发生异常: {ex.Message}");
+ }
+
try
{
ClearAssemblyAnchorMarker();
diff --git a/src/UI/WPF/Views/PathEditingView.xaml b/src/UI/WPF/Views/PathEditingView.xaml
index 6c14832..0cea737 100644
--- a/src/UI/WPF/Views/PathEditingView.xaml
+++ b/src/UI/WPF/Views/PathEditingView.xaml
@@ -462,6 +462,10 @@ NavisworksTransport 路径编辑页签视图 - 采用与动画控制和分层管
Command="{Binding SelectAssemblyStartPointCommand}"
Style="{StaticResource ActionButtonStyle}"
IsEnabled="{Binding CanSelectAssemblyStartPoint}"/>
+
diff --git a/src/Utils/Assembly/AssemblyEndFaceAnalyzer.cs b/src/Utils/Assembly/AssemblyEndFaceAnalyzer.cs
new file mode 100644
index 0000000..7eda516
--- /dev/null
+++ b/src/Utils/Assembly/AssemblyEndFaceAnalyzer.cs
@@ -0,0 +1,342 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Numerics;
+using Autodesk.Navisworks.Api;
+
+namespace NavisworksTransport.Utils.GeometryAnalysis
+{
+ ///
+ /// 基于端面上的三个种子点,从真实三角几何中识别端面并估计几何中心。
+ /// 算法核心使用纯 Vector3,便于单测独立运行。
+ ///
+ public static class AssemblyEndFaceAnalyzer
+ {
+ private const double MinimumSeedTriangleAreaSquared = 1e-8;
+ private const double PlaneDistanceTolerance = 1e-3;
+ private const double NormalAlignmentCosineThreshold = 0.98;
+ private const double MinimumProjectedExtent = 1e-6;
+
+ public static EndFaceAnalysisResult Analyze(ModelItem modelItem, Point3D seedPoint1, Point3D seedPoint2, Point3D seedPoint3)
+ {
+ if (modelItem == null)
+ {
+ throw new ArgumentNullException(nameof(modelItem));
+ }
+
+ List triangles = GeometryHelper.ExtractTriangles(new[] { modelItem })
+ .Select(ConvertTriangle)
+ .ToList();
+
+ return Analyze(triangles, ToVector3(seedPoint1), ToVector3(seedPoint2), ToVector3(seedPoint3));
+ }
+
+ public static EndFaceAnalysisResult Analyze(
+ IEnumerable triangles,
+ Vector3 seedPoint1,
+ Vector3 seedPoint2,
+ Vector3 seedPoint3)
+ {
+ if (triangles == null)
+ {
+ throw new ArgumentNullException(nameof(triangles));
+ }
+
+ List triangleList = triangles.ToList();
+ if (triangleList.Count == 0)
+ {
+ return EndFaceAnalysisResult.Failure("没有可用的三角几何数据。");
+ }
+
+ PlaneDefinition seedPlane;
+ if (!TryCreatePlane(seedPoint1, seedPoint2, seedPoint3, out seedPlane))
+ {
+ return EndFaceAnalysisResult.Failure("三个端面点近似共线,无法拟合端面平面。");
+ }
+
+ var candidateTriangles = new List();
+ var candidatePoints = new HashSet(new Vector3EqualityComparer());
+ double maxPlaneDeviation = 0.0;
+
+ foreach (AnalysisTriangle3 triangle in triangleList)
+ {
+ Vector3 triangleNormal;
+ if (!TryGetTriangleNormal(triangle, out triangleNormal))
+ {
+ continue;
+ }
+
+ double normalAlignment = Math.Abs(Vector3.Dot(triangleNormal, seedPlane.Normal));
+ if (normalAlignment < NormalAlignmentCosineThreshold)
+ {
+ continue;
+ }
+
+ double distance1 = Math.Abs(GetSignedDistanceToPlane(seedPlane, triangle.Point1));
+ double distance2 = Math.Abs(GetSignedDistanceToPlane(seedPlane, triangle.Point2));
+ double distance3 = Math.Abs(GetSignedDistanceToPlane(seedPlane, triangle.Point3));
+ double triangleDeviation = Math.Max(distance1, Math.Max(distance2, distance3));
+ if (triangleDeviation > PlaneDistanceTolerance)
+ {
+ continue;
+ }
+
+ candidateTriangles.Add(triangle);
+ candidatePoints.Add(ProjectPointToPlane(seedPlane, triangle.Point1));
+ candidatePoints.Add(ProjectPointToPlane(seedPlane, triangle.Point2));
+ candidatePoints.Add(ProjectPointToPlane(seedPlane, triangle.Point3));
+ maxPlaneDeviation = Math.Max(maxPlaneDeviation, triangleDeviation);
+ }
+
+ if (candidateTriangles.Count == 0 || candidatePoints.Count < 3)
+ {
+ return EndFaceAnalysisResult.Failure("没有识别到足够的端面共面三角形。");
+ }
+
+ PlaneDefinition refinedPlane = RefinePlane(seedPlane, candidatePoints);
+ PlaneBasis planeBasis = CreatePlaneBasis(refinedPlane);
+
+ double minU = double.MaxValue;
+ double maxU = double.MinValue;
+ double minV = double.MaxValue;
+ double maxV = double.MinValue;
+
+ foreach (Vector3 candidatePoint in candidatePoints)
+ {
+ Vector2 projected = ProjectToPlane2D(candidatePoint, planeBasis);
+ minU = Math.Min(minU, projected.X);
+ maxU = Math.Max(maxU, projected.X);
+ minV = Math.Min(minV, projected.Y);
+ maxV = Math.Max(maxV, projected.Y);
+ }
+
+ double extentU = maxU - minU;
+ double extentV = maxV - minV;
+ if (extentU < MinimumProjectedExtent || extentV < MinimumProjectedExtent)
+ {
+ return EndFaceAnalysisResult.Failure("识别到的端面分布过窄,无法稳定求中心。");
+ }
+
+ Vector3 center = ProjectFromPlane2D((minU + maxU) / 2.0, (minV + maxV) / 2.0, planeBasis);
+
+ return EndFaceAnalysisResult.Success(
+ center,
+ refinedPlane.Normal,
+ candidateTriangles.Count,
+ candidatePoints.Count,
+ maxPlaneDeviation,
+ $"端面识别完成: 三角形={candidateTriangles.Count}, 顶点={candidatePoints.Count}, 偏差={maxPlaneDeviation:F6}");
+ }
+
+ public static Point3D ToPoint3D(Vector3 point)
+ {
+ return new Point3D(point.X, point.Y, point.Z);
+ }
+
+ private static AnalysisTriangle3 ConvertTriangle(Triangle3D triangle)
+ {
+ return new AnalysisTriangle3(ToVector3(triangle.Point1), ToVector3(triangle.Point2), ToVector3(triangle.Point3));
+ }
+
+ private static Vector3 ToVector3(Point3D point)
+ {
+ return new Vector3((float)point.X, (float)point.Y, (float)point.Z);
+ }
+
+ private static bool TryCreatePlane(Vector3 point1, Vector3 point2, Vector3 point3, out PlaneDefinition plane)
+ {
+ Vector3 edge1 = point2 - point1;
+ Vector3 edge2 = point3 - point1;
+ Vector3 normal = Vector3.Cross(edge1, edge2);
+ if (normal.LengthSquared() < MinimumSeedTriangleAreaSquared)
+ {
+ plane = default(PlaneDefinition);
+ return false;
+ }
+
+ Vector3 normalizedNormal = Vector3.Normalize(normal);
+ plane = new PlaneDefinition(normalizedNormal, Vector3.Dot(normalizedNormal, point1));
+ return true;
+ }
+
+ private static bool TryGetTriangleNormal(AnalysisTriangle3 triangle, out Vector3 normal)
+ {
+ Vector3 edge1 = triangle.Point2 - triangle.Point1;
+ Vector3 edge2 = triangle.Point3 - triangle.Point1;
+ Vector3 cross = Vector3.Cross(edge1, edge2);
+ if (cross.LengthSquared() < 1e-12)
+ {
+ normal = Vector3.Zero;
+ return false;
+ }
+
+ normal = Vector3.Normalize(cross);
+ return true;
+ }
+
+ private static PlaneDefinition RefinePlane(PlaneDefinition seedPlane, IEnumerable candidatePoints)
+ {
+ double averageOffset = candidatePoints.Average(point => Vector3.Dot(seedPlane.Normal, point));
+ return new PlaneDefinition(seedPlane.Normal, averageOffset);
+ }
+
+ private static double GetSignedDistanceToPlane(PlaneDefinition plane, Vector3 point)
+ {
+ return Vector3.Dot(plane.Normal, point) - plane.Offset;
+ }
+
+ private static Vector3 ProjectPointToPlane(PlaneDefinition plane, Vector3 point)
+ {
+ float signedDistance = (float)GetSignedDistanceToPlane(plane, point);
+ return point - plane.Normal * signedDistance;
+ }
+
+ private static PlaneBasis CreatePlaneBasis(PlaneDefinition plane)
+ {
+ Vector3 reference = Math.Abs(plane.Normal.Z) < 0.9f
+ ? new Vector3(0.0f, 0.0f, 1.0f)
+ : new Vector3(1.0f, 0.0f, 0.0f);
+ Vector3 axisU = Vector3.Normalize(Vector3.Cross(reference, plane.Normal));
+ Vector3 axisV = Vector3.Normalize(Vector3.Cross(plane.Normal, axisU));
+ Vector3 origin = plane.Normal * (float)plane.Offset;
+ return new PlaneBasis(origin, axisU, axisV);
+ }
+
+ private static Vector2 ProjectToPlane2D(Vector3 point, PlaneBasis basis)
+ {
+ Vector3 vector = point - basis.Origin;
+ return new Vector2(Vector3.Dot(vector, basis.AxisU), Vector3.Dot(vector, basis.AxisV));
+ }
+
+ private static Vector3 ProjectFromPlane2D(double u, double v, PlaneBasis basis)
+ {
+ return basis.Origin + basis.AxisU * (float)u + basis.AxisV * (float)v;
+ }
+
+ private struct PlaneDefinition
+ {
+ public PlaneDefinition(Vector3 normal, double offset)
+ {
+ Normal = normal;
+ Offset = offset;
+ }
+
+ public Vector3 Normal { get; }
+ public double Offset { get; }
+ }
+
+ private struct PlaneBasis
+ {
+ public PlaneBasis(Vector3 origin, Vector3 axisU, Vector3 axisV)
+ {
+ Origin = origin;
+ AxisU = axisU;
+ AxisV = axisV;
+ }
+
+ public Vector3 Origin { get; }
+ public Vector3 AxisU { get; }
+ public Vector3 AxisV { get; }
+ }
+
+ private sealed class Vector3EqualityComparer : IEqualityComparer
+ {
+ private const float Tolerance = 1e-5f;
+
+ public bool Equals(Vector3 x, Vector3 y)
+ {
+ return Math.Abs(x.X - y.X) <= Tolerance &&
+ Math.Abs(x.Y - y.Y) <= Tolerance &&
+ Math.Abs(x.Z - y.Z) <= Tolerance;
+ }
+
+ public int GetHashCode(Vector3 obj)
+ {
+ int x = (int)Math.Round(obj.X / Tolerance);
+ int y = (int)Math.Round(obj.Y / Tolerance);
+ int z = (int)Math.Round(obj.Z / Tolerance);
+ unchecked
+ {
+ int hash = 17;
+ hash = hash * 31 + x;
+ hash = hash * 31 + y;
+ hash = hash * 31 + z;
+ return hash;
+ }
+ }
+ }
+ }
+
+ public struct AnalysisTriangle3
+ {
+ public AnalysisTriangle3(Vector3 point1, Vector3 point2, Vector3 point3)
+ {
+ Point1 = point1;
+ Point2 = point2;
+ Point3 = point3;
+ }
+
+ public Vector3 Point1 { get; }
+ public Vector3 Point2 { get; }
+ public Vector3 Point3 { get; }
+ }
+
+ public sealed class EndFaceAnalysisResult
+ {
+ private EndFaceAnalysisResult(
+ bool isReliable,
+ Vector3 center,
+ Vector3 normal,
+ int candidateTriangleCount,
+ int candidateVertexCount,
+ double maxPlaneDeviation,
+ string diagnosticMessage)
+ {
+ IsReliable = isReliable;
+ Center = center;
+ Normal = normal;
+ CandidateTriangleCount = candidateTriangleCount;
+ CandidateVertexCount = candidateVertexCount;
+ MaxPlaneDeviation = maxPlaneDeviation;
+ DiagnosticMessage = diagnosticMessage;
+ }
+
+ public bool IsReliable { get; }
+ public Vector3 Center { get; }
+ public Vector3 Normal { get; }
+ public int CandidateTriangleCount { get; }
+ public int CandidateVertexCount { get; }
+ public double MaxPlaneDeviation { get; }
+ public string DiagnosticMessage { get; }
+
+ public static EndFaceAnalysisResult Success(
+ Vector3 center,
+ Vector3 normal,
+ int candidateTriangleCount,
+ int candidateVertexCount,
+ double maxPlaneDeviation,
+ string diagnosticMessage)
+ {
+ return new EndFaceAnalysisResult(
+ true,
+ center,
+ normal,
+ candidateTriangleCount,
+ candidateVertexCount,
+ maxPlaneDeviation,
+ diagnosticMessage);
+ }
+
+ public static EndFaceAnalysisResult Failure(string diagnosticMessage)
+ {
+ return new EndFaceAnalysisResult(
+ false,
+ Vector3.Zero,
+ Vector3.Zero,
+ 0,
+ 0,
+ 0.0,
+ diagnosticMessage);
+ }
+ }
+}
From 93ad55c725a928ccd2f115052a25d2ccd13d7bfb Mon Sep 17 00:00:00 2001
From: tian <11429339@qq.com>
Date: Sun, 22 Mar 2026 23:58:11 +0800
Subject: [PATCH 18/87] Refine assembly installation workflow UI
---
NavisworksTransport.UnitTests.csproj | 1 +
TransportPlugin.csproj | 1 +
...semblyInstallationReferenceBuilderTests.cs | 43 ++
src/Core/PathPointRenderPlugin.cs | 85 ++++
src/UI/WPF/ViewModels/PathEditingViewModel.cs | 402 +++++++++++++++++-
src/UI/WPF/Views/PathEditingView.xaml | 316 +++++---------
.../AssemblyInstallationReferenceBuilder.cs | 75 ++++
.../CanonicalRailPoseBuilder.cs | 1 +
8 files changed, 701 insertions(+), 223 deletions(-)
create mode 100644 UnitTests/CoordinateSystem/AssemblyInstallationReferenceBuilderTests.cs
create mode 100644 src/Utils/Assembly/AssemblyInstallationReferenceBuilder.cs
diff --git a/NavisworksTransport.UnitTests.csproj b/NavisworksTransport.UnitTests.csproj
index eb50839..6ae959b 100644
--- a/NavisworksTransport.UnitTests.csproj
+++ b/NavisworksTransport.UnitTests.csproj
@@ -62,6 +62,7 @@
+
diff --git a/TransportPlugin.csproj b/TransportPlugin.csproj
index bc1bef4..ce75558 100644
--- a/TransportPlugin.csproj
+++ b/TransportPlugin.csproj
@@ -140,6 +140,7 @@
+
diff --git a/UnitTests/CoordinateSystem/AssemblyInstallationReferenceBuilderTests.cs b/UnitTests/CoordinateSystem/AssemblyInstallationReferenceBuilderTests.cs
new file mode 100644
index 0000000..55f7396
--- /dev/null
+++ b/UnitTests/CoordinateSystem/AssemblyInstallationReferenceBuilderTests.cs
@@ -0,0 +1,43 @@
+using System;
+using System.Numerics;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using NavisworksTransport.Utils.GeometryAnalysis;
+
+namespace NavisworksTransport.UnitTests.CoordinateSystem
+{
+ [TestClass]
+ public class AssemblyInstallationReferenceBuilderTests
+ {
+ [TestMethod]
+ public void Build_ShouldCreatePlaneParallelToOpticalAxisAndProjectedAnchor()
+ {
+ Vector3 axisBase = new Vector3(0f, 0f, 0f);
+ Vector3 axisDirection = Vector3.UnitX;
+ Vector3 pickPoint = new Vector3(2f, 3f, 4f);
+
+ AssemblyInstallationReferenceResult result =
+ AssemblyInstallationReferenceBuilder.Build(axisBase, axisDirection, pickPoint);
+
+ Assert.AreEqual(5f, result.OffsetDistance, 1e-4f);
+ Assert.AreEqual(2f, result.AnchorPoint.X, 1e-4f);
+ Assert.AreEqual(3f, result.AnchorPoint.Y, 1e-4f);
+ Assert.AreEqual(4f, result.AnchorPoint.Z, 1e-4f);
+ Assert.AreEqual(0f, Vector3.Dot(result.PlaneNormal, result.OpticalAxisDirection), 1e-4f);
+ Assert.AreEqual(0f, Vector3.Dot(result.PlaneSpanDirection, result.OpticalAxisDirection), 1e-4f);
+ Assert.AreEqual(0f, Vector3.Dot(result.PlaneNormal, result.PlaneSpanDirection), 1e-4f);
+ }
+
+ [TestMethod]
+ public void Build_WhenPickPointTooCloseToOpticalAxis_ShouldThrow()
+ {
+ Vector3 axisBase = new Vector3(0f, 0f, 0f);
+ Vector3 axisDirection = Vector3.UnitX;
+ Vector3 pickPoint = new Vector3(2f, 0f, 0f);
+
+ InvalidOperationException ex = Assert.ThrowsException(
+ () => AssemblyInstallationReferenceBuilder.Build(axisBase, axisDirection, pickPoint));
+
+ StringAssert.Contains(ex.Message, "距离光轴过小");
+ }
+ }
+}
diff --git a/src/Core/PathPointRenderPlugin.cs b/src/Core/PathPointRenderPlugin.cs
index 04a1fbc..36b7d80 100644
--- a/src/Core/PathPointRenderPlugin.cs
+++ b/src/Core/PathPointRenderPlugin.cs
@@ -133,6 +133,11 @@ namespace NavisworksTransport
///
AssemblyGuideLine,
+ ///
+ /// 直线装配安装参考面样式(绿色半透明)
+ ///
+ AssemblyInstallationPlane,
+
///
/// 吊装路径样式(紫色)
///
@@ -338,6 +343,11 @@ namespace NavisworksTransport
///
public List ObjectSpaceMarkers { get; set; }
+ ///
+ /// 平面标记集合
+ ///
+ public List PlaneMarkers { get; set; }
+
///
/// 是否显示控制点可视化(用户意图)
///
@@ -358,6 +368,7 @@ namespace NavisworksTransport
PathLineMarkers = new List();
TangentMarkers = new List();
ObjectSpaceMarkers = new List();
+ PlaneMarkers = new List();
LastUpdated = DateTime.Now;
}
@@ -633,6 +644,11 @@ namespace NavisworksTransport
RenderPointMarker(graphics, pointMarker);
}
+ foreach (var planeMarker in visualization.PlaneMarkers)
+ {
+ RenderPlaneMarker(graphics, planeMarker);
+ }
+
// 渲染控制点连线(半透明)
foreach (var controlLineMarker in visualization.ControlLineMarkers)
{
@@ -802,6 +818,55 @@ namespace NavisworksTransport
}
}
+ public void RenderRectanglePlane(
+ string pathId,
+ Point3D origin,
+ Vector3D xVector,
+ Vector3D yVector,
+ RenderStyleName renderStyleName,
+ bool filled = true)
+ {
+ if (string.IsNullOrWhiteSpace(pathId))
+ {
+ throw new ArgumentException("pathId 不能为空。", nameof(pathId));
+ }
+
+ try
+ {
+ var visualization = new PathVisualization
+ {
+ PathId = pathId,
+ PathRoute = new PathRoute(pathId)
+ {
+ Id = pathId,
+ Description = $"矩形平面可视化:{pathId}"
+ }
+ };
+
+ var renderStyle = GetRenderStyle(renderStyleName);
+ visualization.PlaneMarkers.Add(new PlaneMarker
+ {
+ Origin = origin,
+ XVector = xVector,
+ YVector = yVector,
+ Color = renderStyle.Color,
+ Alpha = renderStyle.Alpha,
+ Filled = filled
+ });
+
+ lock (_lockObject)
+ {
+ _pathVisualizations[pathId] = visualization;
+ }
+
+ RequestViewRefresh();
+ }
+ catch (Exception ex)
+ {
+ LogManager.Error($"[平面渲染] 渲染矩形平面失败: {ex.Message}", ex);
+ }
+ }
+
///
/// 清除网格可视化
///
@@ -960,6 +1025,7 @@ namespace NavisworksTransport
visualization.PointMarkers.Clear();
visualization.ControlLineMarkers.Clear(); // 确保没有控制点连线
visualization.PathLineMarkers.Clear(); // 确保没有路径连线
+ visualization.PlaneMarkers.Clear();
var points = visualization.PathRoute.Points;
if (points.Count == 0) return;
@@ -2347,6 +2413,9 @@ namespace NavisworksTransport
case RenderStyleName.AssemblyGuideLine:
return new RenderStyle(Color.FromByteRGB(76, 175, 80), 0.85); // Material Green装配基准向量
+ case RenderStyleName.AssemblyInstallationPlane:
+ return new RenderStyle(Color.FromByteRGB(76, 175, 80), 0.5); // Material Green安装参考面,50%透明
+
case RenderStyleName.HoistingLine:
return new RenderStyle(Color.FromByteRGB(156, 39, 176), 0.8); // Material Purple吊装路径,20%透明
@@ -3151,6 +3220,12 @@ namespace NavisworksTransport
}
}
+ private void RenderPlaneMarker(Graphics graphics, PlaneMarker marker)
+ {
+ graphics.Color(marker.Color, marker.Alpha);
+ graphics.Rectangle(marker.Origin, marker.XVector, marker.YVector, marker.Filled);
+ }
+
///
/// 渲染连线标记(支持直线段和圆弧段)
///
@@ -3512,4 +3587,14 @@ namespace NavisworksTransport
return $"CircleMarker[序号={SequenceNumber}, 类型={PointType}, 中心=({Center.X:F2},{Center.Y:F2},{Center.Z:F2}), 半径={Radius:F2}]";
}
}
+
+ public class PlaneMarker
+ {
+ public Point3D Origin { get; set; }
+ public Vector3D XVector { get; set; }
+ public Vector3D YVector { get; set; }
+ public Color Color { get; set; }
+ public double Alpha { get; set; }
+ public bool Filled { get; set; } = true;
+ }
}
diff --git a/src/UI/WPF/ViewModels/PathEditingViewModel.cs b/src/UI/WPF/ViewModels/PathEditingViewModel.cs
index ef84caa..1335e08 100644
--- a/src/UI/WPF/ViewModels/PathEditingViewModel.cs
+++ b/src/UI/WPF/ViewModels/PathEditingViewModel.cs
@@ -141,11 +141,19 @@ namespace NavisworksTransport.UI.WPF.ViewModels
private bool _hasAssemblyTerminalObject;
private bool _isSelectingAssemblyStartPoint;
private bool _isSelectingAssemblyEndFacePoints;
+ private bool _isSelectingAssemblyInstallationPoint;
private bool _hasAssemblyEndFaceAnalysis;
+ private bool _hasAssemblyInstallationReference;
private ModelItem _assemblyTerminalObject;
private Point3D _assemblyStartPoint;
private Point3D _assemblyEndFaceCenterPoint;
private Vector3 _assemblyEndFaceNormal;
+ private Point3D _assemblyInstallationPickPoint;
+ private Point3D _assemblyInstallationBaseAnchorPoint;
+ private Point3D _assemblyInstallationAnchorPoint;
+ private Vector3 _assemblyInstallationPlaneNormal;
+ private Vector3 _assemblyInstallationPlaneSpanDirection;
+ private double _assemblyInstallationOffsetDistanceInMeters;
private readonly List _assemblyEndFaceSeedPoints = new List();
private const string AssemblyAnchorMarkerPathId = "assembly_anchor_marker";
private const string AssemblyCenterGuideLinePathId = "assembly_center_guide_line";
@@ -153,6 +161,13 @@ namespace NavisworksTransport.UI.WPF.ViewModels
private const string AssemblyEndFaceSeedPathId = "assembly_end_face_seed_points";
private const string AssemblyEndFaceCenterPathId = "assembly_end_face_center";
private const string AssemblyEndFaceNormalPathId = "assembly_end_face_normal";
+ private const string AssemblyInstallationPickPointPathId = "assembly_install_pick_point";
+ private const string AssemblyInstallationAnchorPointPathId = "assembly_install_anchor_point";
+ private const string AssemblyInstallationCenterLinePathId = "assembly_install_center_line";
+ private const string AssemblyInstallationOffsetLinePathId = "assembly_install_offset_line";
+ private const string AssemblyInstallationPlanePathId = "assembly_install_plane";
+ private const double AssemblyInstallationPlanePaddingInMeters = 0.5;
+ private const double AssemblyInstallationLineLengthScale = 1.2;
// 自动路径起点和终点的路径对象引用(用于正确的ID管理)
private PathRoute _autoPathStartPointRoute = null;
@@ -375,6 +390,10 @@ namespace NavisworksTransport.UI.WPF.ViewModels
{
if (SetProperty(ref _assemblyAnchorVerticalOffsetInMeters, value) && HasAssemblyTerminalObject)
{
+ if (_hasAssemblyInstallationReference)
+ {
+ UpdateAssemblyInstallationAnchorFromVerticalOffset();
+ }
RefreshAssemblyTerminalObjectInfo();
RenderAssemblyAnchorMarker();
RefreshAssemblyReferenceRodIfNeeded();
@@ -969,6 +988,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
public ICommand CaptureAssemblyTerminalObjectCommand { get; private set; }
public ICommand GenerateAssemblyReferenceRodCommand { get; private set; }
public ICommand SelectAssemblyStartPointCommand { get; private set; }
+ public ICommand SelectAssemblyInstallationPointCommand { get; private set; }
public ICommand ClearAssemblyReferenceRodCommand { get; private set; }
public ICommand AnalyzeAssemblyTerminalFaceCommand { get; private set; }
@@ -1007,16 +1027,27 @@ namespace NavisworksTransport.UI.WPF.ViewModels
public bool CanExecuteSaveAsPath => SelectedPathRoute != null && SelectedPathRoute.Points.Count > 0;
public bool CanGenerateAssemblyReferenceRod => HasAssemblyTerminalObject &&
+ _hasAssemblyInstallationReference &&
AssemblyReferenceRodLengthInMeters > 0 &&
AssemblyReferenceRodDiameterInMeters > 0;
public bool CanSelectAssemblyStartPoint => HasAssemblyTerminalObject &&
_pathPlanningManager != null &&
AssemblyReferencePathManager.Instance.HasReferenceLine &&
!IsSelectingAssemblyStartPoint;
+ public bool CanSelectAssemblyInstallationPoint => HasAssemblyTerminalObject &&
+ _pathPlanningManager != null &&
+ !_isSelectingAssemblyStartPoint &&
+ !_isSelectingAssemblyEndFacePoints &&
+ !_isSelectingAssemblyInstallationPoint;
public bool CanAnalyzeAssemblyTerminalFace => HasAssemblyTerminalObject &&
_pathPlanningManager != null &&
!IsSelectingAssemblyStartPoint &&
- !IsSelectingAssemblyEndFacePoints;
+ !IsSelectingAssemblyEndFacePoints &&
+ !_isSelectingAssemblyInstallationPoint;
+
+ public string AssemblyInstallationPointButtonText => _hasAssemblyInstallationReference
+ ? "重选安装点"
+ : "选安装点";
public bool IsSelectingAssemblyEndFacePoints => _isSelectingAssemblyEndFacePoints;
@@ -1301,6 +1332,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
CaptureAssemblyTerminalObjectCommand = new RelayCommand(async () => await ExecuteCaptureAssemblyTerminalObjectAsync());
GenerateAssemblyReferenceRodCommand = new RelayCommand(async () => await ExecuteGenerateAssemblyReferenceRodAsync(), () => CanGenerateAssemblyReferenceRod);
SelectAssemblyStartPointCommand = new RelayCommand(async () => await ExecuteSelectAssemblyStartPointAsync(), () => CanSelectAssemblyStartPoint);
+ SelectAssemblyInstallationPointCommand = new RelayCommand(async () => await ExecuteSelectAssemblyInstallationPointAsync(), () => CanSelectAssemblyInstallationPoint);
ClearAssemblyReferenceRodCommand = new RelayCommand(() => ExecuteClearAssemblyReferenceRod());
AnalyzeAssemblyTerminalFaceCommand = new RelayCommand(async () => await ExecuteAnalyzeAssemblyTerminalFaceAsync(), () => CanAnalyzeAssemblyTerminalFace);
}
@@ -1332,12 +1364,15 @@ namespace NavisworksTransport.UI.WPF.ViewModels
HasAssemblyTerminalObject = true;
AssemblyStartPointText = "未选择";
AssemblyTerminalObjectName = ModelItemAnalysisHelper.GetSafeDisplayName(selectedItem);
- InitializeAssemblyAnchorVerticalOffsetFromTerminalObject();
+ ResetAssemblyEndFaceAnalysisState();
+ ResetAssemblyInstallationReferenceState();
RefreshAssemblyTerminalObjectInfo();
- RenderAssemblyAnchorMarker();
+ ClearAssemblyAnchorMarker();
ClearAssemblyEndFaceAnalysisVisuals();
+ ClearAssemblyInstallationReferenceVisuals();
OnPropertyChanged(nameof(CanGenerateAssemblyReferenceRod));
OnPropertyChanged(nameof(CanSelectAssemblyStartPoint));
+ OnPropertyChanged(nameof(CanSelectAssemblyInstallationPoint));
OnPropertyChanged(nameof(CanAnalyzeAssemblyTerminalFace));
UpdateMainStatus($"已捕获终点箱体: {AssemblyTerminalObjectName}");
LogManager.Info($"[直线装配] 已捕获终点箱体: {AssemblyTerminalObjectName}");
@@ -1450,13 +1485,18 @@ namespace NavisworksTransport.UI.WPF.ViewModels
CleanupAssemblyReferenceSelection();
CleanupAssemblyEndFaceSelection(clearVisuals: false);
+ CleanupAssemblyInstallationSelection();
ClearAssemblyEndFaceAnalysisVisuals();
+ ResetAssemblyEndFaceAnalysisState();
+ ClearAssemblyInstallationReferenceVisuals();
+ ResetAssemblyInstallationReferenceState();
_assemblyEndFaceSeedPoints.Clear();
_pathPlanningManager.DisableMouseHandling();
_isSelectingAssemblyEndFacePoints = true;
OnPropertyChanged(nameof(CanAnalyzeAssemblyTerminalFace));
OnPropertyChanged(nameof(CanSelectAssemblyStartPoint));
+ OnPropertyChanged(nameof(CanSelectAssemblyInstallationPoint));
PathClickToolPlugin.MouseClicked -= OnAssemblyEndFaceMouseClicked;
PathClickToolPlugin.MouseClicked += OnAssemblyEndFaceMouseClicked;
@@ -1472,6 +1512,45 @@ namespace NavisworksTransport.UI.WPF.ViewModels
}, "分析终端端面");
}
+ private async Task ExecuteSelectAssemblyInstallationPointAsync()
+ {
+ await SafeExecuteAsync(() =>
+ {
+ if (_pathPlanningManager == null)
+ {
+ throw new InvalidOperationException("路径规划管理器未初始化,无法选择安装点。");
+ }
+
+ if (_assemblyTerminalObject == null || !ModelItemAnalysisHelper.IsModelItemValid(_assemblyTerminalObject))
+ {
+ throw new InvalidOperationException("终点箱体未设置或已失效,请先捕获终点箱体。");
+ }
+
+ CleanupAssemblyReferenceSelection();
+ CleanupAssemblyEndFaceSelection(clearVisuals: false);
+ CleanupAssemblyInstallationSelection();
+ ClearAssemblyInstallationReferenceVisuals();
+
+ _pathPlanningManager.DisableMouseHandling();
+ _isSelectingAssemblyInstallationPoint = true;
+ OnPropertyChanged(nameof(CanAnalyzeAssemblyTerminalFace));
+ OnPropertyChanged(nameof(CanSelectAssemblyStartPoint));
+ OnPropertyChanged(nameof(CanSelectAssemblyInstallationPoint));
+
+ PathClickToolPlugin.MouseClicked -= OnAssemblyInstallationMouseClicked;
+ PathClickToolPlugin.MouseClicked += OnAssemblyInstallationMouseClicked;
+
+ if (!ForceReinitializeToolPlugin(subscribeToEvents: false))
+ {
+ CleanupAssemblyInstallationSelection();
+ throw new InvalidOperationException("ToolPlugin 初始化失败,请重试。");
+ }
+
+ UpdateMainStatus("请在终点箱体表面点击安装点,系统将按该点计算安装参考面和终点锚点。");
+ LogManager.Info("[直线装配] 已进入安装点拾取模式");
+ }, "选择安装点");
+ }
+
private async void OnAssemblyReferenceMouseClicked(object sender, PickItemResult pickResult)
{
try
@@ -1544,6 +1623,35 @@ namespace NavisworksTransport.UI.WPF.ViewModels
}
}
+ private async void OnAssemblyInstallationMouseClicked(object sender, PickItemResult pickResult)
+ {
+ try
+ {
+ if (!_isSelectingAssemblyInstallationPoint || pickResult == null)
+ {
+ return;
+ }
+
+ await SafeExecuteAsync(() =>
+ {
+ if (!IsPickOnAssemblyTerminalObject(pickResult))
+ {
+ UpdateMainStatus("请点击当前终点箱体表面,不要点到其他对象。");
+ return;
+ }
+
+ BuildAndRenderAssemblyInstallationReference(pickResult.Point);
+ CleanupAssemblyInstallationSelection();
+ }, "处理安装点拾取");
+ }
+ catch (Exception ex)
+ {
+ LogManager.Error($"[直线装配] 安装点计算失败: {ex.Message}", ex);
+ UpdateMainStatus($"安装点计算失败: {ex.Message}");
+ CleanupAssemblyInstallationSelection();
+ }
+ }
+
private void CreateAssemblyLinearRoute(Point3D startPoint)
{
Point3D endPoint = AssemblyReferencePathManager.Instance.ReferenceLineEnd;
@@ -1610,11 +1718,13 @@ namespace NavisworksTransport.UI.WPF.ViewModels
ClearAssemblyCenterGuideLine();
ClearAssemblyReferenceLine();
ClearAssemblyEndFaceAnalysisVisuals();
+ ClearAssemblyInstallationReferenceVisuals();
if (resetStartPointText)
{
AssemblyStartPointText = "未选择";
}
OnPropertyChanged(nameof(CanSelectAssemblyStartPoint));
+ OnPropertyChanged(nameof(CanSelectAssemblyInstallationPoint));
OnPropertyChanged(nameof(CanAnalyzeAssemblyTerminalFace));
if (!string.IsNullOrWhiteSpace(statusMessage))
{
@@ -1634,6 +1744,11 @@ namespace NavisworksTransport.UI.WPF.ViewModels
throw new InvalidOperationException("终点箱体未设置或已失效,无法计算对接点");
}
+ if (_hasAssemblyInstallationReference && _assemblyInstallationAnchorPoint != null)
+ {
+ return _assemblyInstallationAnchorPoint;
+ }
+
HostCoordinateAdapter adapter = CoordinateSystemManager.Instance.CreateHostAdapter();
ProjectReferenceFrame projectFrame = CreateAssemblyProjectReferenceFrame(adapter);
@@ -1654,6 +1769,39 @@ namespace NavisworksTransport.UI.WPF.ViewModels
return adapter.FromCanonicalPoint(canonicalAnchorPoint);
}
+ private void UpdateAssemblyInstallationAnchorFromVerticalOffset()
+ {
+ if (!_hasAssemblyInstallationReference ||
+ _assemblyInstallationBaseAnchorPoint == null)
+ {
+ return;
+ }
+
+ double offset = UnitsConverter.ConvertFromMeters(_assemblyAnchorVerticalOffsetInMeters);
+ _assemblyInstallationAnchorPoint = new Point3D(
+ _assemblyInstallationBaseAnchorPoint.X + _assemblyInstallationPlaneNormal.X * (float)offset,
+ _assemblyInstallationBaseAnchorPoint.Y + _assemblyInstallationPlaneNormal.Y * (float)offset,
+ _assemblyInstallationBaseAnchorPoint.Z + _assemblyInstallationPlaneNormal.Z * (float)offset);
+
+ RenderAssemblyInstallationAnchorPoint(_assemblyInstallationAnchorPoint);
+ RenderAssemblyInstallationPlane(_assemblyInstallationAnchorPoint, GetCurrentAssemblyOpticalAxisDirection(), _assemblyInstallationPlaneSpanDirection);
+ }
+
+ private Vector3 GetCurrentAssemblyOpticalAxisDirection()
+ {
+ Point3D opticalAxisReferencePoint = GetAssemblyOpticalAxisReferencePoint();
+ Point3D sphereCenterPoint = new Point3D(
+ AssemblySphereCenterX,
+ AssemblySphereCenterY,
+ AssemblySphereCenterZ);
+
+ Vector3 opticalAxisDirection = new Vector3(
+ (float)(opticalAxisReferencePoint.X - sphereCenterPoint.X),
+ (float)(opticalAxisReferencePoint.Y - sphereCenterPoint.Y),
+ (float)(opticalAxisReferencePoint.Z - sphereCenterPoint.Z));
+ return Vector3.Normalize(opticalAxisDirection);
+ }
+
private Point3D GetAssemblyOpticalAxisReferencePoint()
{
if (_hasAssemblyEndFaceAnalysis && _assemblyEndFaceCenterPoint != null)
@@ -1719,18 +1867,35 @@ namespace NavisworksTransport.UI.WPF.ViewModels
}
var bounds = _assemblyTerminalObject.BoundingBox();
- Point3D anchorPoint = GetAssemblyTerminalAnchorPoint();
string anchorText = GetAssemblyAnchorText();
string mountText = AssemblyMountMode == RailMountMode.OverRail ? "轨上安装" : "轨下安装";
Point3D opticalAxisReferencePoint = GetAssemblyOpticalAxisReferencePoint();
string opticalAxisText = _hasAssemblyEndFaceAnalysis
? string.Format("端面中心=({0:F2}, {1:F2}, {2:F2})", opticalAxisReferencePoint.X, opticalAxisReferencePoint.Y, opticalAxisReferencePoint.Z)
: string.Format("箱体中心=({0:F2}, {1:F2}, {2:F2})", opticalAxisReferencePoint.X, opticalAxisReferencePoint.Y, opticalAxisReferencePoint.Z);
+ string anchorPointText = _hasAssemblyInstallationReference
+ ? string.Format("{0}点=({1:F2}, {2:F2}, {3:F2})",
+ anchorText,
+ _assemblyInstallationAnchorPoint.X,
+ _assemblyInstallationAnchorPoint.Y,
+ _assemblyInstallationAnchorPoint.Z)
+ : string.Format("{0}点=未选择", anchorText);
+ string installationText = _hasAssemblyInstallationReference
+ ? string.Format(
+ "选定安装点=({0:F2}, {1:F2}, {2:F2}),安装点=({3:F2}, {4:F2}, {5:F2}),偏距={6:F3}m",
+ _assemblyInstallationPickPoint.X,
+ _assemblyInstallationPickPoint.Y,
+ _assemblyInstallationPickPoint.Z,
+ _assemblyInstallationAnchorPoint.X,
+ _assemblyInstallationAnchorPoint.Y,
+ _assemblyInstallationAnchorPoint.Z,
+ _assemblyInstallationOffsetDistanceInMeters)
+ : "安装点=未选择";
if (referenceStartPoint != null && referenceEndPoint != null)
{
AssemblyTerminalObjectInfo = string.Format(
- "中心=({0:F2}, {1:F2}, {2:F2}),尺寸=({3:F2}, {4:F2}, {5:F2}),{6},{7}点=({8:F2}, {9:F2}, {10:F2}),{11},垂直偏移={12:F3}m,辅助线外端=({13:F2}, {14:F2}, {15:F2})",
+ "中心=({0:F2}, {1:F2}, {2:F2}),尺寸=({3:F2}, {4:F2}, {5:F2}),{6},{7},{8},{9},垂直偏移={10:F3}m,辅助线外端=({11:F2}, {12:F2}, {13:F2})",
bounds.Center.X,
bounds.Center.Y,
bounds.Center.Z,
@@ -1738,11 +1903,9 @@ namespace NavisworksTransport.UI.WPF.ViewModels
bounds.Max.Y - bounds.Min.Y,
bounds.Max.Z - bounds.Min.Z,
mountText,
- anchorText,
- referenceEndPoint.X,
- referenceEndPoint.Y,
- referenceEndPoint.Z,
+ anchorPointText,
opticalAxisText,
+ installationText,
AssemblyAnchorVerticalOffsetInMeters,
referenceStartPoint.X,
referenceStartPoint.Y,
@@ -1751,7 +1914,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
}
AssemblyTerminalObjectInfo = string.Format(
- "中心=({0:F2}, {1:F2}, {2:F2}),尺寸=({3:F2}, {4:F2}, {5:F2}),{6},{7}点=({8:F2}, {9:F2}, {10:F2}),{11},垂直偏移={12:F3}m",
+ "中心=({0:F2}, {1:F2}, {2:F2}),尺寸=({3:F2}, {4:F2}, {5:F2}),{6},{7},{8},{9},垂直偏移={10:F3}m",
bounds.Center.X,
bounds.Center.Y,
bounds.Center.Z,
@@ -1759,11 +1922,9 @@ namespace NavisworksTransport.UI.WPF.ViewModels
bounds.Max.Y - bounds.Min.Y,
bounds.Max.Z - bounds.Min.Z,
mountText,
- anchorText,
- anchorPoint.X,
- anchorPoint.Y,
- anchorPoint.Z,
+ anchorPointText,
opticalAxisText,
+ installationText,
AssemblyAnchorVerticalOffsetInMeters);
}
@@ -2015,7 +2176,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
Id = AssemblyEndFaceCenterPathId,
Description = "终端安装端面中心"
};
- AddVisualizationPoint(markerRoute, centerPoint, "端面中心", PathPointType.EndPoint);
+ AddVisualizationPoint(markerRoute, centerPoint, "端面中心", PathPointType.WayPoint);
renderPlugin.RenderPointOnly(markerRoute);
}
@@ -2053,6 +2214,158 @@ namespace NavisworksTransport.UI.WPF.ViewModels
RenderStyleName.AssemblyGuideLine);
}
+ private void BuildAndRenderAssemblyInstallationReference(Point3D pickPoint)
+ {
+ Point3D opticalAxisReferencePoint = GetAssemblyOpticalAxisReferencePoint();
+ Point3D sphereCenterPoint = new Point3D(
+ AssemblySphereCenterX,
+ AssemblySphereCenterY,
+ AssemblySphereCenterZ);
+
+ Vector3 opticalAxisDirection = new Vector3(
+ (float)(opticalAxisReferencePoint.X - sphereCenterPoint.X),
+ (float)(opticalAxisReferencePoint.Y - sphereCenterPoint.Y),
+ (float)(opticalAxisReferencePoint.Z - sphereCenterPoint.Z));
+
+ AssemblyInstallationReferenceResult result = AssemblyInstallationReferenceBuilder.Build(
+ new Vector3((float)opticalAxisReferencePoint.X, (float)opticalAxisReferencePoint.Y, (float)opticalAxisReferencePoint.Z),
+ opticalAxisDirection,
+ new Vector3((float)pickPoint.X, (float)pickPoint.Y, (float)pickPoint.Z));
+
+ _hasAssemblyInstallationReference = true;
+ _assemblyInstallationPickPoint = pickPoint;
+ _assemblyInstallationBaseAnchorPoint = AssemblyEndFaceAnalyzer.ToPoint3D(result.AnchorPoint);
+ _assemblyInstallationPlaneNormal = result.PlaneNormal;
+ _assemblyInstallationPlaneSpanDirection = result.PlaneSpanDirection;
+ _assemblyInstallationOffsetDistanceInMeters = UnitsConverter.ConvertToMeters(result.OffsetDistance);
+ _assemblyAnchorVerticalOffsetInMeters = DefaultAssemblyAnchorVerticalOffsetInMeters;
+ OnPropertyChanged(nameof(AssemblyAnchorVerticalOffsetInMeters));
+ UpdateAssemblyInstallationAnchorFromVerticalOffset();
+
+ RenderAssemblyInstallationPickPoint(_assemblyInstallationPickPoint);
+ RenderAssemblyInstallationCenterLine(AssemblyEndFaceAnalyzer.ToPoint3D(result.InstallLineBasePoint), result.OpticalAxisDirection);
+
+ RefreshAssemblyTerminalObjectInfo();
+ RefreshAssemblyReferenceRodIfNeeded();
+ UpdateMainStatus(
+ $"安装参考已计算:安装点=({_assemblyInstallationAnchorPoint.X:F2}, {_assemblyInstallationAnchorPoint.Y:F2}, {_assemblyInstallationAnchorPoint.Z:F2}),偏距={_assemblyInstallationOffsetDistanceInMeters:F3}m");
+ LogManager.Info(
+ $"[直线装配] 安装参考已计算: 选定安装点=({pickPoint.X:F3}, {pickPoint.Y:F3}, {pickPoint.Z:F3}), " +
+ $"安装点=({_assemblyInstallationAnchorPoint.X:F3}, {_assemblyInstallationAnchorPoint.Y:F3}, {_assemblyInstallationAnchorPoint.Z:F3}), " +
+ $"偏距={_assemblyInstallationOffsetDistanceInMeters:F3}m, 平面法向=({result.PlaneNormal.X:F4}, {result.PlaneNormal.Y:F4}, {result.PlaneNormal.Z:F4})");
+ OnPropertyChanged(nameof(CanGenerateAssemblyReferenceRod));
+ OnPropertyChanged(nameof(AssemblyInstallationPointButtonText));
+ }
+
+ private void RenderAssemblyInstallationPickPoint(Point3D pickPoint)
+ {
+ var renderPlugin = PathPointRenderPlugin.Instance;
+ if (renderPlugin == null)
+ {
+ return;
+ }
+
+ renderPlugin.RemovePath(AssemblyInstallationPickPointPathId);
+ var markerRoute = new PathRoute("安装点")
+ {
+ Id = AssemblyInstallationPickPointPathId,
+ Description = "终端安装点"
+ };
+ AddVisualizationPoint(markerRoute, pickPoint, "安装点", PathPointType.WayPoint);
+ renderPlugin.RenderPointOnly(markerRoute);
+ }
+
+ private void RenderAssemblyInstallationAnchorPoint(Point3D anchorPoint)
+ {
+ var renderPlugin = PathPointRenderPlugin.Instance;
+ if (renderPlugin == null)
+ {
+ return;
+ }
+
+ renderPlugin.RemovePath(AssemblyInstallationAnchorPointPathId);
+ var markerRoute = new PathRoute("安装点")
+ {
+ Id = AssemblyInstallationAnchorPointPathId,
+ Description = "终端安装点"
+ };
+ AddVisualizationPoint(markerRoute, anchorPoint, "安装点", PathPointType.WayPoint);
+ renderPlugin.RenderPointOnly(markerRoute);
+ }
+
+ private void RenderAssemblyInstallationCenterLine(Point3D lineBasePoint, Vector3 axisDirection)
+ {
+ var renderPlugin = PathPointRenderPlugin.Instance;
+ if (renderPlugin == null)
+ {
+ return;
+ }
+
+ renderPlugin.ClearRailBaseline(AssemblyInstallationCenterLinePathId);
+ double halfLength = GetAssemblyInstallationPlaneSideLength() * AssemblyInstallationLineLengthScale / 2.0;
+ Point3D startPoint = new Point3D(
+ lineBasePoint.X - axisDirection.X * (float)halfLength,
+ lineBasePoint.Y - axisDirection.Y * (float)halfLength,
+ lineBasePoint.Z - axisDirection.Z * (float)halfLength);
+ Point3D endPoint = new Point3D(
+ lineBasePoint.X + axisDirection.X * (float)halfLength,
+ lineBasePoint.Y + axisDirection.Y * (float)halfLength,
+ lineBasePoint.Z + axisDirection.Z * (float)halfLength);
+ renderPlugin.RenderRailBaseline(
+ AssemblyInstallationCenterLinePathId,
+ new List { startPoint, endPoint },
+ RenderStyleName.AssemblyGuideLine);
+ }
+
+ private void RenderAssemblyInstallationPlane(Point3D planeCenter, Vector3 axisDirection, Vector3 planeSpanDirection)
+ {
+ var renderPlugin = PathPointRenderPlugin.Instance;
+ if (renderPlugin == null)
+ {
+ return;
+ }
+
+ double sideLength = GetAssemblyInstallationPlaneSideLength();
+ double halfLength = sideLength / 2.0;
+ double thickness = UnitsConverter.ConvertFromMeters(0.01);
+
+ Vector3D xVector = new Vector3D(
+ axisDirection.X * (float)sideLength,
+ axisDirection.Y * (float)sideLength,
+ axisDirection.Z * (float)sideLength);
+ Vector3D yVector = new Vector3D(
+ planeSpanDirection.X * (float)sideLength,
+ planeSpanDirection.Y * (float)sideLength,
+ planeSpanDirection.Z * (float)sideLength);
+ Point3D origin = new Point3D(
+ planeCenter.X - axisDirection.X * (float)halfLength - planeSpanDirection.X * (float)halfLength + _assemblyInstallationPlaneNormal.X * thickness,
+ planeCenter.Y - axisDirection.Y * (float)halfLength - planeSpanDirection.Y * (float)halfLength + _assemblyInstallationPlaneNormal.Y * thickness,
+ planeCenter.Z - axisDirection.Z * (float)halfLength - planeSpanDirection.Z * (float)halfLength + _assemblyInstallationPlaneNormal.Z * thickness);
+
+ renderPlugin.RemovePath(AssemblyInstallationPlanePathId);
+ renderPlugin.RenderRectanglePlane(
+ AssemblyInstallationPlanePathId,
+ origin,
+ xVector,
+ yVector,
+ RenderStyleName.AssemblyInstallationPlane,
+ filled: true);
+ }
+
+ private double GetAssemblyInstallationPlaneSideLength()
+ {
+ if (_assemblyTerminalObject == null || !ModelItemAnalysisHelper.IsModelItemValid(_assemblyTerminalObject))
+ {
+ return UnitsConverter.ConvertFromMeters(1.0);
+ }
+
+ BoundingBox3D bounds = _assemblyTerminalObject.BoundingBox();
+ double maxSize = Math.Max(
+ bounds.Max.X - bounds.Min.X,
+ Math.Max(bounds.Max.Y - bounds.Min.Y, bounds.Max.Z - bounds.Min.Z));
+ return maxSize + UnitsConverter.ConvertFromMeters(AssemblyInstallationPlanePaddingInMeters) * 2.0;
+ }
+
private ProjectReferenceFrame CreateAssemblyProjectReferenceFrame(HostCoordinateAdapter adapter)
{
if (adapter == null)
@@ -2116,11 +2429,45 @@ namespace NavisworksTransport.UI.WPF.ViewModels
renderPlugin.RemovePath(AssemblyEndFaceSeedPathId);
renderPlugin.RemovePath(AssemblyEndFaceCenterPathId);
renderPlugin.ClearRailBaseline(AssemblyEndFaceNormalPathId);
+ }
+
+ private void ResetAssemblyEndFaceAnalysisState()
+ {
_hasAssemblyEndFaceAnalysis = false;
_assemblyEndFaceCenterPoint = null;
_assemblyEndFaceNormal = default(Vector3);
}
+ private void ClearAssemblyInstallationReferenceVisuals()
+ {
+ var renderPlugin = PathPointRenderPlugin.Instance;
+ if (renderPlugin == null)
+ {
+ return;
+ }
+
+ renderPlugin.RemovePath(AssemblyInstallationPickPointPathId);
+ renderPlugin.RemovePath(AssemblyInstallationAnchorPointPathId);
+ renderPlugin.ClearRailBaseline(AssemblyInstallationCenterLinePathId);
+ renderPlugin.ClearRailBaseline(AssemblyInstallationOffsetLinePathId);
+ renderPlugin.RemovePath(AssemblyInstallationPlanePathId);
+ }
+
+ private void ResetAssemblyInstallationReferenceState()
+ {
+ _hasAssemblyInstallationReference = false;
+ _assemblyInstallationPickPoint = null;
+ _assemblyInstallationBaseAnchorPoint = null;
+ _assemblyInstallationAnchorPoint = null;
+ _assemblyInstallationPlaneNormal = default(Vector3);
+ _assemblyInstallationPlaneSpanDirection = default(Vector3);
+ _assemblyInstallationOffsetDistanceInMeters = 0.0;
+ _assemblyAnchorVerticalOffsetInMeters = DefaultAssemblyAnchorVerticalOffsetInMeters;
+ OnPropertyChanged(nameof(AssemblyAnchorVerticalOffsetInMeters));
+ OnPropertyChanged(nameof(CanGenerateAssemblyReferenceRod));
+ OnPropertyChanged(nameof(AssemblyInstallationPointButtonText));
+ }
+
private void CleanupAssemblyReferenceSelection()
{
try
@@ -2137,6 +2484,25 @@ namespace NavisworksTransport.UI.WPF.ViewModels
}
}
+ private void CleanupAssemblyInstallationSelection()
+ {
+ try
+ {
+ PathClickToolPlugin.MouseClicked -= OnAssemblyInstallationMouseClicked;
+ _isSelectingAssemblyInstallationPoint = false;
+ _pathPlanningManager?.EnableMouseHandling();
+ _pathPlanningManager?.StopClickTool();
+ OnPropertyChanged(nameof(CanAnalyzeAssemblyTerminalFace));
+ OnPropertyChanged(nameof(CanSelectAssemblyStartPoint));
+ OnPropertyChanged(nameof(CanSelectAssemblyInstallationPoint));
+ OnPropertyChanged(nameof(AssemblyInstallationPointButtonText));
+ }
+ catch (Exception ex)
+ {
+ LogManager.Error($"[直线装配] 清理参考安装点拾取状态失败: {ex.Message}", ex);
+ }
+ }
+
private void CleanupAssemblyEndFaceSelection(bool clearVisuals)
{
try
@@ -2149,9 +2515,12 @@ namespace NavisworksTransport.UI.WPF.ViewModels
{
_assemblyEndFaceSeedPoints.Clear();
ClearAssemblyEndFaceAnalysisVisuals();
+ ResetAssemblyEndFaceAnalysisState();
}
OnPropertyChanged(nameof(CanAnalyzeAssemblyTerminalFace));
OnPropertyChanged(nameof(CanSelectAssemblyStartPoint));
+ OnPropertyChanged(nameof(CanSelectAssemblyInstallationPoint));
+ OnPropertyChanged(nameof(AssemblyInstallationPointButtonText));
}
catch (Exception ex)
{
@@ -2208,9 +2577,6 @@ namespace NavisworksTransport.UI.WPF.ViewModels
RenderAssemblyEndFaceNormal(centerPoint, result.Normal);
RefreshAssemblyTerminalObjectInfo();
RefreshAssemblyReferenceRodIfNeeded();
-
- AssemblyTerminalObjectInfo =
- $"{AssemblyTerminalObjectInfo} | 端面中心=({centerPoint.X:F2}, {centerPoint.Y:F2}, {centerPoint.Z:F2}),候选三角形={result.CandidateTriangleCount},偏差={result.MaxPlaneDeviation:F6}";
UpdateMainStatus($"端面分析完成:中心=({centerPoint.X:F2}, {centerPoint.Y:F2}, {centerPoint.Z:F2}),候选三角形={result.CandidateTriangleCount}");
LogManager.Info(
$"[直线装配] 端面分析完成: 中心=({centerPoint.X:F3}, {centerPoint.Y:F3}, {centerPoint.Z:F3}), " +
diff --git a/src/UI/WPF/Views/PathEditingView.xaml b/src/UI/WPF/Views/PathEditingView.xaml
index 0cea737..f71468e 100644
--- a/src/UI/WPF/Views/PathEditingView.xaml
+++ b/src/UI/WPF/Views/PathEditingView.xaml
@@ -263,6 +263,117 @@ NavisworksTransport 路径编辑页签视图 - 采用与动画控制和分层管
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -300,211 +411,6 @@ NavisworksTransport 路径编辑页签视图 - 采用与动画控制和分层管
Foreground="#FF2B579A"
Margin="0,5,0,0"/>
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/Utils/Assembly/AssemblyInstallationReferenceBuilder.cs b/src/Utils/Assembly/AssemblyInstallationReferenceBuilder.cs
new file mode 100644
index 0000000..2167c93
--- /dev/null
+++ b/src/Utils/Assembly/AssemblyInstallationReferenceBuilder.cs
@@ -0,0 +1,75 @@
+using System;
+using System.Numerics;
+
+namespace NavisworksTransport.Utils.GeometryAnalysis
+{
+ public sealed class AssemblyInstallationReferenceResult
+ {
+ public Vector3 OpticalAxisBasePoint { get; set; }
+ public Vector3 OpticalAxisDirection { get; set; }
+ public Vector3 PickPoint { get; set; }
+ public Vector3 PlaneNormal { get; set; }
+ public Vector3 PlaneSpanDirection { get; set; }
+ public float OffsetDistance { get; set; }
+ public Vector3 InstallLineBasePoint { get; set; }
+ public Vector3 AnchorPoint { get; set; }
+ }
+
+ public static class AssemblyInstallationReferenceBuilder
+ {
+ private const float MinimumOffsetDistance = 1e-4f;
+
+ public static AssemblyInstallationReferenceResult Build(
+ Vector3 opticalAxisBasePoint,
+ Vector3 opticalAxisDirection,
+ Vector3 pickPoint)
+ {
+ float axisLength = opticalAxisDirection.Length();
+ if (axisLength < 1e-6f)
+ {
+ throw new InvalidOperationException("光轴方向长度过小,无法计算安装参考。");
+ }
+
+ Vector3 axisDir = opticalAxisDirection / axisLength;
+ Vector3 axisProjection = ProjectPointToLine(pickPoint, opticalAxisBasePoint, axisDir);
+ Vector3 rawOffset = pickPoint - axisProjection;
+ float offsetDistance = rawOffset.Length();
+ if (offsetDistance < MinimumOffsetDistance)
+ {
+ throw new InvalidOperationException("参考安装点距离光轴过小,无法确定安装参考面,请重新选择。");
+ }
+
+ Vector3 planeNormal = rawOffset / offsetDistance;
+ Vector3 planeSpanDirection = Vector3.Cross(axisDir, planeNormal);
+ float spanLength = planeSpanDirection.Length();
+ if (spanLength < 1e-6f)
+ {
+ throw new InvalidOperationException("安装参考面横向方向计算失败,请重新选择参考安装点。");
+ }
+
+ planeSpanDirection /= spanLength;
+
+ Vector3 installLineBasePoint = opticalAxisBasePoint + planeNormal * offsetDistance;
+ Vector3 anchorPoint = ProjectPointToLine(pickPoint, installLineBasePoint, axisDir);
+
+ return new AssemblyInstallationReferenceResult
+ {
+ OpticalAxisBasePoint = opticalAxisBasePoint,
+ OpticalAxisDirection = axisDir,
+ PickPoint = pickPoint,
+ PlaneNormal = planeNormal,
+ PlaneSpanDirection = planeSpanDirection,
+ OffsetDistance = offsetDistance,
+ InstallLineBasePoint = installLineBasePoint,
+ AnchorPoint = anchorPoint
+ };
+ }
+
+ private static Vector3 ProjectPointToLine(Vector3 point, Vector3 linePoint, Vector3 lineDirection)
+ {
+ Vector3 pointVector = point - linePoint;
+ float projectionLength = Vector3.Dot(pointVector, lineDirection);
+ return linePoint + lineDirection * projectionLength;
+ }
+ }
+}
diff --git a/src/Utils/CoordinateSystem/CanonicalRailPoseBuilder.cs b/src/Utils/CoordinateSystem/CanonicalRailPoseBuilder.cs
index 405f01f..ee77355 100644
--- a/src/Utils/CoordinateSystem/CanonicalRailPoseBuilder.cs
+++ b/src/Utils/CoordinateSystem/CanonicalRailPoseBuilder.cs
@@ -104,5 +104,6 @@ namespace NavisworksTransport.Utils.CoordinateSystem
rotation = convention.CreateQuaternion(forward, up);
return true;
}
+
}
}
From e9128282b5e9738207a6c3fa5dd66b63bc53f721 Mon Sep 17 00:00:00 2001
From: tian <11429339@qq.com>
Date: Mon, 23 Mar 2026 00:11:49 +0800
Subject: [PATCH 19/87] Fix rail rotation correction semantics
---
.../CanonicalRailPoseBuilderTests.cs | 82 +++++++++++++++++++
doc/working/current-engineering-state.md | 29 ++++++-
src/Core/Animation/PathAnimationManager.cs | 2 +
.../CanonicalRailPoseBuilder.cs | 79 +++++++++++++++++-
src/Utils/RailPathPoseHelper.cs | 55 ++++++++++++-
5 files changed, 240 insertions(+), 7 deletions(-)
diff --git a/UnitTests/CoordinateSystem/CanonicalRailPoseBuilderTests.cs b/UnitTests/CoordinateSystem/CanonicalRailPoseBuilderTests.cs
index 3e2fca1..0104b64 100644
--- a/UnitTests/CoordinateSystem/CanonicalRailPoseBuilderTests.cs
+++ b/UnitTests/CoordinateSystem/CanonicalRailPoseBuilderTests.cs
@@ -1,5 +1,6 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using NavisworksTransport.Utils.CoordinateSystem;
+using System;
using System.Numerics;
namespace NavisworksTransport.UnitTests.CoordinateSystem
@@ -82,6 +83,60 @@ namespace NavisworksTransport.UnitTests.CoordinateSystem
Assert.AreEqual(0.0, Vector3.Dot(frame.Lateral, frame.Normal), 1e-6);
}
+ [TestMethod]
+ public void ZeroLocalUpRotation_ShouldMatchCurrentRailBaseline()
+ {
+ var convention = ModelAxisConvention.CreateRailAssetConvention();
+
+ Assert.IsTrue(CanonicalRailPoseBuilder.TryCreateQuaternion(
+ new Vector3(0, 0, 0),
+ new Vector3(1, 0, 0),
+ new Vector3(2, 0, 0),
+ Vector3.UnitZ,
+ convention,
+ out Quaternion baselineRotation));
+
+ Assert.IsTrue(CanonicalRailPoseBuilder.TryCreateQuaternion(
+ new Vector3(0, 0, 0),
+ new Vector3(1, 0, 0),
+ new Vector3(2, 0, 0),
+ Vector3.UnitZ,
+ convention,
+ 0.0,
+ out Quaternion correctedRotation));
+
+ AssertQuaternionEquivalent(baselineRotation, correctedRotation);
+ }
+
+ [TestMethod]
+ public void LocalUpRotation_ShouldAlignCorrectedLocalForwardWithRailTangent()
+ {
+ var convention = ModelAxisConvention.CreateRailAssetConvention();
+
+ Assert.IsTrue(CanonicalRailPoseBuilder.TryCreateQuaternion(
+ new Vector3(0, 0, 0),
+ new Vector3(1, 0, 0),
+ new Vector3(2, 0, 0),
+ Vector3.UnitZ,
+ convention,
+ 90.0,
+ out Quaternion rotation));
+
+ Quaternion localPreRotation = Quaternion.CreateFromAxisAngle(
+ Vector3.Normalize(convention.UpUnitVector),
+ (float)(90.0 * Math.PI / 180.0));
+
+ Vector3 correctedLocalForward = Vector3.Normalize(Vector3.Transform(convention.ForwardUnitVector, localPreRotation));
+ Vector3 correctedLocalUp = Vector3.Normalize(Vector3.Transform(convention.UpUnitVector, localPreRotation));
+ Matrix4x4 linear = Matrix4x4.CreateFromQuaternion(rotation);
+
+ Vector3 mappedForward = TransformLocalVector(linear, correctedLocalForward);
+ Vector3 mappedUp = TransformLocalVector(linear, correctedLocalUp);
+
+ AssertVector(mappedForward, 1, 0, 0);
+ AssertVector(mappedUp, 0, 0, 1);
+ }
+
private static void AssertColumn(Matrix4x4 matrix, int column, double x, double y, double z)
{
switch (column)
@@ -106,5 +161,32 @@ namespace NavisworksTransport.UnitTests.CoordinateSystem
break;
}
}
+
+ private static void AssertVector(Vector3 actual, double x, double y, double z)
+ {
+ Assert.AreEqual(x, actual.X, 1e-6);
+ Assert.AreEqual(y, actual.Y, 1e-6);
+ Assert.AreEqual(z, actual.Z, 1e-6);
+ }
+
+ private static void AssertQuaternionEquivalent(Quaternion expected, Quaternion actual)
+ {
+ double dot = Math.Abs(
+ expected.X * actual.X +
+ expected.Y * actual.Y +
+ expected.Z * actual.Z +
+ expected.W * actual.W);
+
+ Assert.AreEqual(1.0, dot, 1e-6);
+ }
+
+ private static Vector3 TransformLocalVector(Matrix4x4 linear, Vector3 localVector)
+ {
+ Vector3 world = new Vector3(
+ linear.M11 * localVector.X + linear.M12 * localVector.Y + linear.M13 * localVector.Z,
+ linear.M21 * localVector.X + linear.M22 * localVector.Y + linear.M23 * localVector.Z,
+ linear.M31 * localVector.X + linear.M32 * localVector.Y + linear.M33 * localVector.Z);
+ return Vector3.Normalize(world);
+ }
}
}
diff --git a/doc/working/current-engineering-state.md b/doc/working/current-engineering-state.md
index 73e6ff1..9b0e59c 100644
--- a/doc/working/current-engineering-state.md
+++ b/doc/working/current-engineering-state.md
@@ -1,6 +1,6 @@
# 当前工程状态
-更新时间:2026-03-22
+更新时间:2026-03-23
## 1. 当前稳定状态
@@ -136,8 +136,31 @@
- 那么 `YUp` 模型下聚焦就会跑到侧视图。
- 当前原则:
- 聚焦输入/输出使用宿主坐标
- - 方向语义通过 `HostCoordinateAdapter` 明确适配
- - 不要在视点辅助逻辑里直接假设宿主一定是 `ZUp`
+- 方向语义通过 `HostCoordinateAdapter` 明确适配
+- 不要在视点辅助逻辑里直接假设宿主一定是 `ZUp`
+
+### 4.7 Rail 角度修正
+
+- `Rail` 的“调整角度”和 `Ground / Hoisting` 不是同一种实现语义。
+- `Ground / Hoisting` 当前语义:
+ - 在内部 `Canonical` 坐标系里,绕统一 `CanonicalUp` 做水平角度修正。
+- `Rail` 当前正确语义:
+ - 角度修正必须先作用在构件局部轴约定上,等价于“CAD 原位局部预旋转”
+ - 然后再进入 `Rail` 的切向/法向姿态求解
+ - 不能在物体已经落到起点后,再在宿主空间对最终 pose 补一个世界轴旋转
+- 当前实现链路已经收束到:
+ - `CanonicalRailPoseBuilder`
+ - `RailPathPoseHelper`
+ - `PathAnimationManager`
+- 当前规则:
+ - `Rail 0°` 时,必须严格保持当前稳定基线,不允许改变既有 `Rail` 姿态
+ - `Rail` 有角度修正时,对齐到路径切向的是“修正后的 local forward”
+ - `Rail` 路径切向/法向框架本身不能因为角度修正被改坏
+ - `Rail` 通行空间和真实物体姿态必须共享同一套角度语义,不能一个改局部 footprint、一个改世界 pose
+- 调试优先级:
+ 1. 先验证 `0°` 基线是否保持不变
+ 2. 再验证修正后 `forward` 是否仍沿 rail 切向
+ 3. 最后再看 Navisworks 应用层是否正确按三步增量姿态落位
## 5. 当前保留的日志策略
diff --git a/src/Core/Animation/PathAnimationManager.cs b/src/Core/Animation/PathAnimationManager.cs
index 2bccc0e..6298b25 100644
--- a/src/Core/Animation/PathAnimationManager.cs
+++ b/src/Core/Animation/PathAnimationManager.cs
@@ -675,6 +675,7 @@ namespace NavisworksTransport.Core.Animation
_pathPoints[0],
nextPoint,
GetCurrentRailModelAxisConvention(),
+ _objectRotationCorrection,
out var railRotation))
{
var railLinearTransform = new Transform3D(railRotation).Linear;
@@ -1037,6 +1038,7 @@ namespace NavisworksTransport.Core.Animation
framePosition,
nextFramePoint,
GetCurrentRailModelAxisConvention(),
+ _objectRotationCorrection,
out var railRotation))
{
frame.Rotation = railRotation;
diff --git a/src/Utils/CoordinateSystem/CanonicalRailPoseBuilder.cs b/src/Utils/CoordinateSystem/CanonicalRailPoseBuilder.cs
index ee77355..e20e521 100644
--- a/src/Utils/CoordinateSystem/CanonicalRailPoseBuilder.cs
+++ b/src/Utils/CoordinateSystem/CanonicalRailPoseBuilder.cs
@@ -88,6 +88,25 @@ namespace NavisworksTransport.Utils.CoordinateSystem
Vector3 canonicalUp,
ModelAxisConvention convention,
out Quaternion rotation)
+ {
+ return TryCreateQuaternion(
+ previousPoint,
+ currentPoint,
+ nextPoint,
+ canonicalUp,
+ convention,
+ 0.0,
+ out rotation);
+ }
+
+ public static bool TryCreateQuaternion(
+ Vector3 previousPoint,
+ Vector3 currentPoint,
+ Vector3 nextPoint,
+ Vector3 canonicalUp,
+ ModelAxisConvention convention,
+ double localUpRotationDegrees,
+ out Quaternion rotation)
{
rotation = Quaternion.Identity;
@@ -101,9 +120,67 @@ namespace NavisworksTransport.Utils.CoordinateSystem
return false;
}
- rotation = convention.CreateQuaternion(forward, up);
+ if (Math.Abs(localUpRotationDegrees) < 1e-9)
+ {
+ rotation = convention.CreateQuaternion(forward, up);
+ return true;
+ }
+
+ float correctionRadians = (float)(localUpRotationDegrees * Math.PI / 180.0);
+ Quaternion localPreRotation = Quaternion.CreateFromAxisAngle(
+ Vector3.Normalize(convention.UpUnitVector),
+ correctionRadians);
+
+ Vector3 correctedLocalForward = Vector3.Normalize(Vector3.Transform(convention.ForwardUnitVector, localPreRotation));
+ Vector3 correctedLocalUp = Vector3.Normalize(Vector3.Transform(convention.UpUnitVector, localPreRotation));
+ rotation = CreateQuaternionFromLocalAxes(correctedLocalForward, correctedLocalUp, forward, up);
return true;
}
+ private static Quaternion CreateQuaternionFromLocalAxes(
+ Vector3 localForward,
+ Vector3 localUp,
+ Vector3 worldForward,
+ Vector3 worldUp)
+ {
+ Vector3 normalizedLocalForward = Vector3.Normalize(localForward);
+ Vector3 normalizedLocalUp = Vector3.Normalize(localUp);
+ Vector3 normalizedWorldForward = Vector3.Normalize(worldForward);
+ Vector3 normalizedWorldUp = Vector3.Normalize(worldUp);
+
+ Vector3 localRemaining = Vector3.Normalize(Vector3.Cross(normalizedLocalForward, normalizedLocalUp));
+ Vector3 worldRemaining = Vector3.Normalize(Vector3.Cross(normalizedWorldForward, normalizedWorldUp));
+
+ Vector3 worldX = MapOriginalLocalAxis(Vector3.UnitX, normalizedLocalForward, normalizedLocalUp, localRemaining, normalizedWorldForward, normalizedWorldUp, worldRemaining);
+ Vector3 worldY = MapOriginalLocalAxis(Vector3.UnitY, normalizedLocalForward, normalizedLocalUp, localRemaining, normalizedWorldForward, normalizedWorldUp, worldRemaining);
+ Vector3 worldZ = MapOriginalLocalAxis(Vector3.UnitZ, normalizedLocalForward, normalizedLocalUp, localRemaining, normalizedWorldForward, normalizedWorldUp, worldRemaining);
+
+ Matrix4x4 linear = new Matrix4x4(
+ worldX.X, worldY.X, worldZ.X, 0f,
+ worldX.Y, worldY.Y, worldZ.Y, 0f,
+ worldX.Z, worldY.Z, worldZ.Z, 0f,
+ 0f, 0f, 0f, 1f);
+ return Quaternion.CreateFromRotationMatrix(linear);
+ }
+
+ private static Vector3 MapOriginalLocalAxis(
+ Vector3 originalAxis,
+ Vector3 localForward,
+ Vector3 localUp,
+ Vector3 localRemaining,
+ Vector3 worldForward,
+ Vector3 worldUp,
+ Vector3 worldRemaining)
+ {
+ float forwardWeight = Vector3.Dot(originalAxis, localForward);
+ float upWeight = Vector3.Dot(originalAxis, localUp);
+ float remainingWeight = Vector3.Dot(originalAxis, localRemaining);
+
+ return Vector3.Normalize(
+ forwardWeight * worldForward +
+ upWeight * worldUp +
+ remainingWeight * worldRemaining);
+ }
+
}
}
diff --git a/src/Utils/RailPathPoseHelper.cs b/src/Utils/RailPathPoseHelper.cs
index 82b507c..2b74480 100644
--- a/src/Utils/RailPathPoseHelper.cs
+++ b/src/Utils/RailPathPoseHelper.cs
@@ -182,6 +182,23 @@ namespace NavisworksTransport.Utils
Point3D nextPoint,
ModelAxisConvention axisConvention,
out Matrix3 linearTransform)
+ {
+ return TryCreateRailLinearTransform(
+ previousPoint,
+ currentPoint,
+ nextPoint,
+ axisConvention,
+ 0.0,
+ out linearTransform);
+ }
+
+ public static bool TryCreateRailLinearTransform(
+ Point3D previousPoint,
+ Point3D currentPoint,
+ Point3D nextPoint,
+ ModelAxisConvention axisConvention,
+ double localUpRotationDegrees,
+ out Matrix3 linearTransform)
{
linearTransform = null;
@@ -208,7 +225,16 @@ namespace NavisworksTransport.Utils
var canonicalForward = frame.Forward;
var canonicalLateral = frame.Lateral;
var canonicalUp = frame.Normal;
- Quaternion canonicalRotation = axisConvention.CreateQuaternion(canonicalForward, canonicalUp);
+ Quaternion canonicalRotation = CanonicalRailPoseBuilder.TryCreateQuaternion(
+ canonicalPreviousPoint,
+ canonicalCurrentPoint,
+ canonicalNextPoint,
+ HostCoordinateAdapter.CanonicalUpVector3,
+ axisConvention,
+ localUpRotationDegrees,
+ out var correctedRotation)
+ ? correctedRotation
+ : axisConvention.CreateQuaternion(canonicalForward, canonicalUp);
Matrix4x4 canonicalLinear = Matrix4x4.CreateFromQuaternion(canonicalRotation);
var hostXAxis = Normalize(adapter.FromCanonicalVector(ToNavVector(new Vector3(canonicalLinear.M11, canonicalLinear.M21, canonicalLinear.M31))));
@@ -229,7 +255,7 @@ namespace NavisworksTransport.Utils
$"Canonical侧向=({canonicalLateral.X:F4},{canonicalLateral.Y:F4},{canonicalLateral.Z:F4}), " +
$"Canonical法向=({canonicalUp.X:F4},{canonicalUp.Y:F4},{canonicalUp.Z:F4}), " +
$"Host法向=({hostZAxis.X:F4},{hostZAxis.Y:F4},{hostZAxis.Z:F4}), " +
- $"模型Forward={axisConvention.ForwardAxis}, 模型Up={axisConvention.UpAxis}");
+ $"模型Forward={axisConvention.ForwardAxis}, 模型Up={axisConvention.UpAxis}, 本地预旋转={localUpRotationDegrees:F1}°");
return true;
}
@@ -257,11 +283,34 @@ namespace NavisworksTransport.Utils
Point3D nextPoint,
ModelAxisConvention axisConvention,
out Rotation3D rotation)
+ {
+ return TryCreateRailRotation(
+ previousPoint,
+ currentPoint,
+ nextPoint,
+ axisConvention,
+ 0.0,
+ out rotation);
+ }
+
+ public static bool TryCreateRailRotation(
+ Point3D previousPoint,
+ Point3D currentPoint,
+ Point3D nextPoint,
+ ModelAxisConvention axisConvention,
+ double localUpRotationDegrees,
+ out Rotation3D rotation)
{
rotation = Rotation3D.Identity;
LogRotationConstructorConventionOnce();
- if (!TryCreateRailLinearTransform(previousPoint, currentPoint, nextPoint, axisConvention, out var linearTransform))
+ if (!TryCreateRailLinearTransform(
+ previousPoint,
+ currentPoint,
+ nextPoint,
+ axisConvention,
+ localUpRotationDegrees,
+ out var linearTransform))
{
return false;
}
From 1cf4fe59673ec91cb22b4273a6a7da0eb75a8f44 Mon Sep 17 00:00:00 2001
From: tian <11429339@qq.com>
Date: Mon, 23 Mar 2026 12:25:00 +0800
Subject: [PATCH 20/87] Align terminal rail normals and object space rendering
---
NavisworksTransport.UnitTests.csproj | 1 +
TransportPlugin.csproj | 1 +
...semblyInstallationReferenceBuilderTests.cs | 55 +++++
.../CanonicalRailPoseBuilderTests.cs | 17 ++
.../ObjectSpaceOrientationHelperTests.cs | 40 ++++
src/Core/Animation/PathAnimationManager.cs | 2 +
src/Core/PathPlanningModels.cs | 7 +
src/Core/PathPointRenderPlugin.cs | 210 +++++-------------
src/UI/WPF/ViewModels/PathEditingViewModel.cs | 80 ++++++-
.../AssemblyInstallationReferenceBuilder.cs | 67 ++++++
.../CanonicalRailPoseBuilder.cs | 79 ++++++-
.../ObjectSpaceOrientationHelper.cs | 92 ++++++++
src/Utils/RailPathPoseHelper.cs | 90 +++++++-
13 files changed, 568 insertions(+), 173 deletions(-)
create mode 100644 UnitTests/CoordinateSystem/ObjectSpaceOrientationHelperTests.cs
create mode 100644 src/Utils/CoordinateSystem/ObjectSpaceOrientationHelper.cs
diff --git a/NavisworksTransport.UnitTests.csproj b/NavisworksTransport.UnitTests.csproj
index 6ae959b..d79b769 100644
--- a/NavisworksTransport.UnitTests.csproj
+++ b/NavisworksTransport.UnitTests.csproj
@@ -56,6 +56,7 @@
+
diff --git a/TransportPlugin.csproj b/TransportPlugin.csproj
index ce75558..af09c90 100644
--- a/TransportPlugin.csproj
+++ b/TransportPlugin.csproj
@@ -338,6 +338,7 @@
+
diff --git a/UnitTests/CoordinateSystem/AssemblyInstallationReferenceBuilderTests.cs b/UnitTests/CoordinateSystem/AssemblyInstallationReferenceBuilderTests.cs
index 55f7396..a058cdf 100644
--- a/UnitTests/CoordinateSystem/AssemblyInstallationReferenceBuilderTests.cs
+++ b/UnitTests/CoordinateSystem/AssemblyInstallationReferenceBuilderTests.cs
@@ -27,6 +27,61 @@ namespace NavisworksTransport.UnitTests.CoordinateSystem
Assert.AreEqual(0f, Vector3.Dot(result.PlaneNormal, result.PlaneSpanDirection), 1e-4f);
}
+ [TestMethod]
+ public void BuildFromTwoPoints_ShouldCreatePlaneParallelToOpticalAxisAndContainBothPoints()
+ {
+ Vector3 axisBase = new Vector3(0f, 0f, 0f);
+ Vector3 axisDirection = Vector3.UnitX;
+ Vector3 pickPoint1 = new Vector3(2f, 3f, 4f);
+ Vector3 pickPoint2 = new Vector3(6f, 3f, 6f);
+
+ AssemblyInstallationReferenceResult result =
+ AssemblyInstallationReferenceBuilder.Build(axisBase, axisDirection, pickPoint1, pickPoint2);
+
+ Assert.AreEqual(3f, result.OffsetDistance, 1e-4f);
+ Assert.AreEqual(4f, result.AnchorPoint.X, 1e-4f);
+ Assert.AreEqual(3f, result.AnchorPoint.Y, 1e-4f);
+ Assert.AreEqual(0f, result.AnchorPoint.Z, 1e-4f);
+ Assert.AreEqual(0f, Vector3.Dot(result.PlaneNormal, result.OpticalAxisDirection), 1e-4f);
+ Assert.AreEqual(0f, Vector3.Dot(result.PlaneSpanDirection, result.OpticalAxisDirection), 1e-4f);
+ Assert.AreEqual(0f, Vector3.Dot(result.PlaneNormal, result.PlaneSpanDirection), 1e-4f);
+ Assert.AreEqual(0f, Vector3.Dot(pickPoint1 - result.AnchorPoint, result.PlaneNormal), 1e-4f);
+ Assert.AreEqual(0f, Vector3.Dot(pickPoint2 - result.AnchorPoint, result.PlaneNormal), 1e-4f);
+ }
+
+ [TestMethod]
+ public void BuildFromTwoPoints_ShouldUseOpticalAxisProjectionAsInstallationCenterLine()
+ {
+ Vector3 axisBase = new Vector3(0f, 0f, 0f);
+ Vector3 axisDirection = Vector3.UnitX;
+ Vector3 pickPoint1 = new Vector3(2f, 3f, 4f);
+ Vector3 pickPoint2 = new Vector3(6f, 3f, 6f);
+
+ AssemblyInstallationReferenceResult result =
+ AssemblyInstallationReferenceBuilder.Build(axisBase, axisDirection, pickPoint1, pickPoint2);
+
+ Assert.AreEqual(0f, result.InstallLineBasePoint.X, 1e-4f);
+ Assert.AreEqual(3f, result.InstallLineBasePoint.Y, 1e-4f);
+ Assert.AreEqual(0f, result.InstallLineBasePoint.Z, 1e-4f);
+ Assert.AreEqual(4f, result.AnchorPoint.X, 1e-4f);
+ Assert.AreEqual(3f, result.AnchorPoint.Y, 1e-4f);
+ Assert.AreEqual(0f, result.AnchorPoint.Z, 1e-4f);
+ }
+
+ [TestMethod]
+ public void BuildFromTwoPoints_WhenPlanePointsAreNearlyAxisAligned_ShouldThrow()
+ {
+ Vector3 axisBase = new Vector3(0f, 0f, 0f);
+ Vector3 axisDirection = Vector3.UnitX;
+ Vector3 pickPoint1 = new Vector3(2f, 3f, 4f);
+ Vector3 pickPoint2 = new Vector3(6f, 3.00001f, 4.00001f);
+
+ InvalidOperationException ex = Assert.ThrowsException(
+ () => AssemblyInstallationReferenceBuilder.Build(axisBase, axisDirection, pickPoint1, pickPoint2));
+
+ StringAssert.Contains(ex.Message, "无法确定安装参考面");
+ }
+
[TestMethod]
public void Build_WhenPickPointTooCloseToOpticalAxis_ShouldThrow()
{
diff --git a/UnitTests/CoordinateSystem/CanonicalRailPoseBuilderTests.cs b/UnitTests/CoordinateSystem/CanonicalRailPoseBuilderTests.cs
index 0104b64..deef6f7 100644
--- a/UnitTests/CoordinateSystem/CanonicalRailPoseBuilderTests.cs
+++ b/UnitTests/CoordinateSystem/CanonicalRailPoseBuilderTests.cs
@@ -137,6 +137,23 @@ namespace NavisworksTransport.UnitTests.CoordinateSystem
AssertVector(mappedUp, 0, 0, 1);
}
+ [TestMethod]
+ public void PreferredNormal_ShouldOverrideCanonicalUpForRailFrame()
+ {
+ bool ok = CanonicalRailPoseBuilder.TryCreateLocalFrame(
+ new Vector3(0, 0, 0),
+ new Vector3(1, 0, 0),
+ new Vector3(2, 0, 0),
+ Vector3.UnitZ,
+ Vector3.UnitY,
+ out RailLocalFrame frame);
+
+ Assert.IsTrue(ok);
+ AssertVector(frame.Forward, 1, 0, 0);
+ AssertVector(frame.Normal, 0, 1, 0);
+ AssertVector(frame.Lateral, 0, 0, -1);
+ }
+
private static void AssertColumn(Matrix4x4 matrix, int column, double x, double y, double z)
{
switch (column)
diff --git a/UnitTests/CoordinateSystem/ObjectSpaceOrientationHelperTests.cs b/UnitTests/CoordinateSystem/ObjectSpaceOrientationHelperTests.cs
new file mode 100644
index 0000000..a15baf4
--- /dev/null
+++ b/UnitTests/CoordinateSystem/ObjectSpaceOrientationHelperTests.cs
@@ -0,0 +1,40 @@
+using System.Numerics;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using NavisworksTransport.Utils.CoordinateSystem;
+
+namespace NavisworksTransport.UnitTests.CoordinateSystem
+{
+ [TestClass]
+ public class ObjectSpaceOrientationHelperTests
+ {
+ [TestMethod]
+ public void CalculateAxes_ShouldHonorProvidedUpReference()
+ {
+ var segmentDirection = new Vector3(10f, 0f, 0f);
+ var upReference = new Vector3(0f, 1f, 0f);
+
+ var (_, up) = ObjectSpaceOrientationHelper.CalculateAxes(segmentDirection, upReference);
+
+ Assert.AreEqual(0.0, up.X, 1e-6);
+ Assert.AreEqual(1.0, System.Math.Abs(up.Y), 1e-6);
+ Assert.AreEqual(0.0, up.Z, 1e-6);
+ }
+
+ [TestMethod]
+ public void CalculateAxes_ForVerticalSegment_ShouldUseHorizontalDirectionWithProvidedUpReference()
+ {
+ var segmentDirection = new Vector3(0f, 10f, 0f);
+ var upReference = new Vector3(0f, 1f, 0f);
+ var horizontalDirection = new Vector3(1f, 0f, 0f);
+
+ var (right, up) = ObjectSpaceOrientationHelper.CalculateAxes(segmentDirection, upReference, horizontalDirection);
+
+ Assert.AreEqual(0.0, right.X, 1e-6);
+ Assert.AreEqual(0.0, right.Y, 1e-6);
+ Assert.AreEqual(1.0, right.Z, 1e-6);
+ Assert.AreEqual(1.0, up.X, 1e-6);
+ Assert.AreEqual(0.0, up.Y, 1e-6);
+ Assert.AreEqual(0.0, up.Z, 1e-6);
+ }
+ }
+}
diff --git a/src/Core/Animation/PathAnimationManager.cs b/src/Core/Animation/PathAnimationManager.cs
index 6298b25..e6ef4fa 100644
--- a/src/Core/Animation/PathAnimationManager.cs
+++ b/src/Core/Animation/PathAnimationManager.cs
@@ -671,6 +671,7 @@ namespace NavisworksTransport.Core.Animation
LogManager.Debug($"[移动到起点] Rail路径调整: 参考点=({_pathPoints[0].X:F2},{_pathPoints[0].Y:F2},{_pathPoints[0].Z:F2}), 物体中心=({startPosition.X:F2},{startPosition.Y:F2},{startPosition.Z:F2}), 物体高度={objectHeight:F2}, 安装={_route.RailMountMode}, 对接={(PathRoute.IsTopPayloadAnchorForMountMode(_route.RailMountMode) ? "顶面对接" : "底面对接")}");
if (RailPathPoseHelper.TryCreateRailRotation(
+ _route,
previousPoint,
_pathPoints[0],
nextPoint,
@@ -1034,6 +1035,7 @@ namespace NavisworksTransport.Core.Animation
if (_route.PathType == PathType.Rail &&
RailPathPoseHelper.TryCreateRailRotation(
+ _route,
previousFramePoint,
framePosition,
nextFramePoint,
diff --git a/src/Core/PathPlanningModels.cs b/src/Core/PathPlanningModels.cs
index e3ba6bb..1a497b6 100644
--- a/src/Core/PathPlanningModels.cs
+++ b/src/Core/PathPlanningModels.cs
@@ -737,6 +737,12 @@ namespace NavisworksTransport
///
public RailPathDefinitionMode RailPathDefinitionMode { get; set; }
+ ///
+ /// Rail 路径优先法向(宿主坐标)。
+ /// 仅在存在显式安装面/业务法向时使用;为空时退回当前默认求解。
+ ///
+ public Point3D RailPreferredNormal { get; set; }
+
// 数据库分析相关属性
///
/// 碰撞数量(从数据库加载)
@@ -777,6 +783,7 @@ namespace NavisworksTransport
IsCurved = false;
RailMountMode = RailMountMode.UnderRail;
RailPathDefinitionMode = RailPathDefinitionMode.LegacySuspensionPoint;
+ RailPreferredNormal = null;
}
///
diff --git a/src/Core/PathPointRenderPlugin.cs b/src/Core/PathPointRenderPlugin.cs
index 36b7d80..13983d2 100644
--- a/src/Core/PathPointRenderPlugin.cs
+++ b/src/Core/PathPointRenderPlugin.cs
@@ -297,6 +297,11 @@ namespace NavisworksTransport
///
public Vector3D HorizontalDirection { get; set; }
+ ///
+ /// 通行空间的优先 up 方向(例如 Rail 安装面法向)
+ ///
+ public Vector3D UpDirection { get; set; }
+
public override string ToString()
{
return $"LineMarker[{FromIndex}->{ToIndex}, 类型={SegmentType}, 起点=({StartPoint.X:F2},{StartPoint.Y:F2},{StartPoint.Z:F2}), 终点=({EndPoint.X:F2},{EndPoint.Y:F2},{EndPoint.Z:F2})]";
@@ -1658,7 +1663,8 @@ namespace NavisworksTransport
var endPoint = edge.SampledPoints.Last();
// 计算长方体的轴向量(right和up向量)
- var (right, up) = CalculateCuboidAxes(startPoint, endPoint);
+ var preferredUp = ResolveObjectSpaceUpDirection(visualization.PathRoute);
+ var (right, up) = CalculateCuboidAxes(startPoint, endPoint, preferredUp);
// 根据路径类型调整起点和终点位置
// 关键修复:垂直偏移应该沿着长方体的轴线方向(up向量)平移
@@ -1710,7 +1716,8 @@ namespace NavisworksTransport
SampledPoints = edge.SampledPoints,
Height = height, // 根据模式设置高度
Width = width, // 根据模式设置宽度
- Opacity = lineOpacity // 根据模式设置透明度
+ Opacity = lineOpacity, // 根据模式设置透明度
+ UpDirection = preferredUp
};
visualization.PathLineMarkers.Add(lineMarker);
}
@@ -1736,7 +1743,8 @@ namespace NavisworksTransport
var endPoint = edge.Trajectory.Te;
// 计算长方体的轴向量(right和up向量)
- var (right, up) = CalculateCuboidAxes(startPoint, endPoint);
+ var preferredUp = ResolveObjectSpaceUpDirection(visualization.PathRoute);
+ var (right, up) = CalculateCuboidAxes(startPoint, endPoint, preferredUp);
// 根据路径类型调整起点和终点位置
// 关键修复:垂直偏移应该沿着长方体的轴线方向(up向量)平移
@@ -1818,7 +1826,8 @@ namespace NavisworksTransport
SampledPoints = adjustedSampledPoints,
Height = height, // 根据模式设置高度
Width = width, // 根据模式设置宽度
- Opacity = arcLineOpacity // 根据模式设置透明度
+ Opacity = arcLineOpacity, // 根据模式设置透明度
+ UpDirection = preferredUp
};
visualization.PathLineMarkers.Add(arcMarker);
@@ -1838,73 +1847,13 @@ namespace NavisworksTransport
/// 路径段终点
/// 水平段方向向量(用于垂直路径渲染时确定通行空间方向)
/// 元组(right向量,up向量)
- private (Vector3D right, Vector3D up) CalculateCuboidAxes(Point3D startPoint, Point3D endPoint, Vector3D horizontalDirection = null)
+ private (Vector3D right, Vector3D up) CalculateCuboidAxes(Point3D startPoint, Point3D endPoint, Vector3D upReference = null, Vector3D horizontalDirection = null)
{
- // 计算路径段的方向向量
- var segmentDirection = new Vector3D(
- endPoint.X - startPoint.X,
- endPoint.Y - startPoint.Y,
- endPoint.Z - startPoint.Z
- );
- var segmentLength = Math.Sqrt(
- segmentDirection.X * segmentDirection.X +
- segmentDirection.Y * segmentDirection.Y +
- segmentDirection.Z * segmentDirection.Z
- );
-
- // 归一化方向向量
- var normalizedDirection = new Vector3D(
- segmentDirection.X / segmentLength,
- segmentDirection.Y / segmentLength,
- segmentDirection.Z / segmentLength
- );
-
- // 使用宿主 up 方向构造水平 right 向量,避免把世界 Z 写死到 Y-up 项目里。
- var hostUp = GetHostUpVector();
- var right = Cross(normalizedDirection, hostUp);
-
- // 如果方向向量接近宿主 up(垂直段),则使用水平段方向重新构造 right。
- double parallelToUp = Math.Abs(
- normalizedDirection.X * hostUp.X +
- normalizedDirection.Y * hostUp.Y +
- normalizedDirection.Z * hostUp.Z);
- if (parallelToUp > 0.9)
- {
- if (horizontalDirection != null)
- {
- right = Cross(horizontalDirection, hostUp);
- }
- else
- {
- right = Math.Abs(hostUp.X) < 0.9
- ? Normalize(Cross(hostUp, new Vector3D(1, 0, 0)))
- : Normalize(Cross(hostUp, new Vector3D(0, 1, 0)));
- }
- }
-
- // 归一化right向量
- var rightLength = Math.Sqrt(right.X * right.X + right.Y * right.Y + right.Z * right.Z);
- if (rightLength > 0.001)
- {
- right = new Vector3D(right.X / rightLength, right.Y / rightLength, right.Z / rightLength);
- }
-
- // 计算高度向量(垂直于方向向量和宽度向量)- 这是长方体的轴线方向
- // up = direction × right
- var up = new Vector3D(
- normalizedDirection.Y * right.Z - normalizedDirection.Z * right.Y,
- normalizedDirection.Z * right.X - normalizedDirection.X * right.Z,
- normalizedDirection.X * right.Y - normalizedDirection.Y * right.X
- );
-
- // 归一化up向量
- var upLength = Math.Sqrt(up.X * up.X + up.Y * up.Y + up.Z * up.Z);
- if (upLength > 0.001)
- {
- up = new Vector3D(up.X / upLength, up.Y / upLength, up.Z / upLength);
- }
-
- return (right, up);
+ return ObjectSpaceOrientationHelper.CalculateAxes(
+ startPoint,
+ endPoint,
+ upReference ?? GetHostUpVector(),
+ horizontalDirection);
}
///
@@ -2009,7 +1958,8 @@ namespace NavisworksTransport
CalculatePassageSpaceParameters(visualization, isVerticalSegment, isTurn90Horizontal, isFirstOrLastSegment);
// 计算长方体的轴向量(right和up向量)
- var (right, up) = CalculateCuboidAxes(startPoint.Position, endPoint.Position, horizontalDirection);
+ var preferredUp = ResolveObjectSpaceUpDirection(visualization.PathRoute);
+ var (right, up) = CalculateCuboidAxes(startPoint.Position, endPoint.Position, preferredUp, horizontalDirection);
// 根据路径类型调整起点和终点位置
// 关键修复:垂直偏移应该沿着长方体的轴线方向(up向量)平移,而不是简单地添加到Z坐标上
@@ -2099,7 +2049,8 @@ namespace NavisworksTransport
Height = height,
Width = width,
Opacity = lineOpacity,
- HorizontalDirection = horizontalDirection // 设置水平段方向向量
+ HorizontalDirection = horizontalDirection, // 设置水平段方向向量
+ UpDirection = preferredUp
};
visualization.PathLineMarkers.Add(lineMarker);
}
@@ -2797,7 +2748,7 @@ namespace NavisworksTransport
/// 宽度
/// 高度
/// 水平段方向向量(用于垂直路径渲染时确定通行空间方向)
- private void RenderCuboidMarker(Graphics graphics, Point3D startPoint, Point3D endPoint, double width, double height, Vector3D horizontalDirection = null)
+ private void RenderCuboidMarker(Graphics graphics, Point3D startPoint, Point3D endPoint, double width, double height, Vector3D horizontalDirection = null, Vector3D upReference = null)
{
try
{
@@ -2817,47 +2768,11 @@ namespace NavisworksTransport
// 归一化方向向量
direction = new Vector3D(direction.X / length, direction.Y / length, direction.Z / length);
- var hostUp = GetHostUpVector();
- var right = Cross(direction, hostUp);
-
- double parallelToUp = Math.Abs(
- direction.X * hostUp.X +
- direction.Y * hostUp.Y +
- direction.Z * hostUp.Z);
- if (parallelToUp > 0.9)
- {
- if (horizontalDirection != null)
- {
- right = Cross(horizontalDirection, hostUp);
- }
- else
- {
- right = Math.Abs(hostUp.X) < 0.9
- ? Normalize(Cross(hostUp, new Vector3D(1, 0, 0)))
- : Normalize(Cross(hostUp, new Vector3D(0, 1, 0)));
- }
- }
-
- // 归一化right向量
- var rightLength = Math.Sqrt(right.X * right.X + right.Y * right.Y + right.Z * right.Z);
- if (rightLength > 0.001)
- {
- right = new Vector3D(right.X / rightLength, right.Y / rightLength, right.Z / rightLength);
- }
-
- // 计算高度向量(垂直于方向向量和宽度向量)
- var up = new Vector3D(
- direction.Y * right.Z - direction.Z * right.Y,
- direction.Z * right.X - direction.X * right.Z,
- direction.X * right.Y - direction.Y * right.X
- );
-
- // 归一化up向量
- var upLength = Math.Sqrt(up.X * up.X + up.Y * up.Y + up.Z * up.Z);
- if (upLength > 0.001)
- {
- up = new Vector3D(up.X / upLength, up.Y / upLength, up.Z / upLength);
- }
+ var (right, up) = ObjectSpaceOrientationHelper.CalculateAxes(
+ startPoint,
+ endPoint,
+ upReference ?? GetHostUpVector(),
+ horizontalDirection);
// 计算长方体的边界框和向量
var halfWidth = width / 2.0;
@@ -2894,7 +2809,7 @@ namespace NavisworksTransport
/// 高度
/// 指定长度(覆盖默认计算值)
/// 水平段方向向量(用于垂直路径渲染时确定通行空间方向)
- private void RenderCuboidMarkerWithLength(Graphics graphics, Point3D startPoint, Point3D endPoint, double width, double height, double length, Vector3D horizontalDirection = null)
+ private void RenderCuboidMarkerWithLength(Graphics graphics, Point3D startPoint, Point3D endPoint, double width, double height, double length, Vector3D horizontalDirection = null, Vector3D upReference = null)
{
try
{
@@ -2912,47 +2827,11 @@ namespace NavisworksTransport
direction = new Vector3D(direction.X / directionLength, direction.Y / directionLength, direction.Z / directionLength);
- var hostUp = GetHostUpVector();
- var right = Cross(direction, hostUp);
-
- double parallelToUp = Math.Abs(
- direction.X * hostUp.X +
- direction.Y * hostUp.Y +
- direction.Z * hostUp.Z);
- if (parallelToUp > 0.9)
- {
- if (horizontalDirection != null)
- {
- right = Cross(horizontalDirection, hostUp);
- }
- else
- {
- right = Math.Abs(hostUp.X) < 0.9
- ? Normalize(Cross(hostUp, new Vector3D(1, 0, 0)))
- : Normalize(Cross(hostUp, new Vector3D(0, 1, 0)));
- }
- }
-
- // 归一化right向量
- var rightLength = Math.Sqrt(right.X * right.X + right.Y * right.Y + right.Z * right.Z);
- if (rightLength > 0.001)
- {
- right = new Vector3D(right.X / rightLength, right.Y / rightLength, right.Z / rightLength);
- }
-
- // 计算高度向量(垂直于方向向量和宽度向量)
- var up = new Vector3D(
- direction.Y * right.Z - direction.Z * right.Y,
- direction.Z * right.X - direction.X * right.Z,
- direction.X * right.Y - direction.Y * right.X
- );
-
- // 归一化up向量
- var upLength = Math.Sqrt(up.X * up.X + up.Y * up.Y + up.Z * up.Z);
- if (upLength > 0.001)
- {
- up = new Vector3D(up.X / upLength, up.Y / upLength, up.Z / upLength);
- }
+ var (right, up) = ObjectSpaceOrientationHelper.CalculateAxes(
+ startPoint,
+ endPoint,
+ upReference ?? GetHostUpVector(),
+ horizontalDirection);
// 计算长方体的边界框和向量
var halfWidth = width / 2.0;
@@ -3009,7 +2888,8 @@ namespace NavisworksTransport
lineMarker.EndPoint,
width,
lineMarker.Height,
- lineMarker.HorizontalDirection
+ lineMarker.HorizontalDirection,
+ lineMarker.UpDirection
);
}
}
@@ -3052,11 +2932,25 @@ namespace NavisworksTransport
width,
lineMarker.Height,
_passageAlongPath,
- lineMarker.HorizontalDirection
+ lineMarker.HorizontalDirection,
+ lineMarker.UpDirection
);
}
}
+ private Vector3D ResolveObjectSpaceUpDirection(PathRoute route)
+ {
+ if (route?.PathType == NavisworksTransport.PathType.Rail && route.RailPreferredNormal != null)
+ {
+ return Normalize(new Vector3D(
+ route.RailPreferredNormal.X,
+ route.RailPreferredNormal.Y,
+ route.RailPreferredNormal.Z));
+ }
+
+ return GetHostUpVector();
+ }
+
///
/// 计算采样点处的切线方向
///
diff --git a/src/UI/WPF/ViewModels/PathEditingViewModel.cs b/src/UI/WPF/ViewModels/PathEditingViewModel.cs
index 1335e08..05e2cd6 100644
--- a/src/UI/WPF/ViewModels/PathEditingViewModel.cs
+++ b/src/UI/WPF/ViewModels/PathEditingViewModel.cs
@@ -154,6 +154,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
private Vector3 _assemblyInstallationPlaneNormal;
private Vector3 _assemblyInstallationPlaneSpanDirection;
private double _assemblyInstallationOffsetDistanceInMeters;
+ private readonly List _assemblyInstallationSeedPoints = new List();
private readonly List _assemblyEndFaceSeedPoints = new List();
private const string AssemblyAnchorMarkerPathId = "assembly_anchor_marker";
private const string AssemblyCenterGuideLinePathId = "assembly_center_guide_line";
@@ -1347,6 +1348,8 @@ namespace NavisworksTransport.UI.WPF.ViewModels
{
await SafeExecuteAsync(() =>
{
+ ClearNonGridPathVisualizations("[直线装配] 捕获箱体");
+
var document = NavisApplication.ActiveDocument;
var selectedItem = document?.CurrentSelection?.SelectedItems?.FirstOrDefault();
if (selectedItem == null)
@@ -1379,6 +1382,30 @@ namespace NavisworksTransport.UI.WPF.ViewModels
}, "捕获终点箱体");
}
+ private void ClearNonGridPathVisualizations(string context)
+ {
+ if (PathPointRenderPlugin.Instance == null)
+ {
+ return;
+ }
+
+ try
+ {
+ PathPointRenderPlugin.Instance.ClearPathsExcept(
+ "grid_visualization_all",
+ "grid_visualization_channel",
+ "grid_visualization_unknown",
+ "grid_visualization_obstacle",
+ "grid_visualization_door");
+ LogManager.Info($"{context}:已清除现有路径可视化显示(保留网格可视化)");
+ }
+ catch (Exception ex)
+ {
+ LogManager.Error($"{context}:清除现有路径可视化失败: {ex.Message}", ex);
+ throw;
+ }
+ }
+
private async Task ExecuteGenerateAssemblyReferenceRodAsync()
{
await SafeExecuteAsync(() =>
@@ -1546,7 +1573,8 @@ namespace NavisworksTransport.UI.WPF.ViewModels
throw new InvalidOperationException("ToolPlugin 初始化失败,请重试。");
}
- UpdateMainStatus("请在终点箱体表面点击安装点,系统将按该点计算安装参考面和终点锚点。");
+ _assemblyInstallationSeedPoints.Clear();
+ UpdateMainStatus("请在终点箱体表面连续点击两个安装面点,系统将按两点和光轴计算安装参考面与安装点。");
LogManager.Info("[直线装配] 已进入安装点拾取模式");
}, "选择安装点");
}
@@ -1640,7 +1668,21 @@ namespace NavisworksTransport.UI.WPF.ViewModels
return;
}
- BuildAndRenderAssemblyInstallationReference(pickResult.Point);
+ _assemblyInstallationSeedPoints.Add(pickResult.Point);
+ RenderAssemblyInstallationPickPoints(_assemblyInstallationSeedPoints);
+
+ int pickedCount = _assemblyInstallationSeedPoints.Count;
+ LogManager.Info($"[直线装配] 已记录安装面点 {pickedCount}: ({pickResult.Point.X:F3}, {pickResult.Point.Y:F3}, {pickResult.Point.Z:F3})");
+
+ if (pickedCount < 2)
+ {
+ UpdateMainStatus("已记录安装面点 1/2,请继续在同一安装面上点击第二个点。");
+ return;
+ }
+
+ BuildAndRenderAssemblyInstallationReference(
+ _assemblyInstallationSeedPoints[0],
+ _assemblyInstallationSeedPoints[1]);
CleanupAssemblyInstallationSelection();
}, "处理安装点拾取");
}
@@ -1665,6 +1707,14 @@ namespace NavisworksTransport.UI.WPF.ViewModels
RailPathDefinitionMode = RailPathDefinitionMode.RailCenterLine
};
+ if (_hasAssemblyInstallationReference)
+ {
+ route.RailPreferredNormal = new Point3D(
+ _assemblyInstallationPlaneNormal.X,
+ _assemblyInstallationPlaneNormal.Y,
+ _assemblyInstallationPlaneNormal.Z);
+ }
+
route.AddPoint(new PathPoint(startPoint, "装配起点", PathPointType.StartPoint));
route.AddPoint(new PathPoint(endPoint, "装配终点", PathPointType.EndPoint));
@@ -1706,7 +1756,9 @@ namespace NavisworksTransport.UI.WPF.ViewModels
GetAssemblyAnchorText());
OnPropertyChanged(nameof(PathRoutes));
- LogManager.Info($"[直线装配] 已生成并完成路径: {route.Name},起点=({startPoint.X:F2}, {startPoint.Y:F2}, {startPoint.Z:F2}),终点=({endPoint.X:F2}, {endPoint.Y:F2}, {endPoint.Z:F2})");
+ LogManager.Info(
+ $"[直线装配] 已生成并完成路径: {route.Name},起点=({startPoint.X:F2}, {startPoint.Y:F2}, {startPoint.Z:F2}),终点=({endPoint.X:F2}, {endPoint.Y:F2}, {endPoint.Z:F2})" +
+ $"{(route.RailPreferredNormal != null ? $",安装法向=({route.RailPreferredNormal.X:F3}, {route.RailPreferredNormal.Y:F3}, {route.RailPreferredNormal.Z:F3})" : string.Empty)}");
}
private void HideAssemblyReferenceVisuals(bool resetStartPointText, string statusMessage, string logMessage)
@@ -1882,7 +1934,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
: string.Format("{0}点=未选择", anchorText);
string installationText = _hasAssemblyInstallationReference
? string.Format(
- "选定安装点=({0:F2}, {1:F2}, {2:F2}),安装点=({3:F2}, {4:F2}, {5:F2}),偏距={6:F3}m",
+ "安装面点中点=({0:F2}, {1:F2}, {2:F2}),安装点=({3:F2}, {4:F2}, {5:F2}),偏距={6:F3}m",
_assemblyInstallationPickPoint.X,
_assemblyInstallationPickPoint.Y,
_assemblyInstallationPickPoint.Z,
@@ -2214,7 +2266,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
RenderStyleName.AssemblyGuideLine);
}
- private void BuildAndRenderAssemblyInstallationReference(Point3D pickPoint)
+ private void BuildAndRenderAssemblyInstallationReference(Point3D firstPickPoint, Point3D secondPickPoint)
{
Point3D opticalAxisReferencePoint = GetAssemblyOpticalAxisReferencePoint();
Point3D sphereCenterPoint = new Point3D(
@@ -2230,10 +2282,11 @@ namespace NavisworksTransport.UI.WPF.ViewModels
AssemblyInstallationReferenceResult result = AssemblyInstallationReferenceBuilder.Build(
new Vector3((float)opticalAxisReferencePoint.X, (float)opticalAxisReferencePoint.Y, (float)opticalAxisReferencePoint.Z),
opticalAxisDirection,
- new Vector3((float)pickPoint.X, (float)pickPoint.Y, (float)pickPoint.Z));
+ new Vector3((float)firstPickPoint.X, (float)firstPickPoint.Y, (float)firstPickPoint.Z),
+ new Vector3((float)secondPickPoint.X, (float)secondPickPoint.Y, (float)secondPickPoint.Z));
_hasAssemblyInstallationReference = true;
- _assemblyInstallationPickPoint = pickPoint;
+ _assemblyInstallationPickPoint = AssemblyEndFaceAnalyzer.ToPoint3D((result.PickPoint + result.SecondaryPickPoint) * 0.5f);
_assemblyInstallationBaseAnchorPoint = AssemblyEndFaceAnalyzer.ToPoint3D(result.AnchorPoint);
_assemblyInstallationPlaneNormal = result.PlaneNormal;
_assemblyInstallationPlaneSpanDirection = result.PlaneSpanDirection;
@@ -2242,7 +2295,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
OnPropertyChanged(nameof(AssemblyAnchorVerticalOffsetInMeters));
UpdateAssemblyInstallationAnchorFromVerticalOffset();
- RenderAssemblyInstallationPickPoint(_assemblyInstallationPickPoint);
+ RenderAssemblyInstallationPickPoints(_assemblyInstallationSeedPoints);
RenderAssemblyInstallationCenterLine(AssemblyEndFaceAnalyzer.ToPoint3D(result.InstallLineBasePoint), result.OpticalAxisDirection);
RefreshAssemblyTerminalObjectInfo();
@@ -2250,14 +2303,15 @@ namespace NavisworksTransport.UI.WPF.ViewModels
UpdateMainStatus(
$"安装参考已计算:安装点=({_assemblyInstallationAnchorPoint.X:F2}, {_assemblyInstallationAnchorPoint.Y:F2}, {_assemblyInstallationAnchorPoint.Z:F2}),偏距={_assemblyInstallationOffsetDistanceInMeters:F3}m");
LogManager.Info(
- $"[直线装配] 安装参考已计算: 选定安装点=({pickPoint.X:F3}, {pickPoint.Y:F3}, {pickPoint.Z:F3}), " +
+ $"[直线装配] 安装参考已计算: 安装面点1=({firstPickPoint.X:F3}, {firstPickPoint.Y:F3}, {firstPickPoint.Z:F3}), " +
+ $"安装面点2=({secondPickPoint.X:F3}, {secondPickPoint.Y:F3}, {secondPickPoint.Z:F3}), " +
$"安装点=({_assemblyInstallationAnchorPoint.X:F3}, {_assemblyInstallationAnchorPoint.Y:F3}, {_assemblyInstallationAnchorPoint.Z:F3}), " +
$"偏距={_assemblyInstallationOffsetDistanceInMeters:F3}m, 平面法向=({result.PlaneNormal.X:F4}, {result.PlaneNormal.Y:F4}, {result.PlaneNormal.Z:F4})");
OnPropertyChanged(nameof(CanGenerateAssemblyReferenceRod));
OnPropertyChanged(nameof(AssemblyInstallationPointButtonText));
}
- private void RenderAssemblyInstallationPickPoint(Point3D pickPoint)
+ private void RenderAssemblyInstallationPickPoints(IReadOnlyList pickPoints)
{
var renderPlugin = PathPointRenderPlugin.Instance;
if (renderPlugin == null)
@@ -2271,7 +2325,10 @@ namespace NavisworksTransport.UI.WPF.ViewModels
Id = AssemblyInstallationPickPointPathId,
Description = "终端安装点"
};
- AddVisualizationPoint(markerRoute, pickPoint, "安装点", PathPointType.WayPoint);
+ for (int i = 0; i < pickPoints.Count; i++)
+ {
+ AddVisualizationPoint(markerRoute, pickPoints[i], $"安装面点{i + 1}", PathPointType.WayPoint);
+ }
renderPlugin.RenderPointOnly(markerRoute);
}
@@ -2462,6 +2519,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
_assemblyInstallationPlaneNormal = default(Vector3);
_assemblyInstallationPlaneSpanDirection = default(Vector3);
_assemblyInstallationOffsetDistanceInMeters = 0.0;
+ _assemblyInstallationSeedPoints.Clear();
_assemblyAnchorVerticalOffsetInMeters = DefaultAssemblyAnchorVerticalOffsetInMeters;
OnPropertyChanged(nameof(AssemblyAnchorVerticalOffsetInMeters));
OnPropertyChanged(nameof(CanGenerateAssemblyReferenceRod));
diff --git a/src/Utils/Assembly/AssemblyInstallationReferenceBuilder.cs b/src/Utils/Assembly/AssemblyInstallationReferenceBuilder.cs
index 2167c93..16f01eb 100644
--- a/src/Utils/Assembly/AssemblyInstallationReferenceBuilder.cs
+++ b/src/Utils/Assembly/AssemblyInstallationReferenceBuilder.cs
@@ -8,6 +8,7 @@ namespace NavisworksTransport.Utils.GeometryAnalysis
public Vector3 OpticalAxisBasePoint { get; set; }
public Vector3 OpticalAxisDirection { get; set; }
public Vector3 PickPoint { get; set; }
+ public Vector3 SecondaryPickPoint { get; set; }
public Vector3 PlaneNormal { get; set; }
public Vector3 PlaneSpanDirection { get; set; }
public float OffsetDistance { get; set; }
@@ -57,6 +58,72 @@ namespace NavisworksTransport.Utils.GeometryAnalysis
OpticalAxisBasePoint = opticalAxisBasePoint,
OpticalAxisDirection = axisDir,
PickPoint = pickPoint,
+ SecondaryPickPoint = pickPoint,
+ PlaneNormal = planeNormal,
+ PlaneSpanDirection = planeSpanDirection,
+ OffsetDistance = offsetDistance,
+ InstallLineBasePoint = installLineBasePoint,
+ AnchorPoint = anchorPoint
+ };
+ }
+
+ public static AssemblyInstallationReferenceResult Build(
+ Vector3 opticalAxisBasePoint,
+ Vector3 opticalAxisDirection,
+ Vector3 firstPickPoint,
+ Vector3 secondPickPoint)
+ {
+ float axisLength = opticalAxisDirection.Length();
+ if (axisLength < 1e-6f)
+ {
+ throw new InvalidOperationException("光轴方向长度过小,无法计算安装参考。");
+ }
+
+ Vector3 axisDir = opticalAxisDirection / axisLength;
+ Vector3 pickSegment = secondPickPoint - firstPickPoint;
+ Vector3 axisAlignedComponent = axisDir * Vector3.Dot(pickSegment, axisDir);
+ Vector3 planeSpanDirection = pickSegment - axisAlignedComponent;
+ float spanLength = planeSpanDirection.Length();
+ if (spanLength < MinimumOffsetDistance)
+ {
+ throw new InvalidOperationException("两次安装点拾取几乎与光轴平行,无法确定安装参考面,请重新选择。");
+ }
+
+ planeSpanDirection /= spanLength;
+
+ Vector3 midpoint = (firstPickPoint + secondPickPoint) * 0.5f;
+ Vector3 planeNormal = Vector3.Cross(axisDir, planeSpanDirection);
+ float normalLength = planeNormal.Length();
+ if (normalLength < 1e-6f)
+ {
+ throw new InvalidOperationException("安装参考面法向计算失败,请重新选择。");
+ }
+
+ planeNormal /= normalLength;
+
+ Vector3 midpointOffset = midpoint - opticalAxisBasePoint;
+ float signedOffset = Vector3.Dot(midpointOffset, planeNormal);
+ if (Math.Abs(signedOffset) < MinimumOffsetDistance)
+ {
+ throw new InvalidOperationException("安装面中点距离光轴过小,无法确定安装参考面,请重新选择。");
+ }
+
+ if (signedOffset < 0f)
+ {
+ planeNormal = -planeNormal;
+ signedOffset = -signedOffset;
+ }
+
+ float offsetDistance = signedOffset;
+ Vector3 installLineBasePoint = opticalAxisBasePoint + planeNormal * offsetDistance;
+ Vector3 anchorPoint = ProjectPointToLine(midpoint, installLineBasePoint, axisDir);
+
+ return new AssemblyInstallationReferenceResult
+ {
+ OpticalAxisBasePoint = opticalAxisBasePoint,
+ OpticalAxisDirection = axisDir,
+ PickPoint = firstPickPoint,
+ SecondaryPickPoint = secondPickPoint,
PlaneNormal = planeNormal,
PlaneSpanDirection = planeSpanDirection,
OffsetDistance = offsetDistance,
diff --git a/src/Utils/CoordinateSystem/CanonicalRailPoseBuilder.cs b/src/Utils/CoordinateSystem/CanonicalRailPoseBuilder.cs
index e20e521..f1d5faa 100644
--- a/src/Utils/CoordinateSystem/CanonicalRailPoseBuilder.cs
+++ b/src/Utils/CoordinateSystem/CanonicalRailPoseBuilder.cs
@@ -17,6 +17,23 @@ namespace NavisworksTransport.Utils.CoordinateSystem
Vector3 nextPoint,
Vector3 canonicalUp,
out RailLocalFrame frame)
+ {
+ return TryCreateLocalFrame(
+ previousPoint,
+ currentPoint,
+ nextPoint,
+ canonicalUp,
+ null,
+ out frame);
+ }
+
+ public static bool TryCreateLocalFrame(
+ Vector3 previousPoint,
+ Vector3 currentPoint,
+ Vector3 nextPoint,
+ Vector3 canonicalUp,
+ Vector3? preferredNormal,
+ out RailLocalFrame frame)
{
frame = null;
@@ -25,6 +42,7 @@ namespace NavisworksTransport.Utils.CoordinateSystem
currentPoint,
nextPoint,
canonicalUp,
+ preferredNormal,
out var forward,
out var lateral,
out var up))
@@ -44,6 +62,27 @@ namespace NavisworksTransport.Utils.CoordinateSystem
out Vector3 forward,
out Vector3 lateral,
out Vector3 up)
+ {
+ return TryCreateBasis(
+ previousPoint,
+ currentPoint,
+ nextPoint,
+ canonicalUp,
+ null,
+ out forward,
+ out lateral,
+ out up);
+ }
+
+ public static bool TryCreateBasis(
+ Vector3 previousPoint,
+ Vector3 currentPoint,
+ Vector3 nextPoint,
+ Vector3 canonicalUp,
+ Vector3? preferredNormal,
+ out Vector3 forward,
+ out Vector3 lateral,
+ out Vector3 up)
{
forward = default;
lateral = default;
@@ -61,7 +100,13 @@ namespace NavisworksTransport.Utils.CoordinateSystem
}
forward = Vector3.Normalize(tangent);
- Vector3 projectedUp = canonicalUp - Vector3.Dot(canonicalUp, forward) * forward;
+ Vector3 referenceUp = canonicalUp;
+ if (preferredNormal.HasValue && preferredNormal.Value.LengthSquared() >= TangentEpsilon)
+ {
+ referenceUp = Vector3.Normalize(preferredNormal.Value);
+ }
+
+ Vector3 projectedUp = referenceUp - Vector3.Dot(referenceUp, forward) * forward;
if (projectedUp.LengthSquared() < TangentEpsilon)
{
projectedUp = Math.Abs(forward.Z) < 0.9f
@@ -95,6 +140,7 @@ namespace NavisworksTransport.Utils.CoordinateSystem
nextPoint,
canonicalUp,
convention,
+ null,
0.0,
out rotation);
}
@@ -107,6 +153,27 @@ namespace NavisworksTransport.Utils.CoordinateSystem
ModelAxisConvention convention,
double localUpRotationDegrees,
out Quaternion rotation)
+ {
+ return TryCreateQuaternion(
+ previousPoint,
+ currentPoint,
+ nextPoint,
+ canonicalUp,
+ convention,
+ null,
+ localUpRotationDegrees,
+ out rotation);
+ }
+
+ public static bool TryCreateQuaternion(
+ Vector3 previousPoint,
+ Vector3 currentPoint,
+ Vector3 nextPoint,
+ Vector3 canonicalUp,
+ ModelAxisConvention convention,
+ Vector3? preferredNormal,
+ double localUpRotationDegrees,
+ out Quaternion rotation)
{
rotation = Quaternion.Identity;
@@ -115,7 +182,15 @@ namespace NavisworksTransport.Utils.CoordinateSystem
throw new ArgumentNullException(nameof(convention));
}
- if (!TryCreateBasis(previousPoint, currentPoint, nextPoint, canonicalUp, out var forward, out _, out var up))
+ if (!TryCreateBasis(
+ previousPoint,
+ currentPoint,
+ nextPoint,
+ canonicalUp,
+ preferredNormal,
+ out var forward,
+ out _,
+ out var up))
{
return false;
}
diff --git a/src/Utils/CoordinateSystem/ObjectSpaceOrientationHelper.cs b/src/Utils/CoordinateSystem/ObjectSpaceOrientationHelper.cs
new file mode 100644
index 0000000..65d9e2d
--- /dev/null
+++ b/src/Utils/CoordinateSystem/ObjectSpaceOrientationHelper.cs
@@ -0,0 +1,92 @@
+using System;
+using System.Numerics;
+using Autodesk.Navisworks.Api;
+
+namespace NavisworksTransport.Utils.CoordinateSystem
+{
+ public static class ObjectSpaceOrientationHelper
+ {
+ public static (Vector3D right, Vector3D up) CalculateAxes(
+ Point3D startPoint,
+ Point3D endPoint,
+ Vector3D upReference,
+ Vector3D horizontalDirection = null)
+ {
+ var segmentDirection = new Vector3(
+ (float)(endPoint.X - startPoint.X),
+ (float)(endPoint.Y - startPoint.Y),
+ (float)(endPoint.Z - startPoint.Z));
+
+ var (right, up) = CalculateAxes(
+ segmentDirection,
+ ToNumerics(upReference),
+ horizontalDirection == null ? (Vector3?)null : ToNumerics(horizontalDirection));
+
+ return (ToNavisworks(right), ToNavisworks(up));
+ }
+
+ public static (Vector3 right, Vector3 up) CalculateAxes(
+ Vector3 segmentDirection,
+ Vector3 upReference,
+ Vector3? horizontalDirection = null)
+ {
+ double segmentLength = Math.Sqrt(
+ segmentDirection.X * segmentDirection.X +
+ segmentDirection.Y * segmentDirection.Y +
+ segmentDirection.Z * segmentDirection.Z);
+
+ if (segmentLength < 1e-9)
+ {
+ throw new InvalidOperationException("路径段长度过小,无法计算通行空间方向。");
+ }
+
+ var normalizedDirection = Vector3.Normalize(segmentDirection);
+ var normalizedUpReference = Normalize(upReference);
+ var right = Cross(normalizedDirection, normalizedUpReference);
+
+ double parallelToUp = Math.Abs(Vector3.Dot(normalizedDirection, normalizedUpReference));
+ if (parallelToUp > 0.9)
+ {
+ if (horizontalDirection.HasValue)
+ {
+ right = Cross(horizontalDirection.Value, normalizedUpReference);
+ }
+ else
+ {
+ right = Math.Abs(normalizedUpReference.X) < 0.9f
+ ? Normalize(Cross(normalizedUpReference, new Vector3(1f, 0f, 0f)))
+ : Normalize(Cross(normalizedUpReference, new Vector3(0f, 1f, 0f)));
+ }
+ }
+
+ right = Normalize(right);
+ var up = Normalize(Cross(normalizedDirection, right));
+ return (right, up);
+ }
+
+ private static Vector3 Cross(Vector3 a, Vector3 b)
+ {
+ return Vector3.Cross(a, b);
+ }
+
+ private static Vector3 Normalize(Vector3 vector)
+ {
+ if (vector.LengthSquared() < 1e-9f)
+ {
+ return Vector3.Zero;
+ }
+
+ return Vector3.Normalize(vector);
+ }
+
+ private static Vector3 ToNumerics(Vector3D vector)
+ {
+ return new Vector3((float)vector.X, (float)vector.Y, (float)vector.Z);
+ }
+
+ private static Vector3D ToNavisworks(Vector3 vector)
+ {
+ return new Vector3D(vector.X, vector.Y, vector.Z);
+ }
+ }
+}
diff --git a/src/Utils/RailPathPoseHelper.cs b/src/Utils/RailPathPoseHelper.cs
index 2b74480..8fd306a 100644
--- a/src/Utils/RailPathPoseHelper.cs
+++ b/src/Utils/RailPathPoseHelper.cs
@@ -135,7 +135,7 @@ namespace NavisworksTransport.Utils
var adapterForRail = CoordinateSystemManager.Instance.CreateHostAdapter();
var canonicalReferencePointForRail = adapterForRail.ToCanonicalPoint(referencePoint);
- if (!TryCreateCanonicalLocalFrame(previousPoint, referencePoint, nextPoint, out RailLocalFrame frame))
+ if (!TryCreateCanonicalLocalFrame(route, previousPoint, referencePoint, nextPoint, out RailLocalFrame frame))
{
var canonicalCenterPointForRailFallback = new Point3D(
canonicalReferencePointForRail.X,
@@ -165,6 +165,22 @@ namespace NavisworksTransport.Utils
out Matrix3 linearTransform)
{
return TryCreateRailLinearTransform(
+ null,
+ previousPoint,
+ currentPoint,
+ nextPoint,
+ out linearTransform);
+ }
+
+ public static bool TryCreateRailLinearTransform(
+ PathRoute route,
+ Point3D previousPoint,
+ Point3D currentPoint,
+ Point3D nextPoint,
+ out Matrix3 linearTransform)
+ {
+ return TryCreateRailLinearTransform(
+ route,
previousPoint,
currentPoint,
nextPoint,
@@ -177,6 +193,7 @@ namespace NavisworksTransport.Utils
/// 通过模型局部轴约定显式指定本地哪个轴代表 forward/up。
///
public static bool TryCreateRailLinearTransform(
+ PathRoute route,
Point3D previousPoint,
Point3D currentPoint,
Point3D nextPoint,
@@ -184,6 +201,7 @@ namespace NavisworksTransport.Utils
out Matrix3 linearTransform)
{
return TryCreateRailLinearTransform(
+ route,
previousPoint,
currentPoint,
nextPoint,
@@ -193,6 +211,7 @@ namespace NavisworksTransport.Utils
}
public static bool TryCreateRailLinearTransform(
+ PathRoute route,
Point3D previousPoint,
Point3D currentPoint,
Point3D nextPoint,
@@ -217,6 +236,7 @@ namespace NavisworksTransport.Utils
canonicalCurrentPoint,
canonicalNextPoint,
HostCoordinateAdapter.CanonicalUpVector3,
+ ResolvePreferredNormal(route),
out RailLocalFrame frame))
{
return false;
@@ -231,6 +251,7 @@ namespace NavisworksTransport.Utils
canonicalNextPoint,
HostCoordinateAdapter.CanonicalUpVector3,
axisConvention,
+ ResolvePreferredNormal(route),
localUpRotationDegrees,
out var correctedRotation)
? correctedRotation
@@ -270,6 +291,22 @@ namespace NavisworksTransport.Utils
out Rotation3D rotation)
{
return TryCreateRailRotation(
+ null,
+ previousPoint,
+ currentPoint,
+ nextPoint,
+ out rotation);
+ }
+
+ public static bool TryCreateRailRotation(
+ PathRoute route,
+ Point3D previousPoint,
+ Point3D currentPoint,
+ Point3D nextPoint,
+ out Rotation3D rotation)
+ {
+ return TryCreateRailRotation(
+ route,
previousPoint,
currentPoint,
nextPoint,
@@ -285,6 +322,24 @@ namespace NavisworksTransport.Utils
out Rotation3D rotation)
{
return TryCreateRailRotation(
+ null,
+ previousPoint,
+ currentPoint,
+ nextPoint,
+ axisConvention,
+ out rotation);
+ }
+
+ public static bool TryCreateRailRotation(
+ PathRoute route,
+ Point3D previousPoint,
+ Point3D currentPoint,
+ Point3D nextPoint,
+ ModelAxisConvention axisConvention,
+ out Rotation3D rotation)
+ {
+ return TryCreateRailRotation(
+ route,
previousPoint,
currentPoint,
nextPoint,
@@ -294,6 +349,7 @@ namespace NavisworksTransport.Utils
}
public static bool TryCreateRailRotation(
+ PathRoute route,
Point3D previousPoint,
Point3D currentPoint,
Point3D nextPoint,
@@ -305,6 +361,7 @@ namespace NavisworksTransport.Utils
LogRotationConstructorConventionOnce();
if (!TryCreateRailLinearTransform(
+ route,
previousPoint,
currentPoint,
nextPoint,
@@ -446,7 +503,7 @@ namespace NavisworksTransport.Utils
private static Vector3D ResolveRailNormal(Point3D previousPoint, Point3D currentPoint, Point3D nextPoint)
{
- if (TryCreateCanonicalLocalFrame(previousPoint, currentPoint, nextPoint, out RailLocalFrame frame))
+ if (TryCreateCanonicalLocalFrame(null, previousPoint, currentPoint, nextPoint, out RailLocalFrame frame))
{
return ToNavVector(frame.Normal);
}
@@ -479,6 +536,7 @@ namespace NavisworksTransport.Utils
}
private static bool TryCreateCanonicalLocalFrame(
+ PathRoute route,
Point3D previousPoint,
Point3D currentPoint,
Point3D nextPoint,
@@ -490,9 +548,37 @@ namespace NavisworksTransport.Utils
ToNumerics(adapter.ToCanonicalPoint(currentPoint)),
ToNumerics(adapter.ToCanonicalPoint(nextPoint)),
HostCoordinateAdapter.CanonicalUpVector3,
+ ResolvePreferredNormal(route),
out frame);
}
+ private static Vector3? ResolvePreferredNormal(PathRoute route)
+ {
+ if (route?.RailPreferredNormal == null)
+ {
+ return null;
+ }
+
+ var preferredNormal = new Vector3(
+ (float)route.RailPreferredNormal.X,
+ (float)route.RailPreferredNormal.Y,
+ (float)route.RailPreferredNormal.Z);
+
+ if (preferredNormal.LengthSquared() < 1e-9f)
+ {
+ return null;
+ }
+
+ var adapter = CoordinateSystemManager.Instance.CreateHostAdapter();
+ Vector3 canonicalPreferredNormal = adapter.ToCanonicalVector3(preferredNormal);
+ if (canonicalPreferredNormal.LengthSquared() < 1e-9f)
+ {
+ return null;
+ }
+
+ return Vector3.Normalize(canonicalPreferredNormal);
+ }
+
private static Vector3D Normalize(Vector3D vector)
{
double lengthSquared = vector.X * vector.X + vector.Y * vector.Y + vector.Z * vector.Z;
From d52b1aef082912d637175f1bc18b2b7bc0000fe0 Mon Sep 17 00:00:00 2001
From: tian <11429339@qq.com>
Date: Mon, 23 Mar 2026 21:00:02 +0800
Subject: [PATCH 21/87] Checkpoint animation rotation refactor state
---
AGENTS.md | 13 ++
TransportPlugin.csproj | 3 +
.../CanonicalPlanarPoseBuilderTests.cs | 74 ++++++++
.../CanonicalRailPoseBuilderTests.cs | 28 +++
.../HostCoordinateAdapterTests.cs | 15 ++
.../ObjectSpaceOrientationHelperTests.cs | 16 ++
deploy-plugin.bat | 27 ++-
...oordinate-system-canonical-space-design.md | 54 +++++-
doc/requirement/todo_features.md | 7 +-
src/Core/Animation/PathAnimationManager.cs | 159 +++++++++++-------
src/Core/PathPointRenderPlugin.cs | 19 ++-
src/Core/VirtualObjectManager.cs | 4 +-
.../ViewModels/AnimationControlViewModel.cs | 156 ++++++++---------
src/UI/WPF/Views/EditRotationWindow.xaml | 60 ++++++-
src/UI/WPF/Views/EditRotationWindow.xaml.cs | 121 +++++++++++--
.../CanonicalPlanarPoseBuilder.cs | 52 +++++-
.../CanonicalRailPoseBuilder.cs | 108 ++++++------
.../CoordinateSystem/HostCoordinateAdapter.cs | 26 +++
.../CoordinateSystem/LocalAxisPoseBuilder.cs | 76 +++++++++
.../LocalEulerRotationCorrection.cs | 111 ++++++++++++
.../ObjectSpaceOrientationHelper.cs | 21 +++
.../RotatedObjectExtentHelper.cs | 58 +++++++
src/Utils/RailPathPoseHelper.cs | 45 ++++-
23 files changed, 1015 insertions(+), 238 deletions(-)
create mode 100644 src/Utils/CoordinateSystem/LocalAxisPoseBuilder.cs
create mode 100644 src/Utils/CoordinateSystem/LocalEulerRotationCorrection.cs
create mode 100644 src/Utils/CoordinateSystem/RotatedObjectExtentHelper.cs
diff --git a/AGENTS.md b/AGENTS.md
index d5d4b7e..9e55c6a 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -308,6 +308,19 @@ double distance = GeometryHelper.Distance(pointA, pointB);
- 需要坐标转换?→ 使用 `CoordinateConverter`
- 需要路径相关工具?→ 使用 `PathHelper`
+#### 5. 临时定位代码必须回收
+
+为定位问题临时加入的兜底调用、强制刷新、额外同步、绕过分支等代码,如果后续确认**不是真正根因**,在根因修复后必须恢复或删除,不能把这类临时补丁长期保留在正式代码中。
+
+```csharp
+// ❌ 错误:为掩盖问题长期保留的强制调用
+DoNormalUpdate();
+ForceRefreshAgain(); // 只是为了“看起来好了”
+
+// ✅ 正确:先定位根因,再移除临时补丁
+DoNormalUpdate();
+```
+
### 单位系统 - 极其重要
**所有网格地图和路径规划计算必须使用模型单位,严禁混用米制单位。**
diff --git a/TransportPlugin.csproj b/TransportPlugin.csproj
index af09c90..0b5454e 100644
--- a/TransportPlugin.csproj
+++ b/TransportPlugin.csproj
@@ -334,11 +334,14 @@
+
+
+
diff --git a/UnitTests/CoordinateSystem/CanonicalPlanarPoseBuilderTests.cs b/UnitTests/CoordinateSystem/CanonicalPlanarPoseBuilderTests.cs
index b0b6b0a..89b058c 100644
--- a/UnitTests/CoordinateSystem/CanonicalPlanarPoseBuilderTests.cs
+++ b/UnitTests/CoordinateSystem/CanonicalPlanarPoseBuilderTests.cs
@@ -1,5 +1,6 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using NavisworksTransport.Utils.CoordinateSystem;
+using System;
using System.Numerics;
namespace NavisworksTransport.UnitTests.CoordinateSystem
@@ -55,6 +56,52 @@ namespace NavisworksTransport.UnitTests.CoordinateSystem
Assert.AreEqual(1.0, linear.M33, 1e-6);
}
+ [TestMethod]
+ public void ZeroLocalEulerCorrection_ShouldMatchCurrentPlanarBaseline()
+ {
+ var convention = ModelAxisConvention.CreateDefaultForHost(CoordinateSystemType.ZUp);
+
+ Assert.IsTrue(CanonicalPlanarPoseBuilder.TryCreateQuaternionFromForward(
+ new Vector3(5, 0, 0),
+ Vector3.UnitZ,
+ convention,
+ out Quaternion baselineRotation));
+
+ Assert.IsTrue(CanonicalPlanarPoseBuilder.TryCreateQuaternionFromForward(
+ new Vector3(5, 0, 0),
+ Vector3.UnitZ,
+ convention,
+ LocalEulerRotationCorrection.Zero,
+ out Quaternion correctedRotation));
+
+ AssertQuaternionEquivalent(baselineRotation, correctedRotation);
+ }
+
+ [TestMethod]
+ public void LocalEulerCorrection_ShouldAlignCorrectedLocalForwardWithPlanarForward()
+ {
+ var convention = ModelAxisConvention.CreateDefaultForHost(CoordinateSystemType.ZUp);
+ var correction = new LocalEulerRotationCorrection(0.0, 0.0, 90.0);
+ var adapter = new HostCoordinateAdapter(CoordinateSystemType.ZUp);
+ Quaternion correctionQuaternion = adapter.CreateCanonicalRotationCorrection(correction);
+
+ Assert.IsTrue(CanonicalPlanarPoseBuilder.TryCreateQuaternionFromForward(
+ new Vector3(5, 0, 0),
+ Vector3.UnitZ,
+ convention,
+ correctionQuaternion,
+ out Quaternion rotation));
+
+ LocalAxisPoseBuilder.ApplyLocalPreRotation(convention, correctionQuaternion, out var correctedLocalForward, out var correctedLocalUp);
+ Matrix4x4 linear = Matrix4x4.CreateFromQuaternion(rotation);
+
+ Vector3 mappedForward = TransformLocalVector(linear, correctedLocalForward);
+ Vector3 mappedUp = TransformLocalVector(linear, correctedLocalUp);
+
+ AssertVector(mappedForward, 1, 0, 0);
+ AssertVector(mappedUp, 0, 0, 1);
+ }
+
private static void AssertColumn(Matrix4x4 matrix, int column, double x, double y, double z)
{
switch (column)
@@ -79,5 +126,32 @@ namespace NavisworksTransport.UnitTests.CoordinateSystem
break;
}
}
+
+ private static void AssertVector(Vector3 actual, double x, double y, double z)
+ {
+ Assert.AreEqual(x, actual.X, 1e-6);
+ Assert.AreEqual(y, actual.Y, 1e-6);
+ Assert.AreEqual(z, actual.Z, 1e-6);
+ }
+
+ private static void AssertQuaternionEquivalent(Quaternion expected, Quaternion actual)
+ {
+ double dot = Math.Abs(
+ expected.X * actual.X +
+ expected.Y * actual.Y +
+ expected.Z * actual.Z +
+ expected.W * actual.W);
+
+ Assert.AreEqual(1.0, dot, 1e-6);
+ }
+
+ private static Vector3 TransformLocalVector(Matrix4x4 linear, Vector3 localVector)
+ {
+ Vector3 world = new Vector3(
+ linear.M11 * localVector.X + linear.M12 * localVector.Y + linear.M13 * localVector.Z,
+ linear.M21 * localVector.X + linear.M22 * localVector.Y + linear.M23 * localVector.Z,
+ linear.M31 * localVector.X + linear.M32 * localVector.Y + linear.M33 * localVector.Z);
+ return Vector3.Normalize(world);
+ }
}
}
diff --git a/UnitTests/CoordinateSystem/CanonicalRailPoseBuilderTests.cs b/UnitTests/CoordinateSystem/CanonicalRailPoseBuilderTests.cs
index deef6f7..74b6405 100644
--- a/UnitTests/CoordinateSystem/CanonicalRailPoseBuilderTests.cs
+++ b/UnitTests/CoordinateSystem/CanonicalRailPoseBuilderTests.cs
@@ -137,6 +137,34 @@ namespace NavisworksTransport.UnitTests.CoordinateSystem
AssertVector(mappedUp, 0, 0, 1);
}
+ [TestMethod]
+ public void LocalEulerCorrection_ShouldAlignCorrectedLocalForwardWithRailTangent()
+ {
+ var convention = ModelAxisConvention.CreateRailAssetConvention();
+ var correction = new LocalEulerRotationCorrection(0.0, 90.0, 0.0);
+ var adapter = new HostCoordinateAdapter(CoordinateSystemType.ZUp);
+ Quaternion correctionQuaternion = adapter.CreateCanonicalRotationCorrection(correction);
+
+ Assert.IsTrue(CanonicalRailPoseBuilder.TryCreateQuaternion(
+ new Vector3(0, 0, 0),
+ new Vector3(1, 0, 0),
+ new Vector3(2, 0, 0),
+ Vector3.UnitZ,
+ convention,
+ null,
+ correctionQuaternion,
+ out Quaternion rotation));
+
+ LocalAxisPoseBuilder.ApplyLocalPreRotation(convention, correctionQuaternion, out var correctedLocalForward, out var correctedLocalUp);
+ Matrix4x4 linear = Matrix4x4.CreateFromQuaternion(rotation);
+
+ Vector3 mappedForward = TransformLocalVector(linear, correctedLocalForward);
+ Vector3 mappedUp = TransformLocalVector(linear, correctedLocalUp);
+
+ AssertVector(mappedForward, 1, 0, 0);
+ AssertVector(mappedUp, 0, 0, 1);
+ }
+
[TestMethod]
public void PreferredNormal_ShouldOverrideCanonicalUpForRailFrame()
{
diff --git a/UnitTests/CoordinateSystem/HostCoordinateAdapterTests.cs b/UnitTests/CoordinateSystem/HostCoordinateAdapterTests.cs
index 3d54d44..9f77b59 100644
--- a/UnitTests/CoordinateSystem/HostCoordinateAdapterTests.cs
+++ b/UnitTests/CoordinateSystem/HostCoordinateAdapterTests.cs
@@ -1,6 +1,7 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using NavisworksTransport.Utils.CoordinateSystem;
using System.Numerics;
+using System;
using Autodesk.Navisworks.Api;
namespace NavisworksTransport.UnitTests.CoordinateSystem
@@ -143,6 +144,20 @@ namespace NavisworksTransport.UnitTests.CoordinateSystem
Assert.AreEqual(0.0000, hostLinear.M33, 1e-3);
}
+ [TestMethod]
+ public void YUp_HostRotationCorrection_ShouldMapHostYAxisToCanonicalUp()
+ {
+ var adapter = new HostCoordinateAdapter(CoordinateSystemType.YUp);
+ var correction = new LocalEulerRotationCorrection(0.0, 90.0, 0.0);
+
+ Quaternion canonicalCorrection = adapter.CreateCanonicalRotationCorrection(correction);
+ Vector3 rotatedForward = Vector3.Transform(Vector3.UnitX, canonicalCorrection);
+
+ Assert.AreEqual(0.0, rotatedForward.X, 1e-6);
+ Assert.AreEqual(1.0, rotatedForward.Y, 1e-6);
+ Assert.AreEqual(0.0, rotatedForward.Z, 1e-6);
+ }
+
private static void AssertPoint(Vector3 point, double x, double y, double z)
{
Assert.AreEqual(x, point.X, 1e-9);
diff --git a/UnitTests/CoordinateSystem/ObjectSpaceOrientationHelperTests.cs b/UnitTests/CoordinateSystem/ObjectSpaceOrientationHelperTests.cs
index a15baf4..f60f624 100644
--- a/UnitTests/CoordinateSystem/ObjectSpaceOrientationHelperTests.cs
+++ b/UnitTests/CoordinateSystem/ObjectSpaceOrientationHelperTests.cs
@@ -36,5 +36,21 @@ namespace NavisworksTransport.UnitTests.CoordinateSystem
Assert.AreEqual(0.0, up.Y, 1e-6);
Assert.AreEqual(0.0, up.Z, 1e-6);
}
+
+ [TestMethod]
+ public void TryCalculateAxes_ShouldReturnFalse_ForZeroLengthSegment()
+ {
+ var segmentDirection = Vector3.Zero;
+ var upReference = new Vector3(0f, 1f, 0f);
+
+ bool ok = ObjectSpaceOrientationHelper.TryCalculateAxes(
+ segmentDirection,
+ upReference,
+ out var axes);
+
+ Assert.IsFalse(ok);
+ Assert.AreEqual(Vector3.Zero, axes.right);
+ Assert.AreEqual(Vector3.Zero, axes.up);
+ }
}
}
diff --git a/deploy-plugin.bat b/deploy-plugin.bat
index 9c39165..0c5a3cd 100644
--- a/deploy-plugin.bat
+++ b/deploy-plugin.bat
@@ -7,6 +7,21 @@ powershell -NoProfile -ExecutionPolicy Bypass -Command ^
"$ErrorActionPreference = 'Stop';" ^
"$targetDir = [System.IO.Path]::GetFullPath('%TARGET_DIR%');" ^
"$buildDir = [System.IO.Path]::GetFullPath('%BUILD_DIR%');" ^
+ "$deployDllNames = @(" ^
+ " 'TransportPlugin.dll'," ^
+ " 'geometry4Sharp.dll'," ^
+ " 'Newtonsoft.Json.dll'," ^
+ " 'Roy-T.AStar.dll'," ^
+ " 'SQLite.Interop.dll'," ^
+ " 'System.Data.SQLite.dll'," ^
+ " 'Tomlyn.dll'" ^
+ ");" ^
+ "$stalePluginFiles = @(" ^
+ " 'Autodesk.Navisworks.Api.dll'," ^
+ " 'NavisworksTransport.UnitTests.dll'," ^
+ " 'Microsoft.VisualStudio.TestPlatform.TestFramework.dll'," ^
+ " 'Microsoft.VisualStudio.TestPlatform.TestFramework.Extensions.dll'" ^
+ ");" ^
"function Test-FileUnlocked([string]$path) {" ^
" if (-not (Test-Path $path)) { return $true }" ^
" try {" ^
@@ -39,14 +54,22 @@ powershell -NoProfile -ExecutionPolicy Bypass -Command ^
"foreach ($keyFile in $keyTargetFiles) { Wait-FileUnlocked $keyFile $deadline }" ^
"" ^
"New-Item -ItemType Directory -Force -Path $targetDir | Out-Null;" ^
- "Copy-Item (Join-Path $buildDir '*.dll') $targetDir -Force;" ^
+ "foreach ($staleFileName in $stalePluginFiles) {" ^
+ " $stalePath = Join-Path $targetDir $staleFileName;" ^
+ " if (Test-Path $stalePath) { Remove-Item $stalePath -Force }" ^
+ "}" ^
+ "foreach ($dllName in $deployDllNames) {" ^
+ " $sourceDll = Join-Path $buildDir $dllName;" ^
+ " if (-not (Test-Path $sourceDll)) { throw ('Deployment source file missing: ' + $sourceDll) }" ^
+ " Copy-Item $sourceDll $targetDir -Force;" ^
+ "}" ^
"if (Test-Path (Join-Path $buildDir 'resources')) {" ^
" New-Item -ItemType Directory -Force -Path (Join-Path $targetDir 'resources') | Out-Null;" ^
" Copy-Item (Join-Path $buildDir 'resources\\*') (Join-Path $targetDir 'resources') -Recurse -Force;" ^
"}" ^
"" ^
"$sourceFiles = @();" ^
- "$sourceFiles += Get-ChildItem $buildDir -File -Filter '*.dll';" ^
+ "foreach ($dllName in $deployDllNames) { $sourceFiles += Get-Item (Join-Path $buildDir $dllName) }" ^
"if (Test-Path (Join-Path $buildDir 'resources')) {" ^
" $sourceFiles += Get-ChildItem (Join-Path $buildDir 'resources') -File;" ^
"}" ^
diff --git a/doc/design/2026/coordinate-system-canonical-space-design.md b/doc/design/2026/coordinate-system-canonical-space-design.md
index 7aa02af..9c06e51 100644
--- a/doc/design/2026/coordinate-system-canonical-space-design.md
+++ b/doc/design/2026/coordinate-system-canonical-space-design.md
@@ -41,6 +41,7 @@
- Navisworks 世界坐标统一视为**外部坐标**
- 程序内部统一使用一套**规范内部坐标**
- 工程语义使用独立的**业务基准坐标**
+- 只有插件自带资源才允许引入**资产坐标系**
- 坐标转换只发生在少数边界入口
- 业务逻辑禁止直接依赖宿主坐标语义
@@ -95,6 +96,7 @@ flowchart LR
特点:
- 是程序的输入/输出坐标
+- UI 文本框、对话框、日志、鼠标拾取结果,统一按宿主坐标系解释和显示
- 可能来自 `Y-up` 项目,也可能来自 `Z-up` 项目
- 不应直接当成内部计算坐标
@@ -118,6 +120,29 @@ flowchart LR
- 项目 up 方向的业务解释
- 终端安装参考面
- 轨道参考面
+
+### 3.2.1 术语约束
+
+后续文档、代码注释和日志中,统一使用以下说法:
+
+- **宿主坐标系(Host Space)**
+ - 指 Navisworks 文档坐标系
+ - `Y-up` / `Z-up` 的判断只属于这一层
+- **内部坐标系(Canonical Space)**
+ - 指程序内部统一使用的 `Z-up` 坐标系
+ - 仅供内部计算使用,禁止直接暴露给 UI
+- **资产坐标系(Asset Space)**
+ - 只用于插件自带资源
+ - 当前明确只有两类:`虚拟物体` 与 `单位圆柱体(参考杆资源)`
+ - 用于描述这些资源文件自身的 `Forward/Up/Side` 轴约定
+
+禁止继续使用含糊的“本地坐标系”说法,因为它容易混淆:
+
+- 宿主坐标系
+- 内部坐标系
+- 资产坐标系
+
+后续凡是 UI 输入/输出,必须明确是**宿主坐标系**语义;凡是姿态或几何内部计算,必须明确是**内部坐标系**语义;凡是虚拟物体/单位圆柱体资源朝向,必须明确是**资产坐标系**语义。
- 业务锚点
这层不能偷用世界原点,也不能偷用宿主世界轴。
@@ -154,24 +179,35 @@ flowchart LR
都必须基于这组定义来解释,不能继续偷用“世界 Z 就是 up”的旧假设。
-### 3.4 模型局部轴约定是独立层
+### 3.4 资产坐标系是独立层
-除了宿主坐标系和内部统一坐标系,还必须区分**模型局部轴约定**。
+除了宿主坐标系和内部统一坐标系,还必须区分**资产坐标系**。
+
+注意,这一层**不是所有模型都有**。当前项目里只有插件自带资源需要它:
+
+- 虚拟物体资源
+- 单位圆柱体参考杆资源
这是另一层独立定义,至少包含:
-- `LocalForwardAxis`
-- `LocalUpAxis`
+- `AssetForwardAxis`
+- `AssetUpAxis`
例如:
-- 虚拟物体资源通常按程序约定构建,可能是 `Local X = Forward, Local Z = Up`
-- 真实模型如果来自 `Y-up` 项目,则很可能是 `Local X = Forward, Local Y = Up`
+- 虚拟物体资源通常按程序约定构建,可能是 `Asset X = Forward, Asset Z = Up`
+- 单位圆柱体参考杆资源当前约定为 `Asset X = 杆轴方向, Asset Z = 截面 Up`
+
+而对于客户真实模型:
+
+- 不单独引入“资产坐标系”概念
+- UI 和数据输入输出仍只认宿主坐标系
+- 内部算法如需统一姿态,先进入 Canonical Space,再叠加业务参考信息
这意味着:
- 即使宿主坐标系转换已经正确
-- 如果模型局部轴约定没有显式处理
+- 如果资产坐标系语义没有显式处理
- 动画和姿态仍然会出现“路径对了、通行空间对了、模型自己站歪了”的现象
因此,后续姿态系统必须显式区分:
@@ -179,7 +215,7 @@ flowchart LR
1. 宿主坐标系定义
2. 内部统一坐标系定义
3. 业务基准定义
-4. 模型局部轴约定
+4. 资产坐标系约定(仅限虚拟物体和单位圆柱体等插件自带资源)
### 3.5 Rail 局部坐标系与动画跟踪点
@@ -599,6 +635,8 @@ WPF 插件的最终部署,必须依赖完整主项目构建产物,而不应
- **Navisworks 世界坐标统一视为外部坐标**
- **程序内部统一使用 Canonical Space(Z-up)**
- **业务基准点单独建模**
+- **UI 输入输出统一使用宿主坐标系**
+- **资产坐标系只属于插件自带资源,不得泛化成“所有模型都有本地轴”**
- **坐标转换只发生在边界层**
这是一条更接近业界三维软件成熟做法的路线。
diff --git a/doc/requirement/todo_features.md b/doc/requirement/todo_features.md
index 17d59b3..b886dc7 100644
--- a/doc/requirement/todo_features.md
+++ b/doc/requirement/todo_features.md
@@ -2,9 +2,14 @@
## 功能点
+### [2026/3/20]
+
+1. [x] (优化)实现Yup模型兼容
+2. [x] (功能)运动物体在起点支持三个方向的旋转
+
### [2026/3/18]
-1. [x](功能)增加轨上路径(角度可倾斜)
+1. [x] (功能)增加终端仿真需要的轨上和轨下路径(角度可倾斜)
### [2026/3/11]
diff --git a/src/Core/Animation/PathAnimationManager.cs b/src/Core/Animation/PathAnimationManager.cs
index e6ef4fa..3650993 100644
--- a/src/Core/Animation/PathAnimationManager.cs
+++ b/src/Core/Animation/PathAnimationManager.cs
@@ -88,12 +88,19 @@ namespace NavisworksTransport.Core.Animation
}
}
+ internal enum AnimatedObjectMode
+ {
+ None,
+ RealObject,
+ VirtualObject
+ }
+
public class PathAnimationManager
{
private static PathAnimationManager _instance;
private static readonly Dictionary _animationHashToRecordId = new Dictionary(); // 动画配置哈希到检测记录ID的映射
private ModelItem _animatedObject;
- private bool _isVirtualObject = false; // 是否使用虚拟物体
+ private AnimatedObjectMode _animatedObjectMode = AnimatedObjectMode.None;
private double _virtualObjectLength = 0; // 虚拟物体长度(模型单位)
private double _virtualObjectWidth = 0; // 虚拟物体宽度(模型单位)
private double _virtualObjectHeight = 0; // 虚拟物体高度(模型单位)
@@ -105,7 +112,7 @@ namespace NavisworksTransport.Core.Animation
private bool _manualCollisionOverrideEnabled = false;
// === 角度修正 ===
- private double _objectRotationCorrection = 0.0; // 物体角度修正值(度,顺时针)
+ private LocalEulerRotationCorrection _objectRotationCorrection = LocalEulerRotationCorrection.Zero; // 物体绕宿主 X/Y/Z 轴的角度修正
// === 碰撞排除列表缓存管理(从LogisticsAnimationManager迁移)===
private ModelItem _currentCachedAnimationObject;
@@ -182,6 +189,12 @@ namespace NavisworksTransport.Core.Animation
private bool _savedObjectHasCustomRotation = false;
private bool _hasSavedObjectState = false;
+ private bool IsVirtualObjectMode => _animatedObjectMode == AnimatedObjectMode.VirtualObject;
+ private bool IsRealObjectMode => _animatedObjectMode == AnimatedObjectMode.RealObject;
+
+ private ModelItem CurrentControlledObject =>
+ IsVirtualObjectMode ? VirtualObjectManager.Instance.CurrentVirtualObject : _animatedObject;
+
///
/// 当前正在进行动画的对象
///
@@ -397,7 +410,7 @@ namespace NavisworksTransport.Core.Animation
// 🔥 支持虚拟物体的恢复
ModelItem objectToRestore = null;
- bool isVirtual = _isVirtualObject;
+ bool isVirtual = IsVirtualObjectMode;
if (isVirtual)
{
@@ -458,7 +471,19 @@ namespace NavisworksTransport.Core.Animation
public void SetAnimatedObject(ModelItem animatedObject)
{
_animatedObject = animatedObject;
- _isVirtualObject = (animatedObject == null);
+ _animatedObjectMode = animatedObject == null
+ ? AnimatedObjectMode.None
+ : AnimatedObjectMode.RealObject;
+
+ if (animatedObject != null)
+ {
+ _originalTransform = animatedObject.Transform;
+ _originalCenter = animatedObject.BoundingBox().Center;
+ _trackedPosition = GetTrackedObjectPosition(animatedObject);
+ _currentYaw = ModelItemTransformHelper.GetYawFromTransform(_originalTransform);
+ _trackedRotation = _originalTransform.Factor().Rotation;
+ _hasTrackedRotation = true;
+ }
}
///
@@ -467,7 +492,9 @@ namespace NavisworksTransport.Core.Animation
///
public void SetVirtualObjectParameters(bool isVirtualObject, double length, double width, double height)
{
- _isVirtualObject = isVirtualObject;
+ _animatedObjectMode = isVirtualObject
+ ? AnimatedObjectMode.VirtualObject
+ : (_animatedObject != null ? AnimatedObjectMode.RealObject : AnimatedObjectMode.None);
_virtualObjectLength = length; // 模型单位
_virtualObjectWidth = width; // 模型单位
_virtualObjectHeight = height; // 模型单位
@@ -592,8 +619,8 @@ namespace NavisworksTransport.Core.Animation
{
// 🔥 重要:先恢复物体到原始状态(CAD位置和原始朝向),确保每次计算都从相同的状态开始
// 这样可以避免之前旋转导致的包围盒尺寸计算不准确的问题
- // 注意:也要处理虚拟物体(_isVirtualObject为true时_animatedObject可能为null)
- if (_animatedObject != null || _isVirtualObject)
+ // 注意:也要处理虚拟物体(当前控制对象可能不在 _animatedObject 中)
+ if (CurrentControlledObject != null || IsVirtualObjectMode)
{
RestoreObjectToCADPosition();
LogManager.Info($"[移动到起点] 已恢复物体到原始状态, _currentYaw={_currentYaw * 180 / Math.PI:F2}°");
@@ -603,6 +630,12 @@ namespace NavisworksTransport.Core.Animation
if (animatedObject != null)
{
_animatedObject = animatedObject;
+ bool isVirtualObject =
+ VirtualObjectManager.Instance.IsVirtualObjectActive &&
+ ReferenceEquals(animatedObject, VirtualObjectManager.Instance.CurrentVirtualObject);
+ _animatedObjectMode = isVirtualObject
+ ? AnimatedObjectMode.VirtualObject
+ : AnimatedObjectMode.RealObject;
_originalTransform = animatedObject.Transform;
_originalCenter = animatedObject.BoundingBox().Center;
_trackedPosition = GetTrackedObjectPosition(animatedObject);
@@ -611,6 +644,9 @@ namespace NavisworksTransport.Core.Animation
_currentYaw = ModelItemTransformHelper.GetYawFromTransform(_originalTransform);
_trackedRotation = _originalTransform.Factor().Rotation;
_hasTrackedRotation = true;
+
+ LogManager.Info(
+ $"[移动到起点] 更新动画对象内部状态: 对象={animatedObject.DisplayName}, 虚拟物体={IsVirtualObjectMode}");
}
if (pathPoints != null)
@@ -629,7 +665,7 @@ namespace NavisworksTransport.Core.Animation
double pathDirectionYaw = Math.Atan2(_pathPoints[1].Y - _pathPoints[0].Y, _pathPoints[1].X - _pathPoints[0].X);
double yaw;
- LogManager.Info($"[移动到起点] 路径方向yaw: {pathDirectionYaw * 180 / Math.PI:F2}度, 角度修正: {_objectRotationCorrection:F1}度");
+ LogManager.Info($"[移动到起点] 路径方向yaw: {pathDirectionYaw * 180 / Math.PI:F2}度, 角度修正: {_objectRotationCorrection}");
// 根据路径类型调整朝向
if (_route.PathType == PathType.Hoisting)
@@ -646,12 +682,12 @@ namespace NavisworksTransport.Core.Animation
LogManager.Debug($"[移动到起点] 地面/空轨路径使用路径方向: {yaw * 180 / Math.PI:F2}度");
}
- // 应用角度修正值(顺时针,转换为弧度)
- if (_objectRotationCorrection != 0.0)
+ // 旧 yaw 链仅保留给未进入完整三维姿态的遗留路径。
+ if (!_objectRotationCorrection.IsZero)
{
- double correctionRad = _objectRotationCorrection * Math.PI / 180.0;
+ double correctionRad = _objectRotationCorrection.ZDegrees * Math.PI / 180.0;
yaw += correctionRad;
- LogManager.Debug($"[移动到起点] 应用角度修正: {_objectRotationCorrection:F1}°, 修正后yaw: {yaw * 180 / Math.PI:F2}度");
+ LogManager.Debug($"[移动到起点] 应用遗留Z轴修正: {_objectRotationCorrection.ZDegrees:F1}°, 修正后yaw: {yaw * 180 / Math.PI:F2}度");
}
// 根据路径类型调整起点位置
@@ -1147,9 +1183,9 @@ namespace NavisworksTransport.Core.Animation
LogManager.Debug($"被撞物体位置:{GetObjectPosition(collider).X:F2},{GetObjectPosition(collider).Y:F2},{GetObjectPosition(collider).Z:F2}");
double actualYawRadians = yawRadians;
- if (!frame.HasCustomRotation && _objectRotationCorrection != 0.0)
+ if (!frame.HasCustomRotation && !_objectRotationCorrection.IsZero)
{
- actualYawRadians += _objectRotationCorrection * Math.PI / 180.0;
+ actualYawRadians += _objectRotationCorrection.ZDegrees * Math.PI / 180.0;
}
var collisionResult = new CollisionResult
@@ -1548,7 +1584,7 @@ namespace NavisworksTransport.Core.Animation
{
var firstFrame = _animationFrames[0];
- LogManager.Debug($"[动画开始] _currentYaw之前={_currentYaw * 180 / Math.PI:F2}度, _isVirtualObject={_isVirtualObject}");
+ LogManager.Debug($"[动画开始] _currentYaw之前={_currentYaw * 180 / Math.PI:F2}度, 模式={_animatedObjectMode}");
// 🔥 确保物体在起点位置和朝向
// 注意:MoveObjectToPathStart 已在 StartAnimation 中调用,这里只是日志记录
@@ -1829,7 +1865,7 @@ namespace NavisworksTransport.Core.Animation
_detectionTolerance,
_currentRouteId,
_animatedObject,
- _isVirtualObject,
+ IsVirtualObjectMode,
_animationFrameRate,
_animationDuration,
_virtualObjectLength,
@@ -2411,8 +2447,8 @@ namespace NavisworksTransport.Core.Animation
{
try
{
- bool isRailRealObject = _route?.PathType == PathType.Rail && !_isVirtualObject && _animatedObject != null;
- if (_isVirtualObject)
+ bool isRailRealObject = _route?.PathType == PathType.Rail && IsRealObjectMode && _animatedObject != null;
+ if (IsVirtualObjectMode)
{
VirtualObjectManager.Instance.MoveVirtualObject(newPosition, newRotation);
}
@@ -2475,7 +2511,7 @@ namespace NavisworksTransport.Core.Animation
}
else if (_route?.PathType == PathType.Ground || _route?.PathType == PathType.Hoisting)
{
- LogHostActualPoseAxes(_isVirtualObject ? VirtualObjectManager.Instance.CurrentVirtualObject : _animatedObject, "[平面姿态应用后宿主姿态]", false);
+ LogHostActualPoseAxes(CurrentControlledObject, "[平面姿态应用后宿主姿态]", false);
}
}
catch (Exception ex)
@@ -2688,7 +2724,7 @@ namespace NavisworksTransport.Core.Animation
return false;
}
- if (_isVirtualObject)
+ if (IsVirtualObjectMode)
{
return VirtualObjectManager.Instance.IsVirtualObjectActive &&
ReferenceEquals(VirtualObjectManager.Instance.CurrentVirtualObject, obj);
@@ -2723,7 +2759,7 @@ namespace NavisworksTransport.Core.Animation
try
{
var originalAnimatedObject = _animatedObject;
- if (!_isVirtualObject)
+ if (!IsVirtualObjectMode)
{
_animatedObject = obj;
}
@@ -3243,7 +3279,7 @@ namespace NavisworksTransport.Core.Animation
///
private double GetAnimatedObjectHeight()
{
- if (_isVirtualObject)
+ if (IsVirtualObjectMode)
{
return _virtualObjectHeight;
}
@@ -3334,7 +3370,7 @@ namespace NavisworksTransport.Core.Animation
_pathName = pathName;
_currentRouteId = routeId;
- _isVirtualObject = isVirtualObject; // 设置是否使用虚拟物体
+ _animatedObjectMode = isVirtualObject ? AnimatedObjectMode.VirtualObject : AnimatedObjectMode.RealObject;
_virtualObjectLength = virtualObjectLength; // 模型单位
_virtualObjectWidth = virtualObjectWidth; // 模型单位
_virtualObjectHeight = virtualObjectHeight; // 模型单位
@@ -3351,12 +3387,12 @@ namespace NavisworksTransport.Core.Animation
// 保持当前的 _currentYaw(因为物体可能已经被 MoveObjectToPathStart 旋转)
// 不要从 Transform 中提取,因为 Transform 返回的是原始值,不是当前值
- LogManager.Debug($"[CreateAnimation] 保持_currentYaw={_currentYaw * 180 / Math.PI:F2}度不变, _isVirtualObject={_isVirtualObject}");
+ LogManager.Debug($"[CreateAnimation] 保持_currentYaw={_currentYaw * 180 / Math.PI:F2}度不变, 模式={_animatedObjectMode}");
}
// 设置动画参数并预计算动画帧
// 注意:物体应该在调用CreateAnimation之前已经通过MoveObjectToPathStart旋转到起点
- LogManager.Debug($"[CreateAnimation开始] _currentYaw之前={_currentYaw * 180 / Math.PI:F2}度, _isVirtualObject={_isVirtualObject}");
+ LogManager.Debug($"[CreateAnimation开始] _currentYaw之前={_currentYaw * 180 / Math.PI:F2}度, 模式={_animatedObjectMode}");
SetupAnimation(animatedObject, durationSeconds, _route);
LogManager.Debug($"[CreateAnimation结束] _currentYaw之后={_currentYaw * 180 / Math.PI:F2}度");
// 设置动画状态为Ready(动画已生成,可以播放)
@@ -3405,7 +3441,7 @@ namespace NavisworksTransport.Core.Animation
private ModelAxisConvention GetCurrentRailModelAxisConvention()
{
- if (_isVirtualObject)
+ if (IsVirtualObjectMode)
{
return ModelAxisConvention.CreateVirtualObjectAssetConvention();
}
@@ -3413,14 +3449,14 @@ namespace NavisworksTransport.Core.Animation
var adapter = CoordinateSystemManager.Instance.CreateHostAdapter();
var convention = ModelAxisConvention.CreateDefaultForHost(adapter.HostType);
LogManager.Info(
- $"[Rail姿态修正] Host={adapter.HostType}, 虚拟物体={_isVirtualObject}, " +
+ $"[Rail姿态修正] Host={adapter.HostType}, 虚拟物体={IsVirtualObjectMode}, " +
$"Forward={convention.ForwardAxis}, Up={convention.UpAxis}");
return convention;
}
private ModelAxisConvention GetCurrentModelAxisConvention()
{
- if (_isVirtualObject)
+ if (IsVirtualObjectMode)
{
return ModelAxisConvention.CreateVirtualObjectAssetConvention();
}
@@ -3487,6 +3523,7 @@ namespace NavisworksTransport.Core.Animation
Vector3 canonicalPrevious = ToNumerics(adapter.ToCanonicalPoint(previousPoint));
Vector3 canonicalCurrent = ToNumerics(adapter.ToCanonicalPoint(currentPoint));
Vector3 canonicalNext = ToNumerics(adapter.ToCanonicalPoint(nextPoint));
+ Quaternion correctionQuaternion = adapter.CreateCanonicalRotationCorrection(_objectRotationCorrection);
Vector3 forward = canonicalNext - canonicalPrevious;
if (forward.LengthSquared() < 1e-6f)
@@ -3494,12 +3531,11 @@ namespace NavisworksTransport.Core.Animation
forward = canonicalNext - canonicalCurrent;
}
- forward = ApplyPlanarRotationCorrection(forward);
-
if (!CanonicalPlanarPoseBuilder.TryCreateQuaternionFromForward(
forward,
HostCoordinateAdapter.CanonicalUpVector3,
convention,
+ correctionQuaternion,
out var quaternion))
{
return false;
@@ -3516,12 +3552,12 @@ namespace NavisworksTransport.Core.Animation
var adapter = CoordinateSystemManager.Instance.CreateHostAdapter();
var convention = GetCurrentModelAxisConvention();
Vector3 canonicalForward = adapter.ToCanonicalVector3(new Vector3((float)hostForward.X, (float)hostForward.Y, (float)hostForward.Z));
- canonicalForward = ApplyPlanarRotationCorrection(canonicalForward);
-
+ Quaternion correctionQuaternion = adapter.CreateCanonicalRotationCorrection(_objectRotationCorrection);
if (!CanonicalPlanarPoseBuilder.TryCreateQuaternionFromForward(
canonicalForward,
HostCoordinateAdapter.CanonicalUpVector3,
convention,
+ correctionQuaternion,
out var quaternion))
{
return false;
@@ -3531,25 +3567,10 @@ namespace NavisworksTransport.Core.Animation
return true;
}
- private Vector3 ApplyPlanarRotationCorrection(Vector3 canonicalForward)
- {
- if (Math.Abs(_objectRotationCorrection) < 1e-9)
- {
- return canonicalForward;
- }
-
- float correctionRadians = (float)(_objectRotationCorrection * Math.PI / 180.0);
- Quaternion correction = Quaternion.CreateFromAxisAngle(
- Vector3.Normalize(HostCoordinateAdapter.CanonicalUpVector3),
- correctionRadians);
- return Vector3.Transform(canonicalForward, correction);
- }
-
///
- /// 设置物体角度修正值(度,顺时针)
+ /// 设置物体绕宿主 X/Y/Z 轴的角度修正
///
- /// 角度修正值(度)
- public void SetObjectRotationCorrection(double rotationCorrection)
+ public void SetObjectRotationCorrection(LocalEulerRotationCorrection rotationCorrection)
{
_objectRotationCorrection = rotationCorrection;
@@ -3560,7 +3581,7 @@ namespace NavisworksTransport.Core.Animation
{
// 重新计算并应用朝向
MoveObjectToPathStart();
- LogManager.Info($"[角度修正] 物体角度已更新: {_objectRotationCorrection:F1}°");
+ LogManager.Info($"[角度修正] 物体角度已更新: {_objectRotationCorrection}");
}
catch (Exception ex)
{
@@ -3570,13 +3591,36 @@ namespace NavisworksTransport.Core.Animation
}
///
- /// 直接设置物体角度修正值(不触发物体旋转)
+ /// 保留给旧单轴调用方:按当前宿主 up 轴映射成三轴角度修正。
///
- /// 角度修正值(度)
- public void SetObjectRotationCorrectionDirect(double rotationCorrection)
+ public void SetObjectRotationCorrection(double rotationCorrection)
+ {
+ SetObjectRotationCorrection(CreateLegacyUpAxisCorrection(rotationCorrection));
+ }
+
+ ///
+ /// 直接设置物体绕宿主 X/Y/Z 轴的角度修正(不触发物体旋转)
+ ///
+ public void SetObjectRotationCorrectionDirect(LocalEulerRotationCorrection rotationCorrection)
{
_objectRotationCorrection = rotationCorrection;
- LogManager.Debug($"[角度修正] 直接设置角度修正值: {_objectRotationCorrection:F1}°(不触发旋转)");
+ LogManager.Debug($"[角度修正] 直接设置角度修正值: {_objectRotationCorrection}(不触发旋转)");
+ }
+
+ ///
+ /// 保留给旧单轴调用方:按当前宿主 up 轴映射成三轴角度修正。
+ ///
+ public void SetObjectRotationCorrectionDirect(double rotationCorrection)
+ {
+ SetObjectRotationCorrectionDirect(CreateLegacyUpAxisCorrection(rotationCorrection));
+ }
+
+ private LocalEulerRotationCorrection CreateLegacyUpAxisCorrection(double rotationCorrection)
+ {
+ var adapter = CoordinateSystemManager.Instance.CreateHostAdapter();
+ return adapter.HostType == CoordinateSystemType.YUp
+ ? new LocalEulerRotationCorrection(0.0, rotationCorrection, 0.0)
+ : new LocalEulerRotationCorrection(0.0, 0.0, rotationCorrection);
}
#region 动画实现方法
@@ -3732,11 +3776,6 @@ namespace NavisworksTransport.Core.Animation
}
double actualYaw = frameData.YawRadians;
- if (_objectRotationCorrection != 0.0)
- {
- double correctionRad = _objectRotationCorrection * Math.PI / 180.0;
- actualYaw += correctionRad;
- }
if (frameData.HasCustomRotation)
{
@@ -3858,7 +3897,7 @@ namespace NavisworksTransport.Core.Animation
sb.Append("|");
// 包含角度修正(确保角度修正改变时重新检测)
- sb.Append($"RotationCorrection:{_objectRotationCorrection:F2}deg");
+ sb.Append($"RotationCorrection:{_objectRotationCorrection}");
sb.Append("|");
// 包含手工检测对象列表(确保手工指定模式的目标变化时重新检测)
diff --git a/src/Core/PathPointRenderPlugin.cs b/src/Core/PathPointRenderPlugin.cs
index 13983d2..a06c3d2 100644
--- a/src/Core/PathPointRenderPlugin.cs
+++ b/src/Core/PathPointRenderPlugin.cs
@@ -1886,6 +1886,19 @@ namespace NavisworksTransport
{
var startPoint = sortedPoints[i];
var endPoint = sortedPoints[i + 1];
+ double segmentDx = endPoint.Position.X - startPoint.Position.X;
+ double segmentDy = endPoint.Position.Y - startPoint.Position.Y;
+ double segmentDz = endPoint.Position.Z - startPoint.Position.Z;
+ double segmentLength = Math.Sqrt(
+ segmentDx * segmentDx +
+ segmentDy * segmentDy +
+ segmentDz * segmentDz);
+
+ if (segmentLength < 0.001)
+ {
+ LogManager.Debug($"[路径渲染] 跳过零长度路径段: 索引={i}, 路径={visualization.PathRoute?.Name}");
+ continue;
+ }
// 对于吊装路径,判断是否是垂直段和起吊/下降段
bool isVerticalSegment = false;
@@ -1911,11 +1924,7 @@ namespace NavisworksTransport
// 沿宿主 up 判断垂直段,避免把世界 Z 误当成向上轴
var hostUp = GetHostUpVector();
- double dx = endPoint.Position.X - startPoint.Position.X;
- double dy = endPoint.Position.Y - startPoint.Position.Y;
- double dz = endPoint.Position.Z - startPoint.Position.Z;
- double upDelta = dx * hostUp.X + dy * hostUp.Y + dz * hostUp.Z;
- double segmentLength = Math.Sqrt(dx * dx + dy * dy + dz * dz);
+ double upDelta = segmentDx * hostUp.X + segmentDy * hostUp.Y + segmentDz * hostUp.Z;
double horizontalDist = Math.Sqrt(Math.Max(0.0, segmentLength * segmentLength - upDelta * upDelta));
// 如果沿宿主 up 的变化占主导(垂直段),或者是第一个/最后一个线段
bool isZDominant = Math.Abs(upDelta) > horizontalDist * 2.0;
diff --git a/src/Core/VirtualObjectManager.cs b/src/Core/VirtualObjectManager.cs
index f8e7647..311d93b 100644
--- a/src/Core/VirtualObjectManager.cs
+++ b/src/Core/VirtualObjectManager.cs
@@ -213,7 +213,9 @@ namespace NavisworksTransport.Core
try
{
LogVirtualObjectGeometry("[虚拟物体姿态] 应用前");
- ModelItemTransformHelper.MoveItemToPositionAndRotation(_virtualObjectModelItem, position, rotation);
+ // 虚拟物体当前尺寸是通过永久变换中的缩放实现的。
+ // 应用完整姿态时必须保留当前缩放,否则会破坏虚拟物体的资产姿态和几何语义。
+ ModelItemTransformHelper.MoveItemToPositionAndRotationWithCurrentScale(_virtualObjectModelItem, position, rotation);
var actualBounds = _virtualObjectModelItem.BoundingBox();
Point3D actualCenter = actualBounds?.Center ?? new Point3D(0, 0, 0);
LogManager.Info(
diff --git a/src/UI/WPF/ViewModels/AnimationControlViewModel.cs b/src/UI/WPF/ViewModels/AnimationControlViewModel.cs
index 59c3aa1..ffd37f2 100644
--- a/src/UI/WPF/ViewModels/AnimationControlViewModel.cs
+++ b/src/UI/WPF/ViewModels/AnimationControlViewModel.cs
@@ -4,6 +4,7 @@ using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Linq;
+using System.Numerics;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using System.Windows;
@@ -355,7 +356,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
private double _safetyMarginInMeters; // 检测间隙(米),从路径编辑同步
// 角度修正相关字段
- private double _objectRotationCorrection; // 物体角度修正值(度,顺时针)
+ private LocalEulerRotationCorrection _objectRotationCorrection = LocalEulerRotationCorrection.Zero; // 物体绕宿主 X/Y/Z 轴的角度修正
// 移动物体原始尺寸(米,在选择物体时保存)
private double _objectOriginalLength; // 物体原始长度(X方向)
@@ -708,8 +709,8 @@ namespace NavisworksTransport.UI.WPF.ViewModels
VirtualObjectLengthInMeters, VirtualObjectWidthInMeters, VirtualObjectHeightInMeters);
// 重置角度修正值(虚拟物体重新选择时重置)
- ObjectRotationCorrection = 0.0;
- LogManager.Debug($"[切换虚拟物体] 已重置角度修正值为0度");
+ ObjectRotationCorrection = LocalEulerRotationCorrection.Zero;
+ LogManager.Debug("[切换虚拟物体] 已重置角度修正值为0");
// 当切换到虚拟物体模式时,根据路径类型打开通行空间可视化
UpdatePassageSpaceVisualization();
@@ -792,9 +793,9 @@ namespace NavisworksTransport.UI.WPF.ViewModels
}
///
- /// 物体角度修正值(度,顺时针)
+ /// 物体绕宿主 X/Y/Z 轴的角度修正。
///
- public double ObjectRotationCorrection
+ public LocalEulerRotationCorrection ObjectRotationCorrection
{
get => _objectRotationCorrection;
set
@@ -1261,8 +1262,8 @@ namespace NavisworksTransport.UI.WPF.ViewModels
SelectAnimatedObjectCommand = new RelayCommand(() =>
{
// 重置角度修正值(每个物体的旋转独立)
- ObjectRotationCorrection = 0.0;
- LogManager.Debug($"[选择物体] 已重置角度修正值为0度");
+ ObjectRotationCorrection = LocalEulerRotationCorrection.Zero;
+ LogManager.Debug("[选择物体] 已重置角度修正值为0");
ExecuteSelectAnimatedObject();
});
ClearAnimatedObjectCommand = new RelayCommand(ExecuteClearAnimatedObject, () => HasSelectedAnimatedObject);
@@ -1714,19 +1715,30 @@ namespace NavisworksTransport.UI.WPF.ViewModels
// 3. 设置新物体(会触发UpdatePassageSpaceVisualization)
SelectedAnimatedObject = newObject;
+ _pathAnimationManager?.SetAnimatedObject(newObject);
LogManager.Info($"已选择移动物体: {SelectedAnimatedObject.DisplayName}");
+ // 选择实体物体意味着切换到实体模式,避免后续起点同步仍走虚拟物体分支。
+ if (UseVirtualObject)
+ {
+ UseVirtualObject = false;
+ LogManager.Debug("[选择物体] 已切换到实体物体模式");
+ }
+
// 只有选择不同的物体时,才重置角度修正值
if (!isSameObject)
{
// 重置 ViewModel 中的角度修正值(会自动同步到 PathAnimationManager)
- ObjectRotationCorrection = 0.0;
- LogManager.Debug($"[选择物体] 已重置角度修正值为0度(新物体)");
+ ObjectRotationCorrection = LocalEulerRotationCorrection.Zero;
+ LogManager.Debug("[选择物体] 已重置角度修正值为0(新物体)");
}
else
{
LogManager.Debug($"[选择物体] 保持当前角度修正值(同一物体)");
}
+
+ // 显式同步一次到当前路径起点,避免依赖 setter 分支导致真实物体停留在 CAD 原位。
+ MoveAnimatedObjectToPathStart();
}
catch (Exception ex)
{
@@ -2151,8 +2163,8 @@ namespace NavisworksTransport.UI.WPF.ViewModels
}
// 2. 重置角度修正值为0
- ObjectRotationCorrection = 0.0;
- LogManager.Debug("[清除物体] 已重置角度修正值为0度");
+ ObjectRotationCorrection = LocalEulerRotationCorrection.Zero;
+ LogManager.Debug("[清除物体] 已重置角度修正值为0");
// 3. 重置属性
SelectedAnimatedObject = null;
@@ -2182,9 +2194,9 @@ namespace NavisworksTransport.UI.WPF.ViewModels
var dialog = new Views.EditRotationWindow(_objectRotationCorrection);
if (dialog.ShowDialog() == true)
{
- ObjectRotationCorrection = dialog.RotationAngle;
- LogManager.Info($"物体角度修正已更新: {_objectRotationCorrection:F1}°");
- UpdateMainStatus($"物体角度修正: {_objectRotationCorrection:F1}°");
+ ObjectRotationCorrection = dialog.RotationCorrection;
+ LogManager.Info($"物体角度修正已更新: {_objectRotationCorrection}");
+ UpdateMainStatus($"物体角度修正: {_objectRotationCorrection}");
// 应用角度修正到物体
UpdateObjectRotation();
@@ -2211,7 +2223,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
{
// 将角度修正传递给动画管理器
_pathAnimationManager.SetObjectRotationCorrection(_objectRotationCorrection);
- LogManager.Debug($"[角度修正] 已更新物体旋转: {_objectRotationCorrection:F1}°");
+ LogManager.Debug($"[角度修正] 已更新物体旋转: {_objectRotationCorrection}");
}
}
catch (Exception ex)
@@ -3191,7 +3203,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
IsVirtualObject = UseVirtualObject,
AnimatedObjectName = UseVirtualObject ? "虚拟物体" : SelectedAnimatedObject?.DisplayName,
DetectAllObjects = !IsManualCollisionTargetEnabled,
- ObjectRotationCorrection = _objectRotationCorrection,
+ ObjectRotationCorrection = _objectRotationCorrection.ZDegrees,
Description = $"{scenarioName}于 {DateTime.Now:yyyy-MM-dd HH:mm:ss}",
TestName = testName,
TestTime = DateTime.Now,
@@ -3356,7 +3368,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
UseVirtualObject ? VirtualObjectLengthInMeters * factor : (double?)null, // 米 → 模型单位
UseVirtualObject ? VirtualObjectWidthInMeters * factor : (double?)null,
UseVirtualObject ? VirtualObjectHeightInMeters * factor : (double?)null,
- _objectRotationCorrection,
+ _objectRotationCorrection.ZDegrees,
IsManualCollisionTargetEnabled
);
@@ -4302,54 +4314,56 @@ namespace NavisworksTransport.UI.WPF.ViewModels
}
///
- /// 计算旋转后的有效宽度和长度
+ /// 计算本地三轴预旋转后的有效尺寸
/// 返回值为模型单位(与Navisworks API一致)
///
- /// (有效宽度, 有效长度) - 模型单位
- private (double effectiveWidth, double effectiveLength) CalculateRotatedDimensions()
+ /// (沿路径, 垂直路径, 法线) - 模型单位
+ private (double effectiveAlongPath, double effectiveAcrossPath, double effectiveNormalToPath) CalculateRotatedDimensions()
{
- double baseLengthMeters, baseWidthMeters;
+ double forwardSize;
+ double sideSize;
+ double upSize;
+ ModelAxisConvention axisConvention;
if (UseVirtualObject)
{
- baseLengthMeters = VirtualObjectLengthInMeters;
- baseWidthMeters = VirtualObjectWidthInMeters;
+ double metersToUnits = UnitsConverter.GetMetersToUnitsConversionFactor();
+ forwardSize = VirtualObjectLengthInMeters * metersToUnits;
+ sideSize = VirtualObjectWidthInMeters * metersToUnits;
+ upSize = VirtualObjectHeightInMeters * metersToUnits;
+ axisConvention = ModelAxisConvention.CreateVirtualObjectAssetConvention();
}
else if (SelectedAnimatedObject != null)
{
- // 使用保存的原始尺寸(米),避免物体旋转后包围盒尺寸变化
- baseLengthMeters = _objectOriginalLength;
- baseWidthMeters = _objectOriginalWidth;
+ double metersToUnits = UnitsConverter.GetMetersToUnitsConversionFactor();
+ forwardSize = _objectOriginalLength * metersToUnits;
+ sideSize = _objectOriginalWidth * metersToUnits;
+ upSize = _objectOriginalHeight * metersToUnits;
+
+ var adapter = CoordinateSystemManager.Instance.CreateHostAdapter();
+ axisConvention = CurrentPathRoute?.PathType == NavisworksTransport.PathType.Rail
+ ? ModelAxisConvention.CreateRailAssetConvention()
+ : ModelAxisConvention.CreateDefaultForHost(adapter.HostType);
}
else
{
- return (0, 0);
+ return (0, 0, 0);
}
- // 转换为模型单位
- var metersToUnits = UnitsConverter.GetMetersToUnitsConversionFactor();
- double baseLength = baseLengthMeters * metersToUnits;
- double baseWidth = baseWidthMeters * metersToUnits;
+ Quaternion correctionQuaternion = CoordinateSystemManager.Instance.CreateHostAdapter()
+ .CreateCanonicalRotationCorrection(_objectRotationCorrection);
+ var result = RotatedObjectExtentHelper.CalculateProjectedSemanticExtents(
+ axisConvention,
+ forwardSize,
+ sideSize,
+ upSize,
+ correctionQuaternion);
+ double unitsToMeters = UnitsConverter.GetUnitsToMetersConversionFactor();
+ LogManager.Debug(
+ $"[角度修正] 旋转后尺寸: 原始({forwardSize * unitsToMeters:F2}m×{sideSize * unitsToMeters:F2}m×{upSize * unitsToMeters:F2}m) -> " +
+ $"有效({result.forwardExtent * unitsToMeters:F2}m×{result.sideExtent * unitsToMeters:F2}m×{result.upExtent * unitsToMeters:F2}m), 角度={_objectRotationCorrection}");
- // 如果没有角度修正,直接返回原始尺寸
- if (_objectRotationCorrection == 0.0)
- {
- return (baseWidth, baseLength);
- }
-
- // 计算旋转后的有效尺寸(投影)
- double rotationRad = _objectRotationCorrection * Math.PI / 180.0;
- double cos = Math.Abs(Math.Cos(rotationRad));
- double sin = Math.Abs(Math.Sin(rotationRad));
-
- // 旋转后的宽度 = |宽度*cos| + |长度*sin|
- double effectiveWidth = baseWidth * cos + baseLength * sin;
- // 旋转后的长度 = |长度*cos| + |宽度*sin|
- double effectiveLength = baseLength * cos + baseWidth * sin;
-
- LogManager.Debug($"[角度修正] 旋转后尺寸: 原始({baseLengthMeters:F2}m×{baseWidthMeters:F2}m) -> 有效({effectiveLength / metersToUnits:F2}m×{effectiveWidth / metersToUnits:F2}m), 角度={_objectRotationCorrection:F1}°");
-
- return (effectiveWidth, effectiveLength);
+ return result;
}
///
@@ -4368,7 +4382,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
}
// 计算旋转后的有效尺寸(考虑角度修正)
- (double effectiveWidth, double effectiveLength) = CalculateRotatedDimensions();
+ (double effectiveLength, double effectiveWidth, double effectiveHeight) = CalculateRotatedDimensions();
var renderPlugin = PathPointRenderPlugin.Instance;
if (renderPlugin == null)
@@ -4383,8 +4397,6 @@ namespace NavisworksTransport.UI.WPF.ViewModels
// 将安全间隙从米转换为模型单位
var metersToUnitsPassage = UnitsConverter.GetMetersToUnitsConversionFactor();
double safetyMargin = _safetyMarginInMeters * metersToUnitsPassage;
- double virtualObjectHeight = VirtualObjectHeightInMeters * metersToUnitsPassage;
-
if (UseVirtualObject)
{
// 虚拟物体的xyz定义:X方向 = 长度(前进方向),Y方向 = 宽度(侧面),Z方向 = 高度
@@ -4392,11 +4404,11 @@ namespace NavisworksTransport.UI.WPF.ViewModels
{
// 空中路径(空轨或吊装):高度上下都加间隙
passageAcrossPath = effectiveWidth + 2 * safetyMargin; // 旋转后宽度 + 2*间隙(垂直于路径方向)
- passageNormalToPath = virtualObjectHeight + 2 * safetyMargin; // Z方向(高度)+ 2*间隙(法线方向)
+ passageNormalToPath = effectiveHeight + 2 * safetyMargin; // 法线方向高度 + 2*间隙
passageAlongPath = effectiveLength; // 旋转后长度(沿路径方向)
// 吊装路径分段参数
passageNormalToPathVertical = effectiveLength + 2 * safetyMargin; // 垂直段:物体长度 + 2*间隙
- passageNormalToPathHorizontal = virtualObjectHeight + 2 * safetyMargin; // 水平段:物体高度 + 2*间隙
+ passageNormalToPathHorizontal = effectiveHeight + 2 * safetyMargin; // 水平段:物体高度 + 2*间隙
LogManager.Debug($"[通行空间可视化] 空中路径使用虚拟物体尺寸: 有效长度={effectiveLength / metersToUnitsPassage:F2}m, 有效宽度={effectiveWidth / metersToUnitsPassage:F2}m, 通行空间沿路径={passageAlongPath / metersToUnitsPassage:F2}m, 垂直路径={passageAcrossPath / metersToUnitsPassage:F2}m, 法线={passageNormalToPath / metersToUnitsPassage:F2}m");
}
else if (CurrentPathRoute.PathType == NavisworksTransport.PathType.Ground)
@@ -4404,7 +4416,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
// 地面路径:物体的长度方向(X轴,前进方向)朝向路径方向
// 通行空间沿路径方向 = X方向尺寸(长度),垂直于路径方向 = Y方向尺寸(宽度),高度上方加间隙(下方不需要,因为有地面)
passageAcrossPath = effectiveWidth + 2 * safetyMargin; // 旋转后宽度 + 2*间隙(垂直于路径方向)
- passageNormalToPath = virtualObjectHeight + safetyMargin; // Z方向(高度)+ 间隙(法线方向,只有上方)
+ passageNormalToPath = effectiveHeight + safetyMargin; // 法线方向高度 + 间隙(法线方向,只有上方)
passageAlongPath = effectiveLength; // 旋转后长度(沿路径方向)
// 地面路径不需要分段参数
passageNormalToPathVertical = passageNormalToPath;
@@ -4419,7 +4431,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
// - 轨下安装:顶面贴路径,底面留间隙
// - 轨上安装:底面贴路径,顶面留间隙
passageAcrossPath = effectiveWidth + 2 * safetyMargin; // 旋转后宽度 + 2*间隙(垂直于路径方向)
- passageNormalToPath = virtualObjectHeight + safetyMargin; // 法线方向单侧留间隙
+ passageNormalToPath = effectiveHeight + safetyMargin; // 法线方向单侧留间隙
passageAlongPath = effectiveLength; // 旋转后长度(沿路径方向)
// 空轨路径不需要分段参数
passageNormalToPathVertical = passageNormalToPath;
@@ -4429,26 +4441,18 @@ namespace NavisworksTransport.UI.WPF.ViewModels
}
else if (SelectedAnimatedObject != null)
{
- // 使用选择物体的局部轴语义尺寸(模型单位)
- var bbox = SelectedAnimatedObject.BoundingBox();
- var adapter = CoordinateSystemManager.Instance.CreateHostAdapter();
- var axisConvention = ModelAxisConvention.CreateDefaultForHost(adapter.HostType);
- double sizeAlongPath = axisConvention.GetForwardSize(bbox);
- double sizeAcrossPath = axisConvention.GetSideSize(bbox);
- double sizeNormalToPath = axisConvention.GetUpSize(bbox);
-
// 根据路径类型确定通行空间的尺寸
if (CurrentPathRoute.PathType == NavisworksTransport.PathType.Rail || CurrentPathRoute.PathType == NavisworksTransport.PathType.Hoisting)
{
// 空中路径(空轨或吊装):
// 高度上下都加间隙
passageAcrossPath = effectiveWidth + 2 * safetyMargin; // 旋转后宽度 + 2*间隙(垂直于路径方向)
- passageNormalToPath = sizeNormalToPath + 2 * safetyMargin; // 局部up方向高度 + 2*间隙(法线方向)
+ passageNormalToPath = effectiveHeight + 2 * safetyMargin; // 局部up方向高度 + 2*间隙(法线方向)
passageAlongPath = effectiveLength; // 旋转后长度(沿路径方向)
// 吊装路径分段参数
passageNormalToPathVertical = effectiveLength + 2 * safetyMargin; // 垂直段:物体长度 + 2*间隙
- passageNormalToPathHorizontal = sizeNormalToPath + 2 * safetyMargin; // 水平段:物体高度 + 2*间隙
- LogManager.Debug($"[通行空间可视化] 空中路径使用物体尺寸 ({SelectedAnimatedObject.DisplayName}): 局部长度={sizeAlongPath / metersToUnitsPassage:F2}m, 局部宽度={sizeAcrossPath / metersToUnitsPassage:F2}m, 局部高度={sizeNormalToPath / metersToUnitsPassage:F2}m, 有效长度={effectiveLength / metersToUnitsPassage:F2}m, 有效宽度={effectiveWidth / metersToUnitsPassage:F2}m, 通行空间沿路径={passageAlongPath / metersToUnitsPassage:F2}m, 垂直路径={passageAcrossPath / metersToUnitsPassage:F2}m, 法线={passageNormalToPath / metersToUnitsPassage:F2}m");
+ passageNormalToPathHorizontal = effectiveHeight + 2 * safetyMargin; // 水平段:物体高度 + 2*间隙
+ LogManager.Debug($"[通行空间可视化] 空中路径使用物体尺寸 ({SelectedAnimatedObject.DisplayName}): 有效长度={effectiveLength / metersToUnitsPassage:F2}m, 有效宽度={effectiveWidth / metersToUnitsPassage:F2}m, 有效高度={effectiveHeight / metersToUnitsPassage:F2}m, 通行空间沿路径={passageAlongPath / metersToUnitsPassage:F2}m, 垂直路径={passageAcrossPath / metersToUnitsPassage:F2}m, 法线={passageNormalToPath / metersToUnitsPassage:F2}m");
}
else if (CurrentPathRoute.PathType == NavisworksTransport.PathType.Ground)
{
@@ -4456,12 +4460,12 @@ namespace NavisworksTransport.UI.WPF.ViewModels
// 物体的长度方向(X轴,前进方向)朝向路径方向
// 通行空间沿路径方向 = X方向尺寸(长度),垂直于路径方向 = Y方向尺寸(宽度),高度上方加间隙(下方不需要,因为有地面)
passageAcrossPath = effectiveWidth + 2 * safetyMargin; // 旋转后宽度 + 2*间隙(垂直于路径方向)
- passageNormalToPath = sizeNormalToPath + safetyMargin; // 局部up方向高度 + 间隙(法线方向,只有上方)
+ passageNormalToPath = effectiveHeight + safetyMargin; // 局部up方向高度 + 间隙(法线方向,只有上方)
passageAlongPath = effectiveLength; // 旋转后长度(沿路径方向)
// 地面路径不需要分段参数
passageNormalToPathVertical = passageNormalToPath;
passageNormalToPathHorizontal = passageNormalToPath;
- LogManager.Debug($"[通行空间可视化] 地面路径使用物体尺寸 ({SelectedAnimatedObject.DisplayName}): 局部长度={sizeAlongPath / metersToUnitsPassage:F2}m, 局部宽度={sizeAcrossPath / metersToUnitsPassage:F2}m, 局部高度={sizeNormalToPath / metersToUnitsPassage:F2}m, 有效长度={effectiveLength / metersToUnitsPassage:F2}m, 有效宽度={effectiveWidth / metersToUnitsPassage:F2}m, 通行空间沿路径={passageAlongPath / metersToUnitsPassage:F2}m, 垂直路径={passageAcrossPath / metersToUnitsPassage:F2}m, 法线={passageNormalToPath / metersToUnitsPassage:F2}m");
+ LogManager.Debug($"[通行空间可视化] 地面路径使用物体尺寸 ({SelectedAnimatedObject.DisplayName}): 有效长度={effectiveLength / metersToUnitsPassage:F2}m, 有效宽度={effectiveWidth / metersToUnitsPassage:F2}m, 有效高度={effectiveHeight / metersToUnitsPassage:F2}m, 通行空间沿路径={passageAlongPath / metersToUnitsPassage:F2}m, 垂直路径={passageAcrossPath / metersToUnitsPassage:F2}m, 法线={passageNormalToPath / metersToUnitsPassage:F2}m");
}
else // Rail
{
@@ -4472,11 +4476,11 @@ namespace NavisworksTransport.UI.WPF.ViewModels
// - 轨下安装:顶面贴路径,底面留间隙
// - 轨上安装:底面贴路径,顶面留间隙
passageAcrossPath = effectiveWidth + 2 * safetyMargin; // 旋转后宽度 + 2*间隙(垂直于路径方向)
- passageNormalToPath = sizeNormalToPath + safetyMargin; // 局部up方向高度 + 单侧间隙(法线方向)
+ passageNormalToPath = effectiveHeight + safetyMargin; // 局部up方向高度 + 单侧间隙(法线方向)
passageAlongPath = effectiveLength; // 旋转后长度(沿路径方向)
passageNormalToPathVertical = passageNormalToPath;
passageNormalToPathHorizontal = passageNormalToPath;
- LogManager.Debug($"[通行空间可视化] 空轨路径使用物体尺寸 ({SelectedAnimatedObject.DisplayName}): 局部长度={sizeAlongPath / metersToUnitsPassage:F2}m, 局部宽度={sizeAcrossPath / metersToUnitsPassage:F2}m, 局部高度={sizeNormalToPath / metersToUnitsPassage:F2}m, 有效长度={effectiveLength / metersToUnitsPassage:F2}m, 有效宽度={effectiveWidth / metersToUnitsPassage:F2}m, 通行空间沿路径={passageAlongPath / metersToUnitsPassage:F2}m, 垂直路径={passageAcrossPath / metersToUnitsPassage:F2}m, 法线={passageNormalToPath / metersToUnitsPassage:F2}m");
+ LogManager.Debug($"[通行空间可视化] 空轨路径使用物体尺寸 ({SelectedAnimatedObject.DisplayName}): 有效长度={effectiveLength / metersToUnitsPassage:F2}m, 有效宽度={effectiveWidth / metersToUnitsPassage:F2}m, 有效高度={effectiveHeight / metersToUnitsPassage:F2}m, 通行空间沿路径={passageAlongPath / metersToUnitsPassage:F2}m, 垂直路径={passageAcrossPath / metersToUnitsPassage:F2}m, 法线={passageNormalToPath / metersToUnitsPassage:F2}m");
}
}
else
@@ -4950,8 +4954,8 @@ namespace NavisworksTransport.UI.WPF.ViewModels
}
// 重置角度修正值为0
- ObjectRotationCorrection = 0.0;
- LogManager.Debug("[重置状态] 已重置角度修正值为0度");
+ ObjectRotationCorrection = LocalEulerRotationCorrection.Zero;
+ LogManager.Debug("[重置状态] 已重置角度修正值为0");
// 清空选中对象
SelectedAnimatedObject = null;
@@ -5261,7 +5265,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
DetectAllObjects = !IsManualCollisionTargetEnabled,
// 角度修正配置
- ObjectRotationCorrection = _objectRotationCorrection,
+ ObjectRotationCorrection = _objectRotationCorrection.ZDegrees,
// 🔥 关联的检测记录ID
DetectionRecordId = detectionRecordId
@@ -5346,7 +5350,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
$"虚拟物体: {queueItem.IsVirtualObject}, " +
$"帧率: {queueItem.FrameRate}, " +
$"时长: {queueItem.DurationSeconds:F2}秒, " +
- $"角度修正: {queueItem.ObjectRotationCorrection:F1}°, " +
+ $"角度修正: {_objectRotationCorrection}, " +
$"ID: {itemId}");
UpdateMainStatus($"已添加到批处理队列: {CurrentPathRoute.Name}");
diff --git a/src/UI/WPF/Views/EditRotationWindow.xaml b/src/UI/WPF/Views/EditRotationWindow.xaml
index f23922e..5eda71f 100644
--- a/src/UI/WPF/Views/EditRotationWindow.xaml
+++ b/src/UI/WPF/Views/EditRotationWindow.xaml
@@ -1,7 +1,7 @@
-
+
-
-
+
+
-
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
HoistingLine
+ ,
+
+ ///
+ /// 真实物体原始位姿 X 轴样式(红色)
+ ///
+ TransformAxisX,
+
+ ///
+ /// 真实物体原始位姿 Y 轴样式(绿色)
+ ///
+ TransformAxisY,
+
+ ///
+ /// 真实物体原始位姿 Z 轴样式(蓝色)
+ ///
+ TransformAxisZ
}
///
@@ -2379,6 +2395,15 @@ namespace NavisworksTransport
case RenderStyleName.HoistingLine:
return new RenderStyle(Color.FromByteRGB(156, 39, 176), 0.8); // Material Purple吊装路径,20%透明
+ case RenderStyleName.TransformAxisX:
+ return new RenderStyle(Color.FromByteRGB(244, 67, 54), 0.9); // Material Red
+
+ case RenderStyleName.TransformAxisY:
+ return new RenderStyle(Color.FromByteRGB(76, 175, 80), 0.9); // Material Green
+
+ case RenderStyleName.TransformAxisZ:
+ return new RenderStyle(Color.FromByteRGB(33, 150, 243), 0.9); // Material Blue
+
default:
return new RenderStyle(Color.White, 1.0); // 默认白色,完全不透明
}
diff --git a/src/UI/WPF/ViewModels/SystemManagementViewModel.cs b/src/UI/WPF/ViewModels/SystemManagementViewModel.cs
index f24006e..432b116 100644
--- a/src/UI/WPF/ViewModels/SystemManagementViewModel.cs
+++ b/src/UI/WPF/ViewModels/SystemManagementViewModel.cs
@@ -1,9 +1,15 @@
using System;
using System.Collections.ObjectModel;
+using System.Collections.Generic;
+using System.Linq;
+using System.Numerics;
+using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Input;
using System.Windows.Threading;
+using Autodesk.Navisworks.Api;
+using ComApiBridge = Autodesk.Navisworks.Api.ComApi.ComApiBridge;
using NavisworksTransport.UI.WPF.Collections;
using NavisworksTransport.Core;
using NavisworksTransport.Core.Config;
@@ -21,6 +27,8 @@ namespace NavisworksTransport.UI.WPF.ViewModels
#region 私有字段
private readonly UIStateManager _uiStateManager;
+ private readonly Dictionary _documentFragmentDefaultUpAxes = new Dictionary(StringComparer.OrdinalIgnoreCase);
+ private readonly HashSet _documentRotationHintShown = new HashSet(StringComparer.OrdinalIgnoreCase);
// 日志管理字段
private ObservableCollection _logLevels;
@@ -174,10 +182,13 @@ namespace NavisworksTransport.UI.WPF.ViewModels
public ICommand TestVoxelPathFindingCommand { get; private set; }
public ICommand ReadTransformTestCommand { get; private set; }
public ICommand CoordinateSystemExplorerCommand { get; private set; }
+ public ICommand CaptureRealObjectTransformCommand { get; private set; }
+ public ICommand DetectFragmentDefaultUpCommand { get; private set; }
// 坐标系设置
public ObservableCollection CoordinateSystemOptions { get; private set; }
+ public ObservableCollection FragmentDefaultUpAxisOptions { get; private set; }
private string _selectedCoordinateSystem = "AutoDetect";
public string SelectedCoordinateSystem
@@ -199,6 +210,21 @@ namespace NavisworksTransport.UI.WPF.ViewModels
set => SetProperty(ref _currentCoordinateSystemInfo, value);
}
+ private string _selectedFragmentDefaultUpAxis = "Y";
+ public string SelectedFragmentDefaultUpAxis
+ {
+ get => _selectedFragmentDefaultUpAxis;
+ set
+ {
+ if (SetProperty(ref _selectedFragmentDefaultUpAxis, value))
+ {
+ SaveCurrentDocumentFragmentDefaultUpAxis(value);
+ UpdateMainStatus($"当前文档 fragment 默认Up已设置为: {value}");
+ LogManager.Info($"[fragment默认Up] 当前文档设置为: {value}");
+ }
+ }
+ }
+
#endregion
#region 构造函数
@@ -311,14 +337,18 @@ namespace NavisworksTransport.UI.WPF.ViewModels
TestVoxelPathFindingCommand = new RelayCommand(() => ExecuteTestVoxelPathFinding());
ReadTransformTestCommand = new RelayCommand(() => ExecuteReadTransformTest());
CoordinateSystemExplorerCommand = new RelayCommand(() => ExecuteCoordinateSystemExplorer());
+ CaptureRealObjectTransformCommand = new RelayCommand(() => ExecuteCaptureRealObjectTransform());
+ DetectFragmentDefaultUpCommand = new RelayCommand(() => ExecuteDetectFragmentDefaultUp());
// 初始化坐标系选项
CoordinateSystemOptions = new ObservableCollection { "AutoDetect", "ZUp", "YUp" };
+ FragmentDefaultUpAxisOptions = new ObservableCollection { "Y", "Z" };
// 从配置加载当前坐标系设置
var configType = ConfigManager.Instance.Current.CoordinateSystem?.Type ?? "AutoDetect";
_selectedCoordinateSystem = configType;
+ _selectedFragmentDefaultUpAxis = GetCurrentDocumentFragmentDefaultUpAxis();
// 订阅文档事件(用于自动检测坐标系)
SubscribeToDocumentEvents();
@@ -389,6 +419,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
// 更新显示信息
UpdateCoordinateSystemInfo();
+ ShowRootModelRotationHintIfNeeded();
LogManager.Info($"坐标系初始化完成: {configType}");
}
@@ -436,6 +467,156 @@ namespace NavisworksTransport.UI.WPF.ViewModels
{
var info = CoordinateSystemManager.Instance.GetCurrentInfo();
CurrentCoordinateSystemInfo = $"当前坐标系: {info}";
+ RefreshFragmentDefaultUpAxisSelectionFromCurrentDocument();
+ }
+
+ private void RefreshFragmentDefaultUpAxisSelectionFromCurrentDocument()
+ {
+ string resolvedAxis = GetCurrentDocumentFragmentDefaultUpAxis();
+ if (_selectedFragmentDefaultUpAxis != resolvedAxis)
+ {
+ SetProperty(ref _selectedFragmentDefaultUpAxis, resolvedAxis, nameof(SelectedFragmentDefaultUpAxis));
+ }
+ }
+
+ private string GetCurrentDocumentFragmentDefaultUpAxis()
+ {
+ string documentKey = GetCurrentDocumentKey();
+ if (!string.IsNullOrWhiteSpace(documentKey) &&
+ _documentFragmentDefaultUpAxes.TryGetValue(documentKey, out string storedAxis) &&
+ IsSupportedFragmentDefaultUpAxis(storedAxis))
+ {
+ return storedAxis;
+ }
+
+ return GetCurrentHostUpAxis();
+ }
+
+ private void SaveCurrentDocumentFragmentDefaultUpAxis(string axisName)
+ {
+ if (!IsSupportedFragmentDefaultUpAxis(axisName))
+ {
+ return;
+ }
+
+ string documentKey = GetCurrentDocumentKey();
+ if (string.IsNullOrWhiteSpace(documentKey))
+ {
+ return;
+ }
+
+ _documentFragmentDefaultUpAxes[documentKey] = axisName;
+ }
+
+ private string GetCurrentDocumentKey()
+ {
+ var doc = Autodesk.Navisworks.Api.Application.ActiveDocument;
+ if (doc == null || doc.IsClear)
+ {
+ return string.Empty;
+ }
+
+ if (!string.IsNullOrWhiteSpace(doc.FileName))
+ {
+ return doc.FileName;
+ }
+
+ return doc.Title ?? string.Empty;
+ }
+
+ private string GetCurrentHostUpAxis()
+ {
+ var doc = Autodesk.Navisworks.Api.Application.ActiveDocument;
+ if (doc != null && !doc.IsClear)
+ {
+ UnitVector3D worldUp = doc.CurrentViewpoint.Value.WorldUpVector;
+ if (!worldUp.IsZero)
+ {
+ return Math.Abs(worldUp.Y) >= Math.Abs(worldUp.Z) ? "Y" : "Z";
+ }
+
+ Vector3D documentUp = doc.UpVector;
+ if (!documentUp.IsZero)
+ {
+ return Math.Abs(documentUp.Y) >= Math.Abs(documentUp.Z) ? "Y" : "Z";
+ }
+ }
+
+ if (string.Equals(_selectedCoordinateSystem, "YUp", StringComparison.OrdinalIgnoreCase))
+ {
+ return "Y";
+ }
+
+ return "Z";
+ }
+
+ private static bool IsSupportedFragmentDefaultUpAxis(string axisName)
+ {
+ return string.Equals(axisName, "Y", StringComparison.OrdinalIgnoreCase) ||
+ string.Equals(axisName, "Z", StringComparison.OrdinalIgnoreCase);
+ }
+
+ private void ShowRootModelRotationHintIfNeeded()
+ {
+ string documentKey = GetCurrentDocumentKey();
+ if (string.IsNullOrWhiteSpace(documentKey) || _documentRotationHintShown.Contains(documentKey))
+ {
+ return;
+ }
+
+ var doc = Autodesk.Navisworks.Api.Application.ActiveDocument;
+ if (doc == null || doc.IsClear || doc.Models == null || doc.Models.Count == 0)
+ {
+ return;
+ }
+
+ if (!TryGetRootModelRotationHint(doc, out string hintMessage))
+ {
+ return;
+ }
+
+ _documentRotationHintShown.Add(documentKey);
+ LogManager.Info($"[模型整体旋转提示] {hintMessage}");
+ System.Windows.MessageBox.Show(
+ hintMessage,
+ "坐标系提示",
+ System.Windows.MessageBoxButton.OK,
+ System.Windows.MessageBoxImage.Information);
+ }
+
+ private static bool TryGetRootModelRotationHint(Document doc, out string hintMessage)
+ {
+ hintMessage = null;
+ if (doc == null || doc.Models == null || doc.Models.Count == 0)
+ {
+ return false;
+ }
+
+ const double minAngleDegrees = 1.0;
+
+ for (int i = 0; i < doc.Models.Count; i++)
+ {
+ Model model = doc.Models[i];
+ ModelItem rootItem = model?.RootItem;
+ if (rootItem == null)
+ {
+ continue;
+ }
+
+ Transform3DComponents components = rootItem.Transform.Factor();
+ var axisAngle = components.Rotation.ToAxisAndAngle();
+ double angleDegrees = Math.Abs(axisAngle.Angle) * 180.0 / Math.PI;
+ if (angleDegrees < minAngleDegrees)
+ {
+ continue;
+ }
+
+ hintMessage =
+ $"检测到根模型存在整体旋转(约 {angleDegrees:F1}°),模型可能经过坐标系转换;如后续真实物体姿态异常,请检查 Fragment默认Up 设置。";
+ return true;
+ }
+
+ return false;
}
#region 文档事件处理
@@ -566,6 +747,11 @@ namespace NavisworksTransport.UI.WPF.ViewModels
}
});
}
+
+ _ = _uiStateManager.ExecuteUIUpdateAsync(() =>
+ {
+ RefreshFragmentDefaultUpAxisSelectionFromCurrentDocument();
+ });
}
#endregion
@@ -1315,6 +1501,541 @@ namespace NavisworksTransport.UI.WPF.ViewModels
}, "体素路径规划测试");
}
+ ///
+ /// 捕获选中的真实物体,显示原始变换信息并可视化三轴。
+ ///
+ private void ExecuteCaptureRealObjectTransform()
+ {
+ SafeExecute(() =>
+ {
+ var doc = Autodesk.Navisworks.Api.Application.ActiveDocument;
+ if (doc == null || doc.IsClear)
+ {
+ System.Windows.MessageBox.Show(
+ "没有活动的文档!请先打开一个模型。",
+ "错误",
+ System.Windows.MessageBoxButton.OK,
+ System.Windows.MessageBoxImage.Error);
+ return;
+ }
+
+ var selection = doc.CurrentSelection.SelectedItems;
+ if (selection.Count == 0)
+ {
+ ClearCapturedRealObjectTransformVisualization();
+ System.Windows.MessageBox.Show(
+ "请先选择一个真实物体!",
+ "提示",
+ System.Windows.MessageBoxButton.OK,
+ System.Windows.MessageBoxImage.Information);
+ return;
+ }
+
+ ModelItem item = selection[0];
+ BoundingBox3D bounds = item.BoundingBox();
+ Point3D boundsCenter = bounds.Center;
+ TransformDiagnosticInfo selectedInfo = CreateTransformDiagnosticInfo(item, "选中节点");
+ List geometryDescendants = CollectGeometryDescendants(item);
+ TransformDiagnosticInfo geometryInfo = geometryDescendants.Count > 0
+ ? CreateTransformDiagnosticInfo(geometryDescendants[0], "首个几何后代")
+ : null;
+ FragmentOrientationAnalysis fragmentAnalysis = AnalyzeFragmentOrientations(item);
+
+ Quaternion visualizationQuaternion = fragmentAnalysis != null && fragmentAnalysis.HasRepresentativeOrientation
+ ? fragmentAnalysis.RepresentativeRotation
+ : geometryInfo?.RotationQuaternion ?? selectedInfo.RotationQuaternion;
+
+ double axisLength = GetTransformAxisVisualizationLength(bounds);
+ RenderCapturedRealObjectTransformAxes(boundsCenter, visualizationQuaternion, axisLength);
+
+ var info = new StringBuilder();
+ info.AppendLine("=== 真实物体原始变换信息 ===");
+ info.AppendLine();
+ info.AppendLine($"对象: {item.DisplayName}");
+ info.AppendLine($"ClassName: {item.ClassName}");
+ info.AppendLine($"HasGeometry: {item.HasGeometry}");
+ info.AppendLine();
+ info.AppendLine("【包围盒】");
+ info.AppendLine($"Min = ({bounds.Min.X:F3}, {bounds.Min.Y:F3}, {bounds.Min.Z:F3})");
+ info.AppendLine($"Max = ({bounds.Max.X:F3}, {bounds.Max.Y:F3}, {bounds.Max.Z:F3})");
+ info.AppendLine($"Center = ({boundsCenter.X:F3}, {boundsCenter.Y:F3}, {boundsCenter.Z:F3})");
+ info.AppendLine();
+
+ AppendTransformDiagnosticInfo(info, selectedInfo);
+
+ info.AppendLine("【几何后代】");
+ info.AppendLine($"几何后代数量 = {geometryDescendants.Count}");
+ if (geometryInfo != null)
+ {
+ AppendTransformDiagnosticInfo(info, geometryInfo);
+ }
+ else
+ {
+ info.AppendLine("未找到 HasGeometry=True 的后代节点");
+ info.AppendLine();
+ }
+
+ AppendFragmentAnalysis(info, fragmentAnalysis);
+ info.AppendLine();
+ info.AppendLine("【可视化说明】");
+ info.AppendLine($"三轴原点使用包围盒中心: ({boundsCenter.X:F3}, {boundsCenter.Y:F3}, {boundsCenter.Z:F3})");
+ info.AppendLine($"轴长度 = {axisLength:F3}");
+ info.AppendLine($"三轴方向来源 = {(fragmentAnalysis != null && fragmentAnalysis.HasRepresentativeOrientation ? "fragment代表姿态" : geometryInfo != null ? "首个几何后代Transform" : "选中节点Transform")}");
+ info.AppendLine("红色=X轴,绿色=Y轴,蓝色=Z轴");
+
+ string message = info.ToString();
+ LogManager.Info("[真实物体原始位姿]\n" + message);
+
+ var resultDialog = new NavisworksTransport.UI.WPF.Views.CoordinateSystemResultDialog
+ {
+ Title = "真实物体原始变换信息",
+ ResultText = message
+ };
+ DialogHelper.SetOwnerSafely(resultDialog);
+ resultDialog.ShowDialog();
+
+ UpdateMainStatus($"已捕获真实物体原始位姿:{item.DisplayName}");
+ }, "捕获真实物体原始位姿");
+ }
+
+ private void ExecuteDetectFragmentDefaultUp()
+ {
+ SafeExecute(() =>
+ {
+ var doc = Autodesk.Navisworks.Api.Application.ActiveDocument;
+ if (doc == null || doc.IsClear)
+ {
+ System.Windows.MessageBox.Show(
+ "没有活动的文档!请先打开一个模型。",
+ "错误",
+ System.Windows.MessageBoxButton.OK,
+ System.Windows.MessageBoxImage.Error);
+ return;
+ }
+
+ var selection = doc.CurrentSelection.SelectedItems;
+ if (selection.Count == 0)
+ {
+ System.Windows.MessageBox.Show(
+ "请先选择一个明显水平放置的真实物体,再执行 fragment 默认Up检测。",
+ "提示",
+ System.Windows.MessageBoxButton.OK,
+ System.Windows.MessageBoxImage.Information);
+ return;
+ }
+
+ ModelItem item = selection[0];
+ FragmentOrientationAnalysis fragmentAnalysis = AnalyzeFragmentOrientations(item);
+ if (fragmentAnalysis == null || !fragmentAnalysis.HasRepresentativeOrientation)
+ {
+ System.Windows.MessageBox.Show(
+ "未能读取选中物体的 fragment 代表姿态,无法检测默认Up。",
+ "提示",
+ System.Windows.MessageBoxButton.OK,
+ System.Windows.MessageBoxImage.Warning);
+ return;
+ }
+
+ string hostUpAxis = GetCurrentHostUpAxis();
+ Vector3 hostUpVector = hostUpAxis == "Y" ? Vector3.UnitY : Vector3.UnitZ;
+ double yAlignment = Math.Abs(Vector3.Dot(fragmentAnalysis.RepresentativeYAxis, hostUpVector));
+ double zAlignment = Math.Abs(Vector3.Dot(fragmentAnalysis.RepresentativeZAxis, hostUpVector));
+ string detectedAxis = zAlignment > yAlignment ? "Z" : "Y";
+ string currentAxis = SelectedFragmentDefaultUpAxis;
+ LogManager.Info(
+ $"[fragment默认Up检测] 对象={item.DisplayName}, 宿主Up={hostUpAxis}, 当前值={currentAxis}, 检测值={detectedAxis}, " +
+ $"Y对齐度={yAlignment:F4}, Z对齐度={zAlignment:F4}");
+
+ if (string.Equals(detectedAxis, currentAxis, StringComparison.OrdinalIgnoreCase))
+ {
+ System.Windows.MessageBox.Show(
+ $"检测完成:当前文档 Fragment默认Up 已是 {currentAxis}。",
+ "Fragment默认Up检测",
+ System.Windows.MessageBoxButton.OK,
+ System.Windows.MessageBoxImage.Information);
+ UpdateMainStatus($"Fragment默认Up检测完成:当前值 {currentAxis} 已匹配");
+ return;
+ }
+
+ SetProperty(ref _selectedFragmentDefaultUpAxis, detectedAxis, nameof(SelectedFragmentDefaultUpAxis));
+ SaveCurrentDocumentFragmentDefaultUpAxis(detectedAxis);
+
+ System.Windows.MessageBox.Show(
+ $"检测完成:当前文档 Fragment默认Up 已自动改为 {detectedAxis}。",
+ "Fragment默认Up检测",
+ System.Windows.MessageBoxButton.OK,
+ System.Windows.MessageBoxImage.Information);
+
+ UpdateMainStatus($"当前文档 fragment 默认Up已自动更新为: {detectedAxis}");
+ LogManager.Info($"[fragment默认Up] 检测后自动更新为: {detectedAxis}");
+ }, "检测Fragment默认Up");
+ }
+
+ private void ClearCapturedRealObjectTransformVisualization()
+ {
+ var renderPlugin = PathPointRenderPlugin.Instance;
+ if (renderPlugin == null)
+ {
+ return;
+ }
+
+ renderPlugin.ClearRailBaseline("system_transform_axis_x");
+ renderPlugin.ClearRailBaseline("system_transform_axis_y");
+ renderPlugin.ClearRailBaseline("system_transform_axis_z");
+ }
+
+ private void RenderCapturedRealObjectTransformAxes(Point3D origin, Quaternion rotationQuaternion, double axisLength)
+ {
+ var renderPlugin = PathPointRenderPlugin.Instance;
+ if (renderPlugin == null)
+ {
+ return;
+ }
+
+ ClearCapturedRealObjectTransformVisualization();
+
+ RenderTransformAxis(renderPlugin, "system_transform_axis_x", origin, Vector3.Transform(Vector3.UnitX, rotationQuaternion), axisLength, RenderStyleName.TransformAxisX);
+ RenderTransformAxis(renderPlugin, "system_transform_axis_y", origin, Vector3.Transform(Vector3.UnitY, rotationQuaternion), axisLength, RenderStyleName.TransformAxisY);
+ RenderTransformAxis(renderPlugin, "system_transform_axis_z", origin, Vector3.Transform(Vector3.UnitZ, rotationQuaternion), axisLength, RenderStyleName.TransformAxisZ);
+ }
+
+ private static void RenderTransformAxis(
+ PathPointRenderPlugin renderPlugin,
+ string pathId,
+ Point3D origin,
+ Vector3 direction,
+ double axisLength,
+ RenderStyleName renderStyleName)
+ {
+ if (direction.LengthSquared() < 1e-8f)
+ {
+ return;
+ }
+
+ Vector3 normalizedDirection = Vector3.Normalize(direction);
+ Point3D endPoint = new Point3D(
+ origin.X + normalizedDirection.X * (float)axisLength,
+ origin.Y + normalizedDirection.Y * (float)axisLength,
+ origin.Z + normalizedDirection.Z * (float)axisLength);
+
+ renderPlugin.RenderRailBaseline(
+ pathId,
+ new System.Collections.Generic.List { origin, endPoint },
+ renderStyleName);
+ }
+
+ private static double GetTransformAxisVisualizationLength(BoundingBox3D bounds)
+ {
+ double xSpan = bounds.Max.X - bounds.Min.X;
+ double ySpan = bounds.Max.Y - bounds.Min.Y;
+ double zSpan = bounds.Max.Z - bounds.Min.Z;
+ double maxSpan = Math.Max(xSpan, Math.Max(ySpan, zSpan));
+ double minLength = UnitsConverter.ConvertFromMeters(0.5);
+ return Math.Max(minLength, maxSpan * 0.3);
+ }
+
+ private static Quaternion ToQuaternion(Rotation3D rotation)
+ {
+ var axisAngle = rotation.ToAxisAndAngle();
+ if (axisAngle.Axis.IsZero || Math.Abs(axisAngle.Angle) < 1e-9)
+ {
+ return Quaternion.Identity;
+ }
+
+ return Quaternion.Normalize(Quaternion.CreateFromAxisAngle(
+ new Vector3((float)axisAngle.Axis.X, (float)axisAngle.Axis.Y, (float)axisAngle.Axis.Z),
+ (float)axisAngle.Angle));
+ }
+
+ private static TransformDiagnosticInfo CreateTransformDiagnosticInfo(ModelItem item, string label)
+ {
+ Transform3DComponents components = item.Transform.Factor();
+ var axisAngle = components.Rotation.ToAxisAndAngle();
+ Quaternion rotationQuaternion = ToQuaternion(components.Rotation);
+
+ return new TransformDiagnosticInfo
+ {
+ Label = label,
+ DisplayName = item.DisplayName,
+ ClassName = item.ClassName,
+ HasGeometry = item.HasGeometry,
+ Translation = components.Translation,
+ Scale = components.Scale,
+ RotationAxis = axisAngle.Axis,
+ RotationAngleRadians = axisAngle.Angle,
+ RotationQuaternion = rotationQuaternion,
+ HostXAxis = Vector3.Normalize(Vector3.Transform(Vector3.UnitX, rotationQuaternion)),
+ HostYAxis = Vector3.Normalize(Vector3.Transform(Vector3.UnitY, rotationQuaternion)),
+ HostZAxis = Vector3.Normalize(Vector3.Transform(Vector3.UnitZ, rotationQuaternion))
+ };
+ }
+
+ private static void AppendTransformDiagnosticInfo(StringBuilder info, TransformDiagnosticInfo diagnostic)
+ {
+ info.AppendLine($"【{diagnostic.Label}】");
+ info.AppendLine($"对象 = {diagnostic.DisplayName}");
+ info.AppendLine($"ClassName = {diagnostic.ClassName}");
+ info.AppendLine($"HasGeometry = {diagnostic.HasGeometry}");
+ info.AppendLine($"Translation = ({diagnostic.Translation.X:F3}, {diagnostic.Translation.Y:F3}, {diagnostic.Translation.Z:F3})");
+ info.AppendLine($"Scale = ({diagnostic.Scale.X:F3}, {diagnostic.Scale.Y:F3}, {diagnostic.Scale.Z:F3})");
+ info.AppendLine($"Rotation.Axis = ({diagnostic.RotationAxis.X:F4}, {diagnostic.RotationAxis.Y:F4}, {diagnostic.RotationAxis.Z:F4})");
+ info.AppendLine($"Rotation.Angle = {diagnostic.RotationAngleRadians:F6} rad ({diagnostic.RotationAngleRadians * 180.0 / Math.PI:F2}°)");
+ info.AppendLine($"X轴 = ({diagnostic.HostXAxis.X:F4}, {diagnostic.HostXAxis.Y:F4}, {diagnostic.HostXAxis.Z:F4})");
+ info.AppendLine($"Y轴 = ({diagnostic.HostYAxis.X:F4}, {diagnostic.HostYAxis.Y:F4}, {diagnostic.HostYAxis.Z:F4})");
+ info.AppendLine($"Z轴 = ({diagnostic.HostZAxis.X:F4}, {diagnostic.HostZAxis.Y:F4}, {diagnostic.HostZAxis.Z:F4})");
+ info.AppendLine();
+ }
+
+ private static List CollectGeometryDescendants(ModelItem root)
+ {
+ var result = new List();
+ if (root == null)
+ {
+ return result;
+ }
+
+ CollectGeometryDescendantsRecursive(root, result);
+ return result;
+ }
+
+ private static void CollectGeometryDescendantsRecursive(ModelItem item, List result)
+ {
+ if (item == null)
+ {
+ return;
+ }
+
+ if (item.HasGeometry)
+ {
+ result.Add(item);
+ }
+
+ foreach (ModelItem child in item.Children)
+ {
+ CollectGeometryDescendantsRecursive(child, result);
+ }
+ }
+
+ private static FragmentOrientationAnalysis AnalyzeFragmentOrientations(ModelItem item)
+ {
+ if (item == null)
+ {
+ return null;
+ }
+
+ Autodesk.Navisworks.Api.ModelItemCollection collection = new Autodesk.Navisworks.Api.ModelItemCollection { item };
+ var comSelection = ComApiBridge.ToInwOpSelection(collection);
+ List fragments = null;
+ try
+ {
+ fragments = GeometryHelper.GetAllFragments(comSelection);
+ var validMatrices = fragments
+ .Where(f => f.TransformMatrix != null && f.TransformMatrix.Length == 16)
+ .Select(f => f.TransformMatrix)
+ .ToList();
+
+ var analysis = new FragmentOrientationAnalysis
+ {
+ FragmentCount = fragments.Count,
+ ValidTransformCount = validMatrices.Count
+ };
+
+ if (validMatrices.Count == 0)
+ {
+ return analysis;
+ }
+
+ Vector3 referenceX = NormalizeOrUnitX(GetMatrixAxis(validMatrices[0], 0));
+ Vector3 referenceY = NormalizeOrUnitY(GetMatrixAxis(validMatrices[0], 1));
+ Vector3 referenceZ = NormalizeOrUnitZ(GetMatrixAxis(validMatrices[0], 2));
+
+ Vector3 sumX = referenceX;
+ Vector3 sumY = referenceY;
+ Vector3 sumZ = referenceZ;
+ double xDotSum = 1.0;
+ double yDotSum = 1.0;
+ double zDotSum = 1.0;
+
+ for (int i = 1; i < validMatrices.Count; i++)
+ {
+ Vector3 axisX = NormalizeWithReference(GetMatrixAxis(validMatrices[i], 0), referenceX);
+ Vector3 axisY = NormalizeWithReference(GetMatrixAxis(validMatrices[i], 1), referenceY);
+ Vector3 axisZ = NormalizeWithReference(GetMatrixAxis(validMatrices[i], 2), referenceZ);
+
+ xDotSum += Math.Abs(Vector3.Dot(axisX, referenceX));
+ yDotSum += Math.Abs(Vector3.Dot(axisY, referenceY));
+ zDotSum += Math.Abs(Vector3.Dot(axisZ, referenceZ));
+
+ sumX += axisX;
+ sumY += axisY;
+ sumZ += axisZ;
+ }
+
+ Vector3 representativeX = NormalizeOrUnitX(sumX);
+ Vector3 representativeY = sumY - Vector3.Dot(sumY, representativeX) * representativeX;
+ representativeY = NormalizeOrUnitY(representativeY);
+ Vector3 representativeZ = Vector3.Normalize(Vector3.Cross(representativeX, representativeY));
+ if (Vector3.Dot(representativeZ, NormalizeOrUnitZ(sumZ)) < 0)
+ {
+ representativeZ = -representativeZ;
+ }
+ representativeY = Vector3.Normalize(Vector3.Cross(representativeZ, representativeX));
+
+ Matrix4x4 linear = new Matrix4x4(
+ representativeX.X, representativeY.X, representativeZ.X, 0f,
+ representativeX.Y, representativeY.Y, representativeZ.Y, 0f,
+ representativeX.Z, representativeY.Z, representativeZ.Z, 0f,
+ 0f, 0f, 0f, 1f);
+
+ analysis.HasRepresentativeOrientation = true;
+ analysis.RepresentativeRotation = Quaternion.Normalize(Quaternion.CreateFromRotationMatrix(linear));
+ analysis.RepresentativeXAxis = representativeX;
+ analysis.RepresentativeYAxis = representativeY;
+ analysis.RepresentativeZAxis = representativeZ;
+ analysis.AverageXAxisConsistency = xDotSum / validMatrices.Count;
+ analysis.AverageYAxisConsistency = yDotSum / validMatrices.Count;
+ analysis.AverageZAxisConsistency = zDotSum / validMatrices.Count;
+ analysis.SampleTranslation = GetMatrixTranslation(validMatrices[0]);
+ return analysis;
+ }
+ finally
+ {
+ if (fragments != null)
+ {
+ foreach (var fragment in fragments)
+ {
+ if (fragment?.Fragment != null)
+ {
+ try
+ {
+ Marshal.ReleaseComObject(fragment.Fragment);
+ }
+ catch
+ {
+ }
+ }
+ }
+ }
+
+ if (comSelection != null)
+ {
+ try
+ {
+ Marshal.ReleaseComObject(comSelection);
+ }
+ catch
+ {
+ }
+ }
+ }
+ }
+
+ private static void AppendFragmentAnalysis(StringBuilder info, FragmentOrientationAnalysis analysis)
+ {
+ info.AppendLine("【Fragment 姿态分析】");
+ if (analysis == null)
+ {
+ info.AppendLine("未能执行 fragment 分析");
+ info.AppendLine();
+ return;
+ }
+
+ info.AppendLine($"Fragment 总数 = {analysis.FragmentCount}");
+ info.AppendLine($"有效变换矩阵数 = {analysis.ValidTransformCount}");
+ if (!analysis.HasRepresentativeOrientation)
+ {
+ info.AppendLine("未得到可用的 fragment 代表姿态");
+ info.AppendLine();
+ return;
+ }
+
+ info.AppendLine($"示例平移 = ({analysis.SampleTranslation.X:F3}, {analysis.SampleTranslation.Y:F3}, {analysis.SampleTranslation.Z:F3})");
+ info.AppendLine($"代表X轴 = ({analysis.RepresentativeXAxis.X:F4}, {analysis.RepresentativeXAxis.Y:F4}, {analysis.RepresentativeXAxis.Z:F4})");
+ info.AppendLine($"代表Y轴 = ({analysis.RepresentativeYAxis.X:F4}, {analysis.RepresentativeYAxis.Y:F4}, {analysis.RepresentativeYAxis.Z:F4})");
+ info.AppendLine($"代表Z轴 = ({analysis.RepresentativeZAxis.X:F4}, {analysis.RepresentativeZAxis.Y:F4}, {analysis.RepresentativeZAxis.Z:F4})");
+ info.AppendLine($"X轴一致性 = {analysis.AverageXAxisConsistency:F4}");
+ info.AppendLine($"Y轴一致性 = {analysis.AverageYAxisConsistency:F4}");
+ info.AppendLine($"Z轴一致性 = {analysis.AverageZAxisConsistency:F4}");
+ info.AppendLine();
+ }
+
+ private static Vector3 GetMatrixAxis(double[] matrix, int axisIndex)
+ {
+ switch (axisIndex)
+ {
+ case 0:
+ return new Vector3((float)matrix[0], (float)matrix[1], (float)matrix[2]);
+ case 1:
+ return new Vector3((float)matrix[4], (float)matrix[5], (float)matrix[6]);
+ case 2:
+ return new Vector3((float)matrix[8], (float)matrix[9], (float)matrix[10]);
+ default:
+ throw new ArgumentOutOfRangeException(nameof(axisIndex));
+ }
+ }
+
+ private static Vector3 GetMatrixTranslation(double[] matrix)
+ {
+ return new Vector3((float)matrix[12], (float)matrix[13], (float)matrix[14]);
+ }
+
+ private static Vector3 NormalizeWithReference(Vector3 axis, Vector3 reference)
+ {
+ Vector3 normalized = NormalizeOrFallback(axis, reference);
+ return Vector3.Dot(normalized, reference) < 0 ? -normalized : normalized;
+ }
+
+ private static Vector3 NormalizeOrUnitX(Vector3 value)
+ {
+ return NormalizeOrFallback(value, Vector3.UnitX);
+ }
+
+ private static Vector3 NormalizeOrUnitY(Vector3 value)
+ {
+ return NormalizeOrFallback(value, Vector3.UnitY);
+ }
+
+ private static Vector3 NormalizeOrUnitZ(Vector3 value)
+ {
+ return NormalizeOrFallback(value, Vector3.UnitZ);
+ }
+
+ private static Vector3 NormalizeOrFallback(Vector3 value, Vector3 fallback)
+ {
+ return value.LengthSquared() < 1e-8f ? fallback : Vector3.Normalize(value);
+ }
+
+ private sealed class TransformDiagnosticInfo
+ {
+ public string Label { get; set; }
+ public string DisplayName { get; set; }
+ public string ClassName { get; set; }
+ public bool HasGeometry { get; set; }
+ public Vector3D Translation { get; set; }
+ public Vector3D Scale { get; set; }
+ public Vector3D RotationAxis { get; set; }
+ public double RotationAngleRadians { get; set; }
+ public Quaternion RotationQuaternion { get; set; }
+ public Vector3 HostXAxis { get; set; }
+ public Vector3 HostYAxis { get; set; }
+ public Vector3 HostZAxis { get; set; }
+ }
+
+ private sealed class FragmentOrientationAnalysis
+ {
+ public int FragmentCount { get; set; }
+ public int ValidTransformCount { get; set; }
+ public bool HasRepresentativeOrientation { get; set; }
+ public Quaternion RepresentativeRotation { get; set; }
+ public Vector3 RepresentativeXAxis { get; set; }
+ public Vector3 RepresentativeYAxis { get; set; }
+ public Vector3 RepresentativeZAxis { get; set; }
+ public double AverageXAxisConsistency { get; set; }
+ public double AverageYAxisConsistency { get; set; }
+ public double AverageZAxisConsistency { get; set; }
+ public Vector3 SampleTranslation { get; set; }
+ }
+
///
/// 读取选中对象的Transform信息,并测试旋转
///
@@ -1864,4 +2585,4 @@ namespace NavisworksTransport.UI.WPF.ViewModels
#endregion
}
-}
\ No newline at end of file
+}
diff --git a/src/UI/WPF/Views/SystemManagementView.xaml b/src/UI/WPF/Views/SystemManagementView.xaml
index 6b05f58..94f3a06 100644
--- a/src/UI/WPF/Views/SystemManagementView.xaml
+++ b/src/UI/WPF/Views/SystemManagementView.xaml
@@ -178,6 +178,29 @@ NavisworksTransport 系统管理页签视图 - 采用与其他页签一致的Nav
ToolTip="AutoDetect: 自动检测, ZUp: Z轴向上, YUp: Y轴向上"/>
+
+
+
+
+
+
+
+
+
+
-
-
+ ToolTip="读取选中真实物体的原始Transform,并在3D视图中可视化X/Y/Z三轴"/>
diff --git a/src/Utils/CoordinateSystem/FragmentRepresentativePoseHelper.cs b/src/Utils/CoordinateSystem/FragmentRepresentativePoseHelper.cs
new file mode 100644
index 0000000..98a9ee0
--- /dev/null
+++ b/src/Utils/CoordinateSystem/FragmentRepresentativePoseHelper.cs
@@ -0,0 +1,251 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Numerics;
+using Autodesk.Navisworks.Api;
+using ComApi = Autodesk.Navisworks.Api.Interop.ComApi;
+using ComApiBridge = Autodesk.Navisworks.Api.ComApi.ComApiBridge;
+
+namespace NavisworksTransport.Utils.CoordinateSystem
+{
+ ///
+ /// Fragment 代表姿态辅助工具。
+ ///
+ /// 语义:
+ /// - 从 fragment 的 world 矩阵中提取旋转
+ /// - 对多个 fragment 旋转做同半球加权平均
+ /// - 返回一个可作为真实物体参考位姿的代表旋转
+ ///
+ public static class FragmentRepresentativePoseHelper
+ {
+ private const float AxisEpsilon = 1e-6f;
+
+ ///
+ /// 从 ModelItem 的 fragment 几何中提取代表旋转。
+ ///
+ public static bool TryGetRepresentativeRotation(ModelItem item, out Quaternion representativeRotation)
+ {
+ representativeRotation = Quaternion.Identity;
+
+ if (item == null)
+ {
+ return false;
+ }
+
+ try
+ {
+ var selection = ComApiBridge.ToInwOpSelection(new ModelItemCollection { item });
+ var fragmentInfos = GeometryHelper.GetAllFragments(selection);
+ return TryGetRepresentativeRotation(
+ fragmentInfos.Select(fragmentInfo => fragmentInfo.TransformMatrix),
+ out representativeRotation);
+ }
+ catch (Exception ex)
+ {
+ LogManager.Warning($"[fragment代表姿态] 从ModelItem提取失败: {ex.Message}");
+ return false;
+ }
+ }
+
+ ///
+ /// 从 fragment 矩阵集合中提取代表旋转。
+ ///
+ public static bool TryGetRepresentativeRotation(
+ IEnumerable fragmentMatrices,
+ out Quaternion representativeRotation)
+ {
+ representativeRotation = Quaternion.Identity;
+
+ if (fragmentMatrices == null)
+ {
+ return false;
+ }
+
+ var fragmentRotations = new List();
+ foreach (double[] fragmentMatrix in fragmentMatrices)
+ {
+ if (TryExtractRotationFromFragmentMatrix(fragmentMatrix, out Quaternion fragmentRotation))
+ {
+ fragmentRotations.Add(fragmentRotation);
+ }
+ }
+
+ return TryGetRepresentativeRotation(fragmentRotations, out representativeRotation);
+ }
+
+ ///
+ /// 从 fragment 旋转集合中提取代表旋转。
+ ///
+ public static bool TryGetRepresentativeRotation(
+ IEnumerable fragmentRotations,
+ out Quaternion representativeRotation)
+ {
+ representativeRotation = Quaternion.Identity;
+
+ if (fragmentRotations == null)
+ {
+ return false;
+ }
+
+ var rotations = fragmentRotations
+ .Where(rotation => TryNormalize(rotation, out _))
+ .Select(rotation => Quaternion.Normalize(rotation))
+ .ToList();
+
+ if (rotations.Count == 0)
+ {
+ return false;
+ }
+
+ Quaternion seed = rotations[0];
+ Vector4 sum = Vector4.Zero;
+ foreach (Quaternion rotation in rotations)
+ {
+ Quaternion aligned = Quaternion.Dot(seed, rotation) < 0.0f
+ ? new Quaternion(-rotation.X, -rotation.Y, -rotation.Z, -rotation.W)
+ : rotation;
+
+ sum += new Vector4(aligned.X, aligned.Y, aligned.Z, aligned.W);
+ }
+
+ if (sum.LengthSquared() < AxisEpsilon)
+ {
+ return false;
+ }
+
+ representativeRotation = Quaternion.Normalize(new Quaternion(sum.X, sum.Y, sum.Z, sum.W));
+ return true;
+ }
+
+ ///
+ /// 从 fragment 的 4x4 world matrix 中提取旋转。
+ /// 仅使用旋转部分,忽略平移。
+ ///
+ public static bool TryExtractRotationFromFragmentMatrix(double[] fragmentMatrix, out Quaternion rotation)
+ {
+ rotation = Quaternion.Identity;
+
+ if (fragmentMatrix == null || fragmentMatrix.Length != 16)
+ {
+ return false;
+ }
+
+ Vector3 axisX = new Vector3(
+ (float)fragmentMatrix[0],
+ (float)fragmentMatrix[1],
+ (float)fragmentMatrix[2]);
+ Vector3 axisY = new Vector3(
+ (float)fragmentMatrix[4],
+ (float)fragmentMatrix[5],
+ (float)fragmentMatrix[6]);
+ Vector3 axisZ = new Vector3(
+ (float)fragmentMatrix[8],
+ (float)fragmentMatrix[9],
+ (float)fragmentMatrix[10]);
+
+ if (!TryNormalize(axisX, out axisX) ||
+ !TryNormalize(axisY, out axisY) ||
+ !TryNormalize(axisZ, out axisZ))
+ {
+ return false;
+ }
+
+ // 先将三个轴正交化,避免 fragment 矩阵里存在轻微数值误差或尺度干扰。
+ Vector3 orthogonalY = axisY - Vector3.Dot(axisY, axisX) * axisX;
+ if (!TryNormalize(orthogonalY, out orthogonalY))
+ {
+ orthogonalY = Vector3.Normalize(Vector3.Cross(axisZ, axisX));
+ }
+
+ Vector3 orthogonalZ = Vector3.Cross(axisX, orthogonalY);
+ if (!TryNormalize(orthogonalZ, out orthogonalZ))
+ {
+ return false;
+ }
+
+ orthogonalY = Vector3.Normalize(Vector3.Cross(orthogonalZ, axisX));
+
+ rotation = Quaternion.Normalize(CreateQuaternionFromBasis(axisX, orthogonalY, orthogonalZ));
+ return true;
+ }
+
+ private static Quaternion CreateQuaternionFromBasis(Vector3 axisX, Vector3 axisY, Vector3 axisZ)
+ {
+ float m00 = axisX.X;
+ float m01 = axisY.X;
+ float m02 = axisZ.X;
+ float m10 = axisX.Y;
+ float m11 = axisY.Y;
+ float m12 = axisZ.Y;
+ float m20 = axisX.Z;
+ float m21 = axisY.Z;
+ float m22 = axisZ.Z;
+
+ float trace = m00 + m11 + m22;
+ float qx;
+ float qy;
+ float qz;
+ float qw;
+
+ if (trace > 0.0f)
+ {
+ float s = (float)Math.Sqrt(trace + 1.0f) * 2.0f;
+ qw = 0.25f * s;
+ qx = (m21 - m12) / s;
+ qy = (m02 - m20) / s;
+ qz = (m10 - m01) / s;
+ }
+ else if (m00 > m11 && m00 > m22)
+ {
+ float s = (float)Math.Sqrt(1.0f + m00 - m11 - m22) * 2.0f;
+ qw = (m21 - m12) / s;
+ qx = 0.25f * s;
+ qy = (m01 + m10) / s;
+ qz = (m02 + m20) / s;
+ }
+ else if (m11 > m22)
+ {
+ float s = (float)Math.Sqrt(1.0f + m11 - m00 - m22) * 2.0f;
+ qw = (m02 - m20) / s;
+ qx = (m01 + m10) / s;
+ qy = 0.25f * s;
+ qz = (m12 + m21) / s;
+ }
+ else
+ {
+ float s = (float)Math.Sqrt(1.0f + m22 - m00 - m11) * 2.0f;
+ qw = (m10 - m01) / s;
+ qx = (m02 + m20) / s;
+ qy = (m12 + m21) / s;
+ qz = 0.25f * s;
+ }
+
+ return new Quaternion(qx, qy, qz, qw);
+ }
+
+ private static bool TryNormalize(Quaternion value, out Quaternion normalized)
+ {
+ float lengthSquared = value.X * value.X + value.Y * value.Y + value.Z * value.Z + value.W * value.W;
+ if (lengthSquared < AxisEpsilon)
+ {
+ normalized = Quaternion.Identity;
+ return false;
+ }
+
+ normalized = Quaternion.Normalize(value);
+ return true;
+ }
+
+ private static bool TryNormalize(Vector3 value, out Vector3 normalized)
+ {
+ if (value.LengthSquared() < AxisEpsilon)
+ {
+ normalized = Vector3.Zero;
+ return false;
+ }
+
+ normalized = Vector3.Normalize(value);
+ return true;
+ }
+ }
+}
diff --git a/src/Utils/CoordinateSystem/HostCoordinateAdapter.cs b/src/Utils/CoordinateSystem/HostCoordinateAdapter.cs
index 7a97e95..b780a22 100644
--- a/src/Utils/CoordinateSystem/HostCoordinateAdapter.cs
+++ b/src/Utils/CoordinateSystem/HostCoordinateAdapter.cs
@@ -182,19 +182,18 @@ namespace NavisworksTransport.Utils.CoordinateSystem
{
Matrix4x4 canonicalLinear = Matrix4x4.CreateFromQuaternion(canonicalRotation);
Matrix4x4 hostLinear = FromCanonicalLinearTransform(canonicalLinear);
- return CreateRotationFromLinearComponents(
- hostLinear.M11, hostLinear.M12, hostLinear.M13,
- hostLinear.M21, hostLinear.M22, hostLinear.M23,
- hostLinear.M31, hostLinear.M32, hostLinear.M33);
+ Quaternion hostQuaternion = Quaternion.Normalize(Quaternion.CreateFromRotationMatrix(hostLinear));
+ return new Rotation3D(hostQuaternion.X, hostQuaternion.Y, hostQuaternion.Z, hostQuaternion.W);
}
public Rotation3D FromHostQuaternion(Quaternion hostRotation)
{
- Matrix4x4 hostLinear = Matrix4x4.CreateFromQuaternion(hostRotation);
- return CreateRotationFromLinearComponents(
- hostLinear.M11, hostLinear.M12, hostLinear.M13,
- hostLinear.M21, hostLinear.M22, hostLinear.M23,
- hostLinear.M31, hostLinear.M32, hostLinear.M33);
+ Quaternion normalizedHostRotation = Quaternion.Normalize(hostRotation);
+ return new Rotation3D(
+ normalizedHostRotation.X,
+ normalizedHostRotation.Y,
+ normalizedHostRotation.Z,
+ normalizedHostRotation.W);
}
public Quaternion CreateHostRotationCorrection(LocalEulerRotationCorrection correction)
@@ -237,53 +236,6 @@ namespace NavisworksTransport.Utils.CoordinateSystem
return Quaternion.Normalize(qz * qy * qx);
}
- private static Rotation3D CreateRotationFromLinearComponents(
- double m00, double m01, double m02,
- double m10, double m11, double m12,
- double m20, double m21, double m22)
- {
- double qx;
- double qy;
- double qz;
- double qw;
-
- double trace = m00 + m11 + m22;
- if (trace > 0.0)
- {
- double s = Math.Sqrt(trace + 1.0) * 2.0;
- qw = 0.25 * s;
- qx = (m21 - m12) / s;
- qy = (m02 - m20) / s;
- qz = (m10 - m01) / s;
- }
- else if (m00 > m11 && m00 > m22)
- {
- double s = Math.Sqrt(1.0 + m00 - m11 - m22) * 2.0;
- qw = (m21 - m12) / s;
- qx = 0.25 * s;
- qy = (m01 + m10) / s;
- qz = (m02 + m20) / s;
- }
- else if (m11 > m22)
- {
- double s = Math.Sqrt(1.0 + m11 - m00 - m22) * 2.0;
- qw = (m02 - m20) / s;
- qx = (m01 + m10) / s;
- qy = 0.25 * s;
- qz = (m12 + m21) / s;
- }
- else
- {
- double s = Math.Sqrt(1.0 + m22 - m00 - m11) * 2.0;
- qw = (m10 - m01) / s;
- qx = (m02 + m20) / s;
- qy = (m12 + m21) / s;
- qz = 0.25 * s;
- }
-
- return new Rotation3D(qx, qy, qz, qw);
- }
-
public CanonicalBounds3 ToCanonicalBounds3(CanonicalBounds3 hostBounds)
{
return RebuildBounds3(hostBounds, ToCanonicalPoint3);
diff --git a/src/Utils/CoordinateSystem/RealObjectPlanarPoseSolver.cs b/src/Utils/CoordinateSystem/RealObjectPlanarPoseSolver.cs
new file mode 100644
index 0000000..10ee5a9
--- /dev/null
+++ b/src/Utils/CoordinateSystem/RealObjectPlanarPoseSolver.cs
@@ -0,0 +1,235 @@
+using System;
+using System.Numerics;
+
+namespace NavisworksTransport.Utils.CoordinateSystem
+{
+ ///
+ /// 真实物体在地面/吊装平面路径下的起点姿态求解器。
+ ///
+ /// 语义:
+ /// - 输入和输出都保持在宿主坐标系语义下
+ /// - 真实物体没有资产坐标系
+ /// - 从原始参考位姿的 6 个候选轴里,选择与路径前进方向最接近的那个
+ /// - 先求“原始前进轴 -> 路径前进方向”的旋转差,再作用到原始参考位姿上
+ ///
+ public static class RealObjectPlanarPoseSolver
+ {
+ private const float AxisEpsilon = 1e-6f;
+
+ public static bool TryCreatePlanarPoseFromReferencePose(
+ Quaternion referenceRotation,
+ Vector3 desiredForward,
+ Vector3 desiredUp,
+ out RealObjectPlanarPoseSolution solution)
+ {
+ solution = null;
+
+ if (!TryNormalize(referenceRotation, out Quaternion normalizedReferenceRotation))
+ {
+ return false;
+ }
+
+ if (!TryNormalize(desiredUp, out Vector3 normalizedUp))
+ {
+ return false;
+ }
+
+ if (!TryNormalize(desiredForward, out Vector3 normalizedForward))
+ {
+ return false;
+ }
+
+ CandidateAxis bestCandidate = SelectBestCandidateAxis(
+ normalizedReferenceRotation,
+ normalizedForward);
+
+ if (bestCandidate.IsInvalid)
+ {
+ return false;
+ }
+
+ Quaternion rotationDelta = CreateShortestArcQuaternion(
+ bestCandidate.WorldAxis,
+ normalizedForward,
+ normalizedUp);
+
+ Quaternion baselineRotation = Quaternion.Normalize(rotationDelta * normalizedReferenceRotation);
+ solution = new RealObjectPlanarPoseSolution(
+ normalizedForward,
+ normalizedUp,
+ bestCandidate.LocalAxis,
+ bestCandidate.WorldAxis,
+ rotationDelta,
+ baselineRotation);
+ return true;
+ }
+
+ private static CandidateAxis SelectBestCandidateAxis(
+ Quaternion referenceRotation,
+ Vector3 desiredForward)
+ {
+ CandidateAxis bestCandidate = CandidateAxis.Invalid;
+
+ foreach (var candidate in EnumerateCandidateAxes(referenceRotation))
+ {
+ float score = Vector3.Dot(candidate.WorldAxis, desiredForward);
+ if (bestCandidate.IsInvalid || score > bestCandidate.Score)
+ {
+ bestCandidate = new CandidateAxis(
+ candidate.LocalAxis,
+ candidate.WorldAxis,
+ score);
+ }
+ }
+
+ return bestCandidate;
+ }
+
+ private static CandidateAxis[] EnumerateCandidateAxes(Quaternion referenceRotation)
+ {
+ return new[]
+ {
+ CreateCandidateAxis(Vector3.UnitX, referenceRotation),
+ CreateCandidateAxis(-Vector3.UnitX, referenceRotation),
+ CreateCandidateAxis(Vector3.UnitY, referenceRotation),
+ CreateCandidateAxis(-Vector3.UnitY, referenceRotation),
+ CreateCandidateAxis(Vector3.UnitZ, referenceRotation),
+ CreateCandidateAxis(-Vector3.UnitZ, referenceRotation)
+ };
+ }
+
+ private static CandidateAxis CreateCandidateAxis(
+ Vector3 localAxis,
+ Quaternion referenceRotation)
+ {
+ Vector3 worldAxis = Vector3.Normalize(Vector3.Transform(localAxis, referenceRotation));
+ return new CandidateAxis(localAxis, worldAxis, float.MinValue);
+ }
+
+ private static Quaternion CreateShortestArcQuaternion(
+ Vector3 from,
+ Vector3 to,
+ Vector3 desiredUp)
+ {
+ Vector3 normalizedFrom = Vector3.Normalize(from);
+ Vector3 normalizedTo = Vector3.Normalize(to);
+
+ float dot = Vector3.Dot(normalizedFrom, normalizedTo);
+ dot = Math.Max(-1.0f, Math.Min(1.0f, dot));
+
+ if (dot > 1.0f - 1e-6f)
+ {
+ return Quaternion.Identity;
+ }
+
+ if (dot < -1.0f + 1e-6f)
+ {
+ Vector3 axis = Vector3.Cross(normalizedFrom, desiredUp);
+ if (axis.LengthSquared() < AxisEpsilon)
+ {
+ axis = Vector3.Cross(normalizedFrom, GetAnyOrthogonalVector(normalizedFrom));
+ }
+
+ axis = Vector3.Normalize(axis);
+ return Quaternion.Normalize(Quaternion.CreateFromAxisAngle(axis, (float)Math.PI));
+ }
+
+ Vector3 cross = Vector3.Cross(normalizedFrom, normalizedTo);
+ if (cross.LengthSquared() < AxisEpsilon)
+ {
+ return Quaternion.Identity;
+ }
+
+ cross = Vector3.Normalize(cross);
+ double angle = Math.Acos(dot);
+ return Quaternion.Normalize(Quaternion.CreateFromAxisAngle(cross, (float)angle));
+ }
+
+ private static Vector3 GetAnyOrthogonalVector(Vector3 source)
+ {
+ if (Math.Abs(source.X) < 0.9f)
+ {
+ return Vector3.UnitX;
+ }
+
+ if (Math.Abs(source.Y) < 0.9f)
+ {
+ return Vector3.UnitY;
+ }
+
+ return Vector3.UnitZ;
+ }
+
+ private static bool TryNormalize(Quaternion value, out Quaternion normalized)
+ {
+ float lengthSquared = value.X * value.X + value.Y * value.Y + value.Z * value.Z + value.W * value.W;
+ if (lengthSquared < AxisEpsilon)
+ {
+ normalized = Quaternion.Identity;
+ return false;
+ }
+
+ normalized = Quaternion.Normalize(value);
+ return true;
+ }
+
+ private static bool TryNormalize(Vector3 value, out Vector3 normalized)
+ {
+ if (value.LengthSquared() < AxisEpsilon)
+ {
+ normalized = Vector3.Zero;
+ return false;
+ }
+
+ normalized = Vector3.Normalize(value);
+ return true;
+ }
+
+ private struct CandidateAxis
+ {
+ public static CandidateAxis Invalid => new CandidateAxis(Vector3.Zero, Vector3.Zero, float.MinValue);
+
+ public Vector3 LocalAxis { get; }
+ public Vector3 WorldAxis { get; }
+ public float Score { get; }
+
+ public bool IsInvalid => Score == float.MinValue;
+
+ public CandidateAxis(Vector3 localAxis, Vector3 worldAxis, float score)
+ {
+ LocalAxis = localAxis;
+ WorldAxis = worldAxis;
+ Score = score;
+ }
+ }
+ }
+
+ ///
+ /// 真实物体平面起点姿态求解结果。
+ ///
+ public sealed class RealObjectPlanarPoseSolution
+ {
+ public Vector3 ProjectedForward { get; }
+ public Vector3 DesiredUp { get; }
+ public Vector3 SelectedReferenceAxisLocal { get; }
+ public Vector3 SelectedReferenceAxisWorld { get; }
+ public Quaternion RotationDelta { get; }
+ public Quaternion BaselineRotation { get; }
+
+ public RealObjectPlanarPoseSolution(
+ Vector3 projectedForward,
+ Vector3 desiredUp,
+ Vector3 selectedReferenceAxisLocal,
+ Vector3 selectedReferenceAxisWorld,
+ Quaternion rotationDelta,
+ Quaternion baselineRotation)
+ {
+ ProjectedForward = projectedForward;
+ DesiredUp = desiredUp;
+ SelectedReferenceAxisLocal = selectedReferenceAxisLocal;
+ SelectedReferenceAxisWorld = selectedReferenceAxisWorld;
+ RotationDelta = rotationDelta;
+ BaselineRotation = baselineRotation;
+ }
+ }
+}
From 290df34dec544ba651cac3aa8b93fbe18252cad0 Mon Sep 17 00:00:00 2001
From: tian <11429339@qq.com>
Date: Tue, 24 Mar 2026 21:04:24 +0800
Subject: [PATCH 25/87] Interpret fragment up for real object pose
---
AGENTS.md | 29 +++
TransportPlugin.csproj | 1 +
.../FragmentRepresentativePoseHelperTests.cs | 49 +++++
.../RealObjectPlanarPoseSolverTests.cs | 129 ++----------
doc/design/2026/NavisworksAPI使用方法.md | 9 +
...oordinate-system-canonical-space-design.md | 22 ++
src/Core/Animation/PathAnimationManager.cs | 196 ++++++++++++++----
.../ViewModels/SystemManagementViewModel.cs | 65 +-----
.../FragmentDefaultUpContext.cs | 90 ++++++++
.../FragmentRepresentativePoseHelper.cs | 118 +++++++++++
10 files changed, 494 insertions(+), 214 deletions(-)
create mode 100644 src/Utils/CoordinateSystem/FragmentDefaultUpContext.cs
diff --git a/AGENTS.md b/AGENTS.md
index d5ee793..9a438af 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -133,6 +133,35 @@
禁止再使用“本地坐标系”这种含糊说法。
+### 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
+var rotation = new Rotation3D(qx, qy, qz, qw);
+```
+
+- 严禁再写成:
+
+```csharp
+var rotation = new Rotation3D(qw, qx, qy, qz); // 错误
+```
+
+- 以后遇到姿态问题,禁止再把故障归因到 “Navisworks 四元数分量顺序可能又不一样了”。
+- 如果问题涉及真实物体或 fragment 参考姿态的轴语义,优先检查:
+ - fragment 三轴的业务解释是否正确
+ - 参考姿态是否应该用显式三轴而不是只传 quaternion
+ - 宿主坐标系 / 内部坐标系 / 资产坐标系 是否被混用
+
### 4.2 真实物体 vs 虚拟物体
这是当前架构里最容易被改坏的地方。
diff --git a/TransportPlugin.csproj b/TransportPlugin.csproj
index f515279..e2b3465 100644
--- a/TransportPlugin.csproj
+++ b/TransportPlugin.csproj
@@ -340,6 +340,7 @@
+
diff --git a/UnitTests/CoordinateSystem/FragmentRepresentativePoseHelperTests.cs b/UnitTests/CoordinateSystem/FragmentRepresentativePoseHelperTests.cs
index 6efb96f..751eba4 100644
--- a/UnitTests/CoordinateSystem/FragmentRepresentativePoseHelperTests.cs
+++ b/UnitTests/CoordinateSystem/FragmentRepresentativePoseHelperTests.cs
@@ -98,6 +98,55 @@ namespace NavisworksTransport.UnitTests.CoordinateSystem
AssertVector(Vector3.Transform(Vector3.UnitZ, rotation), axisZ, 1e-4);
}
+ [TestMethod]
+ public void RepresentativeFrame_ShouldExposeSameAxes_AsRepresentativeRotation()
+ {
+ Vector3 axisX = new Vector3(1.0f, 0.0f, 0.0f);
+ Vector3 axisY = new Vector3(0.0f, 0.0f, 1.0f);
+ Vector3 axisZ = new Vector3(0.0f, -1.0f, 0.0f);
+
+ double[] fragmentMatrix =
+ {
+ axisX.X, axisX.Y, axisX.Z, 0.0,
+ axisY.X, axisY.Y, axisY.Z, 0.0,
+ axisZ.X, axisZ.Y, axisZ.Z, 0.0,
+ 0.0, 0.0, 0.0, 1.0
+ };
+
+ bool ok = FragmentRepresentativePoseHelper.TryGetRepresentativeFrame(
+ new[] { fragmentMatrix },
+ out FragmentRepresentativePoseHelper.RepresentativeFrame frame);
+
+ Assert.IsTrue(ok);
+ AssertVector(frame.AxisX, axisX, 1e-4);
+ AssertVector(frame.AxisY, axisY, 1e-4);
+ AssertVector(frame.AxisZ, axisZ, 1e-4);
+ AssertVector(Vector3.Transform(Vector3.UnitX, frame.Rotation), axisX, 1e-4);
+ AssertVector(Vector3.Transform(Vector3.UnitY, frame.Rotation), axisY, 1e-4);
+ AssertVector(Vector3.Transform(Vector3.UnitZ, frame.Rotation), axisZ, 1e-4);
+ }
+
+ [TestMethod]
+ public void TryInterpretRepresentativeFrame_YUpHost_WithFragmentDefaultUpZ_ShouldMapZToHostY()
+ {
+ var raw = new FragmentRepresentativePoseHelper.RepresentativeFrame(
+ new Vector3(1.0f, 0.0f, 0.0f),
+ new Vector3(0.0f, 0.0f, -1.0f),
+ new Vector3(0.0f, 1.0f, 0.0f),
+ Quaternion.Identity);
+
+ bool ok = FragmentRepresentativePoseHelper.TryInterpretRepresentativeFrame(
+ raw,
+ "Z",
+ "Y",
+ out var interpreted);
+
+ Assert.IsTrue(ok);
+ AssertVector(interpreted.AxisX, new Vector3(1.0f, 0.0f, 0.0f), 1e-4);
+ AssertVector(interpreted.AxisY, new Vector3(0.0f, 1.0f, 0.0f), 1e-4);
+ AssertVector(interpreted.AxisZ, new Vector3(0.0f, 0.0f, 1.0f), 1e-4);
+ }
+
private static double[] CreateFragmentMatrix(Quaternion rotation, Vector3 translation)
{
Vector3 axisX = Vector3.Transform(Vector3.UnitX, rotation);
diff --git a/UnitTests/CoordinateSystem/RealObjectPlanarPoseSolverTests.cs b/UnitTests/CoordinateSystem/RealObjectPlanarPoseSolverTests.cs
index a9679c0..a57f560 100644
--- a/UnitTests/CoordinateSystem/RealObjectPlanarPoseSolverTests.cs
+++ b/UnitTests/CoordinateSystem/RealObjectPlanarPoseSolverTests.cs
@@ -2,7 +2,6 @@ using Microsoft.VisualStudio.TestTools.UnitTesting;
using NavisworksTransport.Utils.CoordinateSystem;
using System;
using System.Numerics;
-using Autodesk.Navisworks.Api;
namespace NavisworksTransport.UnitTests.CoordinateSystem
{
@@ -12,9 +11,7 @@ namespace NavisworksTransport.UnitTests.CoordinateSystem
[TestMethod]
public void ShouldChooseClosestCandidateAxis_FromRotatedReferencePose()
{
- Quaternion referenceRotation = Quaternion.Normalize(
- Quaternion.CreateFromAxisAngle(Vector3.UnitZ, (float)(Math.PI / 6.0)));
-
+ Quaternion referenceRotation = Quaternion.CreateFromAxisAngle(Vector3.UnitZ, (float)(Math.PI / 6.0));
Vector3 desiredForward = new Vector3(1.0f, 0.2f, 0.1f);
Vector3 desiredUp = Vector3.UnitZ;
@@ -26,48 +23,37 @@ namespace NavisworksTransport.UnitTests.CoordinateSystem
Assert.IsTrue(ok);
AssertVector(solution.SelectedReferenceAxisLocal, 1.0, 0.0, 0.0);
- AssertVector(solution.ProjectedForward, 0.97590, 0.19518, 0.09759, 1e-4);
- Vector3 transformedSelectedAxis = Vector3.Transform(solution.SelectedReferenceAxisLocal, solution.BaselineRotation);
- AssertVector(transformedSelectedAxis, solution.ProjectedForward.X, solution.ProjectedForward.Y, solution.ProjectedForward.Z, 1e-4);
+ Vector3 transformedSelectedAxis = Vector3.Normalize(Vector3.Transform(solution.SelectedReferenceAxisLocal, solution.BaselineRotation));
+ AssertVector(transformedSelectedAxis, desiredForward.X / desiredForward.Length(), desiredForward.Y / desiredForward.Length(), desiredForward.Z / desiredForward.Length(), 1e-4);
}
[TestMethod]
public void ShouldPreferNegativeAxisWhenItMatchesForwardBest()
{
- Quaternion referenceRotation = Quaternion.Identity;
- Vector3 desiredForward = new Vector3(-0.1f, -1.0f, 0.05f);
- Vector3 desiredUp = Vector3.UnitZ;
-
bool ok = RealObjectPlanarPoseSolver.TryCreatePlanarPoseFromReferencePose(
- referenceRotation,
- desiredForward,
- desiredUp,
+ Quaternion.Identity,
+ new Vector3(-0.1f, -1.0f, 0.05f),
+ Vector3.UnitZ,
out RealObjectPlanarPoseSolution solution);
Assert.IsTrue(ok);
AssertVector(solution.SelectedReferenceAxisLocal, 0.0, -1.0, 0.0);
-
- Vector3 transformedSelectedAxis = Vector3.Transform(solution.SelectedReferenceAxisLocal, solution.BaselineRotation);
- AssertVector(transformedSelectedAxis, solution.ProjectedForward.X, solution.ProjectedForward.Y, solution.ProjectedForward.Z, 1e-4);
}
[TestMethod]
- public void ShouldRotateWholeReferencePose_NotFlattenItToHostUpPlane()
+ public void ShouldRespectDesiredUpPlane_WhenDesiredForwardHasVerticalComponent()
{
- Quaternion referenceRotation = Quaternion.Identity;
Vector3 desiredForward = Vector3.Normalize(new Vector3(1.0f, 1.0f, 1.0f));
bool ok = RealObjectPlanarPoseSolver.TryCreatePlanarPoseFromReferencePose(
- referenceRotation,
+ Quaternion.Identity,
desiredForward,
Vector3.UnitY,
out RealObjectPlanarPoseSolution solution);
Assert.IsTrue(ok);
- AssertVector(solution.SelectedReferenceAxisLocal, 1.0, 0.0, 0.0);
-
- Vector3 transformedSelectedAxis = Vector3.Transform(solution.SelectedReferenceAxisLocal, solution.BaselineRotation);
+ Vector3 transformedSelectedAxis = Vector3.Normalize(Vector3.Transform(solution.SelectedReferenceAxisLocal, solution.BaselineRotation));
AssertVector(transformedSelectedAxis, desiredForward.X, desiredForward.Y, desiredForward.Z, 1e-4);
}
@@ -90,95 +76,16 @@ namespace NavisworksTransport.UnitTests.CoordinateSystem
Assert.IsTrue(ok);
- Vector3 transformedSelectedAxis = Vector3.Transform(solution.SelectedReferenceAxisLocal, solution.BaselineRotation);
- AssertVector(transformedSelectedAxis, desiredForward.X, desiredForward.Y, desiredForward.Z, 1e-4);
+ Vector3 transformedX = Vector3.Normalize(Vector3.Transform(Vector3.UnitX, solution.BaselineRotation));
+ Vector3 transformedY = Vector3.Normalize(Vector3.Transform(Vector3.UnitY, solution.BaselineRotation));
+ Vector3 transformedZ = Vector3.Normalize(Vector3.Transform(Vector3.UnitZ, solution.BaselineRotation));
- Vector3 transformedReferenceUp = Vector3.Normalize(Vector3.Transform(Vector3.UnitZ, solution.BaselineRotation));
- Assert.AreEqual(1.0, transformedReferenceUp.Length(), 1e-4);
- Assert.AreEqual(0.0, Vector3.Dot(transformedSelectedAxis, transformedReferenceUp), 1e-4);
- }
-
- [TestMethod]
- public void YUp_HorizontalRealObject_ToAnotherHorizontalDirection_ShouldKeepVerticalAxis()
- {
- Vector3 referenceX = new Vector3(1.0f, 0.0f, 0.0f);
- Vector3 referenceY = new Vector3(0.0f, 0.0f, 1.0f);
- Vector3 referenceZ = new Vector3(0.0f, -1.0f, 0.0f);
- Quaternion referenceRotation = CreateQuaternionFromBasis(referenceX, referenceY, referenceZ);
-
- Vector3 desiredForward = Vector3.Normalize(new Vector3(-0.8987f, 0.0f, 0.4386f));
- Vector3 desiredUp = Vector3.UnitY;
-
- bool ok = RealObjectPlanarPoseSolver.TryCreatePlanarPoseFromReferencePose(
- referenceRotation,
- desiredForward,
- desiredUp,
- out RealObjectPlanarPoseSolution solution);
-
- Assert.IsTrue(ok);
- AssertVector(solution.SelectedReferenceAxisLocal, -1.0, 0.0, 0.0);
-
- Vector3 transformedForward = Vector3.Normalize(
- Vector3.Transform(solution.SelectedReferenceAxisLocal, solution.BaselineRotation));
- Vector3 transformedVertical = Vector3.Normalize(
- Vector3.Transform(-Vector3.UnitZ, solution.BaselineRotation));
-
- AssertVector(transformedForward, desiredForward.X, desiredForward.Y, desiredForward.Z, 1e-4);
- AssertVector(transformedVertical, 0.0, 1.0, 0.0, 1e-4);
- }
-
- private static Quaternion CreateQuaternionFromBasis(Vector3 axisX, Vector3 axisY, Vector3 axisZ)
- {
- float m00 = axisX.X;
- float m01 = axisY.X;
- float m02 = axisZ.X;
- float m10 = axisX.Y;
- float m11 = axisY.Y;
- float m12 = axisZ.Y;
- float m20 = axisX.Z;
- float m21 = axisY.Z;
- float m22 = axisZ.Z;
-
- float trace = m00 + m11 + m22;
- float qx;
- float qy;
- float qz;
- float qw;
-
- if (trace > 0.0f)
- {
- float s = (float)Math.Sqrt(trace + 1.0f) * 2.0f;
- qw = 0.25f * s;
- qx = (m21 - m12) / s;
- qy = (m02 - m20) / s;
- qz = (m10 - m01) / s;
- }
- else if (m00 > m11 && m00 > m22)
- {
- float s = (float)Math.Sqrt(1.0f + m00 - m11 - m22) * 2.0f;
- qw = (m21 - m12) / s;
- qx = 0.25f * s;
- qy = (m01 + m10) / s;
- qz = (m02 + m20) / s;
- }
- else if (m11 > m22)
- {
- float s = (float)Math.Sqrt(1.0f + m11 - m00 - m22) * 2.0f;
- qw = (m02 - m20) / s;
- qx = (m01 + m10) / s;
- qy = 0.25f * s;
- qz = (m12 + m21) / s;
- }
- else
- {
- float s = (float)Math.Sqrt(1.0f + m22 - m00 - m11) * 2.0f;
- qw = (m10 - m01) / s;
- qx = (m02 + m20) / s;
- qy = (m12 + m21) / s;
- qz = 0.25f * s;
- }
-
- return Quaternion.Normalize(new Quaternion(qx, qy, qz, qw));
+ Assert.AreEqual(1.0, transformedX.Length(), 1e-4);
+ Assert.AreEqual(1.0, transformedY.Length(), 1e-4);
+ Assert.AreEqual(1.0, transformedZ.Length(), 1e-4);
+ Assert.AreEqual(0.0, Vector3.Dot(transformedX, transformedY), 1e-4);
+ Assert.AreEqual(0.0, Vector3.Dot(transformedY, transformedZ), 1e-4);
+ Assert.AreEqual(0.0, Vector3.Dot(transformedZ, transformedX), 1e-4);
}
private static void AssertVector(Vector3 vector, double x, double y, double z, double tolerance = 1e-6)
diff --git a/doc/design/2026/NavisworksAPI使用方法.md b/doc/design/2026/NavisworksAPI使用方法.md
index 39f03e4..e70b26d 100644
--- a/doc/design/2026/NavisworksAPI使用方法.md
+++ b/doc/design/2026/NavisworksAPI使用方法.md
@@ -779,6 +779,15 @@ var rotation = new Rotation3D(qw, qx, qy, qz); // ❌ 错误
这个结论已经在 Rail 三维姿态、参考杆三维定向的实际运行中验证通过。
+**项目级硬约束:**
+
+- 后续任何代码和文档中,都不允许再重新解释 `Rotation3D` 的四元数分量顺序
+- 遇到姿态问题时,不要再把问题归因到 `Rotation3D(x,y,z,w)` 是否可能变成了别的顺序
+- 如果真实物体或 fragment 参考姿态出现轴翻转/符号错误,应优先检查:
+ - fragment 三轴的业务语义解释
+ - 宿主坐标系 / 内部坐标系 / 资产坐标系 的边界
+ - 是否应该传递显式参考轴,而不是只传 quaternion
+
#### 11.7.2.2 三维参考杆/任意向量对齐的正确用法
适用场景:
diff --git a/doc/design/2026/coordinate-system-canonical-space-design.md b/doc/design/2026/coordinate-system-canonical-space-design.md
index 9c06e51..77acdb5 100644
--- a/doc/design/2026/coordinate-system-canonical-space-design.md
+++ b/doc/design/2026/coordinate-system-canonical-space-design.md
@@ -145,6 +145,28 @@ flowchart LR
后续凡是 UI 输入/输出,必须明确是**宿主坐标系**语义;凡是姿态或几何内部计算,必须明确是**内部坐标系**语义;凡是虚拟物体/单位圆柱体资源朝向,必须明确是**资产坐标系**语义。
- 业务锚点
+### 3.2.2 Quaternion / Rotation3D 解释约束
+
+坐标系分层之外,还必须固定一条旋转解释规则:
+
+- Navisworks `Rotation3D(double, double, double, double)` 的参数顺序固定为 `x, y, z, w`
+- `Rotation3D.A/B/C/D` 固定对应 quaternion 的 `x/y/z/w`
+
+这条规则在项目中视为硬约束,禁止后续代码再次用不同顺序解释四元数。
+
+因此:
+
+- 内部坐标系里算出的 quaternion `(qx, qy, qz, qw)`,写回 Navisworks 时必须使用:
+
+```csharp
+var rotation = new Rotation3D(qx, qy, qz, qw);
+```
+
+- 后续遇到姿态异常时,不应再次怀疑 `Rotation3D` 分量顺序;应优先检查:
+ - 宿主坐标系 / 内部坐标系 / 资产坐标系 是否混用
+ - fragment 参考姿态是否被误当成业务语义姿态
+ - 真实物体是否错误地只传 quaternion 而没有保留显式参考轴
+
这层不能偷用世界原点,也不能偷用宿主世界轴。
### 3.3 坐标系定义必须包含的语义
diff --git a/src/Core/Animation/PathAnimationManager.cs b/src/Core/Animation/PathAnimationManager.cs
index 2d24ba7..fa76673 100644
--- a/src/Core/Animation/PathAnimationManager.cs
+++ b/src/Core/Animation/PathAnimationManager.cs
@@ -102,6 +102,7 @@ namespace NavisworksTransport.Core.Animation
{
private static PathAnimationManager _instance;
private static readonly Dictionary _animationHashToRecordId = new Dictionary(); // 动画配置哈希到检测记录ID的映射
+ private static readonly HashSet _realObjectFragmentUpMismatchHintsShown = new HashSet(StringComparer.OrdinalIgnoreCase);
private ModelItem _animatedObject;
private AnimatedObjectMode _animatedObjectMode = AnimatedObjectMode.None;
private double _virtualObjectLength = 0; // 虚拟物体长度(模型单位)
@@ -111,6 +112,9 @@ namespace NavisworksTransport.Core.Animation
private double _realObjectWidth = 0; // 真实物体宽度(模型单位,固定物理尺寸)
private double _realObjectHeight = 0; // 真实物体高度(模型单位,固定物理尺寸)
private Quaternion _realObjectReferenceRotation = Quaternion.Identity;
+ private Vector3 _realObjectReferenceAxisX = Vector3.UnitX;
+ private Vector3 _realObjectReferenceAxisY = Vector3.UnitY;
+ private Vector3 _realObjectReferenceAxisZ = Vector3.UnitZ;
private bool _hasRealObjectReferenceRotation = false;
private List _pathPoints;
private List _manualCollisionTargets = new List();
@@ -443,12 +447,10 @@ namespace NavisworksTransport.Core.Animation
// 2. 重置内部跟踪变量(同步到原始状态)
// 注意:ResetPermanentTransform 后物体的 Transform 属性会自动变回原始值
- _currentYaw = ModelItemTransformHelper.GetYawFromTransform(objectToRestore.Transform);
- _trackedRotation = objectToRestore.Transform.Factor().Rotation;
- _hasTrackedRotation = true;
+ SyncTrackedRotationToObjectReference(objectToRestore, isVirtual);
- var originalBoundingBox = objectToRestore.BoundingBox();
- _trackedPosition = GetTrackedObjectPosition(objectToRestore);
+ var originalBoundingBox = objectToRestore.BoundingBox();
+ _trackedPosition = GetTrackedObjectPosition(objectToRestore);
string objectName = isVirtual ? "虚拟物体" : objectToRestore.DisplayName;
LogManager.Info($"[归位] {objectName} 已彻底恢复到原始位置, yaw={_currentYaw:F3}");
@@ -486,10 +488,7 @@ namespace NavisworksTransport.Core.Animation
_originalTransform = animatedObject.Transform;
_originalCenter = animatedObject.BoundingBox().Center;
_trackedPosition = GetTrackedObjectPosition(animatedObject);
- _currentYaw = ModelItemTransformHelper.GetYawFromTransform(_originalTransform);
- _trackedRotation = _originalTransform.Factor().Rotation;
- _hasTrackedRotation = true;
- TryCaptureRealObjectReferenceRotation(animatedObject, out _);
+ SyncTrackedRotationToObjectReference(animatedObject, isVirtualObject: false);
}
}
@@ -651,16 +650,7 @@ namespace NavisworksTransport.Core.Animation
_originalTransform = animatedObject.Transform;
_originalCenter = animatedObject.BoundingBox().Center;
_trackedPosition = GetTrackedObjectPosition(animatedObject);
-
- // 统一逻辑:从当前 Transform 中提取实际朝向(无论虚拟物体还是普通物体)
- _currentYaw = ModelItemTransformHelper.GetYawFromTransform(_originalTransform);
- _trackedRotation = _originalTransform.Factor().Rotation;
- _hasTrackedRotation = true;
-
- if (!isVirtualObject)
- {
- TryCaptureRealObjectReferenceRotation(animatedObject, out _);
- }
+ SyncTrackedRotationToObjectReference(animatedObject, isVirtualObject);
LogManager.Info(
$"[移动到起点] 更新动画对象内部状态: 对象={animatedObject.DisplayName}, 虚拟物体={IsVirtualObjectMode}");
@@ -2471,15 +2461,27 @@ namespace NavisworksTransport.Core.Animation
{
VirtualObjectManager.Instance.MoveVirtualObject(newPosition, newRotation);
}
- else if (_animatedObject != null)
- {
- Point3D currentPositionForTransform = _trackedPosition;
- var currentRotation = _hasTrackedRotation
- ? _trackedRotation
- : _animatedObject.Transform.Factor().Rotation;
- try
- {
- var actualHostPosition = GetTrackedObjectPosition(_animatedObject);
+ else if (_animatedObject != null)
+ {
+ Point3D currentPositionForTransform = _trackedPosition;
+ Rotation3D currentRotation;
+ if (_hasTrackedRotation)
+ {
+ currentRotation = _trackedRotation;
+ }
+ else if (IsRealObjectMode && _hasRealObjectReferenceRotation)
+ {
+ currentRotation = CoordinateSystemManager.Instance
+ .CreateHostAdapter()
+ .FromHostQuaternion(_realObjectReferenceRotation);
+ }
+ else
+ {
+ currentRotation = _animatedObject.Transform.Factor().Rotation;
+ }
+ try
+ {
+ var actualHostPosition = GetTrackedObjectPosition(_animatedObject);
currentPositionForTransform = actualHostPosition;
LogManager.Info(
$"[动画姿态入口] {_animatedObject.DisplayName} 宿主即时读回点=({actualHostPosition.X:F3},{actualHostPosition.Y:F3},{actualHostPosition.Z:F3})");
@@ -2493,11 +2495,11 @@ namespace NavisworksTransport.Core.Animation
LogManager.Info(
$"[动画姿态入口] {_animatedObject.DisplayName} 跟踪点=({_trackedPosition.X:F3},{_trackedPosition.Y:F3},{_trackedPosition.Z:F3}), " +
$"目标点=({newPosition.X:F3},{newPosition.Y:F3},{newPosition.Z:F3})");
- LogManager.Info(
- $"[动画姿态入口] {_animatedObject.DisplayName} 跟踪姿态: " +
- $"X=({currentLinear.Get(0, 0):F4},{currentLinear.Get(1, 0):F4},{currentLinear.Get(2, 0):F4}), " +
- $"Y=({currentLinear.Get(0, 1):F4},{currentLinear.Get(1, 1):F4},{currentLinear.Get(2, 1):F4}), " +
- $"Z=({currentLinear.Get(0, 2):F4},{currentLinear.Get(1, 2):F4},{currentLinear.Get(2, 2):F4})");
+ LogManager.Info(
+ $"[动画姿态入口] {_animatedObject.DisplayName} 跟踪姿态: " +
+ $"X=({currentLinear.Get(0, 0):F4},{currentLinear.Get(1, 0):F4},{currentLinear.Get(2, 0):F4}), " +
+ $"Y=({currentLinear.Get(0, 1):F4},{currentLinear.Get(1, 1):F4},{currentLinear.Get(2, 1):F4}), " +
+ $"Z=({currentLinear.Get(0, 2):F4},{currentLinear.Get(1, 2):F4},{currentLinear.Get(2, 2):F4})");
LogManager.Info(
$"[动画姿态入口] {_animatedObject.DisplayName} 目标姿态: " +
$"X=({targetLinear.Get(0, 0):F4},{targetLinear.Get(1, 0):F4},{targetLinear.Get(2, 0):F4}), " +
@@ -3665,10 +3667,10 @@ namespace NavisworksTransport.Core.Animation
var adapter = CoordinateSystemManager.Instance.CreateHostAdapter();
if (!RealObjectPlanarPoseSolver.TryCreatePlanarPoseFromReferencePose(
- referenceRotation,
- hostForward,
- adapter.HostUpVector3,
- out var solution))
+ referenceRotation,
+ hostForward,
+ adapter.HostUpVector3,
+ out var solution))
{
return false;
}
@@ -3676,13 +3678,76 @@ namespace NavisworksTransport.Core.Animation
Quaternion hostComposedQuaternion = adapter.ComposeHostQuaternion(solution.BaselineRotation, _objectRotationCorrection);
rotation = adapter.FromHostQuaternion(hostComposedQuaternion);
+ Matrix4x4 baselineLinear = Matrix4x4.CreateFromQuaternion(solution.BaselineRotation);
+ Matrix4x4 hostComposedLinear = Matrix4x4.CreateFromQuaternion(hostComposedQuaternion);
+
LogManager.Info(
$"[真实物体起点姿态] 选中参考轴=({solution.SelectedReferenceAxisLocal.X:F3},{solution.SelectedReferenceAxisLocal.Y:F3},{solution.SelectedReferenceAxisLocal.Z:F3}), " +
$"投影前进=({solution.ProjectedForward.X:F3},{solution.ProjectedForward.Y:F3},{solution.ProjectedForward.Z:F3}), " +
- $"基姿态已计算");
+ $"BaselineX=({baselineLinear.M11:F4},{baselineLinear.M21:F4},{baselineLinear.M31:F4}), " +
+ $"BaselineY=({baselineLinear.M12:F4},{baselineLinear.M22:F4},{baselineLinear.M32:F4}), " +
+ $"BaselineZ=({baselineLinear.M13:F4},{baselineLinear.M23:F4},{baselineLinear.M33:F4}), " +
+ $"HostComposeX=({hostComposedLinear.M11:F4},{hostComposedLinear.M21:F4},{hostComposedLinear.M31:F4}), " +
+ $"HostComposeY=({hostComposedLinear.M12:F4},{hostComposedLinear.M22:F4},{hostComposedLinear.M32:F4}), " +
+ $"HostComposeZ=({hostComposedLinear.M13:F4},{hostComposedLinear.M23:F4},{hostComposedLinear.M33:F4})");
return true;
}
+ private void ShowRealObjectFragmentUpMismatchHintIfNeeded(Vector3 representativeYAxis, Vector3 representativeZAxis)
+ {
+ if (_route == null || (_route.PathType != PathType.Ground && _route.PathType != PathType.Hoisting))
+ {
+ return;
+ }
+
+ if (!IsRealObjectMode)
+ {
+ return;
+ }
+
+ var doc = NavisApplication.ActiveDocument;
+ string hostUpAxis = FragmentDefaultUpContext.GetCurrentHostUpAxis(doc);
+ string configuredFragmentUpAxis = FragmentDefaultUpContext.GetCurrentDocumentFragmentDefaultUpAxis(doc);
+ Vector3 hostUpVector = string.Equals(hostUpAxis, "Y", StringComparison.OrdinalIgnoreCase)
+ ? Vector3.UnitY
+ : Vector3.UnitZ;
+
+ float yAlignment = Math.Abs(Vector3.Dot(Vector3.Normalize(representativeYAxis), hostUpVector));
+ float zAlignment = Math.Abs(Vector3.Dot(Vector3.Normalize(representativeZAxis), hostUpVector));
+ string detectedFragmentUpAxis = zAlignment > yAlignment ? "Z" : "Y";
+
+ if (string.Equals(configuredFragmentUpAxis, detectedFragmentUpAxis, StringComparison.OrdinalIgnoreCase))
+ {
+ return;
+ }
+
+ string documentKey = FragmentDefaultUpContext.GetCurrentDocumentKey(doc);
+ if (string.IsNullOrWhiteSpace(documentKey))
+ {
+ return;
+ }
+
+ string hintKey = $"{documentKey}|{configuredFragmentUpAxis}|{detectedFragmentUpAxis}";
+ lock (_realObjectFragmentUpMismatchHintsShown)
+ {
+ if (_realObjectFragmentUpMismatchHintsShown.Contains(hintKey))
+ {
+ return;
+ }
+
+ _realObjectFragmentUpMismatchHintsShown.Add(hintKey);
+ }
+
+ string message =
+ $"请到系统管理的坐标系设置中,将 Fragment默认Up 调整为 {detectedFragmentUpAxis}。";
+ LogManager.Info($"[fragment默认Up提示] 当前值={configuredFragmentUpAxis}, 检测值={detectedFragmentUpAxis}, 模型Up={hostUpAxis}。{message}");
+ DialogHelper.ShowMessageBox(
+ message,
+ "Fragment默认Up提示",
+ MessageBoxButton.OK,
+ MessageBoxImage.Information);
+ }
+
private bool TryGetRealObjectReferenceRotation(out Quaternion referenceRotation)
{
referenceRotation = Quaternion.Identity;
@@ -3760,22 +3825,38 @@ namespace NavisworksTransport.Core.Animation
return false;
}
- if (!FragmentRepresentativePoseHelper.TryGetRepresentativeRotation(fragmentMatrices, out referenceRotation))
+ if (!FragmentRepresentativePoseHelper.TryGetRepresentativeFrame(fragmentMatrices, out var rawRepresentativeFrame))
{
LogManager.Warning($"[真实物体参考姿态] {sourceObject.DisplayName} fragment 代表姿态求解失败");
return false;
}
+ var doc = NavisApplication.ActiveDocument;
+ string fragmentDefaultUpAxis = FragmentDefaultUpContext.GetCurrentDocumentFragmentDefaultUpAxis(doc);
+ string hostUpAxis = FragmentDefaultUpContext.GetCurrentHostUpAxis(doc);
+ if (!FragmentRepresentativePoseHelper.TryInterpretRepresentativeFrame(
+ rawRepresentativeFrame,
+ fragmentDefaultUpAxis,
+ hostUpAxis,
+ out var representativeFrame))
+ {
+ LogManager.Warning($"[真实物体参考姿态] {sourceObject.DisplayName} fragment 姿态解释失败");
+ return false;
+ }
+
+ referenceRotation = representativeFrame.Rotation;
_realObjectReferenceRotation = referenceRotation;
+ _realObjectReferenceAxisX = representativeFrame.AxisX;
+ _realObjectReferenceAxisY = representativeFrame.AxisY;
+ _realObjectReferenceAxisZ = representativeFrame.AxisZ;
_hasRealObjectReferenceRotation = true;
- Matrix4x4 linear = Matrix4x4.CreateFromQuaternion(referenceRotation);
LogManager.Info(
$"[真实物体参考姿态] {sourceObject.DisplayName} 使用 fragment 代表姿态: " +
- $"X=({linear.M11:F4},{linear.M21:F4},{linear.M31:F4}), " +
- $"Y=({linear.M12:F4},{linear.M22:F4},{linear.M32:F4}), " +
- $"Z=({linear.M13:F4},{linear.M23:F4},{linear.M33:F4}), " +
- $"fragment数量={fragmentMatrices.Count}");
+ $"X=({_realObjectReferenceAxisX.X:F4},{_realObjectReferenceAxisX.Y:F4},{_realObjectReferenceAxisX.Z:F4}), " +
+ $"Y=({_realObjectReferenceAxisY.X:F4},{_realObjectReferenceAxisY.Y:F4},{_realObjectReferenceAxisY.Z:F4}), " +
+ $"Z=({_realObjectReferenceAxisZ.X:F4},{_realObjectReferenceAxisZ.Y:F4},{_realObjectReferenceAxisZ.Z:F4}), " +
+ $"fragment数量={fragmentMatrices.Count}, Fragment默认Up={fragmentDefaultUpAxis}, 模型Up={hostUpAxis}");
return true;
}
finally
@@ -3816,9 +3897,36 @@ namespace NavisworksTransport.Core.Animation
}
}
+ private void SyncTrackedRotationToObjectReference(ModelItem sourceObject, bool isVirtualObject)
+ {
+ if (sourceObject == null)
+ {
+ _trackedRotation = Rotation3D.Identity;
+ _hasTrackedRotation = false;
+ _currentYaw = 0.0;
+ return;
+ }
+
+ if (!isVirtualObject && TryCaptureRealObjectReferenceRotation(sourceObject, out Quaternion referenceRotation))
+ {
+ var adapter = CoordinateSystemManager.Instance.CreateHostAdapter();
+ _trackedRotation = adapter.FromHostQuaternion(referenceRotation);
+ _hasTrackedRotation = true;
+ _currentYaw = ModelItemTransformHelper.GetYawFromRotation(_trackedRotation);
+ return;
+ }
+
+ _currentYaw = ModelItemTransformHelper.GetYawFromTransform(sourceObject.Transform);
+ _trackedRotation = sourceObject.Transform.Factor().Rotation;
+ _hasTrackedRotation = true;
+ }
+
private void ResetRealObjectReferenceRotation()
{
_realObjectReferenceRotation = Quaternion.Identity;
+ _realObjectReferenceAxisX = Vector3.UnitX;
+ _realObjectReferenceAxisY = Vector3.UnitY;
+ _realObjectReferenceAxisZ = Vector3.UnitZ;
_hasRealObjectReferenceRotation = false;
}
diff --git a/src/UI/WPF/ViewModels/SystemManagementViewModel.cs b/src/UI/WPF/ViewModels/SystemManagementViewModel.cs
index 432b116..7695501 100644
--- a/src/UI/WPF/ViewModels/SystemManagementViewModel.cs
+++ b/src/UI/WPF/ViewModels/SystemManagementViewModel.cs
@@ -27,7 +27,6 @@ namespace NavisworksTransport.UI.WPF.ViewModels
#region 私有字段
private readonly UIStateManager _uiStateManager;
- private readonly Dictionary _documentFragmentDefaultUpAxes = new Dictionary(StringComparer.OrdinalIgnoreCase);
private readonly HashSet _documentRotationHintShown = new HashSet(StringComparer.OrdinalIgnoreCase);
// 日志管理字段
@@ -472,7 +471,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
private void RefreshFragmentDefaultUpAxisSelectionFromCurrentDocument()
{
- string resolvedAxis = GetCurrentDocumentFragmentDefaultUpAxis();
+ string resolvedAxis = FragmentDefaultUpContext.GetCurrentDocumentFragmentDefaultUpAxis();
if (_selectedFragmentDefaultUpAxis != resolvedAxis)
{
SetProperty(ref _selectedFragmentDefaultUpAxis, resolvedAxis, nameof(SelectedFragmentDefaultUpAxis));
@@ -481,79 +480,27 @@ namespace NavisworksTransport.UI.WPF.ViewModels
private string GetCurrentDocumentFragmentDefaultUpAxis()
{
- string documentKey = GetCurrentDocumentKey();
- if (!string.IsNullOrWhiteSpace(documentKey) &&
- _documentFragmentDefaultUpAxes.TryGetValue(documentKey, out string storedAxis) &&
- IsSupportedFragmentDefaultUpAxis(storedAxis))
- {
- return storedAxis;
- }
-
- return GetCurrentHostUpAxis();
+ return FragmentDefaultUpContext.GetCurrentDocumentFragmentDefaultUpAxis();
}
private void SaveCurrentDocumentFragmentDefaultUpAxis(string axisName)
{
- if (!IsSupportedFragmentDefaultUpAxis(axisName))
- {
- return;
- }
-
- string documentKey = GetCurrentDocumentKey();
- if (string.IsNullOrWhiteSpace(documentKey))
- {
- return;
- }
-
- _documentFragmentDefaultUpAxes[documentKey] = axisName;
+ FragmentDefaultUpContext.SetCurrentDocumentFragmentDefaultUpAxis(axisName);
}
private string GetCurrentDocumentKey()
{
- var doc = Autodesk.Navisworks.Api.Application.ActiveDocument;
- if (doc == null || doc.IsClear)
- {
- return string.Empty;
- }
-
- if (!string.IsNullOrWhiteSpace(doc.FileName))
- {
- return doc.FileName;
- }
-
- return doc.Title ?? string.Empty;
+ return FragmentDefaultUpContext.GetCurrentDocumentKey();
}
private string GetCurrentHostUpAxis()
{
- var doc = Autodesk.Navisworks.Api.Application.ActiveDocument;
- if (doc != null && !doc.IsClear)
- {
- UnitVector3D worldUp = doc.CurrentViewpoint.Value.WorldUpVector;
- if (!worldUp.IsZero)
- {
- return Math.Abs(worldUp.Y) >= Math.Abs(worldUp.Z) ? "Y" : "Z";
- }
-
- Vector3D documentUp = doc.UpVector;
- if (!documentUp.IsZero)
- {
- return Math.Abs(documentUp.Y) >= Math.Abs(documentUp.Z) ? "Y" : "Z";
- }
- }
-
- if (string.Equals(_selectedCoordinateSystem, "YUp", StringComparison.OrdinalIgnoreCase))
- {
- return "Y";
- }
-
- return "Z";
+ return FragmentDefaultUpContext.GetCurrentHostUpAxis();
}
private static bool IsSupportedFragmentDefaultUpAxis(string axisName)
{
- return string.Equals(axisName, "Y", StringComparison.OrdinalIgnoreCase) ||
- string.Equals(axisName, "Z", StringComparison.OrdinalIgnoreCase);
+ return FragmentDefaultUpContext.IsSupportedAxis(axisName);
}
private void ShowRootModelRotationHintIfNeeded()
diff --git a/src/Utils/CoordinateSystem/FragmentDefaultUpContext.cs b/src/Utils/CoordinateSystem/FragmentDefaultUpContext.cs
new file mode 100644
index 0000000..32ab907
--- /dev/null
+++ b/src/Utils/CoordinateSystem/FragmentDefaultUpContext.cs
@@ -0,0 +1,90 @@
+using System;
+using System.Collections.Concurrent;
+using Autodesk.Navisworks.Api;
+
+namespace NavisworksTransport.Utils.CoordinateSystem
+{
+ ///
+ /// 当前文档级的 fragment 默认 Up 运行时状态。
+ /// 不写入系统配置,只在当前进程内按文档维持。
+ ///
+ internal static class FragmentDefaultUpContext
+ {
+ private static readonly ConcurrentDictionary DocumentFragmentDefaultUpAxes =
+ new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase);
+
+ public static string GetCurrentDocumentKey(Document doc = null)
+ {
+ doc = doc ?? Autodesk.Navisworks.Api.Application.ActiveDocument;
+ if (doc == null || doc.IsClear)
+ {
+ return string.Empty;
+ }
+
+ if (!string.IsNullOrWhiteSpace(doc.FileName))
+ {
+ return doc.FileName;
+ }
+
+ return doc.Title ?? string.Empty;
+ }
+
+ public static string GetCurrentHostUpAxis(Document doc = null)
+ {
+ doc = doc ?? Autodesk.Navisworks.Api.Application.ActiveDocument;
+ if (doc != null && !doc.IsClear)
+ {
+ UnitVector3D worldUp = doc.CurrentViewpoint.Value.WorldUpVector;
+ if (!worldUp.IsZero)
+ {
+ return Math.Abs(worldUp.Y) >= Math.Abs(worldUp.Z) ? "Y" : "Z";
+ }
+
+ Vector3D documentUp = doc.UpVector;
+ if (!documentUp.IsZero)
+ {
+ return Math.Abs(documentUp.Y) >= Math.Abs(documentUp.Z) ? "Y" : "Z";
+ }
+ }
+
+ return "Z";
+ }
+
+ public static string GetCurrentDocumentFragmentDefaultUpAxis(Document doc = null)
+ {
+ doc = doc ?? Autodesk.Navisworks.Api.Application.ActiveDocument;
+ string documentKey = GetCurrentDocumentKey(doc);
+ if (!string.IsNullOrWhiteSpace(documentKey) &&
+ DocumentFragmentDefaultUpAxes.TryGetValue(documentKey, out string storedAxis) &&
+ IsSupportedAxis(storedAxis))
+ {
+ return storedAxis;
+ }
+
+ return GetCurrentHostUpAxis(doc);
+ }
+
+ public static void SetCurrentDocumentFragmentDefaultUpAxis(string axisName, Document doc = null)
+ {
+ if (!IsSupportedAxis(axisName))
+ {
+ return;
+ }
+
+ doc = doc ?? Autodesk.Navisworks.Api.Application.ActiveDocument;
+ string documentKey = GetCurrentDocumentKey(doc);
+ if (string.IsNullOrWhiteSpace(documentKey))
+ {
+ return;
+ }
+
+ DocumentFragmentDefaultUpAxes[documentKey] = axisName;
+ }
+
+ public static bool IsSupportedAxis(string axisName)
+ {
+ return string.Equals(axisName, "Y", StringComparison.OrdinalIgnoreCase) ||
+ string.Equals(axisName, "Z", StringComparison.OrdinalIgnoreCase);
+ }
+ }
+}
diff --git a/src/Utils/CoordinateSystem/FragmentRepresentativePoseHelper.cs b/src/Utils/CoordinateSystem/FragmentRepresentativePoseHelper.cs
index 98a9ee0..4982a2b 100644
--- a/src/Utils/CoordinateSystem/FragmentRepresentativePoseHelper.cs
+++ b/src/Utils/CoordinateSystem/FragmentRepresentativePoseHelper.cs
@@ -20,6 +20,22 @@ namespace NavisworksTransport.Utils.CoordinateSystem
{
private const float AxisEpsilon = 1e-6f;
+ public readonly struct RepresentativeFrame
+ {
+ public Vector3 AxisX { get; }
+ public Vector3 AxisY { get; }
+ public Vector3 AxisZ { get; }
+ public Quaternion Rotation { get; }
+
+ public RepresentativeFrame(Vector3 axisX, Vector3 axisY, Vector3 axisZ, Quaternion rotation)
+ {
+ AxisX = axisX;
+ AxisY = axisY;
+ AxisZ = axisZ;
+ Rotation = rotation;
+ }
+ }
+
///
/// 从 ModelItem 的 fragment 几何中提取代表旋转。
///
@@ -73,6 +89,108 @@ namespace NavisworksTransport.Utils.CoordinateSystem
return TryGetRepresentativeRotation(fragmentRotations, out representativeRotation);
}
+ public static bool TryGetRepresentativeFrame(
+ IEnumerable fragmentMatrices,
+ out RepresentativeFrame representativeFrame)
+ {
+ representativeFrame = default;
+
+ if (fragmentMatrices == null)
+ {
+ return false;
+ }
+
+ var rotations = new List();
+ foreach (double[] fragmentMatrix in fragmentMatrices)
+ {
+ if (TryExtractRotationFromFragmentMatrix(fragmentMatrix, out Quaternion fragmentRotation))
+ {
+ rotations.Add(fragmentRotation);
+ }
+ }
+
+ if (!TryGetRepresentativeRotation(rotations, out Quaternion representativeRotation))
+ {
+ return false;
+ }
+
+ Vector3 axisX = Vector3.Normalize(Vector3.Transform(Vector3.UnitX, representativeRotation));
+ Vector3 axisY = Vector3.Normalize(Vector3.Transform(Vector3.UnitY, representativeRotation));
+ Vector3 axisZ = Vector3.Normalize(Vector3.Transform(Vector3.UnitZ, representativeRotation));
+ representativeFrame = new RepresentativeFrame(axisX, axisY, axisZ, representativeRotation);
+ return true;
+ }
+
+ public static bool TryInterpretRepresentativeFrame(
+ RepresentativeFrame rawFrame,
+ string fragmentDefaultUpAxis,
+ string hostUpAxis,
+ out RepresentativeFrame interpretedFrame)
+ {
+ interpretedFrame = default;
+
+ bool hostIsYUp = string.Equals(hostUpAxis, "Y", StringComparison.OrdinalIgnoreCase);
+ bool fragmentIsYUp = string.Equals(fragmentDefaultUpAxis, "Y", StringComparison.OrdinalIgnoreCase);
+
+ Vector3 semanticAxisX = rawFrame.AxisX;
+ Vector3 semanticAxisY;
+ Vector3 semanticAxisZ;
+
+ if (hostIsYUp == fragmentIsYUp)
+ {
+ semanticAxisY = rawFrame.AxisY;
+ semanticAxisZ = rawFrame.AxisZ;
+ }
+ else if (hostIsYUp)
+ {
+ // fragment 按 ZUp 解释,宿主按 YUp 解释:X 保持,Y 取原始 Z,Z 由右手系重建。
+ semanticAxisY = rawFrame.AxisZ;
+ semanticAxisZ = Vector3.Normalize(Vector3.Cross(semanticAxisX, semanticAxisY));
+ }
+ else
+ {
+ // fragment 按 YUp 解释,宿主按 ZUp 解释:X 保持,Z 取原始 Y,Y 由右手系重建。
+ semanticAxisZ = rawFrame.AxisY;
+ semanticAxisY = Vector3.Normalize(Vector3.Cross(semanticAxisZ, semanticAxisX));
+ }
+
+ if (!TryNormalize(semanticAxisX, out semanticAxisX) ||
+ !TryNormalize(semanticAxisY, out semanticAxisY) ||
+ !TryNormalize(semanticAxisZ, out semanticAxisZ))
+ {
+ return false;
+ }
+
+ // 用解释后的三轴重新正交化,保证右手系稳定。
+ Vector3 axisX = semanticAxisX;
+ Vector3 axisY = semanticAxisY - Vector3.Dot(semanticAxisY, axisX) * axisX;
+ if (!TryNormalize(axisY, out axisY))
+ {
+ return false;
+ }
+
+ Vector3 axisZ = Vector3.Cross(axisX, axisY);
+ if (!TryNormalize(axisZ, out axisZ))
+ {
+ return false;
+ }
+
+ if (hostIsYUp && Vector3.Dot(axisY, semanticAxisY) < 0.0f)
+ {
+ axisY = -axisY;
+ axisZ = -axisZ;
+ }
+ else if (!hostIsYUp && Vector3.Dot(axisZ, semanticAxisZ) < 0.0f)
+ {
+ axisY = -axisY;
+ axisZ = -axisZ;
+ }
+
+ Quaternion rotation = Quaternion.Normalize(CreateQuaternionFromBasis(axisX, axisY, axisZ));
+ interpretedFrame = new RepresentativeFrame(axisX, axisY, axisZ, rotation);
+ return true;
+ }
+
///
/// 从 fragment 旋转集合中提取代表旋转。
///
From 6c74ea13199699fc11d2b706514616d713ea09f0 Mon Sep 17 00:00:00 2001
From: tian <11429339@qq.com>
Date: Tue, 24 Mar 2026 21:59:53 +0800
Subject: [PATCH 26/87] Fix host-axis rotation correction for real objects
---
.../HostCoordinateAdapterTests.cs | 129 +++++++++++++++++-
.../ViewModels/AnimationControlViewModel.cs | 2 -
.../CoordinateSystem/HostCoordinateAdapter.cs | 44 +++++-
.../LocalEulerRotationCorrection.cs | 20 +++
4 files changed, 184 insertions(+), 11 deletions(-)
diff --git a/UnitTests/CoordinateSystem/HostCoordinateAdapterTests.cs b/UnitTests/CoordinateSystem/HostCoordinateAdapterTests.cs
index 12d773a..c206309 100644
--- a/UnitTests/CoordinateSystem/HostCoordinateAdapterTests.cs
+++ b/UnitTests/CoordinateSystem/HostCoordinateAdapterTests.cs
@@ -169,6 +169,28 @@ namespace NavisworksTransport.UnitTests.CoordinateSystem
AssertVector(rotatedUp, 0.0, 1.0, 0.0);
}
+ [TestMethod]
+ public void YUp_HostRotationCorrection_X90_ShouldRotateHostZIntoHostY()
+ {
+ var adapter = new HostCoordinateAdapter(CoordinateSystemType.YUp);
+ Quaternion hostCorrection = adapter.CreateHostRotationCorrection(
+ new LocalEulerRotationCorrection(90.0, 0.0, 0.0));
+
+ Vector3 rotatedHostZ = Vector3.Transform(Vector3.UnitZ, hostCorrection);
+ AssertVector(rotatedHostZ, 0.0, -1.0, 0.0);
+ }
+
+ [TestMethod]
+ public void YUp_HostRotationCorrection_Z90_ShouldRotateHostXIntoNegativeHostY()
+ {
+ var adapter = new HostCoordinateAdapter(CoordinateSystemType.YUp);
+ Quaternion hostCorrection = adapter.CreateHostRotationCorrection(
+ new LocalEulerRotationCorrection(0.0, 0.0, 90.0));
+
+ Vector3 rotatedHostX = Vector3.Transform(Vector3.UnitX, hostCorrection);
+ AssertVector(rotatedHostX, 0.0, 1.0, 0.0);
+ }
+
[TestMethod]
public void ComposeHostQuaternion_ShouldApplyHostCorrectionAfterBaseline()
{
@@ -183,7 +205,7 @@ namespace NavisworksTransport.UnitTests.CoordinateSystem
Assert.AreEqual(0.0, linear.M11, 1e-6);
Assert.AreEqual(0.0, linear.M21, 1e-6);
- Assert.AreEqual(1.0, linear.M31, 1e-6);
+ Assert.AreEqual(-1.0, linear.M31, 1e-6);
Assert.AreEqual(0.0, linear.M12, 1e-6);
Assert.AreEqual(1.0, linear.M22, 1e-6);
@@ -214,10 +236,10 @@ namespace NavisworksTransport.UnitTests.CoordinateSystem
Matrix4x4 linear = Matrix4x4.CreateFromQuaternion(composed);
Assert.AreEqual(0.0, linear.M11, 1e-6);
- Assert.AreEqual(-1.0, linear.M21, 1e-6);
+ Assert.AreEqual(1.0, linear.M21, 1e-6);
Assert.AreEqual(0.0, linear.M31, 1e-6);
- Assert.AreEqual(1.0, linear.M12, 1e-6);
+ Assert.AreEqual(-1.0, linear.M12, 1e-6);
Assert.AreEqual(0.0, linear.M22, 1e-6);
Assert.AreEqual(0.0, linear.M32, 1e-6);
@@ -226,6 +248,63 @@ namespace NavisworksTransport.UnitTests.CoordinateSystem
Assert.AreEqual(1.0, linear.M33, 1e-6);
}
+ [TestMethod]
+ public void YUp_ComposeHostQuaternion_ForGroundBaseline_ShouldRotateAxesAroundHostXAxis()
+ {
+ var adapter = new HostCoordinateAdapter(CoordinateSystemType.YUp);
+ Vector3 baselineX = new Vector3(-0.8987f, 0.0000f, -0.4386f);
+ Vector3 baselineY = new Vector3(0.0000f, 1.0000f, 0.0000f);
+ Vector3 baselineZ = new Vector3(0.4386f, 0.0000f, -0.8987f);
+ Quaternion baseline = CreateQuaternionFromAxes(baselineX, baselineY, baselineZ);
+
+ var correction = new LocalEulerRotationCorrection(90.0, 0.0, 0.0);
+ Quaternion composed = adapter.ComposeHostQuaternion(baseline, correction);
+ Quaternion hostCorrection = adapter.CreateHostRotationCorrection(correction);
+ Matrix4x4 linear = Matrix4x4.CreateFromQuaternion(composed);
+
+ AssertAxis(linear, 1, Vector3.Transform(baselineX, hostCorrection));
+ AssertAxis(linear, 2, Vector3.Transform(baselineY, hostCorrection));
+ AssertAxis(linear, 3, Vector3.Transform(baselineZ, hostCorrection));
+ }
+
+ [TestMethod]
+ public void YUp_ComposeHostQuaternion_ForGroundBaseline_ShouldRotateAxesAroundHostYAxis()
+ {
+ var adapter = new HostCoordinateAdapter(CoordinateSystemType.YUp);
+ Vector3 baselineX = new Vector3(-0.8987f, 0.0000f, -0.4386f);
+ Vector3 baselineY = new Vector3(0.0000f, 1.0000f, 0.0000f);
+ Vector3 baselineZ = new Vector3(0.4386f, 0.0000f, -0.8987f);
+ Quaternion baseline = CreateQuaternionFromAxes(baselineX, baselineY, baselineZ);
+
+ var correction = new LocalEulerRotationCorrection(0.0, 90.0, 0.0);
+ Quaternion composed = adapter.ComposeHostQuaternion(baseline, correction);
+ Quaternion hostCorrection = adapter.CreateHostRotationCorrection(correction);
+ Matrix4x4 linear = Matrix4x4.CreateFromQuaternion(composed);
+
+ AssertAxis(linear, 1, Vector3.Transform(baselineX, hostCorrection));
+ AssertAxis(linear, 2, Vector3.Transform(baselineY, hostCorrection));
+ AssertAxis(linear, 3, Vector3.Transform(baselineZ, hostCorrection));
+ }
+
+ [TestMethod]
+ public void YUp_ComposeHostQuaternion_ForGroundBaseline_ShouldRotateAxesAroundHostZAxis()
+ {
+ var adapter = new HostCoordinateAdapter(CoordinateSystemType.YUp);
+ Vector3 baselineX = new Vector3(-0.8987f, 0.0000f, -0.4386f);
+ Vector3 baselineY = new Vector3(0.0000f, 1.0000f, 0.0000f);
+ Vector3 baselineZ = new Vector3(0.4386f, 0.0000f, -0.8987f);
+ Quaternion baseline = CreateQuaternionFromAxes(baselineX, baselineY, baselineZ);
+
+ var correction = new LocalEulerRotationCorrection(0.0, 0.0, 90.0);
+ Quaternion composed = adapter.ComposeHostQuaternion(baseline, correction);
+ Quaternion hostCorrection = adapter.CreateHostRotationCorrection(correction);
+ Matrix4x4 linear = Matrix4x4.CreateFromQuaternion(composed);
+
+ AssertAxis(linear, 1, Vector3.Transform(baselineX, hostCorrection));
+ AssertAxis(linear, 2, Vector3.Transform(baselineY, hostCorrection));
+ AssertAxis(linear, 3, Vector3.Transform(baselineZ, hostCorrection));
+ }
+
private static void AssertPoint(Vector3 point, double x, double y, double z)
{
Assert.AreEqual(x, point.X, 1e-9);
@@ -233,11 +312,47 @@ namespace NavisworksTransport.UnitTests.CoordinateSystem
Assert.AreEqual(z, point.Z, 1e-9);
}
- private static void AssertVector(Vector3 vector, double x, double y, double z)
+ private static void AssertVector(Vector3 vector, double x, double y, double z, double tolerance = 1e-6)
{
- Assert.AreEqual(x, vector.X, 1e-9);
- Assert.AreEqual(y, vector.Y, 1e-9);
- Assert.AreEqual(z, vector.Z, 1e-9);
+ Assert.AreEqual(x, vector.X, tolerance);
+ Assert.AreEqual(y, vector.Y, tolerance);
+ Assert.AreEqual(z, vector.Z, tolerance);
+ }
+
+ private static Quaternion CreateQuaternionFromAxes(Vector3 xAxis, Vector3 yAxis, Vector3 zAxis)
+ {
+ Matrix4x4 basis = new Matrix4x4(
+ xAxis.X, yAxis.X, zAxis.X, 0f,
+ xAxis.Y, yAxis.Y, zAxis.Y, 0f,
+ xAxis.Z, yAxis.Z, zAxis.Z, 0f,
+ 0f, 0f, 0f, 1f);
+ return Quaternion.Normalize(Quaternion.CreateFromRotationMatrix(basis));
+ }
+
+ private static void AssertAxis(Matrix4x4 linear, int axisIndex, Vector3 expectedAxis, double tolerance = 1e-4)
+ {
+ Vector3 normalizedExpected = Vector3.Normalize(expectedAxis);
+ switch (axisIndex)
+ {
+ case 1:
+ Assert.AreEqual(normalizedExpected.X, linear.M11, tolerance);
+ Assert.AreEqual(normalizedExpected.Y, linear.M21, tolerance);
+ Assert.AreEqual(normalizedExpected.Z, linear.M31, tolerance);
+ break;
+ case 2:
+ Assert.AreEqual(normalizedExpected.X, linear.M12, tolerance);
+ Assert.AreEqual(normalizedExpected.Y, linear.M22, tolerance);
+ Assert.AreEqual(normalizedExpected.Z, linear.M32, tolerance);
+ break;
+ case 3:
+ Assert.AreEqual(normalizedExpected.X, linear.M13, tolerance);
+ Assert.AreEqual(normalizedExpected.Y, linear.M23, tolerance);
+ Assert.AreEqual(normalizedExpected.Z, linear.M33, tolerance);
+ break;
+ default:
+ Assert.Fail($"未知轴索引: {axisIndex}");
+ break;
+ }
}
}
}
diff --git a/src/UI/WPF/ViewModels/AnimationControlViewModel.cs b/src/UI/WPF/ViewModels/AnimationControlViewModel.cs
index ac82b2c..635d6b2 100644
--- a/src/UI/WPF/ViewModels/AnimationControlViewModel.cs
+++ b/src/UI/WPF/ViewModels/AnimationControlViewModel.cs
@@ -1737,8 +1737,6 @@ namespace NavisworksTransport.UI.WPF.ViewModels
LogManager.Debug($"[选择物体] 保持当前角度修正值(同一物体)");
}
- // 显式同步一次到当前路径起点,避免依赖 setter 分支导致真实物体停留在 CAD 原位。
- MoveAnimatedObjectToPathStart();
}
catch (Exception ex)
{
diff --git a/src/Utils/CoordinateSystem/HostCoordinateAdapter.cs b/src/Utils/CoordinateSystem/HostCoordinateAdapter.cs
index b780a22..ef2bf29 100644
--- a/src/Utils/CoordinateSystem/HostCoordinateAdapter.cs
+++ b/src/Utils/CoordinateSystem/HostCoordinateAdapter.cs
@@ -186,6 +186,15 @@ namespace NavisworksTransport.Utils.CoordinateSystem
return new Rotation3D(hostQuaternion.X, hostQuaternion.Y, hostQuaternion.Z, hostQuaternion.W);
}
+ ///
+ /// 将“宿主坐标系语义下”的四元数直接包装成 Navisworks 的 Rotation3D。
+ ///
+ /// 项目硬约束:
+ /// - Rotation3D(x, y, z, w) 的参数顺序固定是 (x, y, z, w)
+ /// - Rotation3D.A/B/C/D 分别对应 x/y/z/w
+ ///
+ /// 这里不做任何轴交换、符号翻转或重新解释,只负责把已经确定好的宿主四元数写入 Navisworks。
+ ///
public Rotation3D FromHostQuaternion(Quaternion hostRotation)
{
Quaternion normalizedHostRotation = Quaternion.Normalize(hostRotation);
@@ -196,6 +205,17 @@ namespace NavisworksTransport.Utils.CoordinateSystem
normalizedHostRotation.W);
}
+ ///
+ /// 根据宿主坐标系的世界轴,构造一个“宿主 X/Y/Z 角度修正”四元数。
+ ///
+ /// 关键约束:
+ /// - correction.XDegrees 永远表示绕宿主世界 X 轴旋转
+ /// - correction.YDegrees 永远表示绕宿主世界 Y 轴旋转
+ /// - correction.ZDegrees 永远表示绕宿主世界 Z 轴旋转
+ /// - 角度应用顺序固定为 X -> Y -> Z,对应四元数组合 qz * qy * qx
+ ///
+ /// 本方法只负责生成“宿主世界轴修正”本身,不负责和某个基姿态做组合。
+ ///
public Quaternion CreateHostRotationCorrection(LocalEulerRotationCorrection correction)
{
if (correction.IsZero)
@@ -209,15 +229,35 @@ namespace NavisworksTransport.Utils.CoordinateSystem
return Quaternion.Normalize(qz * qy * qx);
}
+ ///
+ /// 将宿主世界轴角度修正叠加到一个已经位于宿主坐标系中的基姿态上。
+ ///
+ /// 参数语义:
+ /// - baselineQuaternion: 已经求好的宿主基姿态
+ /// - correction: 仍按宿主世界 X/Y/Z 解释的角度修正
+ ///
+ /// 注意:
+ /// - 这里的组合顺序直接决定“修正是绕宿主世界轴”还是“绕基姿态当前轴”
+ /// - 后续若修改乘法顺序,必须同步更新真实场景测试,不能靠肉眼猜
+ /// - 本方法只用于宿主坐标系;不要把资产坐标系或内部坐标系的四元数传进来混用
+ ///
public Quaternion ComposeHostQuaternion(Quaternion baselineQuaternion, LocalEulerRotationCorrection correction)
{
Quaternion correctionQuaternion = CreateHostRotationCorrection(correction);
- return Quaternion.Normalize(correctionQuaternion * baselineQuaternion);
+ Quaternion inverseCorrectionQuaternion = Quaternion.Conjugate(correctionQuaternion);
+ return Quaternion.Normalize(baselineQuaternion * inverseCorrectionQuaternion);
}
///
/// 将宿主 X/Y/Z 三轴旋转角转换为内部 Canonical Space 中的旋转四元数。
- /// 约定:先绕宿主 X,再绕宿主 Y,最后绕宿主 Z。
+ ///
+ /// 关键约束:
+ /// - 输入角度仍然按宿主世界 X/Y/Z 解释
+ /// - 输出四元数位于内部坐标系
+ /// - 角度应用顺序固定为 X -> Y -> Z,对应四元数组合 qz * qy * qx
+ ///
+ /// 该方法只做“宿主世界轴 -> 内部坐标系轴”的映射,
+ /// 不负责决定真实物体、虚拟物体或路径姿态链应该如何消费这个修正。
///
public Quaternion CreateCanonicalRotationCorrection(LocalEulerRotationCorrection correction)
{
diff --git a/src/Utils/CoordinateSystem/LocalEulerRotationCorrection.cs b/src/Utils/CoordinateSystem/LocalEulerRotationCorrection.cs
index 61f8e27..6f9d3a9 100644
--- a/src/Utils/CoordinateSystem/LocalEulerRotationCorrection.cs
+++ b/src/Utils/CoordinateSystem/LocalEulerRotationCorrection.cs
@@ -7,6 +7,15 @@ namespace NavisworksTransport.Utils.CoordinateSystem
///
/// 三轴旋转修正角(单位:度)。
/// 该结构本身只保存 X/Y/Z 三个角度值,不直接绑定宿主/内部/资产语义。
+ ///
+ /// 关键约束:
+ /// - 构造参数顺序固定为 (xDegrees, yDegrees, zDegrees)
+ /// - 属性语义固定为 XDegrees / YDegrees / ZDegrees
+ /// - 本结构只表示“沿某三根给定轴分别旋转多少度”,不负责决定这些轴属于宿主坐标系、内部坐标系还是资产坐标系
+ ///
+ /// 因此,调用方必须先明确:
+ /// 1. 当前三根轴分别是什么
+ /// 2. 这些角度是按哪种坐标语义解释
///
public struct LocalEulerRotationCorrection : IEquatable
{
@@ -28,6 +37,17 @@ namespace NavisworksTransport.Utils.CoordinateSystem
Math.Abs(YDegrees) < 1e-9 &&
Math.Abs(ZDegrees) < 1e-9;
+ ///
+ /// 基于调用方传入的三根轴,按 X -> Y -> Z 的角度顺序构造旋转四元数。
+ ///
+ /// 注意:
+ /// - 参数顺序固定为 (axisX, axisY, axisZ)
+ /// - 角度来源固定为 (XDegrees, YDegrees, ZDegrees)
+ /// - 组合顺序固定为 qz * qy * qx
+ ///
+ /// 这里的 qz * qy * qx 只表示“先应用 X,再应用 Y,再应用 Z”这组三轴修正,
+ /// 不自动代表宿主世界轴或物体当前姿态轴。轴语义完全由调用方决定。
+ ///
public Quaternion CreateQuaternion(Vector3 axisX, Vector3 axisY, Vector3 axisZ)
{
Quaternion qx = Quaternion.CreateFromAxisAngle(Vector3.Normalize(axisX), DegreesToRadians(XDegrees));
From 138eb43a67ecdbf42abfa65763d7a8304ad2e763 Mon Sep 17 00:00:00 2001
From: tian <11429339@qq.com>
Date: Wed, 25 Mar 2026 00:11:07 +0800
Subject: [PATCH 27/87] Unify real-object rail pose interpretation
---
AGENTS.md | 27 ++
NavisworksTransport.UnitTests.csproj | 2 +
TransportPlugin.csproj | 2 +
.../HostCoordinateAdapterTests.cs | 57 ++++
...alObjectRailAxisConventionResolverTests.cs | 89 ++++++
.../RealObjectRailExtentResolverTests.cs | 142 ++++++++++
doc/working/current-engineering-state.md | 51 +++-
src/Core/Animation/PathAnimationManager.cs | 255 ++++++++++++++++--
.../ViewModels/AnimationControlViewModel.cs | 44 ++-
.../ViewModels/SystemManagementViewModel.cs | 57 +++-
.../RealObjectRailAxisConventionResolver.cs | 132 +++++++++
.../RealObjectRailExtentResolver.cs | 77 ++++++
12 files changed, 901 insertions(+), 34 deletions(-)
create mode 100644 UnitTests/CoordinateSystem/RealObjectRailAxisConventionResolverTests.cs
create mode 100644 UnitTests/CoordinateSystem/RealObjectRailExtentResolverTests.cs
create mode 100644 src/Utils/CoordinateSystem/RealObjectRailAxisConventionResolver.cs
create mode 100644 src/Utils/CoordinateSystem/RealObjectRailExtentResolver.cs
diff --git a/AGENTS.md b/AGENTS.md
index 9a438af..b58d67a 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -172,6 +172,20 @@ var rotation = new Rotation3D(qw, qx, qy, qz); // 错误
- 可以视为直接生活在宿主坐标系里
- 角度调整对话框里的 `X/Y/Z`,对真实物体应按**宿主坐标系**消费
+#### 真实物体参考姿态
+
+- 真实物体不要再直接依赖 `ModelItem.Transform.Factor().Rotation`
+ - 对很多 Revit 导入件,这个值是单位旋转,不代表真实显示姿态
+- 真实物体参考姿态应优先来自 fragment 代表姿态
+- `Fragment默认Up` 的用途是:
+ - 把 fragment 参考框架解释成当前宿主坐标语义下的真实姿态
+ - 不是仅仅挑一个“竖直候选轴”
+- 在真实物体链路里,必须先完成“fragment 姿态解释”,再谈:
+ - 前进方向
+ - 路径对齐
+ - 角度调整
+- `_trackedRotation` 对真实物体必须优先跟踪“解释后的真实参考姿态”,不能回退成 `Transform` 的单位旋转
+
#### 虚拟物体
- 有明确资产坐标系
@@ -194,6 +208,15 @@ var rotation = new Rotation3D(qw, qx, qy, qz); // 错误
- 不能像平面路径那样在宿主空间随意补旋转
- 必须并入 `canonical -> rail pose` 链
- `Rail 0°` 基线必须稳定,不能被角度修正逻辑污染
+- `Rail` 真实物体不应再退回默认 `PositiveX / PositiveY`
+- 三类路径都必须先复用同一个“对象姿态解释层”:
+ - `Ground / Hoisting`
+ - `forward = 路径方向`
+ - `up = 宿主 up`
+ - `Rail`
+ - `forward = rail 切向`
+ - `up = rail 法向 / preferred normal`
+- 统一的是“对象参考姿态来源与解释方式”,不是把三类路径都强行改成同一个 `up`
### 4.4 通行空间与物体姿态的关系
@@ -209,6 +232,10 @@ var rotation = new Rotation3D(qw, qx, qy, qz); // 错误
- 是否一条链用了“原始高度”
- 另一条链用了“旋转后法线尺寸”
+- `Rail` 真实物体尤其要检查:
+ - 起点/逐帧中心偏移是否仍在用原始 `objectHeight`
+ - 通行空间是否仍在用旧 `RailAssetConvention`
+ - 通行空间、路径偏移、最终姿态是否共享同一套“最终姿态尺寸语义”
### 4.5 终端安装仿真
diff --git a/NavisworksTransport.UnitTests.csproj b/NavisworksTransport.UnitTests.csproj
index dcde989..21c8463 100644
--- a/NavisworksTransport.UnitTests.csproj
+++ b/NavisworksTransport.UnitTests.csproj
@@ -64,6 +64,8 @@
+
+
diff --git a/TransportPlugin.csproj b/TransportPlugin.csproj
index e2b3465..4d8bfaa 100644
--- a/TransportPlugin.csproj
+++ b/TransportPlugin.csproj
@@ -343,6 +343,8 @@
+
+
diff --git a/UnitTests/CoordinateSystem/HostCoordinateAdapterTests.cs b/UnitTests/CoordinateSystem/HostCoordinateAdapterTests.cs
index c206309..3ea652f 100644
--- a/UnitTests/CoordinateSystem/HostCoordinateAdapterTests.cs
+++ b/UnitTests/CoordinateSystem/HostCoordinateAdapterTests.cs
@@ -305,6 +305,63 @@ namespace NavisworksTransport.UnitTests.CoordinateSystem
AssertAxis(linear, 3, Vector3.Transform(baselineZ, hostCorrection));
}
+ [TestMethod]
+ public void YUp_ComposeHostQuaternion_ForRailBaseline_ShouldRotateAxesAroundHostXAxis()
+ {
+ var adapter = new HostCoordinateAdapter(CoordinateSystemType.YUp);
+ Vector3 baselineX = new Vector3(-0.9900f, 0.1403f, -0.0163f);
+ Vector3 baselineY = new Vector3(0.1403f, 0.9901f, 0.0000f);
+ Vector3 baselineZ = new Vector3(0.0161f, -0.0023f, -0.9999f);
+ Quaternion baseline = CreateQuaternionFromAxes(baselineX, baselineY, baselineZ);
+
+ var correction = new LocalEulerRotationCorrection(90.0, 0.0, 0.0);
+ Quaternion composed = adapter.ComposeHostQuaternion(baseline, correction);
+ Quaternion hostCorrection = adapter.CreateHostRotationCorrection(correction);
+ Matrix4x4 linear = Matrix4x4.CreateFromQuaternion(composed);
+
+ AssertAxis(linear, 1, Vector3.Transform(baselineX, hostCorrection));
+ AssertAxis(linear, 2, Vector3.Transform(baselineY, hostCorrection));
+ AssertAxis(linear, 3, Vector3.Transform(baselineZ, hostCorrection));
+ }
+
+ [TestMethod]
+ public void YUp_ComposeHostQuaternion_ForRailBaseline_ShouldRotateAxesAroundHostYAxis()
+ {
+ var adapter = new HostCoordinateAdapter(CoordinateSystemType.YUp);
+ Vector3 baselineX = new Vector3(-0.9900f, 0.1403f, -0.0163f);
+ Vector3 baselineY = new Vector3(0.1403f, 0.9901f, 0.0000f);
+ Vector3 baselineZ = new Vector3(0.0161f, -0.0023f, -0.9999f);
+ Quaternion baseline = CreateQuaternionFromAxes(baselineX, baselineY, baselineZ);
+
+ var correction = new LocalEulerRotationCorrection(0.0, 90.0, 0.0);
+ Quaternion composed = adapter.ComposeHostQuaternion(baseline, correction);
+ Quaternion hostCorrection = adapter.CreateHostRotationCorrection(correction);
+ Matrix4x4 linear = Matrix4x4.CreateFromQuaternion(composed);
+
+ AssertAxis(linear, 1, Vector3.Transform(baselineX, hostCorrection));
+ AssertAxis(linear, 2, Vector3.Transform(baselineY, hostCorrection));
+ AssertAxis(linear, 3, Vector3.Transform(baselineZ, hostCorrection));
+ }
+
+ [TestMethod]
+ public void YUp_ComposeHostQuaternion_ForRailBaseline_ShouldRotateAxesAroundHostZAxis()
+ {
+ var adapter = new HostCoordinateAdapter(CoordinateSystemType.YUp);
+ Vector3 baselineX = new Vector3(-0.9900f, 0.1403f, -0.0163f);
+ Vector3 baselineY = new Vector3(0.1403f, 0.9901f, 0.0000f);
+ Vector3 baselineZ = new Vector3(0.0161f, -0.0023f, -0.9999f);
+ Quaternion baseline = CreateQuaternionFromAxes(baselineX, baselineY, baselineZ);
+
+ var correction = new LocalEulerRotationCorrection(0.0, 0.0, 90.0);
+ Quaternion composed = adapter.ComposeHostQuaternion(baseline, correction);
+ Quaternion hostCorrection = adapter.CreateHostRotationCorrection(correction);
+ Matrix4x4 linear = Matrix4x4.CreateFromQuaternion(composed);
+
+ AssertAxis(linear, 1, Vector3.Transform(baselineX, hostCorrection));
+ AssertAxis(linear, 2, Vector3.Transform(baselineY, hostCorrection));
+ AssertAxis(linear, 3, Vector3.Transform(baselineZ, hostCorrection));
+ }
+
private static void AssertPoint(Vector3 point, double x, double y, double z)
{
Assert.AreEqual(x, point.X, 1e-9);
diff --git a/UnitTests/CoordinateSystem/RealObjectRailAxisConventionResolverTests.cs b/UnitTests/CoordinateSystem/RealObjectRailAxisConventionResolverTests.cs
new file mode 100644
index 0000000..27284da
--- /dev/null
+++ b/UnitTests/CoordinateSystem/RealObjectRailAxisConventionResolverTests.cs
@@ -0,0 +1,89 @@
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using NavisworksTransport.Utils.CoordinateSystem;
+using System.Numerics;
+
+namespace NavisworksTransport.UnitTests.CoordinateSystem
+{
+ [TestClass]
+ public class RealObjectRailAxisConventionResolverTests
+ {
+ [TestMethod]
+ public void YUp_InterpretedReferencePose_ShouldChoosePositiveXAndPositiveYForRail()
+ {
+ Vector3 referenceAxisX = new Vector3(-1.0f, 0.0f, 0.0f);
+ Vector3 referenceAxisY = new Vector3(0.0f, 1.0f, 0.0f);
+ Vector3 referenceAxisZ = new Vector3(0.0f, 0.0f, -1.0f);
+ Vector3 desiredForward = Vector3.Normalize(new Vector3(-0.8987f, 0.0f, 0.4386f));
+
+ bool ok = RealObjectRailAxisConventionResolver.TryResolve(
+ referenceAxisX,
+ referenceAxisY,
+ referenceAxisZ,
+ desiredForward,
+ CoordinateSystemType.YUp,
+ out ModelAxisConvention convention,
+ out LocalAxisDirection selectedForwardAxis,
+ out Vector3 selectedForwardWorldAxis);
+
+ Assert.IsTrue(ok);
+ Assert.AreEqual(LocalAxisDirection.PositiveX, selectedForwardAxis);
+ Assert.AreEqual(LocalAxisDirection.PositiveX, convention.ForwardAxis);
+ Assert.AreEqual(LocalAxisDirection.PositiveY, convention.UpAxis);
+ AssertVector(selectedForwardWorldAxis, -1.0, 0.0, 0.0);
+ }
+
+ [TestMethod]
+ public void ZUp_InterpretedReferencePose_ShouldChoosePositiveXAndPositiveZForRail()
+ {
+ Vector3 referenceAxisX = new Vector3(-1.0f, 0.0f, 0.0f);
+ Vector3 referenceAxisY = new Vector3(0.0f, 1.0f, 0.0f);
+ Vector3 referenceAxisZ = new Vector3(0.0f, 0.0f, 1.0f);
+ Vector3 desiredForward = Vector3.Normalize(new Vector3(-0.9f, 0.3f, 0.0f));
+
+ bool ok = RealObjectRailAxisConventionResolver.TryResolve(
+ referenceAxisX,
+ referenceAxisY,
+ referenceAxisZ,
+ desiredForward,
+ CoordinateSystemType.ZUp,
+ out ModelAxisConvention convention,
+ out LocalAxisDirection selectedForwardAxis,
+ out _);
+
+ Assert.IsTrue(ok);
+ Assert.AreEqual(LocalAxisDirection.PositiveX, selectedForwardAxis);
+ Assert.AreEqual(LocalAxisDirection.PositiveX, convention.ForwardAxis);
+ Assert.AreEqual(LocalAxisDirection.PositiveZ, convention.UpAxis);
+ }
+
+ [TestMethod]
+ public void YUp_ShouldNotSelectYAxisFamilyAsForwardCandidate()
+ {
+ Vector3 referenceAxisX = new Vector3(1.0f, 0.0f, 0.0f);
+ Vector3 referenceAxisY = new Vector3(0.0f, 1.0f, 0.0f);
+ Vector3 referenceAxisZ = new Vector3(0.0f, 0.0f, 1.0f);
+ Vector3 desiredForward = Vector3.Normalize(new Vector3(0.0f, 1.0f, 0.01f));
+
+ bool ok = RealObjectRailAxisConventionResolver.TryResolve(
+ referenceAxisX,
+ referenceAxisY,
+ referenceAxisZ,
+ desiredForward,
+ CoordinateSystemType.YUp,
+ out _,
+ out LocalAxisDirection selectedForwardAxis,
+ out _);
+
+ Assert.IsTrue(ok);
+ Assert.AreNotEqual(LocalAxisDirection.PositiveY, selectedForwardAxis);
+ Assert.AreNotEqual(LocalAxisDirection.NegativeY, selectedForwardAxis);
+ }
+
+ private static void AssertVector(Vector3 actual, double x, double y, double z, double tolerance = 1e-6)
+ {
+ Assert.AreEqual(x, actual.X, tolerance);
+ Assert.AreEqual(y, actual.Y, tolerance);
+ Assert.AreEqual(z, actual.Z, tolerance);
+ }
+ }
+}
diff --git a/UnitTests/CoordinateSystem/RealObjectRailExtentResolverTests.cs b/UnitTests/CoordinateSystem/RealObjectRailExtentResolverTests.cs
new file mode 100644
index 0000000..7e1c0bb
--- /dev/null
+++ b/UnitTests/CoordinateSystem/RealObjectRailExtentResolverTests.cs
@@ -0,0 +1,142 @@
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using NavisworksTransport.Utils.CoordinateSystem;
+using System;
+using System.Numerics;
+
+namespace NavisworksTransport.UnitTests.CoordinateSystem
+{
+ [TestClass]
+ public class RealObjectRailExtentResolverTests
+ {
+ [TestMethod]
+ public void YUp_ZeroCorrection_ShouldKeepResolvedRailUpExtent()
+ {
+ Vector3 referenceAxisX = new Vector3(-1.0f, 0.0f, 0.0f);
+ Vector3 referenceAxisY = new Vector3(0.0f, 1.0f, 0.0f);
+ Vector3 referenceAxisZ = new Vector3(0.0f, 0.0f, -1.0f);
+ Vector3 desiredForward = Vector3.Normalize(new Vector3(-0.8987f, 0.0f, 0.4386f));
+ Quaternion baseline = Quaternion.Identity;
+
+ bool ok = RealObjectRailExtentResolver.TryResolveProjectedSemanticExtents(
+ referenceAxisX,
+ referenceAxisY,
+ referenceAxisZ,
+ desiredForward,
+ CoordinateSystemType.YUp,
+ forwardSize: 6.0,
+ sideSize: 2.0,
+ upSize: 4.0,
+ baseline,
+ baseline,
+ out ModelAxisConvention convention,
+ out var extents);
+
+ Assert.IsTrue(ok);
+ Assert.AreEqual(LocalAxisDirection.PositiveX, convention.ForwardAxis);
+ Assert.AreEqual(LocalAxisDirection.PositiveY, convention.UpAxis);
+ Assert.AreEqual(6.0, extents.forwardExtent, 1e-6);
+ Assert.AreEqual(2.0, extents.sideExtent, 1e-6);
+ Assert.AreEqual(4.0, extents.upExtent, 1e-6);
+ }
+
+ [TestMethod]
+ public void YUp_HostY90_ShouldKeepResolvedRailUpExtentAndSwapForwardSide()
+ {
+ Vector3 referenceAxisX = new Vector3(-1.0f, 0.0f, 0.0f);
+ Vector3 referenceAxisY = new Vector3(0.0f, 1.0f, 0.0f);
+ Vector3 referenceAxisZ = new Vector3(0.0f, 0.0f, -1.0f);
+ Vector3 desiredForward = Vector3.Normalize(new Vector3(-0.8987f, 0.0f, 0.4386f));
+ Quaternion baseline = Quaternion.Identity;
+ var adapter = new HostCoordinateAdapter(CoordinateSystemType.YUp);
+ Quaternion final = adapter.ComposeHostQuaternion(
+ baseline,
+ new LocalEulerRotationCorrection(0.0, 90.0, 0.0));
+
+ bool ok = RealObjectRailExtentResolver.TryResolveProjectedSemanticExtents(
+ referenceAxisX,
+ referenceAxisY,
+ referenceAxisZ,
+ desiredForward,
+ CoordinateSystemType.YUp,
+ forwardSize: 6.0,
+ sideSize: 2.0,
+ upSize: 4.0,
+ baseline,
+ final,
+ out _,
+ out var extents);
+
+ Assert.IsTrue(ok);
+ Assert.AreEqual(2.0, extents.forwardExtent, 1e-6);
+ Assert.AreEqual(6.0, extents.sideExtent, 1e-6);
+ Assert.AreEqual(4.0, extents.upExtent, 1e-6);
+ }
+
+ [TestMethod]
+ public void YUp_DualAxisCorrection_WithRailBaseline_ShouldProjectAgainstFinalRailPose()
+ {
+ Vector3 referenceAxisX = new Vector3(-1.0f, 0.0f, 0.0f);
+ Vector3 referenceAxisY = new Vector3(0.0f, 1.0f, 0.0f);
+ Vector3 referenceAxisZ = new Vector3(0.0f, 0.0f, -1.0f);
+ Vector3 desiredForward = Vector3.Normalize(new Vector3(-0.8987f, 0.0f, 0.4386f));
+
+ Quaternion baseline = Quaternion.Normalize(Quaternion.CreateFromRotationMatrix(new Matrix4x4(
+ 0.9900f, -0.1403f, -0.0161f, 0f,
+ 0.1403f, 0.9901f, -0.0023f, 0f,
+ 0.0163f, 0.0000f, 0.9999f, 0f,
+ 0f, 0f, 0f, 1f)));
+
+ var adapter = new HostCoordinateAdapter(CoordinateSystemType.YUp);
+ Quaternion final = adapter.ComposeHostQuaternion(
+ baseline,
+ new LocalEulerRotationCorrection(0.0, 90.0, 90.0));
+
+ bool ok = RealObjectRailExtentResolver.TryResolveProjectedSemanticExtents(
+ referenceAxisX,
+ referenceAxisY,
+ referenceAxisZ,
+ desiredForward,
+ CoordinateSystemType.YUp,
+ forwardSize: 6.0,
+ sideSize: 2.0,
+ upSize: 4.0,
+ baseline,
+ final,
+ out ModelAxisConvention convention,
+ out var extents);
+
+ Assert.IsTrue(ok);
+ Assert.AreEqual(LocalAxisDirection.PositiveX, convention.ForwardAxis);
+ Assert.AreEqual(LocalAxisDirection.PositiveY, convention.UpAxis);
+
+ Vector3 localSize = convention.CreateScaleVector3(6.0, 2.0, 4.0);
+ Vector3 rotatedLocalX = Vector3.Normalize(Vector3.Transform(Vector3.UnitX, final));
+ Vector3 rotatedLocalY = Vector3.Normalize(Vector3.Transform(Vector3.UnitY, final));
+ Vector3 rotatedLocalZ = Vector3.Normalize(Vector3.Transform(Vector3.UnitZ, final));
+ Vector3 targetForward = Vector3.Normalize(Vector3.Transform(convention.ForwardUnitVector, baseline));
+ Vector3 targetUp = Vector3.Normalize(Vector3.Transform(convention.UpUnitVector, baseline));
+ Vector3 targetSide = Vector3.Normalize(Vector3.Cross(targetForward, targetUp));
+
+ double expectedForward = ProjectExtent(localSize, rotatedLocalX, rotatedLocalY, rotatedLocalZ, targetForward);
+ double expectedSide = ProjectExtent(localSize, rotatedLocalX, rotatedLocalY, rotatedLocalZ, targetSide);
+ double expectedUp = ProjectExtent(localSize, rotatedLocalX, rotatedLocalY, rotatedLocalZ, targetUp);
+
+ Assert.AreEqual(expectedForward, extents.forwardExtent, 1e-5);
+ Assert.AreEqual(expectedSide, extents.sideExtent, 1e-5);
+ Assert.AreEqual(expectedUp, extents.upExtent, 1e-5);
+ Assert.AreNotEqual(4.0, extents.upExtent, 1e-3, "双轴旋转后,法向尺寸不应仍停留在单轴结果。");
+ }
+
+ private static double ProjectExtent(
+ Vector3 localSize,
+ Vector3 rotatedLocalX,
+ Vector3 rotatedLocalY,
+ Vector3 rotatedLocalZ,
+ Vector3 targetAxis)
+ {
+ return Math.Abs(Vector3.Dot(rotatedLocalX, targetAxis)) * localSize.X +
+ Math.Abs(Vector3.Dot(rotatedLocalY, targetAxis)) * localSize.Y +
+ Math.Abs(Vector3.Dot(rotatedLocalZ, targetAxis)) * localSize.Z;
+ }
+ }
+}
diff --git a/doc/working/current-engineering-state.md b/doc/working/current-engineering-state.md
index 9b0e59c..548662f 100644
--- a/doc/working/current-engineering-state.md
+++ b/doc/working/current-engineering-state.md
@@ -1,6 +1,6 @@
# 当前工程状态
-更新时间:2026-03-23
+更新时间:2026-03-25
## 1. 当前稳定状态
@@ -46,6 +46,24 @@
## 4. 当前必须记住的根因与规则
+### 4.0 真实物体参考姿态与 Fragment默认Up
+
+- 真实物体不能再默认使用 `ModelItem.Transform` 或包围盒正交框架当“原始姿态”。
+- 当前稳定做法是:
+ - 先从 fragment 统计得到代表姿态
+ - 再用当前文档级 `Fragment默认Up` 解释这组 fragment 参考轴
+ - 最终得到当前宿主语义下的真实参考姿态
+- `Fragment默认Up` 的真正用途不是“找一个竖直候选轴”,而是:
+ - 解释 fragment 参考姿态的原始 `Y/Z` 语义
+ - 把 fragment 参考姿态转换成当前宿主坐标语义下可用的真实姿态
+- 当前文档如果 `Fragment默认Up` 设错,会直接导致:
+ - 真实物体起点姿态错误
+ - Ground / Hoisting / Rail 的参考姿态解释错误
+ - 后续角度修正、通行空间、路径贴合全部建立在错误姿态上
+- 当前规则:
+ - 先解释真实物体参考姿态,再做路径对齐
+ - 不能跳过这一步,直接拿 fragment 世界轴去猜业务姿态
+
### 4.1 Rotation / 矩阵语义
- `Rotation3D(a, b, c, d)` 的参数顺序是四元数 `x, y, z, w`。
@@ -162,6 +180,37 @@
2. 再验证修正后 `forward` 是否仍沿 rail 切向
3. 最后再看 Navisworks 应用层是否正确按三步增量姿态落位
+### 4.8 Rail 真实物体必须复用统一姿态解释层
+
+- `Rail` 不应再单独退回默认 `PositiveX / PositiveY` 轴约定。
+- 当前稳定原则:
+ - `Ground / Hoisting / Rail` 三类路径的真实物体,必须先共用同一个“真实参考姿态解释层”
+ - 路径类型的差异只体现在目标路径框架:
+ - `Ground / Hoisting`:`up = 宿主 up`
+ - `Rail`:`up = rail 法向 / preferred normal`
+- 当前 `Rail` 真实物体稳定链路:
+ 1. fragment 代表姿态
+ 2. `Fragment默认Up` 解释后的真实参考姿态
+ 3. `Rail` 路径框架(切向 / 法向)
+ 4. 宿主 `X/Y/Z` 角度修正
+- 不要再让 `Rail` 真实物体单独维护一套与 `Ground / Hoisting` 不同的对象姿态来源。
+
+### 4.9 Rail 真实物体的通行空间与路径偏移
+
+- `Rail` 真实物体的通行空间、起点中心偏移、逐帧法向偏移,必须共享“最终姿态”的同一套尺寸语义。
+- 当前稳定规则:
+ - 不能只根据“宿主轴角度修正”单独投影尺寸
+ - 必须同时吃到:
+ - `Rail` 基姿态
+ - 宿主 `X/Y/Z` 角度修正后的最终姿态
+- 否则会出现典型错误:
+ - 物体本体姿态正确,但起点偏离路径
+ - 单轴旋转时通行空间看起来对,双轴旋转后只跟上一个角度
+ - `Rail` 真实物体通行空间与物体本体方向不一致
+- 当前消费规则:
+ - `AnimationControlViewModel.UpdatePassageSpaceVisualization()` 中,真实物体的 `Rail` 和 `Hoisting` 必须分开消费
+ - `Rail` 真实物体不能再复用 `Hoisting` 的法向/分段参数语义
+
## 5. 当前保留的日志策略
- 保留:
diff --git a/src/Core/Animation/PathAnimationManager.cs b/src/Core/Animation/PathAnimationManager.cs
index fa76673..23e5198 100644
--- a/src/Core/Animation/PathAnimationManager.cs
+++ b/src/Core/Animation/PathAnimationManager.cs
@@ -707,19 +707,16 @@ namespace NavisworksTransport.Core.Animation
}
else if (_route.PathType == PathType.Rail)
{
- double objectHeight = GetAnimatedObjectHeight();
Point3D previousPoint = _pathPoints[0];
Point3D nextPoint = _pathPoints.Count > 1 ? _pathPoints[1] : _pathPoints[0];
+ double objectHeight = GetAnimatedObjectRailNormalExtent(previousPoint, _pathPoints[0], nextPoint);
startPosition = RailPathPoseHelper.ResolveObjectSpaceCenterPosition(_route, startPosition, previousPoint, nextPoint, objectHeight);
LogManager.Debug($"[移动到起点] Rail路径调整: 参考点=({_pathPoints[0].X:F2},{_pathPoints[0].Y:F2},{_pathPoints[0].Z:F2}), 物体中心=({startPosition.X:F2},{startPosition.Y:F2},{startPosition.Z:F2}), 物体高度={objectHeight:F2}, 安装={_route.RailMountMode}, 对接={(PathRoute.IsTopPayloadAnchorForMountMode(_route.RailMountMode) ? "顶面对接" : "底面对接")}");
- if (RailPathPoseHelper.TryCreateRailRotation(
- _route,
+ if (TryCreateRailPathRotation(
previousPoint,
_pathPoints[0],
nextPoint,
- GetCurrentRailModelAxisConvention(),
- _objectRotationCorrection,
out var railRotation))
{
var railLinearTransform = new Transform3D(railRotation).Linear;
@@ -964,7 +961,7 @@ namespace NavisworksTransport.Core.Animation
// 🔥 空中路径:根据路径类型和线段索引调整物体位置
double objectHeight = _route.PathType == PathType.Rail
- ? GetAnimatedObjectHeight()
+ ? GetAnimatedObjectRailNormalExtent(p1, framePosition, p2)
: GetAnimatedObjectGroundContactHeight();
if (_route.PathType == PathType.Hoisting)
@@ -1079,13 +1076,10 @@ namespace NavisworksTransport.Core.Animation
};
if (_route.PathType == PathType.Rail &&
- RailPathPoseHelper.TryCreateRailRotation(
- _route,
+ TryCreateRailPathRotation(
previousFramePoint,
framePosition,
nextFramePoint,
- GetCurrentRailModelAxisConvention(),
- _objectRotationCorrection,
out var railRotation))
{
frame.Rotation = railRotation;
@@ -1941,7 +1935,7 @@ namespace NavisworksTransport.Core.Animation
Point3D expectedTrackedEndPoint;
if (_route.PathType == PathType.Rail)
{
- double objectHeight = GetAnimatedObjectHeight();
+ double objectHeight = GetAnimatedObjectRailNormalExtent(previousAnchorPoint, terminalAnchorPoint, terminalAnchorPoint);
expectedTrackedEndPoint = RailPathPoseHelper.ResolveObjectSpaceCenterPosition(
_route,
terminalAnchorPoint,
@@ -3510,19 +3504,227 @@ namespace NavisworksTransport.Core.Animation
return new Vector3((float)point.X, (float)point.Y, (float)point.Z);
}
- private ModelAxisConvention GetCurrentRailModelAxisConvention()
+ private bool TryGetCurrentRailModelAxisConvention(
+ Point3D previousPoint,
+ Point3D currentPoint,
+ Point3D nextPoint,
+ out ModelAxisConvention convention)
{
+ convention = null;
+
if (IsVirtualObjectMode)
{
- return ModelAxisConvention.CreateVirtualObjectAssetConvention();
+ convention = ModelAxisConvention.CreateVirtualObjectAssetConvention();
+ return true;
}
var adapter = CoordinateSystemManager.Instance.CreateHostAdapter();
- var convention = ModelAxisConvention.CreateDefaultForHost(adapter.HostType);
+ if (IsRealObjectMode && _hasRealObjectReferenceRotation)
+ {
+ Vector3 desiredForward = new Vector3(
+ (float)(nextPoint.X - previousPoint.X),
+ (float)(nextPoint.Y - previousPoint.Y),
+ (float)(nextPoint.Z - previousPoint.Z));
+
+ if (desiredForward.LengthSquared() < 1e-6f)
+ {
+ desiredForward = new Vector3(
+ (float)(nextPoint.X - currentPoint.X),
+ (float)(nextPoint.Y - currentPoint.Y),
+ (float)(nextPoint.Z - currentPoint.Z));
+ }
+
+ if (RealObjectRailAxisConventionResolver.TryResolve(
+ _realObjectReferenceAxisX,
+ _realObjectReferenceAxisY,
+ _realObjectReferenceAxisZ,
+ desiredForward,
+ adapter.HostType,
+ out convention,
+ out var selectedForwardAxis,
+ out var selectedForwardWorldAxis))
+ {
+ LogManager.Info(
+ $"[Rail姿态修正] Host={adapter.HostType}, 虚拟物体={IsVirtualObjectMode}, " +
+ $"真实物体参考姿态生效, Forward={convention.ForwardAxis}, Up={convention.UpAxis}, " +
+ $"选中Forward轴={selectedForwardAxis}, 选中Forward世界轴=({selectedForwardWorldAxis.X:F4},{selectedForwardWorldAxis.Y:F4},{selectedForwardWorldAxis.Z:F4})");
+ return true;
+ }
+ }
+
+ convention = ModelAxisConvention.CreateDefaultForHost(adapter.HostType);
LogManager.Info(
$"[Rail姿态修正] Host={adapter.HostType}, 虚拟物体={IsVirtualObjectMode}, " +
- $"Forward={convention.ForwardAxis}, Up={convention.UpAxis}");
- return convention;
+ $"Forward={convention.ForwardAxis}, Up={convention.UpAxis}, 来源=默认轴约定");
+ return true;
+ }
+
+ private bool TryCalculateCurrentRealObjectRailProjectedExtents(
+ Point3D previousPoint,
+ Point3D currentPoint,
+ Point3D nextPoint,
+ out ModelAxisConvention convention,
+ out (double forwardExtent, double sideExtent, double upExtent) extents)
+ {
+ convention = null;
+ extents = (0.0, 0.0, 0.0);
+
+ if (!IsRealObjectMode ||
+ !_hasRealObjectReferenceRotation ||
+ _realObjectLength <= 0.0 ||
+ _realObjectWidth <= 0.0 ||
+ _realObjectHeight <= 0.0)
+ {
+ return false;
+ }
+
+ var adapter = CoordinateSystemManager.Instance.CreateHostAdapter();
+ Vector3 desiredForward = new Vector3(
+ (float)(nextPoint.X - previousPoint.X),
+ (float)(nextPoint.Y - previousPoint.Y),
+ (float)(nextPoint.Z - previousPoint.Z));
+
+ if (desiredForward.LengthSquared() < 1e-6f)
+ {
+ desiredForward = new Vector3(
+ (float)(nextPoint.X - currentPoint.X),
+ (float)(nextPoint.Y - currentPoint.Y),
+ (float)(nextPoint.Z - currentPoint.Z));
+ }
+
+ if (!TryGetCurrentRailModelAxisConvention(previousPoint, currentPoint, nextPoint, out var railConvention))
+ {
+ return false;
+ }
+
+ if (!RailPathPoseHelper.TryCreateRailRotation(
+ _route,
+ previousPoint,
+ currentPoint,
+ nextPoint,
+ railConvention,
+ LocalEulerRotationCorrection.Zero,
+ out var baselineRotation))
+ {
+ return false;
+ }
+
+ Quaternion baselineHostQuaternion = new Quaternion(
+ (float)baselineRotation.A,
+ (float)baselineRotation.B,
+ (float)baselineRotation.C,
+ (float)baselineRotation.D);
+ Quaternion finalHostQuaternion = adapter.ComposeHostQuaternion(
+ baselineHostQuaternion,
+ _objectRotationCorrection);
+
+ return RealObjectRailExtentResolver.TryResolveProjectedSemanticExtents(
+ _realObjectReferenceAxisX,
+ _realObjectReferenceAxisY,
+ _realObjectReferenceAxisZ,
+ desiredForward,
+ adapter.HostType,
+ _realObjectLength,
+ _realObjectWidth,
+ _realObjectHeight,
+ baselineHostQuaternion,
+ finalHostQuaternion,
+ out convention,
+ out extents);
+ }
+
+ public bool TryGetCurrentRouteRealObjectRailProjectedExtents(
+ out double forwardExtent,
+ out double sideExtent,
+ out double upExtent)
+ {
+ forwardExtent = 0.0;
+ sideExtent = 0.0;
+ upExtent = 0.0;
+
+ if (_route == null || _route.PathType != PathType.Rail || _pathPoints == null || _pathPoints.Count == 0)
+ {
+ return false;
+ }
+
+ Point3D previousPoint = _pathPoints[0];
+ Point3D currentPoint = _pathPoints[0];
+ Point3D nextPoint = _pathPoints.Count > 1 ? _pathPoints[1] : _pathPoints[0];
+
+ if (!TryCalculateCurrentRealObjectRailProjectedExtents(
+ previousPoint,
+ currentPoint,
+ nextPoint,
+ out _,
+ out var extents))
+ {
+ return false;
+ }
+
+ forwardExtent = extents.forwardExtent;
+ sideExtent = extents.sideExtent;
+ upExtent = extents.upExtent;
+ return true;
+ }
+
+ private bool TryCreateRailPathRotation(
+ Point3D previousPoint,
+ Point3D currentPoint,
+ Point3D nextPoint,
+ out Rotation3D rotation)
+ {
+ rotation = Rotation3D.Identity;
+
+ if (!TryGetCurrentRailModelAxisConvention(previousPoint, currentPoint, nextPoint, out var railConvention))
+ {
+ return false;
+ }
+
+ if (IsRealObjectMode)
+ {
+ if (!RailPathPoseHelper.TryCreateRailRotation(
+ _route,
+ previousPoint,
+ currentPoint,
+ nextPoint,
+ railConvention,
+ LocalEulerRotationCorrection.Zero,
+ out var baselineRotation))
+ {
+ return false;
+ }
+
+ var adapter = CoordinateSystemManager.Instance.CreateHostAdapter();
+ Quaternion baselineHostQuaternion = new Quaternion(
+ (float)baselineRotation.A,
+ (float)baselineRotation.B,
+ (float)baselineRotation.C,
+ (float)baselineRotation.D);
+ Quaternion composedHostQuaternion = adapter.ComposeHostQuaternion(
+ baselineHostQuaternion,
+ _objectRotationCorrection);
+ rotation = adapter.FromHostQuaternion(composedHostQuaternion);
+
+ Matrix4x4 baselineLinear = Matrix4x4.CreateFromQuaternion(baselineHostQuaternion);
+ Matrix4x4 composedLinear = Matrix4x4.CreateFromQuaternion(composedHostQuaternion);
+ LogManager.Info(
+ $"[Rail真实物体角度修正] BaselineX=({baselineLinear.M11:F4},{baselineLinear.M21:F4},{baselineLinear.M31:F4}), " +
+ $"BaselineY=({baselineLinear.M12:F4},{baselineLinear.M22:F4},{baselineLinear.M32:F4}), " +
+ $"BaselineZ=({baselineLinear.M13:F4},{baselineLinear.M23:F4},{baselineLinear.M33:F4}), " +
+ $"HostComposeX=({composedLinear.M11:F4},{composedLinear.M21:F4},{composedLinear.M31:F4}), " +
+ $"HostComposeY=({composedLinear.M12:F4},{composedLinear.M22:F4},{composedLinear.M32:F4}), " +
+ $"HostComposeZ=({composedLinear.M13:F4},{composedLinear.M23:F4},{composedLinear.M33:F4})");
+ return true;
+ }
+
+ return RailPathPoseHelper.TryCreateRailRotation(
+ _route,
+ previousPoint,
+ currentPoint,
+ nextPoint,
+ railConvention,
+ _objectRotationCorrection,
+ out rotation);
}
private ModelAxisConvention GetCurrentModelAxisConvention()
@@ -3627,6 +3829,27 @@ namespace NavisworksTransport.Core.Animation
return true;
}
+ private double GetAnimatedObjectRailNormalExtent(
+ Point3D previousPoint,
+ Point3D currentPoint,
+ Point3D nextPoint)
+ {
+ if (TryCalculateCurrentRealObjectRailProjectedExtents(
+ previousPoint,
+ currentPoint,
+ nextPoint,
+ out var convention,
+ out var extents))
+ {
+ LogManager.Debug(
+ $"[Rail法向尺寸] 真实物体有效尺寸: Forward={extents.forwardExtent:F3}, Side={extents.sideExtent:F3}, Up={extents.upExtent:F3}, " +
+ $"ForwardAxis={convention.ForwardAxis}, UpAxis={convention.UpAxis}, 角度={_objectRotationCorrection}");
+ return extents.upExtent;
+ }
+
+ return GetAnimatedObjectHeight();
+ }
+
private bool TryCreatePlanarPathRotationFromHostForward(Vector3D hostForward, out Rotation3D rotation)
{
rotation = Rotation3D.Identity;
diff --git a/src/UI/WPF/ViewModels/AnimationControlViewModel.cs b/src/UI/WPF/ViewModels/AnimationControlViewModel.cs
index 635d6b2..da3b565 100644
--- a/src/UI/WPF/ViewModels/AnimationControlViewModel.cs
+++ b/src/UI/WPF/ViewModels/AnimationControlViewModel.cs
@@ -1713,9 +1713,11 @@ namespace NavisworksTransport.UI.WPF.ViewModels
_pathAnimationManager?.SetRealObjectDimensions(objectLength, objectWidth, objectHeight);
LogManager.Debug($"[选择物体] 保存原始尺寸: 长度={_objectOriginalLength:F2}m, 宽度={_objectOriginalWidth:F2}m, 高度={_objectOriginalHeight:F2}m");
- // 3. 设置新物体(会触发UpdatePassageSpaceVisualization)
- SelectedAnimatedObject = newObject;
+ // 3. 先注册到动画管理器,再设置 SelectedAnimatedObject。
+ // SelectedAnimatedObject 的 setter 会立即触发 MoveAnimatedObjectToPathStart();
+ // 如果先移动、后 SetAnimatedObject,会把“已摆到起点后的当前姿态”再次当成参考姿态缓存。
_pathAnimationManager?.SetAnimatedObject(newObject);
+ SelectedAnimatedObject = newObject;
LogManager.Info($"已选择移动物体: {SelectedAnimatedObject.DisplayName}");
// 选择实体物体意味着切换到实体模式,避免后续起点同步仍走虚拟物体分支。
@@ -2160,16 +2162,18 @@ namespace NavisworksTransport.UI.WPF.ViewModels
LogManager.Info("已清除PathAnimationManager中的动画数据并归位物体");
}
- // 2. 重置角度修正值为0
+ // 2. 先清空选择,避免角度修正归零时又触发“移动到起点”链路。
+ SelectedAnimatedObject = null;
+
+ // 3. 重置角度修正值为0
ObjectRotationCorrection = LocalEulerRotationCorrection.Zero;
LogManager.Debug("[清除物体] 已重置角度修正值为0");
- // 3. 重置属性
- SelectedAnimatedObject = null;
+ // 4. 重置属性
UpdateAnimatedObjectInfo();
UpdateCanGenerateAnimation();
- // 4. 清理高亮
+ // 5. 清理高亮
ModelHighlightHelper.ClearCollisionHighlights();
LogManager.Info("移动物体已完全清除并归位");
@@ -4339,6 +4343,20 @@ namespace NavisworksTransport.UI.WPF.ViewModels
upSize = _objectOriginalHeight * metersToUnits;
var adapter = CoordinateSystemManager.Instance.CreateHostAdapter();
+ if (CurrentPathRoute?.PathType == NavisworksTransport.PathType.Rail &&
+ _pathAnimationManager != null &&
+ _pathAnimationManager.TryGetCurrentRouteRealObjectRailProjectedExtents(
+ out double resolvedForward,
+ out double resolvedSide,
+ out double resolvedUp))
+ {
+ double unitsToMetersForRail = UnitsConverter.GetUnitsToMetersConversionFactor();
+ LogManager.Debug(
+ $"[角度修正] Rail真实物体复用路径姿态尺寸: Forward={resolvedForward * unitsToMetersForRail:F2}m, " +
+ $"Side={resolvedSide * unitsToMetersForRail:F2}m, Up={resolvedUp * unitsToMetersForRail:F2}m, 角度={_objectRotationCorrection}");
+ return (resolvedForward, resolvedSide, resolvedUp);
+ }
+
axisConvention = CurrentPathRoute?.PathType == NavisworksTransport.PathType.Rail
? ModelAxisConvention.CreateRailAssetConvention()
: ModelAxisConvention.CreateDefaultForHost(adapter.HostType);
@@ -4442,9 +4460,9 @@ namespace NavisworksTransport.UI.WPF.ViewModels
else if (SelectedAnimatedObject != null)
{
// 根据路径类型确定通行空间的尺寸
- if (CurrentPathRoute.PathType == NavisworksTransport.PathType.Rail || CurrentPathRoute.PathType == NavisworksTransport.PathType.Hoisting)
+ if (CurrentPathRoute.PathType == NavisworksTransport.PathType.Hoisting)
{
- // 空中路径(空轨或吊装):
+ // 吊装路径:
// 高度上下都加间隙
passageAcrossPath = effectiveWidth + 2 * safetyMargin; // 旋转后宽度 + 2*间隙(垂直于路径方向)
passageNormalToPath = effectiveHeight + 2 * safetyMargin; // 局部up方向高度 + 2*间隙(法线方向)
@@ -4467,7 +4485,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
passageNormalToPathHorizontal = passageNormalToPath;
LogManager.Debug($"[通行空间可视化] 地面路径使用物体尺寸 ({SelectedAnimatedObject.DisplayName}): 有效长度={effectiveLength / metersToUnitsPassage:F2}m, 有效宽度={effectiveWidth / metersToUnitsPassage:F2}m, 有效高度={effectiveHeight / metersToUnitsPassage:F2}m, 通行空间沿路径={passageAlongPath / metersToUnitsPassage:F2}m, 垂直路径={passageAcrossPath / metersToUnitsPassage:F2}m, 法线={passageNormalToPath / metersToUnitsPassage:F2}m");
}
- else // Rail
+ else if (CurrentPathRoute.PathType == NavisworksTransport.PathType.Rail)
{
// 空轨路径(可能有坡度):
// 物体的长度方向(X轴,前进方向)朝向路径方向
@@ -4482,6 +4500,14 @@ namespace NavisworksTransport.UI.WPF.ViewModels
passageNormalToPathHorizontal = passageNormalToPath;
LogManager.Debug($"[通行空间可视化] 空轨路径使用物体尺寸 ({SelectedAnimatedObject.DisplayName}): 有效长度={effectiveLength / metersToUnitsPassage:F2}m, 有效宽度={effectiveWidth / metersToUnitsPassage:F2}m, 有效高度={effectiveHeight / metersToUnitsPassage:F2}m, 通行空间沿路径={passageAlongPath / metersToUnitsPassage:F2}m, 垂直路径={passageAcrossPath / metersToUnitsPassage:F2}m, 法线={passageNormalToPath / metersToUnitsPassage:F2}m");
}
+ else
+ {
+ passageAcrossPath = effectiveWidth + 2 * safetyMargin;
+ passageNormalToPath = effectiveHeight + safetyMargin;
+ passageAlongPath = effectiveLength;
+ passageNormalToPathVertical = passageNormalToPath;
+ passageNormalToPathHorizontal = passageNormalToPath;
+ }
}
else
{
diff --git a/src/UI/WPF/ViewModels/SystemManagementViewModel.cs b/src/UI/WPF/ViewModels/SystemManagementViewModel.cs
index 7695501..a2c1ba7 100644
--- a/src/UI/WPF/ViewModels/SystemManagementViewModel.cs
+++ b/src/UI/WPF/ViewModels/SystemManagementViewModel.cs
@@ -28,6 +28,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
private readonly UIStateManager _uiStateManager;
private readonly HashSet _documentRotationHintShown = new HashSet(StringComparer.OrdinalIgnoreCase);
+ private readonly HashSet _documentRotationHintPending = new HashSet(StringComparer.OrdinalIgnoreCase);
// 日志管理字段
private ObservableCollection _logLevels;
@@ -506,7 +507,9 @@ namespace NavisworksTransport.UI.WPF.ViewModels
private void ShowRootModelRotationHintIfNeeded()
{
string documentKey = GetCurrentDocumentKey();
- if (string.IsNullOrWhiteSpace(documentKey) || _documentRotationHintShown.Contains(documentKey))
+ if (string.IsNullOrWhiteSpace(documentKey) ||
+ _documentRotationHintShown.Contains(documentKey) ||
+ _documentRotationHintPending.Contains(documentKey))
{
return;
}
@@ -522,13 +525,51 @@ namespace NavisworksTransport.UI.WPF.ViewModels
return;
}
- _documentRotationHintShown.Add(documentKey);
- LogManager.Info($"[模型整体旋转提示] {hintMessage}");
- System.Windows.MessageBox.Show(
- hintMessage,
- "坐标系提示",
- System.Windows.MessageBoxButton.OK,
- System.Windows.MessageBoxImage.Information);
+ _documentRotationHintPending.Add(documentKey);
+ _ = Task.Run(async () =>
+ {
+ try
+ {
+ await Task.Delay(1500).ConfigureAwait(false);
+ _uiStateManager?.QueueUIUpdate(() =>
+ {
+ try
+ {
+ string currentDocumentKey = GetCurrentDocumentKey();
+ if (!string.Equals(documentKey, currentDocumentKey, StringComparison.OrdinalIgnoreCase))
+ {
+ _documentRotationHintPending.Remove(documentKey);
+ return;
+ }
+
+ if (_documentRotationHintShown.Contains(documentKey))
+ {
+ _documentRotationHintPending.Remove(documentKey);
+ return;
+ }
+
+ _documentRotationHintShown.Add(documentKey);
+ _documentRotationHintPending.Remove(documentKey);
+ LogManager.Info($"[模型整体旋转提示] {hintMessage}");
+ System.Windows.MessageBox.Show(
+ hintMessage,
+ "坐标系提示",
+ System.Windows.MessageBoxButton.OK,
+ System.Windows.MessageBoxImage.Information);
+ }
+ catch (Exception ex)
+ {
+ _documentRotationHintPending.Remove(documentKey);
+ LogManager.Warning($"[模型整体旋转提示] 延后弹出失败: {ex.Message}");
+ }
+ });
+ }
+ catch (Exception ex)
+ {
+ _documentRotationHintPending.Remove(documentKey);
+ LogManager.Warning($"[模型整体旋转提示] 延后任务失败: {ex.Message}");
+ }
+ });
}
private static bool TryGetRootModelRotationHint(Document doc, out string hintMessage)
diff --git a/src/Utils/CoordinateSystem/RealObjectRailAxisConventionResolver.cs b/src/Utils/CoordinateSystem/RealObjectRailAxisConventionResolver.cs
new file mode 100644
index 0000000..6bf4f7c
--- /dev/null
+++ b/src/Utils/CoordinateSystem/RealObjectRailAxisConventionResolver.cs
@@ -0,0 +1,132 @@
+using System;
+using System.Numerics;
+
+namespace NavisworksTransport.Utils.CoordinateSystem
+{
+ ///
+ /// 根据“已解释为当前宿主语义”的真实物体参考姿态,解析 Rail 路径应使用的局部轴约定。
+ ///
+ /// 语义:
+ /// - 真实物体没有资产坐标系,但 fragment 参考姿态经过 Fragment默认Up 解释后,
+ /// 可以得到“当前宿主语义下”的真实参考三轴。
+ /// - Rail 与 Ground/Hoisting 的统一点在于:都先解释对象姿态,再进入路径框架。
+ /// - Rail 的特殊性只在路径框架:forward 来自 rail 切向,up 来自 rail 法向。
+ /// - 本解析器只负责回答:对这个真实物体来说,哪个局部 forward 轴最接近当前路径 forward;
+ /// up 轴始终采用当前宿主语义对应的轴族(YUp => +Y,ZUp => +Z)。
+ ///
+ public static class RealObjectRailAxisConventionResolver
+ {
+ private const float AxisEpsilon = 1e-6f;
+
+ public static bool TryResolve(
+ Vector3 referenceAxisX,
+ Vector3 referenceAxisY,
+ Vector3 referenceAxisZ,
+ Vector3 desiredForward,
+ CoordinateSystemType hostType,
+ out ModelAxisConvention convention,
+ out LocalAxisDirection selectedForwardAxis,
+ out Vector3 selectedForwardWorldAxis)
+ {
+ convention = null;
+ selectedForwardAxis = LocalAxisDirection.PositiveX;
+ selectedForwardWorldAxis = Vector3.Zero;
+
+ if (!TryNormalize(desiredForward, out Vector3 normalizedForward))
+ {
+ return false;
+ }
+
+ if (!TryNormalize(referenceAxisX, out Vector3 axisX) ||
+ !TryNormalize(referenceAxisY, out Vector3 axisY) ||
+ !TryNormalize(referenceAxisZ, out Vector3 axisZ))
+ {
+ return false;
+ }
+
+ LocalAxisDirection semanticUpAxis =
+ hostType == CoordinateSystemType.YUp
+ ? LocalAxisDirection.PositiveY
+ : LocalAxisDirection.PositiveZ;
+
+ Candidate best = Candidate.Invalid;
+ foreach (Candidate candidate in EnumerateForwardCandidates(axisX, axisY, axisZ, semanticUpAxis))
+ {
+ float score = Vector3.Dot(candidate.WorldAxis, normalizedForward);
+ if (best.IsInvalid || score > best.Score)
+ {
+ best = new Candidate(candidate.Axis, candidate.WorldAxis, score);
+ }
+ }
+
+ if (best.IsInvalid)
+ {
+ return false;
+ }
+
+ convention = new ModelAxisConvention(best.Axis, semanticUpAxis);
+ selectedForwardAxis = best.Axis;
+ selectedForwardWorldAxis = best.WorldAxis;
+ return true;
+ }
+
+ private static Candidate[] EnumerateForwardCandidates(
+ Vector3 axisX,
+ Vector3 axisY,
+ Vector3 axisZ,
+ LocalAxisDirection semanticUpAxis)
+ {
+ switch (semanticUpAxis)
+ {
+ case LocalAxisDirection.PositiveY:
+ return new[]
+ {
+ new Candidate(LocalAxisDirection.PositiveX, axisX, 0f),
+ new Candidate(LocalAxisDirection.NegativeX, -axisX, 0f),
+ new Candidate(LocalAxisDirection.PositiveZ, axisZ, 0f),
+ new Candidate(LocalAxisDirection.NegativeZ, -axisZ, 0f)
+ };
+
+ case LocalAxisDirection.PositiveZ:
+ default:
+ return new[]
+ {
+ new Candidate(LocalAxisDirection.PositiveX, axisX, 0f),
+ new Candidate(LocalAxisDirection.NegativeX, -axisX, 0f),
+ new Candidate(LocalAxisDirection.PositiveY, axisY, 0f),
+ new Candidate(LocalAxisDirection.NegativeY, -axisY, 0f)
+ };
+ }
+ }
+
+ private static bool TryNormalize(Vector3 value, out Vector3 normalized)
+ {
+ if (value.LengthSquared() < AxisEpsilon)
+ {
+ normalized = Vector3.Zero;
+ return false;
+ }
+
+ normalized = Vector3.Normalize(value);
+ return true;
+ }
+
+ private readonly struct Candidate
+ {
+ public static Candidate Invalid => new Candidate(LocalAxisDirection.PositiveX, Vector3.Zero, float.MinValue);
+
+ public LocalAxisDirection Axis { get; }
+ public Vector3 WorldAxis { get; }
+ public float Score { get; }
+
+ public bool IsInvalid => Score == float.MinValue;
+
+ public Candidate(LocalAxisDirection axis, Vector3 worldAxis, float score)
+ {
+ Axis = axis;
+ WorldAxis = worldAxis;
+ Score = score;
+ }
+ }
+ }
+}
diff --git a/src/Utils/CoordinateSystem/RealObjectRailExtentResolver.cs b/src/Utils/CoordinateSystem/RealObjectRailExtentResolver.cs
new file mode 100644
index 0000000..2406a20
--- /dev/null
+++ b/src/Utils/CoordinateSystem/RealObjectRailExtentResolver.cs
@@ -0,0 +1,77 @@
+using System;
+using System.Numerics;
+
+namespace NavisworksTransport.Utils.CoordinateSystem
+{
+ ///
+ /// 真实物体在 Rail 路径下的尺寸语义解析。
+ /// 先根据解释后的真实参考姿态解析 rail 轴约定,再使用:
+ /// 1. rail 基姿态(零角度时对象与路径框架对齐后的宿主姿态)
+ /// 2. 最终姿态(rail 基姿态再叠加宿主轴角度修正)
+ /// 共同计算沿 rail forward/side/normal 的有效尺寸。
+ ///
+ /// 关键点:
+ /// - 这里不能只吃“角度修正”本身;否则双轴旋转时会漏掉 rail 基姿态语义。
+ /// - 目标语义轴来自 rail 基姿态,而不是宿主世界轴。
+ ///
+ public static class RealObjectRailExtentResolver
+ {
+ public static bool TryResolveProjectedSemanticExtents(
+ Vector3 referenceAxisX,
+ Vector3 referenceAxisY,
+ Vector3 referenceAxisZ,
+ Vector3 desiredForward,
+ CoordinateSystemType hostType,
+ double forwardSize,
+ double sideSize,
+ double upSize,
+ Quaternion baselineHostQuaternion,
+ Quaternion finalHostQuaternion,
+ out ModelAxisConvention convention,
+ out (double forwardExtent, double sideExtent, double upExtent) extents)
+ {
+ convention = null;
+ extents = (0.0, 0.0, 0.0);
+
+ if (!RealObjectRailAxisConventionResolver.TryResolve(
+ referenceAxisX,
+ referenceAxisY,
+ referenceAxisZ,
+ desiredForward,
+ hostType,
+ out convention,
+ out _,
+ out _))
+ {
+ return false;
+ }
+
+ Vector3 localSize = convention.CreateScaleVector3(forwardSize, sideSize, upSize);
+ Vector3 rotatedLocalX = Vector3.Normalize(Vector3.Transform(Vector3.UnitX, finalHostQuaternion));
+ Vector3 rotatedLocalY = Vector3.Normalize(Vector3.Transform(Vector3.UnitY, finalHostQuaternion));
+ Vector3 rotatedLocalZ = Vector3.Normalize(Vector3.Transform(Vector3.UnitZ, finalHostQuaternion));
+
+ Vector3 targetForward = Vector3.Normalize(Vector3.Transform(convention.ForwardUnitVector, baselineHostQuaternion));
+ Vector3 targetUp = Vector3.Normalize(Vector3.Transform(convention.UpUnitVector, baselineHostQuaternion));
+ Vector3 targetSide = Vector3.Normalize(Vector3.Cross(targetForward, targetUp));
+
+ extents = (
+ ProjectExtent(localSize, rotatedLocalX, rotatedLocalY, rotatedLocalZ, targetForward),
+ ProjectExtent(localSize, rotatedLocalX, rotatedLocalY, rotatedLocalZ, targetSide),
+ ProjectExtent(localSize, rotatedLocalX, rotatedLocalY, rotatedLocalZ, targetUp));
+ return true;
+ }
+
+ private static double ProjectExtent(
+ Vector3 localSize,
+ Vector3 rotatedLocalX,
+ Vector3 rotatedLocalY,
+ Vector3 rotatedLocalZ,
+ Vector3 targetAxis)
+ {
+ return Math.Abs(Vector3.Dot(rotatedLocalX, targetAxis)) * localSize.X +
+ Math.Abs(Vector3.Dot(rotatedLocalY, targetAxis)) * localSize.Y +
+ Math.Abs(Vector3.Dot(rotatedLocalZ, targetAxis)) * localSize.Z;
+ }
+ }
+}
From 029c7e37add3bd60eddd8595906ff62f36ae9624 Mon Sep 17 00:00:00 2001
From: tian <11429339@qq.com>
Date: Wed, 25 Mar 2026 01:41:15 +0800
Subject: [PATCH 28/87] Checkpoint current pose and path state
---
NavisworksTransport.UnitTests.csproj | 4 +
TransportPlugin.csproj | 5 +
.../PathTargetFrameResolverTests.cs | 45 +++
.../RealObjectPlanarPoseSolverTests.cs | 36 +++
.../RealObjectProjectedExtentResolverTests.cs | 66 ++++
.../RealObjectReferencePoseResolverTests.cs | 76 +++++
UnitTests/Core/PathPersistenceTests.cs | 117 +++++++
doc/working/current-engineering-state.md | 5 +
src/Core/Animation/PathAnimationManager.cs | 294 +++++++++++-------
src/Core/PathDataManager.cs | 38 +++
src/Core/PathDatabase.cs | 38 ++-
src/Core/PathPlanningManager.cs | 8 +
src/Core/PathPlanningModels.cs | 6 +-
.../ViewModels/AnimationControlViewModel.cs | 14 +
.../ViewModels/SystemManagementViewModel.cs | 22 +-
src/Utils/CoordinateSystem/PathTargetFrame.cs | 33 ++
.../PathTargetFrameResolver.cs | 49 +++
.../RealObjectPlanarPoseSolver.cs | 79 ++++-
.../RealObjectProjectedExtentResolver.cs | 54 ++++
.../RealObjectRailExtentResolver.cs | 32 +-
.../RealObjectReferencePose.cs | 37 +++
.../RealObjectReferencePoseResolver.cs | 162 ++++++++++
src/Utils/RailPathPoseHelper.cs | 26 ++
23 files changed, 1086 insertions(+), 160 deletions(-)
create mode 100644 UnitTests/CoordinateSystem/PathTargetFrameResolverTests.cs
create mode 100644 UnitTests/CoordinateSystem/RealObjectProjectedExtentResolverTests.cs
create mode 100644 UnitTests/CoordinateSystem/RealObjectReferencePoseResolverTests.cs
create mode 100644 UnitTests/Core/PathPersistenceTests.cs
create mode 100644 src/Utils/CoordinateSystem/PathTargetFrame.cs
create mode 100644 src/Utils/CoordinateSystem/PathTargetFrameResolver.cs
create mode 100644 src/Utils/CoordinateSystem/RealObjectProjectedExtentResolver.cs
create mode 100644 src/Utils/CoordinateSystem/RealObjectReferencePose.cs
create mode 100644 src/Utils/CoordinateSystem/RealObjectReferencePoseResolver.cs
diff --git a/NavisworksTransport.UnitTests.csproj b/NavisworksTransport.UnitTests.csproj
index 21c8463..b109f54 100644
--- a/NavisworksTransport.UnitTests.csproj
+++ b/NavisworksTransport.UnitTests.csproj
@@ -53,6 +53,7 @@
+
@@ -64,8 +65,11 @@
+
+
+
diff --git a/TransportPlugin.csproj b/TransportPlugin.csproj
index 4d8bfaa..f126d00 100644
--- a/TransportPlugin.csproj
+++ b/TransportPlugin.csproj
@@ -342,7 +342,12 @@
+
+
+
+
+
diff --git a/UnitTests/CoordinateSystem/PathTargetFrameResolverTests.cs b/UnitTests/CoordinateSystem/PathTargetFrameResolverTests.cs
new file mode 100644
index 0000000..d060e02
--- /dev/null
+++ b/UnitTests/CoordinateSystem/PathTargetFrameResolverTests.cs
@@ -0,0 +1,45 @@
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using NavisworksTransport.Utils.CoordinateSystem;
+using System.Numerics;
+
+namespace NavisworksTransport.UnitTests.CoordinateSystem
+{
+ [TestClass]
+ public class PathTargetFrameResolverTests
+ {
+ [TestMethod]
+ public void YUp_PlanarFrame_ShouldUseHostYAsUp()
+ {
+ bool ok = PathTargetFrameResolver.TryCreatePlanarHostFrame(
+ new Vector3(2.0f, 0.0f, 1.0f),
+ CoordinateSystemType.YUp,
+ out PathTargetFrame frame);
+
+ Assert.IsTrue(ok);
+ AssertVector(frame.Up, 0.0, 1.0, 0.0);
+ Assert.AreEqual(0.0, Vector3.Dot(frame.Forward, frame.Up), 1e-6);
+ Assert.AreEqual(1.0, frame.Side.Length(), 1e-6);
+ }
+
+ [TestMethod]
+ public void ZUp_PlanarFrame_ShouldUseHostZAsUp()
+ {
+ bool ok = PathTargetFrameResolver.TryCreatePlanarHostFrame(
+ new Vector3(2.0f, 1.0f, 0.0f),
+ CoordinateSystemType.ZUp,
+ out PathTargetFrame frame);
+
+ Assert.IsTrue(ok);
+ AssertVector(frame.Up, 0.0, 0.0, 1.0);
+ Assert.AreEqual(0.0, Vector3.Dot(frame.Forward, frame.Up), 1e-6);
+ Assert.AreEqual(1.0, frame.Side.Length(), 1e-6);
+ }
+
+ private static void AssertVector(Vector3 actual, double x, double y, double z, double tolerance = 1e-6)
+ {
+ Assert.AreEqual(x, actual.X, tolerance);
+ Assert.AreEqual(y, actual.Y, tolerance);
+ Assert.AreEqual(z, actual.Z, tolerance);
+ }
+ }
+}
diff --git a/UnitTests/CoordinateSystem/RealObjectPlanarPoseSolverTests.cs b/UnitTests/CoordinateSystem/RealObjectPlanarPoseSolverTests.cs
index a57f560..9acc6a3 100644
--- a/UnitTests/CoordinateSystem/RealObjectPlanarPoseSolverTests.cs
+++ b/UnitTests/CoordinateSystem/RealObjectPlanarPoseSolverTests.cs
@@ -88,6 +88,42 @@ namespace NavisworksTransport.UnitTests.CoordinateSystem
Assert.AreEqual(0.0, Vector3.Dot(transformedZ, transformedX), 1e-4);
}
+ [TestMethod]
+ public void FixedReferenceAxis_ShouldKeepAxisFamilyAcrossTurn()
+ {
+ Quaternion referenceRotation = Quaternion.CreateFromAxisAngle(Vector3.UnitY, (float)Math.PI);
+ Vector3 desiredForwardStart = Vector3.Normalize(new Vector3(-0.95f, 0.0f, 0.31f));
+ Vector3 desiredForwardTurn = Vector3.Normalize(new Vector3(0.22f, 0.0f, 0.97f));
+ Vector3 desiredUp = Vector3.UnitY;
+
+ bool startOk = RealObjectPlanarPoseSolver.TryCreatePlanarPoseFromReferencePose(
+ referenceRotation,
+ desiredForwardStart,
+ desiredUp,
+ out RealObjectPlanarPoseSolution startSolution);
+
+ Assert.IsTrue(startOk);
+
+ bool turnOk = RealObjectPlanarPoseSolver.TryCreatePlanarPoseFromReferencePose(
+ referenceRotation,
+ desiredForwardTurn,
+ desiredUp,
+ startSolution.SelectedReferenceAxisDirection,
+ out RealObjectPlanarPoseSolution turnSolution);
+
+ Assert.IsTrue(turnOk);
+ Assert.AreEqual(startSolution.SelectedReferenceAxisDirection, turnSolution.SelectedReferenceAxisDirection);
+
+ Vector3 transformedSelectedAxis = Vector3.Normalize(
+ Vector3.Transform(turnSolution.SelectedReferenceAxisLocal, turnSolution.BaselineRotation));
+ AssertVector(
+ transformedSelectedAxis,
+ desiredForwardTurn.X,
+ desiredForwardTurn.Y,
+ desiredForwardTurn.Z,
+ 1e-4);
+ }
+
private static void AssertVector(Vector3 vector, double x, double y, double z, double tolerance = 1e-6)
{
Assert.AreEqual(x, vector.X, tolerance);
diff --git a/UnitTests/CoordinateSystem/RealObjectProjectedExtentResolverTests.cs b/UnitTests/CoordinateSystem/RealObjectProjectedExtentResolverTests.cs
new file mode 100644
index 0000000..87fa470
--- /dev/null
+++ b/UnitTests/CoordinateSystem/RealObjectProjectedExtentResolverTests.cs
@@ -0,0 +1,66 @@
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using NavisworksTransport.Utils.CoordinateSystem;
+using System.Numerics;
+
+namespace NavisworksTransport.UnitTests.CoordinateSystem
+{
+ [TestClass]
+ public class RealObjectProjectedExtentResolverTests
+ {
+ [TestMethod]
+ public void Ground_DualAxisCorrection_ShouldProjectAgainstFinalPose()
+ {
+ var convention = new ModelAxisConvention(LocalAxisDirection.PositiveX, LocalAxisDirection.PositiveY);
+ Vector3 targetForward = Vector3.Normalize(new Vector3(-0.8987f, 0.0f, 0.4386f));
+ Vector3 targetUp = Vector3.UnitY;
+
+ Quaternion baseline = Quaternion.Normalize(Quaternion.CreateFromRotationMatrix(new Matrix4x4(
+ -0.8987f, 0.0000f, 0.4386f, 0f,
+ 0.0000f, 1.0000f, 0.0000f, 0f,
+ -0.4386f, 0.0000f,-0.8987f, 0f,
+ 0f, 0f, 0f, 1f)));
+
+ var adapter = new HostCoordinateAdapter(CoordinateSystemType.YUp);
+ Quaternion final = adapter.ComposeHostQuaternion(
+ baseline,
+ new LocalEulerRotationCorrection(90.0, 0.0, 90.0));
+
+ var extents = RealObjectProjectedExtentResolver.CalculateProjectedSemanticExtents(
+ convention,
+ forwardSize: 6.0,
+ sideSize: 2.0,
+ upSize: 4.0,
+ baseline,
+ final,
+ targetForward,
+ targetUp);
+
+ Vector3 localSize = convention.CreateScaleVector3(6.0, 2.0, 4.0);
+ Vector3 rotatedLocalX = Vector3.Normalize(Vector3.Transform(Vector3.UnitX, final));
+ Vector3 rotatedLocalY = Vector3.Normalize(Vector3.Transform(Vector3.UnitY, final));
+ Vector3 rotatedLocalZ = Vector3.Normalize(Vector3.Transform(Vector3.UnitZ, final));
+ Vector3 targetSide = Vector3.Normalize(Vector3.Cross(targetForward, targetUp));
+
+ double expectedForward = ProjectExtent(localSize, rotatedLocalX, rotatedLocalY, rotatedLocalZ, targetForward);
+ double expectedSide = ProjectExtent(localSize, rotatedLocalX, rotatedLocalY, rotatedLocalZ, targetSide);
+ double expectedUp = ProjectExtent(localSize, rotatedLocalX, rotatedLocalY, rotatedLocalZ, targetUp);
+
+ Assert.AreEqual(expectedForward, extents.forwardExtent, 1e-5);
+ Assert.AreEqual(expectedSide, extents.sideExtent, 1e-5);
+ Assert.AreEqual(expectedUp, extents.upExtent, 1e-5);
+ Assert.AreNotEqual(4.0, extents.upExtent, 1e-3, "双轴旋转后,Ground 的法线尺寸不应停留在旧高度。");
+ }
+
+ private static double ProjectExtent(
+ Vector3 localSize,
+ Vector3 rotatedLocalX,
+ Vector3 rotatedLocalY,
+ Vector3 rotatedLocalZ,
+ Vector3 targetAxis)
+ {
+ return System.Math.Abs(Vector3.Dot(rotatedLocalX, targetAxis)) * localSize.X +
+ System.Math.Abs(Vector3.Dot(rotatedLocalY, targetAxis)) * localSize.Y +
+ System.Math.Abs(Vector3.Dot(rotatedLocalZ, targetAxis)) * localSize.Z;
+ }
+ }
+}
diff --git a/UnitTests/CoordinateSystem/RealObjectReferencePoseResolverTests.cs b/UnitTests/CoordinateSystem/RealObjectReferencePoseResolverTests.cs
new file mode 100644
index 0000000..a731e10
--- /dev/null
+++ b/UnitTests/CoordinateSystem/RealObjectReferencePoseResolverTests.cs
@@ -0,0 +1,76 @@
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using NavisworksTransport.Utils.CoordinateSystem;
+using System.Collections.Generic;
+using System.Numerics;
+
+namespace NavisworksTransport.UnitTests.CoordinateSystem
+{
+ [TestClass]
+ public class RealObjectReferencePoseResolverTests
+ {
+ [TestMethod]
+ public void YUp_HostWithFragmentDefaultZ_ShouldInterpretRepresentativeFrameToHostSemantics()
+ {
+ var fragmentMatrices = new List
+ {
+ CreateMatrix(
+ axisX: new Vector3(1.0f, 0.0f, 0.0f),
+ axisY: new Vector3(0.0f, 1.0f, 0.0f),
+ axisZ: new Vector3(0.0f, 0.0f, 1.0f))
+ };
+
+ bool ok = RealObjectReferencePoseResolver.TryResolveFromFragmentMatrices(
+ fragmentMatrices,
+ fragmentDefaultUpAxis: "Z",
+ hostUpAxis: "Y",
+ out RealObjectReferencePose pose);
+
+ Assert.IsTrue(ok);
+ AssertVector(pose.AxisX, 1.0, 0.0, 0.0);
+ AssertVector(pose.AxisY, 0.0, 0.0, 1.0);
+ AssertVector(pose.AxisZ, 0.0, -1.0, 0.0);
+ }
+
+ [TestMethod]
+ public void DetectDefaultUpAxis_ShouldReturnZ_WhenRepresentativeZMatchesHostUp()
+ {
+ var fragmentMatrices = new List
+ {
+ CreateMatrix(
+ axisX: new Vector3(1.0f, 0.0f, 0.0f),
+ axisY: new Vector3(0.0f, 0.0f, -1.0f),
+ axisZ: new Vector3(0.0f, 1.0f, 0.0f))
+ };
+
+ bool ok = RealObjectReferencePoseResolver.TryDetectDefaultUpAxis(
+ fragmentMatrices,
+ "Y",
+ out string detectedAxis,
+ out float yAlignment,
+ out float zAlignment);
+
+ Assert.IsTrue(ok);
+ Assert.AreEqual("Z", detectedAxis);
+ Assert.AreEqual(0.0, yAlignment, 1e-6);
+ Assert.AreEqual(1.0, zAlignment, 1e-6);
+ }
+
+ private static double[] CreateMatrix(Vector3 axisX, Vector3 axisY, Vector3 axisZ)
+ {
+ return new double[]
+ {
+ axisX.X, axisX.Y, axisX.Z, 0.0,
+ axisY.X, axisY.Y, axisY.Z, 0.0,
+ axisZ.X, axisZ.Y, axisZ.Z, 0.0,
+ 0.0, 0.0, 0.0, 1.0
+ };
+ }
+
+ private static void AssertVector(Vector3 actual, double x, double y, double z, double tolerance = 1e-6)
+ {
+ Assert.AreEqual(x, actual.X, tolerance);
+ Assert.AreEqual(y, actual.Y, tolerance);
+ Assert.AreEqual(z, actual.Z, tolerance);
+ }
+ }
+}
diff --git a/UnitTests/Core/PathPersistenceTests.cs b/UnitTests/Core/PathPersistenceTests.cs
new file mode 100644
index 0000000..fa47c56
--- /dev/null
+++ b/UnitTests/Core/PathPersistenceTests.cs
@@ -0,0 +1,117 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using Autodesk.Navisworks.Api;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace NavisworksTransport.UnitTests.Core
+{
+ [TestClass]
+ public class PathPersistenceTests
+ {
+ [TestMethod]
+ public void PathDataManager_ShouldImport_RailPreferredNormal_FromJson()
+ {
+ var tempDir = Path.Combine(Path.GetTempPath(), "NavisworksTransportTests", Guid.NewGuid().ToString("N"));
+ Directory.CreateDirectory(tempDir);
+ var filePath = Path.Combine(tempDir, "route.json");
+
+ try
+ {
+ File.WriteAllText(filePath,
+@"{
+ ""PathPlanningData"": {
+ ""version"": ""1.0"",
+ ""generator"": ""test"",
+ ""timestamp"": ""2026-03-25T00:00:00"",
+ ""ProjectInfo"": {
+ ""name"": ""test"",
+ ""description"": ""test"",
+ ""units"": ""meters"",
+ ""coordinateSystem"": ""Global""
+ },
+ ""Routes"": [
+ {
+ ""id"": ""route-1"",
+ ""name"": ""rail-test"",
+ ""description"": """",
+ ""pathType"": ""Rail"",
+ ""railMountMode"": ""OverRail"",
+ ""railPathDefinitionMode"": ""RailCenterLine"",
+ ""railPreferredNormal"": { ""x"": -0.2, ""y"": 0.5, ""z"": 0.8 },
+ ""totalLength"": 10.0,
+ ""objectLimits"": { ""maxLength"": 0, ""maxWidth"": 0, ""maxHeight"": 0, ""safetyMargin"": 0 },
+ ""gridSize"": 1.0,
+ ""liftHeight"": 0.0,
+ ""created"": ""2026-03-25T00:00:00"",
+ ""points"": [
+ { ""id"": ""p1"", ""name"": ""start"", ""type"": ""StartPoint"", ""index"": 0, ""x"": 0, ""y"": 0, ""z"": 0, ""created"": ""2026-03-25T00:00:00"" },
+ { ""id"": ""p2"", ""name"": ""end"", ""type"": ""EndPoint"", ""index"": 1, ""x"": 10, ""y"": 0, ""z"": 0, ""created"": ""2026-03-25T00:00:00"" }
+ ]
+ }
+ ]
+ }
+}");
+
+ var manager = new PathDataManager();
+ var importedRoutes = manager.ImportFromJson(filePath);
+ Assert.AreEqual(1, importedRoutes.Count);
+ Assert.IsNotNull(importedRoutes[0].RailPreferredNormal);
+ }
+ finally
+ {
+ SafeDelete(tempDir);
+ }
+ }
+
+ [TestMethod]
+ public void PathDataManager_ShouldImport_RailPreferredNormal_FromXml()
+ {
+ var tempDir = Path.Combine(Path.GetTempPath(), "NavisworksTransportTests", Guid.NewGuid().ToString("N"));
+ Directory.CreateDirectory(tempDir);
+ var filePath = Path.Combine(tempDir, "route.xml");
+
+ try
+ {
+ File.WriteAllText(filePath,
+@"
+
+
+
+
+
+
+
+
+
+
+");
+
+ var manager = new PathDataManager();
+ var importedRoutes = manager.ImportFromXml(filePath);
+ Assert.AreEqual(1, importedRoutes.Count);
+ Assert.IsNotNull(importedRoutes[0].RailPreferredNormal);
+ }
+ finally
+ {
+ SafeDelete(tempDir);
+ }
+ }
+
+ private static void SafeDelete(string directory)
+ {
+ if (!Directory.Exists(directory))
+ {
+ return;
+ }
+
+ try
+ {
+ Directory.Delete(directory, true);
+ }
+ catch
+ {
+ }
+ }
+ }
+}
diff --git a/doc/working/current-engineering-state.md b/doc/working/current-engineering-state.md
index 548662f..3e99d9e 100644
--- a/doc/working/current-engineering-state.md
+++ b/doc/working/current-engineering-state.md
@@ -97,6 +97,11 @@
- 地面/吊装路径已经开始走完整姿态链路,不再允许悄悄退回旧 `yaw` 方案。
- 如果完整姿态生成失败,应直接暴露错误,而不是 fallback。
+- 当前已知边界:
+ - 地面路径播放阶段,真实物体的多轴角度修正还没有完全收稳。
+ - 目前稳定可用的是:`Y` 轴转动。
+ - `X / Z` 轴在起点静态预览或直线段里可能看起来合理,但路径一旦拐弯,逐帧按宿主世界轴重算会让“俯仰/侧倾”语义发生耦合,表现成不符合现场直觉的侧旋。
+ - 因此当前阶段,对地面路径应按“只稳定支持 `Y` 轴转动”使用;`X / Z` 轴播放链问题留待后续专项修复。
- `YUp` 吊装路径创建主链路已补齐到宿主坐标适配架构:
- 提升、水平移动、下降、终点落地都不能再把世界 `Z` 硬编码成“向上”。
- 终点必须使用用户最后一次点击的地面点,不能回填起点地面高程。
diff --git a/src/Core/Animation/PathAnimationManager.cs b/src/Core/Animation/PathAnimationManager.cs
index 23e5198..da2e878 100644
--- a/src/Core/Animation/PathAnimationManager.cs
+++ b/src/Core/Animation/PathAnimationManager.cs
@@ -116,6 +116,8 @@ namespace NavisworksTransport.Core.Animation
private Vector3 _realObjectReferenceAxisY = Vector3.UnitY;
private Vector3 _realObjectReferenceAxisZ = Vector3.UnitZ;
private bool _hasRealObjectReferenceRotation = false;
+ private LocalAxisDirection _realObjectPlanarSelectedForwardAxis = LocalAxisDirection.PositiveX;
+ private bool _hasRealObjectPlanarSelectedForwardAxis = false;
private List _pathPoints;
private List _manualCollisionTargets = new List();
private bool _manualCollisionOverrideEnabled = false;
@@ -3521,28 +3523,16 @@ namespace NavisworksTransport.Core.Animation
var adapter = CoordinateSystemManager.Instance.CreateHostAdapter();
if (IsRealObjectMode && _hasRealObjectReferenceRotation)
{
- Vector3 desiredForward = new Vector3(
- (float)(nextPoint.X - previousPoint.X),
- (float)(nextPoint.Y - previousPoint.Y),
- (float)(nextPoint.Z - previousPoint.Z));
-
- if (desiredForward.LengthSquared() < 1e-6f)
- {
- desiredForward = new Vector3(
- (float)(nextPoint.X - currentPoint.X),
- (float)(nextPoint.Y - currentPoint.Y),
- (float)(nextPoint.Z - currentPoint.Z));
- }
-
- if (RealObjectRailAxisConventionResolver.TryResolve(
- _realObjectReferenceAxisX,
- _realObjectReferenceAxisY,
- _realObjectReferenceAxisZ,
- desiredForward,
- adapter.HostType,
- out convention,
- out var selectedForwardAxis,
- out var selectedForwardWorldAxis))
+ if (PathTargetFrameResolver.TryCreateRailHostFrame(_route, previousPoint, currentPoint, nextPoint, out var targetFrame) &&
+ RealObjectRailAxisConventionResolver.TryResolve(
+ _realObjectReferenceAxisX,
+ _realObjectReferenceAxisY,
+ _realObjectReferenceAxisZ,
+ targetFrame.Forward,
+ adapter.HostType,
+ out convention,
+ out var selectedForwardAxis,
+ out var selectedForwardWorldAxis))
{
LogManager.Info(
$"[Rail姿态修正] Host={adapter.HostType}, 虚拟物体={IsVirtualObjectMode}, " +
@@ -3579,17 +3569,9 @@ namespace NavisworksTransport.Core.Animation
}
var adapter = CoordinateSystemManager.Instance.CreateHostAdapter();
- Vector3 desiredForward = new Vector3(
- (float)(nextPoint.X - previousPoint.X),
- (float)(nextPoint.Y - previousPoint.Y),
- (float)(nextPoint.Z - previousPoint.Z));
-
- if (desiredForward.LengthSquared() < 1e-6f)
+ if (!PathTargetFrameResolver.TryCreateRailHostFrame(_route, previousPoint, currentPoint, nextPoint, out var targetFrame))
{
- desiredForward = new Vector3(
- (float)(nextPoint.X - currentPoint.X),
- (float)(nextPoint.Y - currentPoint.Y),
- (float)(nextPoint.Z - currentPoint.Z));
+ return false;
}
if (!TryGetCurrentRailModelAxisConvention(previousPoint, currentPoint, nextPoint, out var railConvention))
@@ -3622,7 +3604,7 @@ namespace NavisworksTransport.Core.Animation
_realObjectReferenceAxisX,
_realObjectReferenceAxisY,
_realObjectReferenceAxisZ,
- desiredForward,
+ targetFrame.Forward,
adapter.HostType,
_realObjectLength,
_realObjectWidth,
@@ -3633,6 +3615,103 @@ namespace NavisworksTransport.Core.Animation
out extents);
}
+ private bool TryCalculateCurrentRealObjectPlanarProjectedExtents(
+ Point3D previousPoint,
+ Point3D currentPoint,
+ Point3D nextPoint,
+ out ModelAxisConvention convention,
+ out (double forwardExtent, double sideExtent, double upExtent) extents)
+ {
+ convention = null;
+ extents = (0.0, 0.0, 0.0);
+
+ if (!IsRealObjectMode ||
+ !_hasRealObjectReferenceRotation ||
+ _realObjectLength <= 0.0 ||
+ _realObjectWidth <= 0.0 ||
+ _realObjectHeight <= 0.0)
+ {
+ return false;
+ }
+
+ Vector3 hostForward = ToNumerics(nextPoint) - ToNumerics(previousPoint);
+ if (hostForward.LengthSquared() < 1e-6f)
+ {
+ hostForward = ToNumerics(nextPoint) - ToNumerics(currentPoint);
+ }
+
+ var adapter = CoordinateSystemManager.Instance.CreateHostAdapter();
+ if (!PathTargetFrameResolver.TryCreatePlanarHostFrame(hostForward, adapter.HostType, out var targetFrame))
+ {
+ return false;
+ }
+
+ if (!TryCreateRealObjectPlanarPoseSolution(
+ targetFrame.Forward,
+ targetFrame.Up,
+ out var solution))
+ {
+ return false;
+ }
+
+ LocalAxisDirection semanticUpAxis =
+ adapter.HostType == CoordinateSystemType.YUp
+ ? LocalAxisDirection.PositiveY
+ : LocalAxisDirection.PositiveZ;
+ convention = new ModelAxisConvention(solution.SelectedReferenceAxisDirection, semanticUpAxis);
+
+ Quaternion finalHostQuaternion = adapter.ComposeHostQuaternion(
+ solution.BaselineRotation,
+ _objectRotationCorrection);
+ extents = RealObjectProjectedExtentResolver.CalculateProjectedSemanticExtents(
+ convention,
+ _realObjectLength,
+ _realObjectWidth,
+ _realObjectHeight,
+ solution.BaselineRotation,
+ finalHostQuaternion,
+ targetFrame.Forward,
+ targetFrame.Up);
+ return true;
+ }
+
+ public bool TryGetCurrentRouteRealObjectPlanarProjectedExtents(
+ out double forwardExtent,
+ out double sideExtent,
+ out double upExtent)
+ {
+ forwardExtent = 0.0;
+ sideExtent = 0.0;
+ upExtent = 0.0;
+
+ if (_route == null ||
+ (_route.PathType != PathType.Ground && _route.PathType != PathType.Hoisting) ||
+ _pathPoints == null ||
+ _pathPoints.Count == 0)
+ {
+ return false;
+ }
+
+ Point3D previousPoint = _pathPoints[0];
+ Point3D currentPoint = _pathPoints[0];
+ Point3D nextPoint = _pathPoints.Count > 1 ? _pathPoints[1] : _pathPoints[0];
+
+ if (!TryCalculateCurrentRealObjectPlanarProjectedExtents(
+ previousPoint,
+ currentPoint,
+ nextPoint,
+ out _,
+ out var extents))
+ {
+ return false;
+ }
+
+ forwardExtent = extents.forwardExtent;
+ sideExtent = extents.sideExtent;
+ upExtent = extents.upExtent;
+ return true;
+ }
+
public bool TryGetCurrentRouteRealObjectRailProjectedExtents(
out double forwardExtent,
out double sideExtent,
@@ -3883,21 +3962,23 @@ namespace NavisworksTransport.Core.Animation
{
rotation = Rotation3D.Identity;
- if (!TryGetRealObjectReferenceRotation(out var referenceRotation))
+ if (!PathTargetFrameResolver.TryCreatePlanarHostFrame(
+ hostForward,
+ CoordinateSystemManager.Instance.CreateHostAdapter().HostType,
+ out var targetFrame))
{
return false;
}
- var adapter = CoordinateSystemManager.Instance.CreateHostAdapter();
- if (!RealObjectPlanarPoseSolver.TryCreatePlanarPoseFromReferencePose(
- referenceRotation,
- hostForward,
- adapter.HostUpVector3,
+ if (!TryCreateRealObjectPlanarPoseSolution(
+ targetFrame.Forward,
+ targetFrame.Up,
out var solution))
{
return false;
}
+ var adapter = CoordinateSystemManager.Instance.CreateHostAdapter();
Quaternion hostComposedQuaternion = adapter.ComposeHostQuaternion(solution.BaselineRotation, _objectRotationCorrection);
rotation = adapter.FromHostQuaternion(hostComposedQuaternion);
@@ -3916,6 +3997,45 @@ namespace NavisworksTransport.Core.Animation
return true;
}
+ private bool TryCreateRealObjectPlanarPoseSolution(
+ Vector3 targetForward,
+ Vector3 targetUp,
+ out RealObjectPlanarPoseSolution solution)
+ {
+ solution = null;
+
+ if (!TryGetRealObjectReferenceRotation(out var referenceRotation))
+ {
+ return false;
+ }
+
+ LocalAxisDirection? fixedForwardAxis = null;
+ if ((_route?.PathType == PathType.Ground || _route?.PathType == PathType.Hoisting) &&
+ _hasRealObjectPlanarSelectedForwardAxis)
+ {
+ fixedForwardAxis = _realObjectPlanarSelectedForwardAxis;
+ }
+
+ if (!RealObjectPlanarPoseSolver.TryCreatePlanarPoseFromReferencePose(
+ referenceRotation,
+ targetForward,
+ targetUp,
+ fixedForwardAxis,
+ out solution))
+ {
+ return false;
+ }
+
+ if ((_route?.PathType == PathType.Ground || _route?.PathType == PathType.Hoisting) &&
+ !_hasRealObjectPlanarSelectedForwardAxis)
+ {
+ _realObjectPlanarSelectedForwardAxis = solution.SelectedReferenceAxisDirection;
+ _hasRealObjectPlanarSelectedForwardAxis = true;
+ }
+
+ return true;
+ }
+
private void ShowRealObjectFragmentUpMismatchHintIfNeeded(Vector3 representativeYAxis, Vector3 representativeZAxis)
{
if (_route == null || (_route.PathType != PathType.Ground && _route.PathType != PathType.Hoisting))
@@ -4033,85 +4153,27 @@ namespace NavisworksTransport.Core.Animation
try
{
- var selection = ComApiBridge.ToInwOpSelection(new ModelItemCollection { sourceObject });
- var fragmentInfos = GeometryHelper.GetAllFragments(selection);
- try
+ var doc = NavisApplication.ActiveDocument;
+ if (!RealObjectReferencePoseResolver.TryResolveFromModelItem(sourceObject, doc, out var referencePose))
{
- var fragmentMatrices = fragmentInfos
- .Where(fragmentInfo => fragmentInfo.TransformMatrix != null && fragmentInfo.TransformMatrix.Length == 16)
- .Select(fragmentInfo => fragmentInfo.TransformMatrix)
- .ToList();
-
- if (fragmentMatrices.Count == 0)
- {
- LogManager.Warning($"[真实物体参考姿态] {sourceObject.DisplayName} 未提取到 fragment 代表姿态");
- return false;
- }
-
- if (!FragmentRepresentativePoseHelper.TryGetRepresentativeFrame(fragmentMatrices, out var rawRepresentativeFrame))
- {
- LogManager.Warning($"[真实物体参考姿态] {sourceObject.DisplayName} fragment 代表姿态求解失败");
- return false;
- }
-
- var doc = NavisApplication.ActiveDocument;
- string fragmentDefaultUpAxis = FragmentDefaultUpContext.GetCurrentDocumentFragmentDefaultUpAxis(doc);
- string hostUpAxis = FragmentDefaultUpContext.GetCurrentHostUpAxis(doc);
- if (!FragmentRepresentativePoseHelper.TryInterpretRepresentativeFrame(
- rawRepresentativeFrame,
- fragmentDefaultUpAxis,
- hostUpAxis,
- out var representativeFrame))
- {
- LogManager.Warning($"[真实物体参考姿态] {sourceObject.DisplayName} fragment 姿态解释失败");
- return false;
- }
-
- referenceRotation = representativeFrame.Rotation;
- _realObjectReferenceRotation = referenceRotation;
- _realObjectReferenceAxisX = representativeFrame.AxisX;
- _realObjectReferenceAxisY = representativeFrame.AxisY;
- _realObjectReferenceAxisZ = representativeFrame.AxisZ;
- _hasRealObjectReferenceRotation = true;
-
- LogManager.Info(
- $"[真实物体参考姿态] {sourceObject.DisplayName} 使用 fragment 代表姿态: " +
- $"X=({_realObjectReferenceAxisX.X:F4},{_realObjectReferenceAxisX.Y:F4},{_realObjectReferenceAxisX.Z:F4}), " +
- $"Y=({_realObjectReferenceAxisY.X:F4},{_realObjectReferenceAxisY.Y:F4},{_realObjectReferenceAxisY.Z:F4}), " +
- $"Z=({_realObjectReferenceAxisZ.X:F4},{_realObjectReferenceAxisZ.Y:F4},{_realObjectReferenceAxisZ.Z:F4}), " +
- $"fragment数量={fragmentMatrices.Count}, Fragment默认Up={fragmentDefaultUpAxis}, 模型Up={hostUpAxis}");
- return true;
+ LogManager.Warning($"[真实物体参考姿态] {sourceObject.DisplayName} fragment 姿态解析失败");
+ return false;
}
- finally
- {
- if (fragmentInfos != null)
- {
- foreach (var fragmentInfo in fragmentInfos)
- {
- if (fragmentInfo?.Fragment != null)
- {
- try
- {
- Marshal.ReleaseComObject(fragmentInfo.Fragment);
- }
- catch
- {
- }
- }
- }
- }
- if (selection != null)
- {
- try
- {
- Marshal.ReleaseComObject(selection);
- }
- catch
- {
- }
- }
- }
+ referenceRotation = referencePose.Rotation;
+ _realObjectReferenceRotation = referenceRotation;
+ _realObjectReferenceAxisX = referencePose.AxisX;
+ _realObjectReferenceAxisY = referencePose.AxisY;
+ _realObjectReferenceAxisZ = referencePose.AxisZ;
+ _hasRealObjectReferenceRotation = true;
+
+ LogManager.Info(
+ $"[真实物体参考姿态] {sourceObject.DisplayName} 使用 fragment 代表姿态: " +
+ $"X=({_realObjectReferenceAxisX.X:F4},{_realObjectReferenceAxisX.Y:F4},{_realObjectReferenceAxisX.Z:F4}), " +
+ $"Y=({_realObjectReferenceAxisY.X:F4},{_realObjectReferenceAxisY.Y:F4},{_realObjectReferenceAxisY.Z:F4}), " +
+ $"Z=({_realObjectReferenceAxisZ.X:F4},{_realObjectReferenceAxisZ.Y:F4},{_realObjectReferenceAxisZ.Z:F4}), " +
+ $"fragment数量={referencePose.FragmentCount}, Fragment默认Up={referencePose.FragmentDefaultUpAxis}, 模型Up={referencePose.HostUpAxis}");
+ return true;
}
catch (Exception ex)
{
@@ -4151,6 +4213,8 @@ namespace NavisworksTransport.Core.Animation
_realObjectReferenceAxisY = Vector3.UnitY;
_realObjectReferenceAxisZ = Vector3.UnitZ;
_hasRealObjectReferenceRotation = false;
+ _realObjectPlanarSelectedForwardAxis = LocalAxisDirection.PositiveX;
+ _hasRealObjectPlanarSelectedForwardAxis = false;
}
///
diff --git a/src/Core/PathDataManager.cs b/src/Core/PathDataManager.cs
index 17036f3..98c8e30 100644
--- a/src/Core/PathDataManager.cs
+++ b/src/Core/PathDataManager.cs
@@ -50,6 +50,7 @@ namespace NavisworksTransport
public string pathType { get; set; }
public string railMountMode { get; set; }
public string railPathDefinitionMode { get; set; }
+ public JsonVector3 railPreferredNormal { get; set; }
public double totalLength { get; set; }
public JsonObjectLimits objectLimits { get; set; }
public double gridSize { get; set; }
@@ -58,6 +59,13 @@ namespace NavisworksTransport
public JsonPathPoint[] points { get; set; }
}
+ public class JsonVector3
+ {
+ public double x { get; set; }
+ public double y { get; set; }
+ public double z { get; set; }
+ }
+
///
/// JSON格式的项目信息
///
@@ -279,6 +287,12 @@ namespace NavisworksTransport
pathType = route.PathType.ToString(),
railMountMode = route.RailMountMode.ToString(),
railPathDefinitionMode = route.RailPathDefinitionMode.ToString(),
+ railPreferredNormal = route.RailPreferredNormal != null ? new
+ {
+ x = Math.Round(route.RailPreferredNormal.X, exportSettings?.Precision ?? 3),
+ y = Math.Round(route.RailPreferredNormal.Y, exportSettings?.Precision ?? 3),
+ z = Math.Round(route.RailPreferredNormal.Z, exportSettings?.Precision ?? 3)
+ } : null,
totalLength = Math.Round(route.TotalLength, exportSettings?.Precision ?? 3),
objectLimits = new
{
@@ -528,6 +542,14 @@ namespace NavisworksTransport
route.RailPathDefinitionMode = railPathDefinitionMode;
}
+ if (jsonRoute.railPreferredNormal != null)
+ {
+ route.RailPreferredNormal = new Point3D(
+ jsonRoute.railPreferredNormal.x,
+ jsonRoute.railPreferredNormal.y,
+ jsonRoute.railPreferredNormal.z);
+ }
+
// 解析路径点
var points = new List();
foreach (var jsonPoint in jsonRoute.points)
@@ -1387,6 +1409,12 @@ namespace NavisworksTransport
routeElement.SetAttribute("pathType", route.PathType.ToString());
routeElement.SetAttribute("railMountMode", route.RailMountMode.ToString());
routeElement.SetAttribute("railPathDefinitionMode", route.RailPathDefinitionMode.ToString());
+ if (route.RailPreferredNormal != null)
+ {
+ routeElement.SetAttribute("railPreferredNormalX", route.RailPreferredNormal.X.ToString("F3"));
+ routeElement.SetAttribute("railPreferredNormalY", route.RailPreferredNormal.Y.ToString("F3"));
+ routeElement.SetAttribute("railPreferredNormalZ", route.RailPreferredNormal.Z.ToString("F3"));
+ }
routeElement.SetAttribute("totalLength", route.TotalLength.ToString("F3"));
routeElement.SetAttribute("maxObjectLength", route.MaxObjectLength.ToString("F3"));
routeElement.SetAttribute("maxObjectWidth", route.MaxObjectWidth.ToString("F3"));
@@ -1529,6 +1557,16 @@ namespace NavisworksTransport
route.RailPathDefinitionMode = railPathDefinitionMode;
}
+ if (double.TryParse(routeNode.Attributes?["railPreferredNormalX"]?.Value, out var railPreferredNormalX) &&
+ double.TryParse(routeNode.Attributes?["railPreferredNormalY"]?.Value, out var railPreferredNormalY) &&
+ double.TryParse(routeNode.Attributes?["railPreferredNormalZ"]?.Value, out var railPreferredNormalZ))
+ {
+ route.RailPreferredNormal = new Point3D(
+ railPreferredNormalX,
+ railPreferredNormalY,
+ railPreferredNormalZ);
+ }
+
// 解析创建时间
if (DateTime.TryParse(routeNode.Attributes?["created"]?.Value, out DateTime createdTime))
{
diff --git a/src/Core/PathDatabase.cs b/src/Core/PathDatabase.cs
index 44af0c6..09d4d91 100644
--- a/src/Core/PathDatabase.cs
+++ b/src/Core/PathDatabase.cs
@@ -101,6 +101,9 @@ namespace NavisworksTransport
LiftHeight REAL,
RailMountMode INTEGER,
RailPathDefinitionMode INTEGER,
+ RailPreferredNormalX REAL,
+ RailPreferredNormalY REAL,
+ RailPreferredNormalZ REAL,
CreatedTime DATETIME,
LastModified DATETIME
)
@@ -463,8 +466,8 @@ namespace NavisworksTransport
// 路径长度由 PathRoute.TotalLength 计算属性实时从 Edges/Points 计算
var sql = @"
INSERT OR REPLACE INTO PathRoutes
- (Id, Name, EstimatedTime, TurnRadius, IsCurved, MaxObjectLength, MaxObjectWidth, MaxObjectHeight, SafetyMargin, GridSize, PathType, LiftHeight, RailMountMode, RailPathDefinitionMode, CreatedTime, LastModified)
- VALUES (@id, @name, @time, @turnRadius, @isCurved, @maxLength, @maxWidth, @maxHeight, @safetyMargin, @gridSize, @pathType, @liftHeightMeters, @railMountMode, @railPathDefinitionMode, @created, @modified)
+ (Id, Name, EstimatedTime, TurnRadius, IsCurved, MaxObjectLength, MaxObjectWidth, MaxObjectHeight, SafetyMargin, GridSize, PathType, LiftHeight, RailMountMode, RailPathDefinitionMode, RailPreferredNormalX, RailPreferredNormalY, RailPreferredNormalZ, CreatedTime, LastModified)
+ VALUES (@id, @name, @time, @turnRadius, @isCurved, @maxLength, @maxWidth, @maxHeight, @safetyMargin, @gridSize, @pathType, @liftHeightMeters, @railMountMode, @railPathDefinitionMode, @railPreferredNormalX, @railPreferredNormalY, @railPreferredNormalZ, @created, @modified)
";
using (var cmd = new SQLiteCommand(sql, _connection))
@@ -483,6 +486,9 @@ namespace NavisworksTransport
cmd.Parameters.AddWithValue("@liftHeightMeters", route.LiftHeight);
cmd.Parameters.AddWithValue("@railMountMode", (int)route.RailMountMode);
cmd.Parameters.AddWithValue("@railPathDefinitionMode", (int)route.RailPathDefinitionMode);
+ cmd.Parameters.AddWithValue("@railPreferredNormalX", route.RailPreferredNormal != null ? (object)route.RailPreferredNormal.X : DBNull.Value);
+ cmd.Parameters.AddWithValue("@railPreferredNormalY", route.RailPreferredNormal != null ? (object)route.RailPreferredNormal.Y : DBNull.Value);
+ cmd.Parameters.AddWithValue("@railPreferredNormalZ", route.RailPreferredNormal != null ? (object)route.RailPreferredNormal.Z : DBNull.Value);
cmd.Parameters.AddWithValue("@created", route.CreatedTime);
cmd.Parameters.AddWithValue("@modified", DateTime.Now);
cmd.ExecuteNonQuery();
@@ -1544,6 +1550,16 @@ namespace NavisworksTransport
LastModified = Convert.ToDateTime(reader["LastModified"])
};
+ if (!reader.IsDBNull(reader.GetOrdinal("RailPreferredNormalX")) &&
+ !reader.IsDBNull(reader.GetOrdinal("RailPreferredNormalY")) &&
+ !reader.IsDBNull(reader.GetOrdinal("RailPreferredNormalZ")))
+ {
+ route.RailPreferredNormal = new Point3D(
+ Convert.ToDouble(reader["RailPreferredNormalX"]),
+ Convert.ToDouble(reader["RailPreferredNormalY"]),
+ Convert.ToDouble(reader["RailPreferredNormalZ"]));
+ }
+
// 添加分析结果(如果存在)
if (!reader.IsDBNull(reader.GetOrdinal("CollisionCount")))
{
@@ -2721,7 +2737,8 @@ namespace NavisworksTransport
cmd.CommandText = @"
SELECT Id, Name, CreatedTime, LastModified,
TurnRadius, IsCurved, MaxObjectLength, MaxObjectWidth, MaxObjectHeight,
- SafetyMargin, GridSize, PathType, LiftHeight, RailMountMode, RailPathDefinitionMode
+ SafetyMargin, GridSize, PathType, LiftHeight, RailMountMode, RailPathDefinitionMode,
+ RailPreferredNormalX, RailPreferredNormalY, RailPreferredNormalZ
FROM PathRoutes
WHERE Id = @Id";
@@ -2750,6 +2767,16 @@ namespace NavisworksTransport
RailPathDefinitionMode = (RailPathDefinitionMode)Convert.ToInt32(reader["RailPathDefinitionMode"])
};
+ if (reader["RailPreferredNormalX"] != DBNull.Value &&
+ reader["RailPreferredNormalY"] != DBNull.Value &&
+ reader["RailPreferredNormalZ"] != DBNull.Value)
+ {
+ route.RailPreferredNormal = new Point3D(
+ Convert.ToDouble(reader["RailPreferredNormalX"]),
+ Convert.ToDouble(reader["RailPreferredNormalY"]),
+ Convert.ToDouble(reader["RailPreferredNormalZ"]));
+ }
+
// 加载路径点和路径边
LoadPathPoints(route);
LoadPathEdges(route);
@@ -2784,7 +2811,10 @@ namespace NavisworksTransport
var columnsToAdd = new Dictionary
{
["railmountmode"] = "ALTER TABLE PathRoutes ADD COLUMN RailMountMode INTEGER DEFAULT 0",
- ["railpathdefinitionmode"] = "ALTER TABLE PathRoutes ADD COLUMN RailPathDefinitionMode INTEGER DEFAULT 0"
+ ["railpathdefinitionmode"] = "ALTER TABLE PathRoutes ADD COLUMN RailPathDefinitionMode INTEGER DEFAULT 0",
+ ["railpreferrednormalx"] = "ALTER TABLE PathRoutes ADD COLUMN RailPreferredNormalX REAL",
+ ["railpreferrednormaly"] = "ALTER TABLE PathRoutes ADD COLUMN RailPreferredNormalY REAL",
+ ["railpreferrednormalz"] = "ALTER TABLE PathRoutes ADD COLUMN RailPreferredNormalZ REAL"
};
foreach (var column in columnsToAdd)
diff --git a/src/Core/PathPlanningManager.cs b/src/Core/PathPlanningManager.cs
index ed6dfd4..f46fe95 100644
--- a/src/Core/PathPlanningManager.cs
+++ b/src/Core/PathPlanningManager.cs
@@ -1269,6 +1269,14 @@ namespace NavisworksTransport
}
// 更新其他属性(不要覆盖TotalLength,因为LoadPathEdges已经根据Edges重新计算了)
route.LiftHeight = loadedRoute.LiftHeight;
+ route.RailMountMode = loadedRoute.RailMountMode;
+ route.RailPathDefinitionMode = loadedRoute.RailPathDefinitionMode;
+ route.RailPreferredNormal = loadedRoute.RailPreferredNormal != null
+ ? new Point3D(
+ loadedRoute.RailPreferredNormal.X,
+ loadedRoute.RailPreferredNormal.Y,
+ loadedRoute.RailPreferredNormal.Z)
+ : null;
LogManager.Debug($"已从数据库加载路径数据: {route.Name}, 共 {loadedRoute.Points.Count} 个点");
}
}
diff --git a/src/Core/PathPlanningModels.cs b/src/Core/PathPlanningModels.cs
index 1a497b6..63341ba 100644
--- a/src/Core/PathPlanningModels.cs
+++ b/src/Core/PathPlanningModels.cs
@@ -806,6 +806,7 @@ namespace NavisworksTransport
Description = string.Empty;
RailMountMode = RailMountMode.UnderRail;
RailPathDefinitionMode = RailPathDefinitionMode.LegacySuspensionPoint;
+ RailPreferredNormal = null;
}
///
@@ -1040,7 +1041,10 @@ namespace NavisworksTransport
MaxObjectHeight = MaxObjectHeight,
SafetyMargin = SafetyMargin,
RailMountMode = RailMountMode,
- RailPathDefinitionMode = RailPathDefinitionMode
+ RailPathDefinitionMode = RailPathDefinitionMode,
+ RailPreferredNormal = RailPreferredNormal != null
+ ? new Point3D(RailPreferredNormal.X, RailPreferredNormal.Y, RailPreferredNormal.Z)
+ : null
};
// 克隆所有路径点
diff --git a/src/UI/WPF/ViewModels/AnimationControlViewModel.cs b/src/UI/WPF/ViewModels/AnimationControlViewModel.cs
index da3b565..6d0fbdd 100644
--- a/src/UI/WPF/ViewModels/AnimationControlViewModel.cs
+++ b/src/UI/WPF/ViewModels/AnimationControlViewModel.cs
@@ -4343,6 +4343,20 @@ namespace NavisworksTransport.UI.WPF.ViewModels
upSize = _objectOriginalHeight * metersToUnits;
var adapter = CoordinateSystemManager.Instance.CreateHostAdapter();
+ if ((CurrentPathRoute?.PathType == NavisworksTransport.PathType.Ground ||
+ CurrentPathRoute?.PathType == NavisworksTransport.PathType.Hoisting) &&
+ _pathAnimationManager != null &&
+ _pathAnimationManager.TryGetCurrentRouteRealObjectPlanarProjectedExtents(
+ out double resolvedPlanarForward,
+ out double resolvedPlanarSide,
+ out double resolvedPlanarUp))
+ {
+ double unitsToMetersForPlanar = UnitsConverter.GetUnitsToMetersConversionFactor();
+ LogManager.Debug(
+ $"[角度修正] 平面真实物体复用最终姿态尺寸: Forward={resolvedPlanarForward * unitsToMetersForPlanar:F2}m, " +
+ $"Side={resolvedPlanarSide * unitsToMetersForPlanar:F2}m, Up={resolvedPlanarUp * unitsToMetersForPlanar:F2}m, 角度={_objectRotationCorrection}");
+ return (resolvedPlanarForward, resolvedPlanarSide, resolvedPlanarUp);
+ }
if (CurrentPathRoute?.PathType == NavisworksTransport.PathType.Rail &&
_pathAnimationManager != null &&
_pathAnimationManager.TryGetCurrentRouteRealObjectRailProjectedExtents(
diff --git a/src/UI/WPF/ViewModels/SystemManagementViewModel.cs b/src/UI/WPF/ViewModels/SystemManagementViewModel.cs
index a2c1ba7..4468645 100644
--- a/src/UI/WPF/ViewModels/SystemManagementViewModel.cs
+++ b/src/UI/WPF/ViewModels/SystemManagementViewModel.cs
@@ -1625,10 +1625,20 @@ namespace NavisworksTransport.UI.WPF.ViewModels
}
string hostUpAxis = GetCurrentHostUpAxis();
- Vector3 hostUpVector = hostUpAxis == "Y" ? Vector3.UnitY : Vector3.UnitZ;
- double yAlignment = Math.Abs(Vector3.Dot(fragmentAnalysis.RepresentativeYAxis, hostUpVector));
- double zAlignment = Math.Abs(Vector3.Dot(fragmentAnalysis.RepresentativeZAxis, hostUpVector));
- string detectedAxis = zAlignment > yAlignment ? "Z" : "Y";
+ if (!RealObjectReferencePoseResolver.TryDetectDefaultUpAxis(
+ fragmentAnalysis.ValidMatrices,
+ hostUpAxis,
+ out string detectedAxis,
+ out float yAlignment,
+ out float zAlignment))
+ {
+ System.Windows.MessageBox.Show(
+ "未能从 fragment 姿态中检测默认Up。",
+ "提示",
+ System.Windows.MessageBoxButton.OK,
+ System.Windows.MessageBoxImage.Warning);
+ return;
+ }
string currentAxis = SelectedFragmentDefaultUpAxis;
LogManager.Info(
$"[fragment默认Up检测] 对象={item.DisplayName}, 宿主Up={hostUpAxis}, 当前值={currentAxis}, 检测值={detectedAxis}, " +
@@ -1825,7 +1835,8 @@ namespace NavisworksTransport.UI.WPF.ViewModels
var analysis = new FragmentOrientationAnalysis
{
FragmentCount = fragments.Count,
- ValidTransformCount = validMatrices.Count
+ ValidTransformCount = validMatrices.Count,
+ ValidMatrices = validMatrices
};
if (validMatrices.Count == 0)
@@ -2013,6 +2024,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
{
public int FragmentCount { get; set; }
public int ValidTransformCount { get; set; }
+ public List ValidMatrices { get; set; }
public bool HasRepresentativeOrientation { get; set; }
public Quaternion RepresentativeRotation { get; set; }
public Vector3 RepresentativeXAxis { get; set; }
diff --git a/src/Utils/CoordinateSystem/PathTargetFrame.cs b/src/Utils/CoordinateSystem/PathTargetFrame.cs
new file mode 100644
index 0000000..832a6cf
--- /dev/null
+++ b/src/Utils/CoordinateSystem/PathTargetFrame.cs
@@ -0,0 +1,33 @@
+using System;
+using System.Numerics;
+
+namespace NavisworksTransport.Utils.CoordinateSystem
+{
+ ///
+ /// 当前宿主语义下的路径目标框架。
+ /// Forward 表示路径前进方向,Up 表示路径语义上的“向上/法向”,Side 由二者叉乘得到。
+ ///
+ public sealed class PathTargetFrame
+ {
+ public PathTargetFrame(Vector3 forward, Vector3 up)
+ {
+ if (forward.LengthSquared() < 1e-12f)
+ {
+ throw new ArgumentException("forward 不能为空。", nameof(forward));
+ }
+
+ if (up.LengthSquared() < 1e-12f)
+ {
+ throw new ArgumentException("up 不能为空。", nameof(up));
+ }
+
+ Forward = Vector3.Normalize(forward);
+ Up = Vector3.Normalize(up);
+ Side = Vector3.Normalize(Vector3.Cross(Forward, Up));
+ }
+
+ public Vector3 Forward { get; }
+ public Vector3 Up { get; }
+ public Vector3 Side { get; }
+ }
+}
diff --git a/src/Utils/CoordinateSystem/PathTargetFrameResolver.cs b/src/Utils/CoordinateSystem/PathTargetFrameResolver.cs
new file mode 100644
index 0000000..b93283c
--- /dev/null
+++ b/src/Utils/CoordinateSystem/PathTargetFrameResolver.cs
@@ -0,0 +1,49 @@
+using System.Numerics;
+using Autodesk.Navisworks.Api;
+
+namespace NavisworksTransport.Utils.CoordinateSystem
+{
+ ///
+ /// 统一的路径目标框架解析入口。
+ /// 对象姿态解释层统一后,Ground/Hoisting/Rail 只在这里区分目标框架。
+ ///
+ public static class PathTargetFrameResolver
+ {
+ public static bool TryCreatePlanarHostFrame(
+ Vector3 hostForward,
+ CoordinateSystemType hostType,
+ out PathTargetFrame frame)
+ {
+ frame = null;
+
+ if (hostForward.LengthSquared() < 1e-9f)
+ {
+ return false;
+ }
+
+ Vector3 hostUp = hostType == CoordinateSystemType.YUp
+ ? Vector3.UnitY
+ : Vector3.UnitZ;
+ frame = new PathTargetFrame(hostForward, hostUp);
+ return true;
+ }
+
+ public static bool TryCreateRailHostFrame(
+ PathRoute route,
+ Point3D previousPoint,
+ Point3D currentPoint,
+ Point3D nextPoint,
+ out PathTargetFrame frame)
+ {
+ frame = null;
+
+ if (!RailPathPoseHelper.TryCreateHostLocalFrame(route, previousPoint, currentPoint, nextPoint, out var localFrame))
+ {
+ return false;
+ }
+
+ frame = new PathTargetFrame(localFrame.Forward, localFrame.Normal);
+ return true;
+ }
+ }
+}
diff --git a/src/Utils/CoordinateSystem/RealObjectPlanarPoseSolver.cs b/src/Utils/CoordinateSystem/RealObjectPlanarPoseSolver.cs
index 10ee5a9..55a7e57 100644
--- a/src/Utils/CoordinateSystem/RealObjectPlanarPoseSolver.cs
+++ b/src/Utils/CoordinateSystem/RealObjectPlanarPoseSolver.cs
@@ -21,6 +21,21 @@ namespace NavisworksTransport.Utils.CoordinateSystem
Vector3 desiredForward,
Vector3 desiredUp,
out RealObjectPlanarPoseSolution solution)
+ {
+ return TryCreatePlanarPoseFromReferencePose(
+ referenceRotation,
+ desiredForward,
+ desiredUp,
+ null,
+ out solution);
+ }
+
+ public static bool TryCreatePlanarPoseFromReferencePose(
+ Quaternion referenceRotation,
+ Vector3 desiredForward,
+ Vector3 desiredUp,
+ LocalAxisDirection? fixedReferenceAxis,
+ out RealObjectPlanarPoseSolution solution)
{
solution = null;
@@ -39,9 +54,11 @@ namespace NavisworksTransport.Utils.CoordinateSystem
return false;
}
- CandidateAxis bestCandidate = SelectBestCandidateAxis(
- normalizedReferenceRotation,
- normalizedForward);
+ CandidateAxis bestCandidate = fixedReferenceAxis.HasValue
+ ? CreateCandidateAxisFromFixedDirection(fixedReferenceAxis.Value, normalizedReferenceRotation)
+ : SelectBestCandidateAxis(
+ normalizedReferenceRotation,
+ normalizedForward);
if (bestCandidate.IsInvalid)
{
@@ -57,6 +74,7 @@ namespace NavisworksTransport.Utils.CoordinateSystem
solution = new RealObjectPlanarPoseSolution(
normalizedForward,
normalizedUp,
+ bestCandidate.LocalAxisDirection,
bestCandidate.LocalAxis,
bestCandidate.WorldAxis,
rotationDelta,
@@ -76,6 +94,7 @@ namespace NavisworksTransport.Utils.CoordinateSystem
if (bestCandidate.IsInvalid || score > bestCandidate.Score)
{
bestCandidate = new CandidateAxis(
+ candidate.LocalAxisDirection,
candidate.LocalAxis,
candidate.WorldAxis,
score);
@@ -85,25 +104,56 @@ namespace NavisworksTransport.Utils.CoordinateSystem
return bestCandidate;
}
+ private static CandidateAxis CreateCandidateAxisFromFixedDirection(
+ LocalAxisDirection direction,
+ Quaternion referenceRotation)
+ {
+ Vector3 localAxis = GetAxisVector(direction);
+ Vector3 worldAxis = Vector3.Normalize(Vector3.Transform(localAxis, referenceRotation));
+ return new CandidateAxis(direction, localAxis, worldAxis, 0f);
+ }
+
private static CandidateAxis[] EnumerateCandidateAxes(Quaternion referenceRotation)
{
return new[]
{
- CreateCandidateAxis(Vector3.UnitX, referenceRotation),
- CreateCandidateAxis(-Vector3.UnitX, referenceRotation),
- CreateCandidateAxis(Vector3.UnitY, referenceRotation),
- CreateCandidateAxis(-Vector3.UnitY, referenceRotation),
- CreateCandidateAxis(Vector3.UnitZ, referenceRotation),
- CreateCandidateAxis(-Vector3.UnitZ, referenceRotation)
+ CreateCandidateAxis(LocalAxisDirection.PositiveX, Vector3.UnitX, referenceRotation),
+ CreateCandidateAxis(LocalAxisDirection.NegativeX, -Vector3.UnitX, referenceRotation),
+ CreateCandidateAxis(LocalAxisDirection.PositiveY, Vector3.UnitY, referenceRotation),
+ CreateCandidateAxis(LocalAxisDirection.NegativeY, -Vector3.UnitY, referenceRotation),
+ CreateCandidateAxis(LocalAxisDirection.PositiveZ, Vector3.UnitZ, referenceRotation),
+ CreateCandidateAxis(LocalAxisDirection.NegativeZ, -Vector3.UnitZ, referenceRotation)
};
}
private static CandidateAxis CreateCandidateAxis(
+ LocalAxisDirection direction,
Vector3 localAxis,
Quaternion referenceRotation)
{
Vector3 worldAxis = Vector3.Normalize(Vector3.Transform(localAxis, referenceRotation));
- return new CandidateAxis(localAxis, worldAxis, float.MinValue);
+ return new CandidateAxis(direction, localAxis, worldAxis, float.MinValue);
+ }
+
+ private static Vector3 GetAxisVector(LocalAxisDirection direction)
+ {
+ switch (direction)
+ {
+ case LocalAxisDirection.PositiveX:
+ return Vector3.UnitX;
+ case LocalAxisDirection.NegativeX:
+ return -Vector3.UnitX;
+ case LocalAxisDirection.PositiveY:
+ return Vector3.UnitY;
+ case LocalAxisDirection.NegativeY:
+ return -Vector3.UnitY;
+ case LocalAxisDirection.PositiveZ:
+ return Vector3.UnitZ;
+ case LocalAxisDirection.NegativeZ:
+ return -Vector3.UnitZ;
+ default:
+ throw new ArgumentOutOfRangeException(nameof(direction), direction, null);
+ }
}
private static Quaternion CreateShortestArcQuaternion(
@@ -187,16 +237,18 @@ namespace NavisworksTransport.Utils.CoordinateSystem
private struct CandidateAxis
{
- public static CandidateAxis Invalid => new CandidateAxis(Vector3.Zero, Vector3.Zero, float.MinValue);
+ public static CandidateAxis Invalid => new CandidateAxis(LocalAxisDirection.PositiveX, Vector3.Zero, Vector3.Zero, float.MinValue);
+ public LocalAxisDirection LocalAxisDirection { get; }
public Vector3 LocalAxis { get; }
public Vector3 WorldAxis { get; }
public float Score { get; }
public bool IsInvalid => Score == float.MinValue;
- public CandidateAxis(Vector3 localAxis, Vector3 worldAxis, float score)
+ public CandidateAxis(LocalAxisDirection localAxisDirection, Vector3 localAxis, Vector3 worldAxis, float score)
{
+ LocalAxisDirection = localAxisDirection;
LocalAxis = localAxis;
WorldAxis = worldAxis;
Score = score;
@@ -211,6 +263,7 @@ namespace NavisworksTransport.Utils.CoordinateSystem
{
public Vector3 ProjectedForward { get; }
public Vector3 DesiredUp { get; }
+ public LocalAxisDirection SelectedReferenceAxisDirection { get; }
public Vector3 SelectedReferenceAxisLocal { get; }
public Vector3 SelectedReferenceAxisWorld { get; }
public Quaternion RotationDelta { get; }
@@ -219,6 +272,7 @@ namespace NavisworksTransport.Utils.CoordinateSystem
public RealObjectPlanarPoseSolution(
Vector3 projectedForward,
Vector3 desiredUp,
+ LocalAxisDirection selectedReferenceAxisDirection,
Vector3 selectedReferenceAxisLocal,
Vector3 selectedReferenceAxisWorld,
Quaternion rotationDelta,
@@ -226,6 +280,7 @@ namespace NavisworksTransport.Utils.CoordinateSystem
{
ProjectedForward = projectedForward;
DesiredUp = desiredUp;
+ SelectedReferenceAxisDirection = selectedReferenceAxisDirection;
SelectedReferenceAxisLocal = selectedReferenceAxisLocal;
SelectedReferenceAxisWorld = selectedReferenceAxisWorld;
RotationDelta = rotationDelta;
diff --git a/src/Utils/CoordinateSystem/RealObjectProjectedExtentResolver.cs b/src/Utils/CoordinateSystem/RealObjectProjectedExtentResolver.cs
new file mode 100644
index 0000000..1805346
--- /dev/null
+++ b/src/Utils/CoordinateSystem/RealObjectProjectedExtentResolver.cs
@@ -0,0 +1,54 @@
+using System;
+using System.Numerics;
+
+namespace NavisworksTransport.Utils.CoordinateSystem
+{
+ ///
+ /// 根据“最终姿态”计算真实物体在目标框架 forward/side/up 三个语义方向上的投影尺寸。
+ /// 该层不区分 Ground / Hoisting / Rail;路径差异只体现在 targetForward / targetUp 的来源。
+ ///
+ public static class RealObjectProjectedExtentResolver
+ {
+ public static (double forwardExtent, double sideExtent, double upExtent) CalculateProjectedSemanticExtents(
+ ModelAxisConvention convention,
+ double forwardSize,
+ double sideSize,
+ double upSize,
+ Quaternion baselineHostQuaternion,
+ Quaternion finalHostQuaternion,
+ Vector3 targetForward,
+ Vector3 targetUp)
+ {
+ if (convention == null)
+ {
+ throw new ArgumentNullException(nameof(convention));
+ }
+
+ Vector3 localSize = convention.CreateScaleVector3(forwardSize, sideSize, upSize);
+ Vector3 rotatedLocalX = Vector3.Normalize(Vector3.Transform(Vector3.UnitX, finalHostQuaternion));
+ Vector3 rotatedLocalY = Vector3.Normalize(Vector3.Transform(Vector3.UnitY, finalHostQuaternion));
+ Vector3 rotatedLocalZ = Vector3.Normalize(Vector3.Transform(Vector3.UnitZ, finalHostQuaternion));
+
+ Vector3 normalizedTargetForward = Vector3.Normalize(targetForward);
+ Vector3 normalizedTargetUp = Vector3.Normalize(targetUp);
+ Vector3 normalizedTargetSide = Vector3.Normalize(Vector3.Cross(normalizedTargetForward, normalizedTargetUp));
+
+ return (
+ ProjectExtent(localSize, rotatedLocalX, rotatedLocalY, rotatedLocalZ, normalizedTargetForward),
+ ProjectExtent(localSize, rotatedLocalX, rotatedLocalY, rotatedLocalZ, normalizedTargetSide),
+ ProjectExtent(localSize, rotatedLocalX, rotatedLocalY, rotatedLocalZ, normalizedTargetUp));
+ }
+
+ private static double ProjectExtent(
+ Vector3 localSize,
+ Vector3 rotatedLocalX,
+ Vector3 rotatedLocalY,
+ Vector3 rotatedLocalZ,
+ Vector3 targetAxis)
+ {
+ return Math.Abs(Vector3.Dot(rotatedLocalX, targetAxis)) * localSize.X +
+ Math.Abs(Vector3.Dot(rotatedLocalY, targetAxis)) * localSize.Y +
+ Math.Abs(Vector3.Dot(rotatedLocalZ, targetAxis)) * localSize.Z;
+ }
+ }
+}
diff --git a/src/Utils/CoordinateSystem/RealObjectRailExtentResolver.cs b/src/Utils/CoordinateSystem/RealObjectRailExtentResolver.cs
index 2406a20..0a3767a 100644
--- a/src/Utils/CoordinateSystem/RealObjectRailExtentResolver.cs
+++ b/src/Utils/CoordinateSystem/RealObjectRailExtentResolver.cs
@@ -46,32 +46,18 @@ namespace NavisworksTransport.Utils.CoordinateSystem
return false;
}
- Vector3 localSize = convention.CreateScaleVector3(forwardSize, sideSize, upSize);
- Vector3 rotatedLocalX = Vector3.Normalize(Vector3.Transform(Vector3.UnitX, finalHostQuaternion));
- Vector3 rotatedLocalY = Vector3.Normalize(Vector3.Transform(Vector3.UnitY, finalHostQuaternion));
- Vector3 rotatedLocalZ = Vector3.Normalize(Vector3.Transform(Vector3.UnitZ, finalHostQuaternion));
-
Vector3 targetForward = Vector3.Normalize(Vector3.Transform(convention.ForwardUnitVector, baselineHostQuaternion));
Vector3 targetUp = Vector3.Normalize(Vector3.Transform(convention.UpUnitVector, baselineHostQuaternion));
- Vector3 targetSide = Vector3.Normalize(Vector3.Cross(targetForward, targetUp));
-
- extents = (
- ProjectExtent(localSize, rotatedLocalX, rotatedLocalY, rotatedLocalZ, targetForward),
- ProjectExtent(localSize, rotatedLocalX, rotatedLocalY, rotatedLocalZ, targetSide),
- ProjectExtent(localSize, rotatedLocalX, rotatedLocalY, rotatedLocalZ, targetUp));
+ extents = RealObjectProjectedExtentResolver.CalculateProjectedSemanticExtents(
+ convention,
+ forwardSize,
+ sideSize,
+ upSize,
+ baselineHostQuaternion,
+ finalHostQuaternion,
+ targetForward,
+ targetUp);
return true;
}
-
- private static double ProjectExtent(
- Vector3 localSize,
- Vector3 rotatedLocalX,
- Vector3 rotatedLocalY,
- Vector3 rotatedLocalZ,
- Vector3 targetAxis)
- {
- return Math.Abs(Vector3.Dot(rotatedLocalX, targetAxis)) * localSize.X +
- Math.Abs(Vector3.Dot(rotatedLocalY, targetAxis)) * localSize.Y +
- Math.Abs(Vector3.Dot(rotatedLocalZ, targetAxis)) * localSize.Z;
- }
}
}
diff --git a/src/Utils/CoordinateSystem/RealObjectReferencePose.cs b/src/Utils/CoordinateSystem/RealObjectReferencePose.cs
new file mode 100644
index 0000000..75c9346
--- /dev/null
+++ b/src/Utils/CoordinateSystem/RealObjectReferencePose.cs
@@ -0,0 +1,37 @@
+using System.Numerics;
+
+namespace NavisworksTransport.Utils.CoordinateSystem
+{
+ ///
+ /// 真实物体在当前宿主语义下解释后的参考姿态。
+ /// 这不是 fragment 原始几何框架,而是已经结合 Fragment默认Up 解释后的结果。
+ ///
+ public sealed class RealObjectReferencePose
+ {
+ public RealObjectReferencePose(
+ Quaternion rotation,
+ Vector3 axisX,
+ Vector3 axisY,
+ Vector3 axisZ,
+ int fragmentCount,
+ string fragmentDefaultUpAxis,
+ string hostUpAxis)
+ {
+ Rotation = rotation;
+ AxisX = axisX;
+ AxisY = axisY;
+ AxisZ = axisZ;
+ FragmentCount = fragmentCount;
+ FragmentDefaultUpAxis = fragmentDefaultUpAxis;
+ HostUpAxis = hostUpAxis;
+ }
+
+ public Quaternion Rotation { get; }
+ public Vector3 AxisX { get; }
+ public Vector3 AxisY { get; }
+ public Vector3 AxisZ { get; }
+ public int FragmentCount { get; }
+ public string FragmentDefaultUpAxis { get; }
+ public string HostUpAxis { get; }
+ }
+}
diff --git a/src/Utils/CoordinateSystem/RealObjectReferencePoseResolver.cs b/src/Utils/CoordinateSystem/RealObjectReferencePoseResolver.cs
new file mode 100644
index 0000000..01fea03
--- /dev/null
+++ b/src/Utils/CoordinateSystem/RealObjectReferencePoseResolver.cs
@@ -0,0 +1,162 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Numerics;
+using System.Runtime.InteropServices;
+using Autodesk.Navisworks.Api;
+using ComApiBridge = Autodesk.Navisworks.Api.ComApi.ComApiBridge;
+
+namespace NavisworksTransport.Utils.CoordinateSystem
+{
+ ///
+ /// 统一的真实物体参考姿态解析入口。
+ /// 负责:
+ /// 1. 从 fragment 提取代表姿态
+ /// 2. 按 Fragment默认Up 解释成当前宿主语义下的真实姿态
+ /// 3. 为 Ground / Hoisting / Rail 提供统一参考姿态来源
+ ///
+ public static class RealObjectReferencePoseResolver
+ {
+ public static bool TryResolveFromModelItem(
+ ModelItem sourceObject,
+ Document doc,
+ out RealObjectReferencePose referencePose)
+ {
+ referencePose = null;
+
+ if (sourceObject == null)
+ {
+ return false;
+ }
+
+ try
+ {
+ var selection = ComApiBridge.ToInwOpSelection(new ModelItemCollection { sourceObject });
+ var fragmentInfos = GeometryHelper.GetAllFragments(selection);
+ try
+ {
+ var fragmentMatrices = fragmentInfos
+ .Where(fragmentInfo => fragmentInfo.TransformMatrix != null && fragmentInfo.TransformMatrix.Length == 16)
+ .Select(fragmentInfo => fragmentInfo.TransformMatrix)
+ .ToList();
+
+ return TryResolveFromFragmentMatrices(fragmentMatrices, doc, out referencePose);
+ }
+ finally
+ {
+ if (fragmentInfos != null)
+ {
+ foreach (var fragmentInfo in fragmentInfos)
+ {
+ if (fragmentInfo?.Fragment != null)
+ {
+ try
+ {
+ Marshal.ReleaseComObject(fragmentInfo.Fragment);
+ }
+ catch
+ {
+ }
+ }
+ }
+ }
+
+ if (selection != null)
+ {
+ try
+ {
+ Marshal.ReleaseComObject(selection);
+ }
+ catch
+ {
+ }
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ LogManager.Warning($"[真实物体参考姿态] 从 fragment 提取代表姿态失败: {ex.Message}");
+ return false;
+ }
+ }
+
+ public static bool TryResolveFromFragmentMatrices(
+ IReadOnlyCollection fragmentMatrices,
+ Document doc,
+ out RealObjectReferencePose referencePose)
+ {
+ string fragmentDefaultUpAxis = FragmentDefaultUpContext.GetCurrentDocumentFragmentDefaultUpAxis(doc);
+ string hostUpAxis = FragmentDefaultUpContext.GetCurrentHostUpAxis(doc);
+ return TryResolveFromFragmentMatrices(fragmentMatrices, fragmentDefaultUpAxis, hostUpAxis, out referencePose);
+ }
+
+ public static bool TryResolveFromFragmentMatrices(
+ IReadOnlyCollection fragmentMatrices,
+ string fragmentDefaultUpAxis,
+ string hostUpAxis,
+ out RealObjectReferencePose referencePose)
+ {
+ referencePose = null;
+
+ if (fragmentMatrices == null || fragmentMatrices.Count == 0)
+ {
+ return false;
+ }
+
+ if (!FragmentRepresentativePoseHelper.TryGetRepresentativeFrame(fragmentMatrices, out var rawFrame))
+ {
+ return false;
+ }
+
+ if (!FragmentRepresentativePoseHelper.TryInterpretRepresentativeFrame(
+ rawFrame,
+ fragmentDefaultUpAxis,
+ hostUpAxis,
+ out var interpretedFrame))
+ {
+ return false;
+ }
+
+ referencePose = new RealObjectReferencePose(
+ interpretedFrame.Rotation,
+ interpretedFrame.AxisX,
+ interpretedFrame.AxisY,
+ interpretedFrame.AxisZ,
+ fragmentMatrices.Count,
+ fragmentDefaultUpAxis,
+ hostUpAxis);
+ return true;
+ }
+
+ public static bool TryDetectDefaultUpAxis(
+ IReadOnlyCollection fragmentMatrices,
+ string hostUpAxis,
+ out string detectedAxis,
+ out float yAlignment,
+ out float zAlignment)
+ {
+ detectedAxis = "Y";
+ yAlignment = 0.0f;
+ zAlignment = 0.0f;
+
+ if (fragmentMatrices == null || fragmentMatrices.Count == 0)
+ {
+ return false;
+ }
+
+ if (!FragmentRepresentativePoseHelper.TryGetRepresentativeFrame(fragmentMatrices, out var rawFrame))
+ {
+ return false;
+ }
+
+ Vector3 hostUpVector = string.Equals(hostUpAxis, "Y", StringComparison.OrdinalIgnoreCase)
+ ? Vector3.UnitY
+ : Vector3.UnitZ;
+
+ yAlignment = Math.Abs(Vector3.Dot(Vector3.Normalize(rawFrame.AxisY), hostUpVector));
+ zAlignment = Math.Abs(Vector3.Dot(Vector3.Normalize(rawFrame.AxisZ), hostUpVector));
+ detectedAxis = zAlignment > yAlignment ? "Z" : "Y";
+ return true;
+ }
+ }
+}
diff --git a/src/Utils/RailPathPoseHelper.cs b/src/Utils/RailPathPoseHelper.cs
index f6a39ca..426a7e8 100644
--- a/src/Utils/RailPathPoseHelper.cs
+++ b/src/Utils/RailPathPoseHelper.cs
@@ -591,6 +591,32 @@ namespace NavisworksTransport.Utils
out frame);
}
+ ///
+ /// 获取 Rail 路径在宿主坐标系中的目标框架。
+ /// 统一供真实物体姿态解释层之后的路径目标框架消费。
+ ///
+ public static bool TryCreateHostLocalFrame(
+ PathRoute route,
+ Point3D previousPoint,
+ Point3D currentPoint,
+ Point3D nextPoint,
+ out RailLocalFrame hostFrame)
+ {
+ hostFrame = null;
+
+ if (!TryCreateCanonicalLocalFrame(route, previousPoint, currentPoint, nextPoint, out var canonicalFrame))
+ {
+ return false;
+ }
+
+ var adapter = CoordinateSystemManager.Instance.CreateHostAdapter();
+ hostFrame = new RailLocalFrame(
+ adapter.FromCanonicalVector3(canonicalFrame.Forward),
+ adapter.FromCanonicalVector3(canonicalFrame.Lateral),
+ adapter.FromCanonicalVector3(canonicalFrame.Normal));
+ return true;
+ }
+
private static Vector3? ResolvePreferredNormal(PathRoute route)
{
if (route?.RailPreferredNormal == null)
From aabf263f0da5dfcd40450ebd419eaaa19fb78e49 Mon Sep 17 00:00:00 2001
From: tian <11429339@qq.com>
Date: Wed, 25 Mar 2026 11:18:03 +0800
Subject: [PATCH 29/87] Fix host rotation adapter split for real and virtual
objects
---
NavisworksTransport.UnitTests.csproj | 2 +
Properties/AssemblyInfo.cs | 3 +-
TransportPlugin.csproj | 5 +
.../FragmentDefaultUpContextTests.cs | 47 +++++
.../VirtualGroundPoseCharacterizationTests.cs | 120 +++++++++++
.../VirtualObjectModelTransformTests.cs | 100 +++++++++
resources/unit_cube_yup.nwc | Bin 0 -> 1806 bytes
resources/unit_cube_yup.obj | 26 +++
src/Core/Animation/PathAnimationManager.cs | 190 ++++++++++++------
src/Core/VirtualObjectManager.cs | 22 +-
.../FragmentDefaultUpContext.cs | 43 +++-
.../CoordinateSystem/HostCoordinateAdapter.cs | 76 +++++++
src/Utils/ModelItemTransformHelper.cs | 148 +++++++++++++-
13 files changed, 693 insertions(+), 89 deletions(-)
create mode 100644 UnitTests/CoordinateSystem/FragmentDefaultUpContextTests.cs
create mode 100644 UnitTests/CoordinateSystem/VirtualGroundPoseCharacterizationTests.cs
create mode 100644 UnitTests/CoordinateSystem/VirtualObjectModelTransformTests.cs
create mode 100644 resources/unit_cube_yup.nwc
create mode 100644 resources/unit_cube_yup.obj
diff --git a/NavisworksTransport.UnitTests.csproj b/NavisworksTransport.UnitTests.csproj
index b109f54..17555ba 100644
--- a/NavisworksTransport.UnitTests.csproj
+++ b/NavisworksTransport.UnitTests.csproj
@@ -72,6 +72,8 @@
+
+
diff --git a/Properties/AssemblyInfo.cs b/Properties/AssemblyInfo.cs
index 1020f6e..0edda3e 100644
--- a/Properties/AssemblyInfo.cs
+++ b/Properties/AssemblyInfo.cs
@@ -31,4 +31,5 @@ using System.Runtime.InteropServices;
//
// 主版本和次版本手动维护,Build和Revision在编译时自动更新
[assembly: AssemblyVersion("1.0.0.0")]
-[assembly: AssemblyFileVersion("1.0.0.0")]
\ No newline at end of file
+[assembly: AssemblyFileVersion("1.0.0.0")]
+[assembly: InternalsVisibleTo("NavisworksTransport.UnitTests")]
diff --git a/TransportPlugin.csproj b/TransportPlugin.csproj
index f126d00..63a7404 100644
--- a/TransportPlugin.csproj
+++ b/TransportPlugin.csproj
@@ -522,6 +522,11 @@
PreserveNewest
resources\unit_cube.nwc
+
+
+ PreserveNewest
+ resources\unit_cube_yup.nwc
+
PreserveNewest
diff --git a/UnitTests/CoordinateSystem/FragmentDefaultUpContextTests.cs b/UnitTests/CoordinateSystem/FragmentDefaultUpContextTests.cs
new file mode 100644
index 0000000..17b135b
--- /dev/null
+++ b/UnitTests/CoordinateSystem/FragmentDefaultUpContextTests.cs
@@ -0,0 +1,47 @@
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using NavisworksTransport.Utils.CoordinateSystem;
+
+namespace NavisworksTransport.UnitTests.CoordinateSystem
+{
+ [TestClass]
+ public class FragmentDefaultUpContextTests
+ {
+ [TestInitialize]
+ public void SetUp()
+ {
+ FragmentDefaultUpContext.ResetForTests();
+ }
+
+ [TestMethod]
+ public void GetOrCreateDocumentKey_ReusesFirstStableKey_WhenDocumentMetadataTemporarilyChanges()
+ {
+ const int runtimeDocumentId = 42;
+
+ string initialKey = FragmentDefaultUpContext.GetOrCreateDocumentKey(
+ runtimeDocumentId,
+ @"C:\models\floor2.nwd",
+ "Floor2");
+
+ string transientKey = FragmentDefaultUpContext.GetOrCreateDocumentKey(
+ runtimeDocumentId,
+ string.Empty,
+ "无标题");
+
+ Assert.AreEqual(initialKey, transientKey);
+ Assert.AreEqual("file:C:\\models\\floor2.nwd", transientKey);
+ }
+
+ [TestMethod]
+ public void GetOrCreateDocumentKey_UsesRuntimeScopedFallback_WhenFileNameIsUnavailable()
+ {
+ const int runtimeDocumentId = 7;
+
+ string key = FragmentDefaultUpContext.GetOrCreateDocumentKey(
+ runtimeDocumentId,
+ string.Empty,
+ "无标题");
+
+ Assert.AreEqual("runtime:7|title:无标题", key);
+ }
+ }
+}
diff --git a/UnitTests/CoordinateSystem/VirtualGroundPoseCharacterizationTests.cs b/UnitTests/CoordinateSystem/VirtualGroundPoseCharacterizationTests.cs
new file mode 100644
index 0000000..f94d79a
--- /dev/null
+++ b/UnitTests/CoordinateSystem/VirtualGroundPoseCharacterizationTests.cs
@@ -0,0 +1,120 @@
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using NavisworksTransport.Utils.CoordinateSystem;
+using System;
+using System.Numerics;
+using Autodesk.Navisworks.Api;
+
+namespace NavisworksTransport.UnitTests.CoordinateSystem
+{
+ [TestClass]
+ public class VirtualGroundPoseCharacterizationTests
+ {
+ [TestMethod]
+ public void YUp_GroundVirtualPose_WithYUpVirtualAsset_ShouldUseHostYAsUp()
+ {
+ var adapter = new HostCoordinateAdapter(CoordinateSystemType.YUp);
+ var convention = ModelAxisConvention.CreateDefaultForHost(CoordinateSystemType.YUp);
+
+ Vector3 hostForward = Vector3.Normalize(new Vector3(-0.8987f, 0.0f, 0.4386f));
+ Vector3 canonicalForward = adapter.ToCanonicalVector3(hostForward);
+
+ bool ok = CanonicalPlanarPoseBuilder.TryCreateQuaternionFromForward(
+ canonicalForward,
+ HostCoordinateAdapter.CanonicalUpVector3,
+ convention,
+ out Quaternion canonicalRotation);
+
+ Assert.IsTrue(ok);
+
+ Matrix4x4 hostLinear = adapter.FromCanonicalLinearTransform(
+ Matrix4x4.CreateFromQuaternion(canonicalRotation));
+
+ AssertColumn(hostLinear, 0, -0.8987, 0.0000, 0.4386);
+ AssertColumn(hostLinear, 1, 0.0000, 1.0000, 0.0000);
+ AssertColumn(hostLinear, 2, -0.4386, 0.0000, -0.8987);
+ }
+
+ [TestMethod]
+ public void YUp_GroundVirtualPose_WithYUpVirtualAsset_ShouldMatchHostYUpDefaultPlanarConvention()
+ {
+ var adapter = new HostCoordinateAdapter(CoordinateSystemType.YUp);
+ Vector3 hostForward = Vector3.Normalize(new Vector3(-0.8987f, 0.0f, 0.4386f));
+ Vector3 canonicalForward = adapter.ToCanonicalVector3(hostForward);
+
+ Assert.IsTrue(CanonicalPlanarPoseBuilder.TryCreateQuaternionFromForward(
+ canonicalForward,
+ HostCoordinateAdapter.CanonicalUpVector3,
+ ModelAxisConvention.CreateDefaultForHost(CoordinateSystemType.YUp),
+ out Quaternion assetRotation));
+
+ Assert.IsTrue(CanonicalPlanarPoseBuilder.TryCreateQuaternionFromForward(
+ canonicalForward,
+ HostCoordinateAdapter.CanonicalUpVector3,
+ ModelAxisConvention.CreateDefaultForHost(CoordinateSystemType.YUp),
+ out Quaternion hostDefaultRotation));
+
+ Matrix4x4 assetHostLinear = adapter.FromCanonicalLinearTransform(
+ Matrix4x4.CreateFromQuaternion(assetRotation));
+ Matrix4x4 hostDefaultLinear = adapter.FromCanonicalLinearTransform(
+ Matrix4x4.CreateFromQuaternion(hostDefaultRotation));
+
+ Assert.AreEqual(1.0, assetHostLinear.M22, 1e-3, "YUp 虚拟物体资源应把 local Y 对到宿主 up。");
+ Assert.AreEqual(hostDefaultLinear.M11, assetHostLinear.M11, 1e-3);
+ Assert.AreEqual(hostDefaultLinear.M21, assetHostLinear.M21, 1e-3);
+ Assert.AreEqual(hostDefaultLinear.M31, assetHostLinear.M31, 1e-3);
+ Assert.AreEqual(hostDefaultLinear.M12, assetHostLinear.M12, 1e-3);
+ Assert.AreEqual(hostDefaultLinear.M22, assetHostLinear.M22, 1e-3);
+ Assert.AreEqual(hostDefaultLinear.M32, assetHostLinear.M32, 1e-3);
+ Assert.AreEqual(hostDefaultLinear.M13, assetHostLinear.M13, 1e-3);
+ Assert.AreEqual(hostDefaultLinear.M23, assetHostLinear.M23, 1e-3);
+ Assert.AreEqual(hostDefaultLinear.M33, assetHostLinear.M33, 1e-3);
+ }
+
+ [TestMethod]
+ public void VirtualObjectReferencePose_ShouldMatchSelectedVirtualAssetConvention()
+ {
+ var yUpAdapter = new HostCoordinateAdapter(CoordinateSystemType.YUp);
+ var zUpAdapter = new HostCoordinateAdapter(CoordinateSystemType.ZUp);
+ var yUpConvention = ModelAxisConvention.CreateDefaultForHost(CoordinateSystemType.YUp);
+ var zUpConvention = ModelAxisConvention.CreateVirtualObjectAssetConvention();
+
+ Matrix4x4 yUpLinear = Matrix4x4.CreateFromQuaternion(
+ yUpConvention.CreateQuaternion(new Vector3(1, 0, 0), yUpAdapter.HostUpVector3));
+ Matrix4x4 zUpLinear = Matrix4x4.CreateFromQuaternion(
+ zUpConvention.CreateQuaternion(new Vector3(1, 0, 0), zUpAdapter.HostUpVector3));
+
+ AssertColumn(yUpLinear, 0, 1.0, 0.0, 0.0);
+ AssertColumn(yUpLinear, 1, 0.0, 1.0, 0.0);
+ AssertColumn(yUpLinear, 2, 0.0, 0.0, 1.0);
+
+ AssertColumn(zUpLinear, 0, 1.0, 0.0, 0.0);
+ AssertColumn(zUpLinear, 1, 0.0, 1.0, 0.0);
+ AssertColumn(zUpLinear, 2, 0.0, 0.0, 1.0);
+ }
+
+ private static void AssertColumn(Matrix4x4 matrix, int column, double x, double y, double z)
+ {
+ switch (column)
+ {
+ case 0:
+ Assert.AreEqual(x, matrix.M11, 1e-3);
+ Assert.AreEqual(y, matrix.M21, 1e-3);
+ Assert.AreEqual(z, matrix.M31, 1e-3);
+ break;
+ case 1:
+ Assert.AreEqual(x, matrix.M12, 1e-3);
+ Assert.AreEqual(y, matrix.M22, 1e-3);
+ Assert.AreEqual(z, matrix.M32, 1e-3);
+ break;
+ case 2:
+ Assert.AreEqual(x, matrix.M13, 1e-3);
+ Assert.AreEqual(y, matrix.M23, 1e-3);
+ Assert.AreEqual(z, matrix.M33, 1e-3);
+ break;
+ default:
+ Assert.Fail("Only first 3 columns are valid.");
+ break;
+ }
+ }
+ }
+}
diff --git a/UnitTests/CoordinateSystem/VirtualObjectModelTransformTests.cs b/UnitTests/CoordinateSystem/VirtualObjectModelTransformTests.cs
new file mode 100644
index 0000000..e571818
--- /dev/null
+++ b/UnitTests/CoordinateSystem/VirtualObjectModelTransformTests.cs
@@ -0,0 +1,100 @@
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using NavisworksTransport.Core;
+
+namespace NavisworksTransport.UnitTests.CoordinateSystem
+{
+ [TestClass]
+ public class VirtualObjectModelTransformTests
+ {
+ [TestMethod]
+ public void BuildModelTransformPreservingScale_ShouldKeepExistingScaleAndReplaceRotationTranslation()
+ {
+ const double scaleX = 4.9213;
+ const double scaleY = 3.2808;
+ const double scaleZ = 6.5617;
+ var targetRotation = new QuaternionData(0.0, 0.7071068, 0.0, 0.7071068);
+
+ VirtualObjectManager.ModelTransformComponents result = VirtualObjectManager.CreateModelTransformPreservingScale(
+ scaleX,
+ scaleY,
+ scaleZ,
+ -177.129,
+ 18.114,
+ -2.793,
+ targetRotation.ToRotation3D());
+
+ Assert.AreEqual(scaleX, result.ScaleX, 1e-4);
+ Assert.AreEqual(scaleY, result.ScaleY, 1e-4);
+ Assert.AreEqual(scaleZ, result.ScaleZ, 1e-4);
+ AssertRotationEquivalent(targetRotation, result.Rotation);
+ Assert.AreEqual(-177.129, result.TranslationX, 1e-6);
+ Assert.AreEqual(18.114, result.TranslationY, 1e-6);
+ Assert.AreEqual(-2.793, result.TranslationZ, 1e-6);
+ }
+
+ [TestMethod]
+ public void BuildModelTransformPreservingScale_ResetPose_ShouldOnlyClearRotationAndTranslation()
+ {
+ VirtualObjectManager.ModelTransformComponents result = VirtualObjectManager.CreateModelTransformPreservingScale(
+ 12.0,
+ 5.0,
+ 7.0,
+ 0.0,
+ 0.0,
+ 0.0,
+ QuaternionData.Identity.ToRotation3D());
+
+ Assert.AreEqual(12.0, result.ScaleX, 1e-6);
+ Assert.AreEqual(5.0, result.ScaleY, 1e-6);
+ Assert.AreEqual(7.0, result.ScaleZ, 1e-6);
+ AssertRotationEquivalent(QuaternionData.Identity, result.Rotation);
+ Assert.AreEqual(0.0, result.TranslationX, 1e-6);
+ Assert.AreEqual(0.0, result.TranslationY, 1e-6);
+ Assert.AreEqual(0.0, result.TranslationZ, 1e-6);
+ }
+
+ private static void AssertRotationEquivalent(QuaternionData expected, Autodesk.Navisworks.Api.Rotation3D actual)
+ {
+ bool same =
+ NearlyEqual(expected.X, actual.A) &&
+ NearlyEqual(expected.Y, actual.B) &&
+ NearlyEqual(expected.Z, actual.C) &&
+ NearlyEqual(expected.W, actual.D);
+ bool negatedSame =
+ NearlyEqual(expected.X, -actual.A) &&
+ NearlyEqual(expected.Y, -actual.B) &&
+ NearlyEqual(expected.Z, -actual.C) &&
+ NearlyEqual(expected.W, -actual.D);
+
+ Assert.IsTrue(same || negatedSame, "Rotation3D quaternion should match target rotation up to sign.");
+ }
+
+ private static bool NearlyEqual(double left, double right)
+ {
+ return System.Math.Abs(left - right) < 1e-6;
+ }
+
+ private struct QuaternionData
+ {
+ public QuaternionData(double x, double y, double z, double w)
+ {
+ X = x;
+ Y = y;
+ Z = z;
+ W = w;
+ }
+
+ public double X { get; }
+ public double Y { get; }
+ public double Z { get; }
+ public double W { get; }
+
+ public Autodesk.Navisworks.Api.Rotation3D ToRotation3D()
+ {
+ return new Autodesk.Navisworks.Api.Rotation3D(X, Y, Z, W);
+ }
+
+ public static QuaternionData Identity => new QuaternionData(0, 0, 0, 1);
+ }
+ }
+}
diff --git a/resources/unit_cube_yup.nwc b/resources/unit_cube_yup.nwc
new file mode 100644
index 0000000000000000000000000000000000000000..47289cdd24b1170ccafc876bab75ff0c0c831ede
GIT binary patch
literal 1806
zcma)7X;_m768=I+`!G0VEvN
z&|pwRPTMX70nxHn4k;i(4!IVjU9O^Tp_P>zvT^;h{nz<5$Mej*^UgdoCf>}zfJ7cE
z;;*X=F@%1p#
zlMc-9B|ORt`GOU?Uq~=%jxi<*Ia8qqQz={1tDi8ZV`KoXi!oF+K-OZstfIsg>;F7p
zGV?m{NEnZ^Q4!pBd(5=wo?lYKw}}7%0faJY1f4E|&RqaoAq-74JZmqc(eWsPX~L{r
zkd_-c0qjp~g5l98qT$i;(I*JW(ZP++*kqYnH(_6?)SC-e&-#X(psDe9po0ar0qUTg
zH5ipSqJzidb^HLpm~8PvC}T#_>2&qENbm`y8-Q0qoK!A)1^@sW0OTV8Kb7eM;uX)a
zxxRQGuYv|FZs@shzgk|knp_k40P_0-_OHH4@l5*9u%@({?(7?Im1Y>~q@6#ne+EcGa(?DM5C9b{M4cMAVm?x%%f$
zU0Q)&gLn|o1Ad*Q6||Xu0eo#hvS2l-!lk0+{{S*o=sh5e^B@hz2mqI0b0h5oC7TN)
zRWCRDN7O)Pgvcl3YlP?ymm{2N&&qVs=pFQ=VLhpXFD&p1A6%ZE2i81-3<3G0nISqN
z*sx;kf32}V>0at;ceM68{q(d=i!F4N16b>s4S(cIRWJ-5`CG^B10GEP$-pq1Y>vDI
zC7B>=A@U<3%^j$-J-Xw^0}mYODAMhgBk%5cOrkXB0mS;zBwyT-yAHrbe?5CuD%J<;
z8D?liC#dRc643sTK0~D<0X3maTNO@d&C|ruaOPr7Peos!Pk|>EjTZk-#XiU4Ul987
zx)`qk3w$rZd>H37Ffgo85EK@W5={Lw5~cpxRyC!M9K;Tb@=WEi{bN&E?98yR6w9HR
z(%4^|SW<4m+s2m;*x3HKA2ZZ}+z<>&D4Sg<}@nD$}qJtXE;#jRr*!T
zlVzHrI$frh~9VqzF=J_?O(c^bEtYr?lV#KsV)3FP2LZC1R=4g!)F#-pt*9jOFRbeyg5{D*IjM-^3WKQ
zuWb(imFrfEe^`!_;ts7&BGPRQcZCaUc%+4LjJc6j+~gCiQR6vjilgizmE(qlE**!j
zHx9YByL?wnc1^CS?d;IEN-&{W3yXH(c-v87v)F-lEV=0O@BmnI(87${=gjMZ@`P5l
z=B`)XaIKN;!|UTM0cfX*mnlI^m9#=I#*aXaMlER
z8k%|7Z7c}kszd2Co#-*n%`UHtb)N;g2Q5hQH5~_XLK&4h14w9+lAZtTV537NXEZ*Q
z+!s&h`}%U;Y?H@5C{fe_WkKWMQt$#xvB(O9CEd*z!`D5Yle;aOS}gkMr!m771MT+9-m6CDg~BP5oVUF;Ju${NRJnD>@{8cBUH
zH;q}7eVM;_#mMOW#`nRO9OK`#Av<=_)n{e}fp*|QZk*P#4z>!*ds1sO$NO{pslQG4yjmbq}^ 0.001;
if (hasTranslation || hasRotation)
@@ -300,7 +289,7 @@ namespace NavisworksTransport.Core
var newComponents = newTransform.Factor();
newComponents.Translation = currentTranslation;
newComponents.Rotation = currentRotation;
-
+
Transform3D combinedTransform = newComponents.Combine();
var doc = Application.ActiveDocument;
var modelItems = new ModelItemCollection { _virtualObjectModelItem };
@@ -332,7 +321,6 @@ namespace NavisworksTransport.Core
lengthMeters / baseSizeMeters,
widthMeters / baseSizeMeters,
heightMeters / baseSizeMeters);
-
Transform3D newTransform = transformComponents.Combine();
doc.Models.SetModelUnitsAndTransform(_virtualObjectModel, _virtualObjectModel.Units, newTransform, false);
diff --git a/src/Utils/CoordinateSystem/FragmentDefaultUpContext.cs b/src/Utils/CoordinateSystem/FragmentDefaultUpContext.cs
index 32ab907..c7eadc4 100644
--- a/src/Utils/CoordinateSystem/FragmentDefaultUpContext.cs
+++ b/src/Utils/CoordinateSystem/FragmentDefaultUpContext.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Concurrent;
+using System.Runtime.CompilerServices;
using Autodesk.Navisworks.Api;
namespace NavisworksTransport.Utils.CoordinateSystem
@@ -13,6 +14,9 @@ namespace NavisworksTransport.Utils.CoordinateSystem
private static readonly ConcurrentDictionary DocumentFragmentDefaultUpAxes =
new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase);
+ private static readonly ConcurrentDictionary RuntimeDocumentKeys =
+ new ConcurrentDictionary();
+
public static string GetCurrentDocumentKey(Document doc = null)
{
doc = doc ?? Autodesk.Navisworks.Api.Application.ActiveDocument;
@@ -21,12 +25,39 @@ namespace NavisworksTransport.Utils.CoordinateSystem
return string.Empty;
}
- if (!string.IsNullOrWhiteSpace(doc.FileName))
+ int runtimeDocumentId = RuntimeHelpers.GetHashCode(doc);
+ return GetOrCreateDocumentKey(runtimeDocumentId, doc.FileName, doc.Title);
+ }
+
+ internal static string GetOrCreateDocumentKey(int runtimeDocumentId, string fileName, string title)
+ {
+ if (runtimeDocumentId <= 0)
{
- return doc.FileName;
+ return string.Empty;
}
- return doc.Title ?? string.Empty;
+ if (RuntimeDocumentKeys.TryGetValue(runtimeDocumentId, out string existingKey) &&
+ !string.IsNullOrWhiteSpace(existingKey))
+ {
+ return existingKey;
+ }
+
+ string resolvedKey;
+ if (!string.IsNullOrWhiteSpace(fileName))
+ {
+ resolvedKey = $"file:{fileName}";
+ }
+ else if (!string.IsNullOrWhiteSpace(title))
+ {
+ resolvedKey = $"runtime:{runtimeDocumentId}|title:{title}";
+ }
+ else
+ {
+ resolvedKey = $"runtime:{runtimeDocumentId}";
+ }
+
+ RuntimeDocumentKeys[runtimeDocumentId] = resolvedKey;
+ return resolvedKey;
}
public static string GetCurrentHostUpAxis(Document doc = null)
@@ -86,5 +117,11 @@ namespace NavisworksTransport.Utils.CoordinateSystem
return string.Equals(axisName, "Y", StringComparison.OrdinalIgnoreCase) ||
string.Equals(axisName, "Z", StringComparison.OrdinalIgnoreCase);
}
+
+ internal static void ResetForTests()
+ {
+ DocumentFragmentDefaultUpAxes.Clear();
+ RuntimeDocumentKeys.Clear();
+ }
}
}
diff --git a/src/Utils/CoordinateSystem/HostCoordinateAdapter.cs b/src/Utils/CoordinateSystem/HostCoordinateAdapter.cs
index ef2bf29..f95f366 100644
--- a/src/Utils/CoordinateSystem/HostCoordinateAdapter.cs
+++ b/src/Utils/CoordinateSystem/HostCoordinateAdapter.cs
@@ -179,6 +179,21 @@ namespace NavisworksTransport.Utils.CoordinateSystem
}
public Rotation3D FromCanonicalRotation(Quaternion canonicalRotation)
+ {
+ return FromCanonicalRotationLegacy(canonicalRotation);
+ }
+
+ public Rotation3D FromCanonicalRotationLegacy(Quaternion canonicalRotation)
+ {
+ Matrix4x4 canonicalLinear = Matrix4x4.CreateFromQuaternion(canonicalRotation);
+ Matrix4x4 hostLinear = FromCanonicalLinearTransform(canonicalLinear);
+ return CreateRotationFromLinearComponents(
+ hostLinear.M11, hostLinear.M12, hostLinear.M13,
+ hostLinear.M21, hostLinear.M22, hostLinear.M23,
+ hostLinear.M31, hostLinear.M32, hostLinear.M33);
+ }
+
+ public Rotation3D FromCanonicalRotationDirect(Quaternion canonicalRotation)
{
Matrix4x4 canonicalLinear = Matrix4x4.CreateFromQuaternion(canonicalRotation);
Matrix4x4 hostLinear = FromCanonicalLinearTransform(canonicalLinear);
@@ -196,6 +211,20 @@ namespace NavisworksTransport.Utils.CoordinateSystem
/// 这里不做任何轴交换、符号翻转或重新解释,只负责把已经确定好的宿主四元数写入 Navisworks。
///
public Rotation3D FromHostQuaternion(Quaternion hostRotation)
+ {
+ return FromHostQuaternionLegacy(hostRotation);
+ }
+
+ public Rotation3D FromHostQuaternionLegacy(Quaternion hostRotation)
+ {
+ Matrix4x4 hostLinear = Matrix4x4.CreateFromQuaternion(hostRotation);
+ return CreateRotationFromLinearComponents(
+ hostLinear.M11, hostLinear.M12, hostLinear.M13,
+ hostLinear.M21, hostLinear.M22, hostLinear.M23,
+ hostLinear.M31, hostLinear.M32, hostLinear.M33);
+ }
+
+ public Rotation3D FromHostQuaternionDirect(Quaternion hostRotation)
{
Quaternion normalizedHostRotation = Quaternion.Normalize(hostRotation);
return new Rotation3D(
@@ -276,6 +305,53 @@ namespace NavisworksTransport.Utils.CoordinateSystem
return Quaternion.Normalize(qz * qy * qx);
}
+ private static Rotation3D CreateRotationFromLinearComponents(
+ double m00, double m01, double m02,
+ double m10, double m11, double m12,
+ double m20, double m21, double m22)
+ {
+ double qx;
+ double qy;
+ double qz;
+ double qw;
+
+ double trace = m00 + m11 + m22;
+ if (trace > 0.0)
+ {
+ double s = Math.Sqrt(trace + 1.0) * 2.0;
+ qw = 0.25 * s;
+ qx = (m21 - m12) / s;
+ qy = (m02 - m20) / s;
+ qz = (m10 - m01) / s;
+ }
+ else if (m00 > m11 && m00 > m22)
+ {
+ double s = Math.Sqrt(1.0 + m00 - m11 - m22) * 2.0;
+ qw = (m21 - m12) / s;
+ qx = 0.25 * s;
+ qy = (m01 + m10) / s;
+ qz = (m02 + m20) / s;
+ }
+ else if (m11 > m22)
+ {
+ double s = Math.Sqrt(1.0 + m11 - m00 - m22) * 2.0;
+ qw = (m02 - m20) / s;
+ qx = (m01 + m10) / s;
+ qy = 0.25 * s;
+ qz = (m12 + m21) / s;
+ }
+ else
+ {
+ double s = Math.Sqrt(1.0 + m22 - m00 - m11) * 2.0;
+ qw = (m10 - m01) / s;
+ qx = (m02 + m20) / s;
+ qy = (m12 + m21) / s;
+ qz = 0.25 * s;
+ }
+
+ return new Rotation3D(qx, qy, qz, qw);
+ }
+
public CanonicalBounds3 ToCanonicalBounds3(CanonicalBounds3 hostBounds)
{
return RebuildBounds3(hostBounds, ToCanonicalPoint3);
diff --git a/src/Utils/ModelItemTransformHelper.cs b/src/Utils/ModelItemTransformHelper.cs
index d76da64..58460e5 100644
--- a/src/Utils/ModelItemTransformHelper.cs
+++ b/src/Utils/ModelItemTransformHelper.cs
@@ -589,6 +589,41 @@ namespace NavisworksTransport.Utils
LogIncrementalTransformActual(item, targetPosition, targetRotation);
}
+ ///
+ /// 读取物体当前实际几何姿态。
+ /// 优先使用 ModelGeometry.ActiveTransform,因为 ModelItem.Transform 只反映原始设计变换。
+ ///
+ public static bool TryGetCurrentGeometryRotation(ModelItem item, out Rotation3D rotation)
+ {
+ rotation = Rotation3D.Identity;
+ if (item == null)
+ {
+ return false;
+ }
+
+ try
+ {
+ ModelGeometry geometry = item.FindFirstGeometry() ?? item.Geometry;
+ if (geometry?.ActiveTransform == null)
+ {
+ return false;
+ }
+
+ rotation = geometry.ActiveTransform.Factor().Rotation;
+ return true;
+ }
+ catch (Exception ex)
+ {
+ LogManager.Warning($"[当前几何姿态] 读取 ActiveTransform 失败: {ex.Message}");
+ return false;
+ }
+ }
+
+ public static void LogCurrentGeometryTransformsForDebug(ModelItem item, string prefix)
+ {
+ LogGeometryLevelTransforms(item, prefix);
+ }
+
private static void LogIncrementalTransformActual(ModelItem item, Point3D targetPosition, Rotation3D targetRotation)
{
try
@@ -614,6 +649,8 @@ namespace NavisworksTransport.Utils
$"期望X=({targetLinear.Get(0, 0):F4},{targetLinear.Get(1, 0):F4},{targetLinear.Get(2, 0):F4}), " +
$"期望Y=({targetLinear.Get(0, 1):F4},{targetLinear.Get(1, 1):F4},{targetLinear.Get(2, 1):F4}), " +
$"期望Z=({targetLinear.Get(0, 2):F4},{targetLinear.Get(1, 2):F4},{targetLinear.Get(2, 2):F4})");
+
+ LogGeometryLevelTransforms(item, "[模型增量姿态应用后][GeometryAPI]");
}
catch (Exception ex)
{
@@ -792,19 +829,21 @@ namespace NavisworksTransport.Utils
var doc = Application.ActiveDocument;
var modelItems = new ModelItemCollection { item };
Vector3D currentScale = new Vector3D(1, 1, 1);
+ ModelGeometry geometry = item.FindFirstGeometry() ?? item.Geometry;
if (preserveCurrentScale)
{
- var currentComponents = item.Transform.Factor();
+ var currentBaseTransform = geometry?.PermanentOverrideTransform ?? item.Transform;
+ var currentComponents = currentBaseTransform.Factor();
currentScale = currentComponents.Scale;
}
doc.Models.ResetPermanentTransform(modelItems);
- var originalTransform = item.Transform;
+ var originalTransform = geometry?.OriginalTransform ?? item.Transform;
var originalComponents = originalTransform.Factor();
var originalRotation = originalComponents.Rotation;
- var originalBounds = item.BoundingBox();
+ var originalBounds = geometry?.BoundingBox ?? item.BoundingBox();
var originalCenterPos = originalBounds.Center;
Rotation3D deltaRotation = BuildDeltaRotation(originalRotation, targetRotation);
@@ -840,6 +879,7 @@ namespace NavisworksTransport.Utils
LogAbsoluteTransformDiagnostics(item, originalRotation, targetRotation, deltaRotation, originalCenterPos, targetPosition, translation);
doc.Models.OverridePermanentTransform(modelItems, transform, false);
+ LogAbsoluteTransformActual(item, preserveCurrentScale, currentScale, targetPosition, targetRotation);
}
private static Rotation3D BuildDeltaRotation(Rotation3D originalRotation, Rotation3D targetRotation)
@@ -893,6 +933,108 @@ namespace NavisworksTransport.Utils
}
}
+ private static void LogAbsoluteTransformActual(
+ ModelItem item,
+ bool preserveCurrentScale,
+ Vector3D currentScale,
+ Point3D targetPosition,
+ Rotation3D targetRotation)
+ {
+ try
+ {
+ Transform3D actualTransform = item.Transform;
+ var actualComponents = actualTransform.Factor();
+ var actualRotation = actualComponents.Rotation;
+ var actualScale = actualComponents.Scale;
+ var actualLinear = new Transform3D(actualRotation).Linear;
+ var targetLinear = new Transform3D(targetRotation).Linear;
+ var actualBounds = item.BoundingBox();
+ Point3D actualCenter = actualBounds?.Center ?? new Point3D(0, 0, 0);
+
+ LogManager.Info(
+ $"[模型姿态应用后] {item.DisplayName} PreserveScale={preserveCurrentScale}, " +
+ $"输入Scale=({currentScale.X:F4},{currentScale.Y:F4},{currentScale.Z:F4}), " +
+ $"实际Scale=({actualScale.X:F4},{actualScale.Y:F4},{actualScale.Z:F4}), " +
+ $"目标中心=({targetPosition.X:F3},{targetPosition.Y:F3},{targetPosition.Z:F3}), " +
+ $"实际中心=({actualCenter.X:F3},{actualCenter.Y:F3},{actualCenter.Z:F3})");
+
+ LogManager.Info(
+ $"[模型姿态应用后] {item.DisplayName} 目标Transform轴: " +
+ $"X=({targetLinear.Get(0, 0):F4},{targetLinear.Get(1, 0):F4},{targetLinear.Get(2, 0):F4}), " +
+ $"Y=({targetLinear.Get(0, 1):F4},{targetLinear.Get(1, 1):F4},{targetLinear.Get(2, 1):F4}), " +
+ $"Z=({targetLinear.Get(0, 2):F4},{targetLinear.Get(1, 2):F4},{targetLinear.Get(2, 2):F4})");
+
+ LogManager.Info(
+ $"[模型姿态应用后] {item.DisplayName} 实际Transform轴: " +
+ $"X=({actualLinear.Get(0, 0):F4},{actualLinear.Get(1, 0):F4},{actualLinear.Get(2, 0):F4}), " +
+ $"Y=({actualLinear.Get(0, 1):F4},{actualLinear.Get(1, 1):F4},{actualLinear.Get(2, 1):F4}), " +
+ $"Z=({actualLinear.Get(0, 2):F4},{actualLinear.Get(1, 2):F4},{actualLinear.Get(2, 2):F4})");
+
+ LogGeometryLevelTransforms(item, "[模型姿态应用后][GeometryAPI]");
+ }
+ catch (Exception ex)
+ {
+ LogManager.Warning($"[模型姿态应用后] 输出诊断日志失败: {ex.Message}");
+ }
+ }
+
+ private static void LogGeometryLevelTransforms(ModelItem item, string prefix)
+ {
+ if (item == null)
+ {
+ return;
+ }
+
+ try
+ {
+ ModelGeometry geometry = item.FindFirstGeometry() ?? item.Geometry;
+ if (geometry == null)
+ {
+ LogManager.Info($"{prefix} Geometry=null");
+ return;
+ }
+
+ LogManager.Info(
+ $"{prefix} GeometryType={geometry.GetType().FullName}, " +
+ $"FragmentCount={geometry.FragmentCount}, " +
+ $"BoundsCenter=({geometry.BoundingBox.Center.X:F3},{geometry.BoundingBox.Center.Y:F3},{geometry.BoundingBox.Center.Z:F3})");
+
+ LogTransformProperty(geometry.OriginalTransform, $"{prefix} OriginalTransform");
+ LogTransformProperty(geometry.PermanentOverrideTransform, $"{prefix} PermanentOverrideTransform");
+ LogTransformProperty(geometry.PermanentTransform, $"{prefix} PermanentTransform");
+ LogTransformProperty(geometry.ActiveTransform, $"{prefix} ActiveTransform");
+ }
+ catch (Exception ex)
+ {
+ LogManager.Warning($"{prefix} 读取几何层当前变换失败: {ex.Message}");
+ }
+ }
+
+ private static void LogTransformProperty(Transform3D transform, string prefix)
+ {
+ if (transform == null)
+ {
+ LogManager.Info($"{prefix}=null");
+ return;
+ }
+
+ try
+ {
+ var linear = transform.Linear;
+ var components = transform.Factor();
+ LogManager.Info(
+ $"{prefix}: " +
+ $"X=({linear.Get(0, 0):F4},{linear.Get(1, 0):F4},{linear.Get(2, 0):F4}), " +
+ $"Y=({linear.Get(0, 1):F4},{linear.Get(1, 1):F4},{linear.Get(2, 1):F4}), " +
+ $"Z=({linear.Get(0, 2):F4},{linear.Get(1, 2):F4},{linear.Get(2, 2):F4}), " +
+ $"T=({components.Translation.X:F3},{components.Translation.Y:F3},{components.Translation.Z:F3})");
+ }
+ catch (Exception ex)
+ {
+ LogManager.Warning($"{prefix} 读取失败: {ex.Message}");
+ }
+ }
+
///
/// 将物体移动到指定位置和朝向,同时保持缩放比例
/// 专为虚拟物体设计,避免缩放被覆盖
From 921dc07856661a001182fb5989a5a6831e1d9585 Mon Sep 17 00:00:00 2001
From: tian <11429339@qq.com>
Date: Wed, 25 Mar 2026 17:19:21 +0800
Subject: [PATCH 30/87] Fix ground real-object forward axis and compensation
---
TransportPlugin.csproj | 5 -
.../PathTargetFrameResolverTests.cs | 59 +++
.../RealObjectPlanarPoseSolverTests.cs | 28 ++
resources/unit_cube_yup.nwc | Bin 1806 -> 0 bytes
resources/unit_cube_yup.obj | 26 --
src/Core/Animation/PathAnimationManager.cs | 413 ++++++++++++++++--
src/Core/VirtualObjectManager.cs | 42 +-
src/UI/WPF/ViewModels/PathEditingViewModel.cs | 48 +-
.../PathTargetFrameResolver.cs | 138 ++++++
9 files changed, 649 insertions(+), 110 deletions(-)
delete mode 100644 resources/unit_cube_yup.nwc
delete mode 100644 resources/unit_cube_yup.obj
diff --git a/TransportPlugin.csproj b/TransportPlugin.csproj
index 63a7404..f126d00 100644
--- a/TransportPlugin.csproj
+++ b/TransportPlugin.csproj
@@ -522,11 +522,6 @@
PreserveNewest
resources\unit_cube.nwc
-
-
- PreserveNewest
- resources\unit_cube_yup.nwc
-
PreserveNewest
diff --git a/UnitTests/CoordinateSystem/PathTargetFrameResolverTests.cs b/UnitTests/CoordinateSystem/PathTargetFrameResolverTests.cs
index d060e02..6bfe5dc 100644
--- a/UnitTests/CoordinateSystem/PathTargetFrameResolverTests.cs
+++ b/UnitTests/CoordinateSystem/PathTargetFrameResolverTests.cs
@@ -1,5 +1,6 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using NavisworksTransport.Utils.CoordinateSystem;
+using System.Collections.Generic;
using System.Numerics;
namespace NavisworksTransport.UnitTests.CoordinateSystem
@@ -35,6 +36,64 @@ namespace NavisworksTransport.UnitTests.CoordinateSystem
Assert.AreEqual(1.0, frame.Side.Length(), 1e-6);
}
+ [TestMethod]
+ public void Hoisting_StartForward_ShouldUseHorizontalSegmentInsteadOfInitialLift()
+ {
+ var pathPoints = new List
+ {
+ new Vector3(-105.992f, -28.615f, 77.043f),
+ new Vector3(-105.992f, 16.230f, 77.043f),
+ new Vector3(-229.870f, 16.230f, -32.982f)
+ };
+
+ bool ok = PathTargetFrameResolver.TryResolvePlanarStartHostForward(
+ NavisworksTransport.PathType.Hoisting,
+ pathPoints,
+ out Vector3 hostForward);
+
+ Assert.IsTrue(ok);
+ AssertVector(Vector3.Normalize(hostForward), -0.7477, 0.0, -0.6640, 1e-4);
+ }
+
+ [TestMethod]
+ public void Hoisting_StartFrame_ShouldUseHorizontalSegmentAndKeepHostUp()
+ {
+ var pathPoints = new List
+ {
+ new Vector3(-105.992f, -28.615f, 77.043f),
+ new Vector3(-105.992f, 16.230f, 77.043f),
+ new Vector3(-229.870f, 16.230f, -32.982f)
+ };
+
+ bool ok = PathTargetFrameResolver.TryCreatePlanarStartHostFrame(
+ NavisworksTransport.PathType.Hoisting,
+ pathPoints,
+ CoordinateSystemType.YUp,
+ out PathTargetFrame frame);
+
+ Assert.IsTrue(ok);
+ AssertVector(frame.Up, 0.0, 1.0, 0.0);
+ Assert.AreEqual(0.0, Vector3.Dot(frame.Forward, frame.Up), 1e-6);
+ }
+
+ [TestMethod]
+ public void YUp_PlanarYaw_ShouldUseHostXZPlane_NotHostXY()
+ {
+ bool ok1 = PathTargetFrameResolver.TryResolvePlanarHostYaw(
+ new Vector3(1.0f, 0.0f, 0.0f),
+ CoordinateSystemType.YUp,
+ out double yaw1);
+ bool ok2 = PathTargetFrameResolver.TryResolvePlanarHostYaw(
+ new Vector3(0.0f, 0.0f, 1.0f),
+ CoordinateSystemType.YUp,
+ out double yaw2);
+
+ Assert.IsTrue(ok1);
+ Assert.IsTrue(ok2);
+ Assert.AreEqual(0.0, yaw1, 1e-6);
+ Assert.AreEqual(-System.Math.PI / 2.0, yaw2, 1e-6);
+ }
+
private static void AssertVector(Vector3 actual, double x, double y, double z, double tolerance = 1e-6)
{
Assert.AreEqual(x, actual.X, tolerance);
diff --git a/UnitTests/CoordinateSystem/RealObjectPlanarPoseSolverTests.cs b/UnitTests/CoordinateSystem/RealObjectPlanarPoseSolverTests.cs
index 9acc6a3..ebf77c7 100644
--- a/UnitTests/CoordinateSystem/RealObjectPlanarPoseSolverTests.cs
+++ b/UnitTests/CoordinateSystem/RealObjectPlanarPoseSolverTests.cs
@@ -124,6 +124,34 @@ namespace NavisworksTransport.UnitTests.CoordinateSystem
1e-4);
}
+ [TestMethod]
+ public void FixedPositiveX_ShouldNotSwitchToZ_WhenPathLeansTowardZ()
+ {
+ Quaternion referenceRotation = Quaternion.Identity;
+ Vector3 desiredForward = Vector3.Normalize(new Vector3(-0.690f, 0.0f, 0.724f));
+ Vector3 desiredUp = Vector3.UnitY;
+
+ bool ok = RealObjectPlanarPoseSolver.TryCreatePlanarPoseFromReferencePose(
+ referenceRotation,
+ desiredForward,
+ desiredUp,
+ LocalAxisDirection.PositiveX,
+ out RealObjectPlanarPoseSolution solution);
+
+ Assert.IsTrue(ok);
+ Assert.AreEqual(LocalAxisDirection.PositiveX, solution.SelectedReferenceAxisDirection);
+ AssertVector(solution.SelectedReferenceAxisLocal, 1.0, 0.0, 0.0);
+
+ Vector3 transformedSelectedAxis = Vector3.Normalize(
+ Vector3.Transform(solution.SelectedReferenceAxisLocal, solution.BaselineRotation));
+ AssertVector(
+ transformedSelectedAxis,
+ desiredForward.X,
+ desiredForward.Y,
+ desiredForward.Z,
+ 1e-4);
+ }
+
private static void AssertVector(Vector3 vector, double x, double y, double z, double tolerance = 1e-6)
{
Assert.AreEqual(x, vector.X, tolerance);
diff --git a/resources/unit_cube_yup.nwc b/resources/unit_cube_yup.nwc
deleted file mode 100644
index 47289cdd24b1170ccafc876bab75ff0c0c831ede..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001
literal 1806
zcma)7X;_m768=I+`!G0VEvN
z&|pwRPTMX70nxHn4k;i(4!IVjU9O^Tp_P>zvT^;h{nz<5$Mej*^UgdoCf>}zfJ7cE
z;;*X=F@%1p#
zlMc-9B|ORt`GOU?Uq~=%jxi<*Ia8qqQz={1tDi8ZV`KoXi!oF+K-OZstfIsg>;F7p
zGV?m{NEnZ^Q4!pBd(5=wo?lYKw}}7%0faJY1f4E|&RqaoAq-74JZmqc(eWsPX~L{r
zkd_-c0qjp~g5l98qT$i;(I*JW(ZP++*kqYnH(_6?)SC-e&-#X(psDe9po0ar0qUTg
zH5ipSqJzidb^HLpm~8PvC}T#_>2&qENbm`y8-Q0qoK!A)1^@sW0OTV8Kb7eM;uX)a
zxxRQGuYv|FZs@shzgk|knp_k40P_0-_OHH4@l5*9u%@({?(7?Im1Y>~q@6#ne+EcGa(?DM5C9b{M4cMAVm?x%%f$
zU0Q)&gLn|o1Ad*Q6||Xu0eo#hvS2l-!lk0+{{S*o=sh5e^B@hz2mqI0b0h5oC7TN)
zRWCRDN7O)Pgvcl3YlP?ymm{2N&&qVs=pFQ=VLhpXFD&p1A6%ZE2i81-3<3G0nISqN
z*sx;kf32}V>0at;ceM68{q(d=i!F4N16b>s4S(cIRWJ-5`CG^B10GEP$-pq1Y>vDI
zC7B>=A@U<3%^j$-J-Xw^0}mYODAMhgBk%5cOrkXB0mS;zBwyT-yAHrbe?5CuD%J<;
z8D?liC#dRc643sTK0~D<0X3maTNO@d&C|ruaOPr7Peos!Pk|>EjTZk-#XiU4Ul987
zx)`qk3w$rZd>H37Ffgo85EK@W5={Lw5~cpxRyC!M9K;Tb@=WEi{bN&E?98yR6w9HR
z(%4^|SW<4m+s2m;*x3HKA2ZZ}+z<>&D4Sg<}@nD$}qJtXE;#jRr*!T
zlVzHrI$frh~9VqzF=J_?O(c^bEtYr?lV#KsV)3FP2LZC1R=4g!)F#-pt*9jOFRbeyg5{D*IjM-^3WKQ
zuWb(imFrfEe^`!_;ts7&BGPRQcZCaUc%+4LjJc6j+~gCiQR6vjilgizmE(qlE**!j
zHx9YByL?wnc1^CS?d;IEN-&{W3yXH(c-v87v)F-lEV=0O@BmnI(87${=gjMZ@`P5l
z=B`)XaIKN;!|UTM0cfX*mnlI^m9#=I#*aXaMlER
z8k%|7Z7c}kszd2Co#-*n%`UHtb)N;g2Q5hQH5~_XLK&4h14w9+lAZtTV537NXEZ*Q
z+!s&h`}%U;Y?H@5C{fe_WkKWMQt$#xvB(O9CEd*z!`D5Yle;aOS}gkMr!m771MT+9-m6CDg~BP5oVUF;Ju${NRJnD>@{8cBUH
zH;q}7eVM;_#mMOW#`nRO9OK`#Av<=_)n{e}fp*|QZk*P#4z>!*ds1sO$NO{pslQG4yjmbq}^ 1e-3)
+ {
+ _groundRealObjectStartCompensation = startOffset;
+ _hasGroundRealObjectStartCompensation = true;
+
+ UpdateObjectPosition(startPosition, planarRotation);
+
+ var correctedAppliedPoint = GetTrackedObjectPosition(CurrentControlledObject ?? _animatedObject);
+ LogManager.Info(
+ $"[路径起点补偿] 补偿后实际trackedPoint=({correctedAppliedPoint.X:F3},{correctedAppliedPoint.Y:F3},{correctedAppliedPoint.Z:F3}), " +
+ $"相对目标偏差=({correctedAppliedPoint.X - startPosition.X:F3},{correctedAppliedPoint.Y - startPosition.Y:F3},{correctedAppliedPoint.Z - startPosition.Z:F3})");
+ }
+ else
+ {
+ _groundRealObjectStartCompensation = new Vector3D(0, 0, 0);
+ _hasGroundRealObjectStartCompensation = false;
+ }
+ }
+
+ if (IsRealObjectMode && _route.PathType == PathType.Ground)
+ {
+ _groundRealObjectBaseRotation = planarRotation;
+ _hasGroundRealObjectBasePose = TryResolveGroundRealObjectBaseYaw(out _groundRealObjectBaseYaw);
+ LogManager.Info(
+ $"[Ground真实物体基姿态] {animatedObject.DisplayName} BaseYaw={_groundRealObjectBaseYaw * 180.0 / Math.PI:F2}°, " +
+ $"已记录基姿态={_hasGroundRealObjectBasePose}, " +
+ $"起点补偿=({_groundRealObjectStartCompensation.X:F3},{_groundRealObjectStartCompensation.Y:F3},{_groundRealObjectStartCompensation.Z:F3}), " +
+ $"已启用补偿={_hasGroundRealObjectStartCompensation}");
+ }
+ else
+ {
+ _hasGroundRealObjectBasePose = false;
+ _groundRealObjectStartCompensation = new Vector3D(0, 0, 0);
+ _hasGroundRealObjectStartCompensation = false;
+ }
}
else if (_route.PathType == PathType.Ground || _route.PathType == PathType.Hoisting)
{
@@ -773,6 +841,11 @@ namespace NavisworksTransport.Core.Animation
{
// 使用 UpdateObjectPosition 统一处理移动和旋转
UpdateObjectPosition(startPosition, yaw);
+
+ var startAppliedPoint = GetTrackedObjectPosition(CurrentControlledObject ?? _animatedObject);
+ LogManager.Info(
+ $"[路径起点诊断] 起点应用后实际trackedPoint=({startAppliedPoint.X:F3},{startAppliedPoint.Y:F3},{startAppliedPoint.Z:F3}), " +
+ $"相对目标偏差=({startAppliedPoint.X - startPosition.X:F3},{startAppliedPoint.Y - startPosition.Y:F3},{startAppliedPoint.Z - startPosition.Z:F3})");
}
string pathTypeName = _route.PathType.GetDisplayName();
@@ -1108,8 +1181,18 @@ namespace NavisworksTransport.Core.Animation
else if ((_route.PathType == PathType.Ground || _route.PathType == PathType.Hoisting) &&
TryCreatePlanarPathRotationForFrame(previousFramePoint, framePosition, nextFramePoint, out var planarRotation))
{
- frame.Rotation = planarRotation;
- frame.HasCustomRotation = true;
+ if (_route.PathType == PathType.Ground &&
+ IsRealObjectMode &&
+ TryCreateGroundRealObjectConstrainedRotation(previousFramePoint, framePosition, nextFramePoint, out var constrainedRotation))
+ {
+ frame.Rotation = constrainedRotation;
+ frame.HasCustomRotation = true;
+ }
+ else
+ {
+ frame.Rotation = planarRotation;
+ frame.HasCustomRotation = true;
+ }
}
else if (_route.PathType == PathType.Ground || _route.PathType == PathType.Hoisting)
{
@@ -1834,8 +1917,18 @@ namespace NavisworksTransport.Core.Animation
// 但后续碰撞链又按最终姿态重新摆正,造成“结束后又往前窜一点”。
if (_animationFrames != null && _animationFrames.Count > 0)
{
- _currentFrameIndex = _animationFrames.Count - 1;
- ApplyAnimationFramePose(_animationFrames[_currentFrameIndex], _currentFrameIndex);
+ int lastFrameIndex = _animationFrames.Count - 1;
+ if (_currentFrameIndex != lastFrameIndex)
+ {
+ _currentFrameIndex = lastFrameIndex;
+ ApplyAnimationFramePose(_animationFrames[_currentFrameIndex], _currentFrameIndex);
+ LogManager.Info("[动画结束] 当前尚未到最后一帧,已补落最终姿态");
+ }
+ else
+ {
+ LogManager.Info("[动画结束] 当前已在最后一帧,保持真实结束位置,不再重复落位");
+ }
+
ProgressChanged?.Invoke(this, 100.0);
try
{
@@ -2496,6 +2589,7 @@ namespace NavisworksTransport.Core.Animation
Point3D currentPositionForTransform = _trackedPosition;
Rotation3D currentRotation;
Rotation3D appliedTargetRotation = newRotation;
+ Point3D appliedTargetPosition = newPosition;
if (_hasTrackedRotation)
{
currentRotation = _trackedRotation;
@@ -2517,17 +2611,54 @@ namespace NavisworksTransport.Core.Animation
currentPositionForTransform = actualHostPosition;
LogManager.Info(
$"[动画姿态入口] {controlledObject.DisplayName} 宿主即时读回点=({actualHostPosition.X:F3},{actualHostPosition.Y:F3},{actualHostPosition.Z:F3})");
+
+ if (IsRealObjectMode &&
+ (_route?.PathType == PathType.Ground || _route?.PathType == PathType.Hoisting) &&
+ ModelItemTransformHelper.TryGetCurrentGeometryRotation(controlledObject, out var actualGeometryRotation))
+ {
+ var trackedLinear = new Transform3D(currentRotation).Linear;
+ var actualLinear = new Transform3D(actualGeometryRotation).Linear;
+ LogManager.Info(
+ $"[平面姿态基线诊断] {controlledObject.DisplayName} 跟踪旋转: " +
+ $"X=({trackedLinear.Get(0, 0):F4},{trackedLinear.Get(1, 0):F4},{trackedLinear.Get(2, 0):F4}), " +
+ $"Y=({trackedLinear.Get(0, 1):F4},{trackedLinear.Get(1, 1):F4},{trackedLinear.Get(2, 1):F4}), " +
+ $"Z=({trackedLinear.Get(0, 2):F4},{trackedLinear.Get(1, 2):F4},{trackedLinear.Get(2, 2):F4})");
+ LogManager.Info(
+ $"[平面姿态基线诊断] {controlledObject.DisplayName} 实际几何旋转: " +
+ $"X=({actualLinear.Get(0, 0):F4},{actualLinear.Get(1, 0):F4},{actualLinear.Get(2, 0):F4}), " +
+ $"Y=({actualLinear.Get(0, 1):F4},{actualLinear.Get(1, 1):F4},{actualLinear.Get(2, 1):F4}), " +
+ $"Z=({actualLinear.Get(0, 2):F4},{actualLinear.Get(1, 2):F4},{actualLinear.Get(2, 2):F4})");
+ }
}
catch (Exception ex)
{
LogManager.Warning($"[动画姿态入口] 读取宿主实际状态失败: {ex.Message}");
}
+ if (IsRealObjectMode &&
+ _route?.PathType == PathType.Ground &&
+ !_suppressGroundRealObjectCompensation &&
+ _hasGroundRealObjectStartCompensation &&
+ _hasGroundRealObjectBasePose &&
+ TryRotateGroundRealObjectCompensation(appliedTargetRotation, out var rotatedCompensation))
+ {
+ appliedTargetPosition = new Point3D(
+ newPosition.X - rotatedCompensation.X,
+ newPosition.Y - rotatedCompensation.Y,
+ newPosition.Z - rotatedCompensation.Z);
+
+ LogManager.Info(
+ $"[Ground路径补偿] {controlledObject.DisplayName} 原目标点=({newPosition.X:F3},{newPosition.Y:F3},{newPosition.Z:F3}), " +
+ $"起点补偿=({_groundRealObjectStartCompensation.X:F3},{_groundRealObjectStartCompensation.Y:F3},{_groundRealObjectStartCompensation.Z:F3}), " +
+ $"旋转后补偿=({rotatedCompensation.X:F3},{rotatedCompensation.Y:F3},{rotatedCompensation.Z:F3}), " +
+ $"应用目标点=({appliedTargetPosition.X:F3},{appliedTargetPosition.Y:F3},{appliedTargetPosition.Z:F3})");
+ }
+
var currentLinear = new Transform3D(currentRotation).Linear;
var targetLinear = new Transform3D(appliedTargetRotation).Linear;
LogManager.Info(
$"[动画姿态入口] {controlledObject.DisplayName} 跟踪点=({_trackedPosition.X:F3},{_trackedPosition.Y:F3},{_trackedPosition.Z:F3}), " +
- $"目标点=({newPosition.X:F3},{newPosition.Y:F3},{newPosition.Z:F3})");
+ $"目标点=({appliedTargetPosition.X:F3},{appliedTargetPosition.Y:F3},{appliedTargetPosition.Z:F3})");
LogManager.Info(
$"[动画姿态入口] {controlledObject.DisplayName} 跟踪姿态: " +
$"X=({currentLinear.Get(0, 0):F4},{currentLinear.Get(1, 0):F4},{currentLinear.Get(2, 0):F4}), " +
@@ -2542,10 +2673,10 @@ namespace NavisworksTransport.Core.Animation
controlledObject,
currentPositionForTransform,
currentRotation,
- newPosition,
+ appliedTargetPosition,
appliedTargetRotation);
- _trackedPosition = newPosition;
+ _trackedPosition = appliedTargetPosition;
_trackedRotation = appliedTargetRotation;
_hasTrackedRotation = true;
_currentYaw = ModelItemTransformHelper.GetYawFromRotation(appliedTargetRotation);
@@ -2737,13 +2868,22 @@ namespace NavisworksTransport.Core.Animation
$"Z=({linear.Get(0, 2):F4},{linear.Get(1, 2):F4},{linear.Get(2, 2):F4})");
}
- if (_savedObjectHasCustomRotation)
+ bool previousSuppressCompensation = _suppressGroundRealObjectCompensation;
+ _suppressGroundRealObjectCompensation = true;
+ try
{
- UpdateObjectPosition(_savedObjectPosition, _savedObjectRotation);
+ if (_savedObjectHasCustomRotation)
+ {
+ UpdateObjectPosition(_savedObjectPosition, _savedObjectRotation);
+ }
+ else
+ {
+ UpdateObjectPosition(_savedObjectPosition, _savedObjectYaw);
+ }
}
- else
+ finally
{
- UpdateObjectPosition(_savedObjectPosition, _savedObjectYaw);
+ _suppressGroundRealObjectCompensation = previousSuppressCompensation;
}
_trackedPosition = _savedObjectPosition;
@@ -3533,6 +3673,154 @@ namespace NavisworksTransport.Core.Animation
return adapter.FromCanonicalPoint(new Point3D(canonicalCenter.X, canonicalCenter.Y, canonicalCenter.Z));
}
+ private bool TryResolveGroundRealObjectBaseYaw(out double yawRadians)
+ {
+ yawRadians = 0.0;
+ if (_route?.PathType != PathType.Ground ||
+ !IsRealObjectMode ||
+ _pathPoints == null ||
+ _pathPoints.Count < 2)
+ {
+ return false;
+ }
+
+ var hostType = CoordinateSystemManager.Instance.ResolvedType;
+ var hostPoints = new List(_pathPoints.Count);
+ for (int i = 0; i < _pathPoints.Count; i++)
+ {
+ hostPoints.Add(new Vector3((float)_pathPoints[i].X, (float)_pathPoints[i].Y, (float)_pathPoints[i].Z));
+ }
+
+ return PathTargetFrameResolver.TryResolvePlanarStartHostYaw(_route.PathType, hostPoints, hostType, out yawRadians);
+ }
+
+ private bool TryCreateGroundRealObjectConstrainedRotation(
+ Point3D previousFramePoint,
+ Point3D currentFramePoint,
+ Point3D nextFramePoint,
+ out Rotation3D rotation)
+ {
+ rotation = Rotation3D.Identity;
+ if (!IsRealObjectMode ||
+ _route?.PathType != PathType.Ground ||
+ !_hasGroundRealObjectBasePose)
+ {
+ return false;
+ }
+
+ if (!PathTargetFrameResolver.TryCreatePlanarHostFrame(
+ new Vector3(
+ (float)(nextFramePoint.X - previousFramePoint.X),
+ (float)(nextFramePoint.Y - previousFramePoint.Y),
+ (float)(nextFramePoint.Z - previousFramePoint.Z)),
+ CoordinateSystemManager.Instance.ResolvedType,
+ out var currentFrame))
+ {
+ return false;
+ }
+
+ if (!PathTargetFrameResolver.TryResolvePlanarHostYaw(
+ currentFrame.Forward,
+ CoordinateSystemManager.Instance.ResolvedType,
+ out double targetYawRadians))
+ {
+ return false;
+ }
+
+ var adapter = CoordinateSystemManager.Instance.CreateHostAdapter();
+ Vector3 hostUp = Vector3.Normalize(adapter.HostUpVector3);
+ double deltaYaw = NormalizeRadians(targetYawRadians - _groundRealObjectBaseYaw);
+ Quaternion deltaQuaternion = Quaternion.CreateFromAxisAngle(hostUp, (float)deltaYaw);
+ Quaternion baseQuaternion = Rotation3DToHostQuaternion(_groundRealObjectBaseRotation);
+ Quaternion targetQuaternion = Quaternion.Normalize(deltaQuaternion * baseQuaternion);
+ rotation = adapter.FromHostQuaternionDirect(targetQuaternion);
+ return true;
+ }
+
+ private bool TryCreateGroundRealObjectConstrainedRotationFromHostForward(
+ Vector3 hostForward,
+ out Rotation3D rotation)
+ {
+ rotation = Rotation3D.Identity;
+ if (!IsRealObjectMode ||
+ _route?.PathType != PathType.Ground ||
+ !_hasGroundRealObjectBasePose)
+ {
+ return false;
+ }
+
+ if (!PathTargetFrameResolver.TryCreatePlanarHostFrame(
+ hostForward,
+ CoordinateSystemManager.Instance.ResolvedType,
+ out var currentFrame))
+ {
+ return false;
+ }
+
+ if (!PathTargetFrameResolver.TryResolvePlanarHostYaw(
+ currentFrame.Forward,
+ CoordinateSystemManager.Instance.ResolvedType,
+ out double targetYawRadians))
+ {
+ return false;
+ }
+
+ var adapter = CoordinateSystemManager.Instance.CreateHostAdapter();
+ Vector3 hostUp = Vector3.Normalize(adapter.HostUpVector3);
+ double deltaYaw = NormalizeRadians(targetYawRadians - _groundRealObjectBaseYaw);
+ Quaternion deltaQuaternion = Quaternion.CreateFromAxisAngle(hostUp, (float)deltaYaw);
+ Quaternion baseQuaternion = Rotation3DToHostQuaternion(_groundRealObjectBaseRotation);
+ Quaternion targetQuaternion = Quaternion.Normalize(deltaQuaternion * baseQuaternion);
+ rotation = adapter.FromHostQuaternionDirect(targetQuaternion);
+ return true;
+ }
+
+ 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 bool TryRotateGroundRealObjectCompensation(Rotation3D targetRotation, out Vector3D rotatedCompensation)
+ {
+ rotatedCompensation = _groundRealObjectStartCompensation;
+ if (!_hasGroundRealObjectBasePose || !_hasGroundRealObjectStartCompensation)
+ {
+ return false;
+ }
+
+ Quaternion baseQuaternion = Rotation3DToHostQuaternion(_groundRealObjectBaseRotation);
+ Quaternion targetQuaternion = Rotation3DToHostQuaternion(targetRotation);
+ Quaternion deltaQuaternion = Quaternion.Normalize(targetQuaternion * Quaternion.Inverse(baseQuaternion));
+ Vector3 baseCompensation = new Vector3(
+ (float)_groundRealObjectStartCompensation.X,
+ (float)_groundRealObjectStartCompensation.Y,
+ (float)_groundRealObjectStartCompensation.Z);
+ Vector3 rotatedVector = Vector3.Transform(baseCompensation, deltaQuaternion);
+ rotatedCompensation = new Vector3D(rotatedVector.X, rotatedVector.Y, rotatedVector.Z);
+ return true;
+ }
+
+ private static double NormalizeRadians(double angle)
+ {
+ while (angle > Math.PI)
+ {
+ angle -= 2.0 * Math.PI;
+ }
+
+ while (angle < -Math.PI)
+ {
+ angle += 2.0 * Math.PI;
+ }
+
+ return angle;
+ }
+
private static Vector3 ToNumerics(Point3D point)
{
return new Vector3((float)point.X, (float)point.Y, (float)point.Z);
@@ -3672,12 +3960,66 @@ namespace NavisworksTransport.Core.Animation
hostForward = ToNumerics(nextPoint) - ToNumerics(currentPoint);
}
+ return TryCalculateCurrentRealObjectPlanarProjectedExtents(
+ hostForward,
+ out convention,
+ out extents);
+ }
+
+ private bool TryCalculateCurrentRealObjectPlanarProjectedExtents(
+ Vector3 hostForward,
+ out ModelAxisConvention convention,
+ out (double forwardExtent, double sideExtent, double upExtent) extents)
+ {
+ convention = null;
+ extents = (0.0, 0.0, 0.0);
+
+ if (!IsRealObjectMode ||
+ !_hasRealObjectReferenceRotation ||
+ _realObjectLength <= 0.0 ||
+ _realObjectWidth <= 0.0 ||
+ _realObjectHeight <= 0.0)
+ {
+ return false;
+ }
+
var adapter = CoordinateSystemManager.Instance.CreateHostAdapter();
if (!PathTargetFrameResolver.TryCreatePlanarHostFrame(hostForward, adapter.HostType, out var targetFrame))
{
return false;
}
+ if (_route?.PathType == PathType.Ground &&
+ _hasGroundRealObjectBasePose &&
+ TryCreateGroundRealObjectConstrainedRotationFromHostForward(hostForward, out var constrainedRotation))
+ {
+ if (!TryCreateRealObjectPlanarPoseSolution(
+ targetFrame.Forward,
+ targetFrame.Up,
+ out var constrainedSolution))
+ {
+ return false;
+ }
+
+ LocalAxisDirection constrainedSemanticUpAxis =
+ adapter.HostType == CoordinateSystemType.YUp
+ ? LocalAxisDirection.PositiveY
+ : LocalAxisDirection.PositiveZ;
+ convention = new ModelAxisConvention(constrainedSolution.SelectedReferenceAxisDirection, constrainedSemanticUpAxis);
+
+ Quaternion constrainedFinalHostQuaternion = Rotation3DToHostQuaternion(constrainedRotation);
+ extents = RealObjectProjectedExtentResolver.CalculateProjectedSemanticExtents(
+ convention,
+ _realObjectLength,
+ _realObjectWidth,
+ _realObjectHeight,
+ constrainedSolution.BaselineRotation,
+ constrainedFinalHostQuaternion,
+ targetFrame.Forward,
+ targetFrame.Up);
+ return true;
+ }
+
if (!TryCreateRealObjectPlanarPoseSolution(
targetFrame.Forward,
targetFrame.Up,
@@ -3724,14 +4066,16 @@ namespace NavisworksTransport.Core.Animation
return false;
}
- Point3D previousPoint = _pathPoints[0];
- Point3D currentPoint = _pathPoints[0];
- Point3D nextPoint = _pathPoints.Count > 1 ? _pathPoints[1] : _pathPoints[0];
+ if (!PathTargetFrameResolver.TryResolvePlanarStartHostForward(
+ _route.PathType,
+ _pathPoints,
+ out var hostForward))
+ {
+ return false;
+ }
if (!TryCalculateCurrentRealObjectPlanarProjectedExtents(
- previousPoint,
- currentPoint,
- nextPoint,
+ hostForward,
out _,
out var extents))
{
@@ -3858,22 +4202,17 @@ namespace NavisworksTransport.Core.Animation
return false;
}
- if (_route?.PathType == PathType.Hoisting)
+ if (PathTargetFrameResolver.TryResolvePlanarStartHostForward(
+ _route?.PathType ?? PathType.Ground,
+ _pathPoints,
+ out var hostForward))
{
- if (_pathPoints.Count < 3)
- {
- return false;
- }
-
return TryCreatePlanarPathRotationFromHostForward(
- new Vector3D(
- _pathPoints[2].X - _pathPoints[1].X,
- _pathPoints[2].Y - _pathPoints[1].Y,
- _pathPoints[2].Z - _pathPoints[1].Z),
+ new Vector3D(hostForward.X, hostForward.Y, hostForward.Z),
out rotation);
}
- return TryCreatePlanarPathRotationFromHostPoints(_pathPoints[0], _pathPoints[0], _pathPoints[1], out rotation);
+ return false;
}
private bool TryCreatePlanarPathRotationForFrame(Point3D previousPoint, Point3D currentPoint, Point3D nextPoint, out Rotation3D rotation)
@@ -4043,10 +4382,11 @@ namespace NavisworksTransport.Core.Animation
}
LocalAxisDirection? fixedForwardAxis = null;
- if ((_route?.PathType == PathType.Ground || _route?.PathType == PathType.Hoisting) &&
- _hasRealObjectPlanarSelectedForwardAxis)
+ if (_route?.PathType == PathType.Ground || _route?.PathType == PathType.Hoisting)
{
- fixedForwardAxis = _realObjectPlanarSelectedForwardAxis;
+ // Ground/Hoisting 的真实物体前进轴采用固定对象语义:
+ // 真实物体默认以本地 +X 表示前进方向,不能因为路径更偏向 Z 就切换成 +Z。
+ fixedForwardAxis = LocalAxisDirection.PositiveX;
}
if (!RealObjectPlanarPoseSolver.TryCreatePlanarPoseFromReferencePose(
@@ -4060,10 +4400,11 @@ namespace NavisworksTransport.Core.Animation
}
if ((_route?.PathType == PathType.Ground || _route?.PathType == PathType.Hoisting) &&
- !_hasRealObjectPlanarSelectedForwardAxis)
+ (!_hasRealObjectPlanarSelectedForwardAxis || _realObjectPlanarSelectedForwardAxis != LocalAxisDirection.PositiveX))
{
- _realObjectPlanarSelectedForwardAxis = solution.SelectedReferenceAxisDirection;
+ _realObjectPlanarSelectedForwardAxis = LocalAxisDirection.PositiveX;
_hasRealObjectPlanarSelectedForwardAxis = true;
+ LogManager.Info("[真实物体平面前进轴] Ground/Hoisting 已固定使用 PositiveX 作为对象前进轴语义。");
}
return true;
@@ -5292,3 +5633,5 @@ namespace NavisworksTransport.Core.Animation
#endregion
}
}
+
+
diff --git a/src/Core/VirtualObjectManager.cs b/src/Core/VirtualObjectManager.cs
index dc9ae85..f84e1c6 100644
--- a/src/Core/VirtualObjectManager.cs
+++ b/src/Core/VirtualObjectManager.cs
@@ -236,19 +236,8 @@ namespace NavisworksTransport.Core
{
var doc = Application.ActiveDocument;
var modelItems = new ModelItemCollection { _virtualObjectModelItem };
- var currentTransform = _virtualObjectModelItem.Transform;
- var currentComponents = currentTransform.Factor();
- var currentScale = currentComponents.Scale;
-
doc.Models.ResetPermanentTransform(modelItems);
-
- var identity = Transform3D.CreateTranslation(new Vector3D(0, 0, 0));
- var components = identity.Factor();
- components.Scale = currentScale;
- Transform3D newTransform = components.Combine();
- doc.Models.OverridePermanentTransform(modelItems, newTransform, false);
-
- LogManager.Info("虚拟物体已恢复到CAD原始位置");
+ LogManager.Info("虚拟物体已恢复到CAD原始位置(仅清除Item姿态,保留Model尺寸)");
}
catch (Exception ex)
{
@@ -271,32 +260,8 @@ namespace NavisworksTransport.Core
_currentLengthMeters = lengthMeters;
_currentWidthMeters = widthMeters;
_currentHeightMeters = heightMeters;
-
- // 应用新缩放
ScaleVirtualObject(lengthMeters, widthMeters, heightMeters);
-
- var currentTransform = _virtualObjectModelItem.Transform;
- var currentComponents = currentTransform.Factor();
- var currentTranslation = currentComponents.Translation;
- var currentRotation = currentComponents.Rotation;
-
- bool hasTranslation = currentTranslation.X != 0 || currentTranslation.Y != 0 || currentTranslation.Z != 0;
- var rotationResult = currentRotation.ToAxisAndAngle();
- bool hasRotation = Math.Abs(rotationResult.Angle) > 0.001;
- if (hasTranslation || hasRotation)
- {
- var newTransform = _virtualObjectModelItem.Transform;
- var newComponents = newTransform.Factor();
- newComponents.Translation = currentTranslation;
- newComponents.Rotation = currentRotation;
-
- Transform3D combinedTransform = newComponents.Combine();
- var doc = Application.ActiveDocument;
- var modelItems = new ModelItemCollection { _virtualObjectModelItem };
- doc.Models.SetModelUnitsAndTransform(_virtualObjectModel, _virtualObjectModel.Units, combinedTransform, false);
- }
-
- LogManager.Info($"虚拟物体尺寸已更新");
+ LogManager.Info("虚拟物体尺寸已更新(仅更新Model尺寸)");
}
finally
{
@@ -399,8 +364,7 @@ namespace NavisworksTransport.Core
string[] possiblePaths = new[]
{
Path.Combine(pluginDir, "resources", "unit_cube.nwc"),
- Path.Combine(pluginDir, "..", "..", "resources", "unit_cube.nwc"),
- @"c:\Users\Tellme\apps\NavisworksTransport\resources\unit_cube.nwc"
+ Path.Combine(pluginDir, "..", "..", "resources", "unit_cube.nwc")
};
foreach (var path in possiblePaths)
diff --git a/src/UI/WPF/ViewModels/PathEditingViewModel.cs b/src/UI/WPF/ViewModels/PathEditingViewModel.cs
index 05e2cd6..ef3b6e3 100644
--- a/src/UI/WPF/ViewModels/PathEditingViewModel.cs
+++ b/src/UI/WPF/ViewModels/PathEditingViewModel.cs
@@ -5188,11 +5188,49 @@ namespace NavisworksTransport.UI.WPF.ViewModels
}
else if (animationVm.SelectedAnimatedObject != null)
{
- // 使用选择物体时保存的原始尺寸(米),不受旋转影响
- objectLengthModel = animationVm.ObjectOriginalLength * metersToUnits;
- objectWidthModel = animationVm.ObjectOriginalWidth * metersToUnits;
- objectHeightModel = animationVm.ObjectOriginalHeight * metersToUnits;
- LogManager.Debug($"[物体参数同步] 使用选择物体保存的原始尺寸: {animationVm.ObjectOriginalLength:F2}m x {animationVm.ObjectOriginalWidth:F2}m x {animationVm.ObjectOriginalHeight:F2}m");
+ var animationManager = NavisworksTransport.Core.Animation.PathAnimationManager.GetInstance();
+ if ((pathType == PathType.Ground || pathType == PathType.Hoisting) &&
+ animationManager != null &&
+ animationManager.TryGetCurrentRouteRealObjectPlanarProjectedExtents(
+ out double resolvedPlanarForward,
+ out double resolvedPlanarSide,
+ out double resolvedPlanarUp))
+ {
+ objectLengthModel = resolvedPlanarForward;
+ objectWidthModel = resolvedPlanarSide;
+ objectHeightModel = resolvedPlanarUp;
+ LogManager.Debug(
+ $"[物体参数同步] 使用真实物体平面路径最终姿态尺寸: " +
+ $"沿路径={resolvedPlanarForward / metersToUnits:F2}m, " +
+ $"垂直路径={resolvedPlanarSide / metersToUnits:F2}m, " +
+ $"法线={resolvedPlanarUp / metersToUnits:F2}m");
+ }
+ else if (pathType == PathType.Rail &&
+ animationManager != null &&
+ animationManager.TryGetCurrentRouteRealObjectRailProjectedExtents(
+ out double resolvedRailForward,
+ out double resolvedRailSide,
+ out double resolvedRailUp))
+ {
+ objectLengthModel = resolvedRailForward;
+ objectWidthModel = resolvedRailSide;
+ objectHeightModel = resolvedRailUp;
+ LogManager.Debug(
+ $"[物体参数同步] 使用真实物体 Rail 最终姿态尺寸: " +
+ $"沿路径={resolvedRailForward / metersToUnits:F2}m, " +
+ $"垂直路径={resolvedRailSide / metersToUnits:F2}m, " +
+ $"法线={resolvedRailUp / metersToUnits:F2}m");
+ }
+ else
+ {
+ // 回退:使用选择物体时保存的原始尺寸(米)
+ objectLengthModel = animationVm.ObjectOriginalLength * metersToUnits;
+ objectWidthModel = animationVm.ObjectOriginalWidth * metersToUnits;
+ objectHeightModel = animationVm.ObjectOriginalHeight * metersToUnits;
+ LogManager.Debug(
+ $"[物体参数同步] 回退使用选择物体保存的原始尺寸: " +
+ $"{animationVm.ObjectOriginalLength:F2}m x {animationVm.ObjectOriginalWidth:F2}m x {animationVm.ObjectOriginalHeight:F2}m");
+ }
}
else
{
diff --git a/src/Utils/CoordinateSystem/PathTargetFrameResolver.cs b/src/Utils/CoordinateSystem/PathTargetFrameResolver.cs
index b93283c..8725d25 100644
--- a/src/Utils/CoordinateSystem/PathTargetFrameResolver.cs
+++ b/src/Utils/CoordinateSystem/PathTargetFrameResolver.cs
@@ -1,3 +1,4 @@
+using System.Collections.Generic;
using System.Numerics;
using Autodesk.Navisworks.Api;
@@ -9,6 +10,107 @@ namespace NavisworksTransport.Utils.CoordinateSystem
///
public static class PathTargetFrameResolver
{
+ public static bool TryResolvePlanarStartHostForward(
+ NavisworksTransport.PathType pathType,
+ IReadOnlyList pathPoints,
+ out Vector3 hostForward)
+ {
+ if (pathPoints == null)
+ {
+ hostForward = Vector3.Zero;
+ return false;
+ }
+
+ var hostPoints = new List(pathPoints.Count);
+ for (int i = 0; i < pathPoints.Count; i++)
+ {
+ hostPoints.Add(new Vector3(
+ (float)pathPoints[i].X,
+ (float)pathPoints[i].Y,
+ (float)pathPoints[i].Z));
+ }
+
+ return TryResolvePlanarStartHostForward(pathType, hostPoints, out hostForward);
+ }
+
+ public static bool TryResolvePlanarStartHostForward(
+ NavisworksTransport.PathType pathType,
+ IReadOnlyList pathPoints,
+ out Vector3 hostForward)
+ {
+ hostForward = Vector3.Zero;
+
+ if (pathPoints == null)
+ {
+ return false;
+ }
+
+ if (pathType == NavisworksTransport.PathType.Hoisting)
+ {
+ if (pathPoints.Count < 3)
+ {
+ return false;
+ }
+
+ hostForward = new Vector3(
+ (float)(pathPoints[2].X - pathPoints[1].X),
+ (float)(pathPoints[2].Y - pathPoints[1].Y),
+ (float)(pathPoints[2].Z - pathPoints[1].Z));
+ return hostForward.LengthSquared() >= 1e-9f;
+ }
+
+ if (pathPoints.Count < 2)
+ {
+ return false;
+ }
+
+ hostForward = new Vector3(
+ (float)(pathPoints[1].X - pathPoints[0].X),
+ (float)(pathPoints[1].Y - pathPoints[0].Y),
+ (float)(pathPoints[1].Z - pathPoints[0].Z));
+ return hostForward.LengthSquared() >= 1e-9f;
+ }
+
+ public static bool TryCreatePlanarStartHostFrame(
+ NavisworksTransport.PathType pathType,
+ IReadOnlyList pathPoints,
+ CoordinateSystemType hostType,
+ out PathTargetFrame frame)
+ {
+ if (pathPoints == null)
+ {
+ frame = null;
+ return false;
+ }
+
+ var hostPoints = new List(pathPoints.Count);
+ for (int i = 0; i < pathPoints.Count; i++)
+ {
+ hostPoints.Add(new Vector3(
+ (float)pathPoints[i].X,
+ (float)pathPoints[i].Y,
+ (float)pathPoints[i].Z));
+ }
+
+ return TryCreatePlanarStartHostFrame(pathType, hostPoints, hostType, out frame);
+ }
+
+ public static bool TryCreatePlanarStartHostFrame(
+ NavisworksTransport.PathType pathType,
+ IReadOnlyList pathPoints,
+ CoordinateSystemType hostType,
+ out PathTargetFrame frame)
+ {
+ frame = null;
+
+ if (!TryResolvePlanarStartHostForward(pathType, pathPoints, out var hostForward))
+ {
+ return false;
+ }
+
+ return TryCreatePlanarHostFrame(hostForward, hostType, out frame);
+ }
+
public static bool TryCreatePlanarHostFrame(
Vector3 hostForward,
CoordinateSystemType hostType,
@@ -28,6 +130,42 @@ namespace NavisworksTransport.Utils.CoordinateSystem
return true;
}
+ public static bool TryResolvePlanarHostYaw(
+ Vector3 hostForward,
+ CoordinateSystemType hostType,
+ out double yawRadians)
+ {
+ yawRadians = 0.0;
+
+ if (hostForward.LengthSquared() < 1e-9f)
+ {
+ return false;
+ }
+
+ var adapter = new HostCoordinateAdapter(hostType);
+ Vector3 canonicalForward = adapter.ToCanonicalVector3(hostForward);
+ canonicalForward.Z = 0f;
+ if (canonicalForward.LengthSquared() < 1e-9f)
+ {
+ return false;
+ }
+
+ canonicalForward = Vector3.Normalize(canonicalForward);
+ yawRadians = System.Math.Atan2(canonicalForward.Y, canonicalForward.X);
+ return true;
+ }
+
+ public static bool TryResolvePlanarStartHostYaw(
+ NavisworksTransport.PathType pathType,
+ IReadOnlyList pathPoints,
+ CoordinateSystemType hostType,
+ out double yawRadians)
+ {
+ yawRadians = 0.0;
+ return TryResolvePlanarStartHostForward(pathType, pathPoints, out var hostForward) &&
+ TryResolvePlanarHostYaw(hostForward, hostType, out yawRadians);
+ }
+
public static bool TryCreateRailHostFrame(
PathRoute route,
Point3D previousPoint,
From 3a5693a453deb53244a3b707350a6b4a19ae29f2 Mon Sep 17 00:00:00 2001
From: tian <11429339@qq.com>
Date: Thu, 26 Mar 2026 01:01:01 +0800
Subject: [PATCH 31/87] Add rail mount placement controls and persistence
---
NavisworksTransport.UnitTests.csproj | 1 +
TransportPlugin.csproj | 1 +
.../CanonicalRailOffsetResolverTests.cs | 21 +
.../ObjectStartPlacementRequestTests.cs | 31 +
UnitTests/Core/PathPersistenceTests.cs | 5 +-
doc/design/2026/NavisworksAPI使用方法.md | 946 ++++++------------
doc/working/current-engineering-state.md | 54 +-
src/Core/Animation/PathAnimationManager.cs | 149 ++-
src/Core/PathDataManager.cs | 10 +
src/Core/PathDatabase.cs | 17 +-
src/Core/PathPlanningManager.cs | 1 +
src/Core/PathPlanningModels.cs | 9 +
.../ViewModels/AnimationControlViewModel.cs | 44 +-
src/UI/WPF/ViewModels/PathEditingViewModel.cs | 54 +-
src/UI/WPF/Views/EditRotationWindow.xaml | 22 +-
src/UI/WPF/Views/EditRotationWindow.xaml.cs | 12 +
src/UI/WPF/Views/PathEditingView.xaml | 332 +++---
.../CanonicalRailOffsetResolver.cs | 3 +-
.../ObjectStartPlacementRequest.cs | 59 ++
src/Utils/RailPathPoseHelper.cs | 6 +-
20 files changed, 1002 insertions(+), 775 deletions(-)
create mode 100644 UnitTests/CoordinateSystem/ObjectStartPlacementRequestTests.cs
create mode 100644 src/Utils/CoordinateSystem/ObjectStartPlacementRequest.cs
diff --git a/NavisworksTransport.UnitTests.csproj b/NavisworksTransport.UnitTests.csproj
index 17555ba..30ef294 100644
--- a/NavisworksTransport.UnitTests.csproj
+++ b/NavisworksTransport.UnitTests.csproj
@@ -58,6 +58,7 @@
+
diff --git a/TransportPlugin.csproj b/TransportPlugin.csproj
index f126d00..fb6e518 100644
--- a/TransportPlugin.csproj
+++ b/TransportPlugin.csproj
@@ -336,6 +336,7 @@
+
diff --git a/UnitTests/CoordinateSystem/CanonicalRailOffsetResolverTests.cs b/UnitTests/CoordinateSystem/CanonicalRailOffsetResolverTests.cs
index ca843f2..d3091af 100644
--- a/UnitTests/CoordinateSystem/CanonicalRailOffsetResolverTests.cs
+++ b/UnitTests/CoordinateSystem/CanonicalRailOffsetResolverTests.cs
@@ -91,6 +91,27 @@ namespace NavisworksTransport.UnitTests.CoordinateSystem
AssertVector(trackedCenter, 0.0, 0.0, 2.0);
}
+ [TestMethod]
+ public void RailNormalOffset_ShouldAddExtraDisplacementAlongRailNormal()
+ {
+ PathRoute route = new PathRoute
+ {
+ PathType = PathType.Rail,
+ RailMountMode = RailMountMode.OverRail,
+ RailPathDefinitionMode = RailPathDefinitionMode.RailCenterLine,
+ RailNormalOffset = 1.5
+ };
+
+ RailLocalFrame frame = new RailLocalFrame(Vector3.UnitX, Vector3.UnitY, Vector3.UnitZ);
+ Vector3 trackedCenter = CanonicalRailOffsetResolver.ResolveTrackedCenter(
+ route,
+ Vector3.Zero,
+ frame,
+ 4.0);
+
+ AssertVector(trackedCenter, 0.0, 0.0, 3.5);
+ }
+
private static void AssertVector(Vector3 actual, double x, double y, double z)
{
Assert.AreEqual(x, actual.X, 1e-6);
diff --git a/UnitTests/CoordinateSystem/ObjectStartPlacementRequestTests.cs b/UnitTests/CoordinateSystem/ObjectStartPlacementRequestTests.cs
new file mode 100644
index 0000000..8c14ad3
--- /dev/null
+++ b/UnitTests/CoordinateSystem/ObjectStartPlacementRequestTests.cs
@@ -0,0 +1,31 @@
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using NavisworksTransport.Utils.CoordinateSystem;
+
+namespace NavisworksTransport.UnitTests.CoordinateSystem
+{
+ [TestClass]
+ public class ObjectStartPlacementRequestTests
+ {
+ [TestMethod]
+ public void TranslationOnly_ShouldPreserveInitialPoseAndClearRotationCorrection()
+ {
+ var request = ObjectStartPlacementRequest.TranslationOnly;
+
+ Assert.AreEqual(ObjectStartPlacementMode.PreserveInitialPose, request.PlacementMode);
+ Assert.IsTrue(request.PreserveInitialPose);
+ Assert.AreEqual(LocalEulerRotationCorrection.Zero, request.RotationCorrection);
+ }
+
+ [TestMethod]
+ public void RotationCorrectionRequest_ShouldKeepCorrectionAndAlignToPathPose()
+ {
+ var correction = new LocalEulerRotationCorrection(15.0, 30.0, 45.0);
+
+ var request = ObjectStartPlacementRequest.CreateRotationCorrection(correction);
+
+ Assert.AreEqual(ObjectStartPlacementMode.AlignToPathPose, request.PlacementMode);
+ Assert.IsFalse(request.PreserveInitialPose);
+ Assert.AreEqual(correction, request.RotationCorrection);
+ }
+ }
+}
diff --git a/UnitTests/Core/PathPersistenceTests.cs b/UnitTests/Core/PathPersistenceTests.cs
index fa47c56..5830002 100644
--- a/UnitTests/Core/PathPersistenceTests.cs
+++ b/UnitTests/Core/PathPersistenceTests.cs
@@ -38,6 +38,7 @@ namespace NavisworksTransport.UnitTests.Core
""pathType"": ""Rail"",
""railMountMode"": ""OverRail"",
""railPathDefinitionMode"": ""RailCenterLine"",
+ ""railNormalOffset"": 0.25,
""railPreferredNormal"": { ""x"": -0.2, ""y"": 0.5, ""z"": 0.8 },
""totalLength"": 10.0,
""objectLimits"": { ""maxLength"": 0, ""maxWidth"": 0, ""maxHeight"": 0, ""safetyMargin"": 0 },
@@ -57,6 +58,7 @@ namespace NavisworksTransport.UnitTests.Core
var importedRoutes = manager.ImportFromJson(filePath);
Assert.AreEqual(1, importedRoutes.Count);
Assert.IsNotNull(importedRoutes[0].RailPreferredNormal);
+ Assert.AreEqual(0.25, importedRoutes[0].RailNormalOffset, 1e-6);
}
finally
{
@@ -78,7 +80,7 @@ namespace NavisworksTransport.UnitTests.Core
-
+
@@ -91,6 +93,7 @@ namespace NavisworksTransport.UnitTests.Core
var importedRoutes = manager.ImportFromXml(filePath);
Assert.AreEqual(1, importedRoutes.Count);
Assert.IsNotNull(importedRoutes[0].RailPreferredNormal);
+ Assert.AreEqual(0.25, importedRoutes[0].RailNormalOffset, 1e-6);
}
finally
{
diff --git a/doc/design/2026/NavisworksAPI使用方法.md b/doc/design/2026/NavisworksAPI使用方法.md
index e70b26d..8f79a8d 100644
--- a/doc/design/2026/NavisworksAPI使用方法.md
+++ b/doc/design/2026/NavisworksAPI使用方法.md
@@ -1,4 +1,4 @@
-# Navisworks API 使用方法指南
+# Navisworks API 使用方法指南
基于真实官方示例的正确API用法总结
@@ -505,686 +505,364 @@ public void BatchNavisworksOperations(List items)
## 11. Transform 变换操作
-### 11.1 Transform 相关 API 概念
+### 11.1 官方文档结论
-**核心概念**:
+以下几条是本项目后续关于变换问题的硬基线,优先级高于历史经验和猜测:
-- `ModelItem.Transform` - 返回设计文件中的原始变换,**只读属性**,**不反映override后的状态**
-- `OverridePermanentTransform()` - 应用增量变换(相对于原始Transform累积)
-- `ResetPermanentTransform()` - 清除所有增量变换,恢复到设计文件原始位置
-- `ModelItem.BoundingBox()` - 返回**当前实际显示**的包围盒(反映override效果)
+- `ModelItem.Transform`
+ - 官方说明:`Returns the Transform attached to this item in the original source design file`
+ - 结论:它表示原始设计文件变换,不是当前显示姿态
+ - 官方页:`doc/navisworks_api/NET/documentation/NetAPIHtml/html/P_Autodesk_Navisworks_Api_ModelItem_Transform.htm`
+- `ModelGeometry.ActiveTransform`
+ - 官方说明:`Returns the currently active transform of the geometry.`
+ - 结论:它表示当前几何实际生效的变换
+ - 官方页:`doc/navisworks_api/NET/documentation/NetAPIHtml/html/P_Autodesk_Navisworks_Api_ModelGeometry_ActiveTransform.htm`
+- `DocumentModels.OverridePermanentTransform(...)`
+ - 官方说明:`Apply an incremental transform to a selection.`
+ - 结论:这是增量变换,不是把对象直接设成目标绝对姿态
+ - 官方页:`doc/navisworks_api/NET/documentation/NetAPIHtml/html/M_Autodesk_Navisworks_Api_DocumentParts_DocumentModels_OverridePermanentTransform_3_131351c5.htm`
+- `DocumentModels.ResetPermanentTransform(...)`
+ - 官方说明:`Reset incremental transforms for all model items contained in the selection.`
+ - 结论:它清掉的是永久增量层
+ - 官方页:`doc/navisworks_api/NET/documentation/NetAPIHtml/html/M_Autodesk_Navisworks_Api_DocumentParts_DocumentModels_ResetPermanentTransform_1_75193b86.htm`
+- `ModelGeometry`
+ - 官方成员表明确提供:
+ - `OriginalTransform`
+ - `PermanentOverrideTransform`
+ - `PermanentTransform`
+ - `ActiveTransform`
+ - 官方页:`doc/navisworks_api/NET/documentation/NetAPIHtml/html/AllMembers_T_Autodesk_Navisworks_Api_ModelGeometry.htm`
-**⚠️ 关键理解**:
+### 11.2 变换层级的正确理解
-1. **`ModelItem.Transform` 永远返回原始值**,即使通过 `OverridePermanentTransform` 改变了物体位置
-2. **Override 信息存储在别处**,不会修改 `ModelItem.Transform` 属性
-3. **要获取实际位置,使用 `BoundingBox().Center`**,它反映override后的实际位置
+以后项目里统一这样理解:
-### 11.2 Transform 操作的正确用法
+- `ModelItem.Transform`
+ - 原始设计文件层变换
+ - 只读
+ - 不反映 `OverridePermanentTransform` 后的当前状态
+- `ModelItem.Geometry` / `ModelItem.FindFirstGeometry()`
+ - 进入几何层的正式入口
+- `ModelGeometry.OriginalTransform`
+ - 几何加载时的原始变换
+- `ModelGeometry.PermanentOverrideTransform`
+ - 施加在几何上的永久增量覆盖层
+- `ModelGeometry.PermanentTransform`
+ - 原始变换和永久覆盖组合后的结果
+- `ModelGeometry.ActiveTransform`
+ - 当前真正生效的几何变换
+ - 一般应优先用它判断当前姿态/当前位置
+
+### 11.3 项目中的推荐读取入口
+
+#### 11.3.1 读“当前实际姿态”
+
+优先顺序:
+
+1. `ModelItem.FindFirstGeometry()` 或 `ModelItem.Geometry`
+2. `ModelGeometry.ActiveTransform`
+3. 必要时再看 `ModelGeometry.PermanentTransform`
```csharp
-// ✅ 获取物体的原始Transform(设计文件中的位置)
-Transform3D originalTransform = modelItem.Transform;
+ModelGeometry geometry = item.FindFirstGeometry();
+if (geometry != null)
+{
+ Transform3D current = geometry.ActiveTransform;
+}
+```
-// ✅ 应用增量变换(累积变换)
+#### 11.3.2 读“原始姿态”
+
+优先顺序:
+
+1. `ModelGeometry.OriginalTransform`
+2. `ModelItem.Transform`
+
+```csharp
+ModelGeometry geometry = item.FindFirstGeometry();
+Transform3D original = geometry != null ? geometry.OriginalTransform : item.Transform;
+```
+
+#### 11.3.3 读“当前实际位置”
+
+推荐顺序:
+
+1. 如果语义上需要几何真实中心,优先 `geometry.BoundingBox.Center`
+2. 如果业务上有自己定义的 tracked point,就用业务 tracked point
+3. 不要把 `ModelItem.Transform.Translation` 直接当成 override 后实际位置
+
+### 11.4 `OverridePermanentTransform` 的正确语义
+
+`OverridePermanentTransform(items, transform, updateModelTransform)` 的本质是:
+
+- 对选中的对象施加增量变换
+- 不是设置对象的最终世界姿态
+
+另外官方 Remarks 里还有一条很重要:
+
+- 如果 selection 包含文件,而且 `updateModelTransform = true`
+- 则不是对 fragments 施加变换
+- 而是更新 File Units and Transform
+
+这也是项目里必须区分的两种用法:
+
+- `updateModelTransform = false`
+ - 对 item/geometry 的永久增量层操作
+- `updateModelTransform = true`
+ - 更新 model/file 层的 units and transform
+
+### 11.5 `ResetPermanentTransform` 的正确语义
+
+`ResetPermanentTransform(items)` 只做一件事:
+
+- 清除选中对象的永久增量变换
+
+它不会修改:
+
+- 原始设计文件几何
+- `ModelItem.Transform`
+- `ModelGeometry.OriginalTransform`
+
+所以“恢复到原始状态”要明确到底指哪一层:
+
+- 对真实物体:
+ - 通常是清掉 override,回到原始几何状态
+- 对虚拟物体:
+ - 如果业务尺寸是后续叠加出来的,单纯 reset 会把业务尺寸也一起清掉
+ - 因此虚拟物体常常需要 reset 后再重放业务尺寸
+
+### 11.6 常见误区
+
+#### 11.6.1 误区:`ModelItem.Transform` 代表当前姿态
+
+错误:
+
+```csharp
+Transform3D current = item.Transform; // 这不是当前 override 后姿态
+```
+
+正确:
+
+```csharp
+ModelGeometry geometry = item.FindFirstGeometry();
+Transform3D current = geometry != null ? geometry.ActiveTransform : item.Transform;
+```
+
+#### 11.6.2 误区:`OverridePermanentTransform` 是绝对落位
+
+错误理解:
+
+- “我把目标旋转/平移直接传进去,显示结果就应该等于它”
+
+正确理解:
+
+- 这是增量层
+- 最终显示结果要结合 `OriginalTransform / PermanentTransform / ActiveTransform` 一起看
+
+#### 11.6.3 误区:只看 `Transform` 不看 `Geometry`
+
+对于很多真实物体和虚拟物体,真正决定当前显示姿态的是:
+
+- `ModelGeometry.ActiveTransform`
+- 或 fragment 层矩阵
+
+而不是:
+
+- `ModelItem.Transform`
+
+### 11.7 Fragment 与 Geometry 的关系
+
+项目里对 fragment 的定位要统一:
+
+- fragment 适合做:
+ - 真实物体参考姿态解释
+ - fragment 代表姿态统计
+ - COM 层几何分析
+- fragment 不应优先替代 `ModelGeometry.ActiveTransform` 来读取“当前显示姿态”
+
+当前结论:
+
+- 读“当前实际姿态”优先用 `ModelGeometry`
+- 读“真实物体参考姿态/原始语义姿态”时,fragment 仍然有用
+
+### 11.8 COM Fragment 变换的使用规则
+
+COM fragment 提供的是:
+
+- `GetLocalToWorldMatrix()`
+ - fragment 从本地到世界的完整矩阵
+
+项目当前实测结论:
+
+- fragment 矩阵解释必须和 `ModelGeometry.ActiveTransform` 对齐验证
+- 不要只凭经验猜行列顺序
+- 当前项目排查中已经验证过:
+ - 解释 fragment 矩阵时,必须以 `.NET ModelGeometry` 的结果为标尺
+
+### 11.9 推荐代码模式
+
+#### 11.9.1 读取当前几何姿态
+
+```csharp
+public static bool TryGetCurrentGeometryTransform(ModelItem item, out Transform3D transform)
+{
+ transform = Transform3D.Identity;
+ if (item == null)
+ {
+ return false;
+ }
+
+ ModelGeometry geometry = item.FindFirstGeometry();
+ if (geometry == null)
+ {
+ return false;
+ }
+
+ transform = geometry.ActiveTransform;
+ return true;
+}
+```
+
+#### 11.9.2 清掉增量层后恢复到原始状态
+
+```csharp
var doc = Application.ActiveDocument;
-var modelItems = new ModelItemCollection { modelItem };
-doc.Models.OverridePermanentTransform(modelItems, newTransform, false);
-
-// ✅ 重置到原始位置(清除所有增量变换)
-doc.Models.ResetPermanentTransform(modelItems);
+var items = new ModelItemCollection { item };
+doc.Models.ResetPermanentTransform(items);
```
-### 11.3 Transform 操作的关键区别
-
-| API方法 | 作用 | 使用场景 | 注意事项 |
-|---------|------|---------|---------|
-| `ModelItem.Transform` | 获取原始变换 | 记录物体初始位置 | 只读属性,返回设计文件位置 |
-| `OverridePermanentTransform()` | 应用增量变换 | 动画中移动物体 | 与现有变换累积,不是绝对位置 |
-| `ResetPermanentTransform()` | 重置到原始位置 | 清除所有移动,恢复初始状态 | 忽略所有之前的变换 |
-
-### 11.4 实际应用案例
-
-**案例1:动画系统中的Transform管理**
+#### 11.9.3 对 item 施加增量变换
```csharp
-// 动画开始时记录原始位置
-private Transform3D _originalTransform;
-
-public void StartAnimation(ModelItem animatedObject)
-{
- // 记录原始Transform
- _originalTransform = animatedObject.Transform;
-
- // 移动到路径起点(增量变换)
- var startTransform = Transform3D.CreateTranslation(startPosition);
- var modelItems = new ModelItemCollection { animatedObject };
- doc.Models.OverridePermanentTransform(modelItems, startTransform, false);
-}
-
-public void ResetAnimation()
-{
- // 动画结束后,使用原始Transform恢复位置
- var modelItems = new ModelItemCollection { _animatedObject };
- doc.Models.OverridePermanentTransform(modelItems, _originalTransform, false);
-}
+var doc = Application.ActiveDocument;
+var items = new ModelItemCollection { item };
+doc.Models.OverridePermanentTransform(items, incrementalTransform, false);
```
-**案例2:用户手动位置恢复**
+#### 11.9.4 对 model 层更新 Units and Transform
```csharp
-public void RestoreToOriginalPosition(ModelItem selectedObject)
-{
- // 不需要记录Transform,直接重置到设计文件原始位置
- var doc = Application.ActiveDocument;
- var modelItems = new ModelItemCollection { selectedObject };
-
- // 清除所有增量变换,恢复到设计文件原始位置
- doc.Models.ResetPermanentTransform(modelItems);
-}
+Document doc = Application.ActiveDocument;
+Model model = doc.Models[0];
+Transform3D oldTransform = model.Transform;
+Transform3DComponents components = oldTransform.Factor();
+
+components.Translation = new Vector3D(x, y, z);
+components.Rotation = new Rotation3D(new UnitVector3D(0, 0, 1), angleInRadians);
+components.Scale = new Vector3D(scaleX, scaleY, scaleZ);
+
+Transform3D newTransform = components.Combine();
+doc.Models.SetModelUnitsAndTransform(model, Units.Meters, newTransform, true);
```
-### 11.5 常见Transform问题和解决方案
+#### 11.9.5 矩阵语义与行列顺序速查
-**问题1:获取不到实际位置**
+这是项目后续排查姿态问题时的硬基线。
+
+##### A. `Transform3D.Linear` / `Matrix3` 的项目语义
+
+项目当前统一按下面这套语义理解:
+
+- `Transform3D.Linear` 是 3x3 线性部分
+- `Matrix3.Get(row, column)` 的参数顺序是:
+ - 先 `row`
+ - 后 `column`
+- 在项目里,`Linear` 的 **列** 表示“局部轴在世界中的方向”
+ - 第 1 列 = 本地 `X` 轴在世界中的方向
+ - 第 2 列 = 本地 `Y` 轴在世界中的方向
+ - 第 3 列 = 本地 `Z` 轴在世界中的方向
+
+也就是说:
```csharp
-// ❌ 错误:以为 Transform 反映当前位置
-var transform = item.Transform;
-// 问题:这永远返回原始Transform,即使物体已被移动
+Vector3D worldX = new Vector3D(
+ linear.Get(0, 0),
+ linear.Get(1, 0),
+ linear.Get(2, 0));
-// ✅ 正确:使用 BoundingBox 获取实际位置
-var actualCenter = item.BoundingBox().Center; // 反映override后的实际位置
+Vector3D worldY = new Vector3D(
+ linear.Get(0, 1),
+ linear.Get(1, 1),
+ linear.Get(2, 1));
+
+Vector3D worldZ = new Vector3D(
+ linear.Get(0, 2),
+ linear.Get(1, 2),
+ linear.Get(2, 2));
```
-**问题2:动画结束后位置不准确**
+##### B. `Matrix3` 构造时的顺序
+
+项目里当前按“逐行传参”构造 `Matrix3`:
```csharp
-// ✅ 动画系统应该记录原始Transform并使用增量恢复
-private Transform3D _originalTransform;
-
-// 动画开始时
-_originalTransform = animatedObject.Transform;
-
-// 动画结束时恢复
-doc.Models.OverridePermanentTransform(modelItems, _originalTransform, false);
+var linear = new Matrix3(
+ m00, m01, m02,
+ m10, m11, m12,
+ m20, m21, m22);
```
-**问题3:记录Transform但不使用**
+其中:
+
+- 第 1 行 = `(m00, m01, m02)`
+- 第 2 行 = `(m10, m11, m12)`
+- 第 3 行 = `(m20, m21, m22)`
+
+如果你的业务语义是“列 = 局部轴”,那么组矩阵时应写成:
```csharp
-// ❌ 不必要:记录Transform但使用Reset
-private Transform3D _originalTransform;
-_originalTransform = selectedItem.Transform; // 记录了但不使用
-doc.Models.ResetPermanentTransform(modelItems); // 直接重置
-
-// ✅ 简化:直接重置,无需记录
-doc.Models.ResetPermanentTransform(modelItems);
+linear = new Matrix3(
+ worldX.X, worldY.X, worldZ.X,
+ worldX.Y, worldY.Y, worldZ.Y,
+ worldX.Z, worldY.Z, worldZ.Z);
```
-### 11.6 Transform 最佳实践
+##### C. `System.Numerics.Matrix4x4` 的项目用法
-1. **选择合适的恢复方式**:
- - 动画系统:使用 `OverridePermanentTransform` + 原始Transform
- - 用户操作:使用 `ResetPermanentTransform` 直接重置
-
-2. **避免不必要的Transform记录**:
- - 如果只需要恢复到设计文件原始位置,使用 `ResetPermanentTransform`
- - 只有需要恢复到特定中间状态时才记录Transform
-
-3. **理解增量vs绝对变换**:
- - `OverridePermanentTransform` 是增量的,会与现有变换叠加
- - `ResetPermanentTransform` 是绝对的,清除所有变换
-
-4. **线程安全**:
- - 所有Transform操作都必须在主UI线程中执行
- - 使用 `Dispatcher.Invoke` 确保线程安全
-
-### 11.7 旋转操作的关键限制和解决方案 ⚠️ 重要
-
-基于实际测试验证的关键发现(2025-12-15)。
-
-#### 11.7.1 旋转中心的API限制
-
-**⚠️ 核心限制:Navisworks API的旋转总是绕世界原点(0,0,0)进行**
+项目里把 `Matrix4x4` 也按同样的“列 = 基向量”语义使用:
```csharp
-// ❌ 错误理解:以为旋转绕物体中心
-var rotation = new Transform3D(new Rotation3D(new UnitVector3D(0, 0, 1), angle));
-doc.Models.OverridePermanentTransform(modelItems, rotation, false);
-// 实际效果:物体绕世界原点(0,0,0)"公转",不是绕自己"自转"
-
-// 🔍 实际测试验证:
-// 物体在 (-2.499, -1.640, 0.500) 位置
-// 旋转45度后移动到 (-0.608, -2.927, 0.500)
-// 验证公式:x' = x*cos(45°) - y*sin(45°) = -0.607 ✓
-// y' = x*sin(45°) + y*cos(45°) = -2.927 ✓
-// 证明:旋转中心是世界原点(0,0,0),不是物体中心
+Matrix4x4 basis = new Matrix4x4(
+ worldX.X, worldY.X, worldZ.X, 0f,
+ worldX.Y, worldY.Y, worldZ.Y, 0f,
+ worldX.Z, worldY.Z, worldZ.Z, 0f,
+ 0f, 0f, 0f, 1f);
```
-**验证代码**:
+在这套写法下:
+
+- 第 1 列是局部 `X` 轴
+- 第 2 列是局部 `Y` 轴
+- 第 3 列是局部 `Z` 轴
+
+然后再用:
```csharp
-// ✅ 测试代码:证明旋转绕世界原点
-var initialCenter = item.BoundingBox().Center; // (-2.499, -1.640, 0.500)
-
-// 应用45度旋转
-var rotation = new Transform3D(new Rotation3D(new UnitVector3D(0, 0, 1), Math.PI/4));
-doc.Models.OverridePermanentTransform(modelItems, rotation, false);
-
-var afterCenter = item.BoundingBox().Center; // (-0.608, -2.927, 0.500)
-
-// 计算期望位置(绕原点旋转)
-double cos45 = Math.Cos(Math.PI/4);
-double sin45 = Math.Sin(Math.PI/4);
-double expectedX = initialCenter.X * cos45 - initialCenter.Y * sin45; // -0.607
-double expectedY = initialCenter.X * sin45 + initialCenter.Y * cos45; // -2.927
-
-// 验证:实际位置 = 期望位置(绕原点旋转)✓
+Quaternion q = Quaternion.CreateFromRotationMatrix(basis);
```
-#### 11.7.2 Transform3DComponents 的行为
+##### D. 最容易犯错的地方
-**关键理解:`Transform3DComponents.Combine()` 的变换顺序**
+1. 把“列是局部轴”误看成“行是局部轴”
+2. 读取 `linear.Get(0, 1)` 时,以为拿到的是 `Y.X`,但后续又按行语义消费
+3. 把 `Matrix3` 直接喂给手写 quaternion 公式时,没有先确认公式使用的是“行主序矩阵”还是“列向量基矩阵”
+4. 日志里看到 `X=(...), Y=(...), Z=(...)` 时,没有先确认它打印的是“列”还是“行”
-```csharp
-// Transform3DComponents.Combine() 应用顺序:
-// 1. Scale(缩放)
-// 2. Rotation(旋转,绕原点)
-// 3. Translation(平移)
+##### E. 项目中的统一建议
-// ❌ 错误:直接设置rotation和translation
-var components = identity.Factor();
-components.Rotation = new Rotation3D(new UnitVector3D(0, 0, 1), deltaYaw);
-components.Translation = deltaPos; // 平移在旋转之后应用
-var transform = components.Combine();
+1. 只要是在解释物体三轴,就统一按“列 = 局部轴在世界中的方向”
+2. `Matrix3 -> Quaternion` 优先使用 `Matrix4x4 + Quaternion.CreateFromRotationMatrix(...)`
+3. 不要在不同文件里混用两套相反的行列语义
+4. 新增日志时,明确写出“这里打印的是列向量/局部轴”,不要只写 `X/Y/Z`
-// 问题:物体先绕原点旋转(产生位置偏移),然后平移
-// 结果:物体"公转"到错误位置
-```
-
-#### 11.7.2.1 Rotation3D 构造函数的真实语义(2026-03-19 新增,重要)
-
-这次在“直线装配参考杆”功能中,验证了一个很容易误用的点:
-
-- `Rotation3D(UnitVector3D, UnitVector3D, UnitVector3D)` **不是**“传入本地 X/Y/Z 三个轴来直接构造三维姿态”
-- 之前如果把它当成“三轴姿态构造器”使用,会得到完全错误的旋转结果
-
-根据 Navisworks .NET API HTML 文档:
-
-- `Rotation3D(UnitVector3D vector1, UnitVector3D vector2)`
- - 文档摘要:`Creates rotation that rotates vector1 to same direction as vector2`
- - 正确语义:**将向量1旋转到与向量2同方向**
-- `Rotation3D(UnitVector3D axis, double angle)`
- - 文档摘要:`Creates rotation about given axis by angle in radians`
- - 正确语义:**绕指定轴按弧度旋转**
-- `Rotation3D(UnitVector3D axis, UnitVector3D vector1, UnitVector3D vector2)`
- - 文档摘要:`Creates rotation about axis by angle between v1 and v2 projected onto plane normal to axis.`
- - 正确语义:**绕 axis 旋转,旋转角由 vector1 和 vector2 在垂直于 axis 的平面上的投影夹角决定**
-
-**结论:**
-
-- 3 参数版本是“轴 + 两个参考向量”的构造,不是“三个基向量/三个坐标轴”
-- 如果需求是“让物体本地某个轴对齐任意空间方向”,优先使用 **2 参数版本**
-
-#### 11.7.2.1.1 Rotation3D 四元数分量顺序的实测结论(2026-03-20 新增,重要)
-
-在 Rail 路径三维姿态调试中,对 `Rotation3D(Double, Double, Double, Double)` 和 `Rotation3D.A/B/C/D` 做了直接实测。
-
-**结论:**
-
-- `Rotation3D(a, b, c, d)` 的 4 个参数顺序是:**`x, y, z, w`**
-- 对应属性:
- - `A = quaternion.x`
- - `B = quaternion.y`
- - `C = quaternion.z`
- - `D = quaternion.w`
-
-实测验证方式:
-
-```csharp
-var rx = new Rotation3D(new UnitVector3D(1, 0, 0), Math.PI / 2.0);
-var ry = new Rotation3D(new UnitVector3D(0, 1, 0), Math.PI / 2.0);
-var rz = new Rotation3D(new UnitVector3D(0, 0, 1), Math.PI / 2.0);
-```
-
-日志结果:
-
-- `X90`: `A=0.707107, B=0.000000, C=0.000000, D=0.707107`
-- `Y90`: `A=0.000000, B=0.707107, C=0.000000, D=0.707107`
-- `Z90`: `A=0.000000, B=0.000000, C=0.707107, D=0.707107`
-
-并且将 `A/B/C/D` 原样重新传回:
-
-```csharp
-var reconstructed = new Rotation3D(rotation.A, rotation.B, rotation.C, rotation.D);
-var linear = new Transform3D(reconstructed).Linear;
-```
-
-读回的线性矩阵与对应的 `X90 / Y90 / Z90` 旋转完全一致。
-
-**因此:**
-
-- 当你已经算出了目标姿态的 quaternion `(qx, qy, qz, qw)` 时,正确写法是:
-
-```csharp
-var rotation = new Rotation3D(qx, qy, qz, qw);
-```
-
-- **不要**写成:
-
-```csharp
-var rotation = new Rotation3D(qw, qx, qy, qz); // ❌ 错误
-```
-
-这个结论已经在 Rail 三维姿态、参考杆三维定向的实际运行中验证通过。
-
-**项目级硬约束:**
-
-- 后续任何代码和文档中,都不允许再重新解释 `Rotation3D` 的四元数分量顺序
-- 遇到姿态问题时,不要再把问题归因到 `Rotation3D(x,y,z,w)` 是否可能变成了别的顺序
-- 如果真实物体或 fragment 参考姿态出现轴翻转/符号错误,应优先检查:
- - fragment 三轴的业务语义解释
- - 宿主坐标系 / 内部坐标系 / 资产坐标系 的边界
- - 是否应该传递显式参考轴,而不是只传 quaternion
-
-#### 11.7.2.2 三维参考杆/任意向量对齐的正确用法
-
-适用场景:
-
-- 参考杆、箭头、圆柱体等“有明确本地长度轴”的资源
-- 需要将资源的本地某个轴,对齐到一个任意空间向量
-
-例如:
-
-- 资源中心在原点
-- 长轴沿本地 `+X`
-- 目标方向是参考线方向 `referenceDirection`
-
-那么旋转应直接写成:
-
-```csharp
-var rotation = new Rotation3D(
- new UnitVector3D(1, 0, 0),
- new UnitVector3D(referenceDirection));
-```
-
-其含义非常直接:
-
-- 把资源的本地 `+X` 轴
-- 旋转到 `referenceDirection`
-
-这正是“直线装配参考杆”最终验证通过的做法。
-
-**不要写成下面这样:**
-
-```csharp
-// ❌ 错误:把三个向量误当成 X/Y/Z 三个姿态轴
-var rotation = new Rotation3D(
- new UnitVector3D(tangent),
- new UnitVector3D(lateral),
- new UnitVector3D(normal));
-```
-
-这段代码的真实语义并不是“建立一个由 tangent/lateral/normal 组成的姿态”,
-而是“绕 tangent 这个轴,根据 lateral 和 normal 的投影夹角去旋转”,
-所以结果通常会完全错误。
-
-#### 11.7.2.3 三维旋转与平移补偿必须分开理解
-
-即使 `Rotation3D(vector1, vector2)` 构造正确,仍然不能忽略 Navisworks 的旋转中心限制:
-
-- `OverridePermanentTransform()` 仍然是**增量变换**
-- 旋转仍然是**绕世界原点**
-- 因此三维姿态正确后,位置仍需要像 yaw 场景一样做补偿
-
-对于“资源中心在原点、目标中心为 `targetCenter`”的对象,推荐模式是:
-
-```csharp
-// 1. 从资源原始状态出发
-doc.Models.ResetPermanentTransform(modelItems);
-
-// 2. 计算旋转:本地 X -> 目标方向
-var rotation = new Rotation3D(
- new UnitVector3D(1, 0, 0),
- new UnitVector3D(referenceDirection));
-
-// 3. 计算原始中心绕世界原点旋转后的位置
-var originalCenter = item.BoundingBox().Center;
-var rotationTransform = new Transform3D(rotation);
-var linear = rotationTransform.Linear;
-var rotatedCenter = new Point3D(
- linear.Get(0, 0) * originalCenter.X + linear.Get(0, 1) * originalCenter.Y + linear.Get(0, 2) * originalCenter.Z,
- linear.Get(1, 0) * originalCenter.X + linear.Get(1, 1) * originalCenter.Y + linear.Get(1, 2) * originalCenter.Z,
- linear.Get(2, 0) * originalCenter.X + linear.Get(2, 1) * originalCenter.Y + linear.Get(2, 2) * originalCenter.Z);
-
-// 4. 用补偿平移把旋转后的中心送到目标中心
-var translation = new Vector3D(
- targetCenter.X - rotatedCenter.X,
- targetCenter.Y - rotatedCenter.Y,
- targetCenter.Z - rotatedCenter.Z);
-
-// 5. 组合并应用增量变换
-var transform = new Transform3D(rotation, translation);
-doc.Models.OverridePermanentTransform(modelItems, transform, false);
-```
-
-**关键原则:**
-
-- 先把“旋转方向”算对
-- 再按“绕原点旋转后的实际位置”计算平移补偿
-- 不要把“姿态构造错误”和“旋转中心补偿缺失”混成一个问题
-
-#### 11.7.2.4 这类问题的调试建议
-
-当三维物体“位置大概对,但方向明显错”时,优先检查:
-
-1. 是否误用了 `Rotation3D(UnitVector3D, UnitVector3D, UnitVector3D)`
-2. 资源文件的本地长度轴是否真的与你的代码约定一致
-3. 是否把 `ModelItem.Transform` 错当成 override 后的真实世界姿态
-
-推荐观察项:
-
-- 参考线/目标向量本身是否正确
-- 资源本地长度轴约定(例如是否沿本地 `+X`)
-- 当前实际显示位置,优先看 `BoundingBox()`,不要只看 `ModelItem.Transform`
-
-#### 11.7.2.5 真实模型做三维旋转时,推荐使用“先移到原点,再旋转,再移到目标点”(2026-03-20 新增,重要)
-
-在 Rail 路径真实模型调试中,一个非常关键的经验是:
-
-- **不要**只在公式层面“猜一个补偿平移”
-- 对真实模型做完整三维姿态时,更稳的做法是显式拆成三步:
-
-```csharp
-// 1. 把当前参考点移到原点
-doc.Models.OverridePermanentTransform(
- modelItems,
- Transform3D.CreateTranslation(new Vector3D(-currentPoint.X, -currentPoint.Y, -currentPoint.Z)),
- false);
-
-// 2. 在原点执行三维旋转
-var rotationComponents = Transform3D.CreateTranslation(new Vector3D(0, 0, 0)).Factor();
-rotationComponents.Rotation = deltaRotation;
-doc.Models.OverridePermanentTransform(modelItems, rotationComponents.Combine(), false);
-
-// 3. 再把参考点移到目标位置
-doc.Models.OverridePermanentTransform(
- modelItems,
- Transform3D.CreateTranslation(new Vector3D(targetPoint.X, targetPoint.Y, targetPoint.Z)),
- false);
-```
-
-**适用场景:**
-
-- 真实模型不是“原点资源”,本身已经位于世界坐标某个远离原点的位置
-- 需要做完整三维旋转(不只是 yaw)
-- 目标是让物体绕某个固定参考点“自转”,而不是带着原始世界位置绕原点“公转”
-
-**核心理解:**
-
-- Navisworks 的旋转仍然是绕世界原点
-- 所以如果想绕“某个固定参考点”旋转,就先把这个参考点移到原点
-- 这个固定参考点不一定必须是中心,也可以是底面中心、安装点或其他局部固定点
-- 但在复杂三维姿态下,**几何中心通常是最稳定、最容易验证的参考点**
-
-**这条原则解决的问题:**
-
-- 虚拟物体因为本来就接近“原点资源”,旋转通常更容易成功
-- 真实模型自带原始世界位置时,如果直接旋转,很容易出现“方向对了,但位置跑很远”
-- 将参考点显式移到原点后再旋转,可以显著降低三维平移补偿出错的概率
-
-#### 11.7.3 正确实现"绕物体中心旋转"
-
-**解决方案:手动计算旋转导致的位置偏移并补偿**
-
-```csharp
-// ✅ 正确方法:计算补偿平移量
-private void UpdateObjectPosition(Point3D newPosition, double newYaw)
-{
- var doc = Application.ActiveDocument;
- var modelItems = new ModelItemCollection { _animatedObject };
-
- // 计算旋转和平移增量
- var deltaPos = new Vector3D(
- newPosition.X - _currentPosition.X,
- newPosition.Y - _currentPosition.Y,
- newPosition.Z - _currentPosition.Z
- );
-
- Transform3D incrementalTransform;
-
- if (!double.IsNaN(newYaw))
- {
- double deltaYaw = newYaw - _currentYaw;
-
- // 🎯 关键:计算绕当前位置旋转的等效变换
- // 1. 如果绕原点旋转deltaYaw,当前位置会移到哪里?
- double cos = Math.Cos(deltaYaw);
- double sin = Math.Sin(deltaYaw);
- double rotatedX = _currentPosition.X * cos - _currentPosition.Y * sin;
- double rotatedY = _currentPosition.X * sin + _currentPosition.Y * cos;
-
- // 2. 我们希望物体绕自己旋转,位置移动到newPosition
- // 所以需要的平移 = newPosition - (旋转后的位置)
- var compensatedTranslation = new Vector3D(
- newPosition.X - rotatedX, // 补偿X方向的偏移
- newPosition.Y - rotatedY, // 补偿Y方向的偏移
- newPosition.Z - _currentPosition.Z // Z保持增量
- );
-
- // 3. 组合:先旋转(绕原点),再平移(补偿+目标位置)
- var identity = Transform3D.CreateTranslation(new Vector3D(0, 0, 0));
- var components = identity.Factor();
- components.Rotation = new Rotation3D(new UnitVector3D(0, 0, 1), deltaYaw);
- components.Translation = compensatedTranslation; // 关键:使用补偿后的平移
-
- incrementalTransform = components.Combine();
- _currentYaw = newYaw;
- }
- else
- {
- // 纯平移:直接使用增量
- incrementalTransform = Transform3D.CreateTranslation(deltaPos);
- }
-
- // 应用增量变换
- doc.Models.OverridePermanentTransform(modelItems, incrementalTransform, false);
- _currentPosition = newPosition;
-}
-```
-
-**原理说明**:
-
-```
-API限制:
- 旋转 → 物体绕(0,0,0)旋转 → 位置从P1偏移到P2
-
-我们需要的效果:
- 旋转 → 物体绕自己旋转 → 位置从P1移动到P_target
-
-解决方案:
- 补偿平移 = P_target - P2
- 最终变换 = Rotation(deltaYaw) + Translation(P_target - P2)
-
-结果:
- 物体先绕原点旋转到P2,然后平移到P_target
- 看起来像是绕自己旋转并移动到目标位置
-```
-
-#### 11.7.4 初始化问题
-
-**⚠️ 重要:初始化yaw必须与第一帧匹配**
-
-```csharp
-// ❌ 错误:初始化为0
-_currentYaw = 0.0;
-// 第一帧调用UpdateObjectPosition时:
-// deltaYaw = firstFrame.YawRadians - 0.0 // 产生大的旋转增量
-// 导致物体从起点"公转"飞走
-
-// ✅ 正确:初始化为第一帧的yaw
-if (_animationFrames != null && _animationFrames.Count > 0)
-{
- _currentYaw = _animationFrames[0].YawRadians; // 使deltaYaw=0
-
- // 第一次调用UpdateObjectPosition
- var firstFrame = _animationFrames[0];
- UpdateObjectPosition(firstFrame.Position, firstFrame.YawRadians);
- // 此时:deltaYaw = firstFrame.YawRadians - firstFrame.YawRadians = 0
- // 结果:只有平移,没有旋转偏移
-}
-```
-
-#### 11.7.5 相关API限制说明
-
-Autodesk官方论坛已确认的限制(Issue NW-53280):
-
-- **无法设置旋转中心点**:API不提供指定旋转中心的方法
-- **UI的Override Transform功能**:也是通过计算补偿实现的
-- **建议的解决方案**:手动计算T(center) × R × T(-center)的等效变换
-
-#### 11.7.6 旋转操作最佳实践
-
-| 场景 | 方法 | 注意事项 |
-|------|------|---------|
-| 简单旋转(原地) | 使用位置补偿公式 | 必须计算旋转导致的偏移 |
-| 旋转+移动 | 组合补偿平移和目标平移 | 理解Combine()的变换顺序 |
-| 动画初始化 | `_currentYaw = firstFrame.YawRadians` | 避免第一帧产生旋转增量 |
-| 调试验证 | 测试物体远离原点的情况 | 原点附近可能掩盖问题 |
-
-#### 11.7.7 关键原则:移动物体前必须先重置到CAD位置 ⚠️ 重要
-
-**问题场景**:
-当物体已经被移动过(如动画结束在终点位置),再次移动时如果直接从当前位置计算增量,会导致错误的结果。
-
-**原因**:
-`OverridePermanentTransform` 的增量是相对于**CAD原始位置**的,不是相对于当前位置。
-
-**❌ 错误做法**:
-```csharp
-// 物体当前在终点位置,但我们要移动到另一个位置
-var currentPos = item.BoundingBox().Center; // 终点位置
-var deltaPos = new Vector3D(
- targetPos.X - currentPos.X, // 从终点计算增量 - 错误!
- targetPos.Y - currentPos.Y,
- targetPos.Z - currentPos.Z
-);
-var transform = Transform3D.CreateTranslation(deltaPos);
-doc.Models.OverridePermanentTransform(modelItems, transform, false);
-// 结果:物体会移动到错误位置(因为增量是相对于CAD位置的)
-```
-
-**✅ 正确做法**:
-```csharp
-// 1. 先重置到CAD原始位置
-doc.Models.ResetPermanentTransform(modelItems);
-
-// 2. 从CAD原始位置计算到目标位置的增量
-var originalBounds = item.BoundingBox();
-var originalPos = new Point3D(
- originalBounds.Center.X,
- originalBounds.Center.Y,
- originalBounds.Min.Z
-);
-var deltaPos = new Vector3D(
- targetPos.X - originalPos.X, // 从CAD位置计算增量 - 正确!
- targetPos.Y - originalPos.Y,
- targetPos.Z - originalPos.Z
-);
-
-// 3. 应用变换
-var transform = Transform3D.CreateTranslation(deltaPos);
-doc.Models.OverridePermanentTransform(modelItems, transform, false);
-```
-
-**使用场景**:
-- 碰撞报告还原物体到碰撞位置
-- 手动指定物体位置
-- 任何需要精确控制物体最终位置的操作
-
-**最佳实践**:
-```csharp
-///
-/// 将物体移动到指定位置和朝向(先回到CAD原始位置)
-///
-public static void MoveItemToPositionAndYaw(ModelItem item, Point3D targetPosition, double targetYaw)
-{
- var doc = Application.ActiveDocument;
- var modelItems = new ModelItemCollection { item };
-
- // 🔥 关键:先回到CAD原始位置
- doc.Models.ResetPermanentTransform(modelItems);
-
- // 获取CAD原始状态
- var originalBounds = item.BoundingBox();
- var originalGroundPos = new Point3D(
- originalBounds.Center.X,
- originalBounds.Center.Y,
- originalBounds.Min.Z
- );
- var originalYaw = GetYawFromTransform(item.Transform);
-
- // 计算从CAD位置到目标位置的增量
- var deltaPos = new Vector3D(
- targetPosition.X - originalGroundPos.X,
- targetPosition.Y - originalGroundPos.Y,
- targetPosition.Z - originalGroundPos.Z
- );
- double deltaYaw = targetYaw - originalYaw;
-
- // 应用增量变换(包含旋转补偿)
- Transform3D transform;
- if (Math.Abs(deltaYaw) > 0.001)
- {
- // 计算旋转补偿
- double cos = Math.Cos(deltaYaw);
- double sin = Math.Sin(deltaYaw);
- double rotatedX = originalGroundPos.X * cos - originalGroundPos.Y * sin;
- double rotatedY = originalGroundPos.X * sin + originalGroundPos.Y * cos;
-
- var compensatedTranslation = new Vector3D(
- targetPosition.X - rotatedX,
- targetPosition.Y - rotatedY,
- deltaPos.Z
- );
-
- var identity = Transform3D.CreateTranslation(new Vector3D(0, 0, 0));
- var components = identity.Factor();
- components.Rotation = new Rotation3D(new UnitVector3D(0, 0, 1), deltaYaw);
- components.Translation = compensatedTranslation;
- transform = components.Combine();
- }
- else
- {
- transform = Transform3D.CreateTranslation(deltaPos);
- }
-
- doc.Models.OverridePermanentTransform(modelItems, transform, false);
-}
-```
-
-**调试技巧**:
-
-```csharp
-// ✅ 测试旋转中心的方法
-// 1. 将物体移动到远离原点的位置(如(-5, -5, 0))
-// 2. 应用旋转
-// 3. 检查物体是否"公转"(位置大幅移动)还是"自转"(位置基本不变)
-// 4. 如果发现"公转",说明没有正确补偿
-
-// ✅ 验证补偿计算的公式
-double expectedX_afterRotation = currentX * cos(angle) - currentY * sin(angle);
-double expectedY_afterRotation = currentX * sin(angle) + currentY * cos(angle);
-var compensationX = targetX - expectedX_afterRotation;
-var compensationY = targetY - expectedY_afterRotation;
-
-LogManager.Debug($"旋转前: ({currentX}, {currentY})");
-LogManager.Debug($"绕原点旋转后: ({expectedX_afterRotation}, {expectedY_afterRotation})");
-LogManager.Debug($"目标位置: ({targetX}, {targetY})");
-LogManager.Debug($"需要补偿: ({compensationX}, {compensationY})");
-```
+### 11.10 项目级硬约束
+1. 读当前姿态时,不要再默认用 `ModelItem.Transform`
+2. 真实物体参考姿态和当前姿态是两回事,不要混
+3. `OverridePermanentTransform` 是增量,不是绝对落位
+4. `ResetPermanentTransform` 清的是增量层,不是原始几何
+5. 涉及当前显示姿态时,优先看 `ModelGeometry.ActiveTransform`
+6. 涉及 fragment 矩阵解释时,先与 `ModelGeometry.ActiveTransform` 对齐验证
## 12. Item属性和自定义属性访问
基于官方示例的正确属性访问方法总结。
diff --git a/doc/working/current-engineering-state.md b/doc/working/current-engineering-state.md
index 3e99d9e..44e23ff 100644
--- a/doc/working/current-engineering-state.md
+++ b/doc/working/current-engineering-state.md
@@ -9,13 +9,27 @@
- `YUp` 模型下,终端安装仿真、`Rail` 姿态、真实物体与虚拟物体通行空间、起点与动画主链路已基本跑通。
- 地面路径在 `YUp` 模型下:
- 虚拟物体起点姿态、动画姿态、转弯姿态已恢复正常。
- - 当前已禁止地面/吊装路径偷偷退回旧 `yaw` 链路。
+ - 真实物体地面路径当前已恢复到可用状态:
+ - 起点落位补偿已接入;
+ - 逐帧补偿会随当前目标姿态一起旋转,不再使用固定世界补偿矢量;
+ - 动画结束后不再因恢复链或重复落最后一帧而再次跳动。
+ - `Ground + 真实物体` 的逐帧姿态约束已经明确:
+ - 动画播放阶段只允许绕宿主 `up` 轴转动;
+ - 不再在播放阶段每帧重建完整三维姿态。
+ - `Ground/Hoisting + 真实物体` 的前进轴语义当前已固定:
+ - 对象级前进轴统一按 `PositiveX` 解释;
+ - 不再因为路径初始方向更偏 `Z` 就把对象前进轴自动切换成 `PositiveZ`。
+- 当前已禁止地面/吊装路径偷偷退回旧 `yaw` 链路。
- 碰撞检测/恢复主链路已稳定:
- `ClashDetective` 三维恢复不能再先 `ResetPermanentTransform`。
- 碰撞恢复、自动报告、自动截图已重新对齐到动画主链路。
- 虚拟物体资源问题已确认并修复:
- 旧 `unit_cube.nwc` 局部几何中心不在原点,会导致虚拟物体中心偏差。
- 新 `unit_cube.nwc` 已替换为几何中心在原点的版本。
+ - 旋转适配入口当前稳定分流:
+ - 虚拟物体继续走 `HostCoordinateAdapter` 的 `Legacy` 入口;
+ - 真实物体走 `Direct` 入口;
+ - 两者不能再强行共用同一条旋转转换链。
## 2. 当前坐标系架构
@@ -63,6 +77,10 @@
- 当前规则:
- 先解释真实物体参考姿态,再做路径对齐
- 不能跳过这一步,直接拿 fragment 世界轴去猜业务姿态
+ - 当前补充规则:
+ - `Ground/Hoisting` 的真实物体“对象前进轴”与 fragment 参考姿态解释是两层语义;
+ - fragment 负责解释真实参考姿态;
+ - 对象前进轴当前在 `Ground/Hoisting` 上固定按 `PositiveX` 语义使用,不再做“按路径方向选最近轴”的自动切换。
### 4.1 Rotation / 矩阵语义
@@ -102,6 +120,15 @@
- 目前稳定可用的是:`Y` 轴转动。
- `X / Z` 轴在起点静态预览或直线段里可能看起来合理,但路径一旦拐弯,逐帧按宿主世界轴重算会让“俯仰/侧倾”语义发生耦合,表现成不符合现场直觉的侧旋。
- 因此当前阶段,对地面路径应按“只稳定支持 `Y` 轴转动”使用;`X / Z` 轴播放链问题留待后续专项修复。
+- 当前新增稳定规则(2026-03-25):
+ - `Ground + 真实物体` 的播放阶段不能再每帧应用完整三维姿态;
+ - 必须以起点基姿态为基线,只叠加宿主 `up` 轴上的单轴旋转。
+- 当前新增稳定规则(2026-03-25):
+ - `Ground + 真实物体` 的起点目标点语义仍然是路径跟踪点,不允许修改路径点本身;
+ - 如真实物体起点落位存在固定偏差,应把补偿施加到物体位置,而不是回写路径点或路径跟踪点语义。
+- 当前新增稳定规则(2026-03-25):
+ - `Ground + 真实物体` 的补偿不是固定世界矢量;
+ - 起点测得的偏差必须随当前目标姿态一起旋转后,再参与逐帧位置应用。
- `YUp` 吊装路径创建主链路已补齐到宿主坐标适配架构:
- 提升、水平移动、下降、终点落地都不能再把世界 `Z` 硬编码成“向上”。
- 终点必须使用用户最后一次点击的地面点,不能回填起点地面高程。
@@ -223,10 +250,35 @@
- 保存/恢复姿态
- 关键碰撞恢复
- 虚拟物体应用后中心/偏差
+ - 真实物体地面路径:
+ - `[路径起点诊断]`
+ - `[路径起点补偿]`
+ - `[Ground路径补偿]`
- 已降级或删除:
- 大量重复逐帧宿主姿态轴日志
- 虚拟物体 `Transform` 即时读回日志(容易误导)
+## 6. 当前 Navisworks 变换 API 结论
+
+- `ModelItem.Transform`
+ - 只表示原始设计文件变换;
+ - 不反映 `OverridePermanentTransform` 后的当前显示姿态。
+- `ModelGeometry.ActiveTransform`
+ - 是当前几何实际生效的变换;
+ - 当前项目里,凡是要读“当前姿态/当前位置”,优先使用这一层。
+- `ModelGeometry`
+ - 关键四层语义:
+ - `OriginalTransform`
+ - `PermanentOverrideTransform`
+ - `PermanentTransform`
+ - `ActiveTransform`
+- `OverridePermanentTransform(...)`
+ - 官方语义是增量变换;
+ - 不是“直接设成最终世界姿态”。
+- `ResetPermanentTransform(...)`
+ - 清掉的是永久增量层;
+ - 不会修改原始设计文件变换。
+
## 6. 当前还值得继续观察的点
- 空轨路径里仍有一个旧 warning:
diff --git a/src/Core/Animation/PathAnimationManager.cs b/src/Core/Animation/PathAnimationManager.cs
index 2190688..9af4ca4 100644
--- a/src/Core/Animation/PathAnimationManager.cs
+++ b/src/Core/Animation/PathAnimationManager.cs
@@ -205,6 +205,9 @@ namespace NavisworksTransport.Core.Animation
private Rotation3D _savedObjectRotation = Rotation3D.Identity;
private bool _savedObjectHasCustomRotation = false;
private bool _hasSavedObjectState = false;
+ private ObjectStartPlacementMode _objectStartPlacementMode = ObjectStartPlacementMode.AlignToPathPose;
+ private Rotation3D _railPreservedPoseRotation = Rotation3D.Identity;
+ private bool _hasRailPreservedPoseRotation = false;
private bool IsVirtualObjectMode => _animatedObjectMode == AnimatedObjectMode.VirtualObject;
private bool IsRealObjectMode => _animatedObjectMode == AnimatedObjectMode.RealObject;
@@ -637,6 +640,8 @@ namespace NavisworksTransport.Core.Animation
/// 是否成功移动
public bool MoveObjectToPathStart(ModelItem animatedObject = null, List pathPoints = null)
{
+ _objectStartPlacementMode = ObjectStartPlacementMode.AlignToPathPose;
+
// 前置检查:必须有路径
if (_route == null)
{
@@ -859,6 +864,97 @@ namespace NavisworksTransport.Core.Animation
}
}
+ public bool MoveObjectToPathStartPreservingInitialPose(ModelItem animatedObject = null, List pathPoints = null)
+ {
+ _objectStartPlacementMode = ObjectStartPlacementMode.PreserveInitialPose;
+
+ if (_route == null)
+ {
+ LogManager.Error("[平移到起点] 路径为空,无法移动物体到路径起点");
+ return false;
+ }
+
+ try
+ {
+ if (CurrentControlledObject != null || IsVirtualObjectMode)
+ {
+ RestoreObjectToCADPosition();
+ LogManager.Info($"[平移到起点] 已恢复物体到原始状态, _currentYaw={_currentYaw * 180 / Math.PI:F2}°");
+ }
+
+ if (animatedObject != null)
+ {
+ _animatedObject = animatedObject;
+ bool isVirtualObject =
+ VirtualObjectManager.Instance.IsVirtualObjectActive &&
+ ReferenceEquals(animatedObject, VirtualObjectManager.Instance.CurrentVirtualObject);
+ _animatedObjectMode = isVirtualObject
+ ? AnimatedObjectMode.VirtualObject
+ : AnimatedObjectMode.RealObject;
+ ResetRealObjectReferenceRotation();
+ _originalTransform = animatedObject.Transform;
+ _originalCenter = animatedObject.BoundingBox().Center;
+ _trackedPosition = GetTrackedObjectPosition(animatedObject);
+ if (isVirtualObject)
+ {
+ _currentYaw = ModelItemTransformHelper.GetYawFromTransform(_originalTransform);
+ _trackedRotation = _originalTransform.Factor().Rotation;
+ _hasTrackedRotation = true;
+ }
+ else
+ {
+ SyncTrackedRotationToObjectReference(animatedObject, isVirtualObject: false);
+ }
+ }
+
+ if (pathPoints != null)
+ {
+ _pathPoints = pathPoints;
+ }
+
+ if (_pathPoints == null || _pathPoints.Count < 2)
+ {
+ LogManager.Warning("[平移到起点] 没有可用的路径点");
+ return false;
+ }
+
+ Point3D pathStartPoint = _pathPoints[0];
+ Point3D startPosition = pathStartPoint;
+ if (_route.PathType == PathType.Hoisting)
+ {
+ startPosition = ResolveGroundTrackedCenter(startPosition, GetAnimatedObjectGroundContactHeight());
+ }
+ else if (_route.PathType == PathType.Rail)
+ {
+ Point3D previousPoint = _pathPoints[0];
+ Point3D nextPoint = _pathPoints.Count > 1 ? _pathPoints[1] : _pathPoints[0];
+ double objectHeight = GetAnimatedObjectRailNormalExtent(previousPoint, _pathPoints[0], nextPoint);
+ startPosition = RailPathPoseHelper.ResolveObjectSpaceCenterPosition(_route, startPosition, previousPoint, nextPoint, objectHeight);
+ }
+ else
+ {
+ startPosition = ResolveGroundTrackedCenter(startPosition, GetAnimatedObjectGroundContactHeight());
+ }
+
+ UpdateObjectPosition(startPosition);
+ _hasGroundRealObjectBasePose = false;
+ _groundRealObjectStartCompensation = new Vector3D(0, 0, 0);
+ _hasGroundRealObjectStartCompensation = false;
+
+ var startAppliedPoint = GetTrackedObjectPosition(CurrentControlledObject ?? _animatedObject);
+ LogManager.Info(
+ $"[平移到起点] 已保持初始位姿移动到起点: 路径point0=({pathStartPoint.X:F3},{pathStartPoint.Y:F3},{pathStartPoint.Z:F3}), " +
+ $"目标trackedPoint=({startPosition.X:F3},{startPosition.Y:F3},{startPosition.Z:F3}), " +
+ $"实际trackedPoint=({startAppliedPoint.X:F3},{startAppliedPoint.Y:F3},{startAppliedPoint.Z:F3}), 路径类型={_route.PathType.GetDisplayName()}");
+ return true;
+ }
+ catch (Exception ex)
+ {
+ LogManager.Error($"保持初始位姿平移到路径起点失败: {ex.Message}");
+ return false;
+ }
+ }
+
///
/// 预计算所有动画帧和碰撞信息
///
@@ -872,7 +968,20 @@ namespace NavisworksTransport.Core.Animation
if (_animatedObject != null && _route != null && _route.Points != null && _route.Points.Count > 0)
{
var pathPoints = _route.Points.Select(p => p.Position).ToList();
- MoveObjectToPathStart(_animatedObject, pathPoints);
+ MoveObjectToPathStartUsingCurrentPlacementMode(_animatedObject, pathPoints);
+ if (_route.PathType == PathType.Rail &&
+ _objectStartPlacementMode == ObjectStartPlacementMode.PreserveInitialPose &&
+ _hasTrackedRotation)
+ {
+ _railPreservedPoseRotation = _trackedRotation;
+ _hasRailPreservedPoseRotation = true;
+ LogManager.Info("[预计算] Rail平移模式已锁定起点姿态为整段动画旋转基线");
+ }
+ else
+ {
+ _railPreservedPoseRotation = Rotation3D.Identity;
+ _hasRailPreservedPoseRotation = false;
+ }
LogManager.Info("[预计算] 物体已移动到路径起点");
}
@@ -1169,11 +1278,18 @@ namespace NavisworksTransport.Core.Animation
};
if (_route.PathType == PathType.Rail &&
- TryCreateRailPathRotation(
- previousFramePoint,
- framePosition,
- nextFramePoint,
- out var railRotation))
+ _objectStartPlacementMode == ObjectStartPlacementMode.PreserveInitialPose &&
+ _hasRailPreservedPoseRotation)
+ {
+ frame.Rotation = _railPreservedPoseRotation;
+ frame.HasCustomRotation = true;
+ }
+ else if (_route.PathType == PathType.Rail &&
+ TryCreateRailPathRotation(
+ previousFramePoint,
+ framePosition,
+ nextFramePoint,
+ out var railRotation))
{
frame.Rotation = railRotation;
frame.HasCustomRotation = true;
@@ -1622,7 +1738,7 @@ namespace NavisworksTransport.Core.Animation
if (needsReset)
{
LogManager.Info($"[动画开始] 物体不在起点,重置到起点: {mismatchSummary}");
- MoveObjectToPathStart();
+ MoveObjectToPathStartUsingCurrentPlacementMode();
}
}
@@ -4623,6 +4739,8 @@ namespace NavisworksTransport.Core.Animation
///
public void SetObjectRotationCorrection(LocalEulerRotationCorrection rotationCorrection)
{
+ _objectStartPlacementMode = ObjectStartPlacementMode.AlignToPathPose;
+ _hasRailPreservedPoseRotation = false;
_objectRotationCorrection = rotationCorrection;
// 如果动画已创建,更新物体到起点的朝向
@@ -4658,6 +4776,23 @@ namespace NavisworksTransport.Core.Animation
LogManager.Debug($"[角度修正] 直接设置角度修正值: {_objectRotationCorrection}(不触发旋转)");
}
+ public void SetObjectStartPlacementMode(ObjectStartPlacementMode placementMode)
+ {
+ _objectStartPlacementMode = placementMode;
+ if (placementMode != ObjectStartPlacementMode.PreserveInitialPose)
+ {
+ _hasRailPreservedPoseRotation = false;
+ }
+ LogManager.Debug($"[起点摆放] 当前模式已设置为: {_objectStartPlacementMode}");
+ }
+
+ private bool MoveObjectToPathStartUsingCurrentPlacementMode(ModelItem animatedObject = null, List pathPoints = null)
+ {
+ return _objectStartPlacementMode == ObjectStartPlacementMode.PreserveInitialPose
+ ? MoveObjectToPathStartPreservingInitialPose(animatedObject, pathPoints)
+ : MoveObjectToPathStart(animatedObject, pathPoints);
+ }
+
///
/// 保留给旧单轴调用方:按当前宿主 up 轴映射成三轴角度修正。
///
diff --git a/src/Core/PathDataManager.cs b/src/Core/PathDataManager.cs
index 98c8e30..c78ae3f 100644
--- a/src/Core/PathDataManager.cs
+++ b/src/Core/PathDataManager.cs
@@ -50,6 +50,7 @@ namespace NavisworksTransport
public string pathType { get; set; }
public string railMountMode { get; set; }
public string railPathDefinitionMode { get; set; }
+ public double railNormalOffset { get; set; }
public JsonVector3 railPreferredNormal { get; set; }
public double totalLength { get; set; }
public JsonObjectLimits objectLimits { get; set; }
@@ -287,6 +288,7 @@ namespace NavisworksTransport
pathType = route.PathType.ToString(),
railMountMode = route.RailMountMode.ToString(),
railPathDefinitionMode = route.RailPathDefinitionMode.ToString(),
+ railNormalOffset = Math.Round(route.RailNormalOffset, exportSettings?.Precision ?? 3),
railPreferredNormal = route.RailPreferredNormal != null ? new
{
x = Math.Round(route.RailPreferredNormal.X, exportSettings?.Precision ?? 3),
@@ -542,6 +544,8 @@ namespace NavisworksTransport
route.RailPathDefinitionMode = railPathDefinitionMode;
}
+ route.RailNormalOffset = jsonRoute.railNormalOffset;
+
if (jsonRoute.railPreferredNormal != null)
{
route.RailPreferredNormal = new Point3D(
@@ -1409,6 +1413,7 @@ namespace NavisworksTransport
routeElement.SetAttribute("pathType", route.PathType.ToString());
routeElement.SetAttribute("railMountMode", route.RailMountMode.ToString());
routeElement.SetAttribute("railPathDefinitionMode", route.RailPathDefinitionMode.ToString());
+ routeElement.SetAttribute("railNormalOffset", route.RailNormalOffset.ToString("F3"));
if (route.RailPreferredNormal != null)
{
routeElement.SetAttribute("railPreferredNormalX", route.RailPreferredNormal.X.ToString("F3"));
@@ -1557,6 +1562,11 @@ namespace NavisworksTransport
route.RailPathDefinitionMode = railPathDefinitionMode;
}
+ if (double.TryParse(routeNode.Attributes?["railNormalOffset"]?.Value, out var railNormalOffset))
+ {
+ route.RailNormalOffset = railNormalOffset;
+ }
+
if (double.TryParse(routeNode.Attributes?["railPreferredNormalX"]?.Value, out var railPreferredNormalX) &&
double.TryParse(routeNode.Attributes?["railPreferredNormalY"]?.Value, out var railPreferredNormalY) &&
double.TryParse(routeNode.Attributes?["railPreferredNormalZ"]?.Value, out var railPreferredNormalZ))
diff --git a/src/Core/PathDatabase.cs b/src/Core/PathDatabase.cs
index 09d4d91..9a0299b 100644
--- a/src/Core/PathDatabase.cs
+++ b/src/Core/PathDatabase.cs
@@ -101,6 +101,7 @@ namespace NavisworksTransport
LiftHeight REAL,
RailMountMode INTEGER,
RailPathDefinitionMode INTEGER,
+ RailNormalOffset REAL,
RailPreferredNormalX REAL,
RailPreferredNormalY REAL,
RailPreferredNormalZ REAL,
@@ -466,8 +467,8 @@ namespace NavisworksTransport
// 路径长度由 PathRoute.TotalLength 计算属性实时从 Edges/Points 计算
var sql = @"
INSERT OR REPLACE INTO PathRoutes
- (Id, Name, EstimatedTime, TurnRadius, IsCurved, MaxObjectLength, MaxObjectWidth, MaxObjectHeight, SafetyMargin, GridSize, PathType, LiftHeight, RailMountMode, RailPathDefinitionMode, RailPreferredNormalX, RailPreferredNormalY, RailPreferredNormalZ, CreatedTime, LastModified)
- VALUES (@id, @name, @time, @turnRadius, @isCurved, @maxLength, @maxWidth, @maxHeight, @safetyMargin, @gridSize, @pathType, @liftHeightMeters, @railMountMode, @railPathDefinitionMode, @railPreferredNormalX, @railPreferredNormalY, @railPreferredNormalZ, @created, @modified)
+ (Id, Name, EstimatedTime, TurnRadius, IsCurved, MaxObjectLength, MaxObjectWidth, MaxObjectHeight, SafetyMargin, GridSize, PathType, LiftHeight, RailMountMode, RailPathDefinitionMode, RailNormalOffset, RailPreferredNormalX, RailPreferredNormalY, RailPreferredNormalZ, CreatedTime, LastModified)
+ VALUES (@id, @name, @time, @turnRadius, @isCurved, @maxLength, @maxWidth, @maxHeight, @safetyMargin, @gridSize, @pathType, @liftHeightMeters, @railMountMode, @railPathDefinitionMode, @railNormalOffset, @railPreferredNormalX, @railPreferredNormalY, @railPreferredNormalZ, @created, @modified)
";
using (var cmd = new SQLiteCommand(sql, _connection))
@@ -486,6 +487,7 @@ namespace NavisworksTransport
cmd.Parameters.AddWithValue("@liftHeightMeters", route.LiftHeight);
cmd.Parameters.AddWithValue("@railMountMode", (int)route.RailMountMode);
cmd.Parameters.AddWithValue("@railPathDefinitionMode", (int)route.RailPathDefinitionMode);
+ cmd.Parameters.AddWithValue("@railNormalOffset", route.RailNormalOffset);
cmd.Parameters.AddWithValue("@railPreferredNormalX", route.RailPreferredNormal != null ? (object)route.RailPreferredNormal.X : DBNull.Value);
cmd.Parameters.AddWithValue("@railPreferredNormalY", route.RailPreferredNormal != null ? (object)route.RailPreferredNormal.Y : DBNull.Value);
cmd.Parameters.AddWithValue("@railPreferredNormalZ", route.RailPreferredNormal != null ? (object)route.RailPreferredNormal.Z : DBNull.Value);
@@ -1546,6 +1548,7 @@ namespace NavisworksTransport
LiftHeight = Convert.ToDouble(reader["LiftHeight"]),
RailMountMode = (RailMountMode)Convert.ToInt32(reader["RailMountMode"]),
RailPathDefinitionMode = (RailPathDefinitionMode)Convert.ToInt32(reader["RailPathDefinitionMode"]),
+ RailNormalOffset = reader["RailNormalOffset"] != DBNull.Value ? Convert.ToDouble(reader["RailNormalOffset"]) : 0.0,
CreatedTime = Convert.ToDateTime(reader["CreatedTime"]),
LastModified = Convert.ToDateTime(reader["LastModified"])
};
@@ -2737,8 +2740,8 @@ namespace NavisworksTransport
cmd.CommandText = @"
SELECT Id, Name, CreatedTime, LastModified,
TurnRadius, IsCurved, MaxObjectLength, MaxObjectWidth, MaxObjectHeight,
- SafetyMargin, GridSize, PathType, LiftHeight, RailMountMode, RailPathDefinitionMode,
- RailPreferredNormalX, RailPreferredNormalY, RailPreferredNormalZ
+ SafetyMargin, GridSize, PathType, LiftHeight, RailMountMode, RailPathDefinitionMode,
+ RailNormalOffset, RailPreferredNormalX, RailPreferredNormalY, RailPreferredNormalZ
FROM PathRoutes
WHERE Id = @Id";
@@ -2764,7 +2767,8 @@ namespace NavisworksTransport
PathType = (PathType)Convert.ToInt32(reader["PathType"]),
LiftHeight = Convert.ToDouble(reader["LiftHeight"]),
RailMountMode = (RailMountMode)Convert.ToInt32(reader["RailMountMode"]),
- RailPathDefinitionMode = (RailPathDefinitionMode)Convert.ToInt32(reader["RailPathDefinitionMode"])
+ RailPathDefinitionMode = (RailPathDefinitionMode)Convert.ToInt32(reader["RailPathDefinitionMode"]),
+ RailNormalOffset = reader["RailNormalOffset"] != DBNull.Value ? Convert.ToDouble(reader["RailNormalOffset"]) : 0.0
};
if (reader["RailPreferredNormalX"] != DBNull.Value &&
@@ -2812,7 +2816,8 @@ namespace NavisworksTransport
{
["railmountmode"] = "ALTER TABLE PathRoutes ADD COLUMN RailMountMode INTEGER DEFAULT 0",
["railpathdefinitionmode"] = "ALTER TABLE PathRoutes ADD COLUMN RailPathDefinitionMode INTEGER DEFAULT 0",
- ["railpreferrednormalx"] = "ALTER TABLE PathRoutes ADD COLUMN RailPreferredNormalX REAL",
+ ["railnormaloffset"] = "ALTER TABLE PathRoutes ADD COLUMN RailNormalOffset REAL DEFAULT 0",
+ ["railpreferrednormalx"] = "ALTER TABLE PathRoutes ADD COLUMN RailPreferredNormalX REAL",
["railpreferrednormaly"] = "ALTER TABLE PathRoutes ADD COLUMN RailPreferredNormalY REAL",
["railpreferrednormalz"] = "ALTER TABLE PathRoutes ADD COLUMN RailPreferredNormalZ REAL"
};
diff --git a/src/Core/PathPlanningManager.cs b/src/Core/PathPlanningManager.cs
index f46fe95..94b5455 100644
--- a/src/Core/PathPlanningManager.cs
+++ b/src/Core/PathPlanningManager.cs
@@ -1271,6 +1271,7 @@ namespace NavisworksTransport
route.LiftHeight = loadedRoute.LiftHeight;
route.RailMountMode = loadedRoute.RailMountMode;
route.RailPathDefinitionMode = loadedRoute.RailPathDefinitionMode;
+ route.RailNormalOffset = loadedRoute.RailNormalOffset;
route.RailPreferredNormal = loadedRoute.RailPreferredNormal != null
? new Point3D(
loadedRoute.RailPreferredNormal.X,
diff --git a/src/Core/PathPlanningModels.cs b/src/Core/PathPlanningModels.cs
index 63341ba..c99356e 100644
--- a/src/Core/PathPlanningModels.cs
+++ b/src/Core/PathPlanningModels.cs
@@ -743,6 +743,12 @@ namespace NavisworksTransport
///
public Point3D RailPreferredNormal { get; set; }
+ ///
+ /// Rail 安装法向偏移(模型单位)。
+ /// 沿 Rail 最终法向对安装参考点施加额外偏移,用于快速微调路径。
+ ///
+ public double RailNormalOffset { get; set; }
+
// 数据库分析相关属性
///
/// 碰撞数量(从数据库加载)
@@ -784,6 +790,7 @@ namespace NavisworksTransport
RailMountMode = RailMountMode.UnderRail;
RailPathDefinitionMode = RailPathDefinitionMode.LegacySuspensionPoint;
RailPreferredNormal = null;
+ RailNormalOffset = 0.0;
}
///
@@ -807,6 +814,7 @@ namespace NavisworksTransport
RailMountMode = RailMountMode.UnderRail;
RailPathDefinitionMode = RailPathDefinitionMode.LegacySuspensionPoint;
RailPreferredNormal = null;
+ RailNormalOffset = 0.0;
}
///
@@ -1042,6 +1050,7 @@ namespace NavisworksTransport
SafetyMargin = SafetyMargin,
RailMountMode = RailMountMode,
RailPathDefinitionMode = RailPathDefinitionMode,
+ RailNormalOffset = RailNormalOffset,
RailPreferredNormal = RailPreferredNormal != null
? new Point3D(RailPreferredNormal.X, RailPreferredNormal.Y, RailPreferredNormal.Z)
: null
diff --git a/src/UI/WPF/ViewModels/AnimationControlViewModel.cs b/src/UI/WPF/ViewModels/AnimationControlViewModel.cs
index 6d0fbdd..52dc0ff 100644
--- a/src/UI/WPF/ViewModels/AnimationControlViewModel.cs
+++ b/src/UI/WPF/ViewModels/AnimationControlViewModel.cs
@@ -2196,7 +2196,15 @@ namespace NavisworksTransport.UI.WPF.ViewModels
var dialog = new Views.EditRotationWindow(_objectRotationCorrection);
if (dialog.ShowDialog() == true)
{
- ObjectRotationCorrection = dialog.RotationCorrection;
+ var placementRequest = dialog.AdjustmentRequest;
+ if (placementRequest.PreserveInitialPose)
+ {
+ ApplyTranslationOnlyObjectPlacement();
+ return;
+ }
+
+ _pathAnimationManager?.SetObjectStartPlacementMode(ObjectStartPlacementMode.AlignToPathPose);
+ ObjectRotationCorrection = placementRequest.RotationCorrection;
LogManager.Info($"物体角度修正已更新: {_objectRotationCorrection}");
UpdateMainStatus($"物体角度修正: {_objectRotationCorrection}");
@@ -2214,6 +2222,40 @@ namespace NavisworksTransport.UI.WPF.ViewModels
}
}
+ private void ApplyTranslationOnlyObjectPlacement()
+ {
+ try
+ {
+ if (_pathAnimationManager == null || !HasSelectedAnimatedObject)
+ {
+ return;
+ }
+
+ _objectRotationCorrection = LocalEulerRotationCorrection.Zero;
+ _pathAnimationManager.SetObjectRotationCorrectionDirect(LocalEulerRotationCorrection.Zero);
+ _pathAnimationManager.SetObjectStartPlacementMode(ObjectStartPlacementMode.PreserveInitialPose);
+ OnPropertyChanged(nameof(ObjectRotationCorrection));
+
+ if (_pathAnimationManager.MoveObjectToPathStartPreservingInitialPose())
+ {
+ LogManager.Info("[角度修正] 已按平移模式将物体移动到路径起点,保持初始位姿");
+ UpdateMainStatus("物体已平移到路径起点并保持初始位姿");
+ }
+ else
+ {
+ LogManager.Warning("[角度修正] 平移模式未能将物体移动到路径起点");
+ UpdateMainStatus("平移模式执行失败");
+ }
+
+ UpdatePassageSpaceVisualization();
+ }
+ catch (Exception ex)
+ {
+ LogManager.Error($"平移到路径起点失败: {ex.Message}");
+ UpdateMainStatus($"平移到路径起点失败: {ex.Message}");
+ }
+ }
+
///
/// 更新物体旋转(应用角度修正)
///
diff --git a/src/UI/WPF/ViewModels/PathEditingViewModel.cs b/src/UI/WPF/ViewModels/PathEditingViewModel.cs
index ef3b6e3..cb660e2 100644
--- a/src/UI/WPF/ViewModels/PathEditingViewModel.cs
+++ b/src/UI/WPF/ViewModels/PathEditingViewModel.cs
@@ -125,6 +125,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
private const double DefaultAssemblyReferenceRodLengthInMeters = 20.0;
private static readonly double DefaultAssemblyReferenceRodDiameterInMeters = CalculateDefaultAssemblyReferenceRodDiameterInMeters();
private const double DefaultAssemblyAnchorVerticalOffsetInMeters = 0.0;
+ private const double RailNormalOffsetNudgeStepInMeters = 0.1;
private const double DefaultAssemblySphereCenterX = 0.0;
private const double DefaultAssemblySphereCenterY = 0.0;
private const double DefaultAssemblySphereCenterZ = 0.0;
@@ -267,6 +268,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
OnPropertyChanged(nameof(CanUsePathLines));
OnPropertyChanged(nameof(IsRailRouteSelected));
OnPropertyChanged(nameof(SelectedRailMountMode));
+ OnPropertyChanged(nameof(SelectedRailNormalOffsetInMeters));
if (!CanUsePathLines && ShowPathLines)
{
// 吊装和空轨路径不能使用路径线,自动关闭
@@ -348,6 +350,38 @@ namespace NavisworksTransport.UI.WPF.ViewModels
}
}
+ public double SelectedRailNormalOffsetInMeters
+ {
+ get
+ {
+ var coreRoute = GetSelectedCoreRoute();
+ if (coreRoute == null || coreRoute.PathType != PathType.Rail)
+ {
+ return 0.0;
+ }
+
+ return UnitsConverter.ConvertToMeters(coreRoute.RailNormalOffset);
+ }
+ set
+ {
+ var coreRoute = GetSelectedCoreRoute();
+ if (coreRoute == null || coreRoute.PathType != PathType.Rail)
+ {
+ return;
+ }
+
+ double offsetInModelUnits = UnitsConverter.ConvertFromMeters(value);
+ if (Math.Abs(coreRoute.RailNormalOffset - offsetInModelUnits) < 1e-6)
+ {
+ return;
+ }
+
+ coreRoute.RailNormalOffset = offsetInModelUnits;
+ ApplyRailRouteConfigurationChange(coreRoute, $"安装法向偏移已更新为 {value:F3} 米");
+ OnPropertyChanged(nameof(SelectedRailNormalOffsetInMeters));
+ }
+ }
+
public string AssemblyTerminalObjectName
{
get => _assemblyTerminalObjectName;
@@ -992,6 +1026,8 @@ namespace NavisworksTransport.UI.WPF.ViewModels
public ICommand SelectAssemblyInstallationPointCommand { get; private set; }
public ICommand ClearAssemblyReferenceRodCommand { get; private set; }
public ICommand AnalyzeAssemblyTerminalFaceCommand { get; private set; }
+ public ICommand DecreaseSelectedRailNormalOffsetCommand { get; private set; }
+ public ICommand IncreaseSelectedRailNormalOffsetCommand { get; private set; }
// 多层吊装命令
public ICommand SelectMultiLevelStartPointCommand { get; private set; }
@@ -1196,6 +1232,19 @@ namespace NavisworksTransport.UI.WPF.ViewModels
LogManager.Info($"[Rail构型] {coreRoute.Name}: {logMessage}");
}
+ private void AdjustSelectedRailNormalOffset(double deltaInMeters)
+ {
+ var coreRoute = GetSelectedCoreRoute();
+ if (coreRoute == null || coreRoute.PathType != PathType.Rail)
+ {
+ return;
+ }
+
+ double currentOffsetInMeters = UnitsConverter.ConvertToMeters(coreRoute.RailNormalOffset);
+ double nextOffsetInMeters = Math.Round(currentOffsetInMeters + deltaInMeters, 3);
+ SelectedRailNormalOffsetInMeters = nextOffsetInMeters;
+ }
+
///
/// 从配置文件加载参数
///
@@ -1336,6 +1385,8 @@ namespace NavisworksTransport.UI.WPF.ViewModels
SelectAssemblyInstallationPointCommand = new RelayCommand(async () => await ExecuteSelectAssemblyInstallationPointAsync(), () => CanSelectAssemblyInstallationPoint);
ClearAssemblyReferenceRodCommand = new RelayCommand(() => ExecuteClearAssemblyReferenceRod());
AnalyzeAssemblyTerminalFaceCommand = new RelayCommand(async () => await ExecuteAnalyzeAssemblyTerminalFaceAsync(), () => CanAnalyzeAssemblyTerminalFace);
+ DecreaseSelectedRailNormalOffsetCommand = new RelayCommand(() => AdjustSelectedRailNormalOffset(-RailNormalOffsetNudgeStepInMeters));
+ IncreaseSelectedRailNormalOffsetCommand = new RelayCommand(() => AdjustSelectedRailNormalOffset(RailNormalOffsetNudgeStepInMeters));
}
#endregion
@@ -1704,7 +1755,8 @@ namespace NavisworksTransport.UI.WPF.ViewModels
Description = $"直线装配路径 - {AssemblyTerminalObjectName}",
PathType = PathType.Rail,
RailMountMode = AssemblyMountMode,
- RailPathDefinitionMode = RailPathDefinitionMode.RailCenterLine
+ RailPathDefinitionMode = RailPathDefinitionMode.RailCenterLine,
+ RailNormalOffset = UnitsConverter.ConvertFromMeters(AssemblyAnchorVerticalOffsetInMeters)
};
if (_hasAssemblyInstallationReference)
diff --git a/src/UI/WPF/Views/EditRotationWindow.xaml b/src/UI/WPF/Views/EditRotationWindow.xaml
index 5eda71f..a1fe247 100644
--- a/src/UI/WPF/Views/EditRotationWindow.xaml
+++ b/src/UI/WPF/Views/EditRotationWindow.xaml
@@ -1,7 +1,7 @@
+ Margin="0,0,10,0"/>
+ Margin="0,0,10,0"/>
+ Margin="0,0,10,0"/>
public double? CustomTurnRadius { get; set; }
+ ///
+ /// 仅用于视图显示的点直径(模型单位)。
+ /// 为 null 或非正数时使用默认点类型尺寸。
+ ///
+ [XmlIgnore]
+ public double? VisualizationDiameter { get; set; }
+
///
/// 方向类型(仅吊装路径使用)
/// 用于标识吊装路径点的移动方向:垂直、纵向(X轴)、横向(Y轴)
@@ -395,6 +402,7 @@ namespace NavisworksTransport
Notes = string.Empty;
SpeedLimit = 0;
CustomTurnRadius = null;
+ VisualizationDiameter = null;
Direction = HoistingPointDirection.Vertical;
}
@@ -415,9 +423,25 @@ namespace NavisworksTransport
Notes = string.Empty;
SpeedLimit = 0;
CustomTurnRadius = null;
+ VisualizationDiameter = null;
Direction = HoistingPointDirection.Vertical;
}
+ ///
+ /// 解析最终用于渲染的点半径(模型单位)。
+ ///
+ /// 默认点半径(模型单位)
+ /// 最终半径(模型单位)
+ public double ResolveVisualizationRadius(double defaultRadius)
+ {
+ if (!VisualizationDiameter.HasValue || VisualizationDiameter.Value <= 0)
+ {
+ return defaultRadius;
+ }
+
+ return VisualizationDiameter.Value * 0.5;
+ }
+
///
/// 返回路径点描述字符串
///
diff --git a/src/Core/PathPointRenderPlugin.cs b/src/Core/PathPointRenderPlugin.cs
index e6b5efd..5823d70 100644
--- a/src/Core/PathPointRenderPlugin.cs
+++ b/src/Core/PathPointRenderPlugin.cs
@@ -2268,7 +2268,8 @@ namespace NavisworksTransport
{
Center = point.Position,
Normal = GetHostUpVector(),
- Radius = isGridVisualization ? GetRadiusForGridVisualization() : GetRadiusForPointType(point.Type),
+ Radius = point.ResolveVisualizationRadius(
+ isGridVisualization ? GetRadiusForGridVisualization() : GetRadiusForPointType(point.Type)),
Color = isGridVisualization ? gridStyle.Color : GetColorForPointType(point.Type),
Alpha = isGridVisualization ? gridStyle.Alpha : 1.0,
Filled = true,
diff --git a/src/UI/WPF/ViewModels/PathEditingViewModel.cs b/src/UI/WPF/ViewModels/PathEditingViewModel.cs
index cb660e2..902e4ae 100644
--- a/src/UI/WPF/ViewModels/PathEditingViewModel.cs
+++ b/src/UI/WPF/ViewModels/PathEditingViewModel.cs
@@ -125,6 +125,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
private const double DefaultAssemblyReferenceRodLengthInMeters = 20.0;
private static readonly double DefaultAssemblyReferenceRodDiameterInMeters = CalculateDefaultAssemblyReferenceRodDiameterInMeters();
private const double DefaultAssemblyAnchorVerticalOffsetInMeters = 0.0;
+ private const double AssemblyVisualizationPointDiameterInMeters = 0.10;
private const double RailNormalOffsetNudgeStepInMeters = 0.1;
private const double DefaultAssemblySphereCenterX = 0.0;
private const double DefaultAssemblySphereCenterY = 0.0;
@@ -2259,7 +2260,12 @@ namespace NavisworksTransport.UI.WPF.ViewModels
for (int i = 0; i < _assemblyEndFaceSeedPoints.Count; i++)
{
- AddVisualizationPoint(markerRoute, _assemblyEndFaceSeedPoints[i], $"端面点{i + 1}", PathPointType.WayPoint);
+ AddVisualizationPoint(
+ markerRoute,
+ _assemblyEndFaceSeedPoints[i],
+ $"端面点{i + 1}",
+ PathPointType.WayPoint,
+ UnitsConverter.ConvertFromMeters(AssemblyVisualizationPointDiameterInMeters));
}
renderPlugin.RenderPointOnly(markerRoute);
@@ -2280,11 +2286,21 @@ namespace NavisworksTransport.UI.WPF.ViewModels
Id = AssemblyEndFaceCenterPathId,
Description = "终端安装端面中心"
};
- AddVisualizationPoint(markerRoute, centerPoint, "端面中心", PathPointType.WayPoint);
+ AddVisualizationPoint(
+ markerRoute,
+ centerPoint,
+ "端面中心",
+ PathPointType.WayPoint,
+ UnitsConverter.ConvertFromMeters(AssemblyVisualizationPointDiameterInMeters));
renderPlugin.RenderPointOnly(markerRoute);
}
- private static void AddVisualizationPoint(PathRoute route, Point3D position, string name, PathPointType type)
+ private static void AddVisualizationPoint(
+ PathRoute route,
+ Point3D position,
+ string name,
+ PathPointType type,
+ double? visualizationDiameter = null)
{
if (route == null)
{
@@ -2293,7 +2309,8 @@ namespace NavisworksTransport.UI.WPF.ViewModels
route.Points.Add(new PathPoint(position, name, type)
{
- Index = route.Points.Count
+ Index = route.Points.Count,
+ VisualizationDiameter = visualizationDiameter
});
}
@@ -2379,7 +2396,12 @@ namespace NavisworksTransport.UI.WPF.ViewModels
};
for (int i = 0; i < pickPoints.Count; i++)
{
- AddVisualizationPoint(markerRoute, pickPoints[i], $"安装面点{i + 1}", PathPointType.WayPoint);
+ AddVisualizationPoint(
+ markerRoute,
+ pickPoints[i],
+ $"安装面点{i + 1}",
+ PathPointType.WayPoint,
+ UnitsConverter.ConvertFromMeters(AssemblyVisualizationPointDiameterInMeters));
}
renderPlugin.RenderPointOnly(markerRoute);
}
@@ -2398,7 +2420,12 @@ namespace NavisworksTransport.UI.WPF.ViewModels
Id = AssemblyInstallationAnchorPointPathId,
Description = "终端安装点"
};
- AddVisualizationPoint(markerRoute, anchorPoint, "安装点", PathPointType.WayPoint);
+ AddVisualizationPoint(
+ markerRoute,
+ anchorPoint,
+ "安装点",
+ PathPointType.WayPoint,
+ UnitsConverter.ConvertFromMeters(AssemblyVisualizationPointDiameterInMeters));
renderPlugin.RenderPointOnly(markerRoute);
}
From ad8d86e79f895b905cbd39dbc75a8d6f347d910b Mon Sep 17 00:00:00 2001
From: tian <11429339@qq.com>
Date: Thu, 26 Mar 2026 13:45:48 +0800
Subject: [PATCH 33/87] Restore assembly pick focus with space
---
src/Core/PathInputMonitor.cs | 16 ++++++++--------
1 file changed, 8 insertions(+), 8 deletions(-)
diff --git a/src/Core/PathInputMonitor.cs b/src/Core/PathInputMonitor.cs
index 6f376c7..57a5525 100644
--- a/src/Core/PathInputMonitor.cs
+++ b/src/Core/PathInputMonitor.cs
@@ -43,17 +43,17 @@ namespace NavisworksTransport
if (key == 32) // 空格键
{
var pathManager = PathPlanningManager.GetActivePathManager();
- if (pathManager != null &&
- (pathManager.PathEditState == PathEditState.Creating ||
- pathManager.PathEditState == PathEditState.AddingPoints ||
- pathManager.PathEditState == PathEditState.EditingPoint))
+ if (pathManager != null)
{
var currentTool = Application.MainDocument.Tool.Value;
if (currentTool != Tool.CustomToolPlugin)
{
- LogManager.Info($"[InputMonitor] 用户按空格键,当前工具为{currentTool},重新激活工具");
- pathManager.ReactivateToolPlugin();
- return true;
+ LogManager.Info($"[InputMonitor] 用户按空格键,当前工具为{currentTool},强制恢复ToolPlugin焦点");
+ bool restored = pathManager.ForceReinitializeToolPlugin(subscribeToEvents: false);
+ if (restored)
+ {
+ return true;
+ }
}
}
}
@@ -67,4 +67,4 @@ namespace NavisworksTransport
}
}
}
-}
\ No newline at end of file
+}
From 9f1924a79774cc9637979aa0a9b265ce431f5dde Mon Sep 17 00:00:00 2001
From: tian <11429339@qq.com>
Date: Thu, 26 Mar 2026 13:46:51 +0800
Subject: [PATCH 34/87] Add real object pose source migration draft
---
...real-object-pose-source-migration-draft.md | 423 ++++++++++++++++++
1 file changed, 423 insertions(+)
create mode 100644 doc/design/2026/real-object-pose-source-migration-draft.md
diff --git a/doc/design/2026/real-object-pose-source-migration-draft.md b/doc/design/2026/real-object-pose-source-migration-draft.md
new file mode 100644
index 0000000..364a370
--- /dev/null
+++ b/doc/design/2026/real-object-pose-source-migration-draft.md
@@ -0,0 +1,423 @@
+# 真实物体位姿来源迁移草案
+
+## 1. 背景
+
+当前真实物体位姿链路中,`fragment` 承担了两种不同语义:
+
+- 读取真实物体的**参考姿态**
+- 间接参与判断真实物体的**当前显示姿态**
+
+这两个语义不应混在一起。
+
+当前工程已确认:
+
+- `ModelItem.Transform` 不能默认视为真实物体当前姿态或原始姿态
+- `fragment` 代表姿态适合做“参考姿态解释”
+- `ModelGeometry.ActiveTransform` 更适合做“当前实际显示姿态”读取
+
+因此,本次迁移目标不是“简单移除 fragment”,而是:
+
+1. 先把**参考姿态来源**与**当前显示变换来源**拆开
+2. 优先把“当前显示姿态读取”切到准确变换 API
+3. 保留 fragment 参考姿态链作为稳定基线
+4. 在测试和 shadow 验证足够充分后,再评估参考姿态主来源是否切换
+
+---
+
+## 2. 当前问题
+
+### 2.1 语义混用
+
+当前真实物体链路同时关心:
+
+- 物体原始业务姿态是什么
+- 物体此刻在 Navisworks 里实际显示成什么姿态
+- 增量变换叠加后,应以哪一层为“当前状态”
+
+如果这三者没有统一入口,就容易出现:
+
+- 起点姿态正确,但播放过程漂移
+- `ResetPermanentTransform` 后内部状态与宿主状态不同步
+- 通行空间、碰撞恢复、动画终点使用了不同的姿态基线
+
+### 2.2 fragment 并不是“当前姿态 API”
+
+fragment 的 `GetLocalToWorldMatrix()` 更接近几何层矩阵。
+
+它适合:
+
+- 统计代表姿态
+- 解释真实物体的原始参考框架
+- 参与 COM 几何分析
+
+它不应优先承担:
+
+- 当前 override 后显示姿态读取
+- 动画增量变换后的宿主状态读回
+
+### 2.3 直接回退 `ModelItem.Transform` 风险很高
+
+对于很多 Revit/复合件导入模型:
+
+- `ModelItem.Transform` 可能是单位旋转
+- 不反映当前 override 后姿态
+- 不足以表达真实物体的业务参考姿态
+
+所以迁移不能走“fragment -> ModelItem.Transform”的单步替换。
+
+---
+
+## 3. 迁移目标
+
+### 3.1 核心目标
+
+建立两条明确分离的来源链:
+
+- **当前显示变换链**
+ - 用于读取当前实际显示姿态
+ - 优先使用 `ModelGeometry.ActiveTransform`
+
+- **参考姿态链**
+ - 用于表达真实物体的原始业务姿态
+ - 当前仍以 fragment 解释链为稳定主来源
+
+### 3.2 非目标
+
+本阶段不做以下事情:
+
+- 不重写 `Ground / Hoisting / Rail` 的姿态求解器
+- 不直接移除现有 fragment 参考姿态链
+- 不修改 Canonical / Host / Asset 三层坐标语义
+- 不在没有验证前,把 `ModelItem.Transform` 升级为真实姿态主来源
+
+---
+
+## 4. 目标架构
+
+### 4.1 新的职责划分
+
+建议新增两类 provider:
+
+#### A. `ICurrentDisplayTransformProvider`
+
+职责:
+
+- 读取当前几何实际显示姿态
+- 读取当前几何实际旋转
+- 读取当前几何原始姿态与 override 后姿态
+
+建议首个实现:
+
+- `GeometryActiveTransformProvider`
+
+优先数据源:
+
+1. `ModelGeometry.ActiveTransform`
+2. `ModelGeometry.PermanentOverrideTransform`
+3. `ModelGeometry.OriginalTransform`
+4. 明确失败,不偷偷 fallback 到不可靠语义
+
+#### B. `IReferencePoseProvider`
+
+职责:
+
+- 提供真实物体参考姿态
+- 输出解释后的 `Rotation + AxisX/Y/Z`
+- 对上层屏蔽具体来源是 fragment 还是 geometry
+
+建议实现顺序:
+
+1. `FragmentReferencePoseProvider`
+ - 复用当前 `RealObjectReferencePoseResolver`
+2. `GeometryReferencePoseProvider`
+ - 后续用于 shadow 验证
+ - 初期不接主链
+
+### 4.2 上层调用关系
+
+建议上层只依赖语义接口:
+
+- `PathAnimationManager`
+ - 读取参考姿态时,只依赖 `IReferencePoseProvider`
+ - 读取当前宿主实际姿态时,只依赖 `ICurrentDisplayTransformProvider`
+
+- `ModelItemTransformHelper`
+ - 统一封装 `ActiveTransform / OriginalTransform / delta transform`
+ - 成为“当前显示变换链”的基础工具层
+
+- `RealObjectPlanarPoseSolver`
+- `CanonicalRailPoseBuilder`
+ - 保持纯数学求解,不感知来源替换
+
+---
+
+## 5. 分阶段迁移方案
+
+### 阶段 0:冻结语义与测试基线
+
+目标:
+
+- 不改正式行为
+- 先把关键业务场景和期望值固定下来
+
+产出:
+
+- 参考姿态语义测试
+- 当前显示姿态读取测试
+- 增量变换恢复测试
+- `YUp / ZUp` 双宿主测试矩阵
+
+通过条件:
+
+- 现有 fragment 主链全部通过基线测试
+
+### 阶段 1:抽象来源接口,但不改行为
+
+目标:
+
+- 只做代码结构调整
+- 不改变当前正式结果
+
+建议动作:
+
+- 新增 `ICurrentDisplayTransformProvider`
+- 新增 `IReferencePoseProvider`
+- 让 `PathAnimationManager` 从直接依赖 helper/fragment,改为依赖 provider
+- 默认配置仍然使用 `FragmentReferencePoseProvider`
+
+通过条件:
+
+- 行为无变化
+- 回归测试全绿
+
+### 阶段 2:迁移“当前显示姿态读取”
+
+目标:
+
+- 把“当前姿态读取”切到准确变换 API
+
+建议动作:
+
+- 将以下读当前姿态的场景统一迁到 `ModelGeometry.ActiveTransform`
+ - 当前旋转读回
+ - 当前显示姿态调试日志
+ - override 后的实际姿态验证
+ - 动画恢复/碰撞恢复前的状态同步
+
+注意:
+
+- 这一阶段不改“参考姿态”来源
+- 只改“当前实际显示变换”的读取
+
+通过条件:
+
+- 起点/播放/暂停/终点/恢复行为与当前主链一致
+- `ResetPermanentTransform` 后内部跟踪状态不再依赖 `ModelItem.Transform`
+
+### 阶段 3:引入 `GeometryReferencePoseProvider` shadow 验证
+
+目标:
+
+- 并行验证 geometry 是否能稳定替代 fragment 参考姿态
+
+建议动作:
+
+- 新增几何级参考姿态读取实现
+- 不接正式主链
+- 只在日志/调试模式下输出与 fragment 基线的差异
+
+建议记录差异:
+
+- quaternion 夹角差
+- `AxisX / AxisY / AxisZ` 与基线的夹角差
+- 多 geometry 是否一致
+- `YUp / ZUp` 下解释后是否仍满足宿主语义
+
+判定规则建议:
+
+- 若 geometry 内部姿态不一致,则自动判定“不适合作为语义参考姿态主来源”
+- 若与 fragment 基线夹角超阈值,则继续保留 fragment 主来源
+
+### 阶段 4:按路径类型逐步接入
+
+目标:
+
+- 在证据足够充分后,才让新的参考姿态来源参与正式链路
+
+建议接入顺序:
+
+1. `Rail`
+2. `Ground`
+3. `Hoisting`
+
+原因:
+
+- `Rail` 当前姿态框架最明确,收益最大
+- `Ground / Hoisting` 对“对象前进轴语义”更敏感
+
+通过条件:
+
+- `Rail 0°` 基线不变
+- Ground 逐帧姿态不退化
+- 通行空间与真实物体仍共用同一尺寸语义
+
+---
+
+## 6. 建议新增/调整的代码结构
+
+### 6.1 建议新增文件
+
+- `src/Utils/CoordinateSystem/ICurrentDisplayTransformProvider.cs`
+- `src/Utils/CoordinateSystem/IReferencePoseProvider.cs`
+- `src/Utils/CoordinateSystem/GeometryActiveTransformProvider.cs`
+- `src/Utils/CoordinateSystem/FragmentReferencePoseProvider.cs`
+- `src/Utils/CoordinateSystem/GeometryReferencePoseProvider.cs`
+
+### 6.2 建议优先调整的现有文件
+
+- `src/Core/Animation/PathAnimationManager.cs`
+ - 只依赖 provider
+ - 去掉对 fragment/helper 的直接耦合
+
+- `src/Utils/ModelItemTransformHelper.cs`
+ - 成为统一的当前显示变换读取入口
+
+- `src/Utils/CoordinateSystem/RealObjectReferencePoseResolver.cs`
+ - 收敛为 fragment provider 的内部实现
+ - 不再直接被业务层广泛调用
+
+### 6.3 不建议本阶段改动的文件
+
+- `CanonicalPlanarPoseBuilder`
+- `CanonicalRailPoseBuilder`
+- `CanonicalTrackedPositionResolver`
+- `RailPathPoseHelper`
+
+原因:
+
+- 这些文件主要负责姿态求解与偏移语义
+- 不是本次来源迁移的根因层
+
+---
+
+## 7. 测试先行策略
+
+### 7.1 测试原则
+
+本次迁移必须遵循:
+
+1. 先补测试
+2. 再抽象接口
+3. 再切换读取来源
+4. 最后才考虑正式接入
+
+### 7.2 第一批必须补的测试
+
+#### A. 参考姿态基线测试
+
+建议位置:
+
+- `UnitTests/CoordinateSystem/RealObjectReferencePoseResolverTests.cs`
+
+锁定内容:
+
+- fragment 多片段平均后的代表姿态
+- `Fragment默认Up` 为 `Y/Z` 时的解释结果
+- `YUp / ZUp` 宿主下输出轴语义一致
+
+#### B. 当前显示姿态读取测试
+
+建议新增:
+
+- `UnitTests/CoordinateSystem/CurrentDisplayTransformProviderTests.cs`
+
+锁定内容:
+
+- `ActiveTransform` 优先级
+- `OriginalTransform / PermanentOverrideTransform / ActiveTransform` 的语义区分
+- override 后读取结果与预期一致
+
+#### C. 动画增量恢复测试
+
+建议新增:
+
+- `UnitTests/CoordinateSystem/IncrementalTransformRestoreTests.cs`
+
+锁定内容:
+
+- reset 后状态同步
+- 当前跟踪姿态与宿主状态一致
+- 不再错误依赖 `ModelItem.Transform`
+
+#### D. 业务回归测试
+
+建议补到现有测试中:
+
+- `Ground + 真实物体`
+- `Rail + 真实物体`
+- `YUp / ZUp`
+- 起点 / 逐帧 / 终点 / restore
+
+### 7.3 第二批 shadow 验证测试
+
+建议新增:
+
+- `UnitTests/CoordinateSystem/GeometryReferencePoseProviderTests.cs`
+
+锁定内容:
+
+- 单 geometry 对象能否稳定导出参考姿态
+- 多 geometry 姿态不一致时是否正确拒绝
+- geometry 基线与 fragment 基线差异是否在阈值内
+
+---
+
+## 8. 验收标准
+
+### 8.1 阶段 1-2 验收
+
+- 正式行为无肉眼可见退化
+- 所有已有姿态回归测试通过
+- `PathAnimationManager` 不再把“当前姿态读取”和“参考姿态来源”混用
+
+### 8.2 阶段 3 验收
+
+- 能输出 geometry vs fragment 的稳定差异报告
+- 能区分“可替代对象”和“不可替代对象”
+
+### 8.3 阶段 4 验收
+
+- `Rail 0°` 基线不变
+- `Ground / Hoisting` 逐帧行为不退化
+- 通行空间、碰撞恢复、终点诊断不出现新的语义分叉
+
+---
+
+## 9. 当前推荐执行顺序
+
+建议下一步按下面顺序推进:
+
+1. 先补测试,不接正式代码
+2. 抽 provider 接口,不改默认实现
+3. 先切“当前显示姿态读取”
+4. 做 geometry 参考姿态 shadow 验证
+5. 证据足够后,再讨论 fragment 主来源是否退出
+
+---
+
+## 10. 当前结论
+
+本次迁移的关键不是“删掉 fragment”,而是:
+
+- 先把语义拆开
+- 先把准确 API 用在该用的地方
+- 先用测试固定业务场景
+- 再用 shadow 验证去证明新的参考姿态来源是否真的可靠
+
+在当前阶段,**最稳的路线**是:
+
+- fragment 继续作为真实物体参考姿态主来源
+- `ModelGeometry.ActiveTransform` 接管当前显示姿态读取
+- 测试先行
+- 分阶段接入
+
From a5d1db6416b7dbdf00407e0a4ae64dd1dced8c77 Mon Sep 17 00:00:00 2001
From: tian <11429339@qq.com>
Date: Thu, 26 Mar 2026 22:49:45 +0800
Subject: [PATCH 35/87] Refine collision analysis dialog and rail assembly
markers
---
.../ViewModels/AnimationControlViewModel.cs | 98 ++++++------
src/UI/WPF/ViewModels/PathEditingViewModel.cs | 6 +-
src/UI/WPF/Views/CollisionAnalysisDialog.xaml | 145 ++++++++++--------
.../WPF/Views/CollisionAnalysisDialog.xaml.cs | 44 +++++-
4 files changed, 178 insertions(+), 115 deletions(-)
diff --git a/src/UI/WPF/ViewModels/AnimationControlViewModel.cs b/src/UI/WPF/ViewModels/AnimationControlViewModel.cs
index 52dc0ff..7fbe3f1 100644
--- a/src/UI/WPF/ViewModels/AnimationControlViewModel.cs
+++ b/src/UI/WPF/ViewModels/AnimationControlViewModel.cs
@@ -3103,50 +3103,9 @@ namespace NavisworksTransport.UI.WPF.ViewModels
{
var dialog = new CollisionAnalysisDialog(hotspots, totalCollisions);
DialogHelper.SetOwnerSafely(dialog);
- var result = dialog.ShowDialog();
-
- if (result == null)
- {
- // 用户取消
- UpdateMainStatus("预计算碰撞分析已取消");
- return;
- }
-
- if (dialog.ExcludedObjects.Count > 0)
- {
- // 用户选择了排除某些物体并重新生成
- LogManager.Info($"[碰撞分析] 用户选择排除 {dialog.ExcludedObjects.Count} 个物体并重新生成");
-
- // 🔥 添加排除对象到UI列表(合并,自动去重)
- AddExcludedObjectsToUIList(dialog.ExcludedObjects);
-
- // 🔥 从UI列表获取所有排除对象(包括之前手工添加的),设置到Manager并重新生成
- var allExcludedObjects = _excludedObjects.Select(e => e.ModelItem).Where(m => m != null).ToList();
- _pathAnimationManager.SetExcludedObjectsAndClearCache(allExcludedObjects);
-
- // 触发重新生成(使用 Dispatcher 避免阻塞 UI)
- System.Windows.Application.Current.Dispatcher.BeginInvoke(new Action(() =>
- {
- ExecuteGenerateAnimation();
- }), DispatcherPriority.Background);
-
- UpdateMainStatus($"已排除 {allExcludedObjects.Count} 个物体,正在重新生成...");
- }
- else
- {
- // 用户选择直接继续(无排除对象)
- LogManager.Info("[碰撞分析] 用户选择直接继续生成");
- UpdateMainStatus($"预计算碰撞分析完成,共 {totalCollisions} 个候选碰撞");
-
- // 🔥 保存检测记录到数据库(保存测试配置)
- var recordId = SaveCollisionDetectionRecord();
- if (recordId.HasValue && IsReusedHistoryRecord(recordId.Value))
- {
- // 用户选择使用历史记录,跳过后续流程
- LogManager.Info("[碰撞分析] 用户选择使用历史记录,跳过动画生成");
- return;
- }
- }
+ dialog.Closed += (_, __) => HandleCollisionAnalysisDialogClosed(dialog, totalCollisions);
+ dialog.Show();
+ dialog.Activate();
}
catch (Exception ex)
{
@@ -3154,6 +3113,57 @@ namespace NavisworksTransport.UI.WPF.ViewModels
}
}
+ private void HandleCollisionAnalysisDialogClosed(CollisionAnalysisDialog dialog, int totalCollisions)
+ {
+ try
+ {
+ if (dialog == null)
+ {
+ return;
+ }
+
+ switch (dialog.SelectedAction)
+ {
+ case CollisionAnalysisDialog.CollisionAnalysisAction.Regenerate:
+ LogManager.Info($"[碰撞分析] 用户选择排除 {dialog.ExcludedObjects.Count} 个物体并重新生成");
+
+ AddExcludedObjectsToUIList(dialog.ExcludedObjects);
+
+ var allExcludedObjects = _excludedObjects.Select(e => e.ModelItem).Where(m => m != null).ToList();
+ _pathAnimationManager.SetExcludedObjectsAndClearCache(allExcludedObjects);
+
+ System.Windows.Application.Current.Dispatcher.BeginInvoke(new Action(() =>
+ {
+ ExecuteGenerateAnimation();
+ }), DispatcherPriority.Background);
+
+ UpdateMainStatus($"已排除 {allExcludedObjects.Count} 个物体,正在重新生成...");
+ break;
+
+ case CollisionAnalysisDialog.CollisionAnalysisAction.Continue:
+ LogManager.Info("[碰撞分析] 用户选择直接继续生成");
+ UpdateMainStatus($"预计算碰撞分析完成,共 {totalCollisions} 个候选碰撞");
+
+ var recordId = SaveCollisionDetectionRecord();
+ if (recordId.HasValue && IsReusedHistoryRecord(recordId.Value))
+ {
+ LogManager.Info("[碰撞分析] 用户选择使用历史记录,跳过动画生成");
+ }
+ break;
+
+ case CollisionAnalysisDialog.CollisionAnalysisAction.Cancel:
+ case CollisionAnalysisDialog.CollisionAnalysisAction.None:
+ default:
+ UpdateMainStatus("预计算碰撞分析已取消");
+ break;
+ }
+ }
+ catch (Exception ex)
+ {
+ LogManager.Error($"[碰撞分析] 处理分析窗口结果失败: {ex.Message}", ex);
+ }
+ }
+
///
/// 保存检测记录到数据库
/// 在生成动画完成后调用,创建检测记录并保存当时的完整配置
diff --git a/src/UI/WPF/ViewModels/PathEditingViewModel.cs b/src/UI/WPF/ViewModels/PathEditingViewModel.cs
index 902e4ae..7a279b4 100644
--- a/src/UI/WPF/ViewModels/PathEditingViewModel.cs
+++ b/src/UI/WPF/ViewModels/PathEditingViewModel.cs
@@ -125,7 +125,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
private const double DefaultAssemblyReferenceRodLengthInMeters = 20.0;
private static readonly double DefaultAssemblyReferenceRodDiameterInMeters = CalculateDefaultAssemblyReferenceRodDiameterInMeters();
private const double DefaultAssemblyAnchorVerticalOffsetInMeters = 0.0;
- private const double AssemblyVisualizationPointDiameterInMeters = 0.10;
+ private const double AssemblyVisualizationPointDiameterInMeters = 0.05;
private const double RailNormalOffsetNudgeStepInMeters = 0.1;
private const double DefaultAssemblySphereCenterX = 0.0;
private const double DefaultAssemblySphereCenterY = 0.0;
@@ -1749,7 +1749,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
private void CreateAssemblyLinearRoute(Point3D startPoint)
{
Point3D endPoint = AssemblyReferencePathManager.Instance.ReferenceLineEnd;
- string routeName = $"直线装配_{_pathPlanningManager.Routes.Count + 1}";
+ string routeName = $"人工_{DateTime.Now:MMdd_HHmmss}";
var route = new PathRoute(routeName)
{
@@ -1768,7 +1768,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
_assemblyInstallationPlaneNormal.Z);
}
- route.AddPoint(new PathPoint(startPoint, "装配起点", PathPointType.StartPoint));
+ route.AddPoint(new PathPoint(startPoint, "起点", PathPointType.StartPoint));
route.AddPoint(new PathPoint(endPoint, "装配终点", PathPointType.EndPoint));
if (!_pathPlanningManager.AddRoute(route))
diff --git a/src/UI/WPF/Views/CollisionAnalysisDialog.xaml b/src/UI/WPF/Views/CollisionAnalysisDialog.xaml
index 8eed1aa..5e2679b 100644
--- a/src/UI/WPF/Views/CollisionAnalysisDialog.xaml
+++ b/src/UI/WPF/Views/CollisionAnalysisDialog.xaml
@@ -8,7 +8,6 @@
Height="720" Width="720"
WindowStartupLocation="CenterOwner"
ResizeMode="CanResize"
- Topmost="True"
Background="White">
@@ -97,67 +96,89 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/UI/WPF/Views/CollisionAnalysisDialog.xaml.cs b/src/UI/WPF/Views/CollisionAnalysisDialog.xaml.cs
index 86e3a3e..a76f603 100644
--- a/src/UI/WPF/Views/CollisionAnalysisDialog.xaml.cs
+++ b/src/UI/WPF/Views/CollisionAnalysisDialog.xaml.cs
@@ -15,8 +15,17 @@ namespace NavisworksTransport.UI.WPF.Views
///
public partial class CollisionAnalysisDialog : Window
{
+ public enum CollisionAnalysisAction
+ {
+ None,
+ Continue,
+ Regenerate,
+ Cancel
+ }
+
private List _viewModels;
public List ExcludedObjects { get; private set; }
+ public CollisionAnalysisAction SelectedAction { get; private set; } = CollisionAnalysisAction.None;
// 使用项目中定义的预计算碰撞结果高亮类别(紫色 #9C27B0)
private const string PrecomputedHighlightCategory = ModelHighlightHelper.PrecomputeCollisionResultsCategory;
@@ -122,17 +131,14 @@ namespace NavisworksTransport.UI.WPF.Views
// 清除预计算高亮
ModelHighlightHelper.ClearCategory(PrecomputedHighlightCategory);
- // 根据是否有排除对象设置对话框结果
if (ExcludedObjects.Count > 0)
{
- // 有排除对象,需要重新生成
- DialogResult = true;
+ SelectedAction = CollisionAnalysisAction.Regenerate;
LogManager.Info($"[碰撞分析] 用户选择排除 {ExcludedObjects.Count} 个物体并重新生成");
}
else
{
- // 没有排除对象,直接继续
- DialogResult = false;
+ SelectedAction = CollisionAnalysisAction.Continue;
LogManager.Info("[碰撞分析] 用户选择直接继续生成(无排除对象)");
}
@@ -144,10 +150,36 @@ namespace NavisworksTransport.UI.WPF.Views
// 清除预计算高亮
ModelHighlightHelper.ClearCategory(PrecomputedHighlightCategory);
- DialogResult = null;
+ SelectedAction = CollisionAnalysisAction.Cancel;
Close();
}
+ private void SelectAllButton_Click(object sender, RoutedEventArgs e)
+ {
+ if (_viewModels == null)
+ {
+ return;
+ }
+
+ foreach (var viewModel in _viewModels)
+ {
+ viewModel.IsExcluded = true;
+ }
+ }
+
+ private void ClearAllButton_Click(object sender, RoutedEventArgs e)
+ {
+ if (_viewModels == null)
+ {
+ return;
+ }
+
+ foreach (var viewModel in _viewModels)
+ {
+ viewModel.IsExcluded = false;
+ }
+ }
+
///
/// 视图模型
///
From 7db5a7a201ad9c477ff8bd8b056c0774308ad09c Mon Sep 17 00:00:00 2001
From: tian <11429339@qq.com>
Date: Sat, 28 Mar 2026 12:36:24 +0800
Subject: [PATCH 36/87] Add path duplication workflow
---
NavisworksTransport.UnitTests.csproj | 2 +
UnitTests/Core/PathHelperTests.cs | 28 ++++++++
UnitTests/Core/PathRouteCloneTests.cs | 71 +++++++++++++++++++
src/Core/PathPlanningModels.cs | 48 +++++++++++--
src/UI/WPF/ViewModels/PathEditingViewModel.cs | 47 ++++++++++++
src/UI/WPF/Views/PathEditingView.xaml | 4 ++
src/Utils/PathHelper.cs | 23 +++++-
7 files changed, 217 insertions(+), 6 deletions(-)
create mode 100644 UnitTests/Core/PathHelperTests.cs
create mode 100644 UnitTests/Core/PathRouteCloneTests.cs
diff --git a/NavisworksTransport.UnitTests.csproj b/NavisworksTransport.UnitTests.csproj
index 0a8140c..aa14ce5 100644
--- a/NavisworksTransport.UnitTests.csproj
+++ b/NavisworksTransport.UnitTests.csproj
@@ -53,7 +53,9 @@
+
+
diff --git a/UnitTests/Core/PathHelperTests.cs b/UnitTests/Core/PathHelperTests.cs
new file mode 100644
index 0000000..36bf856
--- /dev/null
+++ b/UnitTests/Core/PathHelperTests.cs
@@ -0,0 +1,28 @@
+using System;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using NavisworksTransport.Utils;
+
+namespace NavisworksTransport.UnitTests.Core
+{
+ [TestClass]
+ public class PathHelperTests
+ {
+ [TestMethod]
+ public void BuildDuplicatedPathName_ShouldRebuildTrailingTimestamp()
+ {
+ var now = new DateTime(2026, 3, 28, 9, 45, 12);
+
+ var duplicatedName = PathHelper.BuildDuplicatedPathName("人工_0328_081530", now);
+
+ Assert.AreEqual("副本_人工_0328_094512", duplicatedName);
+ }
+
+ [TestMethod]
+ public void BuildDuplicatedPathName_ShouldKeepPlainNameWithoutTimestamp()
+ {
+ var duplicatedName = PathHelper.BuildDuplicatedPathName("我的路径");
+
+ Assert.AreEqual("副本_我的路径", duplicatedName);
+ }
+ }
+}
diff --git a/UnitTests/Core/PathRouteCloneTests.cs b/UnitTests/Core/PathRouteCloneTests.cs
new file mode 100644
index 0000000..a1e5c75
--- /dev/null
+++ b/UnitTests/Core/PathRouteCloneTests.cs
@@ -0,0 +1,71 @@
+using Autodesk.Navisworks.Api;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace NavisworksTransport.UnitTests.Core
+{
+ [TestClass]
+ public class PathRouteCloneTests
+ {
+ [TestMethod]
+ public void Clone_ShouldPreserveRailSpecificPropertiesAndGenerateNewIds()
+ {
+ var route = new PathRoute("原路径")
+ {
+ PathType = PathType.Rail,
+ RailMountMode = RailMountMode.OverRail,
+ RailPathDefinitionMode = RailPathDefinitionMode.RailCenterLine,
+ RailNormalOffset = 12.5,
+ RailPreferredNormal = new Point3D(0, 1, 0)
+ };
+
+ route.Points.Add(new PathPoint(new Point3D(1, 2, 3), "起点", PathPointType.StartPoint));
+ route.Points.Add(new PathPoint(new Point3D(4, 5, 6), "终点", PathPointType.EndPoint));
+
+ var clonedRoute = route.Clone();
+
+ Assert.AreEqual(PathType.Rail, clonedRoute.PathType);
+ Assert.AreEqual(RailMountMode.OverRail, clonedRoute.RailMountMode);
+ Assert.AreEqual(RailPathDefinitionMode.RailCenterLine, clonedRoute.RailPathDefinitionMode);
+ Assert.AreEqual(12.5, clonedRoute.RailNormalOffset, 1e-6);
+ Assert.IsNotNull(clonedRoute.RailPreferredNormal);
+ Assert.AreNotEqual(route.Id, clonedRoute.Id);
+ Assert.AreEqual(2, clonedRoute.Points.Count);
+ Assert.AreNotEqual(route.Points[0].Id, clonedRoute.Points[0].Id);
+ Assert.AreEqual(route.Points[0].Name, clonedRoute.Points[0].Name);
+ Assert.AreEqual(route.Points[0].Type, clonedRoute.Points[0].Type);
+ }
+
+ [TestMethod]
+ public void Clone_ShouldCopyGroundEdgesAndRemapPointReferences()
+ {
+ var route = new PathRoute("地面路径")
+ {
+ PathType = PathType.Ground,
+ TurnRadius = 3.0,
+ IsCurved = true
+ };
+
+ var startPoint = new PathPoint(new Point3D(0, 0, 0), "起点", PathPointType.StartPoint);
+ var endPoint = new PathPoint(new Point3D(10, 0, 0), "终点", PathPointType.EndPoint);
+ route.Points.Add(startPoint);
+ route.Points.Add(endPoint);
+ route.Edges.Add(new PathEdge
+ {
+ StartPointId = startPoint.Id,
+ EndPointId = endPoint.Id,
+ SegmentType = PathSegmentType.Straight,
+ PhysicalLength = 10.0
+ });
+
+ var clonedRoute = route.Clone();
+
+ Assert.AreEqual(PathType.Ground, clonedRoute.PathType);
+ Assert.AreEqual(1, clonedRoute.Edges.Count);
+ Assert.AreEqual(3.0, clonedRoute.TurnRadius, 1e-6);
+ Assert.IsTrue(clonedRoute.IsCurved);
+ Assert.AreEqual(clonedRoute.Points[0].Id, clonedRoute.Edges[0].StartPointId);
+ Assert.AreEqual(clonedRoute.Points[1].Id, clonedRoute.Edges[0].EndPointId);
+ Assert.AreNotEqual(route.Edges[0].Id, clonedRoute.Edges[0].Id);
+ }
+ }
+}
diff --git a/src/Core/PathPlanningModels.cs b/src/Core/PathPlanningModels.cs
index 2cbd84b..eabea26 100644
--- a/src/Core/PathPlanningModels.cs
+++ b/src/Core/PathPlanningModels.cs
@@ -1058,7 +1058,7 @@ namespace NavisworksTransport
Id = Guid.NewGuid().ToString(),
EstimatedTime = EstimatedTime,
// TotalLength:计算属性,自动从几何数据计算,克隆时会重新计算
- CreatedTime = CreatedTime,
+ CreatedTime = DateTime.Now,
LastModified = DateTime.Now,
Description = Description,
AssociatedChannelIds = new List(AssociatedChannelIds),
@@ -1072,14 +1072,18 @@ namespace NavisworksTransport
MaxObjectWidth = MaxObjectWidth,
MaxObjectHeight = MaxObjectHeight,
SafetyMargin = SafetyMargin,
+ TurnRadius = TurnRadius,
+ IsCurved = IsCurved,
+ PathType = PathType,
+ LiftHeight = LiftHeight,
RailMountMode = RailMountMode,
RailPathDefinitionMode = RailPathDefinitionMode,
RailNormalOffset = RailNormalOffset,
- RailPreferredNormal = RailPreferredNormal != null
- ? new Point3D(RailPreferredNormal.X, RailPreferredNormal.Y, RailPreferredNormal.Z)
- : null
+ RailPreferredNormal = RailPreferredNormal
};
+ var pointIdMap = new Dictionary();
+
// 克隆所有路径点
foreach (var point in Points)
{
@@ -1088,9 +1092,43 @@ namespace NavisworksTransport
Id = Guid.NewGuid().ToString(),
Index = point.Index,
CreatedTime = point.CreatedTime,
- Notes = point.Notes
+ Notes = point.Notes,
+ SpeedLimit = point.SpeedLimit,
+ CustomTurnRadius = point.CustomTurnRadius,
+ VisualizationDiameter = point.VisualizationDiameter,
+ Direction = point.Direction
};
clonedRoute.Points.Add(clonedPoint);
+ pointIdMap[point.Id] = clonedPoint.Id;
+ }
+
+ foreach (var edge in Edges)
+ {
+ var clonedEdge = new PathEdge
+ {
+ Id = Guid.NewGuid().ToString(),
+ StartPointId = pointIdMap.ContainsKey(edge.StartPointId) ? pointIdMap[edge.StartPointId] : edge.StartPointId,
+ EndPointId = pointIdMap.ContainsKey(edge.EndPointId) ? pointIdMap[edge.EndPointId] : edge.EndPointId,
+ SegmentType = edge.SegmentType,
+ PhysicalLength = edge.PhysicalLength,
+ Trajectory = edge.Trajectory != null
+ ? new ArcTrajectory
+ {
+ Ts = edge.Trajectory.Ts,
+ Te = edge.Trajectory.Te,
+ ArcCenter = edge.Trajectory.ArcCenter,
+ RequestedRadius = edge.Trajectory.RequestedRadius,
+ ActualRadius = edge.Trajectory.ActualRadius,
+ DeflectionAngle = edge.Trajectory.DeflectionAngle,
+ ArcLength = edge.Trajectory.ArcLength
+ }
+ : null,
+ SampledPoints = edge.SampledPoints != null
+ ? new List(edge.SampledPoints)
+ : new List()
+ };
+
+ clonedRoute.Edges.Add(clonedEdge);
}
return clonedRoute;
diff --git a/src/UI/WPF/ViewModels/PathEditingViewModel.cs b/src/UI/WPF/ViewModels/PathEditingViewModel.cs
index 7a279b4..019a605 100644
--- a/src/UI/WPF/ViewModels/PathEditingViewModel.cs
+++ b/src/UI/WPF/ViewModels/PathEditingViewModel.cs
@@ -1005,6 +1005,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
public ICommand NewRailPathCommand { get; private set; }
public ICommand NewHoistingPathCommand { get; private set; }
public ICommand NewMultiLevelHoistingPathCommand { get; private set; }
+ public ICommand DuplicatePathCommand { get; private set; }
public ICommand DeletePathCommand { get; private set; }
public ICommand RenamePathCommand { get; private set; }
public ICommand StartEditCommand { get; private set; }
@@ -1364,6 +1365,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
PickMultiLevelTargetCommand = new RelayCommand((item) => ExecutePickMultiLevelTarget(item));
CreateMultiLevelPathCommand = new RelayCommand(async () => await ExecuteCreateMultiLevelPathAsync(), () => CanCreateMultiLevelPath);
CancelMultiLevelHoistingCommand = new RelayCommand(() => ExecuteCancelMultiLevelHoisting());
+ DuplicatePathCommand = new RelayCommand(async () => await ExecuteDuplicatePathAsync());
DeletePathCommand = new RelayCommand(async () => await ExecuteDeletePathAsync());
RenamePathCommand = new RelayCommand(async () => await ExecuteRenamePathAsync());
StartEditCommand = new RelayCommand(async () => await ExecuteAddPathPointAsync(), () => CanExecuteStartEdit);
@@ -3595,6 +3597,51 @@ namespace NavisworksTransport.UI.WPF.ViewModels
}, "删除路径");
}
+ private async Task ExecuteDuplicatePathAsync()
+ {
+ if (SelectedPathRoute == null || _pathPlanningManager == null) return;
+
+ await SafeExecuteAsync(async () =>
+ {
+ var sourceRoute = SelectedPathRoute.Route ??
+ _pathPlanningManager.Routes.FirstOrDefault(r => r.Id == SelectedPathRoute.Id);
+ if (sourceRoute == null)
+ {
+ throw new InvalidOperationException("未找到要复制的路径数据");
+ }
+
+ UpdateMainStatus($"正在复制路径: {sourceRoute.Name}...");
+
+ var duplicatedRoute = sourceRoute.Clone();
+ duplicatedRoute.Name = PathHelper.BuildDuplicatedPathName(sourceRoute.Name);
+ duplicatedRoute.CreatedTime = DateTime.Now;
+ duplicatedRoute.LastModified = DateTime.Now;
+
+ if (!_pathPlanningManager.AddRoute(duplicatedRoute))
+ {
+ throw new InvalidOperationException($"复制路径失败: {sourceRoute.Name}");
+ }
+
+ var duplicatedPathViewModel = new PathRouteViewModel(isFromDatabase: true)
+ {
+ Route = duplicatedRoute,
+ IsActive = false
+ };
+ duplicatedPathViewModel.SetTimeInfo(duplicatedRoute.CreatedTime, duplicatedRoute.LastModified);
+
+ foreach (var point in CreatePathPointViewModelsFromCoreRoute(duplicatedRoute))
+ {
+ duplicatedPathViewModel.Points.Add(point);
+ }
+
+ PathRoutes.Add(duplicatedPathViewModel);
+ SelectedPathRoute = duplicatedPathViewModel;
+ UpdateMainStatus($"已复制路径: {sourceRoute.Name} -> {duplicatedPathViewModel.Name}");
+
+ await Task.CompletedTask;
+ }, "复制路径");
+ }
+
#endregion
#region 坐标编辑命令
diff --git a/src/UI/WPF/Views/PathEditingView.xaml b/src/UI/WPF/Views/PathEditingView.xaml
index 5cef289..84e645a 100644
--- a/src/UI/WPF/Views/PathEditingView.xaml
+++ b/src/UI/WPF/Views/PathEditingView.xaml
@@ -214,6 +214,10 @@ NavisworksTransport 路径编辑页签视图 - 采用与动画控制和分层管
Command="{Binding NewHoistingPathCommand}"
Style="{StaticResource ActionButtonStyle}"
ToolTip="创建吊装路径(支持多层)"/>
+
+ /// 生成复制路径名称。
+ /// 如果原名称以 MMdd_HHmmss 结尾,则重建时间戳;否则仅添加“副本_”前缀。
+ ///
+ public static string BuildDuplicatedPathName(string originalName, DateTime? now = null)
+ {
+ var normalizedName = string.IsNullOrWhiteSpace(originalName) ? "路径" : originalName.Trim();
+ var timestampPattern = new Regex(@"^(.*)_\d{4}_\d{6}$");
+ var match = timestampPattern.Match(normalizedName);
+
+ if (!match.Success)
+ {
+ return $"副本_{normalizedName}";
+ }
+
+ var baseName = match.Groups[1].Value;
+ var timestamp = (now ?? DateTime.Now).ToString("MMdd_HHmmss");
+ return $"副本_{baseName}_{timestamp}";
+ }
+
///
/// 计算从HTML文件到目标文件的相对路径
///
@@ -203,4 +224,4 @@ namespace NavisworksTransport.Utils
}
}
}
-}
\ No newline at end of file
+}
From 95500c97175d9527fa6422c81c03c2d35c94c7db Mon Sep 17 00:00:00 2001
From: tian <11429339@qq.com>
Date: Sat, 28 Mar 2026 12:42:54 +0800
Subject: [PATCH 37/87] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E9=83=A8=E5=88=86?=
=?UTF-8?q?=E6=97=A5=E5=BF=97=E7=BA=A7=E5=88=AB=E4=B8=BADebug?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/Core/Animation/PathAnimationManager.cs | 18 +++++++++---------
src/Core/PathDatabase.cs | 2 +-
.../ViewModels/AnimationControlViewModel.cs | 4 ++--
src/UI/WPF/ViewModels/PathEditingViewModel.cs | 8 +-------
src/Utils/ModelItemTransformHelper.cs | 12 ++++++------
src/Utils/RailPathPoseHelper.cs | 4 ++--
6 files changed, 21 insertions(+), 27 deletions(-)
diff --git a/src/Core/Animation/PathAnimationManager.cs b/src/Core/Animation/PathAnimationManager.cs
index c991f10..4ed30db 100644
--- a/src/Core/Animation/PathAnimationManager.cs
+++ b/src/Core/Animation/PathAnimationManager.cs
@@ -2758,7 +2758,7 @@ namespace NavisworksTransport.Core.Animation
{
var actualHostPosition = GetTrackedObjectPosition(controlledObject);
currentPositionForTransform = actualHostPosition;
- LogManager.Info(
+ LogManager.Debug(
$"[动画姿态入口] {controlledObject.DisplayName} 宿主即时读回点=({actualHostPosition.X:F3},{actualHostPosition.Y:F3},{actualHostPosition.Z:F3})");
if (IsRealObjectMode &&
@@ -2767,12 +2767,12 @@ namespace NavisworksTransport.Core.Animation
{
var trackedLinear = new Transform3D(currentRotation).Linear;
var actualLinear = new Transform3D(actualGeometryRotation).Linear;
- LogManager.Info(
+ LogManager.Debug(
$"[平面姿态基线诊断] {controlledObject.DisplayName} 跟踪旋转: " +
$"X=({trackedLinear.Get(0, 0):F4},{trackedLinear.Get(1, 0):F4},{trackedLinear.Get(2, 0):F4}), " +
$"Y=({trackedLinear.Get(0, 1):F4},{trackedLinear.Get(1, 1):F4},{trackedLinear.Get(2, 1):F4}), " +
$"Z=({trackedLinear.Get(0, 2):F4},{trackedLinear.Get(1, 2):F4},{trackedLinear.Get(2, 2):F4})");
- LogManager.Info(
+ LogManager.Debug(
$"[平面姿态基线诊断] {controlledObject.DisplayName} 实际几何旋转: " +
$"X=({actualLinear.Get(0, 0):F4},{actualLinear.Get(1, 0):F4},{actualLinear.Get(2, 0):F4}), " +
$"Y=({actualLinear.Get(0, 1):F4},{actualLinear.Get(1, 1):F4},{actualLinear.Get(2, 1):F4}), " +
@@ -2805,15 +2805,15 @@ namespace NavisworksTransport.Core.Animation
var currentLinear = new Transform3D(currentRotation).Linear;
var targetLinear = new Transform3D(appliedTargetRotation).Linear;
- LogManager.Info(
+ LogManager.Debug(
$"[动画姿态入口] {controlledObject.DisplayName} 跟踪点=({_trackedPosition.X:F3},{_trackedPosition.Y:F3},{_trackedPosition.Z:F3}), " +
$"目标点=({appliedTargetPosition.X:F3},{appliedTargetPosition.Y:F3},{appliedTargetPosition.Z:F3})");
- LogManager.Info(
+ LogManager.Debug(
$"[动画姿态入口] {controlledObject.DisplayName} 跟踪姿态: " +
$"X=({currentLinear.Get(0, 0):F4},{currentLinear.Get(1, 0):F4},{currentLinear.Get(2, 0):F4}), " +
$"Y=({currentLinear.Get(0, 1):F4},{currentLinear.Get(1, 1):F4},{currentLinear.Get(2, 1):F4}), " +
$"Z=({currentLinear.Get(0, 2):F4},{currentLinear.Get(1, 2):F4},{currentLinear.Get(2, 2):F4})");
- LogManager.Info(
+ LogManager.Debug(
$"[动画姿态入口] {controlledObject.DisplayName} 目标姿态: " +
$"X=({targetLinear.Get(0, 0):F4},{targetLinear.Get(1, 0):F4},{targetLinear.Get(2, 0):F4}), " +
$"Y=({targetLinear.Get(0, 1):F4},{targetLinear.Get(1, 1):F4},{targetLinear.Get(2, 1):F4}), " +
@@ -4050,7 +4050,7 @@ namespace NavisworksTransport.Core.Animation
out var selectedForwardAxis,
out var selectedForwardWorldAxis))
{
- LogManager.Info(
+ LogManager.Debug(
$"[Rail姿态修正] Host={adapter.HostType}, 虚拟物体={IsVirtualObjectMode}, " +
$"真实物体参考姿态生效, Forward={convention.ForwardAxis}, Up={convention.UpAxis}, " +
$"选中Forward轴={selectedForwardAxis}, 选中Forward世界轴=({selectedForwardWorldAxis.X:F4},{selectedForwardWorldAxis.Y:F4},{selectedForwardWorldAxis.Z:F4})");
@@ -4059,7 +4059,7 @@ namespace NavisworksTransport.Core.Animation
}
convention = ModelAxisConvention.CreateDefaultForHost(adapter.HostType);
- LogManager.Info(
+ LogManager.Debug(
$"[Rail姿态修正] Host={adapter.HostType}, 虚拟物体={IsVirtualObjectMode}, " +
$"Forward={convention.ForwardAxis}, Up={convention.UpAxis}, 来源=默认轴约定");
return true;
@@ -4358,7 +4358,7 @@ namespace NavisworksTransport.Core.Animation
Matrix4x4 baselineLinear = Matrix4x4.CreateFromQuaternion(baselineHostQuaternion);
Matrix4x4 composedLinear = Matrix4x4.CreateFromQuaternion(composedHostQuaternion);
- LogManager.Info(
+ LogManager.Debug(
$"[Rail真实物体角度修正] BaselineX=({baselineLinear.M11:F4},{baselineLinear.M21:F4},{baselineLinear.M31:F4}), " +
$"BaselineY=({baselineLinear.M12:F4},{baselineLinear.M22:F4},{baselineLinear.M32:F4}), " +
$"BaselineZ=({baselineLinear.M13:F4},{baselineLinear.M23:F4},{baselineLinear.M33:F4}), " +
diff --git a/src/Core/PathDatabase.cs b/src/Core/PathDatabase.cs
index 9a0299b..857d56a 100644
--- a/src/Core/PathDatabase.cs
+++ b/src/Core/PathDatabase.cs
@@ -1208,7 +1208,7 @@ namespace NavisworksTransport
}
}
- LogManager.Info($"[GetClashDetectiveCollisionObjects] 查询到 {results.Count} 个碰撞对象,DetectionRecordId={detectionRecordId}");
+ LogManager.Info($"[获取碰撞对象] 查询到 {results.Count} 个碰撞对象,DetectionRecordId={detectionRecordId}");
}
catch (Exception ex)
{
diff --git a/src/UI/WPF/ViewModels/AnimationControlViewModel.cs b/src/UI/WPF/ViewModels/AnimationControlViewModel.cs
index 7fbe3f1..242fcd0 100644
--- a/src/UI/WPF/ViewModels/AnimationControlViewModel.cs
+++ b/src/UI/WPF/ViewModels/AnimationControlViewModel.cs
@@ -3660,7 +3660,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
}
var records = pathDatabase.GetDetectionResultsByPath(pathName);
- LogManager.Info($"[刷新碰撞历史] 查询到 {records.Count} 条记录,路径={pathName}");
+ LogManager.Debug($"[刷新碰撞历史] 查询到 {records.Count} 条记录,路径={pathName}");
_clashDetectiveResults.Clear();
foreach (var record in records)
@@ -3693,7 +3693,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
PathId = record.ObjectPathId
};
resultViewModel.AnimatedObject = Autodesk.Navisworks.Api.Application.ActiveDocument.Models.ResolvePathId(animatedObjectPathId);
- LogManager.Info($"[碰撞历史] 已加载运动物体: {resultViewModel.AnimatedObject?.DisplayName ?? "未找到"} for TestName={record.TestName}");
+ LogManager.Debug($"[碰撞历史] 已加载运动物体: {resultViewModel.AnimatedObject?.DisplayName ?? "未找到"} for TestName={record.TestName}");
}
catch (Exception ex)
{
diff --git a/src/UI/WPF/ViewModels/PathEditingViewModel.cs b/src/UI/WPF/ViewModels/PathEditingViewModel.cs
index 019a605..36b2e4d 100644
--- a/src/UI/WPF/ViewModels/PathEditingViewModel.cs
+++ b/src/UI/WPF/ViewModels/PathEditingViewModel.cs
@@ -6095,8 +6095,6 @@ namespace NavisworksTransport.UI.WPF.ViewModels
// 处理从数据库加载的历史路径
foreach (var coreRoute in e.Routes)
{
- LogManager.Info($"*** 开始处理数据库加载路径: {coreRoute.Name} ***");
-
// 创建对应的 WPF ViewModel
var dbPathViewModel = new PathRouteViewModel(isFromDatabase: true)
{
@@ -6115,26 +6113,22 @@ namespace NavisworksTransport.UI.WPF.ViewModels
dbPathViewModel.Points.Add(point);
}
- LogManager.Info($"*** 已创建PathRouteViewModel(数据库路径),包含 {dbPathViewModel.Points.Count} 个点 ***");
-
// 检查是否已存在同名路径,避免重复添加
var existingPath = PathRoutes.FirstOrDefault(p => p.Name == coreRoute.Name);
if (existingPath == null)
{
// 添加到 UI 列表
PathRoutes.Add(dbPathViewModel);
- LogManager.Info($"*** 数据库路径已添加到UI列表: {coreRoute.Name},当前PathRoutes.Count = {PathRoutes.Count} ***");
}
else
{
- LogManager.Info($"*** 数据库路径已存在,跳过: {coreRoute.Name} ***");
+ LogManager.Debug($"*** 数据库路径已存在,跳过: {coreRoute.Name} ***");
}
}
// 更新状态消息
string statusMessage = $"✅ 已从{e.LoadSource}加载 {e.RouteCount} 条历史路径";
UpdateMainStatus(statusMessage);
- LogManager.Info($"*** 状态已更新: {statusMessage} ***");
// 自动提取并显示所有空轨模型的基准线
ExtractAndRenderRailBaselinePaths();
diff --git a/src/Utils/ModelItemTransformHelper.cs b/src/Utils/ModelItemTransformHelper.cs
index 58460e5..dd2f95a 100644
--- a/src/Utils/ModelItemTransformHelper.cs
+++ b/src/Utils/ModelItemTransformHelper.cs
@@ -540,24 +540,24 @@ namespace NavisworksTransport.Utils
targetPosition.Y - rotatedCurrentPosition.Y,
targetPosition.Z - rotatedCurrentPosition.Z);
- LogManager.Info(
+ LogManager.Debug(
$"[模型增量姿态] {item.DisplayName} 当前=({currentPosition.X:F3},{currentPosition.Y:F3},{currentPosition.Z:F3}), " +
$"目标=({targetPosition.X:F3},{targetPosition.Y:F3},{targetPosition.Z:F3}), " +
$"平移=({translation.X:F3},{translation.Y:F3},{translation.Z:F3})");
- LogManager.Info(
+ LogManager.Debug(
$"[模型增量姿态] {item.DisplayName} 当前旋转: " +
$"X=({currentLinear.Get(0, 0):F4},{currentLinear.Get(1, 0):F4},{currentLinear.Get(2, 0):F4}), " +
$"Y=({currentLinear.Get(0, 1):F4},{currentLinear.Get(1, 1):F4},{currentLinear.Get(2, 1):F4}), " +
$"Z=({currentLinear.Get(0, 2):F4},{currentLinear.Get(1, 2):F4},{currentLinear.Get(2, 2):F4})");
- LogManager.Info(
+ LogManager.Debug(
$"[模型增量姿态] {item.DisplayName} 目标旋转: " +
$"X=({targetLinear.Get(0, 0):F4},{targetLinear.Get(1, 0):F4},{targetLinear.Get(2, 0):F4}), " +
$"Y=({targetLinear.Get(0, 1):F4},{targetLinear.Get(1, 1):F4},{targetLinear.Get(2, 1):F4}), " +
$"Z=({targetLinear.Get(0, 2):F4},{targetLinear.Get(1, 2):F4},{targetLinear.Get(2, 2):F4})");
- LogManager.Info(
+ LogManager.Debug(
$"[模型增量姿态] {item.DisplayName} 增量旋转: " +
$"X=({deltaLinear.Get(0, 0):F4},{deltaLinear.Get(1, 0):F4},{deltaLinear.Get(2, 0):F4}), " +
$"Y=({deltaLinear.Get(0, 1):F4},{deltaLinear.Get(1, 1):F4},{deltaLinear.Get(2, 1):F4}), " +
@@ -635,13 +635,13 @@ namespace NavisworksTransport.Utils
var targetLinear = new Transform3D(targetRotation).Linear;
var actualPosition = actualBounds.Center;
- LogManager.Info(
+ LogManager.Debug(
$"[模型增量姿态] {item.DisplayName} 立即读回位置(可能滞后): " +
$"实际=({actualPosition.X:F3},{actualPosition.Y:F3},{actualPosition.Z:F3}), " +
$"期望=({targetPosition.X:F3},{targetPosition.Y:F3},{targetPosition.Z:F3}), " +
$"偏差=({actualPosition.X - targetPosition.X:F3},{actualPosition.Y - targetPosition.Y:F3},{actualPosition.Z - targetPosition.Z:F3})");
- LogManager.Info(
+ LogManager.Debug(
$"[模型增量姿态] {item.DisplayName} 立即读回旋转(可能滞后/不反映override): " +
$"实际X=({actualLinear.Get(0, 0):F4},{actualLinear.Get(1, 0):F4},{actualLinear.Get(2, 0):F4}), " +
$"实际Y=({actualLinear.Get(0, 1):F4},{actualLinear.Get(1, 1):F4},{actualLinear.Get(2, 1):F4}), " +
diff --git a/src/Utils/RailPathPoseHelper.cs b/src/Utils/RailPathPoseHelper.cs
index b485e47..22edc97 100644
--- a/src/Utils/RailPathPoseHelper.cs
+++ b/src/Utils/RailPathPoseHelper.cs
@@ -291,7 +291,7 @@ namespace NavisworksTransport.Utils
hostXAxis.Y, hostYAxis.Y, hostZAxis.Y,
hostXAxis.Z, hostYAxis.Z, hostZAxis.Z);
- LogManager.Info(
+ LogManager.Debug(
$"[Rail姿态] Canonical切向=({canonicalForward.X:F4},{canonicalForward.Y:F4},{canonicalForward.Z:F4}), " +
$"Canonical侧向=({canonicalLateral.X:F4},{canonicalLateral.Y:F4},{canonicalLateral.Z:F4}), " +
$"Canonical法向=({canonicalUp.X:F4},{canonicalUp.Y:F4},{canonicalUp.Z:F4}), " +
@@ -414,7 +414,7 @@ namespace NavisworksTransport.Utils
rotation = CreateRotationFromLinearTransform(linearTransform);
var verifiedLinear = new Transform3D(rotation).Linear;
- LogManager.Info(
+ LogManager.Debug(
$"[Rail旋转验证] X=({verifiedLinear.Get(0, 0):F4},{verifiedLinear.Get(1, 0):F4},{verifiedLinear.Get(2, 0):F4}), " +
$"Y=({verifiedLinear.Get(0, 1):F4},{verifiedLinear.Get(1, 1):F4},{verifiedLinear.Get(2, 1):F4}), " +
$"Z=({verifiedLinear.Get(0, 2):F4},{verifiedLinear.Get(1, 2):F4},{verifiedLinear.Get(2, 2):F4})");
From e4788c17a3c41e96355a7082d115c6ee17341a8c Mon Sep 17 00:00:00 2001
From: tian <11429339@qq.com>
Date: Sat, 28 Mar 2026 13:54:48 +0800
Subject: [PATCH 38/87] Improve rail path installation point editing
---
src/UI/WPF/ViewModels/PathEditingViewModel.cs | 177 +++++++++++++++++-
src/UI/WPF/Views/PathEditingView.xaml | 45 ++++-
2 files changed, 205 insertions(+), 17 deletions(-)
diff --git a/src/UI/WPF/ViewModels/PathEditingViewModel.cs b/src/UI/WPF/ViewModels/PathEditingViewModel.cs
index 36b2e4d..8700eed 100644
--- a/src/UI/WPF/ViewModels/PathEditingViewModel.cs
+++ b/src/UI/WPF/ViewModels/PathEditingViewModel.cs
@@ -156,6 +156,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
private Vector3 _assemblyInstallationPlaneNormal;
private Vector3 _assemblyInstallationPlaneSpanDirection;
private double _assemblyInstallationOffsetDistanceInMeters;
+ private string _assemblyInstallationReferenceRouteId;
private readonly List _assemblyInstallationSeedPoints = new List();
private readonly List _assemblyEndFaceSeedPoints = new List();
private const string AssemblyAnchorMarkerPathId = "assembly_anchor_marker";
@@ -213,14 +214,21 @@ namespace NavisworksTransport.UI.WPF.ViewModels
{
// 路径变更时清除选中的路径点
SelectedPathPoint = null;
+ ClearAssemblyInstallationReferenceVisuals();
// 🔧 修复:同步PathPlanningManager的CurrentRoute
if (_pathPlanningManager != null && value != null)
{
// 查找对应的Core路径对象
- var coreRoute = _pathPlanningManager.Routes?.FirstOrDefault(r => r.Name == value.Name);
+ var coreRoute = _pathPlanningManager.Routes?.FirstOrDefault(r => r.Id == value.Id)
+ ?? _pathPlanningManager.Routes?.FirstOrDefault(r => r.Name == value.Name);
if (coreRoute != null)
{
+ if (coreRoute.PathType == PathType.Rail)
+ {
+ AssemblyMountMode = coreRoute.RailMountMode;
+ }
+
// 🔥 先设置可视化模式(在 SetCurrentRoute 之前,确保事件触发时状态已就绪)
UpdatePassageSpaceVisualizationForPathType(coreRoute.PathType);
@@ -270,6 +278,8 @@ namespace NavisworksTransport.UI.WPF.ViewModels
OnPropertyChanged(nameof(IsRailRouteSelected));
OnPropertyChanged(nameof(SelectedRailMountMode));
OnPropertyChanged(nameof(SelectedRailNormalOffsetInMeters));
+ OnPropertyChanged(nameof(CanSelectAssemblyInstallationPoint));
+ OnPropertyChanged(nameof(AssemblyInstallationPointButtonText));
if (!CanUsePathLines && ShowPathLines)
{
// 吊装和空轨路径不能使用路径线,自动关闭
@@ -346,6 +356,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
}
coreRoute.RailMountMode = value;
+ AssemblyMountMode = value;
ApplyRailRouteConfigurationChange(coreRoute, $"安装方式已更新为 {value}");
OnPropertyChanged(nameof(SelectedRailMountMode));
}
@@ -378,7 +389,15 @@ namespace NavisworksTransport.UI.WPF.ViewModels
}
coreRoute.RailNormalOffset = offsetInModelUnits;
- ApplyRailRouteConfigurationChange(coreRoute, $"安装法向偏移已更新为 {value:F3} 米");
+ if (IsAssemblyInstallationReferenceActiveForRoute(coreRoute))
+ {
+ UpdateAssemblyInstallationAnchorFromVerticalOffset();
+ PersistAssemblyInstallationReferenceToRailRoute(coreRoute, $"安装法向偏移已更新为 {value:F3} 米");
+ }
+ else
+ {
+ ApplyRailRouteConfigurationChange(coreRoute, $"安装法向偏移已更新为 {value:F3} 米");
+ }
OnPropertyChanged(nameof(SelectedRailNormalOffsetInMeters));
}
}
@@ -426,9 +445,14 @@ namespace NavisworksTransport.UI.WPF.ViewModels
{
if (SetProperty(ref _assemblyAnchorVerticalOffsetInMeters, value) && HasAssemblyTerminalObject)
{
+ var coreRoute = GetSelectedCoreRoute();
if (_hasAssemblyInstallationReference)
{
UpdateAssemblyInstallationAnchorFromVerticalOffset();
+ if (IsAssemblyInstallationReferenceActiveForRoute(coreRoute))
+ {
+ PersistAssemblyInstallationReferenceToRailRoute(coreRoute, $"安装法向偏移已更新为 {value:F3} 米");
+ }
}
RefreshAssemblyTerminalObjectInfo();
RenderAssemblyAnchorMarker();
@@ -543,7 +567,8 @@ namespace NavisworksTransport.UI.WPF.ViewModels
if (_selectedPathRoute != null && _selectedPathRoute.Points.Count > 0)
{
// 查找对应的Core路径对象
- var coreRoute = _pathPlanningManager?.Routes?.FirstOrDefault(r => r.Name == _selectedPathRoute.Name);
+ var coreRoute = _pathPlanningManager?.Routes?.FirstOrDefault(r => r.Id == _selectedPathRoute.Id)
+ ?? _pathPlanningManager?.Routes?.FirstOrDefault(r => r.Name == _selectedPathRoute.Name);
if (coreRoute != null)
{
// 使用PathPlanningManager的绘制方法来保持一致性
@@ -556,6 +581,8 @@ namespace NavisworksTransport.UI.WPF.ViewModels
}
}
+ RefreshSelectedRailInstallationReferenceVisuals();
+
// 3. 恢复网格可视化(如果网格可视化已启用且当前路径有关联的GridMap)
if (_pathPlanningManager?.IsAnyGridVisualizationEnabled == true)
{
@@ -1074,6 +1101,8 @@ namespace NavisworksTransport.UI.WPF.ViewModels
AssemblyReferencePathManager.Instance.HasReferenceLine &&
!IsSelectingAssemblyStartPoint;
public bool CanSelectAssemblyInstallationPoint => HasAssemblyTerminalObject &&
+ IsRailRouteSelected &&
+ SelectedPathRoute != null &&
_pathPlanningManager != null &&
!_isSelectingAssemblyStartPoint &&
!_isSelectingAssemblyEndFacePoints &&
@@ -1084,7 +1113,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
!IsSelectingAssemblyEndFacePoints &&
!_isSelectingAssemblyInstallationPoint;
- public string AssemblyInstallationPointButtonText => _hasAssemblyInstallationReference
+ public string AssemblyInstallationPointButtonText => HasSelectedRailInstallationReference()
? "重选安装点"
: "选安装点";
@@ -1219,7 +1248,8 @@ namespace NavisworksTransport.UI.WPF.ViewModels
return null;
}
- return _pathPlanningManager.Routes?.FirstOrDefault(route => route.Name == SelectedPathRoute.Name);
+ return _pathPlanningManager.Routes?.FirstOrDefault(route => route.Id == SelectedPathRoute.Id)
+ ?? _pathPlanningManager.Routes?.FirstOrDefault(route => route.Name == SelectedPathRoute.Name);
}
private void ApplyRailRouteConfigurationChange(PathRoute coreRoute, string logMessage)
@@ -1234,6 +1264,115 @@ namespace NavisworksTransport.UI.WPF.ViewModels
LogManager.Info($"[Rail构型] {coreRoute.Name}: {logMessage}");
}
+ private bool HasSelectedRailInstallationReference()
+ {
+ var coreRoute = GetSelectedCoreRoute();
+ return coreRoute != null &&
+ coreRoute.PathType == PathType.Rail &&
+ coreRoute.RailPreferredNormal != null;
+ }
+
+ private bool IsAssemblyInstallationReferenceActiveForRoute(PathRoute route)
+ {
+ return route != null &&
+ route.PathType == PathType.Rail &&
+ _hasAssemblyInstallationReference &&
+ _assemblyInstallationAnchorPoint != null &&
+ !string.IsNullOrWhiteSpace(_assemblyInstallationReferenceRouteId) &&
+ string.Equals(_assemblyInstallationReferenceRouteId, route.Id, StringComparison.Ordinal);
+ }
+
+ private int GetRailRouteInstallationPointIndex(PathRoute route)
+ {
+ if (route?.Points == null || route.Points.Count == 0)
+ {
+ return -1;
+ }
+
+ for (int i = route.Points.Count - 1; i >= 0; i--)
+ {
+ if (route.Points[i].Type == PathPointType.EndPoint)
+ {
+ return i;
+ }
+ }
+
+ return route.Points.Count - 1;
+ }
+
+ private void PersistAssemblyInstallationReferenceToRailRoute(PathRoute route, string logMessage)
+ {
+ if (route == null)
+ {
+ throw new ArgumentNullException(nameof(route));
+ }
+
+ if (route.PathType != PathType.Rail)
+ {
+ throw new InvalidOperationException("只有 Rail 路径支持安装点参数更新。");
+ }
+
+ if (!_hasAssemblyInstallationReference || _assemblyInstallationAnchorPoint == null)
+ {
+ throw new InvalidOperationException("当前没有可应用的安装点参考。");
+ }
+
+ int installationPointIndex = GetRailRouteInstallationPointIndex(route);
+ if (installationPointIndex < 0)
+ {
+ throw new InvalidOperationException("当前 Rail 路径没有可更新的终点。");
+ }
+
+ if (!_pathPlanningManager.UpdatePathPointWithConstraints(route, installationPointIndex, _assemblyInstallationAnchorPoint))
+ {
+ throw new InvalidOperationException("更新 Rail 路径安装点失败。");
+ }
+
+ route.RailPreferredNormal = new Point3D(
+ _assemblyInstallationPlaneNormal.X,
+ _assemblyInstallationPlaneNormal.Y,
+ _assemblyInstallationPlaneNormal.Z);
+ route.RailNormalOffset = UnitsConverter.ConvertFromMeters(AssemblyAnchorVerticalOffsetInMeters);
+ route.LastModified = DateTime.Now;
+ _assemblyInstallationReferenceRouteId = route.Id;
+
+ _pathPlanningManager.SavePathToDatabase(route);
+ _pathPlanningManager.DrawRouteVisualization(route, isAutoPath: false);
+ _pathPlanningManager.RaiseVisualizationStateChanged();
+
+ if (SelectedPathRoute != null && SelectedPathRoute.Id == route.Id)
+ {
+ SyncPathViewModelFromCoreRoute(SelectedPathRoute, route, preserveSelection: true);
+ OnPropertyChanged(nameof(SelectedRailNormalOffsetInMeters));
+ OnPropertyChanged(nameof(AssemblyInstallationPointButtonText));
+ RefreshAssemblyTerminalObjectInfo();
+ RefreshSelectedRailInstallationReferenceVisuals();
+ }
+
+ LogManager.Info($"[Rail构型] {route.Name}: {logMessage}");
+ }
+
+ private void RefreshSelectedRailInstallationReferenceVisuals()
+ {
+ if (!_isSelectingAssemblyInstallationPoint)
+ {
+ ClearAssemblyInstallationReferenceVisuals();
+ return;
+ }
+
+ var coreRoute = GetSelectedCoreRoute();
+ if (!IsAssemblyInstallationReferenceActiveForRoute(coreRoute))
+ {
+ ClearAssemblyInstallationReferenceVisuals();
+ return;
+ }
+
+ RenderAssemblyInstallationPickPoints(_assemblyInstallationSeedPoints);
+ RenderAssemblyInstallationAnchorPoint(_assemblyInstallationAnchorPoint);
+ RenderAssemblyInstallationCenterLine(_assemblyInstallationPickPoint, GetCurrentAssemblyOpticalAxisDirection());
+ RenderAssemblyInstallationPlane(_assemblyInstallationAnchorPoint, GetCurrentAssemblyOpticalAxisDirection(), _assemblyInstallationPlaneSpanDirection);
+ }
+
private void AdjustSelectedRailNormalOffset(double deltaInMeters)
{
var coreRoute = GetSelectedCoreRoute();
@@ -1564,6 +1703,14 @@ namespace NavisworksTransport.UI.WPF.ViewModels
throw new InvalidOperationException("终点箱体未设置或已失效,请先捕获终点箱体。");
}
+ var selectedRailRoute = GetSelectedCoreRoute();
+ if (selectedRailRoute == null || selectedRailRoute.PathType != PathType.Rail)
+ {
+ throw new InvalidOperationException("请先选中一条需要编辑安装点的 Rail 路径。");
+ }
+
+ AssemblyMountMode = selectedRailRoute.RailMountMode;
+
CleanupAssemblyReferenceSelection();
CleanupAssemblyEndFaceSelection(clearVisuals: false);
CleanupAssemblyInstallationSelection();
@@ -1737,6 +1884,13 @@ namespace NavisworksTransport.UI.WPF.ViewModels
BuildAndRenderAssemblyInstallationReference(
_assemblyInstallationSeedPoints[0],
_assemblyInstallationSeedPoints[1]);
+ var selectedRailRoute = GetSelectedCoreRoute();
+ if (selectedRailRoute != null && selectedRailRoute.PathType == PathType.Rail)
+ {
+ PersistAssemblyInstallationReferenceToRailRoute(
+ selectedRailRoute,
+ "已重选安装点并同步更新 Rail 路径安装参数");
+ }
CleanupAssemblyInstallationSelection();
}, "处理安装点拾取");
}
@@ -1890,8 +2044,15 @@ namespace NavisworksTransport.UI.WPF.ViewModels
_assemblyInstallationBaseAnchorPoint.Y + _assemblyInstallationPlaneNormal.Y * (float)offset,
_assemblyInstallationBaseAnchorPoint.Z + _assemblyInstallationPlaneNormal.Z * (float)offset);
- RenderAssemblyInstallationAnchorPoint(_assemblyInstallationAnchorPoint);
- RenderAssemblyInstallationPlane(_assemblyInstallationAnchorPoint, GetCurrentAssemblyOpticalAxisDirection(), _assemblyInstallationPlaneSpanDirection);
+ if (_isSelectingAssemblyInstallationPoint)
+ {
+ RenderAssemblyInstallationAnchorPoint(_assemblyInstallationAnchorPoint);
+ RenderAssemblyInstallationPlane(_assemblyInstallationAnchorPoint, GetCurrentAssemblyOpticalAxisDirection(), _assemblyInstallationPlaneSpanDirection);
+ }
+ else
+ {
+ ClearAssemblyInstallationReferenceVisuals();
+ }
}
private Vector3 GetCurrentAssemblyOpticalAxisDirection()
@@ -2600,6 +2761,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
_assemblyInstallationPlaneNormal = default(Vector3);
_assemblyInstallationPlaneSpanDirection = default(Vector3);
_assemblyInstallationOffsetDistanceInMeters = 0.0;
+ _assemblyInstallationReferenceRouteId = null;
_assemblyInstallationSeedPoints.Clear();
_assemblyAnchorVerticalOffsetInMeters = DefaultAssemblyAnchorVerticalOffsetInMeters;
OnPropertyChanged(nameof(AssemblyAnchorVerticalOffsetInMeters));
@@ -2631,6 +2793,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
_isSelectingAssemblyInstallationPoint = false;
_pathPlanningManager?.EnableMouseHandling();
_pathPlanningManager?.StopClickTool();
+ ClearAssemblyInstallationReferenceVisuals();
OnPropertyChanged(nameof(CanAnalyzeAssemblyTerminalFace));
OnPropertyChanged(nameof(CanSelectAssemblyStartPoint));
OnPropertyChanged(nameof(CanSelectAssemblyInstallationPoint));
diff --git a/src/UI/WPF/Views/PathEditingView.xaml b/src/UI/WPF/Views/PathEditingView.xaml
index 84e645a..b9c1eac 100644
--- a/src/UI/WPF/Views/PathEditingView.xaml
+++ b/src/UI/WPF/Views/PathEditingView.xaml
@@ -522,6 +522,7 @@ NavisworksTransport 路径编辑页签视图 - 采用与动画控制和分层管
+
+
+
+
+
+
+
-
+ VerticalAlignment="Center"/>
-
-
-
+
+
+
From 115d70db6697ded0191f15ee9d736347c3754b17 Mon Sep 17 00:00:00 2001
From: tian <11429339@qq.com>
Date: Sat, 28 Mar 2026 14:45:24 +0800
Subject: [PATCH 39/87] Add rail start point repositioning
---
src/UI/WPF/ViewModels/PathEditingViewModel.cs | 248 ++++++++++++++++--
src/UI/WPF/Views/PathEditingView.xaml | 6 +
2 files changed, 234 insertions(+), 20 deletions(-)
diff --git a/src/UI/WPF/ViewModels/PathEditingViewModel.cs b/src/UI/WPF/ViewModels/PathEditingViewModel.cs
index 8700eed..35213ad 100644
--- a/src/UI/WPF/ViewModels/PathEditingViewModel.cs
+++ b/src/UI/WPF/ViewModels/PathEditingViewModel.cs
@@ -142,6 +142,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
private RailMountMode _assemblyMountMode = RailMountMode.UnderRail;
private bool _hasAssemblyTerminalObject;
private bool _isSelectingAssemblyStartPoint;
+ private bool _isEditingSelectedRailStartPoint;
private bool _isSelectingAssemblyEndFacePoints;
private bool _isSelectingAssemblyInstallationPoint;
private bool _hasAssemblyEndFaceAnalysis;
@@ -214,6 +215,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
{
// 路径变更时清除选中的路径点
SelectedPathPoint = null;
+ ClearAssemblyReferenceLineVisuals();
ClearAssemblyInstallationReferenceVisuals();
// 🔧 修复:同步PathPlanningManager的CurrentRoute
@@ -227,6 +229,8 @@ namespace NavisworksTransport.UI.WPF.ViewModels
if (coreRoute.PathType == PathType.Rail)
{
AssemblyMountMode = coreRoute.RailMountMode;
+ _assemblyAnchorVerticalOffsetInMeters = UnitsConverter.ConvertToMeters(coreRoute.RailNormalOffset);
+ OnPropertyChanged(nameof(AssemblyAnchorVerticalOffsetInMeters));
}
// 🔥 先设置可视化模式(在 SetCurrentRoute 之前,确保事件触发时状态已就绪)
@@ -278,6 +282,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
OnPropertyChanged(nameof(IsRailRouteSelected));
OnPropertyChanged(nameof(SelectedRailMountMode));
OnPropertyChanged(nameof(SelectedRailNormalOffsetInMeters));
+ OnPropertyChanged(nameof(CanRepositionRailStartPoint));
OnPropertyChanged(nameof(CanSelectAssemblyInstallationPoint));
OnPropertyChanged(nameof(AssemblyInstallationPointButtonText));
if (!CanUsePathLines && ShowPathLines)
@@ -388,6 +393,8 @@ namespace NavisworksTransport.UI.WPF.ViewModels
return;
}
+ _assemblyAnchorVerticalOffsetInMeters = value;
+ OnPropertyChanged(nameof(AssemblyAnchorVerticalOffsetInMeters));
coreRoute.RailNormalOffset = offsetInModelUnits;
if (IsAssemblyInstallationReferenceActiveForRoute(coreRoute))
{
@@ -455,8 +462,17 @@ namespace NavisworksTransport.UI.WPF.ViewModels
}
}
RefreshAssemblyTerminalObjectInfo();
- RenderAssemblyAnchorMarker();
- RefreshAssemblyReferenceRodIfNeeded();
+ if (coreRoute != null &&
+ coreRoute.PathType == PathType.Rail &&
+ AssemblyReferencePathManager.Instance.HasReferenceLine)
+ {
+ RefreshSelectedRailReferenceLineVisuals();
+ }
+ else
+ {
+ RenderAssemblyAnchorMarker();
+ RefreshAssemblyReferenceRodIfNeeded();
+ }
}
}
}
@@ -1052,6 +1068,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
public ICommand CaptureAssemblyTerminalObjectCommand { get; private set; }
public ICommand GenerateAssemblyReferenceRodCommand { get; private set; }
public ICommand SelectAssemblyStartPointCommand { get; private set; }
+ public ICommand RepositionRailStartPointCommand { get; private set; }
public ICommand SelectAssemblyInstallationPointCommand { get; private set; }
public ICommand ClearAssemblyReferenceRodCommand { get; private set; }
public ICommand AnalyzeAssemblyTerminalFaceCommand { get; private set; }
@@ -1100,6 +1117,14 @@ namespace NavisworksTransport.UI.WPF.ViewModels
_pathPlanningManager != null &&
AssemblyReferencePathManager.Instance.HasReferenceLine &&
!IsSelectingAssemblyStartPoint;
+ public bool CanRepositionRailStartPoint => HasAssemblyTerminalObject &&
+ IsRailRouteSelected &&
+ SelectedPathRoute != null &&
+ _pathPlanningManager != null &&
+ AssemblyReferencePathManager.Instance.HasReferenceLine &&
+ !IsSelectingAssemblyStartPoint &&
+ !_isSelectingAssemblyEndFacePoints &&
+ !_isSelectingAssemblyInstallationPoint;
public bool CanSelectAssemblyInstallationPoint => HasAssemblyTerminalObject &&
IsRailRouteSelected &&
SelectedPathRoute != null &&
@@ -1272,6 +1297,26 @@ namespace NavisworksTransport.UI.WPF.ViewModels
coreRoute.RailPreferredNormal != null;
}
+ private int GetRoutePointIndexByType(PathRoute route, PathPointType pointType, int fallbackIndex)
+ {
+ if (route?.Points == null || route.Points.Count == 0)
+ {
+ return -1;
+ }
+
+ for (int i = 0; i < route.Points.Count; i++)
+ {
+ if (route.Points[i].Type == pointType)
+ {
+ return i;
+ }
+ }
+
+ return fallbackIndex >= 0 && fallbackIndex < route.Points.Count
+ ? fallbackIndex
+ : -1;
+ }
+
private bool IsAssemblyInstallationReferenceActiveForRoute(PathRoute route)
{
return route != null &&
@@ -1284,20 +1329,74 @@ namespace NavisworksTransport.UI.WPF.ViewModels
private int GetRailRouteInstallationPointIndex(PathRoute route)
{
- if (route?.Points == null || route.Points.Count == 0)
+ return GetRoutePointIndexByType(route, PathPointType.EndPoint, route?.Points?.Count - 1 ?? -1);
+ }
+
+ private int GetRailRouteStartPointIndex(PathRoute route)
+ {
+ return GetRoutePointIndexByType(route, PathPointType.StartPoint, 0);
+ }
+
+ private bool TryBuildRailRouteReferenceLine(PathRoute route, out AssemblyReferenceLine referenceLine)
+ {
+ referenceLine = null;
+ if (route == null || route.PathType != PathType.Rail || route.Points == null || route.Points.Count < 2)
{
- return -1;
+ return false;
}
- for (int i = route.Points.Count - 1; i >= 0; i--)
+ int startIndex = GetRailRouteStartPointIndex(route);
+ int endIndex = GetRailRouteInstallationPointIndex(route);
+ if (startIndex < 0 || endIndex < 0 || startIndex == endIndex)
{
- if (route.Points[i].Type == PathPointType.EndPoint)
- {
- return i;
- }
+ return false;
}
- return route.Points.Count - 1;
+ Point3D startPoint = route.Points[startIndex].Position;
+ Point3D endPoint = route.Points[endIndex].Position;
+ Vector3D direction = new Vector3D(
+ startPoint.X - endPoint.X,
+ startPoint.Y - endPoint.Y,
+ startPoint.Z - endPoint.Z);
+ double lengthSquared = direction.X * direction.X + direction.Y * direction.Y + direction.Z * direction.Z;
+ if (lengthSquared < 1e-9)
+ {
+ return false;
+ }
+
+ referenceLine = new AssemblyReferenceLine(startPoint, endPoint, direction.Normalize());
+ return true;
+ }
+
+ private void RefreshSelectedRailReferenceLineVisuals(string statusMessage = null)
+ {
+ if (_assemblyTerminalObject == null || !ModelItemAnalysisHelper.IsModelItemValid(_assemblyTerminalObject))
+ {
+ ClearAssemblyReferenceLineVisuals();
+ return;
+ }
+
+ var coreRoute = GetSelectedCoreRoute();
+ if (!TryBuildRailRouteReferenceLine(coreRoute, out AssemblyReferenceLine referenceLine))
+ {
+ ClearAssemblyReferenceLineVisuals();
+ return;
+ }
+
+ AssemblyReferencePathManager.Instance.CreateOrUpdateReferenceRod(
+ referenceLine.StartPoint,
+ referenceLine.EndPoint,
+ AssemblyReferenceRodDiameterInMeters);
+ AssemblyStartPointText = $"({referenceLine.StartPoint.X:F2}, {referenceLine.StartPoint.Y:F2}, {referenceLine.StartPoint.Z:F2})";
+ RefreshAssemblyTerminalObjectInfo(referenceLine.StartPoint, referenceLine.EndPoint);
+ RenderAssemblyReferenceLine(referenceLine);
+ OnPropertyChanged(nameof(CanSelectAssemblyStartPoint));
+ OnPropertyChanged(nameof(CanRepositionRailStartPoint));
+
+ if (!string.IsNullOrWhiteSpace(statusMessage))
+ {
+ UpdateMainStatus(statusMessage);
+ }
}
private void PersistAssemblyInstallationReferenceToRailRoute(PathRoute route, string logMessage)
@@ -1347,6 +1446,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
OnPropertyChanged(nameof(AssemblyInstallationPointButtonText));
RefreshAssemblyTerminalObjectInfo();
RefreshSelectedRailInstallationReferenceVisuals();
+ RefreshSelectedRailReferenceLineVisuals();
}
LogManager.Info($"[Rail构型] {route.Name}: {logMessage}");
@@ -1524,6 +1624,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
CaptureAssemblyTerminalObjectCommand = new RelayCommand(async () => await ExecuteCaptureAssemblyTerminalObjectAsync());
GenerateAssemblyReferenceRodCommand = new RelayCommand(async () => await ExecuteGenerateAssemblyReferenceRodAsync(), () => CanGenerateAssemblyReferenceRod);
SelectAssemblyStartPointCommand = new RelayCommand(async () => await ExecuteSelectAssemblyStartPointAsync(), () => CanSelectAssemblyStartPoint);
+ RepositionRailStartPointCommand = new RelayCommand(async () => await ExecuteRepositionRailStartPointAsync(), () => CanRepositionRailStartPoint);
SelectAssemblyInstallationPointCommand = new RelayCommand(async () => await ExecuteSelectAssemblyInstallationPointAsync(), () => CanSelectAssemblyInstallationPoint);
ClearAssemblyReferenceRodCommand = new RelayCommand(() => ExecuteClearAssemblyReferenceRod());
AnalyzeAssemblyTerminalFaceCommand = new RelayCommand(async () => await ExecuteAnalyzeAssemblyTerminalFaceAsync(), () => CanAnalyzeAssemblyTerminalFace);
@@ -1568,8 +1669,10 @@ namespace NavisworksTransport.UI.WPF.ViewModels
ClearAssemblyInstallationReferenceVisuals();
OnPropertyChanged(nameof(CanGenerateAssemblyReferenceRod));
OnPropertyChanged(nameof(CanSelectAssemblyStartPoint));
+ OnPropertyChanged(nameof(CanRepositionRailStartPoint));
OnPropertyChanged(nameof(CanSelectAssemblyInstallationPoint));
OnPropertyChanged(nameof(CanAnalyzeAssemblyTerminalFace));
+ RefreshSelectedRailReferenceLineVisuals($"已捕获终点箱体: {AssemblyTerminalObjectName}");
UpdateMainStatus($"已捕获终点箱体: {AssemblyTerminalObjectName}");
LogManager.Info($"[直线装配] 已捕获终点箱体: {AssemblyTerminalObjectName}");
}, "捕获终点箱体");
@@ -1625,6 +1728,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
UpdateMainStatus("已生成终端安装辅助线,请在三维视图中确认终点锚点、方向、外端位置和辅助线贴合情况");
OnPropertyChanged(nameof(CanSelectAssemblyStartPoint));
+ OnPropertyChanged(nameof(CanRepositionRailStartPoint));
LogManager.Info(
$"[直线装配] 已生成参考杆: {AssemblyTerminalObjectName}, " +
$"终点锚点=({referenceLine.EndPoint.X:F2}, {referenceLine.EndPoint.Y:F2}, {referenceLine.EndPoint.Z:F2}), " +
@@ -1674,14 +1778,63 @@ namespace NavisworksTransport.UI.WPF.ViewModels
}, "选择终端安装起点");
}
+ private async Task ExecuteRepositionRailStartPointAsync()
+ {
+ await SafeExecuteAsync(() =>
+ {
+ if (_pathPlanningManager == null)
+ {
+ throw new InvalidOperationException("路径规划管理器未初始化,无法重选起点。");
+ }
+
+ if (_assemblyTerminalObject == null || !ModelItemAnalysisHelper.IsModelItemValid(_assemblyTerminalObject))
+ {
+ throw new InvalidOperationException("终点箱体未设置或已失效,请先捕获终点箱体。");
+ }
+
+ var selectedRailRoute = GetSelectedCoreRoute();
+ if (selectedRailRoute == null || selectedRailRoute.PathType != PathType.Rail)
+ {
+ throw new InvalidOperationException("请先选中一条需要编辑起点的 Rail 路径。");
+ }
+
+ RefreshSelectedRailReferenceLineVisuals();
+ if (!AssemblyReferencePathManager.Instance.HasReferenceLine)
+ {
+ throw new InvalidOperationException("当前 Rail 路径无法生成辅助线,请先检查路径起点和终点。");
+ }
+
+ CleanupAssemblyReferenceSelection();
+
+ _pathPlanningManager.DisableMouseHandling();
+ _isEditingSelectedRailStartPoint = true;
+ IsSelectingAssemblyStartPoint = true;
+ OnPropertyChanged(nameof(CanSelectAssemblyStartPoint));
+ OnPropertyChanged(nameof(CanRepositionRailStartPoint));
+ OnPropertyChanged(nameof(CanRepositionRailStartPoint));
+
+ PathClickToolPlugin.MouseClicked -= OnAssemblyReferenceMouseClicked;
+ PathClickToolPlugin.MouseClicked += OnAssemblyReferenceMouseClicked;
+
+ if (!ForceReinitializeToolPlugin(subscribeToEvents: false))
+ {
+ CleanupAssemblyReferenceSelection();
+ throw new InvalidOperationException("ToolPlugin 初始化失败,请重试。");
+ }
+
+ _pathPlanningManager.StartClickTool(PathPointType.StartPoint);
+ UpdateMainStatus("请在辅助线附近点击新的起点位置,系统会自动吸附到辅助线上并更新当前 Rail 路径。");
+ LogManager.Info("[Rail构型] 已进入起点重选模式");
+ }, "重选 Rail 路径起点");
+ }
+
private void ExecuteClearAssemblyReferenceRod()
{
try
{
- HideAssemblyReferenceVisuals(
- resetStartPointText: false,
- statusMessage: "已隐藏终端安装辅助线",
- logMessage: "[直线装配] 已隐藏辅助线");
+ ClearAssemblyReferenceLineVisuals();
+ UpdateMainStatus("已隐藏终端安装辅助线");
+ LogManager.Info("[直线装配] 已隐藏辅助线");
}
catch (Exception ex)
{
@@ -1724,6 +1877,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
_isSelectingAssemblyEndFacePoints = true;
OnPropertyChanged(nameof(CanAnalyzeAssemblyTerminalFace));
OnPropertyChanged(nameof(CanSelectAssemblyStartPoint));
+ OnPropertyChanged(nameof(CanRepositionRailStartPoint));
OnPropertyChanged(nameof(CanSelectAssemblyInstallationPoint));
PathClickToolPlugin.MouseClicked -= OnAssemblyEndFaceMouseClicked;
@@ -1763,6 +1917,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
_isSelectingAssemblyInstallationPoint = true;
OnPropertyChanged(nameof(CanAnalyzeAssemblyTerminalFace));
OnPropertyChanged(nameof(CanSelectAssemblyStartPoint));
+ OnPropertyChanged(nameof(CanRepositionRailStartPoint));
OnPropertyChanged(nameof(CanSelectAssemblyInstallationPoint));
PathClickToolPlugin.MouseClicked -= OnAssemblyInstallationMouseClicked;
@@ -1798,9 +1953,18 @@ namespace NavisworksTransport.UI.WPF.ViewModels
await SafeExecuteAsync(() =>
{
- CreateAssemblyLinearRoute(projectedStartPoint);
+ if (_isEditingSelectedRailStartPoint)
+ {
+ UpdateSelectedRailRouteStartPoint(projectedStartPoint);
+ UpdateMainStatus("已根据辅助线更新当前 Rail 路径起点");
+ }
+ else
+ {
+ CreateAssemblyLinearRoute(projectedStartPoint);
+ UpdateMainStatus("已根据辅助线起点生成终端安装路径");
+ }
+
CleanupAssemblyReferenceSelection();
- UpdateMainStatus("已根据辅助线起点生成终端安装路径");
}, "生成终端安装路径");
}
catch (Exception ex)
@@ -1970,14 +2134,44 @@ namespace NavisworksTransport.UI.WPF.ViewModels
$"{(route.RailPreferredNormal != null ? $",安装法向=({route.RailPreferredNormal.X:F3}, {route.RailPreferredNormal.Y:F3}, {route.RailPreferredNormal.Z:F3})" : string.Empty)}");
}
+ private void UpdateSelectedRailRouteStartPoint(Point3D startPoint)
+ {
+ var route = GetSelectedCoreRoute();
+ if (route == null || route.PathType != PathType.Rail)
+ {
+ throw new InvalidOperationException("当前没有可更新起点的 Rail 路径。");
+ }
+
+ int startPointIndex = GetRailRouteStartPointIndex(route);
+ if (startPointIndex < 0)
+ {
+ throw new InvalidOperationException("当前 Rail 路径没有可更新的起点。");
+ }
+
+ if (!_pathPlanningManager.UpdatePathPointWithConstraints(route, startPointIndex, startPoint))
+ {
+ throw new InvalidOperationException("更新 Rail 路径起点失败。");
+ }
+
+ route.LastModified = DateTime.Now;
+ _pathPlanningManager.SavePathToDatabase(route);
+ _pathPlanningManager.DrawRouteVisualization(route, isAutoPath: false);
+ _pathPlanningManager.RaiseVisualizationStateChanged();
+
+ if (SelectedPathRoute != null && SelectedPathRoute.Id == route.Id)
+ {
+ SyncPathViewModelFromCoreRoute(SelectedPathRoute, route, preserveSelection: true);
+ RefreshSelectedRailReferenceLineVisuals();
+ }
+
+ LogManager.Info($"[Rail构型] {route.Name}: 已重选起点为 ({startPoint.X:F3}, {startPoint.Y:F3}, {startPoint.Z:F3})");
+ }
+
private void HideAssemblyReferenceVisuals(bool resetStartPointText, string statusMessage, string logMessage)
{
CleanupAssemblyReferenceSelection();
CleanupAssemblyEndFaceSelection(clearVisuals: false);
- AssemblyReferencePathManager.Instance.HideReferenceRod();
- ClearAssemblyAnchorMarker();
- ClearAssemblyCenterGuideLine();
- ClearAssemblyReferenceLine();
+ ClearAssemblyReferenceLineVisuals();
ClearAssemblyEndFaceAnalysisVisuals();
ClearAssemblyInstallationReferenceVisuals();
if (resetStartPointText)
@@ -2717,6 +2911,16 @@ namespace NavisworksTransport.UI.WPF.ViewModels
renderPlugin.ClearRailBaseline(AssemblyCenterGuideLinePathId);
}
+ private void ClearAssemblyReferenceLineVisuals()
+ {
+ AssemblyReferencePathManager.Instance.HideReferenceRod();
+ ClearAssemblyAnchorMarker();
+ ClearAssemblyCenterGuideLine();
+ ClearAssemblyReferenceLine();
+ OnPropertyChanged(nameof(CanSelectAssemblyStartPoint));
+ OnPropertyChanged(nameof(CanRepositionRailStartPoint));
+ }
+
private void ClearAssemblyEndFaceAnalysisVisuals()
{
var renderPlugin = PathPointRenderPlugin.Instance;
@@ -2775,9 +2979,11 @@ namespace NavisworksTransport.UI.WPF.ViewModels
{
PathClickToolPlugin.MouseClicked -= OnAssemblyReferenceMouseClicked;
IsSelectingAssemblyStartPoint = false;
+ _isEditingSelectedRailStartPoint = false;
_pathPlanningManager?.EnableMouseHandling();
_pathPlanningManager?.StopClickTool();
OnPropertyChanged(nameof(CanSelectAssemblyStartPoint));
+ OnPropertyChanged(nameof(CanRepositionRailStartPoint));
}
catch (Exception ex)
{
@@ -2796,6 +3002,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
ClearAssemblyInstallationReferenceVisuals();
OnPropertyChanged(nameof(CanAnalyzeAssemblyTerminalFace));
OnPropertyChanged(nameof(CanSelectAssemblyStartPoint));
+ OnPropertyChanged(nameof(CanRepositionRailStartPoint));
OnPropertyChanged(nameof(CanSelectAssemblyInstallationPoint));
OnPropertyChanged(nameof(AssemblyInstallationPointButtonText));
}
@@ -2821,6 +3028,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
}
OnPropertyChanged(nameof(CanAnalyzeAssemblyTerminalFace));
OnPropertyChanged(nameof(CanSelectAssemblyStartPoint));
+ OnPropertyChanged(nameof(CanRepositionRailStartPoint));
OnPropertyChanged(nameof(CanSelectAssemblyInstallationPoint));
OnPropertyChanged(nameof(AssemblyInstallationPointButtonText));
}
diff --git a/src/UI/WPF/Views/PathEditingView.xaml b/src/UI/WPF/Views/PathEditingView.xaml
index b9c1eac..0249bdd 100644
--- a/src/UI/WPF/Views/PathEditingView.xaml
+++ b/src/UI/WPF/Views/PathEditingView.xaml
@@ -571,9 +571,15 @@ NavisworksTransport 路径编辑页签视图 - 采用与动画控制和分层管
Style="{StaticResource ReadOnlyTextBoxStyle}"/>
+
private void OnCopyCenterClick(object sender, RoutedEventArgs e)
{
- try
- {
- string coordinate = string.Format("{0:0.000}, {1:0.000}, {2:0.000}", CenterX, CenterY, CenterZ);
- System.Windows.Clipboard.SetText(coordinate);
- ShowButtonFeedback(CopyCenterButton);
- }
- catch (Exception ex)
- {
- LogManager.Debug($"[坐标窗口] 复制到剪贴板失败: {ex.Message}");
- }
+ string coordinate = string.Format("{0:0.000}, {1:0.000}, {2:0.000}", CenterX, CenterY, CenterZ);
+ bool success = ClipboardHelper.TrySetText(coordinate, "元素包围盒信息");
+ ShowButtonFeedback(CopyCenterButton, success);
}
///
@@ -209,16 +202,9 @@ namespace NavisworksTransport.UI.WPF.Views
///
private void OnCopyTopCenterClick(object sender, RoutedEventArgs e)
{
- try
- {
- string coordinate = string.Format("{0:0.000}, {1:0.000}, {2:0.000}", TopCenterX, TopCenterY, TopCenterZ);
- System.Windows.Clipboard.SetText(coordinate);
- ShowButtonFeedback(CopyTopButton);
- }
- catch (Exception ex)
- {
- LogManager.Debug($"[坐标窗口] 复制到剪贴板失败: {ex.Message}");
- }
+ string coordinate = string.Format("{0:0.000}, {1:0.000}, {2:0.000}", TopCenterX, TopCenterY, TopCenterZ);
+ bool success = ClipboardHelper.TrySetText(coordinate, "元素包围盒信息");
+ ShowButtonFeedback(CopyTopButton, success);
}
///
@@ -226,33 +212,32 @@ namespace NavisworksTransport.UI.WPF.Views
///
private void OnCopyBottomCenterClick(object sender, RoutedEventArgs e)
{
- try
- {
- string coordinate = string.Format("{0:0.000}, {1:0.000}, {2:0.000}", BottomCenterX, BottomCenterY, BottomCenterZ);
- System.Windows.Clipboard.SetText(coordinate);
- ShowButtonFeedback(CopyBottomButton);
- }
- catch (Exception ex)
- {
- LogManager.Debug($"[坐标窗口] 复制到剪贴板失败: {ex.Message}");
- }
+ string coordinate = string.Format("{0:0.000}, {1:0.000}, {2:0.000}", BottomCenterX, BottomCenterY, BottomCenterZ);
+ bool success = ClipboardHelper.TrySetText(coordinate, "元素包围盒信息");
+ ShowButtonFeedback(CopyBottomButton, success);
}
///
- /// 显示按钮点击反馈(绿色背景闪烁)
+ /// 显示按钮点击反馈
///
- private void ShowButtonFeedback(Button button)
+ private void ShowButtonFeedback(Button button, bool success)
{
if (button == null) return;
var originalBackground = button.Background;
var originalBorderBrush = button.BorderBrush;
- // 变为绿色表示成功
- button.Background = new SolidColorBrush(System.Windows.Media.Color.FromRgb(76, 175, 80));
- button.BorderBrush = new SolidColorBrush(System.Windows.Media.Color.FromRgb(56, 142, 60));
+ if (success)
+ {
+ button.Background = new SolidColorBrush(System.Windows.Media.Color.FromRgb(76, 175, 80));
+ button.BorderBrush = new SolidColorBrush(System.Windows.Media.Color.FromRgb(56, 142, 60));
+ }
+ else
+ {
+ button.Background = new SolidColorBrush(System.Windows.Media.Color.FromRgb(244, 67, 54));
+ button.BorderBrush = new SolidColorBrush(System.Windows.Media.Color.FromRgb(211, 47, 47));
+ }
- // 800毫秒后恢复
var timer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(800) };
timer.Tick += (s, e) =>
{
diff --git a/src/Utils/ClipboardHelper.cs b/src/Utils/ClipboardHelper.cs
new file mode 100644
index 0000000..c6fed38
--- /dev/null
+++ b/src/Utils/ClipboardHelper.cs
@@ -0,0 +1,68 @@
+using System;
+using System.Threading;
+using System.Windows;
+
+namespace NavisworksTransport
+{
+ ///
+ /// 剪贴板工具方法
+ ///
+ public static class ClipboardHelper
+ {
+ private const int MaxAttempts = 6;
+ private const int RetryDelayMilliseconds = 80;
+
+ ///
+ /// 尝试写入文本到剪贴板
+ ///
+ public static bool TrySetText(string text, string context = null)
+ {
+ string clipboardText = text ?? string.Empty;
+ Exception lastException = null;
+
+ for (int attempt = 0; attempt < MaxAttempts; attempt++)
+ {
+ if (TrySetTextOnStaThread(clipboardText, out lastException))
+ {
+ return true;
+ }
+
+ if (attempt < MaxAttempts - 1)
+ {
+ Thread.Sleep(RetryDelayMilliseconds);
+ }
+ }
+
+ string prefix = string.IsNullOrWhiteSpace(context) ? "[剪贴板]" : $"[{context}]";
+ LogManager.Debug($"{prefix} 写入剪贴板失败: {lastException?.Message ?? "未知错误"}");
+ return false;
+ }
+
+ private static bool TrySetTextOnStaThread(string text, out Exception exception)
+ {
+ bool success = false;
+ Exception capturedException = null;
+
+ Thread thread = new Thread(() =>
+ {
+ try
+ {
+ Clipboard.SetDataObject(text, true);
+ success = true;
+ }
+ catch (Exception ex)
+ {
+ capturedException = ex;
+ }
+ });
+
+ thread.SetApartmentState(ApartmentState.STA);
+ thread.IsBackground = true;
+ thread.Start();
+ thread.Join();
+
+ exception = capturedException;
+ return success;
+ }
+ }
+}
From 382923fa325b8a04d882f3b1397827c1cfc7658e Mon Sep 17 00:00:00 2001
From: tian <11429339@qq.com>
Date: Sun, 29 Mar 2026 01:17:06 +0800
Subject: [PATCH 43/87] Relax fragment pose parsing and add fallback
---
.../RealObjectReferencePoseResolverTests.cs | 27 ++++++
src/Core/Animation/PathAnimationManager.cs | 93 ++++++++++++++-----
.../RealObjectReferencePoseResolver.cs | 86 +++++++++--------
3 files changed, 144 insertions(+), 62 deletions(-)
diff --git a/UnitTests/CoordinateSystem/RealObjectReferencePoseResolverTests.cs b/UnitTests/CoordinateSystem/RealObjectReferencePoseResolverTests.cs
index 5d6d355..0df379c 100644
--- a/UnitTests/CoordinateSystem/RealObjectReferencePoseResolverTests.cs
+++ b/UnitTests/CoordinateSystem/RealObjectReferencePoseResolverTests.cs
@@ -91,6 +91,33 @@ namespace NavisworksTransport.UnitTests.CoordinateSystem
Assert.AreEqual(1.0, zAlignment, 1e-6);
}
+ [TestMethod]
+ public void YUp_WorldAxisMapping_ShouldAcceptSlightlyRotatedRawFragmentFrame()
+ {
+ var fragmentMatrices = new List
+ {
+ CreateMatrix(
+ axisX: Vector3.Normalize(new Vector3(0.9980f, 0.0638f, 0.0f)),
+ axisY: new Vector3(0.0f, 0.0f, -1.0f),
+ axisZ: Vector3.Normalize(new Vector3(-0.0638f, 0.9980f, 0.0f)))
+ };
+
+ bool ok = RealObjectReferencePoseResolver.TryResolveFromFragmentMatrices(
+ fragmentMatrices,
+ fragmentDefaultUpAxis: "Y",
+ hostUpAxis: "Y",
+ out RealObjectReferencePose pose);
+
+ Assert.IsTrue(ok);
+ Assert.AreEqual(LocalAxisDirection.PositiveX, pose.HostWorldXAxisLocalAxis);
+ Assert.AreEqual(LocalAxisDirection.PositiveZ, pose.HostWorldYAxisLocalAxis);
+ Assert.AreEqual(LocalAxisDirection.NegativeY, pose.HostWorldZAxisLocalAxis);
+ Assert.AreEqual(LocalAxisDirection.PositiveX, pose.HostSemanticXAxisLocalAxis);
+ Assert.AreEqual(LocalAxisDirection.PositiveY, pose.HostSemanticYAxisLocalAxis);
+ Assert.AreEqual(LocalAxisDirection.PositiveZ, pose.HostSemanticZAxisLocalAxis);
+ Assert.AreEqual(LocalAxisDirection.PositiveY, pose.HostUpLocalAxis);
+ }
+
private static double[] CreateMatrix(Vector3 axisX, Vector3 axisY, Vector3 axisZ)
{
return new double[]
diff --git a/src/Core/Animation/PathAnimationManager.cs b/src/Core/Animation/PathAnimationManager.cs
index 2c87bc5..3fbc733 100644
--- a/src/Core/Animation/PathAnimationManager.cs
+++ b/src/Core/Animation/PathAnimationManager.cs
@@ -4764,35 +4764,15 @@ namespace NavisworksTransport.Core.Animation
return true;
}
- NotifyRealObjectReferencePoseFailure(_animatedObject);
- return false;
+ return TryCaptureFallbackRealObjectReferenceRotation(_animatedObject, out referenceRotation);
}
catch (Exception ex)
{
- LogManager.Error($"[真实物体参考姿态] 读取原始参考位姿失败: {ex.Message}");
- return false;
+ LogManager.Warning($"[真实物体参考姿态] 读取 fragment 参考位姿失败,改用默认姿态: {ex.Message}");
+ return TryCaptureFallbackRealObjectReferenceRotation(_animatedObject, out referenceRotation);
}
}
- private void NotifyRealObjectReferencePoseFailure(ModelItem sourceObject)
- {
- string objectName = sourceObject?.DisplayName ?? "未命名对象";
- string message =
- $"无法从 fragment 提取真实物体参考姿态,已中止起点姿态计算。\n\n" +
- $"对象:{objectName}\n" +
- $"建议:\n" +
- $"1. 检查该真实物体是否存在可用的 fragment 几何。\n" +
- $"2. 重新选择该对象后再试。\n" +
- $"3. 如果是 Revit 导入件,请确认 fragment 结构没有异常简化或丢失。";
-
- LogManager.Error($"[真实物体参考姿态] {objectName} fragment 代表姿态提取失败,已中止。");
- DialogHelper.ShowMessageBox(
- message,
- "fragment 代表姿态提取失败",
- MessageBoxButton.OK,
- MessageBoxImage.Error);
- }
-
private bool TryCaptureRealObjectReferenceRotation(ModelItem sourceObject, out Quaternion referenceRotation)
{
referenceRotation = Quaternion.Identity;
@@ -4845,6 +4825,73 @@ namespace NavisworksTransport.Core.Animation
}
}
+ private bool TryCaptureFallbackRealObjectReferenceRotation(ModelItem sourceObject, out Quaternion referenceRotation)
+ {
+ referenceRotation = Quaternion.Identity;
+
+ if (sourceObject == null)
+ {
+ return false;
+ }
+
+ try
+ {
+ Rotation3D fallbackRotation3D = sourceObject.Transform?.Factor().Rotation ?? Rotation3D.Identity;
+ referenceRotation = new Quaternion(
+ (float)fallbackRotation3D.A,
+ (float)fallbackRotation3D.B,
+ (float)fallbackRotation3D.C,
+ (float)fallbackRotation3D.D);
+
+ if (referenceRotation.LengthSquared() < 1e-6f)
+ {
+ referenceRotation = Quaternion.Identity;
+ }
+ else
+ {
+ referenceRotation = Quaternion.Normalize(referenceRotation);
+ }
+
+ _realObjectReferenceRotation = referenceRotation;
+ _realObjectReferenceAxisX = Vector3.Normalize(Vector3.Transform(Vector3.UnitX, referenceRotation));
+ _realObjectReferenceAxisY = Vector3.Normalize(Vector3.Transform(Vector3.UnitY, referenceRotation));
+ _realObjectReferenceAxisZ = Vector3.Normalize(Vector3.Transform(Vector3.UnitZ, referenceRotation));
+ _realObjectReferenceHostWorldXAxisLocalAxis = LocalAxisDirection.PositiveX;
+ _realObjectReferenceHostWorldYAxisLocalAxis = LocalAxisDirection.PositiveY;
+ _realObjectReferenceHostWorldZAxisLocalAxis = LocalAxisDirection.PositiveZ;
+ _realObjectReferenceHostSemanticXAxisLocalAxis = LocalAxisDirection.PositiveX;
+
+ var adapter = CoordinateSystemManager.Instance.CreateHostAdapter();
+ if (adapter.HostType == CoordinateSystemType.YUp)
+ {
+ _realObjectReferenceHostSemanticYAxisLocalAxis = LocalAxisDirection.PositiveY;
+ _realObjectReferenceHostSemanticZAxisLocalAxis = LocalAxisDirection.PositiveZ;
+ _realObjectReferenceHostUpLocalAxis = LocalAxisDirection.PositiveY;
+ }
+ else
+ {
+ _realObjectReferenceHostSemanticYAxisLocalAxis = LocalAxisDirection.PositiveY;
+ _realObjectReferenceHostSemanticZAxisLocalAxis = LocalAxisDirection.PositiveZ;
+ _realObjectReferenceHostUpLocalAxis = LocalAxisDirection.PositiveZ;
+ }
+
+ _hasRealObjectReferenceRotation = true;
+
+ LogManager.Warning(
+ $"[真实物体参考姿态] {sourceObject.DisplayName} fragment 姿态不可用,已退回默认参考姿态继续业务流程: " +
+ $"FallbackX=({_realObjectReferenceAxisX.X:F4},{_realObjectReferenceAxisX.Y:F4},{_realObjectReferenceAxisX.Z:F4}), " +
+ $"FallbackY=({_realObjectReferenceAxisY.X:F4},{_realObjectReferenceAxisY.Y:F4},{_realObjectReferenceAxisY.Z:F4}), " +
+ $"FallbackZ=({_realObjectReferenceAxisZ.X:F4},{_realObjectReferenceAxisZ.Y:F4},{_realObjectReferenceAxisZ.Z:F4}), " +
+ $"宿主Up对应本地轴={_realObjectReferenceHostUpLocalAxis}");
+ return true;
+ }
+ catch (Exception ex)
+ {
+ LogManager.Error($"[真实物体参考姿态] {sourceObject.DisplayName} 默认参考姿态回退失败: {ex.Message}");
+ return false;
+ }
+ }
+
private void SyncTrackedRotationToObjectReference(ModelItem sourceObject, bool isVirtualObject)
{
if (sourceObject == null)
diff --git a/src/Utils/CoordinateSystem/RealObjectReferencePoseResolver.cs b/src/Utils/CoordinateSystem/RealObjectReferencePoseResolver.cs
index 767daf4..b21dfa4 100644
--- a/src/Utils/CoordinateSystem/RealObjectReferencePoseResolver.cs
+++ b/src/Utils/CoordinateSystem/RealObjectReferencePoseResolver.cs
@@ -169,17 +169,35 @@ namespace NavisworksTransport.Utils.CoordinateSystem
hostWorldYAxisLocalAxis = LocalAxisDirection.PositiveY;
hostWorldZAxisLocalAxis = LocalAxisDirection.PositiveZ;
- if (!TryMatchRawAxisDirection(Vector3.UnitX, rawFrame.AxisX, rawFrame.AxisY, rawFrame.AxisZ, out hostWorldXAxisLocalAxis))
+ if (!TryMatchRawAxisDirection(
+ Vector3.UnitX,
+ rawFrame.AxisX,
+ rawFrame.AxisY,
+ rawFrame.AxisZ,
+ 0.5f,
+ out hostWorldXAxisLocalAxis))
{
return false;
}
- if (!TryMatchRawAxisDirection(Vector3.UnitY, rawFrame.AxisX, rawFrame.AxisY, rawFrame.AxisZ, out hostWorldYAxisLocalAxis))
+ if (!TryMatchRawAxisDirection(
+ Vector3.UnitY,
+ rawFrame.AxisX,
+ rawFrame.AxisY,
+ rawFrame.AxisZ,
+ 0.5f,
+ out hostWorldYAxisLocalAxis))
{
return false;
}
- if (!TryMatchRawAxisDirection(Vector3.UnitZ, rawFrame.AxisX, rawFrame.AxisY, rawFrame.AxisZ, out hostWorldZAxisLocalAxis))
+ if (!TryMatchRawAxisDirection(
+ Vector3.UnitZ,
+ rawFrame.AxisX,
+ rawFrame.AxisY,
+ rawFrame.AxisZ,
+ 0.5f,
+ out hostWorldZAxisLocalAxis))
{
return false;
}
@@ -203,6 +221,7 @@ namespace NavisworksTransport.Utils.CoordinateSystem
rawFrame.AxisX,
rawFrame.AxisY,
rawFrame.AxisZ,
+ 0.95f,
out hostSemanticXAxisLocalAxis))
{
return false;
@@ -213,6 +232,7 @@ namespace NavisworksTransport.Utils.CoordinateSystem
rawFrame.AxisX,
rawFrame.AxisY,
rawFrame.AxisZ,
+ 0.95f,
out hostSemanticYAxisLocalAxis))
{
return false;
@@ -223,6 +243,7 @@ namespace NavisworksTransport.Utils.CoordinateSystem
rawFrame.AxisX,
rawFrame.AxisY,
rawFrame.AxisZ,
+ 0.95f,
out hostSemanticZAxisLocalAxis))
{
return false;
@@ -236,48 +257,35 @@ namespace NavisworksTransport.Utils.CoordinateSystem
Vector3 rawAxisX,
Vector3 rawAxisY,
Vector3 rawAxisZ,
+ float minimumAlignment,
out LocalAxisDirection axisDirection)
{
axisDirection = LocalAxisDirection.PositiveX;
- const float tolerance = 1e-4f;
+ float bestAlignment = float.NegativeInfinity;
- if (Vector3.Distance(targetAxis, rawAxisX) < tolerance)
+ TryUpdateBestMatch(targetAxis, rawAxisX, LocalAxisDirection.PositiveX, ref bestAlignment, ref axisDirection);
+ TryUpdateBestMatch(targetAxis, -rawAxisX, LocalAxisDirection.NegativeX, ref bestAlignment, ref axisDirection);
+ TryUpdateBestMatch(targetAxis, rawAxisY, LocalAxisDirection.PositiveY, ref bestAlignment, ref axisDirection);
+ TryUpdateBestMatch(targetAxis, -rawAxisY, LocalAxisDirection.NegativeY, ref bestAlignment, ref axisDirection);
+ TryUpdateBestMatch(targetAxis, rawAxisZ, LocalAxisDirection.PositiveZ, ref bestAlignment, ref axisDirection);
+ TryUpdateBestMatch(targetAxis, -rawAxisZ, LocalAxisDirection.NegativeZ, ref bestAlignment, ref axisDirection);
+
+ return bestAlignment >= minimumAlignment;
+ }
+
+ private static void TryUpdateBestMatch(
+ Vector3 targetAxis,
+ Vector3 candidateAxis,
+ LocalAxisDirection candidateDirection,
+ ref float bestAlignment,
+ ref LocalAxisDirection bestDirection)
+ {
+ float alignment = Vector3.Dot(Vector3.Normalize(targetAxis), Vector3.Normalize(candidateAxis));
+ if (alignment > bestAlignment)
{
- axisDirection = LocalAxisDirection.PositiveX;
- return true;
+ bestAlignment = alignment;
+ bestDirection = candidateDirection;
}
-
- if (Vector3.Distance(targetAxis, -rawAxisX) < tolerance)
- {
- axisDirection = LocalAxisDirection.NegativeX;
- return true;
- }
-
- if (Vector3.Distance(targetAxis, rawAxisY) < tolerance)
- {
- axisDirection = LocalAxisDirection.PositiveY;
- return true;
- }
-
- if (Vector3.Distance(targetAxis, -rawAxisY) < tolerance)
- {
- axisDirection = LocalAxisDirection.NegativeY;
- return true;
- }
-
- if (Vector3.Distance(targetAxis, rawAxisZ) < tolerance)
- {
- axisDirection = LocalAxisDirection.PositiveZ;
- return true;
- }
-
- if (Vector3.Distance(targetAxis, -rawAxisZ) < tolerance)
- {
- axisDirection = LocalAxisDirection.NegativeZ;
- return true;
- }
-
- return false;
}
public static bool TryDetectDefaultUpAxis(
From 766b5af887f53910b96060aa9c0d15abe60df258 Mon Sep 17 00:00:00 2001
From: tian <11429339@qq.com>
Date: Sun, 29 Mar 2026 18:28:41 +0800
Subject: [PATCH 44/87] Stop auto-switching to path view before animation
playback
---
.../ViewModels/AnimationControlViewModel.cs | 38 -------------------
1 file changed, 38 deletions(-)
diff --git a/src/UI/WPF/ViewModels/AnimationControlViewModel.cs b/src/UI/WPF/ViewModels/AnimationControlViewModel.cs
index 242fcd0..5b7049f 100644
--- a/src/UI/WPF/ViewModels/AnimationControlViewModel.cs
+++ b/src/UI/WPF/ViewModels/AnimationControlViewModel.cs
@@ -1500,25 +1500,6 @@ namespace NavisworksTransport.UI.WPF.ViewModels
{
try
{
- // 自动调整视角到路径中心
- try
- {
- var coreRoute = CurrentPathRoute?.Route;
- if (coreRoute != null)
- {
- ViewpointHelper.AdjustViewpointToPathCenter(coreRoute);
- LogManager.Info($"正向播放前:已自动调整视角到路径中心: {CurrentPathRoute.Name}");
- }
- else
- {
- LogManager.Warning($"正向播放前:CurrentPathRoute.Route 为 null");
- }
- }
- catch (Exception ex)
- {
- LogManager.Error($"正向播放前:调整视角失败: {ex.Message}");
- }
-
_pathAnimationManager?.PlayForward();
UpdateMediaControlProperties();
LogManager.Info("执行正向播放命令");
@@ -1537,25 +1518,6 @@ namespace NavisworksTransport.UI.WPF.ViewModels
{
try
{
- // 自动调整视角到路径中心
- try
- {
- var coreRoute = CurrentPathRoute?.Route;
- if (coreRoute != null)
- {
- ViewpointHelper.AdjustViewpointToPathCenter(coreRoute);
- LogManager.Info($"反向播放前:已自动调整视角到路径中心: {CurrentPathRoute.Name}");
- }
- else
- {
- LogManager.Warning($"反向播放前:CurrentPathRoute.Route 为 null");
- }
- }
- catch (Exception ex)
- {
- LogManager.Error($"反向播放前:调整视角失败: {ex.Message}");
- }
-
_pathAnimationManager?.PlayReverse();
UpdateMediaControlProperties();
LogManager.Info("执行反向播放命令");
From 909337439995e04d326aaf92d9d3a86a84c86f67 Mon Sep 17 00:00:00 2001
From: tian <11429339@qq.com>
Date: Sun, 29 Mar 2026 23:36:43 +0800
Subject: [PATCH 45/87] Use actual hoisting pose baseline for real objects
---
NavisworksTransport.UnitTests.csproj | 1 +
TransportPlugin.csproj | 1 +
.../HoistingRealObjectPoseHelperTests.cs | 73 ++++++++++
.../RealObjectReferencePoseResolverTests.cs | 21 +++
src/Core/Animation/PathAnimationManager.cs | 64 +++++++++
.../HoistingRealObjectPoseHelper.cs | 135 ++++++++++++++++++
.../RealObjectReferencePoseResolver.cs | 38 +++--
7 files changed, 320 insertions(+), 13 deletions(-)
create mode 100644 UnitTests/CoordinateSystem/HoistingRealObjectPoseHelperTests.cs
create mode 100644 src/Utils/CoordinateSystem/HoistingRealObjectPoseHelper.cs
diff --git a/NavisworksTransport.UnitTests.csproj b/NavisworksTransport.UnitTests.csproj
index aa14ce5..4fefe8e 100644
--- a/NavisworksTransport.UnitTests.csproj
+++ b/NavisworksTransport.UnitTests.csproj
@@ -65,6 +65,7 @@
+
diff --git a/TransportPlugin.csproj b/TransportPlugin.csproj
index a697ca9..bc517ad 100644
--- a/TransportPlugin.csproj
+++ b/TransportPlugin.csproj
@@ -345,6 +345,7 @@
+
diff --git a/UnitTests/CoordinateSystem/HoistingRealObjectPoseHelperTests.cs b/UnitTests/CoordinateSystem/HoistingRealObjectPoseHelperTests.cs
new file mode 100644
index 0000000..90ca32f
--- /dev/null
+++ b/UnitTests/CoordinateSystem/HoistingRealObjectPoseHelperTests.cs
@@ -0,0 +1,73 @@
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using NavisworksTransport.Utils.CoordinateSystem;
+using System.Numerics;
+
+namespace NavisworksTransport.UnitTests.CoordinateSystem
+{
+ [TestClass]
+ public class HoistingRealObjectPoseHelperTests
+ {
+ [TestMethod]
+ public void ShouldKeepActualUpAxis_WhenAligningHoistingForward()
+ {
+ Quaternion actualRotation = Quaternion.Normalize(new Quaternion(0.70710677f, 0.0f, 0.0f, 0.70710677f));
+ Vector3 hostUp = Vector3.UnitY;
+ Vector3 targetForward = -Vector3.UnitX;
+
+ bool ok = HoistingRealObjectPoseHelper.TryCreateRotationFromActualPose(
+ actualRotation,
+ targetForward,
+ hostUp,
+ out Quaternion resultRotation,
+ out LocalAxisDirection selectedDirection,
+ out Vector3 selectedAxisLocal,
+ out Vector3 projectedForward);
+
+ Assert.IsTrue(ok);
+ Assert.AreEqual(LocalAxisDirection.PositiveX, selectedDirection);
+ AssertVector(selectedAxisLocal, 1.0, 0.0, 0.0);
+ AssertVector(projectedForward, 1.0, 0.0, 0.0);
+
+ Vector3 transformedUp = Vector3.Normalize(Vector3.Transform(-Vector3.UnitZ, resultRotation));
+ AssertVector(transformedUp, 0.0, 1.0, 0.0, 1e-4);
+
+ Vector3 transformedForward = Vector3.Normalize(Vector3.Transform(Vector3.UnitX, resultRotation));
+ AssertVector(transformedForward, 1.0, 0.0, 0.0, 1e-4);
+ }
+
+ [TestMethod]
+ public void ShouldChooseHorizontalAxisFromActualPose_ForHoisting()
+ {
+ Quaternion actualRotation = Quaternion.Normalize(new Quaternion(0.70710677f, 0.0f, 0.0f, 0.70710677f));
+ Vector3 hostUp = Vector3.UnitY;
+ Vector3 targetForward = Vector3.UnitX;
+
+ bool ok = HoistingRealObjectPoseHelper.TryCreateRotationFromActualPose(
+ actualRotation,
+ targetForward,
+ hostUp,
+ out Quaternion resultRotation,
+ out LocalAxisDirection selectedDirection,
+ out Vector3 selectedAxisLocal,
+ out Vector3 projectedForward);
+
+ Assert.IsTrue(ok);
+ Assert.AreEqual(LocalAxisDirection.PositiveX, selectedDirection);
+ AssertVector(selectedAxisLocal, 1.0, 0.0, 0.0);
+ AssertVector(projectedForward, 1.0, 0.0, 0.0);
+
+ Vector3 transformedUp = Vector3.Normalize(Vector3.Transform(-Vector3.UnitZ, resultRotation));
+ AssertVector(transformedUp, 0.0, 1.0, 0.0, 1e-4);
+
+ Vector3 transformedForward = Vector3.Normalize(Vector3.Transform(Vector3.UnitX, resultRotation));
+ AssertVector(transformedForward, 1.0, 0.0, 0.0, 1e-4);
+ }
+
+ private static void AssertVector(Vector3 actual, double x, double y, double z, double tolerance = 1e-6)
+ {
+ Assert.AreEqual(x, actual.X, tolerance);
+ Assert.AreEqual(y, actual.Y, tolerance);
+ Assert.AreEqual(z, actual.Z, tolerance);
+ }
+ }
+}
diff --git a/UnitTests/CoordinateSystem/RealObjectReferencePoseResolverTests.cs b/UnitTests/CoordinateSystem/RealObjectReferencePoseResolverTests.cs
index 0df379c..0faa079 100644
--- a/UnitTests/CoordinateSystem/RealObjectReferencePoseResolverTests.cs
+++ b/UnitTests/CoordinateSystem/RealObjectReferencePoseResolverTests.cs
@@ -118,6 +118,27 @@ namespace NavisworksTransport.UnitTests.CoordinateSystem
Assert.AreEqual(LocalAxisDirection.PositiveY, pose.HostUpLocalAxis);
}
+ [TestMethod]
+ public void YUp_WorldAxisMapping_ShouldRejectAmbiguousRawFragmentFrame()
+ {
+ var fragmentMatrices = new List
+ {
+ CreateMatrix(
+ axisX: Vector3.Normalize(new Vector3(0.80f, 0.60f, 0.0f)),
+ axisY: new Vector3(0.0f, 0.0f, -1.0f),
+ axisZ: Vector3.Normalize(new Vector3(-0.60f, 0.80f, 0.0f)))
+ };
+
+ bool ok = RealObjectReferencePoseResolver.TryResolveFromFragmentMatrices(
+ fragmentMatrices,
+ fragmentDefaultUpAxis: "Y",
+ hostUpAxis: "Y",
+ out RealObjectReferencePose pose);
+
+ Assert.IsFalse(ok);
+ Assert.IsNull(pose);
+ }
+
private static double[] CreateMatrix(Vector3 axisX, Vector3 axisY, Vector3 axisZ)
{
return new double[]
diff --git a/src/Core/Animation/PathAnimationManager.cs b/src/Core/Animation/PathAnimationManager.cs
index 3fbc733..8ac0584 100644
--- a/src/Core/Animation/PathAnimationManager.cs
+++ b/src/Core/Animation/PathAnimationManager.cs
@@ -4609,6 +4609,12 @@ namespace NavisworksTransport.Core.Animation
{
rotation = Rotation3D.Identity;
+ if (_route?.PathType == PathType.Hoisting &&
+ TryCreateHoistingRealObjectRotationFromActualPose(hostForward, out rotation))
+ {
+ return true;
+ }
+
if (!PathTargetFrameResolver.TryCreatePlanarHostFrame(
hostForward,
CoordinateSystemManager.Instance.CreateHostAdapter().HostType,
@@ -4646,6 +4652,64 @@ namespace NavisworksTransport.Core.Animation
return true;
}
+ private bool TryCreateHoistingRealObjectRotationFromActualPose(Vector3 hostForward, out Rotation3D rotation)
+ {
+ rotation = Rotation3D.Identity;
+
+ if (_animatedObject == null ||
+ !ModelItemTransformHelper.TryGetCurrentGeometryRotation(_animatedObject, out var actualRotation3D))
+ {
+ return false;
+ }
+
+ var actualRotation = new Quaternion(
+ (float)actualRotation3D.A,
+ (float)actualRotation3D.B,
+ (float)actualRotation3D.C,
+ (float)actualRotation3D.D);
+
+ var adapter = CoordinateSystemManager.Instance.CreateHostAdapter();
+ Vector3 hostUp = adapter.HostType == CoordinateSystemType.YUp ? Vector3.UnitY : Vector3.UnitZ;
+ if (!HoistingRealObjectPoseHelper.TryCreateRotationFromActualPose(
+ actualRotation,
+ hostForward,
+ hostUp,
+ out var baselineQuaternion,
+ out var selectedAxisDirection,
+ out var selectedAxisLocal,
+ out var projectedForward))
+ {
+ return false;
+ }
+
+ rotation = adapter.FromHostQuaternionDirect(
+ adapter.ComposeHostQuaternion(baselineQuaternion, _objectRotationCorrection));
+
+ Matrix4x4 baselineLinear = Matrix4x4.CreateFromQuaternion(baselineQuaternion);
+ Matrix4x4 hostComposedLinear = Matrix4x4.CreateFromQuaternion(new Quaternion(
+ (float)rotation.A,
+ (float)rotation.B,
+ (float)rotation.C,
+ (float)rotation.D));
+
+ _realObjectPlanarSelectedForwardAxis = selectedAxisDirection;
+ _hasRealObjectPlanarSelectedForwardAxis = true;
+
+ LogManager.Info(
+ $"[真实物体平面前进轴] Hoisting 已改用实际几何姿态基线,选中对象轴={selectedAxisDirection}。");
+ LogManager.Info(
+ $"[真实物体起点姿态] 选中参考轴=({selectedAxisLocal.X:F3},{selectedAxisLocal.Y:F3},{selectedAxisLocal.Z:F3}), " +
+ $"投影前进=({projectedForward.X:F3},{projectedForward.Y:F3},{projectedForward.Z:F3}), " +
+ $"宿主修正={_objectRotationCorrection}, 本地修正=X=0.0°,Y=0.0°,Z=0.0°, " +
+ $"BaselineX=({baselineLinear.M11:F4},{baselineLinear.M21:F4},{baselineLinear.M31:F4}), " +
+ $"BaselineY=({baselineLinear.M12:F4},{baselineLinear.M22:F4},{baselineLinear.M32:F4}), " +
+ $"BaselineZ=({baselineLinear.M13:F4},{baselineLinear.M23:F4},{baselineLinear.M33:F4}), " +
+ $"HostComposeX=({hostComposedLinear.M11:F4},{hostComposedLinear.M21:F4},{hostComposedLinear.M31:F4}), " +
+ $"HostComposeY=({hostComposedLinear.M12:F4},{hostComposedLinear.M22:F4},{hostComposedLinear.M32:F4}), " +
+ $"HostComposeZ=({hostComposedLinear.M13:F4},{hostComposedLinear.M23:F4},{hostComposedLinear.M33:F4})");
+ return true;
+ }
+
private bool TryCreateRealObjectPlanarPoseSolution(
Vector3 targetForward,
Vector3 targetUp,
diff --git a/src/Utils/CoordinateSystem/HoistingRealObjectPoseHelper.cs b/src/Utils/CoordinateSystem/HoistingRealObjectPoseHelper.cs
new file mode 100644
index 0000000..bd23473
--- /dev/null
+++ b/src/Utils/CoordinateSystem/HoistingRealObjectPoseHelper.cs
@@ -0,0 +1,135 @@
+using System;
+using System.Numerics;
+
+namespace NavisworksTransport.Utils.CoordinateSystem
+{
+ public static class HoistingRealObjectPoseHelper
+ {
+ public static bool TryCreateRotationFromActualPose(
+ Quaternion actualRotation,
+ Vector3 targetForward,
+ Vector3 hostUp,
+ out Quaternion resultRotation,
+ out LocalAxisDirection selectedForwardAxisDirection,
+ out Vector3 selectedForwardAxisLocal,
+ out Vector3 projectedForward)
+ {
+ resultRotation = Quaternion.Identity;
+ selectedForwardAxisDirection = LocalAxisDirection.PositiveX;
+ selectedForwardAxisLocal = Vector3.UnitX;
+ projectedForward = Vector3.Zero;
+
+ if (actualRotation.LengthSquared() < 1e-6f)
+ {
+ return false;
+ }
+
+ Vector3 normalizedHostUp = NormalizeSafe(hostUp);
+ if (normalizedHostUp.LengthSquared() < 1e-6f)
+ {
+ return false;
+ }
+
+ Vector3 normalizedTargetForward = NormalizeOnPlane(targetForward, normalizedHostUp);
+ if (normalizedTargetForward.LengthSquared() < 1e-6f)
+ {
+ return false;
+ }
+
+ Quaternion normalizedActualRotation = Quaternion.Normalize(actualRotation);
+ Vector3[] localAxes =
+ {
+ Vector3.UnitX,
+ Vector3.UnitY,
+ Vector3.UnitZ
+ };
+ LocalAxisDirection[] directions =
+ {
+ LocalAxisDirection.PositiveX,
+ LocalAxisDirection.PositiveY,
+ LocalAxisDirection.PositiveZ
+ };
+
+ float bestScore = float.NegativeInfinity;
+ Vector3 bestProjectedAxis = Vector3.Zero;
+
+ for (int i = 0; i < localAxes.Length; i++)
+ {
+ Vector3 worldAxis = Vector3.Transform(localAxes[i], normalizedActualRotation);
+ float upAlignment = Math.Abs(Vector3.Dot(Vector3.Normalize(worldAxis), normalizedHostUp));
+ if (upAlignment > 0.9f)
+ {
+ continue;
+ }
+
+ Vector3 projectedAxis = NormalizeOnPlane(worldAxis, normalizedHostUp);
+ if (projectedAxis.LengthSquared() < 1e-6f)
+ {
+ continue;
+ }
+
+ float score = Math.Abs(Vector3.Dot(projectedAxis, normalizedTargetForward));
+ if (score > bestScore)
+ {
+ bestScore = score;
+ bestProjectedAxis = projectedAxis;
+ selectedForwardAxisDirection = directions[i];
+ selectedForwardAxisLocal = localAxes[i];
+ }
+ }
+
+ if (bestScore == float.NegativeInfinity)
+ {
+ return false;
+ }
+
+ Vector3 resolvedForward = Vector3.Dot(bestProjectedAxis, normalizedTargetForward) >= 0.0f
+ ? normalizedTargetForward
+ : -normalizedTargetForward;
+
+ projectedForward = resolvedForward;
+ float signedAngle = SignedAngleAroundAxis(bestProjectedAxis, resolvedForward, normalizedHostUp);
+ Quaternion deltaRotation = Quaternion.CreateFromAxisAngle(normalizedHostUp, signedAngle);
+ resultRotation = Quaternion.Normalize(deltaRotation * normalizedActualRotation);
+ return true;
+ }
+
+ private static Vector3 NormalizeOnPlane(Vector3 vector, Vector3 planeNormal)
+ {
+ Vector3 planar = vector - Vector3.Dot(vector, planeNormal) * planeNormal;
+ return NormalizeSafe(planar);
+ }
+
+ private static Vector3 NormalizeSafe(Vector3 vector)
+ {
+ if (vector.LengthSquared() < 1e-6f)
+ {
+ return Vector3.Zero;
+ }
+
+ return Vector3.Normalize(vector);
+ }
+
+ private static float SignedAngleAroundAxis(Vector3 from, Vector3 to, Vector3 axis)
+ {
+ float dot = Vector3.Dot(from, to);
+ if (dot > 1.0f)
+ {
+ dot = 1.0f;
+ }
+ else if (dot < -1.0f)
+ {
+ dot = -1.0f;
+ }
+
+ float angle = (float)Math.Acos(dot);
+ float sign = Math.Sign(Vector3.Dot(axis, Vector3.Cross(from, to)));
+ if (Math.Abs(sign) < 1e-6f)
+ {
+ sign = 1.0f;
+ }
+
+ return angle * sign;
+ }
+ }
+}
diff --git a/src/Utils/CoordinateSystem/RealObjectReferencePoseResolver.cs b/src/Utils/CoordinateSystem/RealObjectReferencePoseResolver.cs
index b21dfa4..13ab441 100644
--- a/src/Utils/CoordinateSystem/RealObjectReferencePoseResolver.cs
+++ b/src/Utils/CoordinateSystem/RealObjectReferencePoseResolver.cs
@@ -17,6 +17,10 @@ namespace NavisworksTransport.Utils.CoordinateSystem
///
public static class RealObjectReferencePoseResolver
{
+ private const float WorldAxisMinimumAlignment = 0.98f;
+ private const float SemanticAxisMinimumAlignment = 0.985f;
+ private const float MinimumBestToSecondGap = 0.02f;
+
public static bool TryResolveFromModelItem(
ModelItem sourceObject,
Document doc,
@@ -174,7 +178,7 @@ namespace NavisworksTransport.Utils.CoordinateSystem
rawFrame.AxisX,
rawFrame.AxisY,
rawFrame.AxisZ,
- 0.5f,
+ WorldAxisMinimumAlignment,
out hostWorldXAxisLocalAxis))
{
return false;
@@ -185,7 +189,7 @@ namespace NavisworksTransport.Utils.CoordinateSystem
rawFrame.AxisX,
rawFrame.AxisY,
rawFrame.AxisZ,
- 0.5f,
+ WorldAxisMinimumAlignment,
out hostWorldYAxisLocalAxis))
{
return false;
@@ -196,7 +200,7 @@ namespace NavisworksTransport.Utils.CoordinateSystem
rawFrame.AxisX,
rawFrame.AxisY,
rawFrame.AxisZ,
- 0.5f,
+ WorldAxisMinimumAlignment,
out hostWorldZAxisLocalAxis))
{
return false;
@@ -221,7 +225,7 @@ namespace NavisworksTransport.Utils.CoordinateSystem
rawFrame.AxisX,
rawFrame.AxisY,
rawFrame.AxisZ,
- 0.95f,
+ SemanticAxisMinimumAlignment,
out hostSemanticXAxisLocalAxis))
{
return false;
@@ -232,7 +236,7 @@ namespace NavisworksTransport.Utils.CoordinateSystem
rawFrame.AxisX,
rawFrame.AxisY,
rawFrame.AxisZ,
- 0.95f,
+ SemanticAxisMinimumAlignment,
out hostSemanticYAxisLocalAxis))
{
return false;
@@ -243,7 +247,7 @@ namespace NavisworksTransport.Utils.CoordinateSystem
rawFrame.AxisX,
rawFrame.AxisY,
rawFrame.AxisZ,
- 0.95f,
+ SemanticAxisMinimumAlignment,
out hostSemanticZAxisLocalAxis))
{
return false;
@@ -262,15 +266,17 @@ namespace NavisworksTransport.Utils.CoordinateSystem
{
axisDirection = LocalAxisDirection.PositiveX;
float bestAlignment = float.NegativeInfinity;
+ float secondBestAlignment = float.NegativeInfinity;
- TryUpdateBestMatch(targetAxis, rawAxisX, LocalAxisDirection.PositiveX, ref bestAlignment, ref axisDirection);
- TryUpdateBestMatch(targetAxis, -rawAxisX, LocalAxisDirection.NegativeX, ref bestAlignment, ref axisDirection);
- TryUpdateBestMatch(targetAxis, rawAxisY, LocalAxisDirection.PositiveY, ref bestAlignment, ref axisDirection);
- TryUpdateBestMatch(targetAxis, -rawAxisY, LocalAxisDirection.NegativeY, ref bestAlignment, ref axisDirection);
- TryUpdateBestMatch(targetAxis, rawAxisZ, LocalAxisDirection.PositiveZ, ref bestAlignment, ref axisDirection);
- TryUpdateBestMatch(targetAxis, -rawAxisZ, LocalAxisDirection.NegativeZ, ref bestAlignment, ref axisDirection);
+ TryUpdateBestMatch(targetAxis, rawAxisX, LocalAxisDirection.PositiveX, ref bestAlignment, ref secondBestAlignment, ref axisDirection);
+ TryUpdateBestMatch(targetAxis, -rawAxisX, LocalAxisDirection.NegativeX, ref bestAlignment, ref secondBestAlignment, ref axisDirection);
+ TryUpdateBestMatch(targetAxis, rawAxisY, LocalAxisDirection.PositiveY, ref bestAlignment, ref secondBestAlignment, ref axisDirection);
+ TryUpdateBestMatch(targetAxis, -rawAxisY, LocalAxisDirection.NegativeY, ref bestAlignment, ref secondBestAlignment, ref axisDirection);
+ TryUpdateBestMatch(targetAxis, rawAxisZ, LocalAxisDirection.PositiveZ, ref bestAlignment, ref secondBestAlignment, ref axisDirection);
+ TryUpdateBestMatch(targetAxis, -rawAxisZ, LocalAxisDirection.NegativeZ, ref bestAlignment, ref secondBestAlignment, ref axisDirection);
- return bestAlignment >= minimumAlignment;
+ return bestAlignment >= minimumAlignment
+ && bestAlignment - secondBestAlignment >= MinimumBestToSecondGap;
}
private static void TryUpdateBestMatch(
@@ -278,14 +284,20 @@ namespace NavisworksTransport.Utils.CoordinateSystem
Vector3 candidateAxis,
LocalAxisDirection candidateDirection,
ref float bestAlignment,
+ ref float secondBestAlignment,
ref LocalAxisDirection bestDirection)
{
float alignment = Vector3.Dot(Vector3.Normalize(targetAxis), Vector3.Normalize(candidateAxis));
if (alignment > bestAlignment)
{
+ secondBestAlignment = bestAlignment;
bestAlignment = alignment;
bestDirection = candidateDirection;
}
+ else if (alignment > secondBestAlignment)
+ {
+ secondBestAlignment = alignment;
+ }
}
public static bool TryDetectDefaultUpAxis(
From 1d71d36e5fb1195ada95add7d2777d787ad5eda4 Mon Sep 17 00:00:00 2001
From: tian <11429339@qq.com>
Date: Mon, 30 Mar 2026 22:13:16 +0800
Subject: [PATCH 46/87] =?UTF-8?q?=E4=BF=AE=E5=A4=8Drail=E8=B7=AF=E5=BE=84?=
=?UTF-8?q?=E7=9A=84=E7=BC=96=E8=BE=91=E7=8A=B6=E6=80=81=E9=97=AE=E9=A2=98?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
AGENTS.md | 44 ++++++
src/UI/WPF/ViewModels/PathEditingViewModel.cs | 138 ++++++++++++++++--
src/UI/WPF/Views/PathEditingView.xaml | 4 +-
src/Utils/ModelItemTransformHelper.cs | 4 +-
4 files changed, 173 insertions(+), 17 deletions(-)
diff --git a/AGENTS.md b/AGENTS.md
index b58d67a..0bc6113 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -133,6 +133,50 @@
禁止再使用“本地坐标系”这种含糊说法。
+### 4.1.2 坐标命名规则
+
+以后凡是变量名、字段名、日志名里出现 `X/Y/Z`、正负轴、forward/up/side 等方向语义,必须在名字上直接带出所属坐标系,避免只看名字时无法判断语义层。
+
+允许的前缀示例:
+
+- `Host...`
+ - 宿主坐标系
+- `Canonical...`
+ - 内部坐标系
+- `Asset...`
+ - 资产坐标系
+- `Local...`
+ - 仅当这里明确表示“对象自身局部轴”时才允许使用
+
+正负轴命名示例:
+
+- `LocalPositiveX`
+- `LocalNegativeY`
+- `HostPositiveZ`
+- `CanonicalPositiveX`
+- `AssetPositiveY`
+
+方向/向量命名示例:
+
+- `hostForward`
+- `hostUp`
+- `canonicalForward`
+- `assetUp`
+- `localXAxis`
+- `localZAxis`
+
+禁止继续使用这类脱离坐标系语义的名字:
+
+- `PositiveX`
+- `NegativeZ`
+- `xAxis`
+- `upAxis`
+- `forwardAxis`
+
+除非变量名里已经明确出现了所属坐标系前缀。
+
+目标是做到:只看名字,就能知道这个方向、轴、向量到底属于宿主、内部、资产,还是对象自身局部轴。
+
### 4.1.1 Quaternion / Rotation3D 固定定义
这是项目级硬约束,不允许在任何修复中重新猜测、重新验证或临时改口:
diff --git a/src/UI/WPF/ViewModels/PathEditingViewModel.cs b/src/UI/WPF/ViewModels/PathEditingViewModel.cs
index 35213ad..a650edd 100644
--- a/src/UI/WPF/ViewModels/PathEditingViewModel.cs
+++ b/src/UI/WPF/ViewModels/PathEditingViewModel.cs
@@ -11,7 +11,6 @@ using System.IO;
using System.Numerics;
using Microsoft.Win32;
using Autodesk.Navisworks.Api;
-using Autodesk.Navisworks.Api.Plugins;
using NavisApplication = Autodesk.Navisworks.Api.Application;
using NavisworksTransport.Core;
using NavisworksTransport.Core.Config;
@@ -21,7 +20,6 @@ using NavisworksTransport.UI.WPF.Models;
using NavisworksTransport.Utils;
using NavisworksTransport.Utils.CoordinateSystem;
using NavisworksTransport.Utils.GeometryAnalysis;
-using NavisworksTransport;
namespace NavisworksTransport.UI.WPF.ViewModels
{
@@ -145,6 +143,8 @@ namespace NavisworksTransport.UI.WPF.ViewModels
private bool _isEditingSelectedRailStartPoint;
private bool _isSelectingAssemblyEndFacePoints;
private bool _isSelectingAssemblyInstallationPoint;
+ private bool _isEditingSelectedRailInstallationPoint;
+ private bool _isEditingSelectedRail;
private bool _hasAssemblyEndFaceAnalysis;
private bool _hasAssemblyInstallationReference;
private ModelItem _assemblyTerminalObject;
@@ -218,6 +218,10 @@ namespace NavisworksTransport.UI.WPF.ViewModels
ClearAssemblyReferenceLineVisuals();
ClearAssemblyInstallationReferenceVisuals();
+ // 重置 Rail 编辑状态
+ _isEditingSelectedRail = false;
+ OnPropertyChanged(nameof(AssemblyInstallationPointButtonText));
+
// 🔧 修复:同步PathPlanningManager的CurrentRoute
if (_pathPlanningManager != null && value != null)
{
@@ -1066,6 +1070,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
public ICommand ExportPathCommand { get; private set; }
public ICommand SaveAsPathCommand { get; private set; }
public ICommand CaptureAssemblyTerminalObjectCommand { get; private set; }
+ public ICommand ReCaptureAssemblyTerminalObjectCommand { get; private set; }
public ICommand GenerateAssemblyReferenceRodCommand { get; private set; }
public ICommand SelectAssemblyStartPointCommand { get; private set; }
public ICommand RepositionRailStartPointCommand { get; private set; }
@@ -1291,6 +1296,12 @@ namespace NavisworksTransport.UI.WPF.ViewModels
private bool HasSelectedRailInstallationReference()
{
+ // 只有在点击"重选箱体"进入编辑状态后才显示"重选安装点"
+ if (!_isEditingSelectedRail)
+ {
+ return false;
+ }
+
var coreRoute = GetSelectedCoreRoute();
return coreRoute != null &&
coreRoute.PathType == PathType.Rail &&
@@ -1376,11 +1387,30 @@ namespace NavisworksTransport.UI.WPF.ViewModels
return;
}
- var coreRoute = GetSelectedCoreRoute();
- if (!TryBuildRailRouteReferenceLine(coreRoute, out AssemblyReferenceLine referenceLine))
+ AssemblyReferenceLine referenceLine;
+
+ if (_isEditingSelectedRail)
{
- ClearAssemblyReferenceLineVisuals();
- return;
+ // 编辑状态:使用默认杆长度构建参考线,方便选择更远的起点
+ try
+ {
+ referenceLine = BuildAssemblyReferenceLine();
+ }
+ catch
+ {
+ ClearAssemblyReferenceLineVisuals();
+ return;
+ }
+ }
+ else
+ {
+ // 非编辑状态:使用路径实际长度
+ var coreRoute = GetSelectedCoreRoute();
+ if (!TryBuildRailRouteReferenceLine(coreRoute, out referenceLine))
+ {
+ ClearAssemblyReferenceLineVisuals();
+ return;
+ }
}
AssemblyReferencePathManager.Instance.CreateOrUpdateReferenceRod(
@@ -1422,11 +1452,31 @@ namespace NavisworksTransport.UI.WPF.ViewModels
throw new InvalidOperationException("当前 Rail 路径没有可更新的终点。");
}
+ // 获取当前起点索引
+ int startPointIndex = GetRailRouteStartPointIndex(route);
+ if (startPointIndex < 0)
+ {
+ throw new InvalidOperationException("当前 Rail 路径没有可更新的起点。");
+ }
+
+ // 计算原路径长度(起点到终点的距离)
+ Point3D currentStartPoint = route.Points[startPointIndex].Position;
+ Point3D currentEndPoint = route.Points[installationPointIndex].Position;
+ double pathLength = GeometryHelper.CalculatePointDistance(currentStartPoint, currentEndPoint);
+
+ // 更新终点(安装点)
if (!_pathPlanningManager.UpdatePathPointWithConstraints(route, installationPointIndex, _assemblyInstallationAnchorPoint))
{
throw new InvalidOperationException("更新 Rail 路径安装点失败。");
}
+ // 根据新终点和原路径长度重新计算起点位置
+ Point3D newStartPoint = CalculateRailStartPointFromEndPoint(_assemblyInstallationAnchorPoint, pathLength);
+ if (!_pathPlanningManager.UpdatePathPointWithConstraints(route, startPointIndex, newStartPoint))
+ {
+ throw new InvalidOperationException("更新 Rail 路径起点失败。");
+ }
+
route.RailPreferredNormal = new Point3D(
_assemblyInstallationPlaneNormal.X,
_assemblyInstallationPlaneNormal.Y,
@@ -1449,7 +1499,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
RefreshSelectedRailReferenceLineVisuals();
}
- LogManager.Info($"[Rail构型] {route.Name}: {logMessage}");
+ LogManager.Info($"[Rail构型] {route.Name}: {logMessage},起点已同步更新为 ({newStartPoint.X:F3}, {newStartPoint.Y:F3}, {newStartPoint.Z:F3})");
}
private void RefreshSelectedRailInstallationReferenceVisuals()
@@ -1622,6 +1672,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
ExportPathCommand = new RelayCommand(async () => await ExecuteExportPathAsync(), () => CanExecuteExportPath);
SaveAsPathCommand = new RelayCommand(async () => await ExecuteSaveAsPathAsync(), () => CanExecuteSaveAsPath);
CaptureAssemblyTerminalObjectCommand = new RelayCommand(async () => await ExecuteCaptureAssemblyTerminalObjectAsync());
+ ReCaptureAssemblyTerminalObjectCommand = new RelayCommand(async () => await ExecuteReCaptureAssemblyTerminalObjectAsync());
GenerateAssemblyReferenceRodCommand = new RelayCommand(async () => await ExecuteGenerateAssemblyReferenceRodAsync(), () => CanGenerateAssemblyReferenceRod);
SelectAssemblyStartPointCommand = new RelayCommand(async () => await ExecuteSelectAssemblyStartPointAsync(), () => CanSelectAssemblyStartPoint);
RepositionRailStartPointCommand = new RelayCommand(async () => await ExecuteRepositionRailStartPointAsync(), () => CanRepositionRailStartPoint);
@@ -1656,6 +1707,9 @@ namespace NavisworksTransport.UI.WPF.ViewModels
throw new InvalidOperationException("当前选择对象无效,请重新选择终点箱体");
}
+ // 注意:不在这里设置 _isEditingSelectedRail
+ // 新建路径时保持 false,只有点击"重选箱体"时才设置为 true
+
_assemblyTerminalObject = selectedItem;
_assemblyStartPoint = Point3D.Origin;
HasAssemblyTerminalObject = true;
@@ -1672,12 +1726,24 @@ namespace NavisworksTransport.UI.WPF.ViewModels
OnPropertyChanged(nameof(CanRepositionRailStartPoint));
OnPropertyChanged(nameof(CanSelectAssemblyInstallationPoint));
OnPropertyChanged(nameof(CanAnalyzeAssemblyTerminalFace));
- RefreshSelectedRailReferenceLineVisuals($"已捕获终点箱体: {AssemblyTerminalObjectName}");
+ OnPropertyChanged(nameof(AssemblyInstallationPointButtonText));
+
+ // 新建路径时隐藏其他路径的辅助线
+ ClearAssemblyReferenceLineVisuals();
+
UpdateMainStatus($"已捕获终点箱体: {AssemblyTerminalObjectName}");
LogManager.Info($"[直线装配] 已捕获终点箱体: {AssemblyTerminalObjectName}");
}, "捕获终点箱体");
}
+ private async Task ExecuteReCaptureAssemblyTerminalObjectAsync()
+ {
+ // 先进入编辑状态,再执行捕获
+ _isEditingSelectedRail = true;
+ OnPropertyChanged(nameof(AssemblyInstallationPointButtonText));
+ await ExecuteCaptureAssemblyTerminalObjectAsync();
+ }
+
private void ClearNonGridPathVisualizations(string context)
{
if (PathPointRenderPlugin.Instance == null)
@@ -1915,6 +1981,11 @@ namespace NavisworksTransport.UI.WPF.ViewModels
_pathPlanningManager.DisableMouseHandling();
_isSelectingAssemblyInstallationPoint = true;
+
+ // 判断是编辑现有路径还是创建新路径
+ var selectedRailRoute = GetSelectedCoreRoute();
+ _isEditingSelectedRailInstallationPoint = selectedRailRoute != null && selectedRailRoute.PathType == PathType.Rail;
+
OnPropertyChanged(nameof(CanAnalyzeAssemblyTerminalFace));
OnPropertyChanged(nameof(CanSelectAssemblyStartPoint));
OnPropertyChanged(nameof(CanRepositionRailStartPoint));
@@ -2048,12 +2119,22 @@ namespace NavisworksTransport.UI.WPF.ViewModels
BuildAndRenderAssemblyInstallationReference(
_assemblyInstallationSeedPoints[0],
_assemblyInstallationSeedPoints[1]);
- var selectedRailRoute = GetSelectedCoreRoute();
- if (selectedRailRoute != null && selectedRailRoute.PathType == PathType.Rail)
+
+ if (_isEditingSelectedRailInstallationPoint)
{
- PersistAssemblyInstallationReferenceToRailRoute(
- selectedRailRoute,
- "已重选安装点并同步更新 Rail 路径安装参数");
+ // 编辑现有路径:更新安装点并同步起点
+ var selectedRailRoute = GetSelectedCoreRoute();
+ if (selectedRailRoute != null && selectedRailRoute.PathType == PathType.Rail)
+ {
+ PersistAssemblyInstallationReferenceToRailRoute(
+ selectedRailRoute,
+ "已重选安装点并同步更新 Rail 路径安装参数");
+ }
+ }
+ else
+ {
+ // 创建新路径:生成参考线,等待用户选择起点
+ LogManager.Info("[直线装配] 安装点已确定,请继续选择起点以生成新路径");
}
CleanupAssemblyInstallationSelection();
}, "处理安装点拾取");
@@ -2224,6 +2305,36 @@ namespace NavisworksTransport.UI.WPF.ViewModels
return adapter.FromCanonicalPoint(canonicalAnchorPoint);
}
+ ///
+ /// 根据 Rail 路径终点和路径长度计算起点位置。
+ /// 起点位于从终点沿光轴方向(远离球心方向)延伸路径长度的位置。
+ ///
+ private Point3D CalculateRailStartPointFromEndPoint(Point3D endPoint, double pathLength)
+ {
+ HostCoordinateAdapter adapter = CoordinateSystemManager.Instance.CreateHostAdapter();
+ ProjectReferenceFrame projectFrame = CreateAssemblyProjectReferenceFrame(adapter);
+
+ Point3D opticalAxisReferencePoint = GetAssemblyOpticalAxisReferencePoint();
+ Point3D canonicalEndPoint = adapter.ToCanonicalPoint(endPoint);
+ Point3D canonicalOpticalAxisRef = adapter.ToCanonicalPoint(opticalAxisReferencePoint);
+
+ // 计算从球心到光轴参考点的方向(作为参考线方向)
+ Vector3D direction = new Vector3D(
+ canonicalOpticalAxisRef.X - projectFrame.SphereCenterInCanonical.X,
+ canonicalOpticalAxisRef.Y - projectFrame.SphereCenterInCanonical.Y,
+ canonicalOpticalAxisRef.Z - projectFrame.SphereCenterInCanonical.Z);
+
+ direction = direction.Normalize();
+
+ // 起点 = 终点 + 方向 * 路径长度
+ Point3D canonicalStartPoint = new Point3D(
+ canonicalEndPoint.X + direction.X * pathLength,
+ canonicalEndPoint.Y + direction.Y * pathLength,
+ canonicalEndPoint.Z + direction.Z * pathLength);
+
+ return adapter.FromCanonicalPoint(canonicalStartPoint);
+ }
+
private void UpdateAssemblyInstallationAnchorFromVerticalOffset()
{
if (!_hasAssemblyInstallationReference ||
@@ -2997,6 +3108,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
{
PathClickToolPlugin.MouseClicked -= OnAssemblyInstallationMouseClicked;
_isSelectingAssemblyInstallationPoint = false;
+ _isEditingSelectedRailInstallationPoint = false;
_pathPlanningManager?.EnableMouseHandling();
_pathPlanningManager?.StopClickTool();
ClearAssemblyInstallationReferenceVisuals();
diff --git a/src/UI/WPF/Views/PathEditingView.xaml b/src/UI/WPF/Views/PathEditingView.xaml
index 0249bdd..b17ac0d 100644
--- a/src/UI/WPF/Views/PathEditingView.xaml
+++ b/src/UI/WPF/Views/PathEditingView.xaml
@@ -560,10 +560,10 @@ NavisworksTransport 路径编辑页签视图 - 采用与动画控制和分层管
Orientation="Horizontal"
VerticalAlignment="Center"
ItemHeight="28">
-
Date: Mon, 30 Mar 2026 23:35:01 +0800
Subject: [PATCH 47/87] Preserve terminal pose when translating rail objects to
path start
---
.../CanonicalRailOffsetResolverTests.cs | 27 ++++
.../RailPreservedPoseTests.cs | 46 +++++++
src/Core/Animation/PathAnimationManager.cs | 128 ++++++++++++++----
.../ViewModels/AnimationControlViewModel.cs | 4 +-
src/Utils/RailPathPoseHelper.cs | 54 +++++++-
5 files changed, 228 insertions(+), 31 deletions(-)
create mode 100644 UnitTests/CoordinateSystem/RailPreservedPoseTests.cs
diff --git a/UnitTests/CoordinateSystem/CanonicalRailOffsetResolverTests.cs b/UnitTests/CoordinateSystem/CanonicalRailOffsetResolverTests.cs
index d3091af..288ee90 100644
--- a/UnitTests/CoordinateSystem/CanonicalRailOffsetResolverTests.cs
+++ b/UnitTests/CoordinateSystem/CanonicalRailOffsetResolverTests.cs
@@ -1,5 +1,6 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using NavisworksTransport.Core;
+using NavisworksTransport.Utils;
using NavisworksTransport.Utils.CoordinateSystem;
using System.Numerics;
@@ -112,6 +113,32 @@ namespace NavisworksTransport.UnitTests.CoordinateSystem
AssertVector(trackedCenter, 0.0, 0.0, 3.5);
}
+ [TestMethod]
+ public void PreservedTrackedCenterOffset_ShouldBeMeasuredFromRailSemanticCenter()
+ {
+ Vector3 semanticTrackedCenter = new Vector3(10f, 20f, 30f);
+ Vector3 actualTrackedCenter = new Vector3(11.25f, 19.5f, 30.75f);
+
+ Vector3 offset = RailPathPoseHelper.CalculatePreservedTrackedCenterOffset(
+ actualTrackedCenter,
+ semanticTrackedCenter);
+
+ AssertVector(offset, 1.25, -0.5, 0.75);
+ }
+
+ [TestMethod]
+ public void ResolvePreservedTrackedCenterPosition_ShouldReuseRailSemanticCenterPlusOffset()
+ {
+ Vector3 semanticTrackedCenter = new Vector3(3f, -2f, 8f);
+ Vector3 preservedOffset = new Vector3(0.2f, -0.4f, 0.6f);
+
+ Vector3 preservedTrackedCenter = RailPathPoseHelper.ResolvePreservedTrackedCenterPosition(
+ semanticTrackedCenter,
+ preservedOffset);
+
+ AssertVector(preservedTrackedCenter, 3.2, -2.4, 8.6);
+ }
+
private static void AssertVector(Vector3 actual, double x, double y, double z)
{
Assert.AreEqual(x, actual.X, 1e-6);
diff --git a/UnitTests/CoordinateSystem/RailPreservedPoseTests.cs b/UnitTests/CoordinateSystem/RailPreservedPoseTests.cs
new file mode 100644
index 0000000..b4212e1
--- /dev/null
+++ b/UnitTests/CoordinateSystem/RailPreservedPoseTests.cs
@@ -0,0 +1,46 @@
+using Autodesk.Navisworks.Api;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using NavisworksTransport.Core.Animation;
+
+namespace NavisworksTransport.UnitTests.CoordinateSystem
+{
+ [TestClass]
+ public class RailPreservedPoseTests
+ {
+ [TestMethod]
+ public void ResolvePreservedPoseRotation_ShouldPreferActualGeometryRotationForRealObjects()
+ {
+ Rotation3D fallbackRotation = new Rotation3D(0.0, 0.0, 0.0, 1.0);
+ Rotation3D actualGeometryRotation = new Rotation3D(0.0, 0.70710678, 0.0, 0.70710678);
+
+ Rotation3D resolved = PathAnimationManager.ResolvePreservedPoseRotation(
+ preferActualGeometryRotation: true,
+ hasActualGeometryRotation: true,
+ actualGeometryRotation: actualGeometryRotation,
+ fallbackRotation: fallbackRotation);
+
+ Assert.AreEqual(actualGeometryRotation.A, resolved.A, 1e-9);
+ Assert.AreEqual(actualGeometryRotation.B, resolved.B, 1e-9);
+ Assert.AreEqual(actualGeometryRotation.C, resolved.C, 1e-9);
+ Assert.AreEqual(actualGeometryRotation.D, resolved.D, 1e-9);
+ }
+
+ [TestMethod]
+ public void ResolvePreservedPoseRotation_ShouldFallbackWhenActualGeometryRotationUnavailable()
+ {
+ Rotation3D fallbackRotation = new Rotation3D(0.0, 0.0, 0.38268343, 0.92387953);
+ Rotation3D actualGeometryRotation = new Rotation3D(0.0, 0.70710678, 0.0, 0.70710678);
+
+ Rotation3D resolved = PathAnimationManager.ResolvePreservedPoseRotation(
+ preferActualGeometryRotation: true,
+ hasActualGeometryRotation: false,
+ actualGeometryRotation: actualGeometryRotation,
+ fallbackRotation: fallbackRotation);
+
+ Assert.AreEqual(fallbackRotation.A, resolved.A, 1e-9);
+ Assert.AreEqual(fallbackRotation.B, resolved.B, 1e-9);
+ Assert.AreEqual(fallbackRotation.C, resolved.C, 1e-9);
+ Assert.AreEqual(fallbackRotation.D, resolved.D, 1e-9);
+ }
+ }
+}
diff --git a/src/Core/Animation/PathAnimationManager.cs b/src/Core/Animation/PathAnimationManager.cs
index 8ac0584..0780bb8 100644
--- a/src/Core/Animation/PathAnimationManager.cs
+++ b/src/Core/Animation/PathAnimationManager.cs
@@ -754,7 +754,8 @@ namespace NavisworksTransport.Core.Animation
{
Point3D previousPoint = _pathPoints[0];
Point3D nextPoint = _pathPoints.Count > 1 ? _pathPoints[1] : _pathPoints[0];
- if (TryResolveRailPreservedPoseTrackedCenter(startPosition, out Point3D preservedTrackedCenter))
+ double objectHeight = GetAnimatedObjectRailNormalExtent(previousPoint, _pathPoints[0], nextPoint);
+ if (TryResolveRailPreservedPoseTrackedCenter(startPosition, previousPoint, nextPoint, objectHeight, out Point3D preservedTrackedCenter))
{
startPosition = preservedTrackedCenter;
LogManager.Debug(
@@ -763,7 +764,6 @@ namespace NavisworksTransport.Core.Animation
}
else
{
- double objectHeight = GetAnimatedObjectRailNormalExtent(previousPoint, _pathPoints[0], nextPoint);
startPosition = RailPathPoseHelper.ResolveObjectSpaceCenterPosition(_route, startPosition, previousPoint, nextPoint, objectHeight);
LogManager.Debug($"[移动到起点] Rail路径调整: 参考点=({_pathPoints[0].X:F2},{_pathPoints[0].Y:F2},{_pathPoints[0].Z:F2}), 物体中心=({startPosition.X:F2},{startPosition.Y:F2},{startPosition.Z:F2}), 物体高度={objectHeight:F2}, 安装={_route.RailMountMode}, 对接={(PathRoute.IsTopPayloadAnchorForMountMode(_route.RailMountMode) ? "顶面对接" : "底面对接")}");
}
@@ -947,12 +947,24 @@ namespace NavisworksTransport.Core.Animation
}
else if (_route.PathType == PathType.Rail)
{
- CaptureRailPreservedPoseTrackedCenterOffset();
Point3D previousPoint = _pathPoints[0];
Point3D nextPoint = _pathPoints.Count > 1 ? _pathPoints[1] : _pathPoints[0];
- if (!TryResolveRailPreservedPoseTrackedCenter(startPosition, out Point3D preservedTrackedCenter))
+ double objectHeight = GetAnimatedObjectRailNormalExtent(previousPoint, _pathPoints[0], nextPoint);
+
+ // Rail 平移模式不是重新求一套“轨上/轨下安装姿态”,
+ // 而是以终点处真实箱体当前位姿为真值,
+ // 记录其相对终点路径参考点的固定残差,再整体搬运到起点。
+ Point3D terminalReferencePoint = _pathPoints[_pathPoints.Count - 1];
+ Point3D terminalPreviousPoint = _pathPoints.Count > 1 ? _pathPoints[_pathPoints.Count - 2] : terminalReferencePoint;
+ Point3D terminalNextPoint = terminalReferencePoint;
+ CaptureRailPreservedPoseTrackedCenterOffset(
+ terminalPreviousPoint,
+ terminalReferencePoint,
+ terminalNextPoint,
+ objectHeight);
+
+ if (!TryResolveRailPreservedPoseTrackedCenter(startPosition, previousPoint, nextPoint, objectHeight, out Point3D preservedTrackedCenter))
{
- double objectHeight = GetAnimatedObjectRailNormalExtent(previousPoint, _pathPoints[0], nextPoint);
preservedTrackedCenter = RailPathPoseHelper.ResolveObjectSpaceCenterPosition(_route, startPosition, previousPoint, nextPoint, objectHeight);
}
@@ -964,13 +976,14 @@ namespace NavisworksTransport.Core.Animation
}
UpdateObjectPosition(startPosition);
+ SyncTrackedRotationToDisplayedPose(CurrentControlledObject ?? _animatedObject);
_hasGroundRealObjectBasePose = false;
_groundRealObjectStartCompensation = new Vector3D(0, 0, 0);
_hasGroundRealObjectStartCompensation = false;
var startAppliedPoint = GetTrackedObjectPosition(CurrentControlledObject ?? _animatedObject);
LogManager.Info(
- $"[平移到起点] 已保持初始位姿移动到起点: 路径point0=({pathStartPoint.X:F3},{pathStartPoint.Y:F3},{pathStartPoint.Z:F3}), " +
+ $"[平移到起点] 已按终点原始位姿残差搬运到起点: 路径point0=({pathStartPoint.X:F3},{pathStartPoint.Y:F3},{pathStartPoint.Z:F3}), " +
$"目标trackedPoint=({startPosition.X:F3},{startPosition.Y:F3},{startPosition.Z:F3}), " +
$"实际trackedPoint=({startAppliedPoint.X:F3},{startAppliedPoint.Y:F3},{startAppliedPoint.Z:F3}), 路径类型={_route.PathType.GetDisplayName()}");
return true;
@@ -1231,7 +1244,7 @@ namespace NavisworksTransport.Core.Animation
}
else if (_route.PathType == PathType.Rail)
{
- if (TryResolveRailPreservedPoseTrackedCenter(framePosition, out Point3D preservedTrackedCenter))
+ if (TryResolveRailPreservedPoseTrackedCenter(framePosition, p1, p2, objectHeight, out Point3D preservedTrackedCenter))
{
framePosition = preservedTrackedCenter;
LogManager.Debug(
@@ -2201,9 +2214,9 @@ namespace NavisworksTransport.Core.Animation
Point3D expectedTrackedEndPoint;
if (_route.PathType == PathType.Rail)
{
- if (!TryResolveRailPreservedPoseTrackedCenter(terminalAnchorPoint, out expectedTrackedEndPoint))
+ double objectHeight = GetAnimatedObjectRailNormalExtent(previousAnchorPoint, terminalAnchorPoint, terminalAnchorPoint);
+ if (!TryResolveRailPreservedPoseTrackedCenter(terminalAnchorPoint, previousAnchorPoint, terminalAnchorPoint, objectHeight, out expectedTrackedEndPoint))
{
- double objectHeight = GetAnimatedObjectRailNormalExtent(previousAnchorPoint, terminalAnchorPoint, terminalAnchorPoint);
expectedTrackedEndPoint = RailPathPoseHelper.ResolveObjectSpaceCenterPosition(
_route,
terminalAnchorPoint,
@@ -2746,7 +2759,6 @@ namespace NavisworksTransport.Core.Animation
Rotation3D currentRotation;
Rotation3D currentRotationForTransform;
Rotation3D actualGeometryRotation = Rotation3D.Identity;
- bool hasActualGeometryRotation = false;
Rotation3D appliedTargetRotation = newRotation;
Rotation3D targetRotationForTransform = newRotation;
Point3D appliedTargetPosition = newPosition;
@@ -2778,7 +2790,6 @@ namespace NavisworksTransport.Core.Animation
_route?.PathType == PathType.Rail) &&
ModelItemTransformHelper.TryGetCurrentGeometryRotation(controlledObject, out actualGeometryRotation))
{
- hasActualGeometryRotation = true;
var trackedLinear = new Transform3D(currentRotation).Linear;
var actualLinear = new Transform3D(actualGeometryRotation).Linear;
string poseTag = _route?.PathType == PathType.Rail
@@ -2834,7 +2845,7 @@ namespace NavisworksTransport.Core.Animation
newPosition.Y - rotatedCompensation.Y,
newPosition.Z - rotatedCompensation.Z);
- LogManager.Info(
+ LogManager.Debug(
$"[Ground路径补偿] {controlledObject.DisplayName} 原目标点=({newPosition.X:F3},{newPosition.Y:F3},{newPosition.Z:F3}), " +
$"起点补偿=({_groundRealObjectStartCompensation.X:F3},{_groundRealObjectStartCompensation.Y:F3},{_groundRealObjectStartCompensation.Z:F3}), " +
$"旋转后补偿=({rotatedCompensation.X:F3},{rotatedCompensation.Y:F3},{rotatedCompensation.Z:F3}), " +
@@ -2860,7 +2871,7 @@ namespace NavisworksTransport.Core.Animation
if (isRailRealObject)
{
var overrideLinear = new Transform3D(targetRotationForTransform).Linear;
- LogManager.Info(
+ LogManager.Debug(
$"[Rail覆盖姿态] {controlledObject.DisplayName} Override目标: " +
$"X=({overrideLinear.Get(0, 0):F4},{overrideLinear.Get(1, 0):F4},{overrideLinear.Get(2, 0):F4}), " +
$"Y=({overrideLinear.Get(0, 1):F4},{overrideLinear.Get(1, 1):F4},{overrideLinear.Get(2, 1):F4}), " +
@@ -3871,7 +3882,12 @@ namespace NavisworksTransport.Core.Animation
return adapter.FromCanonicalPoint(new Point3D(canonicalCenter.X, canonicalCenter.Y, canonicalCenter.Z));
}
- private bool TryResolveRailPreservedPoseTrackedCenter(Point3D referencePoint, out Point3D trackedCenter)
+ private bool TryResolveRailPreservedPoseTrackedCenter(
+ Point3D referencePoint,
+ Point3D previousPoint,
+ Point3D nextPoint,
+ double objectHeight,
+ out Point3D trackedCenter)
{
trackedCenter = referencePoint;
@@ -3883,14 +3899,21 @@ namespace NavisworksTransport.Core.Animation
return false;
}
- trackedCenter = new Point3D(
- referencePoint.X + _railPreservedPoseTrackedCenterOffset.X,
- referencePoint.Y + _railPreservedPoseTrackedCenterOffset.Y,
- referencePoint.Z + _railPreservedPoseTrackedCenterOffset.Z);
+ trackedCenter = RailPathPoseHelper.ResolvePreservedTrackedCenterPosition(
+ _route,
+ referencePoint,
+ previousPoint,
+ nextPoint,
+ objectHeight,
+ _railPreservedPoseTrackedCenterOffset);
return true;
}
- private void CaptureRailPreservedPoseTrackedCenterOffset()
+ private void CaptureRailPreservedPoseTrackedCenterOffset(
+ Point3D previousPoint,
+ Point3D referencePoint,
+ Point3D nextPoint,
+ double objectHeight)
{
_railPreservedPoseTrackedCenterOffset = new Vector3D(0, 0, 0);
_hasRailPreservedPoseTrackedCenterOffset = false;
@@ -3898,26 +3921,75 @@ namespace NavisworksTransport.Core.Animation
if (!IsRealObjectMode ||
_route?.PathType != PathType.Rail ||
_objectStartPlacementMode != ObjectStartPlacementMode.PreserveInitialPose ||
- _pathPoints == null ||
- _pathPoints.Count == 0)
+ CurrentControlledObject == null)
{
return;
}
Point3D currentTrackedCenter = GetTrackedObjectPosition(CurrentControlledObject ?? _animatedObject);
- Point3D terminalAnchorPoint = _pathPoints[_pathPoints.Count - 1];
- _railPreservedPoseTrackedCenterOffset = new Vector3D(
- currentTrackedCenter.X - terminalAnchorPoint.X,
- currentTrackedCenter.Y - terminalAnchorPoint.Y,
- currentTrackedCenter.Z - terminalAnchorPoint.Z);
+ Point3D semanticTrackedCenter = RailPathPoseHelper.ResolveObjectSpaceCenterPosition(
+ _route,
+ referencePoint,
+ previousPoint,
+ nextPoint,
+ objectHeight);
+ _railPreservedPoseTrackedCenterOffset = RailPathPoseHelper.CalculatePreservedTrackedCenterOffset(
+ _route,
+ currentTrackedCenter,
+ referencePoint,
+ previousPoint,
+ nextPoint,
+ objectHeight);
_hasRailPreservedPoseTrackedCenterOffset = true;
LogManager.Info(
- $"[Rail保持姿态补偿] 当前跟踪中心=({currentTrackedCenter.X:F3},{currentTrackedCenter.Y:F3},{currentTrackedCenter.Z:F3}), " +
- $"终点锚点=({terminalAnchorPoint.X:F3},{terminalAnchorPoint.Y:F3},{terminalAnchorPoint.Z:F3}), " +
+ $"[Rail终点原位残差] 当前跟踪中心=({currentTrackedCenter.X:F3},{currentTrackedCenter.Y:F3},{currentTrackedCenter.Z:F3}), " +
+ $"终点路径参考对应中心=({semanticTrackedCenter.X:F3},{semanticTrackedCenter.Y:F3},{semanticTrackedCenter.Z:F3}), " +
$"固定偏移=({_railPreservedPoseTrackedCenterOffset.X:F3},{_railPreservedPoseTrackedCenterOffset.Y:F3},{_railPreservedPoseTrackedCenterOffset.Z:F3})");
}
+ internal static Rotation3D ResolvePreservedPoseRotation(
+ bool preferActualGeometryRotation,
+ bool hasActualGeometryRotation,
+ Rotation3D actualGeometryRotation,
+ Rotation3D fallbackRotation)
+ {
+ return preferActualGeometryRotation && hasActualGeometryRotation
+ ? actualGeometryRotation
+ : fallbackRotation;
+ }
+
+ private void SyncTrackedRotationToDisplayedPose(ModelItem sourceObject)
+ {
+ if (sourceObject == null)
+ {
+ return;
+ }
+
+ Rotation3D actualGeometryRotation = Rotation3D.Identity;
+ bool hasActualGeometryRotation =
+ IsRealObjectMode &&
+ ModelItemTransformHelper.TryGetCurrentGeometryRotation(sourceObject, out actualGeometryRotation);
+
+ Rotation3D resolvedRotation = ResolvePreservedPoseRotation(
+ preferActualGeometryRotation: IsRealObjectMode,
+ hasActualGeometryRotation: hasActualGeometryRotation,
+ actualGeometryRotation: actualGeometryRotation,
+ fallbackRotation: _trackedRotation);
+
+ _trackedRotation = resolvedRotation;
+ _hasTrackedRotation = true;
+ _currentYaw = ModelItemTransformHelper.GetYawFromRotation(resolvedRotation);
+
+ var linear = new Transform3D(resolvedRotation).Linear;
+ LogManager.Info(
+ $"[平移保持终点原始姿态] {sourceObject.DisplayName} 锁定姿态: " +
+ $"X=({linear.Get(0, 0):F4},{linear.Get(1, 0):F4},{linear.Get(2, 0):F4}), " +
+ $"Y=({linear.Get(0, 1):F4},{linear.Get(1, 1):F4},{linear.Get(2, 1):F4}), " +
+ $"Z=({linear.Get(0, 2):F4},{linear.Get(1, 2):F4},{linear.Get(2, 2):F4}), " +
+ $"来源={(hasActualGeometryRotation ? "实际几何姿态" : "跟踪姿态")}");
+ }
+
private bool TryResolveGroundRealObjectBaseYaw(out double yawRadians)
{
yawRadians = 0.0;
diff --git a/src/UI/WPF/ViewModels/AnimationControlViewModel.cs b/src/UI/WPF/ViewModels/AnimationControlViewModel.cs
index 5b7049f..6da6be4 100644
--- a/src/UI/WPF/ViewModels/AnimationControlViewModel.cs
+++ b/src/UI/WPF/ViewModels/AnimationControlViewModel.cs
@@ -2200,8 +2200,8 @@ namespace NavisworksTransport.UI.WPF.ViewModels
if (_pathAnimationManager.MoveObjectToPathStartPreservingInitialPose())
{
- LogManager.Info("[角度修正] 已按平移模式将物体移动到路径起点,保持初始位姿");
- UpdateMainStatus("物体已平移到路径起点并保持初始位姿");
+ LogManager.Info("[角度修正] 已按终点原始位姿整体搬运到路径起点");
+ UpdateMainStatus("物体已按终点原始位姿平移到路径起点");
}
else
{
diff --git a/src/Utils/RailPathPoseHelper.cs b/src/Utils/RailPathPoseHelper.cs
index 22edc97..409c323 100644
--- a/src/Utils/RailPathPoseHelper.cs
+++ b/src/Utils/RailPathPoseHelper.cs
@@ -153,6 +153,58 @@ namespace NavisworksTransport.Utils
return adapterForRail.FromCanonicalPoint(canonicalCenterPointForRail);
}
+ internal static Vector3D CalculatePreservedTrackedCenterOffset(
+ PathRoute route,
+ Point3D actualTrackedCenter,
+ Point3D referencePoint,
+ Point3D previousPoint,
+ Point3D nextPoint,
+ double objectSpaceHeight)
+ {
+ Point3D semanticTrackedCenter = ResolveObjectSpaceCenterPosition(
+ route,
+ referencePoint,
+ previousPoint,
+ nextPoint,
+ objectSpaceHeight);
+
+ return new Vector3D(
+ actualTrackedCenter.X - semanticTrackedCenter.X,
+ actualTrackedCenter.Y - semanticTrackedCenter.Y,
+ actualTrackedCenter.Z - semanticTrackedCenter.Z);
+ }
+
+ internal static Vector3 CalculatePreservedTrackedCenterOffset(Vector3 actualTrackedCenter, Vector3 semanticTrackedCenter)
+ {
+ return actualTrackedCenter - semanticTrackedCenter;
+ }
+
+ internal static Point3D ResolvePreservedTrackedCenterPosition(
+ PathRoute route,
+ Point3D referencePoint,
+ Point3D previousPoint,
+ Point3D nextPoint,
+ double objectSpaceHeight,
+ Vector3D preservedOffset)
+ {
+ Point3D semanticTrackedCenter = ResolveObjectSpaceCenterPosition(
+ route,
+ referencePoint,
+ previousPoint,
+ nextPoint,
+ objectSpaceHeight);
+
+ return new Point3D(
+ semanticTrackedCenter.X + preservedOffset.X,
+ semanticTrackedCenter.Y + preservedOffset.Y,
+ semanticTrackedCenter.Z + preservedOffset.Z);
+ }
+
+ internal static Vector3 ResolvePreservedTrackedCenterPosition(Vector3 semanticTrackedCenter, Vector3 preservedOffset)
+ {
+ return semanticTrackedCenter + preservedOffset;
+ }
+
///
/// 根据轨道路径切向和法向创建 Rail 构件完整三维姿态。
/// 约定:构件本地 X 轴为前进方向,本地 Z 轴为上方向。
@@ -474,7 +526,7 @@ namespace NavisworksTransport.Utils
var rotation = new Rotation3D(qx, qy, qz, qw);
double error = CalculateLinearError(new Transform3D(rotation).Linear, linearTransform);
- LogManager.Info(
+ LogManager.Debug(
$"[Rail旋转验证] quaternion候选=({qx:F4},{qy:F4},{qz:F4},{qw:F4}), 误差={error:F6}");
return rotation;
}
From d4c49fc227ad6f2efaaf4b609681fe68a04b49ec Mon Sep 17 00:00:00 2001
From: tian <11429339@qq.com>
Date: Tue, 31 Mar 2026 00:14:18 +0800
Subject: [PATCH 48/87] Stabilize hoisting pose adjustment flow
---
.../HoistingRealObjectPoseHelperTests.cs | 43 +++++++++
src/Core/Animation/PathAnimationManager.cs | 87 +++++++++++++++++--
.../ViewModels/AnimationControlViewModel.cs | 26 +++---
.../HoistingRealObjectPoseHelper.cs | 32 +++++++
4 files changed, 169 insertions(+), 19 deletions(-)
diff --git a/UnitTests/CoordinateSystem/HoistingRealObjectPoseHelperTests.cs b/UnitTests/CoordinateSystem/HoistingRealObjectPoseHelperTests.cs
index 90ca32f..9280f70 100644
--- a/UnitTests/CoordinateSystem/HoistingRealObjectPoseHelperTests.cs
+++ b/UnitTests/CoordinateSystem/HoistingRealObjectPoseHelperTests.cs
@@ -63,11 +63,54 @@ namespace NavisworksTransport.UnitTests.CoordinateSystem
AssertVector(transformedForward, 1.0, 0.0, 0.0, 1e-4);
}
+ [TestMethod]
+ public void CreateRotationFromPlanarBasePose_ShouldReturnBaseRotation_WhenYawUnchanged()
+ {
+ Quaternion baseRotation = Quaternion.Normalize(
+ Quaternion.CreateFromAxisAngle(Vector3.UnitZ, 0.31f) *
+ Quaternion.CreateFromAxisAngle(Vector3.UnitX, -0.42f));
+
+ Quaternion resultRotation = HoistingRealObjectPoseHelper.CreateRotationFromPlanarBasePose(
+ baseRotation,
+ baseYawRadians: 1.25,
+ targetYawRadians: 1.25,
+ hostUp: Vector3.UnitY);
+
+ AssertQuaternionEquivalent(resultRotation, baseRotation);
+ }
+
+ [TestMethod]
+ public void CreateRotationFromPlanarBasePose_ShouldPreserveTiltAndApplyDeltaYaw()
+ {
+ Quaternion baseRotation = Quaternion.Normalize(
+ Quaternion.CreateFromAxisAngle(Vector3.UnitY, 0.20f) *
+ Quaternion.CreateFromAxisAngle(Vector3.UnitZ, 0.35f) *
+ Quaternion.CreateFromAxisAngle(Vector3.UnitX, -0.40f));
+
+ Quaternion resultRotation = HoistingRealObjectPoseHelper.CreateRotationFromPlanarBasePose(
+ baseRotation,
+ baseYawRadians: 0.0,
+ targetYawRadians: System.Math.PI / 2.0,
+ hostUp: Vector3.UnitY);
+
+ Quaternion expectedRotation = Quaternion.Normalize(
+ Quaternion.CreateFromAxisAngle(Vector3.UnitY, (float)(System.Math.PI / 2.0)) *
+ baseRotation);
+
+ AssertQuaternionEquivalent(resultRotation, expectedRotation);
+ }
+
private static void AssertVector(Vector3 actual, double x, double y, double z, double tolerance = 1e-6)
{
Assert.AreEqual(x, actual.X, tolerance);
Assert.AreEqual(y, actual.Y, tolerance);
Assert.AreEqual(z, actual.Z, tolerance);
}
+
+ private static void AssertQuaternionEquivalent(Quaternion actual, Quaternion expected, double tolerance = 1e-6)
+ {
+ float dot = Quaternion.Dot(Quaternion.Normalize(actual), Quaternion.Normalize(expected));
+ Assert.AreEqual(1.0, System.Math.Abs(dot), tolerance);
+ }
}
}
diff --git a/src/Core/Animation/PathAnimationManager.cs b/src/Core/Animation/PathAnimationManager.cs
index 0780bb8..0f62d66 100644
--- a/src/Core/Animation/PathAnimationManager.cs
+++ b/src/Core/Animation/PathAnimationManager.cs
@@ -195,6 +195,9 @@ namespace NavisworksTransport.Core.Animation
private Rotation3D _groundRealObjectBaseRotation = Rotation3D.Identity;
private double _groundRealObjectBaseYaw = 0.0;
private bool _hasGroundRealObjectBasePose = false;
+ private Rotation3D _hoistingRealObjectBaseRotation = Rotation3D.Identity;
+ private double _hoistingRealObjectBaseYaw = 0.0;
+ private bool _hasHoistingRealObjectBasePose = false;
private Vector3D _groundRealObjectStartCompensation = new Vector3D(0, 0, 0);
private bool _hasGroundRealObjectStartCompensation = false;
private bool _suppressGroundRealObjectCompensation = false;
@@ -844,16 +847,25 @@ namespace NavisworksTransport.Core.Animation
if (IsRealObjectMode && _route.PathType == PathType.Ground)
{
_groundRealObjectBaseRotation = planarRotation;
- _hasGroundRealObjectBasePose = TryResolveGroundRealObjectBaseYaw(out _groundRealObjectBaseYaw);
+ _hasGroundRealObjectBasePose = TryResolvePlanarRealObjectBaseYaw(PathType.Ground, out _groundRealObjectBaseYaw);
LogManager.Info(
$"[Ground真实物体基姿态] {animatedObject.DisplayName} BaseYaw={_groundRealObjectBaseYaw * 180.0 / Math.PI:F2}°, " +
$"已记录基姿态={_hasGroundRealObjectBasePose}, " +
$"起点补偿=({_groundRealObjectStartCompensation.X:F3},{_groundRealObjectStartCompensation.Y:F3},{_groundRealObjectStartCompensation.Z:F3}), " +
$"已启用补偿={_hasGroundRealObjectStartCompensation}");
}
+ else if (IsRealObjectMode && _route.PathType == PathType.Hoisting)
+ {
+ _hoistingRealObjectBaseRotation = planarRotation;
+ _hasHoistingRealObjectBasePose = TryResolvePlanarRealObjectBaseYaw(PathType.Hoisting, out _hoistingRealObjectBaseYaw);
+ LogManager.Debug(
+ $"[Hoisting真实物体基姿态] {(animatedObject ?? _animatedObject)?.DisplayName} BaseYaw={_hoistingRealObjectBaseYaw * 180.0 / Math.PI:F2}°, " +
+ $"已记录基姿态={_hasHoistingRealObjectBasePose}, 姿态将复用于预览/生成/播放");
+ }
else
{
_hasGroundRealObjectBasePose = false;
+ _hasHoistingRealObjectBasePose = false;
_groundRealObjectStartCompensation = new Vector3D(0, 0, 0);
_hasGroundRealObjectStartCompensation = false;
}
@@ -978,6 +990,7 @@ namespace NavisworksTransport.Core.Animation
UpdateObjectPosition(startPosition);
SyncTrackedRotationToDisplayedPose(CurrentControlledObject ?? _animatedObject);
_hasGroundRealObjectBasePose = false;
+ _hasHoistingRealObjectBasePose = false;
_groundRealObjectStartCompensation = new Vector3D(0, 0, 0);
_hasGroundRealObjectStartCompensation = false;
@@ -3990,10 +4003,10 @@ namespace NavisworksTransport.Core.Animation
$"来源={(hasActualGeometryRotation ? "实际几何姿态" : "跟踪姿态")}");
}
- private bool TryResolveGroundRealObjectBaseYaw(out double yawRadians)
+ private bool TryResolvePlanarRealObjectBaseYaw(PathType pathType, out double yawRadians)
{
yawRadians = 0.0;
- if (_route?.PathType != PathType.Ground ||
+ if (_route?.PathType != pathType ||
!IsRealObjectMode ||
_pathPoints == null ||
_pathPoints.Count < 2)
@@ -4008,7 +4021,7 @@ namespace NavisworksTransport.Core.Animation
hostPoints.Add(new Vector3((float)_pathPoints[i].X, (float)_pathPoints[i].Y, (float)_pathPoints[i].Z));
}
- return PathTargetFrameResolver.TryResolvePlanarStartHostYaw(_route.PathType, hostPoints, hostType, out yawRadians);
+ return PathTargetFrameResolver.TryResolvePlanarStartHostYaw(pathType, hostPoints, hostType, out yawRadians);
}
private bool TryCreateGroundRealObjectConstrainedRotation(
@@ -4092,6 +4105,45 @@ namespace NavisworksTransport.Core.Animation
return true;
}
+ private bool TryCreateHoistingRealObjectConstrainedRotationFromHostForward(
+ Vector3 hostForward,
+ out Rotation3D rotation)
+ {
+ rotation = Rotation3D.Identity;
+ if (!IsRealObjectMode ||
+ _route?.PathType != PathType.Hoisting ||
+ !_hasHoistingRealObjectBasePose)
+ {
+ return false;
+ }
+
+ if (!PathTargetFrameResolver.TryCreatePlanarHostFrame(
+ hostForward,
+ CoordinateSystemManager.Instance.ResolvedType,
+ out var currentFrame))
+ {
+ return false;
+ }
+
+ if (!PathTargetFrameResolver.TryResolvePlanarHostYaw(
+ currentFrame.Forward,
+ CoordinateSystemManager.Instance.ResolvedType,
+ out double targetYawRadians))
+ {
+ return false;
+ }
+
+ var adapter = CoordinateSystemManager.Instance.CreateHostAdapter();
+ Quaternion baseQuaternion = Rotation3DToHostQuaternion(_hoistingRealObjectBaseRotation);
+ Quaternion targetQuaternion = HoistingRealObjectPoseHelper.CreateRotationFromPlanarBasePose(
+ baseQuaternion,
+ _hoistingRealObjectBaseYaw,
+ targetYawRadians,
+ Vector3.Normalize(adapter.HostUpVector3));
+ rotation = adapter.FromHostQuaternionDirect(targetQuaternion);
+ return true;
+ }
+
private static Quaternion Rotation3DToHostQuaternion(Rotation3D rotation)
{
var linear = new Transform3D(rotation).Linear;
@@ -4681,6 +4733,13 @@ namespace NavisworksTransport.Core.Animation
{
rotation = Rotation3D.Identity;
+ if (_route?.PathType == PathType.Hoisting &&
+ _hasHoistingRealObjectBasePose &&
+ TryCreateHoistingRealObjectConstrainedRotationFromHostForward(hostForward, out rotation))
+ {
+ return true;
+ }
+
if (_route?.PathType == PathType.Hoisting &&
TryCreateHoistingRealObjectRotationFromActualPose(hostForward, out rotation))
{
@@ -4711,7 +4770,7 @@ namespace NavisworksTransport.Core.Animation
Matrix4x4 baselineLinear = Matrix4x4.CreateFromQuaternion(solution.BaselineRotation);
Matrix4x4 hostComposedLinear = Matrix4x4.CreateFromQuaternion(hostComposedQuaternion);
- LogManager.Info(
+ LogManager.Debug(
$"[真实物体起点姿态] 选中参考轴=({solution.SelectedReferenceAxisLocal.X:F3},{solution.SelectedReferenceAxisLocal.Y:F3},{solution.SelectedReferenceAxisLocal.Z:F3}), " +
$"投影前进=({solution.ProjectedForward.X:F3},{solution.ProjectedForward.Y:F3},{solution.ProjectedForward.Z:F3}), " +
$"宿主修正={_objectRotationCorrection}, 本地修正={localCorrection}, " +
@@ -4767,9 +4826,9 @@ namespace NavisworksTransport.Core.Animation
_realObjectPlanarSelectedForwardAxis = selectedAxisDirection;
_hasRealObjectPlanarSelectedForwardAxis = true;
- LogManager.Info(
+ LogManager.Debug(
$"[真实物体平面前进轴] Hoisting 已改用实际几何姿态基线,选中对象轴={selectedAxisDirection}。");
- LogManager.Info(
+ LogManager.Debug(
$"[真实物体起点姿态] 选中参考轴=({selectedAxisLocal.X:F3},{selectedAxisLocal.Y:F3},{selectedAxisLocal.Z:F3}), " +
$"投影前进=({projectedForward.X:F3},{projectedForward.Y:F3},{projectedForward.Z:F3}), " +
$"宿主修正={_objectRotationCorrection}, 本地修正=X=0.0°,Y=0.0°,Z=0.0°, " +
@@ -4817,7 +4876,7 @@ namespace NavisworksTransport.Core.Animation
{
_realObjectPlanarSelectedForwardAxis = LocalAxisDirection.PositiveX;
_hasRealObjectPlanarSelectedForwardAxis = true;
- LogManager.Info("[真实物体平面前进轴] Ground/Hoisting 已固定使用 PositiveX 作为对象前进轴语义。");
+ LogManager.Debug("[真实物体平面前进轴] Ground/Hoisting 已固定使用 PositiveX 作为对象前进轴语义。");
}
return true;
@@ -5097,6 +5156,16 @@ namespace NavisworksTransport.Core.Animation
_hasRealObjectPlanarSelectedForwardAxis = false;
}
+ private void ResetPlanarRealObjectBasePoseCache()
+ {
+ _groundRealObjectBaseRotation = Rotation3D.Identity;
+ _groundRealObjectBaseYaw = 0.0;
+ _hasGroundRealObjectBasePose = false;
+ _hoistingRealObjectBaseRotation = Rotation3D.Identity;
+ _hoistingRealObjectBaseYaw = 0.0;
+ _hasHoistingRealObjectBasePose = false;
+ }
+
private LocalEulerRotationCorrection ResolveRealObjectLocalRotationCorrection()
{
return HostCoordinateAdapter.RemapHostSemanticCorrectionToLocalAxes(
@@ -5114,6 +5183,7 @@ namespace NavisworksTransport.Core.Animation
_objectStartPlacementMode = ObjectStartPlacementMode.AlignToPathPose;
_hasRailPreservedPoseRotation = false;
_objectRotationCorrection = rotationCorrection;
+ ResetPlanarRealObjectBasePoseCache();
// 如果动画已创建,更新物体到起点的朝向
if (_animatedObject != null && _pathPoints != null && _pathPoints.Count > 0)
@@ -5145,6 +5215,7 @@ namespace NavisworksTransport.Core.Animation
public void SetObjectRotationCorrectionDirect(LocalEulerRotationCorrection rotationCorrection)
{
_objectRotationCorrection = rotationCorrection;
+ ResetPlanarRealObjectBasePoseCache();
LogManager.Debug($"[角度修正] 直接设置角度修正值: {_objectRotationCorrection}(不触发旋转)");
}
diff --git a/src/UI/WPF/ViewModels/AnimationControlViewModel.cs b/src/UI/WPF/ViewModels/AnimationControlViewModel.cs
index 6da6be4..7dfc8b4 100644
--- a/src/UI/WPF/ViewModels/AnimationControlViewModel.cs
+++ b/src/UI/WPF/ViewModels/AnimationControlViewModel.cs
@@ -1675,32 +1675,34 @@ namespace NavisworksTransport.UI.WPF.ViewModels
_pathAnimationManager?.SetRealObjectDimensions(objectLength, objectWidth, objectHeight);
LogManager.Debug($"[选择物体] 保存原始尺寸: 长度={_objectOriginalLength:F2}m, 宽度={_objectOriginalWidth:F2}m, 高度={_objectOriginalHeight:F2}m");
- // 3. 先注册到动画管理器,再设置 SelectedAnimatedObject。
- // SelectedAnimatedObject 的 setter 会立即触发 MoveAnimatedObjectToPathStart();
- // 如果先移动、后 SetAnimatedObject,会把“已摆到起点后的当前姿态”再次当成参考姿态缓存。
- _pathAnimationManager?.SetAnimatedObject(newObject);
- SelectedAnimatedObject = newObject;
- LogManager.Info($"已选择移动物体: {SelectedAnimatedObject.DisplayName}");
-
- // 选择实体物体意味着切换到实体模式,避免后续起点同步仍走虚拟物体分支。
+ // 3. 选择实体物体意味着切换到实体模式,避免后续起点同步仍走虚拟物体分支。
if (UseVirtualObject)
{
UseVirtualObject = false;
LogManager.Debug("[选择物体] 已切换到实体物体模式");
}
- // 只有选择不同的物体时,才重置角度修正值
+ // 4. 对“新物体”先清空角度修正,再触发 SelectedAnimatedObject 的起点落位。
+ // SelectedAnimatedObject 的 setter 会立即触发 MoveAnimatedObjectToPathStart(),
+ // 如果角度在后面才归零,就会先按旧角度把新选择的物体摆到起点。
if (!isSameObject)
{
- // 重置 ViewModel 中的角度修正值(会自动同步到 PathAnimationManager)
+ _pathAnimationManager?.SetObjectRotationCorrectionDirect(LocalEulerRotationCorrection.Zero);
ObjectRotationCorrection = LocalEulerRotationCorrection.Zero;
LogManager.Debug("[选择物体] 已重置角度修正值为0(新物体)");
}
else
{
- LogManager.Debug($"[选择物体] 保持当前角度修正值(同一物体)");
+ LogManager.Debug("[选择物体] 保持当前角度修正值(同一物体)");
}
+ // 5. 先注册到动画管理器,再设置 SelectedAnimatedObject。
+ // SelectedAnimatedObject 的 setter 会立即触发 MoveAnimatedObjectToPathStart();
+ // 如果先移动、后 SetAnimatedObject,会把“已摆到起点后的当前姿态”再次当成参考姿态缓存。
+ _pathAnimationManager?.SetAnimatedObject(newObject);
+ SelectedAnimatedObject = newObject;
+ LogManager.Info($"已选择移动物体: {SelectedAnimatedObject.DisplayName}");
+
}
catch (Exception ex)
{
@@ -2121,6 +2123,8 @@ namespace NavisworksTransport.UI.WPF.ViewModels
// 1. 归位并清理
_pathAnimationManager.RestoreObjectToCADPosition();
_pathAnimationManager.ClearAnimationResults();
+ _pathAnimationManager.SetObjectRotationCorrectionDirect(LocalEulerRotationCorrection.Zero);
+ _pathAnimationManager.SetObjectStartPlacementMode(ObjectStartPlacementMode.AlignToPathPose);
LogManager.Info("已清除PathAnimationManager中的动画数据并归位物体");
}
diff --git a/src/Utils/CoordinateSystem/HoistingRealObjectPoseHelper.cs b/src/Utils/CoordinateSystem/HoistingRealObjectPoseHelper.cs
index bd23473..c5ff9b4 100644
--- a/src/Utils/CoordinateSystem/HoistingRealObjectPoseHelper.cs
+++ b/src/Utils/CoordinateSystem/HoistingRealObjectPoseHelper.cs
@@ -5,6 +5,23 @@ namespace NavisworksTransport.Utils.CoordinateSystem
{
public static class HoistingRealObjectPoseHelper
{
+ public static Quaternion CreateRotationFromPlanarBasePose(
+ Quaternion baseRotation,
+ double baseYawRadians,
+ double targetYawRadians,
+ Vector3 hostUp)
+ {
+ Vector3 normalizedHostUp = NormalizeSafe(hostUp);
+ if (normalizedHostUp.LengthSquared() < 1e-6f)
+ {
+ return Quaternion.Normalize(baseRotation);
+ }
+
+ float deltaYawRadians = NormalizeRadians(targetYawRadians - baseYawRadians);
+ Quaternion deltaRotation = Quaternion.CreateFromAxisAngle(normalizedHostUp, deltaYawRadians);
+ return Quaternion.Normalize(deltaRotation * baseRotation);
+ }
+
public static bool TryCreateRotationFromActualPose(
Quaternion actualRotation,
Vector3 targetForward,
@@ -131,5 +148,20 @@ namespace NavisworksTransport.Utils.CoordinateSystem
return angle * sign;
}
+
+ private static float NormalizeRadians(double angleRadians)
+ {
+ while (angleRadians > Math.PI)
+ {
+ angleRadians -= 2.0 * Math.PI;
+ }
+
+ while (angleRadians < -Math.PI)
+ {
+ angleRadians += 2.0 * Math.PI;
+ }
+
+ return (float)angleRadians;
+ }
}
}
From c9744699b17657c6e237905b50d0de4cdf9148bf Mon Sep 17 00:00:00 2001
From: tian <11429339@qq.com>
Date: Tue, 31 Mar 2026 13:41:15 +0800
Subject: [PATCH 49/87] Separate rail creation and edit assembly flows
---
src/UI/WPF/ViewModels/PathEditingViewModel.cs | 249 ++++++++++--------
src/UI/WPF/Views/PathEditingView.xaml | 24 +-
2 files changed, 151 insertions(+), 122 deletions(-)
diff --git a/src/UI/WPF/ViewModels/PathEditingViewModel.cs b/src/UI/WPF/ViewModels/PathEditingViewModel.cs
index a650edd..aa104d7 100644
--- a/src/UI/WPF/ViewModels/PathEditingViewModel.cs
+++ b/src/UI/WPF/ViewModels/PathEditingViewModel.cs
@@ -71,6 +71,13 @@ namespace NavisworksTransport.UI.WPF.ViewModels
///
public class PathEditingViewModel : ViewModelBase, IDisposable
{
+ private enum RailAssemblyWorkflowMode
+ {
+ None,
+ CreateNewRailPath,
+ EditSelectedRail
+ }
+
#region 私有字段
private PathPlanningManager _pathPlanningManager;
@@ -144,7 +151,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
private bool _isSelectingAssemblyEndFacePoints;
private bool _isSelectingAssemblyInstallationPoint;
private bool _isEditingSelectedRailInstallationPoint;
- private bool _isEditingSelectedRail;
+ private RailAssemblyWorkflowMode _railAssemblyWorkflowMode = RailAssemblyWorkflowMode.None;
private bool _hasAssemblyEndFaceAnalysis;
private bool _hasAssemblyInstallationReference;
private ModelItem _assemblyTerminalObject;
@@ -219,8 +226,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
ClearAssemblyInstallationReferenceVisuals();
// 重置 Rail 编辑状态
- _isEditingSelectedRail = false;
- OnPropertyChanged(nameof(AssemblyInstallationPointButtonText));
+ SetRailAssemblyWorkflowMode(RailAssemblyWorkflowMode.None);
// 🔧 修复:同步PathPlanningManager的CurrentRoute
if (_pathPlanningManager != null && value != null)
@@ -287,8 +293,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
OnPropertyChanged(nameof(SelectedRailMountMode));
OnPropertyChanged(nameof(SelectedRailNormalOffsetInMeters));
OnPropertyChanged(nameof(CanRepositionRailStartPoint));
- OnPropertyChanged(nameof(CanSelectAssemblyInstallationPoint));
- OnPropertyChanged(nameof(AssemblyInstallationPointButtonText));
+ NotifyRailAssemblyCommandStateChanged();
if (!CanUsePathLines && ShowPathLines)
{
// 吊装和空轨路径不能使用路径线,自动关闭
@@ -1069,14 +1074,16 @@ namespace NavisworksTransport.UI.WPF.ViewModels
public ICommand ImportPathCommand { get; private set; }
public ICommand ExportPathCommand { get; private set; }
public ICommand SaveAsPathCommand { get; private set; }
- public ICommand CaptureAssemblyTerminalObjectCommand { get; private set; }
- public ICommand ReCaptureAssemblyTerminalObjectCommand { get; private set; }
+ public ICommand CaptureAssemblyTerminalObjectForCreateCommand { get; private set; }
+ public ICommand ReCaptureAssemblyTerminalObjectForEditCommand { get; private set; }
public ICommand GenerateAssemblyReferenceRodCommand { get; private set; }
- public ICommand SelectAssemblyStartPointCommand { get; private set; }
+ public ICommand SelectAssemblyStartPointForCreateCommand { get; private set; }
public ICommand RepositionRailStartPointCommand { get; private set; }
- public ICommand SelectAssemblyInstallationPointCommand { get; private set; }
+ public ICommand SelectAssemblyInstallationPointForCreateCommand { get; private set; }
+ public ICommand SelectAssemblyInstallationPointForEditCommand { get; private set; }
public ICommand ClearAssemblyReferenceRodCommand { get; private set; }
- public ICommand AnalyzeAssemblyTerminalFaceCommand { get; private set; }
+ public ICommand AnalyzeAssemblyTerminalFaceForCreateCommand { get; private set; }
+ public ICommand AnalyzeAssemblyTerminalFaceForEditCommand { get; private set; }
public ICommand DecreaseSelectedRailNormalOffsetCommand { get; private set; }
public ICommand IncreaseSelectedRailNormalOffsetCommand { get; private set; }
@@ -1118,10 +1125,12 @@ namespace NavisworksTransport.UI.WPF.ViewModels
_hasAssemblyInstallationReference &&
AssemblyReferenceRodLengthInMeters > 0 &&
AssemblyReferenceRodDiameterInMeters > 0;
- public bool CanSelectAssemblyStartPoint => HasAssemblyTerminalObject &&
- _pathPlanningManager != null &&
- AssemblyReferencePathManager.Instance.HasReferenceLine &&
- !IsSelectingAssemblyStartPoint;
+ public bool CanSelectAssemblyStartPointForCreate => HasAssemblyTerminalObject &&
+ _pathPlanningManager != null &&
+ AssemblyReferencePathManager.Instance.HasReferenceLine &&
+ !IsSelectingAssemblyStartPoint &&
+ !_isSelectingAssemblyEndFacePoints &&
+ !_isSelectingAssemblyInstallationPoint;
public bool CanRepositionRailStartPoint => HasAssemblyTerminalObject &&
IsRailRouteSelected &&
SelectedPathRoute != null &&
@@ -1130,25 +1139,44 @@ namespace NavisworksTransport.UI.WPF.ViewModels
!IsSelectingAssemblyStartPoint &&
!_isSelectingAssemblyEndFacePoints &&
!_isSelectingAssemblyInstallationPoint;
- public bool CanSelectAssemblyInstallationPoint => HasAssemblyTerminalObject &&
- IsRailRouteSelected &&
- SelectedPathRoute != null &&
- _pathPlanningManager != null &&
- !_isSelectingAssemblyStartPoint &&
- !_isSelectingAssemblyEndFacePoints &&
- !_isSelectingAssemblyInstallationPoint;
- public bool CanAnalyzeAssemblyTerminalFace => HasAssemblyTerminalObject &&
- _pathPlanningManager != null &&
- !IsSelectingAssemblyStartPoint &&
- !IsSelectingAssemblyEndFacePoints &&
- !_isSelectingAssemblyInstallationPoint;
+ public bool CanSelectAssemblyInstallationPointForCreate => HasAssemblyTerminalObject &&
+ _pathPlanningManager != null &&
+ !_isSelectingAssemblyStartPoint &&
+ !_isSelectingAssemblyEndFacePoints &&
+ !_isSelectingAssemblyInstallationPoint;
+ public bool CanSelectAssemblyInstallationPointForEdit => HasAssemblyTerminalObject &&
+ IsRailRouteSelected &&
+ SelectedPathRoute != null &&
+ _pathPlanningManager != null &&
+ !_isSelectingAssemblyStartPoint &&
+ !_isSelectingAssemblyEndFacePoints &&
+ !_isSelectingAssemblyInstallationPoint;
+ public bool CanAnalyzeAssemblyTerminalFaceForCreate => HasAssemblyTerminalObject &&
+ _pathPlanningManager != null &&
+ !IsSelectingAssemblyStartPoint &&
+ !IsSelectingAssemblyEndFacePoints &&
+ !_isSelectingAssemblyInstallationPoint;
+ public bool CanAnalyzeAssemblyTerminalFaceForEdit => HasAssemblyTerminalObject &&
+ IsRailRouteSelected &&
+ SelectedPathRoute != null &&
+ _pathPlanningManager != null &&
+ !IsSelectingAssemblyStartPoint &&
+ !IsSelectingAssemblyEndFacePoints &&
+ !_isSelectingAssemblyInstallationPoint;
- public string AssemblyInstallationPointButtonText => HasSelectedRailInstallationReference()
+ public string CreateAssemblyInstallationPointButtonText => _hasAssemblyInstallationReference && !IsRailEditingWorkflowActive
+ ? "重选安装点"
+ : "选安装点";
+
+ public string EditAssemblyInstallationPointButtonText => HasSelectedRailInstallationReference()
? "重选安装点"
: "选安装点";
public bool IsSelectingAssemblyEndFacePoints => _isSelectingAssemblyEndFacePoints;
+ private bool IsRailCreateWorkflowActive => _railAssemblyWorkflowMode == RailAssemblyWorkflowMode.CreateNewRailPath;
+ private bool IsRailEditingWorkflowActive => _railAssemblyWorkflowMode == RailAssemblyWorkflowMode.EditSelectedRail;
+
public bool CanExecuteModifyPoint => SelectedPathPoint != null &&
_pathPlanningManager?.PathEditState != PathEditState.EditingPoint;
public bool CanExecuteCancelModifyPoint => _pathPlanningManager?.PathEditState == PathEditState.EditingPoint;
@@ -1294,14 +1322,31 @@ namespace NavisworksTransport.UI.WPF.ViewModels
LogManager.Info($"[Rail构型] {coreRoute.Name}: {logMessage}");
}
- private bool HasSelectedRailInstallationReference()
+ private void SetRailAssemblyWorkflowMode(RailAssemblyWorkflowMode mode)
{
- // 只有在点击"重选箱体"进入编辑状态后才显示"重选安装点"
- if (!_isEditingSelectedRail)
+ if (_railAssemblyWorkflowMode == mode)
{
- return false;
+ return;
}
+ _railAssemblyWorkflowMode = mode;
+ NotifyRailAssemblyCommandStateChanged();
+ }
+
+ private void NotifyRailAssemblyCommandStateChanged()
+ {
+ OnPropertyChanged(nameof(CanAnalyzeAssemblyTerminalFaceForCreate));
+ OnPropertyChanged(nameof(CanAnalyzeAssemblyTerminalFaceForEdit));
+ OnPropertyChanged(nameof(CanSelectAssemblyStartPointForCreate));
+ OnPropertyChanged(nameof(CanRepositionRailStartPoint));
+ OnPropertyChanged(nameof(CanSelectAssemblyInstallationPointForCreate));
+ OnPropertyChanged(nameof(CanSelectAssemblyInstallationPointForEdit));
+ OnPropertyChanged(nameof(CreateAssemblyInstallationPointButtonText));
+ OnPropertyChanged(nameof(EditAssemblyInstallationPointButtonText));
+ }
+
+ private bool HasSelectedRailInstallationReference()
+ {
var coreRoute = GetSelectedCoreRoute();
return coreRoute != null &&
coreRoute.PathType == PathType.Rail &&
@@ -1389,7 +1434,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
AssemblyReferenceLine referenceLine;
- if (_isEditingSelectedRail)
+ if (IsRailEditingWorkflowActive)
{
// 编辑状态:使用默认杆长度构建参考线,方便选择更远的起点
try
@@ -1420,8 +1465,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
AssemblyStartPointText = $"({referenceLine.StartPoint.X:F2}, {referenceLine.StartPoint.Y:F2}, {referenceLine.StartPoint.Z:F2})";
RefreshAssemblyTerminalObjectInfo(referenceLine.StartPoint, referenceLine.EndPoint);
RenderAssemblyReferenceLine(referenceLine);
- OnPropertyChanged(nameof(CanSelectAssemblyStartPoint));
- OnPropertyChanged(nameof(CanRepositionRailStartPoint));
+ NotifyRailAssemblyCommandStateChanged();
if (!string.IsNullOrWhiteSpace(statusMessage))
{
@@ -1493,7 +1537,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
{
SyncPathViewModelFromCoreRoute(SelectedPathRoute, route, preserveSelection: true);
OnPropertyChanged(nameof(SelectedRailNormalOffsetInMeters));
- OnPropertyChanged(nameof(AssemblyInstallationPointButtonText));
+ NotifyRailAssemblyCommandStateChanged();
RefreshAssemblyTerminalObjectInfo();
RefreshSelectedRailInstallationReferenceVisuals();
RefreshSelectedRailReferenceLineVisuals();
@@ -1504,12 +1548,6 @@ namespace NavisworksTransport.UI.WPF.ViewModels
private void RefreshSelectedRailInstallationReferenceVisuals()
{
- if (!_isSelectingAssemblyInstallationPoint)
- {
- ClearAssemblyInstallationReferenceVisuals();
- return;
- }
-
var coreRoute = GetSelectedCoreRoute();
if (!IsAssemblyInstallationReferenceActiveForRoute(coreRoute))
{
@@ -1671,14 +1709,16 @@ namespace NavisworksTransport.UI.WPF.ViewModels
ImportPathCommand = new RelayCommand(async () => await ExecuteImportPathAsync());
ExportPathCommand = new RelayCommand(async () => await ExecuteExportPathAsync(), () => CanExecuteExportPath);
SaveAsPathCommand = new RelayCommand(async () => await ExecuteSaveAsPathAsync(), () => CanExecuteSaveAsPath);
- CaptureAssemblyTerminalObjectCommand = new RelayCommand(async () => await ExecuteCaptureAssemblyTerminalObjectAsync());
- ReCaptureAssemblyTerminalObjectCommand = new RelayCommand(async () => await ExecuteReCaptureAssemblyTerminalObjectAsync());
+ CaptureAssemblyTerminalObjectForCreateCommand = new RelayCommand(async () => await ExecuteCaptureAssemblyTerminalObjectAsync(RailAssemblyWorkflowMode.CreateNewRailPath));
+ ReCaptureAssemblyTerminalObjectForEditCommand = new RelayCommand(async () => await ExecuteCaptureAssemblyTerminalObjectAsync(RailAssemblyWorkflowMode.EditSelectedRail));
GenerateAssemblyReferenceRodCommand = new RelayCommand(async () => await ExecuteGenerateAssemblyReferenceRodAsync(), () => CanGenerateAssemblyReferenceRod);
- SelectAssemblyStartPointCommand = new RelayCommand(async () => await ExecuteSelectAssemblyStartPointAsync(), () => CanSelectAssemblyStartPoint);
+ SelectAssemblyStartPointForCreateCommand = new RelayCommand(async () => await ExecuteSelectAssemblyStartPointAsync(), () => CanSelectAssemblyStartPointForCreate);
RepositionRailStartPointCommand = new RelayCommand(async () => await ExecuteRepositionRailStartPointAsync(), () => CanRepositionRailStartPoint);
- SelectAssemblyInstallationPointCommand = new RelayCommand(async () => await ExecuteSelectAssemblyInstallationPointAsync(), () => CanSelectAssemblyInstallationPoint);
+ SelectAssemblyInstallationPointForCreateCommand = new RelayCommand(async () => await ExecuteSelectAssemblyInstallationPointAsync(RailAssemblyWorkflowMode.CreateNewRailPath), () => CanSelectAssemblyInstallationPointForCreate);
+ SelectAssemblyInstallationPointForEditCommand = new RelayCommand(async () => await ExecuteSelectAssemblyInstallationPointAsync(RailAssemblyWorkflowMode.EditSelectedRail), () => CanSelectAssemblyInstallationPointForEdit);
ClearAssemblyReferenceRodCommand = new RelayCommand(() => ExecuteClearAssemblyReferenceRod());
- AnalyzeAssemblyTerminalFaceCommand = new RelayCommand(async () => await ExecuteAnalyzeAssemblyTerminalFaceAsync(), () => CanAnalyzeAssemblyTerminalFace);
+ AnalyzeAssemblyTerminalFaceForCreateCommand = new RelayCommand(async () => await ExecuteAnalyzeAssemblyTerminalFaceAsync(RailAssemblyWorkflowMode.CreateNewRailPath), () => CanAnalyzeAssemblyTerminalFaceForCreate);
+ AnalyzeAssemblyTerminalFaceForEditCommand = new RelayCommand(async () => await ExecuteAnalyzeAssemblyTerminalFaceAsync(RailAssemblyWorkflowMode.EditSelectedRail), () => CanAnalyzeAssemblyTerminalFaceForEdit);
DecreaseSelectedRailNormalOffsetCommand = new RelayCommand(() => AdjustSelectedRailNormalOffset(-RailNormalOffsetNudgeStepInMeters));
IncreaseSelectedRailNormalOffsetCommand = new RelayCommand(() => AdjustSelectedRailNormalOffset(RailNormalOffsetNudgeStepInMeters));
}
@@ -1689,7 +1729,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
#region 路径管理命令
- private async Task ExecuteCaptureAssemblyTerminalObjectAsync()
+ private async Task ExecuteCaptureAssemblyTerminalObjectAsync(RailAssemblyWorkflowMode mode)
{
await SafeExecuteAsync(() =>
{
@@ -1707,8 +1747,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
throw new InvalidOperationException("当前选择对象无效,请重新选择终点箱体");
}
- // 注意:不在这里设置 _isEditingSelectedRail
- // 新建路径时保持 false,只有点击"重选箱体"时才设置为 true
+ SetRailAssemblyWorkflowMode(mode);
_assemblyTerminalObject = selectedItem;
_assemblyStartPoint = Point3D.Origin;
@@ -1722,11 +1761,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
ClearAssemblyEndFaceAnalysisVisuals();
ClearAssemblyInstallationReferenceVisuals();
OnPropertyChanged(nameof(CanGenerateAssemblyReferenceRod));
- OnPropertyChanged(nameof(CanSelectAssemblyStartPoint));
- OnPropertyChanged(nameof(CanRepositionRailStartPoint));
- OnPropertyChanged(nameof(CanSelectAssemblyInstallationPoint));
- OnPropertyChanged(nameof(CanAnalyzeAssemblyTerminalFace));
- OnPropertyChanged(nameof(AssemblyInstallationPointButtonText));
+ NotifyRailAssemblyCommandStateChanged();
// 新建路径时隐藏其他路径的辅助线
ClearAssemblyReferenceLineVisuals();
@@ -1736,14 +1771,6 @@ namespace NavisworksTransport.UI.WPF.ViewModels
}, "捕获终点箱体");
}
- private async Task ExecuteReCaptureAssemblyTerminalObjectAsync()
- {
- // 先进入编辑状态,再执行捕获
- _isEditingSelectedRail = true;
- OnPropertyChanged(nameof(AssemblyInstallationPointButtonText));
- await ExecuteCaptureAssemblyTerminalObjectAsync();
- }
-
private void ClearNonGridPathVisualizations(string context)
{
if (PathPointRenderPlugin.Instance == null)
@@ -1793,8 +1820,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
FocusOnAssemblyReferenceArea(referenceLine.StartPoint, referenceLine.EndPoint);
UpdateMainStatus("已生成终端安装辅助线,请在三维视图中确认终点锚点、方向、外端位置和辅助线贴合情况");
- OnPropertyChanged(nameof(CanSelectAssemblyStartPoint));
- OnPropertyChanged(nameof(CanRepositionRailStartPoint));
+ NotifyRailAssemblyCommandStateChanged();
LogManager.Info(
$"[直线装配] 已生成参考杆: {AssemblyTerminalObjectName}, " +
$"终点锚点=({referenceLine.EndPoint.X:F2}, {referenceLine.EndPoint.Y:F2}, {referenceLine.EndPoint.Z:F2}), " +
@@ -1827,7 +1853,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
_pathPlanningManager.DisableMouseHandling();
IsSelectingAssemblyStartPoint = true;
- OnPropertyChanged(nameof(CanSelectAssemblyStartPoint));
+ NotifyRailAssemblyCommandStateChanged();
PathClickToolPlugin.MouseClicked -= OnAssemblyReferenceMouseClicked;
PathClickToolPlugin.MouseClicked += OnAssemblyReferenceMouseClicked;
@@ -1864,6 +1890,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
throw new InvalidOperationException("请先选中一条需要编辑起点的 Rail 路径。");
}
+ SetRailAssemblyWorkflowMode(RailAssemblyWorkflowMode.EditSelectedRail);
RefreshSelectedRailReferenceLineVisuals();
if (!AssemblyReferencePathManager.Instance.HasReferenceLine)
{
@@ -1875,9 +1902,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
_pathPlanningManager.DisableMouseHandling();
_isEditingSelectedRailStartPoint = true;
IsSelectingAssemblyStartPoint = true;
- OnPropertyChanged(nameof(CanSelectAssemblyStartPoint));
- OnPropertyChanged(nameof(CanRepositionRailStartPoint));
- OnPropertyChanged(nameof(CanRepositionRailStartPoint));
+ NotifyRailAssemblyCommandStateChanged();
PathClickToolPlugin.MouseClicked -= OnAssemblyReferenceMouseClicked;
PathClickToolPlugin.MouseClicked += OnAssemblyReferenceMouseClicked;
@@ -1899,6 +1924,8 @@ namespace NavisworksTransport.UI.WPF.ViewModels
try
{
ClearAssemblyReferenceLineVisuals();
+ ClearAssemblyEndFaceAnalysisVisuals();
+ ClearAssemblyInstallationReferenceVisuals();
UpdateMainStatus("已隐藏终端安装辅助线");
LogManager.Info("[直线装配] 已隐藏辅助线");
}
@@ -1908,7 +1935,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
}
}
- private async Task ExecuteAnalyzeAssemblyTerminalFaceAsync()
+ private async Task ExecuteAnalyzeAssemblyTerminalFaceAsync(RailAssemblyWorkflowMode workflowMode)
{
await SafeExecuteAsync(() =>
{
@@ -1922,13 +1949,19 @@ namespace NavisworksTransport.UI.WPF.ViewModels
throw new InvalidOperationException("终点箱体未设置或已失效,请先捕获终点箱体。");
}
- var selectedRailRoute = GetSelectedCoreRoute();
- if (selectedRailRoute == null || selectedRailRoute.PathType != PathType.Rail)
+ bool isEditingExistingRailRoute = workflowMode == RailAssemblyWorkflowMode.EditSelectedRail;
+ var selectedRailRoute = isEditingExistingRailRoute ? GetSelectedCoreRoute() : null;
+ if (isEditingExistingRailRoute)
{
- throw new InvalidOperationException("请先选中一条需要编辑安装点的 Rail 路径。");
+ if (selectedRailRoute == null || selectedRailRoute.PathType != PathType.Rail)
+ {
+ throw new InvalidOperationException("请先选中一条需要编辑安装点的 Rail 路径。");
+ }
+
+ AssemblyMountMode = selectedRailRoute.RailMountMode;
}
- AssemblyMountMode = selectedRailRoute.RailMountMode;
+ SetRailAssemblyWorkflowMode(workflowMode);
CleanupAssemblyReferenceSelection();
CleanupAssemblyEndFaceSelection(clearVisuals: false);
@@ -1941,10 +1974,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
_pathPlanningManager.DisableMouseHandling();
_isSelectingAssemblyEndFacePoints = true;
- OnPropertyChanged(nameof(CanAnalyzeAssemblyTerminalFace));
- OnPropertyChanged(nameof(CanSelectAssemblyStartPoint));
- OnPropertyChanged(nameof(CanRepositionRailStartPoint));
- OnPropertyChanged(nameof(CanSelectAssemblyInstallationPoint));
+ NotifyRailAssemblyCommandStateChanged();
PathClickToolPlugin.MouseClicked -= OnAssemblyEndFaceMouseClicked;
PathClickToolPlugin.MouseClicked += OnAssemblyEndFaceMouseClicked;
@@ -1956,11 +1986,12 @@ namespace NavisworksTransport.UI.WPF.ViewModels
}
UpdateMainStatus("请在同一个端面平面上连续点击 3 个点,系统将分析端面中心。");
- LogManager.Info("[直线装配] 已进入端面三点分析模式");
+ LogManager.Info(
+ $"[直线装配] 已进入端面三点分析模式,模式={(isEditingExistingRailRoute ? "编辑现有Rail路径" : "新建Rail路径")}");
}, "分析终端端面");
}
- private async Task ExecuteSelectAssemblyInstallationPointAsync()
+ private async Task ExecuteSelectAssemblyInstallationPointAsync(RailAssemblyWorkflowMode workflowMode)
{
await SafeExecuteAsync(() =>
{
@@ -1974,6 +2005,15 @@ namespace NavisworksTransport.UI.WPF.ViewModels
throw new InvalidOperationException("终点箱体未设置或已失效,请先捕获终点箱体。");
}
+ bool isEditingExistingRailRoute = workflowMode == RailAssemblyWorkflowMode.EditSelectedRail;
+ var selectedRailRoute = isEditingExistingRailRoute ? GetSelectedCoreRoute() : null;
+ if (isEditingExistingRailRoute && (selectedRailRoute == null || selectedRailRoute.PathType != PathType.Rail))
+ {
+ throw new InvalidOperationException("请先选中一条需要编辑安装点的 Rail 路径。");
+ }
+
+ SetRailAssemblyWorkflowMode(workflowMode);
+
CleanupAssemblyReferenceSelection();
CleanupAssemblyEndFaceSelection(clearVisuals: false);
CleanupAssemblyInstallationSelection();
@@ -1982,14 +2022,9 @@ namespace NavisworksTransport.UI.WPF.ViewModels
_pathPlanningManager.DisableMouseHandling();
_isSelectingAssemblyInstallationPoint = true;
- // 判断是编辑现有路径还是创建新路径
- var selectedRailRoute = GetSelectedCoreRoute();
- _isEditingSelectedRailInstallationPoint = selectedRailRoute != null && selectedRailRoute.PathType == PathType.Rail;
+ _isEditingSelectedRailInstallationPoint = isEditingExistingRailRoute;
- OnPropertyChanged(nameof(CanAnalyzeAssemblyTerminalFace));
- OnPropertyChanged(nameof(CanSelectAssemblyStartPoint));
- OnPropertyChanged(nameof(CanRepositionRailStartPoint));
- OnPropertyChanged(nameof(CanSelectAssemblyInstallationPoint));
+ NotifyRailAssemblyCommandStateChanged();
PathClickToolPlugin.MouseClicked -= OnAssemblyInstallationMouseClicked;
PathClickToolPlugin.MouseClicked += OnAssemblyInstallationMouseClicked;
@@ -2136,14 +2171,14 @@ namespace NavisworksTransport.UI.WPF.ViewModels
// 创建新路径:生成参考线,等待用户选择起点
LogManager.Info("[直线装配] 安装点已确定,请继续选择起点以生成新路径");
}
- CleanupAssemblyInstallationSelection();
+ CleanupAssemblyInstallationSelection(clearVisuals: false);
}, "处理安装点拾取");
}
catch (Exception ex)
{
LogManager.Error($"[直线装配] 安装点计算失败: {ex.Message}", ex);
UpdateMainStatus($"安装点计算失败: {ex.Message}");
- CleanupAssemblyInstallationSelection();
+ CleanupAssemblyInstallationSelection(clearVisuals: true);
}
}
@@ -2259,9 +2294,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
{
AssemblyStartPointText = "未选择";
}
- OnPropertyChanged(nameof(CanSelectAssemblyStartPoint));
- OnPropertyChanged(nameof(CanSelectAssemblyInstallationPoint));
- OnPropertyChanged(nameof(CanAnalyzeAssemblyTerminalFace));
+ NotifyRailAssemblyCommandStateChanged();
if (!string.IsNullOrWhiteSpace(statusMessage))
{
UpdateMainStatus(statusMessage);
@@ -2349,9 +2382,11 @@ namespace NavisworksTransport.UI.WPF.ViewModels
_assemblyInstallationBaseAnchorPoint.Y + _assemblyInstallationPlaneNormal.Y * (float)offset,
_assemblyInstallationBaseAnchorPoint.Z + _assemblyInstallationPlaneNormal.Z * (float)offset);
- if (_isSelectingAssemblyInstallationPoint)
+ if (_hasAssemblyInstallationReference)
{
+ RenderAssemblyInstallationPickPoints(_assemblyInstallationSeedPoints);
RenderAssemblyInstallationAnchorPoint(_assemblyInstallationAnchorPoint);
+ RenderAssemblyInstallationCenterLine(_assemblyInstallationPickPoint, GetCurrentAssemblyOpticalAxisDirection());
RenderAssemblyInstallationPlane(_assemblyInstallationAnchorPoint, GetCurrentAssemblyOpticalAxisDirection(), _assemblyInstallationPlaneSpanDirection);
}
else
@@ -2521,7 +2556,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
RenderAssemblyAnchorMarker();
RenderAssemblyCenterGuideLine();
RenderAssemblyReferenceLine(referenceLine);
- OnPropertyChanged(nameof(CanSelectAssemblyStartPoint));
+ NotifyRailAssemblyCommandStateChanged();
}
catch (Exception ex)
{
@@ -2845,7 +2880,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
$"安装点=({_assemblyInstallationAnchorPoint.X:F3}, {_assemblyInstallationAnchorPoint.Y:F3}, {_assemblyInstallationAnchorPoint.Z:F3}), " +
$"偏距={_assemblyInstallationOffsetDistanceInMeters:F3}m, 平面法向=({result.PlaneNormal.X:F4}, {result.PlaneNormal.Y:F4}, {result.PlaneNormal.Z:F4})");
OnPropertyChanged(nameof(CanGenerateAssemblyReferenceRod));
- OnPropertyChanged(nameof(AssemblyInstallationPointButtonText));
+ NotifyRailAssemblyCommandStateChanged();
}
private void RenderAssemblyInstallationPickPoints(IReadOnlyList pickPoints)
@@ -3028,7 +3063,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
ClearAssemblyAnchorMarker();
ClearAssemblyCenterGuideLine();
ClearAssemblyReferenceLine();
- OnPropertyChanged(nameof(CanSelectAssemblyStartPoint));
+ NotifyRailAssemblyCommandStateChanged();
OnPropertyChanged(nameof(CanRepositionRailStartPoint));
}
@@ -3081,7 +3116,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
_assemblyAnchorVerticalOffsetInMeters = DefaultAssemblyAnchorVerticalOffsetInMeters;
OnPropertyChanged(nameof(AssemblyAnchorVerticalOffsetInMeters));
OnPropertyChanged(nameof(CanGenerateAssemblyReferenceRod));
- OnPropertyChanged(nameof(AssemblyInstallationPointButtonText));
+ NotifyRailAssemblyCommandStateChanged();
}
private void CleanupAssemblyReferenceSelection()
@@ -3093,8 +3128,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
_isEditingSelectedRailStartPoint = false;
_pathPlanningManager?.EnableMouseHandling();
_pathPlanningManager?.StopClickTool();
- OnPropertyChanged(nameof(CanSelectAssemblyStartPoint));
- OnPropertyChanged(nameof(CanRepositionRailStartPoint));
+ NotifyRailAssemblyCommandStateChanged();
}
catch (Exception ex)
{
@@ -3102,7 +3136,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
}
}
- private void CleanupAssemblyInstallationSelection()
+ private void CleanupAssemblyInstallationSelection(bool clearVisuals = true)
{
try
{
@@ -3111,12 +3145,11 @@ namespace NavisworksTransport.UI.WPF.ViewModels
_isEditingSelectedRailInstallationPoint = false;
_pathPlanningManager?.EnableMouseHandling();
_pathPlanningManager?.StopClickTool();
- ClearAssemblyInstallationReferenceVisuals();
- OnPropertyChanged(nameof(CanAnalyzeAssemblyTerminalFace));
- OnPropertyChanged(nameof(CanSelectAssemblyStartPoint));
- OnPropertyChanged(nameof(CanRepositionRailStartPoint));
- OnPropertyChanged(nameof(CanSelectAssemblyInstallationPoint));
- OnPropertyChanged(nameof(AssemblyInstallationPointButtonText));
+ if (clearVisuals)
+ {
+ ClearAssemblyInstallationReferenceVisuals();
+ }
+ NotifyRailAssemblyCommandStateChanged();
}
catch (Exception ex)
{
@@ -3138,11 +3171,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
ClearAssemblyEndFaceAnalysisVisuals();
ResetAssemblyEndFaceAnalysisState();
}
- OnPropertyChanged(nameof(CanAnalyzeAssemblyTerminalFace));
- OnPropertyChanged(nameof(CanSelectAssemblyStartPoint));
- OnPropertyChanged(nameof(CanRepositionRailStartPoint));
- OnPropertyChanged(nameof(CanSelectAssemblyInstallationPoint));
- OnPropertyChanged(nameof(AssemblyInstallationPointButtonText));
+ NotifyRailAssemblyCommandStateChanged();
}
catch (Exception ex)
{
diff --git a/src/UI/WPF/Views/PathEditingView.xaml b/src/UI/WPF/Views/PathEditingView.xaml
index b17ac0d..59c1afb 100644
--- a/src/UI/WPF/Views/PathEditingView.xaml
+++ b/src/UI/WPF/Views/PathEditingView.xaml
@@ -478,24 +478,24 @@ NavisworksTransport 路径编辑页签视图 - 采用与动画控制和分层管
-
+
+ IsEnabled="{Binding CanSelectAssemblyInstallationPointForCreate}"/>
+ IsEnabled="{Binding CanSelectAssemblyStartPointForCreate}"/>
@@ -563,17 +563,17 @@ NavisworksTransport 路径编辑页签视图 - 采用与动画控制和分层管
-
Date: Tue, 31 Mar 2026 23:38:47 +0800
Subject: [PATCH 50/87] Add explicit exit for rail assembly workflow
---
src/UI/WPF/ViewModels/PathEditingViewModel.cs | 76 +++++++++++++++++--
src/UI/WPF/Views/PathEditingView.xaml | 10 +++
2 files changed, 80 insertions(+), 6 deletions(-)
diff --git a/src/UI/WPF/ViewModels/PathEditingViewModel.cs b/src/UI/WPF/ViewModels/PathEditingViewModel.cs
index aa104d7..2db19f5 100644
--- a/src/UI/WPF/ViewModels/PathEditingViewModel.cs
+++ b/src/UI/WPF/ViewModels/PathEditingViewModel.cs
@@ -222,11 +222,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
{
// 路径变更时清除选中的路径点
SelectedPathPoint = null;
- ClearAssemblyReferenceLineVisuals();
- ClearAssemblyInstallationReferenceVisuals();
-
- // 重置 Rail 编辑状态
- SetRailAssemblyWorkflowMode(RailAssemblyWorkflowMode.None);
+ ResetRailAssemblyWorkflowState(announce: false);
// 🔧 修复:同步PathPlanningManager的CurrentRoute
if (_pathPlanningManager != null && value != null)
@@ -1082,6 +1078,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
public ICommand SelectAssemblyInstallationPointForCreateCommand { get; private set; }
public ICommand SelectAssemblyInstallationPointForEditCommand { get; private set; }
public ICommand ClearAssemblyReferenceRodCommand { get; private set; }
+ public ICommand CancelRailAssemblyWorkflowCommand { get; private set; }
public ICommand AnalyzeAssemblyTerminalFaceForCreateCommand { get; private set; }
public ICommand AnalyzeAssemblyTerminalFaceForEditCommand { get; private set; }
public ICommand DecreaseSelectedRailNormalOffsetCommand { get; private set; }
@@ -1114,7 +1111,8 @@ namespace NavisworksTransport.UI.WPF.ViewModels
public bool CanExecuteEndEdit => (_pathPlanningManager?.PathEditState == PathEditState.Creating ||
_pathPlanningManager?.PathEditState == PathEditState.Editing ||
_pathPlanningManager?.PathEditState == PathEditState.AddingPoints ||
- _pathPlanningManager?.PathEditState == PathEditState.EditingPoint);
+ _pathPlanningManager?.PathEditState == PathEditState.EditingPoint) ||
+ CanCancelRailAssemblyWorkflow;
public bool CanExecuteClearPath => SelectedPathRoute != null && SelectedPathRoute.Points.Count > 0;
@@ -1173,6 +1171,13 @@ namespace NavisworksTransport.UI.WPF.ViewModels
: "选安装点";
public bool IsSelectingAssemblyEndFacePoints => _isSelectingAssemblyEndFacePoints;
+ public bool CanCancelRailAssemblyWorkflow => _railAssemblyWorkflowMode != RailAssemblyWorkflowMode.None ||
+ HasAssemblyTerminalObject ||
+ _isSelectingAssemblyStartPoint ||
+ _isSelectingAssemblyEndFacePoints ||
+ _isSelectingAssemblyInstallationPoint ||
+ _hasAssemblyEndFaceAnalysis ||
+ _hasAssemblyInstallationReference;
private bool IsRailCreateWorkflowActive => _railAssemblyWorkflowMode == RailAssemblyWorkflowMode.CreateNewRailPath;
private bool IsRailEditingWorkflowActive => _railAssemblyWorkflowMode == RailAssemblyWorkflowMode.EditSelectedRail;
@@ -1343,6 +1348,8 @@ namespace NavisworksTransport.UI.WPF.ViewModels
OnPropertyChanged(nameof(CanSelectAssemblyInstallationPointForEdit));
OnPropertyChanged(nameof(CreateAssemblyInstallationPointButtonText));
OnPropertyChanged(nameof(EditAssemblyInstallationPointButtonText));
+ OnPropertyChanged(nameof(CanCancelRailAssemblyWorkflow));
+ OnPropertyChanged(nameof(CanExecuteEndEdit));
}
private bool HasSelectedRailInstallationReference()
@@ -1717,6 +1724,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
SelectAssemblyInstallationPointForCreateCommand = new RelayCommand(async () => await ExecuteSelectAssemblyInstallationPointAsync(RailAssemblyWorkflowMode.CreateNewRailPath), () => CanSelectAssemblyInstallationPointForCreate);
SelectAssemblyInstallationPointForEditCommand = new RelayCommand(async () => await ExecuteSelectAssemblyInstallationPointAsync(RailAssemblyWorkflowMode.EditSelectedRail), () => CanSelectAssemblyInstallationPointForEdit);
ClearAssemblyReferenceRodCommand = new RelayCommand(() => ExecuteClearAssemblyReferenceRod());
+ CancelRailAssemblyWorkflowCommand = new RelayCommand(() => ExecuteCancelRailAssemblyWorkflow(), () => CanCancelRailAssemblyWorkflow);
AnalyzeAssemblyTerminalFaceForCreateCommand = new RelayCommand(async () => await ExecuteAnalyzeAssemblyTerminalFaceAsync(RailAssemblyWorkflowMode.CreateNewRailPath), () => CanAnalyzeAssemblyTerminalFaceForCreate);
AnalyzeAssemblyTerminalFaceForEditCommand = new RelayCommand(async () => await ExecuteAnalyzeAssemblyTerminalFaceAsync(RailAssemblyWorkflowMode.EditSelectedRail), () => CanAnalyzeAssemblyTerminalFaceForEdit);
DecreaseSelectedRailNormalOffsetCommand = new RelayCommand(() => AdjustSelectedRailNormalOffset(-RailNormalOffsetNudgeStepInMeters));
@@ -2306,6 +2314,55 @@ namespace NavisworksTransport.UI.WPF.ViewModels
}
}
+ private void ResetRailAssemblyWorkflowState(bool announce, string statusMessage = null, string logMessage = null)
+ {
+ CleanupAssemblyReferenceSelection();
+ CleanupAssemblyEndFaceSelection(clearVisuals: true);
+ CleanupAssemblyInstallationSelection(clearVisuals: true);
+ ClearAssemblyReferenceLineVisuals();
+ ClearAssemblyEndFaceAnalysisVisuals();
+ ClearAssemblyInstallationReferenceVisuals();
+ ResetAssemblyEndFaceAnalysisState();
+ ResetAssemblyInstallationReferenceState();
+
+ _assemblyTerminalObject = null;
+ _assemblyStartPoint = Point3D.Origin;
+ HasAssemblyTerminalObject = false;
+ AssemblyTerminalObjectName = "未选择";
+ AssemblyTerminalObjectInfo = "请选择终点处已安装箱体";
+ AssemblyStartPointText = "未选择";
+
+ SetRailAssemblyWorkflowMode(RailAssemblyWorkflowMode.None);
+ OnPropertyChanged(nameof(CanGenerateAssemblyReferenceRod));
+ NotifyRailAssemblyCommandStateChanged();
+
+ if (announce && !string.IsNullOrWhiteSpace(statusMessage))
+ {
+ UpdateMainStatus(statusMessage);
+ }
+
+ if (!string.IsNullOrWhiteSpace(logMessage))
+ {
+ LogManager.Info(logMessage);
+ }
+ }
+
+ private void ExecuteCancelRailAssemblyWorkflow()
+ {
+ try
+ {
+ ResetRailAssemblyWorkflowState(
+ announce: true,
+ statusMessage: "已取消 Rail 装配流程",
+ logMessage: "[直线装配] 已取消 Rail 装配流程并清理状态");
+ }
+ catch (Exception ex)
+ {
+ LogManager.Error($"[直线装配] 取消 Rail 装配流程失败: {ex.Message}", ex);
+ UpdateMainStatus($"取消 Rail 装配流程失败: {ex.Message}");
+ }
+ }
+
private Point3D GetAssemblyTerminalAnchorPoint()
{
if (_assemblyTerminalObject == null || !ModelItemAnalysisHelper.IsModelItemValid(_assemblyTerminalObject))
@@ -4798,6 +4855,13 @@ namespace NavisworksTransport.UI.WPF.ViewModels
{
if (!CanExecuteEndEdit) return;
+ if ((_pathPlanningManager == null || _pathPlanningManager.PathEditState == PathEditState.Viewing) &&
+ CanCancelRailAssemblyWorkflow)
+ {
+ ExecuteCancelRailAssemblyWorkflow();
+ return;
+ }
+
await SafeExecuteAsync(async () =>
{
bool success = false;
diff --git a/src/UI/WPF/Views/PathEditingView.xaml b/src/UI/WPF/Views/PathEditingView.xaml
index 59c1afb..4ea9e67 100644
--- a/src/UI/WPF/Views/PathEditingView.xaml
+++ b/src/UI/WPF/Views/PathEditingView.xaml
@@ -499,6 +499,10 @@ NavisworksTransport 路径编辑页签视图 - 采用与动画控制和分层管
+
@@ -580,6 +584,12 @@ NavisworksTransport 路径编辑页签视图 - 采用与动画控制和分层管
Command="{Binding RepositionRailStartPointCommand}"
IsEnabled="{Binding CanRepositionRailStartPoint}"
Style="{StaticResource SecondaryButtonStyle}"/>
+
public class PathEditingViewModel : ViewModelBase, IDisposable
{
- private enum RailAssemblyWorkflowMode
- {
- None,
- CreateNewRailPath,
- EditSelectedRail
- }
-
#region 私有字段
private PathPlanningManager _pathPlanningManager;
@@ -135,38 +128,13 @@ namespace NavisworksTransport.UI.WPF.ViewModels
private const double DefaultAssemblySphereCenterX = 0.0;
private const double DefaultAssemblySphereCenterY = 0.0;
private const double DefaultAssemblySphereCenterZ = 0.0;
- private string _assemblyTerminalObjectName = "未选择";
- private string _assemblyTerminalObjectInfo = "请选择终点处已安装箱体";
- private double _assemblyReferenceRodLengthInMeters = DefaultAssemblyReferenceRodLengthInMeters;
- private double _assemblyReferenceRodDiameterInMeters = DefaultAssemblyReferenceRodDiameterInMeters;
- private double _assemblyAnchorVerticalOffsetInMeters = DefaultAssemblyAnchorVerticalOffsetInMeters;
- private double _assemblySphereCenterX = DefaultAssemblySphereCenterX;
- private double _assemblySphereCenterY = DefaultAssemblySphereCenterY;
- private double _assemblySphereCenterZ = DefaultAssemblySphereCenterZ;
- private string _assemblyStartPointText = "未选择";
- private RailMountMode _assemblyMountMode = RailMountMode.UnderRail;
- private bool _hasAssemblyTerminalObject;
- private bool _isSelectingAssemblyStartPoint;
- private bool _isEditingSelectedRailStartPoint;
- private bool _isSelectingAssemblyEndFacePoints;
- private bool _isSelectingAssemblyInstallationPoint;
- private bool _isEditingSelectedRailInstallationPoint;
- private RailAssemblyWorkflowMode _railAssemblyWorkflowMode = RailAssemblyWorkflowMode.None;
- private bool _hasAssemblyEndFaceAnalysis;
- private bool _hasAssemblyInstallationReference;
- private ModelItem _assemblyTerminalObject;
- private Point3D _assemblyStartPoint;
- private Point3D _assemblyEndFaceCenterPoint;
- private Vector3 _assemblyEndFaceNormal;
- private Point3D _assemblyInstallationPickPoint;
- private Point3D _assemblyInstallationBaseAnchorPoint;
- private Point3D _assemblyInstallationAnchorPoint;
- private Vector3 _assemblyInstallationPlaneNormal;
- private Vector3 _assemblyInstallationPlaneSpanDirection;
- private double _assemblyInstallationOffsetDistanceInMeters;
- private string _assemblyInstallationReferenceRouteId;
- private readonly List _assemblyInstallationSeedPoints = new List();
- private readonly List _assemblyEndFaceSeedPoints = new List();
+ private readonly RailAssemblyWorkflowContext _railAssemblyContext = new RailAssemblyWorkflowContext(
+ DefaultAssemblyReferenceRodLengthInMeters,
+ DefaultAssemblyReferenceRodDiameterInMeters,
+ DefaultAssemblyAnchorVerticalOffsetInMeters,
+ DefaultAssemblySphereCenterX,
+ DefaultAssemblySphereCenterY,
+ DefaultAssemblySphereCenterZ);
private const string AssemblyAnchorMarkerPathId = "assembly_anchor_marker";
private const string AssemblyCenterGuideLinePathId = "assembly_center_guide_line";
private const string AssemblyReferenceLinePathId = "assembly_reference_line";
@@ -181,6 +149,178 @@ namespace NavisworksTransport.UI.WPF.ViewModels
private const double AssemblyInstallationPlanePaddingInMeters = 0.5;
private const double AssemblyInstallationLineLengthScale = 1.2;
+ private string _assemblyTerminalObjectName
+ {
+ get => _railAssemblyContext.TerminalObjectName;
+ set => _railAssemblyContext.TerminalObjectName = value;
+ }
+
+ private string _assemblyTerminalObjectInfo
+ {
+ get => _railAssemblyContext.TerminalObjectInfo;
+ set => _railAssemblyContext.TerminalObjectInfo = value;
+ }
+
+ private double _assemblyReferenceRodLengthInMeters
+ {
+ get => _railAssemblyContext.ReferenceRodLengthInMeters;
+ set => _railAssemblyContext.ReferenceRodLengthInMeters = value;
+ }
+
+ private double _assemblyReferenceRodDiameterInMeters
+ {
+ get => _railAssemblyContext.ReferenceRodDiameterInMeters;
+ set => _railAssemblyContext.ReferenceRodDiameterInMeters = value;
+ }
+
+ private double _assemblyAnchorVerticalOffsetInMeters
+ {
+ get => _railAssemblyContext.AnchorVerticalOffsetInMeters;
+ set => _railAssemblyContext.AnchorVerticalOffsetInMeters = value;
+ }
+
+ private double _assemblySphereCenterX
+ {
+ get => _railAssemblyContext.SphereCenterX;
+ set => _railAssemblyContext.SphereCenterX = value;
+ }
+
+ private double _assemblySphereCenterY
+ {
+ get => _railAssemblyContext.SphereCenterY;
+ set => _railAssemblyContext.SphereCenterY = value;
+ }
+
+ private double _assemblySphereCenterZ
+ {
+ get => _railAssemblyContext.SphereCenterZ;
+ set => _railAssemblyContext.SphereCenterZ = value;
+ }
+
+ private string _assemblyStartPointText
+ {
+ get => _railAssemblyContext.StartPointText;
+ set => _railAssemblyContext.StartPointText = value;
+ }
+
+ private RailMountMode _assemblyMountMode
+ {
+ get => _railAssemblyContext.MountMode;
+ set => _railAssemblyContext.MountMode = value;
+ }
+
+ private bool _hasAssemblyTerminalObject
+ {
+ get => _railAssemblyContext.HasTerminalObject;
+ set => _railAssemblyContext.HasTerminalObject = value;
+ }
+
+ private bool _isSelectingAssemblyStartPoint
+ {
+ get => _railAssemblyContext.IsSelectingStartPoint;
+ set => _railAssemblyContext.IsSelectingStartPoint = value;
+ }
+
+ private bool _isSelectingAssemblyEndFacePoints
+ {
+ get => _railAssemblyContext.IsSelectingEndFacePoints;
+ set => _railAssemblyContext.IsSelectingEndFacePoints = value;
+ }
+
+ private bool _isSelectingAssemblyInstallationPoint
+ {
+ get => _railAssemblyContext.IsSelectingInstallationPoint;
+ set => _railAssemblyContext.IsSelectingInstallationPoint = value;
+ }
+
+ private RailAssemblyWorkflowMode _railAssemblyWorkflowMode
+ {
+ get => _railAssemblyContext.WorkflowMode;
+ set => _railAssemblyContext.WorkflowMode = value;
+ }
+
+ private bool _hasAssemblyEndFaceAnalysis
+ {
+ get => _railAssemblyContext.HasEndFaceAnalysis;
+ set => _railAssemblyContext.HasEndFaceAnalysis = value;
+ }
+
+ private bool _hasAssemblyInstallationReference
+ {
+ get => _railAssemblyContext.HasInstallationReference;
+ set => _railAssemblyContext.HasInstallationReference = value;
+ }
+
+ private ModelItem _assemblyTerminalObject
+ {
+ get => _railAssemblyContext.TerminalObject;
+ set => _railAssemblyContext.TerminalObject = value;
+ }
+
+ private Point3D _assemblyStartPoint
+ {
+ get => _railAssemblyContext.StartPoint;
+ set => _railAssemblyContext.StartPoint = value;
+ }
+
+ private Point3D _assemblyEndFaceCenterPoint
+ {
+ get => _railAssemblyContext.EndFaceCenterPoint;
+ set => _railAssemblyContext.EndFaceCenterPoint = value;
+ }
+
+ private Vector3 _assemblyEndFaceNormal
+ {
+ get => _railAssemblyContext.EndFaceNormal;
+ set => _railAssemblyContext.EndFaceNormal = value;
+ }
+
+ private Point3D _assemblyInstallationPickPoint
+ {
+ get => _railAssemblyContext.InstallationPickPoint;
+ set => _railAssemblyContext.InstallationPickPoint = value;
+ }
+
+ private Point3D _assemblyInstallationBaseAnchorPoint
+ {
+ get => _railAssemblyContext.InstallationBaseAnchorPoint;
+ set => _railAssemblyContext.InstallationBaseAnchorPoint = value;
+ }
+
+ private Point3D _assemblyInstallationAnchorPoint
+ {
+ get => _railAssemblyContext.InstallationAnchorPoint;
+ set => _railAssemblyContext.InstallationAnchorPoint = value;
+ }
+
+ private Vector3 _assemblyInstallationPlaneNormal
+ {
+ get => _railAssemblyContext.InstallationPlaneNormal;
+ set => _railAssemblyContext.InstallationPlaneNormal = value;
+ }
+
+ private Vector3 _assemblyInstallationPlaneSpanDirection
+ {
+ get => _railAssemblyContext.InstallationPlaneSpanDirection;
+ set => _railAssemblyContext.InstallationPlaneSpanDirection = value;
+ }
+
+ private double _assemblyInstallationOffsetDistanceInMeters
+ {
+ get => _railAssemblyContext.InstallationOffsetDistanceInMeters;
+ set => _railAssemblyContext.InstallationOffsetDistanceInMeters = value;
+ }
+
+ private string _assemblyInstallationReferenceRouteId
+ {
+ get => _railAssemblyContext.InstallationReferenceRouteId;
+ set => _railAssemblyContext.InstallationReferenceRouteId = value;
+ }
+
+ private List _assemblyInstallationSeedPoints => _railAssemblyContext.InstallationSeedPoints;
+
+ private List _assemblyEndFaceSeedPoints => _railAssemblyContext.EndFaceSeedPoints;
+
// 自动路径起点和终点的路径对象引用(用于正确的ID管理)
private PathRoute _autoPathStartPointRoute = null;
private PathRoute _autoPathEndPointRoute = null;
@@ -250,8 +390,8 @@ namespace NavisworksTransport.UI.WPF.ViewModels
{
try
{
- ViewpointHelper.AdjustViewpointToPathCenter(coreRoute);
- LogManager.Info($"UI路径切换:已自动调整视角到路径中心: {value.Name}");
+ ViewpointHelper.AdjustViewpointForPath(coreRoute);
+ LogManager.Info($"UI路径切换:已自动调整视角: {value.Name}, 类型={coreRoute.PathType}");
}
catch (Exception ex)
{
@@ -417,13 +557,31 @@ namespace NavisworksTransport.UI.WPF.ViewModels
public string AssemblyTerminalObjectName
{
get => _assemblyTerminalObjectName;
- set => SetProperty(ref _assemblyTerminalObjectName, value);
+ set
+ {
+ if (string.Equals(_assemblyTerminalObjectName, value, StringComparison.Ordinal))
+ {
+ return;
+ }
+
+ _assemblyTerminalObjectName = value;
+ OnPropertyChanged(nameof(AssemblyTerminalObjectName));
+ }
}
public string AssemblyTerminalObjectInfo
{
get => _assemblyTerminalObjectInfo;
- set => SetProperty(ref _assemblyTerminalObjectInfo, value);
+ set
+ {
+ if (string.Equals(_assemblyTerminalObjectInfo, value, StringComparison.Ordinal))
+ {
+ return;
+ }
+
+ _assemblyTerminalObjectInfo = value;
+ OnPropertyChanged(nameof(AssemblyTerminalObjectInfo));
+ }
}
public double AssemblyReferenceRodLengthInMeters
@@ -431,10 +589,13 @@ namespace NavisworksTransport.UI.WPF.ViewModels
get => _assemblyReferenceRodLengthInMeters;
set
{
- if (SetProperty(ref _assemblyReferenceRodLengthInMeters, value))
+ if (Math.Abs(_assemblyReferenceRodLengthInMeters - value) < 1e-9)
{
- OnPropertyChanged(nameof(CanGenerateAssemblyReferenceRod));
+ return;
}
+
+ _assemblyReferenceRodLengthInMeters = value;
+ OnPropertyChanged(nameof(AssemblyReferenceRodLengthInMeters));
}
}
@@ -443,10 +604,13 @@ namespace NavisworksTransport.UI.WPF.ViewModels
get => _assemblyReferenceRodDiameterInMeters;
set
{
- if (SetProperty(ref _assemblyReferenceRodDiameterInMeters, value))
+ if (Math.Abs(_assemblyReferenceRodDiameterInMeters - value) < 1e-9)
{
- OnPropertyChanged(nameof(CanGenerateAssemblyReferenceRod));
+ return;
}
+
+ _assemblyReferenceRodDiameterInMeters = value;
+ OnPropertyChanged(nameof(AssemblyReferenceRodDiameterInMeters));
}
}
@@ -455,7 +619,15 @@ namespace NavisworksTransport.UI.WPF.ViewModels
get => _assemblyAnchorVerticalOffsetInMeters;
set
{
- if (SetProperty(ref _assemblyAnchorVerticalOffsetInMeters, value) && HasAssemblyTerminalObject)
+ if (Math.Abs(_assemblyAnchorVerticalOffsetInMeters - value) < 1e-9)
+ {
+ return;
+ }
+
+ _assemblyAnchorVerticalOffsetInMeters = value;
+ OnPropertyChanged(nameof(AssemblyAnchorVerticalOffsetInMeters));
+
+ if (HasAssemblyTerminalObject)
{
var coreRoute = GetSelectedCoreRoute();
if (_hasAssemblyInstallationReference)
@@ -475,7 +647,6 @@ namespace NavisworksTransport.UI.WPF.ViewModels
}
else
{
- RenderAssemblyAnchorMarker();
RefreshAssemblyReferenceRodIfNeeded();
}
}
@@ -487,7 +658,15 @@ namespace NavisworksTransport.UI.WPF.ViewModels
get => _assemblySphereCenterX;
set
{
- if (SetProperty(ref _assemblySphereCenterX, value) && HasAssemblyTerminalObject)
+ if (Math.Abs(_assemblySphereCenterX - value) < 1e-9)
+ {
+ return;
+ }
+
+ _assemblySphereCenterX = value;
+ OnPropertyChanged(nameof(AssemblySphereCenterX));
+
+ if (HasAssemblyTerminalObject)
{
RefreshAssemblyTerminalObjectInfo();
RefreshAssemblyReferenceRodIfNeeded();
@@ -500,7 +679,15 @@ namespace NavisworksTransport.UI.WPF.ViewModels
get => _assemblySphereCenterY;
set
{
- if (SetProperty(ref _assemblySphereCenterY, value) && HasAssemblyTerminalObject)
+ if (Math.Abs(_assemblySphereCenterY - value) < 1e-9)
+ {
+ return;
+ }
+
+ _assemblySphereCenterY = value;
+ OnPropertyChanged(nameof(AssemblySphereCenterY));
+
+ if (HasAssemblyTerminalObject)
{
RefreshAssemblyTerminalObjectInfo();
RefreshAssemblyReferenceRodIfNeeded();
@@ -513,7 +700,15 @@ namespace NavisworksTransport.UI.WPF.ViewModels
get => _assemblySphereCenterZ;
set
{
- if (SetProperty(ref _assemblySphereCenterZ, value) && HasAssemblyTerminalObject)
+ if (Math.Abs(_assemblySphereCenterZ - value) < 1e-9)
+ {
+ return;
+ }
+
+ _assemblySphereCenterZ = value;
+ OnPropertyChanged(nameof(AssemblySphereCenterZ));
+
+ if (HasAssemblyTerminalObject)
{
RefreshAssemblyTerminalObjectInfo();
RefreshAssemblyReferenceRodIfNeeded();
@@ -524,13 +719,31 @@ namespace NavisworksTransport.UI.WPF.ViewModels
public bool HasAssemblyTerminalObject
{
get => _hasAssemblyTerminalObject;
- set => SetProperty(ref _hasAssemblyTerminalObject, value);
+ set
+ {
+ if (_hasAssemblyTerminalObject == value)
+ {
+ return;
+ }
+
+ _hasAssemblyTerminalObject = value;
+ OnPropertyChanged(nameof(HasAssemblyTerminalObject));
+ }
}
public string AssemblyStartPointText
{
get => _assemblyStartPointText;
- set => SetProperty(ref _assemblyStartPointText, value);
+ set
+ {
+ if (string.Equals(_assemblyStartPointText, value, StringComparison.Ordinal))
+ {
+ return;
+ }
+
+ _assemblyStartPointText = value;
+ OnPropertyChanged(nameof(AssemblyStartPointText));
+ }
}
public RailMountMode AssemblyMountMode
@@ -538,7 +751,15 @@ namespace NavisworksTransport.UI.WPF.ViewModels
get => _assemblyMountMode;
set
{
- if (SetProperty(ref _assemblyMountMode, value) && HasAssemblyTerminalObject)
+ if (_assemblyMountMode == value)
+ {
+ return;
+ }
+
+ _assemblyMountMode = value;
+ OnPropertyChanged(nameof(AssemblyMountMode));
+
+ if (HasAssemblyTerminalObject)
{
RefreshAssemblyTerminalObjectInfo();
RefreshAssemblyReferenceRodIfNeeded();
@@ -549,7 +770,16 @@ namespace NavisworksTransport.UI.WPF.ViewModels
public bool IsSelectingAssemblyStartPoint
{
get => _isSelectingAssemblyStartPoint;
- set => SetProperty(ref _isSelectingAssemblyStartPoint, value);
+ set
+ {
+ if (_isSelectingAssemblyStartPoint == value)
+ {
+ return;
+ }
+
+ _isSelectingAssemblyStartPoint = value;
+ OnPropertyChanged(nameof(IsSelectingAssemblyStartPoint));
+ }
}
///
/// 更新路径可视化显示:清理其他路径,只显示当前选中的路径
@@ -1072,7 +1302,6 @@ namespace NavisworksTransport.UI.WPF.ViewModels
public ICommand SaveAsPathCommand { get; private set; }
public ICommand CaptureAssemblyTerminalObjectForCreateCommand { get; private set; }
public ICommand ReCaptureAssemblyTerminalObjectForEditCommand { get; private set; }
- public ICommand GenerateAssemblyReferenceRodCommand { get; private set; }
public ICommand SelectAssemblyStartPointForCreateCommand { get; private set; }
public ICommand RepositionRailStartPointCommand { get; private set; }
public ICommand SelectAssemblyInstallationPointForCreateCommand { get; private set; }
@@ -1119,10 +1348,6 @@ namespace NavisworksTransport.UI.WPF.ViewModels
public bool CanExecuteExportPath => _pathPlanningManager?.Routes?.Count > 0 || SelectedPathRoute != null;
public bool CanExecuteSaveAsPath => SelectedPathRoute != null && SelectedPathRoute.Points.Count > 0;
- public bool CanGenerateAssemblyReferenceRod => HasAssemblyTerminalObject &&
- _hasAssemblyInstallationReference &&
- AssemblyReferenceRodLengthInMeters > 0 &&
- AssemblyReferenceRodDiameterInMeters > 0;
public bool CanSelectAssemblyStartPointForCreate => HasAssemblyTerminalObject &&
_pathPlanningManager != null &&
AssemblyReferencePathManager.Instance.HasReferenceLine &&
@@ -1182,6 +1407,28 @@ namespace NavisworksTransport.UI.WPF.ViewModels
private bool IsRailCreateWorkflowActive => _railAssemblyWorkflowMode == RailAssemblyWorkflowMode.CreateNewRailPath;
private bool IsRailEditingWorkflowActive => _railAssemblyWorkflowMode == RailAssemblyWorkflowMode.EditSelectedRail;
+ private static bool IsRailAssemblyEditMode(RailAssemblyWorkflowMode workflowMode)
+ {
+ return workflowMode == RailAssemblyWorkflowMode.EditSelectedRail;
+ }
+
+ private static string GetRailAssemblyWorkflowLabel(RailAssemblyWorkflowMode workflowMode)
+ {
+ return IsRailAssemblyEditMode(workflowMode)
+ ? "编辑现有Rail路径"
+ : "新建Rail路径";
+ }
+
+ private bool ShouldShowAssemblyReferenceLineVisuals()
+ {
+ if (_assemblyTerminalObject == null || !ModelItemAnalysisHelper.IsModelItemValid(_assemblyTerminalObject))
+ {
+ return false;
+ }
+
+ return IsRailEditingWorkflowActive || _hasAssemblyInstallationReference;
+ }
+
public bool CanExecuteModifyPoint => SelectedPathPoint != null &&
_pathPlanningManager?.PathEditState != PathEditState.EditingPoint;
public bool CanExecuteCancelModifyPoint => _pathPlanningManager?.PathEditState == PathEditState.EditingPoint;
@@ -1718,7 +1965,6 @@ namespace NavisworksTransport.UI.WPF.ViewModels
SaveAsPathCommand = new RelayCommand(async () => await ExecuteSaveAsPathAsync(), () => CanExecuteSaveAsPath);
CaptureAssemblyTerminalObjectForCreateCommand = new RelayCommand(async () => await ExecuteCaptureAssemblyTerminalObjectAsync(RailAssemblyWorkflowMode.CreateNewRailPath));
ReCaptureAssemblyTerminalObjectForEditCommand = new RelayCommand(async () => await ExecuteCaptureAssemblyTerminalObjectAsync(RailAssemblyWorkflowMode.EditSelectedRail));
- GenerateAssemblyReferenceRodCommand = new RelayCommand(async () => await ExecuteGenerateAssemblyReferenceRodAsync(), () => CanGenerateAssemblyReferenceRod);
SelectAssemblyStartPointForCreateCommand = new RelayCommand(async () => await ExecuteSelectAssemblyStartPointAsync(), () => CanSelectAssemblyStartPointForCreate);
RepositionRailStartPointCommand = new RelayCommand(async () => await ExecuteRepositionRailStartPointAsync(), () => CanRepositionRailStartPoint);
SelectAssemblyInstallationPointForCreateCommand = new RelayCommand(async () => await ExecuteSelectAssemblyInstallationPointAsync(RailAssemblyWorkflowMode.CreateNewRailPath), () => CanSelectAssemblyInstallationPointForCreate);
@@ -1768,7 +2014,6 @@ namespace NavisworksTransport.UI.WPF.ViewModels
ClearAssemblyAnchorMarker();
ClearAssemblyEndFaceAnalysisVisuals();
ClearAssemblyInstallationReferenceVisuals();
- OnPropertyChanged(nameof(CanGenerateAssemblyReferenceRod));
NotifyRailAssemblyCommandStateChanged();
// 新建路径时隐藏其他路径的辅助线
@@ -1803,41 +2048,6 @@ namespace NavisworksTransport.UI.WPF.ViewModels
}
}
- private async Task ExecuteGenerateAssemblyReferenceRodAsync()
- {
- await SafeExecuteAsync(() =>
- {
- if (_assemblyTerminalObject == null || !ModelItemAnalysisHelper.IsModelItemValid(_assemblyTerminalObject))
- {
- throw new InvalidOperationException("终点箱体未设置或已失效,请重新捕获终点箱体");
- }
-
- AssemblyReferenceLine referenceLine = BuildAssemblyReferenceLine();
-
- AssemblyReferencePathManager.Instance.CreateOrUpdateReferenceRod(
- referenceLine.StartPoint,
- referenceLine.EndPoint,
- AssemblyReferenceRodDiameterInMeters);
-
- _assemblyStartPoint = Point3D.Origin;
- AssemblyStartPointText = "未选择";
- RefreshAssemblyTerminalObjectInfo(referenceLine.StartPoint, referenceLine.EndPoint);
- RenderAssemblyAnchorMarker();
- RenderAssemblyCenterGuideLine();
- RenderAssemblyReferenceLine(referenceLine);
- FocusOnAssemblyReferenceArea(referenceLine.StartPoint, referenceLine.EndPoint);
-
- UpdateMainStatus("已生成终端安装辅助线,请在三维视图中确认终点锚点、方向、外端位置和辅助线贴合情况");
- NotifyRailAssemblyCommandStateChanged();
- LogManager.Info(
- $"[直线装配] 已生成参考杆: {AssemblyTerminalObjectName}, " +
- $"终点锚点=({referenceLine.EndPoint.X:F2}, {referenceLine.EndPoint.Y:F2}, {referenceLine.EndPoint.Z:F2}), " +
- $"参考线外端=({referenceLine.StartPoint.X:F2}, {referenceLine.StartPoint.Y:F2}, {referenceLine.StartPoint.Z:F2}), " +
- $"方向=({referenceLine.Direction.X:F3}, {referenceLine.Direction.Y:F3}, {referenceLine.Direction.Z:F3}), " +
- $"资源={AssemblyReferencePathManager.Instance.ReferenceResourceName}");
- }, "生成终端安装辅助线");
- }
-
private async Task ExecuteSelectAssemblyStartPointAsync()
{
await SafeExecuteAsync(() =>
@@ -1854,27 +2064,13 @@ namespace NavisworksTransport.UI.WPF.ViewModels
if (!AssemblyReferencePathManager.Instance.HasReferenceLine)
{
- throw new InvalidOperationException("请先生成终端安装辅助线");
+ throw new InvalidOperationException("请先选择安装点,系统生成终端安装辅助线后才能取起点");
}
- CleanupAssemblyReferenceSelection();
-
- _pathPlanningManager.DisableMouseHandling();
- IsSelectingAssemblyStartPoint = true;
- NotifyRailAssemblyCommandStateChanged();
-
- PathClickToolPlugin.MouseClicked -= OnAssemblyReferenceMouseClicked;
- PathClickToolPlugin.MouseClicked += OnAssemblyReferenceMouseClicked;
-
- if (!ForceReinitializeToolPlugin(subscribeToEvents: false))
- {
- CleanupAssemblyReferenceSelection();
- throw new InvalidOperationException("ToolPlugin 初始化失败,请重试");
- }
-
- _pathPlanningManager.StartClickTool(PathPointType.StartPoint);
- UpdateMainStatus("请在辅助线附近点击一个起点,系统会自动吸附到辅助线上并生成直线路径");
- LogManager.Info("[直线装配] 已进入起点拾取模式");
+ BeginAssemblyStartPointSelection(
+ statusMessage: "请在辅助线附近点击一个起点,系统会自动吸附到辅助线上并生成直线路径",
+ logMessage: "[直线装配] 已进入起点拾取模式",
+ toolPluginFailureMessage: "ToolPlugin 初始化失败,请重试");
}, "选择终端安装起点");
}
@@ -1905,25 +2101,10 @@ namespace NavisworksTransport.UI.WPF.ViewModels
throw new InvalidOperationException("当前 Rail 路径无法生成辅助线,请先检查路径起点和终点。");
}
- CleanupAssemblyReferenceSelection();
-
- _pathPlanningManager.DisableMouseHandling();
- _isEditingSelectedRailStartPoint = true;
- IsSelectingAssemblyStartPoint = true;
- NotifyRailAssemblyCommandStateChanged();
-
- PathClickToolPlugin.MouseClicked -= OnAssemblyReferenceMouseClicked;
- PathClickToolPlugin.MouseClicked += OnAssemblyReferenceMouseClicked;
-
- if (!ForceReinitializeToolPlugin(subscribeToEvents: false))
- {
- CleanupAssemblyReferenceSelection();
- throw new InvalidOperationException("ToolPlugin 初始化失败,请重试。");
- }
-
- _pathPlanningManager.StartClickTool(PathPointType.StartPoint);
- UpdateMainStatus("请在辅助线附近点击新的起点位置,系统会自动吸附到辅助线上并更新当前 Rail 路径。");
- LogManager.Info("[Rail构型] 已进入起点重选模式");
+ BeginAssemblyStartPointSelection(
+ statusMessage: "请在辅助线附近点击新的起点位置,系统会自动吸附到辅助线上并更新当前 Rail 路径。",
+ logMessage: "[Rail构型] 已进入起点重选模式",
+ toolPluginFailureMessage: "ToolPlugin 初始化失败,请重试。");
}, "重选 Rail 路径起点");
}
@@ -1957,45 +2138,12 @@ namespace NavisworksTransport.UI.WPF.ViewModels
throw new InvalidOperationException("终点箱体未设置或已失效,请先捕获终点箱体。");
}
- bool isEditingExistingRailRoute = workflowMode == RailAssemblyWorkflowMode.EditSelectedRail;
- var selectedRailRoute = isEditingExistingRailRoute ? GetSelectedCoreRoute() : null;
- if (isEditingExistingRailRoute)
- {
- if (selectedRailRoute == null || selectedRailRoute.PathType != PathType.Rail)
- {
- throw new InvalidOperationException("请先选中一条需要编辑安装点的 Rail 路径。");
- }
+ PrepareRailAssemblyWorkflowForMode(
+ workflowMode,
+ editActionDescription: "请先选中一条需要编辑安装点的 Rail 路径。",
+ syncMountModeFromRoute: true);
- AssemblyMountMode = selectedRailRoute.RailMountMode;
- }
-
- SetRailAssemblyWorkflowMode(workflowMode);
-
- CleanupAssemblyReferenceSelection();
- CleanupAssemblyEndFaceSelection(clearVisuals: false);
- CleanupAssemblyInstallationSelection();
- ClearAssemblyEndFaceAnalysisVisuals();
- ResetAssemblyEndFaceAnalysisState();
- ClearAssemblyInstallationReferenceVisuals();
- ResetAssemblyInstallationReferenceState();
- _assemblyEndFaceSeedPoints.Clear();
-
- _pathPlanningManager.DisableMouseHandling();
- _isSelectingAssemblyEndFacePoints = true;
- NotifyRailAssemblyCommandStateChanged();
-
- PathClickToolPlugin.MouseClicked -= OnAssemblyEndFaceMouseClicked;
- PathClickToolPlugin.MouseClicked += OnAssemblyEndFaceMouseClicked;
-
- if (!ForceReinitializeToolPlugin(subscribeToEvents: false))
- {
- CleanupAssemblyEndFaceSelection(clearVisuals: false);
- throw new InvalidOperationException("ToolPlugin 初始化失败,请重试。");
- }
-
- UpdateMainStatus("请在同一个端面平面上连续点击 3 个点,系统将分析端面中心。");
- LogManager.Info(
- $"[直线装配] 已进入端面三点分析模式,模式={(isEditingExistingRailRoute ? "编辑现有Rail路径" : "新建Rail路径")}");
+ BeginAssemblyEndFaceSelection(workflowMode);
}, "分析终端端面");
}
@@ -2013,39 +2161,12 @@ namespace NavisworksTransport.UI.WPF.ViewModels
throw new InvalidOperationException("终点箱体未设置或已失效,请先捕获终点箱体。");
}
- bool isEditingExistingRailRoute = workflowMode == RailAssemblyWorkflowMode.EditSelectedRail;
- var selectedRailRoute = isEditingExistingRailRoute ? GetSelectedCoreRoute() : null;
- if (isEditingExistingRailRoute && (selectedRailRoute == null || selectedRailRoute.PathType != PathType.Rail))
- {
- throw new InvalidOperationException("请先选中一条需要编辑安装点的 Rail 路径。");
- }
+ PrepareRailAssemblyWorkflowForMode(
+ workflowMode,
+ editActionDescription: "请先选中一条需要编辑安装点的 Rail 路径。",
+ syncMountModeFromRoute: false);
- SetRailAssemblyWorkflowMode(workflowMode);
-
- CleanupAssemblyReferenceSelection();
- CleanupAssemblyEndFaceSelection(clearVisuals: false);
- CleanupAssemblyInstallationSelection();
- ClearAssemblyInstallationReferenceVisuals();
-
- _pathPlanningManager.DisableMouseHandling();
- _isSelectingAssemblyInstallationPoint = true;
-
- _isEditingSelectedRailInstallationPoint = isEditingExistingRailRoute;
-
- NotifyRailAssemblyCommandStateChanged();
-
- PathClickToolPlugin.MouseClicked -= OnAssemblyInstallationMouseClicked;
- PathClickToolPlugin.MouseClicked += OnAssemblyInstallationMouseClicked;
-
- if (!ForceReinitializeToolPlugin(subscribeToEvents: false))
- {
- CleanupAssemblyInstallationSelection();
- throw new InvalidOperationException("ToolPlugin 初始化失败,请重试。");
- }
-
- _assemblyInstallationSeedPoints.Clear();
- UpdateMainStatus("请在终点箱体表面连续点击两个安装面点,系统将按两点和光轴计算安装参考面与安装点。");
- LogManager.Info("[直线装配] 已进入安装点拾取模式");
+ BeginAssemblyInstallationSelection();
}, "选择安装点");
}
@@ -2067,16 +2188,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
await SafeExecuteAsync(() =>
{
- if (_isEditingSelectedRailStartPoint)
- {
- UpdateSelectedRailRouteStartPoint(projectedStartPoint);
- UpdateMainStatus("已根据辅助线更新当前 Rail 路径起点");
- }
- else
- {
- CreateAssemblyLinearRoute(projectedStartPoint);
- UpdateMainStatus("已根据辅助线起点生成终端安装路径");
- }
+ ApplyAssemblyStartPointForCurrentWorkflow(projectedStartPoint);
CleanupAssemblyReferenceSelection();
}, "生成终端安装路径");
@@ -2163,22 +2275,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
_assemblyInstallationSeedPoints[0],
_assemblyInstallationSeedPoints[1]);
- if (_isEditingSelectedRailInstallationPoint)
- {
- // 编辑现有路径:更新安装点并同步起点
- var selectedRailRoute = GetSelectedCoreRoute();
- if (selectedRailRoute != null && selectedRailRoute.PathType == PathType.Rail)
- {
- PersistAssemblyInstallationReferenceToRailRoute(
- selectedRailRoute,
- "已重选安装点并同步更新 Rail 路径安装参数");
- }
- }
- else
- {
- // 创建新路径:生成参考线,等待用户选择起点
- LogManager.Info("[直线装配] 安装点已确定,请继续选择起点以生成新路径");
- }
+ ApplyAssemblyInstallationReferenceForCurrentWorkflow();
CleanupAssemblyInstallationSelection(clearVisuals: false);
}, "处理安装点拾取");
}
@@ -2190,6 +2287,138 @@ namespace NavisworksTransport.UI.WPF.ViewModels
}
}
+ private void PrepareRailAssemblyWorkflowForMode(
+ RailAssemblyWorkflowMode workflowMode,
+ string editActionDescription,
+ bool syncMountModeFromRoute)
+ {
+ PathRoute selectedRailRoute = null;
+ if (IsRailAssemblyEditMode(workflowMode))
+ {
+ selectedRailRoute = RequireSelectedRailRouteForAssemblyEdit(editActionDescription);
+ if (syncMountModeFromRoute)
+ {
+ AssemblyMountMode = selectedRailRoute.RailMountMode;
+ }
+ }
+
+ SetRailAssemblyWorkflowMode(workflowMode);
+ }
+
+ private PathRoute RequireSelectedRailRouteForAssemblyEdit(string errorMessage)
+ {
+ var selectedRailRoute = GetSelectedCoreRoute();
+ if (selectedRailRoute == null || selectedRailRoute.PathType != PathType.Rail)
+ {
+ throw new InvalidOperationException(errorMessage);
+ }
+
+ return selectedRailRoute;
+ }
+
+ private void BeginAssemblyStartPointSelection(string statusMessage, string logMessage, string toolPluginFailureMessage)
+ {
+ CleanupAssemblyReferenceSelection();
+
+ _pathPlanningManager.DisableMouseHandling();
+ IsSelectingAssemblyStartPoint = true;
+ NotifyRailAssemblyCommandStateChanged();
+
+ PathClickToolPlugin.MouseClicked -= OnAssemblyReferenceMouseClicked;
+ PathClickToolPlugin.MouseClicked += OnAssemblyReferenceMouseClicked;
+
+ if (!ForceReinitializeToolPlugin(subscribeToEvents: false))
+ {
+ CleanupAssemblyReferenceSelection();
+ throw new InvalidOperationException(toolPluginFailureMessage);
+ }
+
+ _pathPlanningManager.StartClickTool(PathPointType.StartPoint);
+ UpdateMainStatus(statusMessage);
+ LogManager.Info(logMessage);
+ }
+
+ private void BeginAssemblyEndFaceSelection(RailAssemblyWorkflowMode workflowMode)
+ {
+ CleanupAssemblyReferenceSelection();
+ CleanupAssemblyEndFaceSelection(clearVisuals: false);
+ CleanupAssemblyInstallationSelection();
+ ClearAssemblyEndFaceAnalysisVisuals();
+ ResetAssemblyEndFaceAnalysisState();
+ ClearAssemblyInstallationReferenceVisuals();
+ ResetAssemblyInstallationReferenceState();
+ _assemblyEndFaceSeedPoints.Clear();
+
+ _pathPlanningManager.DisableMouseHandling();
+ _isSelectingAssemblyEndFacePoints = true;
+ NotifyRailAssemblyCommandStateChanged();
+
+ PathClickToolPlugin.MouseClicked -= OnAssemblyEndFaceMouseClicked;
+ PathClickToolPlugin.MouseClicked += OnAssemblyEndFaceMouseClicked;
+
+ if (!ForceReinitializeToolPlugin(subscribeToEvents: false))
+ {
+ CleanupAssemblyEndFaceSelection(clearVisuals: false);
+ throw new InvalidOperationException("ToolPlugin 初始化失败,请重试。");
+ }
+
+ UpdateMainStatus("请在同一个端面平面上连续点击 3 个点,系统将分析端面中心。");
+ LogManager.Info($"[直线装配] 已进入端面三点分析模式,模式={GetRailAssemblyWorkflowLabel(workflowMode)}");
+ }
+
+ private void BeginAssemblyInstallationSelection()
+ {
+ CleanupAssemblyReferenceSelection();
+ CleanupAssemblyEndFaceSelection(clearVisuals: false);
+ CleanupAssemblyInstallationSelection();
+ ClearAssemblyInstallationReferenceVisuals();
+
+ _pathPlanningManager.DisableMouseHandling();
+ _isSelectingAssemblyInstallationPoint = true;
+ NotifyRailAssemblyCommandStateChanged();
+
+ PathClickToolPlugin.MouseClicked -= OnAssemblyInstallationMouseClicked;
+ PathClickToolPlugin.MouseClicked += OnAssemblyInstallationMouseClicked;
+
+ if (!ForceReinitializeToolPlugin(subscribeToEvents: false))
+ {
+ CleanupAssemblyInstallationSelection();
+ throw new InvalidOperationException("ToolPlugin 初始化失败,请重试。");
+ }
+
+ _assemblyInstallationSeedPoints.Clear();
+ UpdateMainStatus("请在终点箱体表面连续点击两个安装面点,系统将按两点和光轴计算安装参考面与安装点。");
+ LogManager.Info($"[直线装配] 已进入安装点拾取模式,模式={GetRailAssemblyWorkflowLabel(_railAssemblyWorkflowMode)}");
+ }
+
+ private void ApplyAssemblyStartPointForCurrentWorkflow(Point3D projectedStartPoint)
+ {
+ if (IsRailEditingWorkflowActive)
+ {
+ UpdateSelectedRailRouteStartPoint(projectedStartPoint);
+ ClearAssemblyReferenceLineVisuals();
+ UpdateMainStatus("已根据辅助线更新当前 Rail 路径起点");
+ return;
+ }
+
+ CreateAssemblyLinearRoute(projectedStartPoint);
+ UpdateMainStatus("已根据辅助线起点生成终端安装路径");
+ }
+
+ private void ApplyAssemblyInstallationReferenceForCurrentWorkflow()
+ {
+ if (IsRailEditingWorkflowActive)
+ {
+ var selectedRailRoute = RequireSelectedRailRouteForAssemblyEdit("当前没有可更新安装点的 Rail 路径。");
+ PersistAssemblyInstallationReferenceToRailRoute(
+ selectedRailRoute,
+ "已重选安装点并同步更新 Rail 路径安装参数");
+ return;
+ }
+
+ LogManager.Info("[直线装配] 安装点已确定,请继续选择起点以生成新路径");
+ }
+
private void CreateAssemblyLinearRoute(Point3D startPoint)
{
Point3D endPoint = AssemblyReferencePathManager.Instance.ReferenceLineEnd;
@@ -2324,16 +2553,13 @@ namespace NavisworksTransport.UI.WPF.ViewModels
ClearAssemblyInstallationReferenceVisuals();
ResetAssemblyEndFaceAnalysisState();
ResetAssemblyInstallationReferenceState();
-
- _assemblyTerminalObject = null;
- _assemblyStartPoint = Point3D.Origin;
+ _railAssemblyContext.ResetSession();
HasAssemblyTerminalObject = false;
- AssemblyTerminalObjectName = "未选择";
- AssemblyTerminalObjectInfo = "请选择终点处已安装箱体";
- AssemblyStartPointText = "未选择";
+ AssemblyTerminalObjectName = _assemblyTerminalObjectName;
+ AssemblyTerminalObjectInfo = _assemblyTerminalObjectInfo;
+ AssemblyStartPointText = _assemblyStartPointText;
SetRailAssemblyWorkflowMode(RailAssemblyWorkflowMode.None);
- OnPropertyChanged(nameof(CanGenerateAssemblyReferenceRod));
NotifyRailAssemblyCommandStateChanged();
if (announce && !string.IsNullOrWhiteSpace(statusMessage))
@@ -2595,7 +2821,13 @@ namespace NavisworksTransport.UI.WPF.ViewModels
private void RefreshAssemblyReferenceRodIfNeeded()
{
- if (!AssemblyReferencePathManager.Instance.HasReferenceLine || _assemblyTerminalObject == null)
+ if (!ShouldShowAssemblyReferenceLineVisuals())
+ {
+ ClearAssemblyReferenceLineVisuals();
+ return;
+ }
+
+ if (_assemblyTerminalObject == null)
{
return;
}
@@ -2672,61 +2904,6 @@ namespace NavisworksTransport.UI.WPF.ViewModels
return standardRadiusInMeters * 0.8;
}
- private void FocusOnAssemblyReferenceArea(Point3D startPoint, Point3D endPoint)
- {
- try
- {
- double minX = Math.Min(startPoint.X, endPoint.X);
- double minY = Math.Min(startPoint.Y, endPoint.Y);
- double minZ = Math.Min(startPoint.Z, endPoint.Z);
- double maxX = Math.Max(startPoint.X, endPoint.X);
- double maxY = Math.Max(startPoint.Y, endPoint.Y);
- double maxZ = Math.Max(startPoint.Z, endPoint.Z);
-
- if (_assemblyTerminalObject != null && ModelItemAnalysisHelper.IsModelItemValid(_assemblyTerminalObject))
- {
- var terminalBounds = _assemblyTerminalObject.BoundingBox();
- minX = Math.Min(minX, terminalBounds.Min.X);
- minY = Math.Min(minY, terminalBounds.Min.Y);
- minZ = Math.Min(minZ, terminalBounds.Min.Z);
- maxX = Math.Max(maxX, terminalBounds.Max.X);
- maxY = Math.Max(maxY, terminalBounds.Max.Y);
- maxZ = Math.Max(maxZ, terminalBounds.Max.Z);
- }
-
- Point3D center = new Point3D(
- (minX + maxX) / 2.0,
- (minY + maxY) / 2.0,
- (minZ + maxZ) / 2.0);
-
- double targetSize = Math.Max(
- maxX - minX,
- Math.Max(maxY - minY, maxZ - minZ));
-
- targetSize = Math.Max(targetSize, UnitsConverter.ConvertFromMeters(1.0));
-
- ViewpointHelper.FocusOnPosition(center, targetSize, 45.0, 0.35);
-
- var selection = new List();
- if (_assemblyTerminalObject != null && ModelItemAnalysisHelper.IsModelItemValid(_assemblyTerminalObject))
- {
- selection.Add(_assemblyTerminalObject);
- }
-
- var referenceRod = AssemblyReferencePathManager.Instance.CurrentReferenceRod;
- if (referenceRod != null)
- {
- selection.Add(referenceRod);
- }
-
- NavisworksSelectionHelper.SetModelSelection(selection);
- }
- catch (Exception ex)
- {
- LogManager.Warning($"[直线装配] 聚焦参考杆区域失败: {ex.Message}");
- }
- }
-
private void RenderAssemblyAnchorMarker()
{
if (_assemblyTerminalObject == null || !ModelItemAnalysisHelper.IsModelItemValid(_assemblyTerminalObject))
@@ -2930,13 +3107,12 @@ namespace NavisworksTransport.UI.WPF.ViewModels
RefreshAssemblyTerminalObjectInfo();
RefreshAssemblyReferenceRodIfNeeded();
UpdateMainStatus(
- $"安装参考已计算:安装点=({_assemblyInstallationAnchorPoint.X:F2}, {_assemblyInstallationAnchorPoint.Y:F2}, {_assemblyInstallationAnchorPoint.Z:F2}),偏距={_assemblyInstallationOffsetDistanceInMeters:F3}m");
+ $"安装参考已计算:安装点=({_assemblyInstallationAnchorPoint.X:F2}, {_assemblyInstallationAnchorPoint.Y:F2}, {_assemblyInstallationAnchorPoint.Z:F2}),偏距={_assemblyInstallationOffsetDistanceInMeters:F3}m,可继续取起点");
LogManager.Info(
$"[直线装配] 安装参考已计算: 安装面点1=({firstPickPoint.X:F3}, {firstPickPoint.Y:F3}, {firstPickPoint.Z:F3}), " +
$"安装面点2=({secondPickPoint.X:F3}, {secondPickPoint.Y:F3}, {secondPickPoint.Z:F3}), " +
$"安装点=({_assemblyInstallationAnchorPoint.X:F3}, {_assemblyInstallationAnchorPoint.Y:F3}, {_assemblyInstallationAnchorPoint.Z:F3}), " +
$"偏距={_assemblyInstallationOffsetDistanceInMeters:F3}m, 平面法向=({result.PlaneNormal.X:F4}, {result.PlaneNormal.Y:F4}, {result.PlaneNormal.Z:F4})");
- OnPropertyChanged(nameof(CanGenerateAssemblyReferenceRod));
NotifyRailAssemblyCommandStateChanged();
}
@@ -3139,9 +3315,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
private void ResetAssemblyEndFaceAnalysisState()
{
- _hasAssemblyEndFaceAnalysis = false;
- _assemblyEndFaceCenterPoint = null;
- _assemblyEndFaceNormal = default(Vector3);
+ _railAssemblyContext.ResetEndFaceAnalysis();
}
private void ClearAssemblyInstallationReferenceVisuals()
@@ -3161,18 +3335,8 @@ namespace NavisworksTransport.UI.WPF.ViewModels
private void ResetAssemblyInstallationReferenceState()
{
- _hasAssemblyInstallationReference = false;
- _assemblyInstallationPickPoint = null;
- _assemblyInstallationBaseAnchorPoint = null;
- _assemblyInstallationAnchorPoint = null;
- _assemblyInstallationPlaneNormal = default(Vector3);
- _assemblyInstallationPlaneSpanDirection = default(Vector3);
- _assemblyInstallationOffsetDistanceInMeters = 0.0;
- _assemblyInstallationReferenceRouteId = null;
- _assemblyInstallationSeedPoints.Clear();
- _assemblyAnchorVerticalOffsetInMeters = DefaultAssemblyAnchorVerticalOffsetInMeters;
+ _railAssemblyContext.ResetInstallationReference();
OnPropertyChanged(nameof(AssemblyAnchorVerticalOffsetInMeters));
- OnPropertyChanged(nameof(CanGenerateAssemblyReferenceRod));
NotifyRailAssemblyCommandStateChanged();
}
@@ -3182,7 +3346,6 @@ namespace NavisworksTransport.UI.WPF.ViewModels
{
PathClickToolPlugin.MouseClicked -= OnAssemblyReferenceMouseClicked;
IsSelectingAssemblyStartPoint = false;
- _isEditingSelectedRailStartPoint = false;
_pathPlanningManager?.EnableMouseHandling();
_pathPlanningManager?.StopClickTool();
NotifyRailAssemblyCommandStateChanged();
@@ -3199,7 +3362,6 @@ namespace NavisworksTransport.UI.WPF.ViewModels
{
PathClickToolPlugin.MouseClicked -= OnAssemblyInstallationMouseClicked;
_isSelectingAssemblyInstallationPoint = false;
- _isEditingSelectedRailInstallationPoint = false;
_pathPlanningManager?.EnableMouseHandling();
_pathPlanningManager?.StopClickTool();
if (clearVisuals)
diff --git a/src/UI/WPF/ViewModels/RailAssemblyWorkflowContext.cs b/src/UI/WPF/ViewModels/RailAssemblyWorkflowContext.cs
new file mode 100644
index 0000000..f5c3262
--- /dev/null
+++ b/src/UI/WPF/ViewModels/RailAssemblyWorkflowContext.cs
@@ -0,0 +1,154 @@
+using System.Collections.Generic;
+using System.Numerics;
+using Autodesk.Navisworks.Api;
+
+namespace NavisworksTransport.UI.WPF.ViewModels
+{
+ internal enum RailAssemblyWorkflowMode
+ {
+ None,
+ CreateNewRailPath,
+ EditSelectedRail
+ }
+
+ internal sealed class RailAssemblyWorkflowContext
+ {
+ private const string DefaultTerminalObjectName = "未选择";
+ private const string DefaultTerminalObjectInfo = "请选择终点处已安装箱体";
+ private const string DefaultAssemblyStartPointText = "未选择";
+
+ private readonly double _defaultReferenceRodLengthInMeters;
+ private readonly double _defaultReferenceRodDiameterInMeters;
+ private readonly double _defaultAnchorVerticalOffsetInMeters;
+ private readonly double _defaultSphereCenterX;
+ private readonly double _defaultSphereCenterY;
+ private readonly double _defaultSphereCenterZ;
+
+ public RailAssemblyWorkflowContext(
+ double defaultReferenceRodLengthInMeters,
+ double defaultReferenceRodDiameterInMeters,
+ double defaultAnchorVerticalOffsetInMeters,
+ double defaultSphereCenterX,
+ double defaultSphereCenterY,
+ double defaultSphereCenterZ)
+ {
+ _defaultReferenceRodLengthInMeters = defaultReferenceRodLengthInMeters;
+ _defaultReferenceRodDiameterInMeters = defaultReferenceRodDiameterInMeters;
+ _defaultAnchorVerticalOffsetInMeters = defaultAnchorVerticalOffsetInMeters;
+ _defaultSphereCenterX = defaultSphereCenterX;
+ _defaultSphereCenterY = defaultSphereCenterY;
+ _defaultSphereCenterZ = defaultSphereCenterZ;
+
+ TerminalObjectName = DefaultTerminalObjectName;
+ TerminalObjectInfo = DefaultTerminalObjectInfo;
+ ReferenceRodLengthInMeters = _defaultReferenceRodLengthInMeters;
+ ReferenceRodDiameterInMeters = _defaultReferenceRodDiameterInMeters;
+ AnchorVerticalOffsetInMeters = _defaultAnchorVerticalOffsetInMeters;
+ SphereCenterX = _defaultSphereCenterX;
+ SphereCenterY = _defaultSphereCenterY;
+ SphereCenterZ = _defaultSphereCenterZ;
+ StartPointText = DefaultAssemblyStartPointText;
+ MountMode = RailMountMode.UnderRail;
+ InstallationSeedPoints = new List();
+ EndFaceSeedPoints = new List();
+ }
+
+ public RailAssemblyWorkflowMode WorkflowMode { get; set; }
+
+ public string TerminalObjectName { get; set; }
+
+ public string TerminalObjectInfo { get; set; }
+
+ public double ReferenceRodLengthInMeters { get; set; }
+
+ public double ReferenceRodDiameterInMeters { get; set; }
+
+ public double AnchorVerticalOffsetInMeters { get; set; }
+
+ public double SphereCenterX { get; set; }
+
+ public double SphereCenterY { get; set; }
+
+ public double SphereCenterZ { get; set; }
+
+ public string StartPointText { get; set; }
+
+ public RailMountMode MountMode { get; set; }
+
+ public bool HasTerminalObject { get; set; }
+
+ public bool IsSelectingStartPoint { get; set; }
+
+ public bool IsSelectingEndFacePoints { get; set; }
+
+ public bool IsSelectingInstallationPoint { get; set; }
+
+ public bool HasEndFaceAnalysis { get; set; }
+
+ public bool HasInstallationReference { get; set; }
+
+ public ModelItem TerminalObject { get; set; }
+
+ public Point3D StartPoint { get; set; }
+
+ public Point3D EndFaceCenterPoint { get; set; }
+
+ public Vector3 EndFaceNormal { get; set; }
+
+ public Point3D InstallationPickPoint { get; set; }
+
+ public Point3D InstallationBaseAnchorPoint { get; set; }
+
+ public Point3D InstallationAnchorPoint { get; set; }
+
+ public Vector3 InstallationPlaneNormal { get; set; }
+
+ public Vector3 InstallationPlaneSpanDirection { get; set; }
+
+ public double InstallationOffsetDistanceInMeters { get; set; }
+
+ public string InstallationReferenceRouteId { get; set; }
+
+ public List InstallationSeedPoints { get; }
+
+ public List EndFaceSeedPoints { get; }
+
+ public void ResetEndFaceAnalysis()
+ {
+ HasEndFaceAnalysis = false;
+ EndFaceCenterPoint = null;
+ EndFaceNormal = default(Vector3);
+ EndFaceSeedPoints.Clear();
+ }
+
+ public void ResetInstallationReference()
+ {
+ HasInstallationReference = false;
+ InstallationPickPoint = null;
+ InstallationBaseAnchorPoint = null;
+ InstallationAnchorPoint = null;
+ InstallationPlaneNormal = default(Vector3);
+ InstallationPlaneSpanDirection = default(Vector3);
+ InstallationOffsetDistanceInMeters = 0.0;
+ InstallationReferenceRouteId = null;
+ InstallationSeedPoints.Clear();
+ AnchorVerticalOffsetInMeters = _defaultAnchorVerticalOffsetInMeters;
+ }
+
+ public void ResetSession()
+ {
+ WorkflowMode = RailAssemblyWorkflowMode.None;
+ HasTerminalObject = false;
+ IsSelectingStartPoint = false;
+ IsSelectingEndFacePoints = false;
+ IsSelectingInstallationPoint = false;
+ TerminalObject = null;
+ StartPoint = Point3D.Origin;
+ TerminalObjectName = DefaultTerminalObjectName;
+ TerminalObjectInfo = DefaultTerminalObjectInfo;
+ StartPointText = DefaultAssemblyStartPointText;
+ ResetEndFaceAnalysis();
+ ResetInstallationReference();
+ }
+ }
+}
diff --git a/src/UI/WPF/Views/PathEditingView.xaml b/src/UI/WPF/Views/PathEditingView.xaml
index 4ea9e67..73e7b6d 100644
--- a/src/UI/WPF/Views/PathEditingView.xaml
+++ b/src/UI/WPF/Views/PathEditingView.xaml
@@ -488,10 +488,6 @@ NavisworksTransport 路径编辑页签视图 - 采用与动画控制和分层管
Command="{Binding SelectAssemblyInstallationPointForCreateCommand}"
Style="{StaticResource SecondaryButtonStyle}"
IsEnabled="{Binding CanSelectAssemblyInstallationPointForCreate}"/>
-
public static class ViewpointHelper
{
+ public enum ViewpointStrategy
+ {
+ PathGroundSelection,
+ PathHoistingSelection,
+ PathRailSelection,
+ ModelFocus,
+ CollisionCloseUp
+ }
+
+ internal struct PathViewpointProfile
+ {
+ public PathViewpointProfile(double distanceScale, double minDistanceMeters, double elevationDegrees)
+ {
+ DistanceScale = distanceScale;
+ MinDistanceMeters = minDistanceMeters;
+ ElevationDegrees = elevationDegrees;
+ }
+
+ public double DistanceScale { get; }
+ public double MinDistanceMeters { get; }
+ public double ElevationDegrees { get; }
+ }
+
+ internal struct FocusViewpointProfile
+ {
+ public FocusViewpointProfile(double viewAngleDegrees, double targetViewRatio)
+ {
+ ViewAngleDegrees = viewAngleDegrees;
+ TargetViewRatio = targetViewRatio;
+ }
+
+ public double ViewAngleDegrees { get; }
+ public double TargetViewRatio { get; }
+ }
+
///
/// 包围盒扩展系数(用于 ZoomBox 调整视野范围)
///
@@ -33,6 +69,34 @@ namespace NavisworksTransport.Utils
AdjustViewpointToPathCenter(path);
}
+ ///
+ /// 按路径类型调整视角。
+ /// Ground 使用俯视;
+ /// Hoisting 使用后上方视角;
+ /// Rail 使用侧面轻俯视角。
+ ///
+ public static void AdjustViewpointForPath(PathRoute path)
+ {
+ if (path == null)
+ {
+ throw new ArgumentNullException(nameof(path));
+ }
+
+ switch (path.PathType)
+ {
+ case PathType.Hoisting:
+ AdjustDirectionalViewpointToPath(path, ResolvePathViewpointProfile(ViewpointStrategy.PathHoistingSelection));
+ return;
+ case PathType.Rail:
+ AdjustDirectionalViewpointToPath(path, ResolvePathViewpointProfile(ViewpointStrategy.PathRailSelection));
+ return;
+ case PathType.Ground:
+ default:
+ AdjustViewpointToPathCenter(path);
+ return;
+ }
+ }
+
///
/// 调整视角到路径中心,确保整个路径都在视野内
///
@@ -67,6 +131,60 @@ namespace NavisworksTransport.Utils
AdjustViewpointToBoundingBox(startEndBoundingBox, pathBoundingBox, baseDimension);
}
+ private static void AdjustDirectionalViewpointToPath(PathRoute path, PathViewpointProfile profile)
+ {
+ Document doc = Application.ActiveDocument;
+ if (doc == null || doc.IsClear)
+ {
+ throw new InvalidOperationException("没有活动的文档");
+ }
+
+ if (path?.Points == null || path.Points.Count == 0)
+ {
+ throw new ArgumentException("路径为空或没有路径点", nameof(path));
+ }
+
+ Point3D startPoint = path.Points[0].Position;
+ Point3D endPoint = path.Points[path.Points.Count - 1].Position;
+ BoundingBox3D focusBoundingBox = CreateBoundingBoxFromPoints(startPoint, endPoint);
+ BoundingBox3D viewBoundingBox = CalculatePathBoundingBox(path);
+ Point3D focusCenter = GetBoundingBoxCenter(focusBoundingBox);
+
+ var adapter = CoordinateSystemManager.Instance.CreateHostAdapter();
+ Vector3 hostUp = ToVector3(adapter.HostUpVector);
+ Vector3 rawPathDirection = new Vector3(
+ (float)(endPoint.X - startPoint.X),
+ (float)(endPoint.Y - startPoint.Y),
+ (float)(endPoint.Z - startPoint.Z));
+ Vector3 fallbackForward = ToVector3(ProjectVectorOntoPlane(doc.FrontRightTopViewVector, adapter.HostUpVector));
+ Vector3 horizontalForward = ResolveHorizontalPathForward(rawPathDirection, hostUp, fallbackForward);
+ Vector3 cameraOffsetDirection = ResolveCameraOffsetDirection(path.PathType, hostUp, horizontalForward);
+
+ double maxDimensionModel = GetMaxDimension(viewBoundingBox);
+ double minDistanceModel = UnitsConverter.ConvertFromMeters(profile.MinDistanceMeters);
+ double cameraDistance = Math.Max(maxDimensionModel * profile.DistanceScale, minDistanceModel);
+ Point3D cameraPosition = new Point3D(
+ focusCenter.X + cameraOffsetDirection.X * (float)cameraDistance,
+ focusCenter.Y + cameraOffsetDirection.Y * (float)cameraDistance,
+ focusCenter.Z + cameraOffsetDirection.Z * (float)cameraDistance);
+
+ double expansionMargin = Math.Max(
+ maxDimensionModel * (VIEW_BOUNDING_BOX_EXPANSION_FACTOR - 1.0) / 2.0,
+ UnitsConverter.ConvertFromMeters(2.0));
+
+ ApplyViewpointWithZoomBox(
+ cameraPosition,
+ focusCenter,
+ new Vector3D(hostUp.X, hostUp.Y, hostUp.Z),
+ viewBoundingBox,
+ expansionMargin);
+
+ LogManager.Info(
+ $"视角已调整完成: 类型={path.PathType}, 焦点=({focusCenter.X:F2},{focusCenter.Y:F2},{focusCenter.Z:F2}), " +
+ $"相机=({cameraPosition.X:F2},{cameraPosition.Y:F2},{cameraPosition.Z:F2}), " +
+ $"距离={UnitsConverter.ConvertToMeters(cameraDistance):F2}米, elevation={profile.ElevationDegrees:F1}°");
+ }
+
///
/// 调整视角到指定的包围盒(通用方法)
/// 设置俯视位置,确保包围盒完整显示在视野中
@@ -162,6 +280,146 @@ namespace NavisworksTransport.Utils
return bounds;
}
+ internal static PathViewpointProfile ResolvePathViewpointProfile(PathType pathType)
+ {
+ switch (pathType)
+ {
+ case PathType.Hoisting:
+ return ResolvePathViewpointProfile(ViewpointStrategy.PathHoistingSelection);
+ case PathType.Rail:
+ return ResolvePathViewpointProfile(ViewpointStrategy.PathRailSelection);
+ case PathType.Ground:
+ default:
+ return ResolvePathViewpointProfile(ViewpointStrategy.PathGroundSelection);
+ }
+ }
+
+ internal static PathViewpointProfile ResolvePathViewpointProfile(ViewpointStrategy strategy)
+ {
+ switch (strategy)
+ {
+ case ViewpointStrategy.PathHoistingSelection:
+ return new PathViewpointProfile(distanceScale: 1.2, minDistanceMeters: 8.0, elevationDegrees: 22.0);
+ case ViewpointStrategy.PathRailSelection:
+ return new PathViewpointProfile(distanceScale: 0.9, minDistanceMeters: 6.0, elevationDegrees: 12.0);
+ case ViewpointStrategy.PathGroundSelection:
+ default:
+ return new PathViewpointProfile(distanceScale: 1.0, minDistanceMeters: 12.0, elevationDegrees: 90.0);
+ }
+ }
+
+ internal static FocusViewpointProfile ResolveFocusViewpointProfile(ViewpointStrategy strategy)
+ {
+ switch (strategy)
+ {
+ case ViewpointStrategy.CollisionCloseUp:
+ return new FocusViewpointProfile(viewAngleDegrees: 60.0, targetViewRatio: 0.25);
+ case ViewpointStrategy.ModelFocus:
+ default:
+ return new FocusViewpointProfile(viewAngleDegrees: 60.0, targetViewRatio: 0.25);
+ }
+ }
+
+ internal static Vector3 ResolveHorizontalPathForward(Vector3 rawPathDirection, Vector3 hostUp, Vector3 fallbackForward)
+ {
+ Vector3 projected = ProjectOntoPlane(rawPathDirection, hostUp);
+ if (projected.LengthSquared() > 1e-8f)
+ {
+ return Vector3.Normalize(projected);
+ }
+
+ Vector3 fallbackProjected = ProjectOntoPlane(fallbackForward, hostUp);
+ if (fallbackProjected.LengthSquared() > 1e-8f)
+ {
+ return Vector3.Normalize(fallbackProjected);
+ }
+
+ Vector3 canonicalFallback = Math.Abs(Vector3.Dot(Vector3.UnitX, hostUp)) > 0.9f
+ ? Vector3.UnitY
+ : Vector3.UnitX;
+ return Vector3.Normalize(ProjectOntoPlane(canonicalFallback, hostUp));
+ }
+
+ internal static Vector3 ResolveCameraOffsetDirection(PathType pathType, Vector3 hostUp, Vector3 horizontalForward)
+ {
+ PathViewpointProfile profile = ResolvePathViewpointProfile(pathType);
+ float elevationRadians = (float)(profile.ElevationDegrees * Math.PI / 180.0);
+ float horizontalWeight = (float)Math.Cos(elevationRadians);
+ float verticalWeight = (float)Math.Sin(elevationRadians);
+
+ Vector3 horizontalDirection;
+ switch (pathType)
+ {
+ case PathType.Hoisting:
+ horizontalDirection = -horizontalForward;
+ break;
+ case PathType.Rail:
+ horizontalDirection = Vector3.Cross(hostUp, horizontalForward);
+ if (horizontalDirection.LengthSquared() <= 1e-8f)
+ {
+ horizontalDirection = -horizontalForward;
+ }
+ break;
+ case PathType.Ground:
+ default:
+ horizontalDirection = hostUp;
+ break;
+ }
+
+ Vector3 offset = horizontalDirection * horizontalWeight + hostUp * verticalWeight;
+ return Vector3.Normalize(offset);
+ }
+
+ private static BoundingBox3D CreateBoundingBoxFromPoints(Point3D pointA, Point3D pointB)
+ {
+ return new BoundingBox3D(
+ new Point3D(
+ Math.Min(pointA.X, pointB.X),
+ Math.Min(pointA.Y, pointB.Y),
+ Math.Min(pointA.Z, pointB.Z)),
+ new Point3D(
+ Math.Max(pointA.X, pointB.X),
+ Math.Max(pointA.Y, pointB.Y),
+ Math.Max(pointA.Z, pointB.Z)));
+ }
+
+ private static Point3D GetBoundingBoxCenter(BoundingBox3D boundingBox)
+ {
+ return new Point3D(
+ (boundingBox.Min.X + boundingBox.Max.X) / 2.0,
+ (boundingBox.Min.Y + boundingBox.Max.Y) / 2.0,
+ (boundingBox.Min.Z + boundingBox.Max.Z) / 2.0);
+ }
+
+ private static double GetMaxDimension(BoundingBox3D boundingBox)
+ {
+ return Math.Max(
+ boundingBox.Max.X - boundingBox.Min.X,
+ Math.Max(
+ boundingBox.Max.Y - boundingBox.Min.Y,
+ boundingBox.Max.Z - boundingBox.Min.Z));
+ }
+
+ private static Vector3D ProjectVectorOntoPlane(Vector3D vector, Vector3D planeNormal)
+ {
+ double dot = vector.X * planeNormal.X + vector.Y * planeNormal.Y + vector.Z * planeNormal.Z;
+ return new Vector3D(
+ vector.X - planeNormal.X * dot,
+ vector.Y - planeNormal.Y * dot,
+ vector.Z - planeNormal.Z * dot);
+ }
+
+ private static Vector3 ProjectOntoPlane(Vector3 vector, Vector3 planeNormal)
+ {
+ float dot = Vector3.Dot(vector, planeNormal);
+ return vector - planeNormal * dot;
+ }
+
+ private static Vector3 ToVector3(Vector3D vector)
+ {
+ return new Vector3((float)vector.X, (float)vector.Y, (float)vector.Z);
+ }
+
///
/// 计算所有碰撞位置的总包围盒
///
@@ -401,6 +659,12 @@ namespace NavisworksTransport.Utils
}
}
+ public static void FocusOnModelItem(ModelItem modelItem, ViewpointStrategy strategy)
+ {
+ FocusViewpointProfile profile = ResolveFocusViewpointProfile(strategy);
+ FocusOnModelItem(modelItem, profile.ViewAngleDegrees, profile.TargetViewRatio);
+ }
+
///
/// 聚焦到多个模型元素(如碰撞中的两个对象)
///
@@ -468,6 +732,12 @@ namespace NavisworksTransport.Utils
}
}
+ public static void FocusOnCollision(ModelItem item1, ModelItem item2, ViewpointStrategy strategy)
+ {
+ FocusViewpointProfile profile = ResolveFocusViewpointProfile(strategy);
+ FocusOnCollision(item1, item2, profile.ViewAngleDegrees, profile.TargetViewRatio);
+ }
+
///
/// 聚焦到指定位置和尺寸
///
@@ -499,6 +769,12 @@ namespace NavisworksTransport.Utils
}
}
+ public static void FocusOnPosition(Point3D center, double targetSize, ViewpointStrategy strategy)
+ {
+ FocusViewpointProfile profile = ResolveFocusViewpointProfile(strategy);
+ FocusOnPosition(center, targetSize, profile.ViewAngleDegrees, profile.TargetViewRatio);
+ }
+
///
/// 计算相机位置(使用模型标准前右上视角)
/// 基于 Navisworks 的标准 FrontRightTopViewVector 确定相机方向
From e71ea81cdf505e16dd31aaf6968ce8676738564c Mon Sep 17 00:00:00 2001
From: tian <11429339@qq.com>
Date: Wed, 1 Apr 2026 20:12:57 +0800
Subject: [PATCH 52/87] Fix rail assembly normals and tighten rail camera
---
.../AssemblyEndFaceAnalyzerTests.cs | 22 +++++++++++
.../RailPathPoseHelperTests.cs | 36 ++++++++++++++++++
.../CoordinateSystem/ViewpointHelperTests.cs | 4 +-
src/UI/WPF/ViewModels/PathEditingViewModel.cs | 37 +++++++++++++------
src/Utils/Assembly/AssemblyEndFaceAnalyzer.cs | 23 ++++++++++++
src/Utils/RailPathPoseHelper.cs | 25 +++++++++++++
src/Utils/ViewpointHelper.cs | 2 +-
7 files changed, 135 insertions(+), 14 deletions(-)
create mode 100644 UnitTests/CoordinateSystem/RailPathPoseHelperTests.cs
diff --git a/UnitTests/CoordinateSystem/AssemblyEndFaceAnalyzerTests.cs b/UnitTests/CoordinateSystem/AssemblyEndFaceAnalyzerTests.cs
index 4824dfc..401b846 100644
--- a/UnitTests/CoordinateSystem/AssemblyEndFaceAnalyzerTests.cs
+++ b/UnitTests/CoordinateSystem/AssemblyEndFaceAnalyzerTests.cs
@@ -63,6 +63,28 @@ namespace NavisworksTransport.UnitTests.CoordinateSystem
Assert.IsFalse(result.IsReliable);
}
+ [TestMethod]
+ public void OrientNormalTowardTarget_ShouldFlip_WhenNormalOpposesTargetDirection()
+ {
+ Vector3 orientedNormal = AssemblyEndFaceAnalyzer.OrientNormalTowardTarget(
+ new Vector3(-1f, 0f, 0f),
+ Vector3.Zero,
+ new Vector3(10f, 0f, 0f));
+
+ AssertPoint(orientedNormal, 1.0, 0.0, 0.0);
+ }
+
+ [TestMethod]
+ public void OrientNormalTowardTarget_ShouldKeepDirection_WhenNormalAlreadyFacesTarget()
+ {
+ Vector3 orientedNormal = AssemblyEndFaceAnalyzer.OrientNormalTowardTarget(
+ new Vector3(0f, 1f, 0f),
+ new Vector3(1f, 2f, 3f),
+ new Vector3(1f, 5f, 3f));
+
+ AssertPoint(orientedNormal, 0.0, 1.0, 0.0);
+ }
+
private static IEnumerable CreateRectangleFace(double minX, double maxX, double minY, double maxY, double z)
{
yield return new AnalysisTriangle3(
diff --git a/UnitTests/CoordinateSystem/RailPathPoseHelperTests.cs b/UnitTests/CoordinateSystem/RailPathPoseHelperTests.cs
new file mode 100644
index 0000000..cc93a59
--- /dev/null
+++ b/UnitTests/CoordinateSystem/RailPathPoseHelperTests.cs
@@ -0,0 +1,36 @@
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using NavisworksTransport.Utils;
+using System.Numerics;
+
+namespace NavisworksTransport.UnitTests.CoordinateSystem
+{
+ [TestClass]
+ public class RailPathPoseHelperTests
+ {
+ [TestMethod]
+ public void NormalizePreferredNormalToHostUpHemisphere_ShouldFlip_WhenPreferredNormalOpposesHostUp()
+ {
+ Vector3 hostPreferredNormal = new Vector3(-0.1222f, -0.9676f, 0.2211f);
+ Vector3 normalized = RailPathPoseHelper.NormalizePreferredNormalToHostUpHemisphere(
+ hostPreferredNormal,
+ Vector3.UnitY);
+
+ Assert.IsTrue(Vector3.Dot(normalized, Vector3.UnitY) > 0f);
+ Assert.AreEqual(0.1222 / 1.0008249, normalized.X, 1e-4);
+ Assert.AreEqual(0.9676 / 1.0008249, normalized.Y, 1e-4);
+ Assert.AreEqual(-0.2211 / 1.0008249, normalized.Z, 1e-4);
+ }
+
+ [TestMethod]
+ public void NormalizePreferredNormalToHostUpHemisphere_ShouldKeepDirection_WhenPreferredNormalAlreadyMatchesHostUp()
+ {
+ Vector3 hostPreferredNormal = new Vector3(0.139f, 0.954f, 0.266f);
+ Vector3 normalized = RailPathPoseHelper.NormalizePreferredNormalToHostUpHemisphere(
+ hostPreferredNormal,
+ Vector3.UnitY);
+
+ Assert.IsTrue(Vector3.Dot(normalized, Vector3.UnitY) > 0f);
+ Assert.IsTrue(Vector3.Dot(Vector3.Normalize(hostPreferredNormal), normalized) > 0.9999f);
+ }
+ }
+}
diff --git a/UnitTests/CoordinateSystem/ViewpointHelperTests.cs b/UnitTests/CoordinateSystem/ViewpointHelperTests.cs
index 6858379..90dad79 100644
--- a/UnitTests/CoordinateSystem/ViewpointHelperTests.cs
+++ b/UnitTests/CoordinateSystem/ViewpointHelperTests.cs
@@ -21,8 +21,8 @@ namespace NavisworksTransport.UnitTests.CoordinateSystem
Assert.AreEqual(8.0, hoisting.MinDistanceMeters, 1e-9);
Assert.AreEqual(22.0, hoisting.ElevationDegrees, 1e-9);
- Assert.AreEqual(0.9, rail.DistanceScale, 1e-9);
- Assert.AreEqual(6.0, rail.MinDistanceMeters, 1e-9);
+ Assert.AreEqual(0.75, rail.DistanceScale, 1e-9);
+ Assert.AreEqual(4.5, rail.MinDistanceMeters, 1e-9);
Assert.AreEqual(12.0, rail.ElevationDegrees, 1e-9);
}
diff --git a/src/UI/WPF/ViewModels/PathEditingViewModel.cs b/src/UI/WPF/ViewModels/PathEditingViewModel.cs
index 5370876..6e01e40 100644
--- a/src/UI/WPF/ViewModels/PathEditingViewModel.cs
+++ b/src/UI/WPF/ViewModels/PathEditingViewModel.cs
@@ -1775,10 +1775,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
throw new InvalidOperationException("更新 Rail 路径起点失败。");
}
- route.RailPreferredNormal = new Point3D(
- _assemblyInstallationPlaneNormal.X,
- _assemblyInstallationPlaneNormal.Y,
- _assemblyInstallationPlaneNormal.Z);
+ route.RailPreferredNormal = CreatePersistedAssemblyPreferredNormal();
route.RailNormalOffset = UnitsConverter.ConvertFromMeters(AssemblyAnchorVerticalOffsetInMeters);
route.LastModified = DateTime.Now;
_assemblyInstallationReferenceRouteId = route.Id;
@@ -2435,10 +2432,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
if (_hasAssemblyInstallationReference)
{
- route.RailPreferredNormal = new Point3D(
- _assemblyInstallationPlaneNormal.X,
- _assemblyInstallationPlaneNormal.Y,
- _assemblyInstallationPlaneNormal.Z);
+ route.RailPreferredNormal = CreatePersistedAssemblyPreferredNormal();
}
route.AddPoint(new PathPoint(startPoint, "起点", PathPointType.StartPoint));
@@ -2678,6 +2672,19 @@ namespace NavisworksTransport.UI.WPF.ViewModels
}
}
+ private Point3D CreatePersistedAssemblyPreferredNormal()
+ {
+ HostCoordinateAdapter adapter = CoordinateSystemManager.Instance.CreateHostAdapter();
+ Vector3 normalizedPreferredNormal = RailPathPoseHelper.NormalizePreferredNormalToHostUpHemisphere(
+ _assemblyInstallationPlaneNormal,
+ adapter.HostUpVector3);
+
+ return new Point3D(
+ normalizedPreferredNormal.X,
+ normalizedPreferredNormal.Y,
+ normalizedPreferredNormal.Z);
+ }
+
private Vector3 GetCurrentAssemblyOpticalAxisDirection()
{
Point3D opticalAxisReferencePoint = GetAssemblyOpticalAxisReferencePoint();
@@ -3439,18 +3446,26 @@ namespace NavisworksTransport.UI.WPF.ViewModels
}
Point3D centerPoint = AssemblyEndFaceAnalyzer.ToPoint3D(result.Center);
+ Point3D sphereCenterPoint = new Point3D(
+ AssemblySphereCenterX,
+ AssemblySphereCenterY,
+ AssemblySphereCenterZ);
+ Vector3 orientedNormal = AssemblyEndFaceAnalyzer.OrientNormalTowardTarget(
+ result.Normal,
+ new Vector3((float)centerPoint.X, (float)centerPoint.Y, (float)centerPoint.Z),
+ new Vector3((float)sphereCenterPoint.X, (float)sphereCenterPoint.Y, (float)sphereCenterPoint.Z));
_hasAssemblyEndFaceAnalysis = true;
_assemblyEndFaceCenterPoint = centerPoint;
- _assemblyEndFaceNormal = result.Normal;
+ _assemblyEndFaceNormal = orientedNormal;
RenderAssemblyEndFaceSeedPoints();
RenderAssemblyEndFaceCenter(centerPoint);
- RenderAssemblyEndFaceNormal(centerPoint, result.Normal);
+ RenderAssemblyEndFaceNormal(centerPoint, orientedNormal);
RefreshAssemblyTerminalObjectInfo();
RefreshAssemblyReferenceRodIfNeeded();
UpdateMainStatus($"端面分析完成:中心=({centerPoint.X:F2}, {centerPoint.Y:F2}, {centerPoint.Z:F2}),候选三角形={result.CandidateTriangleCount}");
LogManager.Info(
$"[直线装配] 端面分析完成: 中心=({centerPoint.X:F3}, {centerPoint.Y:F3}, {centerPoint.Z:F3}), " +
- $"法向=({result.Normal.X:F4}, {result.Normal.Y:F4}, {result.Normal.Z:F4}), " +
+ $"法向=({orientedNormal.X:F4}, {orientedNormal.Y:F4}, {orientedNormal.Z:F4}), " +
$"三角形={result.CandidateTriangleCount}, 顶点={result.CandidateVertexCount}, 偏差={result.MaxPlaneDeviation:F6}");
}
diff --git a/src/Utils/Assembly/AssemblyEndFaceAnalyzer.cs b/src/Utils/Assembly/AssemblyEndFaceAnalyzer.cs
index 7eda516..d77cb63 100644
--- a/src/Utils/Assembly/AssemblyEndFaceAnalyzer.cs
+++ b/src/Utils/Assembly/AssemblyEndFaceAnalyzer.cs
@@ -133,6 +133,29 @@ namespace NavisworksTransport.Utils.GeometryAnalysis
return new Point3D(point.X, point.Y, point.Z);
}
+ public static Vector3 OrientNormalTowardTarget(Vector3 normal, Vector3 originPoint, Vector3 targetPoint)
+ {
+ if (normal.LengthSquared() < 1e-12f)
+ {
+ throw new ArgumentException("端面法向长度过小,无法定向。", nameof(normal));
+ }
+
+ Vector3 normalizedNormal = Vector3.Normalize(normal);
+ Vector3 targetDirection = targetPoint - originPoint;
+ if (targetDirection.LengthSquared() < 1e-12f)
+ {
+ return normalizedNormal;
+ }
+
+ Vector3 normalizedTargetDirection = Vector3.Normalize(targetDirection);
+ if (Vector3.Dot(normalizedNormal, normalizedTargetDirection) < 0f)
+ {
+ normalizedNormal = -normalizedNormal;
+ }
+
+ return normalizedNormal;
+ }
+
private static AnalysisTriangle3 ConvertTriangle(Triangle3D triangle)
{
return new AnalysisTriangle3(ToVector3(triangle.Point1), ToVector3(triangle.Point2), ToVector3(triangle.Point3));
diff --git a/src/Utils/RailPathPoseHelper.cs b/src/Utils/RailPathPoseHelper.cs
index 409c323..27b6fb7 100644
--- a/src/Utils/RailPathPoseHelper.cs
+++ b/src/Utils/RailPathPoseHelper.cs
@@ -15,6 +15,31 @@ namespace NavisworksTransport.Utils
private const double TangentEpsilon = 1e-9;
private static bool _rotationConstructorConventionLogged;
+ ///
+ /// 将宿主坐标系中的 Rail 首选法向统一到宿主 up 半球,避免新建装配路径把“轨上侧”写反。
+ ///
+ public static Vector3 NormalizePreferredNormalToHostUpHemisphere(Vector3 hostPreferredNormal, Vector3 hostUpVector)
+ {
+ if (hostPreferredNormal.LengthSquared() < 1e-9f)
+ {
+ throw new ArgumentException("Rail 首选法向长度过小,无法归一化。", nameof(hostPreferredNormal));
+ }
+
+ if (hostUpVector.LengthSquared() < 1e-9f)
+ {
+ throw new ArgumentException("宿主 up 向量长度过小,无法归一化。", nameof(hostUpVector));
+ }
+
+ Vector3 normalizedPreferredNormal = Vector3.Normalize(hostPreferredNormal);
+ Vector3 normalizedHostUp = Vector3.Normalize(hostUpVector);
+ if (Vector3.Dot(normalizedPreferredNormal, normalizedHostUp) < 0f)
+ {
+ normalizedPreferredNormal = -normalizedPreferredNormal;
+ }
+
+ return normalizedPreferredNormal;
+ }
+
///
/// 计算构件底面中心相对于 Rail 参考点的 Z 偏移(模型单位)。
///
diff --git a/src/Utils/ViewpointHelper.cs b/src/Utils/ViewpointHelper.cs
index 68efe15..08aea47 100644
--- a/src/Utils/ViewpointHelper.cs
+++ b/src/Utils/ViewpointHelper.cs
@@ -301,7 +301,7 @@ namespace NavisworksTransport.Utils
case ViewpointStrategy.PathHoistingSelection:
return new PathViewpointProfile(distanceScale: 1.2, minDistanceMeters: 8.0, elevationDegrees: 22.0);
case ViewpointStrategy.PathRailSelection:
- return new PathViewpointProfile(distanceScale: 0.9, minDistanceMeters: 6.0, elevationDegrees: 12.0);
+ return new PathViewpointProfile(distanceScale: 0.75, minDistanceMeters: 4.5, elevationDegrees: 12.0);
case ViewpointStrategy.PathGroundSelection:
default:
return new PathViewpointProfile(distanceScale: 1.0, minDistanceMeters: 12.0, elevationDegrees: 90.0);
From 953306fdb18730c30a64a09ee637c428d036a081 Mon Sep 17 00:00:00 2001
From: tian <11429339@qq.com>
Date: Wed, 1 Apr 2026 23:53:11 +0800
Subject: [PATCH 53/87] Stabilize hoisting pose flow and bounds inspection
---
.../RailPreservedPoseTests.cs | 45 ++++++
src/Core/Animation/PathAnimationManager.cs | 47 ++++---
src/UI/WPF/Views/ModelItemBoundsWindow.xaml | 128 ++++++++++++++++--
.../WPF/Views/ModelItemBoundsWindow.xaml.cs | 105 ++++++++++----
4 files changed, 270 insertions(+), 55 deletions(-)
diff --git a/UnitTests/CoordinateSystem/RailPreservedPoseTests.cs b/UnitTests/CoordinateSystem/RailPreservedPoseTests.cs
index b4212e1..afac4e6 100644
--- a/UnitTests/CoordinateSystem/RailPreservedPoseTests.cs
+++ b/UnitTests/CoordinateSystem/RailPreservedPoseTests.cs
@@ -1,6 +1,7 @@
using Autodesk.Navisworks.Api;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using NavisworksTransport.Core.Animation;
+using NavisworksTransport.Utils.CoordinateSystem;
namespace NavisworksTransport.UnitTests.CoordinateSystem
{
@@ -42,5 +43,49 @@ namespace NavisworksTransport.UnitTests.CoordinateSystem
Assert.AreEqual(fallbackRotation.C, resolved.C, 1e-9);
Assert.AreEqual(fallbackRotation.D, resolved.D, 1e-9);
}
+
+ [TestMethod]
+ public void ShouldPreservePathRotationForFrames_ShouldEnableHoistingWhenTranslationModeHasLockedRotation()
+ {
+ bool shouldPreserve = PathAnimationManager.ShouldPreservePathRotationForFrames(
+ PathType.Hoisting,
+ ObjectStartPlacementMode.PreserveInitialPose,
+ hasPreservedRotation: true);
+
+ Assert.IsTrue(shouldPreserve);
+ }
+
+ [TestMethod]
+ public void ShouldPreservePathRotationForFrames_ShouldEnableRailWhenTranslationModeHasLockedRotation()
+ {
+ bool shouldPreserve = PathAnimationManager.ShouldPreservePathRotationForFrames(
+ PathType.Rail,
+ ObjectStartPlacementMode.PreserveInitialPose,
+ hasPreservedRotation: true);
+
+ Assert.IsTrue(shouldPreserve);
+ }
+
+ [TestMethod]
+ public void ShouldPreservePathRotationForFrames_ShouldDisableGroundEvenInTranslationMode()
+ {
+ bool shouldPreserve = PathAnimationManager.ShouldPreservePathRotationForFrames(
+ PathType.Ground,
+ ObjectStartPlacementMode.PreserveInitialPose,
+ hasPreservedRotation: true);
+
+ Assert.IsFalse(shouldPreserve);
+ }
+
+ [TestMethod]
+ public void ShouldPreservePathRotationForFrames_ShouldDisableWhenLockedRotationMissing()
+ {
+ bool shouldPreserve = PathAnimationManager.ShouldPreservePathRotationForFrames(
+ PathType.Hoisting,
+ ObjectStartPlacementMode.PreserveInitialPose,
+ hasPreservedRotation: false);
+
+ Assert.IsFalse(shouldPreserve);
+ }
}
}
diff --git a/src/Core/Animation/PathAnimationManager.cs b/src/Core/Animation/PathAnimationManager.cs
index 0f62d66..ad1507f 100644
--- a/src/Core/Animation/PathAnimationManager.cs
+++ b/src/Core/Animation/PathAnimationManager.cs
@@ -218,8 +218,8 @@ namespace NavisworksTransport.Core.Animation
private bool _savedObjectHasCustomRotation = false;
private bool _hasSavedObjectState = false;
private ObjectStartPlacementMode _objectStartPlacementMode = ObjectStartPlacementMode.AlignToPathPose;
- private Rotation3D _railPreservedPoseRotation = Rotation3D.Identity;
- private bool _hasRailPreservedPoseRotation = false;
+ private Rotation3D _pathPreservedPoseRotation = Rotation3D.Identity;
+ private bool _hasPathPreservedPoseRotation = false;
private bool IsVirtualObjectMode => _animatedObjectMode == AnimatedObjectMode.VirtualObject;
private bool IsRealObjectMode => _animatedObjectMode == AnimatedObjectMode.RealObject;
@@ -1022,18 +1022,19 @@ namespace NavisworksTransport.Core.Animation
{
var pathPoints = _route.Points.Select(p => p.Position).ToList();
MoveObjectToPathStartUsingCurrentPlacementMode(_animatedObject, pathPoints);
- if (_route.PathType == PathType.Rail &&
- _objectStartPlacementMode == ObjectStartPlacementMode.PreserveInitialPose &&
- _hasTrackedRotation)
+ if (ShouldPreservePathRotationForFrames(
+ _route.PathType,
+ _objectStartPlacementMode,
+ _hasTrackedRotation))
{
- _railPreservedPoseRotation = _trackedRotation;
- _hasRailPreservedPoseRotation = true;
- LogManager.Info("[预计算] Rail平移模式已锁定起点姿态为整段动画旋转基线");
+ _pathPreservedPoseRotation = _trackedRotation;
+ _hasPathPreservedPoseRotation = true;
+ LogManager.Info($"[预计算] {_route.PathType.GetDisplayName()}平移模式已锁定起点姿态为整段动画旋转基线");
}
else
{
- _railPreservedPoseRotation = Rotation3D.Identity;
- _hasRailPreservedPoseRotation = false;
+ _pathPreservedPoseRotation = Rotation3D.Identity;
+ _hasPathPreservedPoseRotation = false;
}
LogManager.Info("[预计算] 物体已移动到路径起点");
}
@@ -1340,11 +1341,12 @@ namespace NavisworksTransport.Core.Animation
Collisions = new List()
};
- if (_route.PathType == PathType.Rail &&
- _objectStartPlacementMode == ObjectStartPlacementMode.PreserveInitialPose &&
- _hasRailPreservedPoseRotation)
+ if (ShouldPreservePathRotationForFrames(
+ _route.PathType,
+ _objectStartPlacementMode,
+ _hasPathPreservedPoseRotation))
{
- frame.Rotation = _railPreservedPoseRotation;
+ frame.Rotation = _pathPreservedPoseRotation;
frame.HasCustomRotation = true;
}
else if (_route.PathType == PathType.Rail &&
@@ -3972,6 +3974,19 @@ namespace NavisworksTransport.Core.Animation
: fallbackRotation;
}
+ internal static bool ShouldPreservePathRotationForFrames(
+ PathType pathType,
+ ObjectStartPlacementMode placementMode,
+ bool hasPreservedRotation)
+ {
+ if (!hasPreservedRotation || placementMode != ObjectStartPlacementMode.PreserveInitialPose)
+ {
+ return false;
+ }
+
+ return pathType == PathType.Rail || pathType == PathType.Hoisting;
+ }
+
private void SyncTrackedRotationToDisplayedPose(ModelItem sourceObject)
{
if (sourceObject == null)
@@ -5181,7 +5196,7 @@ namespace NavisworksTransport.Core.Animation
public void SetObjectRotationCorrection(LocalEulerRotationCorrection rotationCorrection)
{
_objectStartPlacementMode = ObjectStartPlacementMode.AlignToPathPose;
- _hasRailPreservedPoseRotation = false;
+ _hasPathPreservedPoseRotation = false;
_objectRotationCorrection = rotationCorrection;
ResetPlanarRealObjectBasePoseCache();
@@ -5224,7 +5239,7 @@ namespace NavisworksTransport.Core.Animation
_objectStartPlacementMode = placementMode;
if (placementMode != ObjectStartPlacementMode.PreserveInitialPose)
{
- _hasRailPreservedPoseRotation = false;
+ _hasPathPreservedPoseRotation = false;
}
LogManager.Debug($"[起点摆放] 当前模式已设置为: {_objectStartPlacementMode}");
}
diff --git a/src/UI/WPF/Views/ModelItemBoundsWindow.xaml b/src/UI/WPF/Views/ModelItemBoundsWindow.xaml
index 2a8ff5d..1389a4b 100644
--- a/src/UI/WPF/Views/ModelItemBoundsWindow.xaml
+++ b/src/UI/WPF/Views/ModelItemBoundsWindow.xaml
@@ -1,7 +1,7 @@
-
+
@@ -138,8 +138,8 @@
-
-
+
+
@@ -155,7 +155,7 @@
-
+
@@ -165,7 +165,7 @@
-
+
@@ -175,7 +175,7 @@
-
+
@@ -183,7 +183,7 @@
x:Name="CopyTopButton"
Style="{StaticResource IconButtonStyle}"
Click="OnCopyTopCenterClick"
- ToolTip="复制顶面中心点坐标"
+ ToolTip="复制宿主包围盒顶面中心点坐标"
Margin="3,0,0,0">
-
-
-
+
+
+
@@ -208,7 +208,7 @@
-
+
@@ -218,7 +218,7 @@
-
+
@@ -228,7 +228,7 @@
-
+
@@ -236,13 +236,111 @@
x:Name="CopyBottomButton"
Style="{StaticResource IconButtonStyle}"
Click="OnCopyBottomCenterClick"
- ToolTip="复制底面中心点坐标"
+ ToolTip="复制宿主包围盒底面中心点坐标"
Margin="3,0,0,0">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/UI/WPF/Views/ModelItemBoundsWindow.xaml.cs b/src/UI/WPF/Views/ModelItemBoundsWindow.xaml.cs
index f6591a6..b833bb8 100644
--- a/src/UI/WPF/Views/ModelItemBoundsWindow.xaml.cs
+++ b/src/UI/WPF/Views/ModelItemBoundsWindow.xaml.cs
@@ -4,6 +4,8 @@ using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Threading;
using Autodesk.Navisworks.Api;
+using NavisworksTransport.Utils;
+using NavisworksTransport.Utils.CoordinateSystem;
namespace NavisworksTransport.UI.WPF.Views
{
@@ -35,23 +37,41 @@ namespace NavisworksTransport.UI.WPF.Views
/// 中心点Z坐标
public double CenterZ { get; set; }
- /// 顶面中心点X坐标
- public double TopCenterX { get; set; }
+ /// 宿主包围盒顶面中心点X坐标
+ public double HostTopCenterX { get; set; }
- /// 顶面中心点Y坐标
- public double TopCenterY { get; set; }
+ /// 宿主包围盒顶面中心点Y坐标
+ public double HostTopCenterY { get; set; }
- /// 顶面中心点Z坐标
- public double TopCenterZ { get; set; }
+ /// 宿主包围盒顶面中心点Z坐标
+ public double HostTopCenterZ { get; set; }
- /// 底面中心点X坐标
- public double BottomCenterX { get; set; }
+ /// 宿主包围盒底面中心点X坐标
+ public double HostBottomCenterX { get; set; }
- /// 底面中心点Y坐标
- public double BottomCenterY { get; set; }
+ /// 宿主包围盒底面中心点Y坐标
+ public double HostBottomCenterY { get; set; }
- /// 底面中心点Z坐标
- public double BottomCenterZ { get; set; }
+ /// 宿主包围盒底面中心点Z坐标
+ public double HostBottomCenterZ { get; set; }
+
+ /// 对象姿态语义顶面中心点X坐标
+ public double OrientedTopCenterX { get; set; }
+
+ /// 对象姿态语义顶面中心点Y坐标
+ public double OrientedTopCenterY { get; set; }
+
+ /// 对象姿态语义顶面中心点Z坐标
+ public double OrientedTopCenterZ { get; set; }
+
+ /// 对象姿态语义底面中心点X坐标
+ public double OrientedBottomCenterX { get; set; }
+
+ /// 对象姿态语义底面中心点Y坐标
+ public double OrientedBottomCenterY { get; set; }
+
+ /// 对象姿态语义底面中心点Z坐标
+ public double OrientedBottomCenterZ { get; set; }
#endregion
@@ -127,15 +147,36 @@ namespace NavisworksTransport.UI.WPF.Views
CenterY = (bounds.Min.Y + bounds.Max.Y) / 2.0;
CenterZ = (bounds.Min.Z + bounds.Max.Z) / 2.0;
- // 计算顶面中心点(Z最大)
- TopCenterX = CenterX;
- TopCenterY = CenterY;
- TopCenterZ = bounds.Max.Z;
+ HostCoordinateAdapter adapter = CoordinateSystemManager.Instance.CreateHostAdapter();
- // 计算底面中心点(Z最小)
- BottomCenterX = CenterX;
- BottomCenterY = CenterY;
- BottomCenterZ = bounds.Min.Z;
+ Point3D hostBottomCenter = ModelItemTransformHelper.GetHostBottomAnchorPoint(bounds, adapter);
+ double hostHeight = ModelItemTransformHelper.GetHostHeight(bounds, adapter);
+ Point3D hostTopCenter = new Point3D(
+ hostBottomCenter.X + adapter.HostUpVector.X * hostHeight,
+ hostBottomCenter.Y + adapter.HostUpVector.Y * hostHeight,
+ hostBottomCenter.Z + adapter.HostUpVector.Z * hostHeight);
+
+ HostBottomCenterX = hostBottomCenter.X;
+ HostBottomCenterY = hostBottomCenter.Y;
+ HostBottomCenterZ = hostBottomCenter.Z;
+ HostTopCenterX = hostTopCenter.X;
+ HostTopCenterY = hostTopCenter.Y;
+ HostTopCenterZ = hostTopCenter.Z;
+
+ Point3D orientedBottomCenter = ModelItemTransformHelper.GetStableBottomAnchorPoint(bounds, item.Transform);
+ Vector3D objectUp = ModelItemTransformHelper.GetUpDirectionFromTransform(item.Transform);
+ Vector3D localSize = ModelItemTransformHelper.EstimateLocalBoxSize(bounds, item.Transform);
+ Point3D orientedTopCenter = new Point3D(
+ orientedBottomCenter.X + objectUp.X * localSize.Z,
+ orientedBottomCenter.Y + objectUp.Y * localSize.Z,
+ orientedBottomCenter.Z + objectUp.Z * localSize.Z);
+
+ OrientedBottomCenterX = orientedBottomCenter.X;
+ OrientedBottomCenterY = orientedBottomCenter.Y;
+ OrientedBottomCenterZ = orientedBottomCenter.Z;
+ OrientedTopCenterX = orientedTopCenter.X;
+ OrientedTopCenterY = orientedTopCenter.Y;
+ OrientedTopCenterZ = orientedTopCenter.Z;
}
catch (Exception ex)
{
@@ -157,8 +198,10 @@ namespace NavisworksTransport.UI.WPF.Views
{
SizeX = SizeY = SizeZ = 0;
CenterX = CenterY = CenterZ = 0;
- TopCenterX = TopCenterY = TopCenterZ = 0;
- BottomCenterX = BottomCenterY = BottomCenterZ = 0;
+ HostTopCenterX = HostTopCenterY = HostTopCenterZ = 0;
+ HostBottomCenterX = HostBottomCenterY = HostBottomCenterZ = 0;
+ OrientedTopCenterX = OrientedTopCenterY = OrientedTopCenterZ = 0;
+ OrientedBottomCenterX = OrientedBottomCenterY = OrientedBottomCenterZ = 0;
}
///
@@ -202,7 +245,7 @@ namespace NavisworksTransport.UI.WPF.Views
///
private void OnCopyTopCenterClick(object sender, RoutedEventArgs e)
{
- string coordinate = string.Format("{0:0.000}, {1:0.000}, {2:0.000}", TopCenterX, TopCenterY, TopCenterZ);
+ string coordinate = string.Format("{0:0.000}, {1:0.000}, {2:0.000}", HostTopCenterX, HostTopCenterY, HostTopCenterZ);
bool success = ClipboardHelper.TrySetText(coordinate, "元素包围盒信息");
ShowButtonFeedback(CopyTopButton, success);
}
@@ -212,11 +255,25 @@ namespace NavisworksTransport.UI.WPF.Views
///
private void OnCopyBottomCenterClick(object sender, RoutedEventArgs e)
{
- string coordinate = string.Format("{0:0.000}, {1:0.000}, {2:0.000}", BottomCenterX, BottomCenterY, BottomCenterZ);
+ string coordinate = string.Format("{0:0.000}, {1:0.000}, {2:0.000}", HostBottomCenterX, HostBottomCenterY, HostBottomCenterZ);
bool success = ClipboardHelper.TrySetText(coordinate, "元素包围盒信息");
ShowButtonFeedback(CopyBottomButton, success);
}
+ private void OnCopyOrientedTopCenterClick(object sender, RoutedEventArgs e)
+ {
+ string coordinate = string.Format("{0:0.000}, {1:0.000}, {2:0.000}", OrientedTopCenterX, OrientedTopCenterY, OrientedTopCenterZ);
+ bool success = ClipboardHelper.TrySetText(coordinate, "元素包围盒信息");
+ ShowButtonFeedback(CopyOrientedTopButton, success);
+ }
+
+ private void OnCopyOrientedBottomCenterClick(object sender, RoutedEventArgs e)
+ {
+ string coordinate = string.Format("{0:0.000}, {1:0.000}, {2:0.000}", OrientedBottomCenterX, OrientedBottomCenterY, OrientedBottomCenterZ);
+ bool success = ClipboardHelper.TrySetText(coordinate, "元素包围盒信息");
+ ShowButtonFeedback(CopyOrientedBottomButton, success);
+ }
+
///
/// 显示按钮点击反馈
///
From 7ff510daf036438df8753e45186bf54251593eeb Mon Sep 17 00:00:00 2001
From: tian <11429339@qq.com>
Date: Thu, 2 Apr 2026 00:15:58 +0800
Subject: [PATCH 54/87] Use actual hoisting pose without fragment fallback
---
.../RailPreservedPoseTests.cs | 54 ++++
src/Core/Animation/PathAnimationManager.cs | 232 +++++++++++++-----
2 files changed, 227 insertions(+), 59 deletions(-)
diff --git a/UnitTests/CoordinateSystem/RailPreservedPoseTests.cs b/UnitTests/CoordinateSystem/RailPreservedPoseTests.cs
index afac4e6..72f9614 100644
--- a/UnitTests/CoordinateSystem/RailPreservedPoseTests.cs
+++ b/UnitTests/CoordinateSystem/RailPreservedPoseTests.cs
@@ -87,5 +87,59 @@ namespace NavisworksTransport.UnitTests.CoordinateSystem
Assert.IsFalse(shouldPreserve);
}
+
+ [TestMethod]
+ public void ShouldAllowFragmentPlanarFallback_ShouldDisableHoisting()
+ {
+ Assert.IsFalse(PathAnimationManager.ShouldAllowFragmentPlanarFallback(PathType.Hoisting));
+ Assert.IsTrue(PathAnimationManager.ShouldAllowFragmentPlanarFallback(PathType.Ground));
+ Assert.IsTrue(PathAnimationManager.ShouldAllowFragmentPlanarFallback(PathType.Rail));
+ }
+
+ [TestMethod]
+ public void ResolveCurrentRotationBaseline_ShouldPreferActualGeometryForHoisting()
+ {
+ Rotation3D trackedRotation = new Rotation3D(0.0, 0.0, 0.0, 1.0);
+ Rotation3D actualGeometryRotation = new Rotation3D(0.0, 0.70710678, 0.0, 0.70710678);
+
+ Rotation3D resolved = PathAnimationManager.ResolveCurrentRotationBaseline(
+ PathType.Hoisting,
+ isRealObjectMode: true,
+ hasTrackedRotation: true,
+ trackedRotation: trackedRotation,
+ hasReferenceRotation: false,
+ referenceRotation: Rotation3D.Identity,
+ transformRotation: Rotation3D.Identity,
+ hasActualGeometryRotation: true,
+ actualGeometryRotation: actualGeometryRotation);
+
+ Assert.AreEqual(actualGeometryRotation.A, resolved.A, 1e-9);
+ Assert.AreEqual(actualGeometryRotation.B, resolved.B, 1e-9);
+ Assert.AreEqual(actualGeometryRotation.C, resolved.C, 1e-9);
+ Assert.AreEqual(actualGeometryRotation.D, resolved.D, 1e-9);
+ }
+
+ [TestMethod]
+ public void ResolveCurrentRotationBaseline_ShouldKeepTrackedRotationForGround()
+ {
+ Rotation3D trackedRotation = new Rotation3D(0.0, 0.0, 0.38268343, 0.92387953);
+ Rotation3D actualGeometryRotation = new Rotation3D(0.0, 0.70710678, 0.0, 0.70710678);
+
+ Rotation3D resolved = PathAnimationManager.ResolveCurrentRotationBaseline(
+ PathType.Ground,
+ isRealObjectMode: true,
+ hasTrackedRotation: true,
+ trackedRotation: trackedRotation,
+ hasReferenceRotation: false,
+ referenceRotation: Rotation3D.Identity,
+ transformRotation: Rotation3D.Identity,
+ hasActualGeometryRotation: true,
+ actualGeometryRotation: actualGeometryRotation);
+
+ Assert.AreEqual(trackedRotation.A, resolved.A, 1e-9);
+ Assert.AreEqual(trackedRotation.B, resolved.B, 1e-9);
+ Assert.AreEqual(trackedRotation.C, resolved.C, 1e-9);
+ Assert.AreEqual(trackedRotation.D, resolved.D, 1e-9);
+ }
}
}
diff --git a/src/Core/Animation/PathAnimationManager.cs b/src/Core/Animation/PathAnimationManager.cs
index ad1507f..6cdfe7d 100644
--- a/src/Core/Animation/PathAnimationManager.cs
+++ b/src/Core/Animation/PathAnimationManager.cs
@@ -2774,23 +2774,25 @@ namespace NavisworksTransport.Core.Animation
Rotation3D currentRotation;
Rotation3D currentRotationForTransform;
Rotation3D actualGeometryRotation = Rotation3D.Identity;
+ bool hasActualGeometryRotation = false;
Rotation3D appliedTargetRotation = newRotation;
Rotation3D targetRotationForTransform = newRotation;
Point3D appliedTargetPosition = newPosition;
- if (_hasTrackedRotation)
- {
- currentRotation = _trackedRotation;
- }
- else if (IsRealObjectMode && _hasRealObjectReferenceRotation)
- {
- currentRotation = CoordinateSystemManager.Instance
- .CreateHostAdapter()
- .FromHostQuaternionDirect(_realObjectReferenceRotation);
- }
- else
- {
- currentRotation = controlledObject.Transform.Factor().Rotation;
- }
+ Rotation3D referenceRotation = IsRealObjectMode && _hasRealObjectReferenceRotation
+ ? CoordinateSystemManager.Instance.CreateHostAdapter().FromHostQuaternionDirect(_realObjectReferenceRotation)
+ : Rotation3D.Identity;
+ Rotation3D transformRotation = controlledObject.Transform.Factor().Rotation;
+ PathType currentPathType = _route?.PathType ?? PathType.Ground;
+ currentRotation = ResolveCurrentRotationBaseline(
+ currentPathType,
+ IsRealObjectMode,
+ _hasTrackedRotation,
+ _trackedRotation,
+ _hasRealObjectReferenceRotation,
+ referenceRotation,
+ transformRotation,
+ hasActualGeometryRotation: false,
+ actualGeometryRotation: Rotation3D.Identity);
try
{
@@ -2805,6 +2807,17 @@ namespace NavisworksTransport.Core.Animation
_route?.PathType == PathType.Rail) &&
ModelItemTransformHelper.TryGetCurrentGeometryRotation(controlledObject, out actualGeometryRotation))
{
+ hasActualGeometryRotation = true;
+ currentRotation = ResolveCurrentRotationBaseline(
+ currentPathType,
+ IsRealObjectMode,
+ _hasTrackedRotation,
+ _trackedRotation,
+ _hasRealObjectReferenceRotation,
+ referenceRotation,
+ transformRotation,
+ hasActualGeometryRotation,
+ actualGeometryRotation);
var trackedLinear = new Transform3D(currentRotation).Linear;
var actualLinear = new Transform3D(actualGeometryRotation).Linear;
string poseTag = _route?.PathType == PathType.Rail
@@ -3987,6 +4000,42 @@ namespace NavisworksTransport.Core.Animation
return pathType == PathType.Rail || pathType == PathType.Hoisting;
}
+ internal static bool ShouldAllowFragmentPlanarFallback(PathType pathType)
+ {
+ return pathType != PathType.Hoisting;
+ }
+
+ internal static Rotation3D ResolveCurrentRotationBaseline(
+ PathType pathType,
+ bool isRealObjectMode,
+ bool hasTrackedRotation,
+ Rotation3D trackedRotation,
+ bool hasReferenceRotation,
+ Rotation3D referenceRotation,
+ Rotation3D transformRotation,
+ bool hasActualGeometryRotation,
+ Rotation3D actualGeometryRotation)
+ {
+ if (isRealObjectMode &&
+ pathType == PathType.Hoisting &&
+ hasActualGeometryRotation)
+ {
+ return actualGeometryRotation;
+ }
+
+ if (hasTrackedRotation)
+ {
+ return trackedRotation;
+ }
+
+ if (isRealObjectMode && hasReferenceRotation)
+ {
+ return referenceRotation;
+ }
+
+ return transformRotation;
+ }
+
private void SyncTrackedRotationToDisplayedPose(ModelItem sourceObject)
{
if (sourceObject == null)
@@ -4159,6 +4208,71 @@ namespace NavisworksTransport.Core.Animation
return true;
}
+ private bool TryResolveHoistingActualPose(
+ Vector3 hostForward,
+ out Rotation3D rotation,
+ out Quaternion baselineQuaternion,
+ out LocalAxisDirection selectedAxisDirection,
+ out Vector3 selectedAxisLocal,
+ out Vector3 projectedForward)
+ {
+ rotation = Rotation3D.Identity;
+ baselineQuaternion = Quaternion.Identity;
+ selectedAxisDirection = LocalAxisDirection.PositiveX;
+ selectedAxisLocal = Vector3.UnitX;
+ projectedForward = Vector3.Zero;
+
+ if (_animatedObject == null ||
+ !ModelItemTransformHelper.TryGetCurrentGeometryRotation(_animatedObject, out var actualRotation3D))
+ {
+ return false;
+ }
+
+ var actualRotation = new Quaternion(
+ (float)actualRotation3D.A,
+ (float)actualRotation3D.B,
+ (float)actualRotation3D.C,
+ (float)actualRotation3D.D);
+
+ var adapter = CoordinateSystemManager.Instance.CreateHostAdapter();
+ Vector3 hostUp = adapter.HostType == CoordinateSystemType.YUp ? Vector3.UnitY : Vector3.UnitZ;
+ if (!HoistingRealObjectPoseHelper.TryCreateRotationFromActualPose(
+ actualRotation,
+ hostForward,
+ hostUp,
+ out baselineQuaternion,
+ out selectedAxisDirection,
+ out selectedAxisLocal,
+ out projectedForward))
+ {
+ return false;
+ }
+
+ rotation = adapter.FromHostQuaternionDirect(
+ adapter.ComposeHostQuaternion(baselineQuaternion, _objectRotationCorrection));
+
+ Matrix4x4 baselineLinear = Matrix4x4.CreateFromQuaternion(baselineQuaternion);
+ Matrix4x4 hostComposedLinear = Matrix4x4.CreateFromQuaternion(new Quaternion(
+ (float)rotation.A,
+ (float)rotation.B,
+ (float)rotation.C,
+ (float)rotation.D));
+
+ LogManager.Debug(
+ $"[真实物体平面前进轴] Hoisting 已改用实际几何姿态基线,选中对象轴={selectedAxisDirection}。");
+ LogManager.Debug(
+ $"[真实物体起点姿态] 选中参考轴=({selectedAxisLocal.X:F3},{selectedAxisLocal.Y:F3},{selectedAxisLocal.Z:F3}), " +
+ $"投影前进=({projectedForward.X:F3},{projectedForward.Y:F3},{projectedForward.Z:F3}), " +
+ $"宿主修正={_objectRotationCorrection}, 本地修正=X=0.0°,Y=0.0°,Z=0.0°, " +
+ $"BaselineX=({baselineLinear.M11:F4},{baselineLinear.M21:F4},{baselineLinear.M31:F4}), " +
+ $"BaselineY=({baselineLinear.M12:F4},{baselineLinear.M22:F4},{baselineLinear.M32:F4}), " +
+ $"BaselineZ=({baselineLinear.M13:F4},{baselineLinear.M23:F4},{baselineLinear.M33:F4}), " +
+ $"HostComposeX=({hostComposedLinear.M11:F4},{hostComposedLinear.M21:F4},{hostComposedLinear.M31:F4}), " +
+ $"HostComposeY=({hostComposedLinear.M12:F4},{hostComposedLinear.M22:F4},{hostComposedLinear.M32:F4}), " +
+ $"HostComposeZ=({hostComposedLinear.M13:F4},{hostComposedLinear.M23:F4},{hostComposedLinear.M33:F4})");
+ return true;
+ }
+
private static Quaternion Rotation3DToHostQuaternion(Rotation3D rotation)
{
var linear = new Transform3D(rotation).Linear;
@@ -4357,8 +4471,9 @@ namespace NavisworksTransport.Core.Animation
convention = null;
extents = (0.0, 0.0, 0.0);
+ bool requiresReferenceRotation = _route?.PathType != PathType.Hoisting;
if (!IsRealObjectMode ||
- !_hasRealObjectReferenceRotation ||
+ (requiresReferenceRotation && !_hasRealObjectReferenceRotation) ||
_realObjectLength <= 0.0 ||
_realObjectWidth <= 0.0 ||
_realObjectHeight <= 0.0)
@@ -4386,8 +4501,9 @@ namespace NavisworksTransport.Core.Animation
convention = null;
extents = (0.0, 0.0, 0.0);
+ bool requiresReferenceRotation = _route?.PathType != PathType.Hoisting;
if (!IsRealObjectMode ||
- !_hasRealObjectReferenceRotation ||
+ (requiresReferenceRotation && !_hasRealObjectReferenceRotation) ||
_realObjectLength <= 0.0 ||
_realObjectWidth <= 0.0 ||
_realObjectHeight <= 0.0)
@@ -4432,6 +4548,38 @@ namespace NavisworksTransport.Core.Animation
return true;
}
+ if (_route?.PathType == PathType.Hoisting)
+ {
+ if (!TryResolveHoistingActualPose(
+ hostForward,
+ out var hoistingRotation,
+ out var baselineQuaternion,
+ out var selectedAxisDirection,
+ out _,
+ out _))
+ {
+ return false;
+ }
+
+ LocalAxisDirection hoistingSemanticUpAxis =
+ adapter.HostType == CoordinateSystemType.YUp
+ ? LocalAxisDirection.PositiveY
+ : LocalAxisDirection.PositiveZ;
+ convention = new ModelAxisConvention(selectedAxisDirection, hoistingSemanticUpAxis);
+
+ Quaternion hoistingFinalHostQuaternion = Rotation3DToHostQuaternion(hoistingRotation);
+ extents = RealObjectProjectedExtentResolver.CalculateProjectedSemanticExtents(
+ convention,
+ _realObjectLength,
+ _realObjectWidth,
+ _realObjectHeight,
+ baselineQuaternion,
+ hoistingFinalHostQuaternion,
+ targetFrame.Forward,
+ targetFrame.Up);
+ return true;
+ }
+
if (!TryCreateRealObjectPlanarPoseSolution(
targetFrame.Forward,
targetFrame.Up,
@@ -4761,6 +4909,12 @@ namespace NavisworksTransport.Core.Animation
return true;
}
+ if (_route?.PathType == PathType.Hoisting)
+ {
+ LogManager.Error("[吊装真实物体姿态] 无法基于实际几何姿态生成起点姿态,已禁止回退到 fragment 参考姿态。");
+ return false;
+ }
+
if (!PathTargetFrameResolver.TryCreatePlanarHostFrame(
hostForward,
CoordinateSystemManager.Instance.CreateHostAdapter().HostType,
@@ -4800,27 +4954,10 @@ namespace NavisworksTransport.Core.Animation
private bool TryCreateHoistingRealObjectRotationFromActualPose(Vector3 hostForward, out Rotation3D rotation)
{
- rotation = Rotation3D.Identity;
-
- if (_animatedObject == null ||
- !ModelItemTransformHelper.TryGetCurrentGeometryRotation(_animatedObject, out var actualRotation3D))
- {
- return false;
- }
-
- var actualRotation = new Quaternion(
- (float)actualRotation3D.A,
- (float)actualRotation3D.B,
- (float)actualRotation3D.C,
- (float)actualRotation3D.D);
-
- var adapter = CoordinateSystemManager.Instance.CreateHostAdapter();
- Vector3 hostUp = adapter.HostType == CoordinateSystemType.YUp ? Vector3.UnitY : Vector3.UnitZ;
- if (!HoistingRealObjectPoseHelper.TryCreateRotationFromActualPose(
- actualRotation,
+ if (!TryResolveHoistingActualPose(
hostForward,
- hostUp,
- out var baselineQuaternion,
+ out rotation,
+ out _,
out var selectedAxisDirection,
out var selectedAxisLocal,
out var projectedForward))
@@ -4828,31 +4965,8 @@ namespace NavisworksTransport.Core.Animation
return false;
}
- rotation = adapter.FromHostQuaternionDirect(
- adapter.ComposeHostQuaternion(baselineQuaternion, _objectRotationCorrection));
-
- Matrix4x4 baselineLinear = Matrix4x4.CreateFromQuaternion(baselineQuaternion);
- Matrix4x4 hostComposedLinear = Matrix4x4.CreateFromQuaternion(new Quaternion(
- (float)rotation.A,
- (float)rotation.B,
- (float)rotation.C,
- (float)rotation.D));
-
_realObjectPlanarSelectedForwardAxis = selectedAxisDirection;
_hasRealObjectPlanarSelectedForwardAxis = true;
-
- LogManager.Debug(
- $"[真实物体平面前进轴] Hoisting 已改用实际几何姿态基线,选中对象轴={selectedAxisDirection}。");
- LogManager.Debug(
- $"[真实物体起点姿态] 选中参考轴=({selectedAxisLocal.X:F3},{selectedAxisLocal.Y:F3},{selectedAxisLocal.Z:F3}), " +
- $"投影前进=({projectedForward.X:F3},{projectedForward.Y:F3},{projectedForward.Z:F3}), " +
- $"宿主修正={_objectRotationCorrection}, 本地修正=X=0.0°,Y=0.0°,Z=0.0°, " +
- $"BaselineX=({baselineLinear.M11:F4},{baselineLinear.M21:F4},{baselineLinear.M31:F4}), " +
- $"BaselineY=({baselineLinear.M12:F4},{baselineLinear.M22:F4},{baselineLinear.M32:F4}), " +
- $"BaselineZ=({baselineLinear.M13:F4},{baselineLinear.M23:F4},{baselineLinear.M33:F4}), " +
- $"HostComposeX=({hostComposedLinear.M11:F4},{hostComposedLinear.M21:F4},{hostComposedLinear.M31:F4}), " +
- $"HostComposeY=({hostComposedLinear.M12:F4},{hostComposedLinear.M22:F4},{hostComposedLinear.M32:F4}), " +
- $"HostComposeZ=({hostComposedLinear.M13:F4},{hostComposedLinear.M23:F4},{hostComposedLinear.M33:F4})");
return true;
}
From 9e8fea324116ec980c25ca8b9597a322d95162eb Mon Sep 17 00:00:00 2001
From: tian <11429339@qq.com>
Date: Thu, 2 Apr 2026 00:19:36 +0800
Subject: [PATCH 55/87] Unify real-object override rotation application
---
.../CoordinateSystem/RailPreservedPoseTests.cs | 7 +++++++
src/Core/Animation/PathAnimationManager.cs | 17 +++++++++++------
2 files changed, 18 insertions(+), 6 deletions(-)
diff --git a/UnitTests/CoordinateSystem/RailPreservedPoseTests.cs b/UnitTests/CoordinateSystem/RailPreservedPoseTests.cs
index 72f9614..9d3785d 100644
--- a/UnitTests/CoordinateSystem/RailPreservedPoseTests.cs
+++ b/UnitTests/CoordinateSystem/RailPreservedPoseTests.cs
@@ -141,5 +141,12 @@ namespace NavisworksTransport.UnitTests.CoordinateSystem
Assert.AreEqual(trackedRotation.C, resolved.C, 1e-9);
Assert.AreEqual(trackedRotation.D, resolved.D, 1e-9);
}
+
+ [TestMethod]
+ public void ShouldUseRealObjectOverrideRotation_ShouldEnableForRealObjects()
+ {
+ Assert.IsTrue(PathAnimationManager.ShouldUseRealObjectOverrideRotation(true));
+ Assert.IsFalse(PathAnimationManager.ShouldUseRealObjectOverrideRotation(false));
+ }
}
}
diff --git a/src/Core/Animation/PathAnimationManager.cs b/src/Core/Animation/PathAnimationManager.cs
index 6cdfe7d..7e448da 100644
--- a/src/Core/Animation/PathAnimationManager.cs
+++ b/src/Core/Animation/PathAnimationManager.cs
@@ -2748,7 +2748,7 @@ namespace NavisworksTransport.Core.Animation
try
{
ModelItem controlledObject = CurrentControlledObject;
- bool isRailRealObject = _route?.PathType == PathType.Rail && IsRealObjectMode && controlledObject != null;
+ bool usesRealObjectOverrideRotation = ShouldUseRealObjectOverrideRotation(IsRealObjectMode) && controlledObject != null;
if (controlledObject == null)
{
return;
@@ -2841,7 +2841,7 @@ namespace NavisworksTransport.Core.Animation
}
currentRotationForTransform = currentRotation;
- if (isRailRealObject)
+ if (usesRealObjectOverrideRotation)
{
if (ModelItemTransformHelper.TryGetCurrentOverrideRotation(controlledObject, out var currentOverrideRotation))
{
@@ -2896,11 +2896,11 @@ namespace NavisworksTransport.Core.Animation
$"Y=({targetLinear.Get(0, 1):F4},{targetLinear.Get(1, 1):F4},{targetLinear.Get(2, 1):F4}), " +
$"Z=({targetLinear.Get(0, 2):F4},{targetLinear.Get(1, 2):F4},{targetLinear.Get(2, 2):F4})");
- if (isRailRealObject)
+ if (usesRealObjectOverrideRotation)
{
var overrideLinear = new Transform3D(targetRotationForTransform).Linear;
LogManager.Debug(
- $"[Rail覆盖姿态] {controlledObject.DisplayName} Override目标: " +
+ $"[真实物体覆盖姿态] {controlledObject.DisplayName} Override目标: " +
$"X=({overrideLinear.Get(0, 0):F4},{overrideLinear.Get(1, 0):F4},{overrideLinear.Get(2, 0):F4}), " +
$"Y=({overrideLinear.Get(0, 1):F4},{overrideLinear.Get(1, 1):F4},{overrideLinear.Get(2, 1):F4}), " +
$"Z=({overrideLinear.Get(0, 2):F4},{overrideLinear.Get(1, 2):F4},{overrideLinear.Get(2, 2):F4})");
@@ -2918,11 +2918,11 @@ namespace NavisworksTransport.Core.Animation
_hasTrackedRotation = true;
_currentYaw = ModelItemTransformHelper.GetYawFromRotation(appliedTargetRotation);
- if (isRailRealObject)
+ if (usesRealObjectOverrideRotation)
{
Point3D hostActualAfter = GetTrackedObjectPosition(controlledObject);
LogManager.Debug(
- $"[Rail姿态应用] 宿主最终跟踪中心=({hostActualAfter.X:F3},{hostActualAfter.Y:F3},{hostActualAfter.Z:F3}), " +
+ $"[真实物体姿态应用] 宿主最终跟踪中心=({hostActualAfter.X:F3},{hostActualAfter.Y:F3},{hostActualAfter.Z:F3}), " +
$"目标跟踪中心=({newPosition.X:F3},{newPosition.Y:F3},{newPosition.Z:F3}), " +
$"偏差=({hostActualAfter.X - newPosition.X:F3},{hostActualAfter.Y - newPosition.Y:F3},{hostActualAfter.Z - newPosition.Z:F3})");
}
@@ -4036,6 +4036,11 @@ namespace NavisworksTransport.Core.Animation
return transformRotation;
}
+ internal static bool ShouldUseRealObjectOverrideRotation(bool isRealObjectMode)
+ {
+ return isRealObjectMode;
+ }
+
private void SyncTrackedRotationToDisplayedPose(ModelItem sourceObject)
{
if (sourceObject == null)
From 24951d4205f585af364f6166b12cace8f9319ee8 Mon Sep 17 00:00:00 2001
From: tian <11429339@qq.com>
Date: Thu, 2 Apr 2026 01:32:31 +0800
Subject: [PATCH 56/87] Stabilize hoisting start-pose logging and caching
---
.../RailPreservedPoseTests.cs | 7 +
src/Core/Animation/PathAnimationManager.cs | 305 +++++++++++-------
.../ViewModels/AnimationControlViewModel.cs | 5 +-
3 files changed, 199 insertions(+), 118 deletions(-)
diff --git a/UnitTests/CoordinateSystem/RailPreservedPoseTests.cs b/UnitTests/CoordinateSystem/RailPreservedPoseTests.cs
index 9d3785d..1e8326a 100644
--- a/UnitTests/CoordinateSystem/RailPreservedPoseTests.cs
+++ b/UnitTests/CoordinateSystem/RailPreservedPoseTests.cs
@@ -148,5 +148,12 @@ namespace NavisworksTransport.UnitTests.CoordinateSystem
Assert.IsTrue(PathAnimationManager.ShouldUseRealObjectOverrideRotation(true));
Assert.IsFalse(PathAnimationManager.ShouldUseRealObjectOverrideRotation(false));
}
+
+ [TestMethod]
+ public void ResolveHoistingPoseSourceLabel_ShouldDescribeCachedBasePose()
+ {
+ Assert.AreEqual("缓存基姿态", PathAnimationManager.ResolveHoistingPoseSourceLabel(true));
+ Assert.AreEqual("实际几何姿态基线", PathAnimationManager.ResolveHoistingPoseSourceLabel(false));
+ }
}
}
diff --git a/src/Core/Animation/PathAnimationManager.cs b/src/Core/Animation/PathAnimationManager.cs
index 7e448da..76eb2d2 100644
--- a/src/Core/Animation/PathAnimationManager.cs
+++ b/src/Core/Animation/PathAnimationManager.cs
@@ -198,6 +198,8 @@ namespace NavisworksTransport.Core.Animation
private Rotation3D _hoistingRealObjectBaseRotation = Rotation3D.Identity;
private double _hoistingRealObjectBaseYaw = 0.0;
private bool _hasHoistingRealObjectBasePose = false;
+ private Rotation3D _lastResolvedHoistingStartBaselineRotation = Rotation3D.Identity;
+ private bool _hasLastResolvedHoistingStartBaselineRotation = false;
private Vector3D _groundRealObjectStartCompensation = new Vector3D(0, 0, 0);
private bool _hasGroundRealObjectStartCompensation = false;
private bool _suppressGroundRealObjectCompensation = false;
@@ -856,7 +858,15 @@ namespace NavisworksTransport.Core.Animation
}
else if (IsRealObjectMode && _route.PathType == PathType.Hoisting)
{
- _hoistingRealObjectBaseRotation = planarRotation;
+ if (_hasLastResolvedHoistingStartBaselineRotation)
+ {
+ _hoistingRealObjectBaseRotation = _lastResolvedHoistingStartBaselineRotation;
+ }
+ else if (!TryCreateHoistingPlanarBaseRotationAtStart(out _hoistingRealObjectBaseRotation))
+ {
+ _hoistingRealObjectBaseRotation = planarRotation;
+ }
+
_hasHoistingRealObjectBasePose = TryResolvePlanarRealObjectBaseYaw(PathType.Hoisting, out _hoistingRealObjectBaseYaw);
LogManager.Debug(
$"[Hoisting真实物体基姿态] {(animatedObject ?? _animatedObject)?.DisplayName} BaseYaw={_hoistingRealObjectBaseYaw * 180.0 / Math.PI:F2}°, " +
@@ -2823,8 +2833,11 @@ namespace NavisworksTransport.Core.Animation
string poseTag = _route?.PathType == PathType.Rail
? "[Rail姿态基线诊断]"
: "[平面姿态基线诊断]";
+ string baselineLabel = usesRealObjectOverrideRotation
+ ? "当前姿态基线"
+ : "当前跟踪姿态";
LogManager.Debug(
- $"{poseTag} {controlledObject.DisplayName} 跟踪旋转: " +
+ $"{poseTag} {controlledObject.DisplayName} {baselineLabel}: " +
$"X=({trackedLinear.Get(0, 0):F4},{trackedLinear.Get(1, 0):F4},{trackedLinear.Get(2, 0):F4}), " +
$"Y=({trackedLinear.Get(0, 1):F4},{trackedLinear.Get(1, 1):F4},{trackedLinear.Get(2, 1):F4}), " +
$"Z=({trackedLinear.Get(0, 2):F4},{trackedLinear.Get(1, 2):F4},{trackedLinear.Get(2, 2):F4})");
@@ -2880,30 +2893,33 @@ namespace NavisworksTransport.Core.Animation
$"应用目标点=({appliedTargetPosition.X:F3},{appliedTargetPosition.Y:F3},{appliedTargetPosition.Z:F3})");
}
- var currentLinear = new Transform3D(currentRotationForTransform).Linear;
- var targetLinear = new Transform3D(appliedTargetRotation).Linear;
+ var hostCurrentPoseLocalAxesLinear = new Transform3D(currentRotationForTransform).Linear;
+ var hostTargetDisplayPoseLocalAxesLinear = new Transform3D(appliedTargetRotation).Linear;
LogManager.Debug(
$"[动画姿态入口] {controlledObject.DisplayName} 跟踪点=({_trackedPosition.X:F3},{_trackedPosition.Y:F3},{_trackedPosition.Z:F3}), " +
$"目标点=({appliedTargetPosition.X:F3},{appliedTargetPosition.Y:F3},{appliedTargetPosition.Z:F3})");
+ string currentRotationLabel = usesRealObjectOverrideRotation
+ ? "宿主覆盖姿态基线局部轴"
+ : "宿主当前跟踪姿态局部轴";
LogManager.Debug(
- $"[动画姿态入口] {controlledObject.DisplayName} 跟踪姿态: " +
- $"X=({currentLinear.Get(0, 0):F4},{currentLinear.Get(1, 0):F4},{currentLinear.Get(2, 0):F4}), " +
- $"Y=({currentLinear.Get(0, 1):F4},{currentLinear.Get(1, 1):F4},{currentLinear.Get(2, 1):F4}), " +
- $"Z=({currentLinear.Get(0, 2):F4},{currentLinear.Get(1, 2):F4},{currentLinear.Get(2, 2):F4})");
+ $"[动画姿态入口] {controlledObject.DisplayName} {currentRotationLabel}: " +
+ $"hostCurrentLocalXAxis=({hostCurrentPoseLocalAxesLinear.Get(0, 0):F4},{hostCurrentPoseLocalAxesLinear.Get(1, 0):F4},{hostCurrentPoseLocalAxesLinear.Get(2, 0):F4}), " +
+ $"hostCurrentLocalYAxis=({hostCurrentPoseLocalAxesLinear.Get(0, 1):F4},{hostCurrentPoseLocalAxesLinear.Get(1, 1):F4},{hostCurrentPoseLocalAxesLinear.Get(2, 1):F4}), " +
+ $"hostCurrentLocalZAxis=({hostCurrentPoseLocalAxesLinear.Get(0, 2):F4},{hostCurrentPoseLocalAxesLinear.Get(1, 2):F4},{hostCurrentPoseLocalAxesLinear.Get(2, 2):F4})");
LogManager.Debug(
- $"[动画姿态入口] {controlledObject.DisplayName} 目标姿态: " +
- $"X=({targetLinear.Get(0, 0):F4},{targetLinear.Get(1, 0):F4},{targetLinear.Get(2, 0):F4}), " +
- $"Y=({targetLinear.Get(0, 1):F4},{targetLinear.Get(1, 1):F4},{targetLinear.Get(2, 1):F4}), " +
- $"Z=({targetLinear.Get(0, 2):F4},{targetLinear.Get(1, 2):F4},{targetLinear.Get(2, 2):F4})");
+ $"[动画姿态入口] {controlledObject.DisplayName} 宿主目标显示姿态局部轴: " +
+ $"hostTargetDisplayLocalXAxis=({hostTargetDisplayPoseLocalAxesLinear.Get(0, 0):F4},{hostTargetDisplayPoseLocalAxesLinear.Get(1, 0):F4},{hostTargetDisplayPoseLocalAxesLinear.Get(2, 0):F4}), " +
+ $"hostTargetDisplayLocalYAxis=({hostTargetDisplayPoseLocalAxesLinear.Get(0, 1):F4},{hostTargetDisplayPoseLocalAxesLinear.Get(1, 1):F4},{hostTargetDisplayPoseLocalAxesLinear.Get(2, 1):F4}), " +
+ $"hostTargetDisplayLocalZAxis=({hostTargetDisplayPoseLocalAxesLinear.Get(0, 2):F4},{hostTargetDisplayPoseLocalAxesLinear.Get(1, 2):F4},{hostTargetDisplayPoseLocalAxesLinear.Get(2, 2):F4})");
if (usesRealObjectOverrideRotation)
{
- var overrideLinear = new Transform3D(targetRotationForTransform).Linear;
+ var hostTargetOverridePoseLocalAxesLinear = new Transform3D(targetRotationForTransform).Linear;
LogManager.Debug(
- $"[真实物体覆盖姿态] {controlledObject.DisplayName} Override目标: " +
- $"X=({overrideLinear.Get(0, 0):F4},{overrideLinear.Get(1, 0):F4},{overrideLinear.Get(2, 0):F4}), " +
- $"Y=({overrideLinear.Get(0, 1):F4},{overrideLinear.Get(1, 1):F4},{overrideLinear.Get(2, 1):F4}), " +
- $"Z=({overrideLinear.Get(0, 2):F4},{overrideLinear.Get(1, 2):F4},{overrideLinear.Get(2, 2):F4})");
+ $"[真实物体覆盖姿态] {controlledObject.DisplayName} 宿主目标覆盖姿态局部轴: " +
+ $"hostTargetOverrideLocalXAxis=({hostTargetOverridePoseLocalAxesLinear.Get(0, 0):F4},{hostTargetOverridePoseLocalAxesLinear.Get(1, 0):F4},{hostTargetOverridePoseLocalAxesLinear.Get(2, 0):F4}), " +
+ $"hostTargetOverrideLocalYAxis=({hostTargetOverridePoseLocalAxesLinear.Get(0, 1):F4},{hostTargetOverridePoseLocalAxesLinear.Get(1, 1):F4},{hostTargetOverridePoseLocalAxesLinear.Get(2, 1):F4}), " +
+ $"hostTargetOverrideLocalZAxis=({hostTargetOverridePoseLocalAxesLinear.Get(0, 2):F4},{hostTargetOverridePoseLocalAxesLinear.Get(1, 2):F4},{hostTargetOverridePoseLocalAxesLinear.Get(2, 2):F4})");
}
ModelItemTransformHelper.MoveItemIncrementallyToPositionAndRotation(
@@ -4174,45 +4190,6 @@ namespace NavisworksTransport.Core.Animation
return true;
}
- private bool TryCreateHoistingRealObjectConstrainedRotationFromHostForward(
- Vector3 hostForward,
- out Rotation3D rotation)
- {
- rotation = Rotation3D.Identity;
- if (!IsRealObjectMode ||
- _route?.PathType != PathType.Hoisting ||
- !_hasHoistingRealObjectBasePose)
- {
- return false;
- }
-
- if (!PathTargetFrameResolver.TryCreatePlanarHostFrame(
- hostForward,
- CoordinateSystemManager.Instance.ResolvedType,
- out var currentFrame))
- {
- return false;
- }
-
- if (!PathTargetFrameResolver.TryResolvePlanarHostYaw(
- currentFrame.Forward,
- CoordinateSystemManager.Instance.ResolvedType,
- out double targetYawRadians))
- {
- return false;
- }
-
- var adapter = CoordinateSystemManager.Instance.CreateHostAdapter();
- Quaternion baseQuaternion = Rotation3DToHostQuaternion(_hoistingRealObjectBaseRotation);
- Quaternion targetQuaternion = HoistingRealObjectPoseHelper.CreateRotationFromPlanarBasePose(
- baseQuaternion,
- _hoistingRealObjectBaseYaw,
- targetYawRadians,
- Vector3.Normalize(adapter.HostUpVector3));
- rotation = adapter.FromHostQuaternionDirect(targetQuaternion);
- return true;
- }
-
private bool TryResolveHoistingActualPose(
Vector3 hostForward,
out Rotation3D rotation,
@@ -4226,58 +4203,127 @@ namespace NavisworksTransport.Core.Animation
selectedAxisDirection = LocalAxisDirection.PositiveX;
selectedAxisLocal = Vector3.UnitX;
projectedForward = Vector3.Zero;
-
- if (_animatedObject == null ||
- !ModelItemTransformHelper.TryGetCurrentGeometryRotation(_animatedObject, out var actualRotation3D))
- {
- return false;
- }
-
- var actualRotation = new Quaternion(
- (float)actualRotation3D.A,
- (float)actualRotation3D.B,
- (float)actualRotation3D.C,
- (float)actualRotation3D.D);
-
- var adapter = CoordinateSystemManager.Instance.CreateHostAdapter();
- Vector3 hostUp = adapter.HostType == CoordinateSystemType.YUp ? Vector3.UnitY : Vector3.UnitZ;
- if (!HoistingRealObjectPoseHelper.TryCreateRotationFromActualPose(
- actualRotation,
+ string sourceLabel;
+ if (!TryResolveHoistingStartBaselinePose(
hostForward,
- hostUp,
out baselineQuaternion,
out selectedAxisDirection,
out selectedAxisLocal,
- out projectedForward))
+ out projectedForward,
+ out sourceLabel))
{
+ rotation = Rotation3D.Identity;
return false;
}
- rotation = adapter.FromHostQuaternionDirect(
- adapter.ComposeHostQuaternion(baselineQuaternion, _objectRotationCorrection));
+ var adapter = CoordinateSystemManager.Instance.CreateHostAdapter();
+ Quaternion finalHostQuaternion = adapter.ComposeHostQuaternion(
+ baselineQuaternion,
+ _objectRotationCorrection);
+ _lastResolvedHoistingStartBaselineRotation = adapter.FromHostQuaternionDirect(baselineQuaternion);
+ _hasLastResolvedHoistingStartBaselineRotation = true;
+ rotation = adapter.FromHostQuaternionDirect(finalHostQuaternion);
- Matrix4x4 baselineLinear = Matrix4x4.CreateFromQuaternion(baselineQuaternion);
- Matrix4x4 hostComposedLinear = Matrix4x4.CreateFromQuaternion(new Quaternion(
- (float)rotation.A,
- (float)rotation.B,
- (float)rotation.C,
- (float)rotation.D));
+ Matrix4x4 hostBaselineLocalAxesLinear = Matrix4x4.CreateFromQuaternion(baselineQuaternion);
+ Matrix4x4 hostComposedLocalAxesLinear = Matrix4x4.CreateFromQuaternion(finalHostQuaternion);
LogManager.Debug(
- $"[真实物体平面前进轴] Hoisting 已改用实际几何姿态基线,选中对象轴={selectedAxisDirection}。");
+ $"[真实物体平面前进轴] Hoisting 已改用{sourceLabel},选中对象轴={selectedAxisDirection}。");
LogManager.Debug(
$"[真实物体起点姿态] 选中参考轴=({selectedAxisLocal.X:F3},{selectedAxisLocal.Y:F3},{selectedAxisLocal.Z:F3}), " +
$"投影前进=({projectedForward.X:F3},{projectedForward.Y:F3},{projectedForward.Z:F3}), " +
$"宿主修正={_objectRotationCorrection}, 本地修正=X=0.0°,Y=0.0°,Z=0.0°, " +
- $"BaselineX=({baselineLinear.M11:F4},{baselineLinear.M21:F4},{baselineLinear.M31:F4}), " +
- $"BaselineY=({baselineLinear.M12:F4},{baselineLinear.M22:F4},{baselineLinear.M32:F4}), " +
- $"BaselineZ=({baselineLinear.M13:F4},{baselineLinear.M23:F4},{baselineLinear.M33:F4}), " +
- $"HostComposeX=({hostComposedLinear.M11:F4},{hostComposedLinear.M21:F4},{hostComposedLinear.M31:F4}), " +
- $"HostComposeY=({hostComposedLinear.M12:F4},{hostComposedLinear.M22:F4},{hostComposedLinear.M32:F4}), " +
- $"HostComposeZ=({hostComposedLinear.M13:F4},{hostComposedLinear.M23:F4},{hostComposedLinear.M33:F4})");
+ $"hostBaselineLocalXAxis=({hostBaselineLocalAxesLinear.M11:F4},{hostBaselineLocalAxesLinear.M21:F4},{hostBaselineLocalAxesLinear.M31:F4}), " +
+ $"hostBaselineLocalYAxis=({hostBaselineLocalAxesLinear.M12:F4},{hostBaselineLocalAxesLinear.M22:F4},{hostBaselineLocalAxesLinear.M32:F4}), " +
+ $"hostBaselineLocalZAxis=({hostBaselineLocalAxesLinear.M13:F4},{hostBaselineLocalAxesLinear.M23:F4},{hostBaselineLocalAxesLinear.M33:F4}), " +
+ $"hostComposedLocalXAxis=({hostComposedLocalAxesLinear.M11:F4},{hostComposedLocalAxesLinear.M21:F4},{hostComposedLocalAxesLinear.M31:F4}), " +
+ $"hostComposedLocalYAxis=({hostComposedLocalAxesLinear.M12:F4},{hostComposedLocalAxesLinear.M22:F4},{hostComposedLocalAxesLinear.M32:F4}), " +
+ $"hostComposedLocalZAxis=({hostComposedLocalAxesLinear.M13:F4},{hostComposedLocalAxesLinear.M23:F4},{hostComposedLocalAxesLinear.M33:F4})");
return true;
}
+ private bool TryResolveHoistingStartBaselinePose(
+ Vector3 hostForward,
+ out Quaternion baselineQuaternion,
+ out LocalAxisDirection selectedAxisDirection,
+ out Vector3 selectedAxisLocal,
+ out Vector3 projectedForward,
+ out string sourceLabel)
+ {
+ baselineQuaternion = Quaternion.Identity;
+ selectedAxisDirection = _hasRealObjectPlanarSelectedForwardAxis
+ ? _realObjectPlanarSelectedForwardAxis
+ : LocalAxisDirection.PositiveX;
+ selectedAxisLocal = Vector3.UnitX;
+ projectedForward = Vector3.Zero;
+
+ var adapter = CoordinateSystemManager.Instance.CreateHostAdapter();
+ Vector3 hostUp = adapter.HostType == CoordinateSystemType.YUp ? Vector3.UnitY : Vector3.UnitZ;
+ Vector3 normalizedHostForward = hostForward.LengthSquared() > 1e-6f
+ ? Vector3.Normalize(hostForward)
+ : Vector3.UnitX;
+ Vector3 planarProjectedForward = normalizedHostForward - Vector3.Dot(normalizedHostForward, hostUp) * hostUp;
+ projectedForward = planarProjectedForward.LengthSquared() > 1e-6f
+ ? Vector3.Normalize(planarProjectedForward)
+ : Vector3.UnitX;
+
+ if (_hasHoistingRealObjectBasePose)
+ {
+ sourceLabel = ResolveHoistingPoseSourceLabel(true);
+ Quaternion cachedBaseQuaternion = Rotation3DToHostQuaternion(_hoistingRealObjectBaseRotation);
+ if (!PathTargetFrameResolver.TryCreatePlanarHostFrame(
+ projectedForward,
+ CoordinateSystemManager.Instance.ResolvedType,
+ out var currentFrame))
+ {
+ return false;
+ }
+
+ if (!PathTargetFrameResolver.TryResolvePlanarHostYaw(
+ currentFrame.Forward,
+ CoordinateSystemManager.Instance.ResolvedType,
+ out double targetYawRadians))
+ {
+ return false;
+ }
+
+ baselineQuaternion = HoistingRealObjectPoseHelper.CreateRotationFromPlanarBasePose(
+ cachedBaseQuaternion,
+ _hoistingRealObjectBaseYaw,
+ targetYawRadians,
+ Vector3.Normalize(adapter.HostUpVector3));
+ selectedAxisLocal = ResolveReferenceWorldAxisForLocalDirection(
+ selectedAxisDirection,
+ _realObjectReferenceAxisX,
+ _realObjectReferenceAxisY,
+ _realObjectReferenceAxisZ);
+ return true;
+ }
+
+ sourceLabel = ResolveHoistingPoseSourceLabel(false);
+ Rotation3D sourceRotation;
+ if (_animatedObject == null ||
+ !ModelItemTransformHelper.TryGetCurrentGeometryRotation(_animatedObject, out sourceRotation))
+ {
+ return false;
+ }
+
+ Quaternion sourceQuaternion = Rotation3DToHostQuaternion(sourceRotation);
+ return HoistingRealObjectPoseHelper.TryCreateRotationFromActualPose(
+ sourceQuaternion,
+ projectedForward,
+ hostUp,
+ out baselineQuaternion,
+ out selectedAxisDirection,
+ out selectedAxisLocal,
+ out projectedForward);
+ }
+
+ internal static string ResolveHoistingPoseSourceLabel(bool hasHoistingBasePose)
+ {
+ return hasHoistingBasePose ? "缓存基姿态" : "实际几何姿态基线";
+ }
+
private static Quaternion Rotation3DToHostQuaternion(Rotation3D rotation)
{
var linear = new Transform3D(rotation).Linear;
@@ -4726,16 +4772,16 @@ namespace NavisworksTransport.Core.Animation
localCorrection);
rotation = adapter.FromHostQuaternionDirect(composedHostQuaternion);
- Matrix4x4 baselineLinear = Matrix4x4.CreateFromQuaternion(baselineHostQuaternion);
- Matrix4x4 composedLinear = Matrix4x4.CreateFromQuaternion(composedHostQuaternion);
+ Matrix4x4 hostBaselineLocalAxesLinear = Matrix4x4.CreateFromQuaternion(baselineHostQuaternion);
+ Matrix4x4 hostComposedLocalAxesLinear = Matrix4x4.CreateFromQuaternion(composedHostQuaternion);
LogManager.Debug(
- $"[Rail真实物体角度修正] BaselineX=({baselineLinear.M11:F4},{baselineLinear.M21:F4},{baselineLinear.M31:F4}), " +
- $"BaselineY=({baselineLinear.M12:F4},{baselineLinear.M22:F4},{baselineLinear.M32:F4}), " +
- $"BaselineZ=({baselineLinear.M13:F4},{baselineLinear.M23:F4},{baselineLinear.M33:F4}), " +
+ $"[Rail真实物体角度修正] hostBaselineLocalXAxis=({hostBaselineLocalAxesLinear.M11:F4},{hostBaselineLocalAxesLinear.M21:F4},{hostBaselineLocalAxesLinear.M31:F4}), " +
+ $"hostBaselineLocalYAxis=({hostBaselineLocalAxesLinear.M12:F4},{hostBaselineLocalAxesLinear.M22:F4},{hostBaselineLocalAxesLinear.M32:F4}), " +
+ $"hostBaselineLocalZAxis=({hostBaselineLocalAxesLinear.M13:F4},{hostBaselineLocalAxesLinear.M23:F4},{hostBaselineLocalAxesLinear.M33:F4}), " +
$"宿主修正={_objectRotationCorrection}, 本地修正={localCorrection}, " +
- $"HostComposeX=({composedLinear.M11:F4},{composedLinear.M21:F4},{composedLinear.M31:F4}), " +
- $"HostComposeY=({composedLinear.M12:F4},{composedLinear.M22:F4},{composedLinear.M32:F4}), " +
- $"HostComposeZ=({composedLinear.M13:F4},{composedLinear.M23:F4},{composedLinear.M33:F4})");
+ $"hostComposedLocalXAxis=({hostComposedLocalAxesLinear.M11:F4},{hostComposedLocalAxesLinear.M21:F4},{hostComposedLocalAxesLinear.M31:F4}), " +
+ $"hostComposedLocalYAxis=({hostComposedLocalAxesLinear.M12:F4},{hostComposedLocalAxesLinear.M22:F4},{hostComposedLocalAxesLinear.M32:F4}), " +
+ $"hostComposedLocalZAxis=({hostComposedLocalAxesLinear.M13:F4},{hostComposedLocalAxesLinear.M23:F4},{hostComposedLocalAxesLinear.M33:F4})");
return true;
}
@@ -4782,6 +4828,43 @@ namespace NavisworksTransport.Core.Animation
return false;
}
+ private bool TryCreateHoistingPlanarBaseRotationAtStart(out Rotation3D rotation)
+ {
+ rotation = Rotation3D.Identity;
+
+ if (!IsRealObjectMode ||
+ _route?.PathType != PathType.Hoisting ||
+ _pathPoints == null ||
+ _pathPoints.Count < 2)
+ {
+ return false;
+ }
+
+ if (!PathTargetFrameResolver.TryResolvePlanarStartHostForward(
+ PathType.Hoisting,
+ _pathPoints,
+ out var hostForward))
+ {
+ return false;
+ }
+
+ if (!TryResolveHoistingStartBaselinePose(
+ hostForward,
+ out var baselineQuaternion,
+ out _,
+ out _,
+ out _,
+ out _))
+ {
+ return false;
+ }
+
+ rotation = CoordinateSystemManager.Instance
+ .CreateHostAdapter()
+ .FromHostQuaternionDirect(baselineQuaternion);
+ return true;
+ }
+
private bool TryCreatePlanarPathRotationForFrame(Point3D previousPoint, Point3D currentPoint, Point3D nextPoint, out Rotation3D rotation)
{
rotation = Rotation3D.Identity;
@@ -4901,13 +4984,6 @@ namespace NavisworksTransport.Core.Animation
{
rotation = Rotation3D.Identity;
- if (_route?.PathType == PathType.Hoisting &&
- _hasHoistingRealObjectBasePose &&
- TryCreateHoistingRealObjectConstrainedRotationFromHostForward(hostForward, out rotation))
- {
- return true;
- }
-
if (_route?.PathType == PathType.Hoisting &&
TryCreateHoistingRealObjectRotationFromActualPose(hostForward, out rotation))
{
@@ -4941,19 +5017,19 @@ namespace NavisworksTransport.Core.Animation
Quaternion hostComposedQuaternion = adapter.ComposeHostQuaternion(solution.BaselineRotation, localCorrection);
rotation = adapter.FromHostQuaternionDirect(hostComposedQuaternion);
- Matrix4x4 baselineLinear = Matrix4x4.CreateFromQuaternion(solution.BaselineRotation);
- Matrix4x4 hostComposedLinear = Matrix4x4.CreateFromQuaternion(hostComposedQuaternion);
+ Matrix4x4 hostBaselineLocalAxesLinear = Matrix4x4.CreateFromQuaternion(solution.BaselineRotation);
+ Matrix4x4 hostComposedLocalAxesLinear = Matrix4x4.CreateFromQuaternion(hostComposedQuaternion);
LogManager.Debug(
$"[真实物体起点姿态] 选中参考轴=({solution.SelectedReferenceAxisLocal.X:F3},{solution.SelectedReferenceAxisLocal.Y:F3},{solution.SelectedReferenceAxisLocal.Z:F3}), " +
$"投影前进=({solution.ProjectedForward.X:F3},{solution.ProjectedForward.Y:F3},{solution.ProjectedForward.Z:F3}), " +
$"宿主修正={_objectRotationCorrection}, 本地修正={localCorrection}, " +
- $"BaselineX=({baselineLinear.M11:F4},{baselineLinear.M21:F4},{baselineLinear.M31:F4}), " +
- $"BaselineY=({baselineLinear.M12:F4},{baselineLinear.M22:F4},{baselineLinear.M32:F4}), " +
- $"BaselineZ=({baselineLinear.M13:F4},{baselineLinear.M23:F4},{baselineLinear.M33:F4}), " +
- $"HostComposeX=({hostComposedLinear.M11:F4},{hostComposedLinear.M21:F4},{hostComposedLinear.M31:F4}), " +
- $"HostComposeY=({hostComposedLinear.M12:F4},{hostComposedLinear.M22:F4},{hostComposedLinear.M32:F4}), " +
- $"HostComposeZ=({hostComposedLinear.M13:F4},{hostComposedLinear.M23:F4},{hostComposedLinear.M33:F4})");
+ $"hostBaselineLocalXAxis=({hostBaselineLocalAxesLinear.M11:F4},{hostBaselineLocalAxesLinear.M21:F4},{hostBaselineLocalAxesLinear.M31:F4}), " +
+ $"hostBaselineLocalYAxis=({hostBaselineLocalAxesLinear.M12:F4},{hostBaselineLocalAxesLinear.M22:F4},{hostBaselineLocalAxesLinear.M32:F4}), " +
+ $"hostBaselineLocalZAxis=({hostBaselineLocalAxesLinear.M13:F4},{hostBaselineLocalAxesLinear.M23:F4},{hostBaselineLocalAxesLinear.M33:F4}), " +
+ $"hostComposedLocalXAxis=({hostComposedLocalAxesLinear.M11:F4},{hostComposedLocalAxesLinear.M21:F4},{hostComposedLocalAxesLinear.M31:F4}), " +
+ $"hostComposedLocalYAxis=({hostComposedLocalAxesLinear.M12:F4},{hostComposedLocalAxesLinear.M22:F4},{hostComposedLocalAxesLinear.M32:F4}), " +
+ $"hostComposedLocalZAxis=({hostComposedLocalAxesLinear.M13:F4},{hostComposedLocalAxesLinear.M23:F4},{hostComposedLocalAxesLinear.M33:F4})");
return true;
}
@@ -5298,6 +5374,8 @@ namespace NavisworksTransport.Core.Animation
_hoistingRealObjectBaseRotation = Rotation3D.Identity;
_hoistingRealObjectBaseYaw = 0.0;
_hasHoistingRealObjectBasePose = false;
+ _lastResolvedHoistingStartBaselineRotation = Rotation3D.Identity;
+ _hasLastResolvedHoistingStartBaselineRotation = false;
}
private LocalEulerRotationCorrection ResolveRealObjectLocalRotationCorrection()
@@ -5317,7 +5395,6 @@ namespace NavisworksTransport.Core.Animation
_objectStartPlacementMode = ObjectStartPlacementMode.AlignToPathPose;
_hasPathPreservedPoseRotation = false;
_objectRotationCorrection = rotationCorrection;
- ResetPlanarRealObjectBasePoseCache();
// 如果动画已创建,更新物体到起点的朝向
if (_animatedObject != null && _pathPoints != null && _pathPoints.Count > 0)
diff --git a/src/UI/WPF/ViewModels/AnimationControlViewModel.cs b/src/UI/WPF/ViewModels/AnimationControlViewModel.cs
index 4689bb8..9d7e22e 100644
--- a/src/UI/WPF/ViewModels/AnimationControlViewModel.cs
+++ b/src/UI/WPF/ViewModels/AnimationControlViewModel.cs
@@ -2173,10 +2173,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
ObjectRotationCorrection = placementRequest.RotationCorrection;
LogManager.Info($"物体角度修正已更新: {_objectRotationCorrection}");
UpdateMainStatus($"物体角度修正: {_objectRotationCorrection}");
-
- // 应用角度修正到物体
- UpdateObjectRotation();
-
+
// 更新通行空间可视化(考虑旋转后的尺寸)
UpdatePassageSpaceVisualization();
}
From 4ec4cf77eed7639a6398241e61bdec5992be69db Mon Sep 17 00:00:00 2001
From: tian <11429339@qq.com>
Date: Thu, 2 Apr 2026 22:14:26 +0800
Subject: [PATCH 57/87] Refine path viewpoint behavior and config editing
---
.../CoordinateSystem/ViewpointHelperTests.cs | 35 ++++----
resources/default_config.toml | 13 +++
src/Core/Config/ConfigManager.cs | 4 +
src/Core/Config/SystemConfig.cs | 30 +++++++
.../ViewModels/LogisticsControlViewModel.cs | 8 +-
src/UI/WPF/ViewModels/PathEditingViewModel.cs | 9 +-
.../ViewModels/SystemManagementViewModel.cs | 7 +-
src/UI/WPF/Views/ConfigEditorDialog.xaml.cs | 54 +++++++++---
src/Utils/ViewpointHelper.cs | 82 ++++++++++++-------
9 files changed, 180 insertions(+), 62 deletions(-)
diff --git a/UnitTests/CoordinateSystem/ViewpointHelperTests.cs b/UnitTests/CoordinateSystem/ViewpointHelperTests.cs
index 90dad79..168e72a 100644
--- a/UnitTests/CoordinateSystem/ViewpointHelperTests.cs
+++ b/UnitTests/CoordinateSystem/ViewpointHelperTests.cs
@@ -1,6 +1,7 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using NavisworksTransport.Utils;
using System.Numerics;
+using NavisworksTransport.Core.Config;
namespace NavisworksTransport.UnitTests.CoordinateSystem
{
@@ -10,32 +11,35 @@ namespace NavisworksTransport.UnitTests.CoordinateSystem
[TestMethod]
public void ResolvePathViewpointProfile_ShouldReturnExpectedDefaults()
{
+ var config = ConfigManager.Instance.Current.PathEditing;
var ground = ViewpointHelper.ResolvePathViewpointProfile(ViewpointHelper.ViewpointStrategy.PathGroundSelection);
var hoisting = ViewpointHelper.ResolvePathViewpointProfile(ViewpointHelper.ViewpointStrategy.PathHoistingSelection);
var rail = ViewpointHelper.ResolvePathViewpointProfile(ViewpointHelper.ViewpointStrategy.PathRailSelection);
- Assert.AreEqual(1.0, ground.DistanceScale, 1e-9);
- Assert.AreEqual(12.0, ground.MinDistanceMeters, 1e-9);
+ Assert.AreEqual(12.0, ground.CameraDistanceMeters, 1e-9);
+ Assert.AreEqual(90.0, ground.ElevationDegrees, 1e-9);
- Assert.AreEqual(1.2, hoisting.DistanceScale, 1e-9);
- Assert.AreEqual(8.0, hoisting.MinDistanceMeters, 1e-9);
- Assert.AreEqual(22.0, hoisting.ElevationDegrees, 1e-9);
+ Assert.AreEqual(config.HoistingViewDistanceMeters, hoisting.CameraDistanceMeters, 1e-9);
+ Assert.AreEqual(config.HoistingViewElevationDegrees, hoisting.ElevationDegrees, 1e-9);
+ Assert.AreEqual(ViewpointHelper.PathCameraHorizontalMode.Side, hoisting.HorizontalMode);
- Assert.AreEqual(0.75, rail.DistanceScale, 1e-9);
- Assert.AreEqual(4.5, rail.MinDistanceMeters, 1e-9);
- Assert.AreEqual(12.0, rail.ElevationDegrees, 1e-9);
+ Assert.AreEqual(config.RailViewDistanceMeters, rail.CameraDistanceMeters, 1e-9);
+ Assert.AreEqual(config.RailViewElevationDegrees, rail.ElevationDegrees, 1e-9);
+ Assert.AreEqual(ViewpointHelper.PathCameraHorizontalMode.Side, rail.HorizontalMode);
}
[TestMethod]
- public void ResolveCameraOffsetDirection_Hoisting_ShouldLookFromBehindAndAbove()
+ public void ResolveCameraOffsetDirection_Hoisting_ShouldLookFromSideAndSlightlyAbove()
{
Vector3 hostUp = Vector3.UnitZ;
Vector3 forward = Vector3.UnitX;
+ var profile = ViewpointHelper.ResolvePathViewpointProfile(ViewpointHelper.ViewpointStrategy.PathHoistingSelection);
- Vector3 offset = ViewpointHelper.ResolveCameraOffsetDirection(PathType.Hoisting, hostUp, forward);
+ Vector3 offset = ViewpointHelper.ResolveCameraOffsetDirection(profile, hostUp, forward);
- Assert.IsTrue(offset.X < -0.8f);
- Assert.IsTrue(offset.Z > 0.3f);
+ Assert.AreEqual(0.0f, offset.X, 1e-5f);
+ Assert.IsTrue(offset.Y > 0.0f);
+ Assert.IsTrue(offset.Z > 0.0f);
}
[TestMethod]
@@ -43,12 +47,13 @@ namespace NavisworksTransport.UnitTests.CoordinateSystem
{
Vector3 hostUp = Vector3.UnitZ;
Vector3 forward = Vector3.UnitX;
+ var profile = ViewpointHelper.ResolvePathViewpointProfile(ViewpointHelper.ViewpointStrategy.PathRailSelection);
- Vector3 offset = ViewpointHelper.ResolveCameraOffsetDirection(PathType.Rail, hostUp, forward);
+ Vector3 offset = ViewpointHelper.ResolveCameraOffsetDirection(profile, hostUp, forward);
Assert.AreEqual(0.0f, offset.X, 1e-5f);
- Assert.IsTrue(offset.Y > 0.9f);
- Assert.IsTrue(offset.Z > 0.15f);
+ Assert.IsTrue(offset.Y > 0.0f);
+ Assert.IsTrue(offset.Z > 0.0f);
}
[TestMethod]
diff --git a/resources/default_config.toml b/resources/default_config.toml
index da3e2b4..8825899 100644
--- a/resources/default_config.toml
+++ b/resources/default_config.toml
@@ -26,6 +26,19 @@ default_path_turn_radius = 2.5
# 圆弧采样步长(米)- 推荐值:0.02-0.1
arc_sampling_step = 0.05
+# 吊装路径自动视角距离(米)
+# 默认为近距离低侧视
+hoisting_view_distance_meters = 6.0
+
+# 吊装路径自动视角仰角(度)
+hoisting_view_elevation_degrees = 30.0
+
+# Rail 路径自动视角距离(米)
+rail_view_distance_meters = 6.0
+
+# Rail 路径自动视角仰角(度)
+rail_view_elevation_degrees = 30.0
+
[visualization]
# 地图边距比例(0-1之间)
margin_ratio = 0.1
diff --git a/src/Core/Config/ConfigManager.cs b/src/Core/Config/ConfigManager.cs
index a89a216..5b04d96 100644
--- a/src/Core/Config/ConfigManager.cs
+++ b/src/Core/Config/ConfigManager.cs
@@ -394,6 +394,10 @@ namespace NavisworksTransport.Core.Config
config.PathEditing.SafetyMarginMeters = GetDoubleValueWithDefault(pathEdit, "safety_margin_meters", 0.1, missingItems);
config.PathEditing.DefaultPathTurnRadiusMeters = GetDoubleValueWithDefault(pathEdit, "default_path_turn_radius", 2.5, missingItems);
config.PathEditing.ArcSamplingStepMeters = GetDoubleValueWithDefault(pathEdit, "arc_sampling_step", 0.05, missingItems);
+ config.PathEditing.HoistingViewDistanceMeters = GetDoubleValueWithDefault(pathEdit, "hoisting_view_distance_meters", 6.0, missingItems);
+ config.PathEditing.HoistingViewElevationDegrees = GetDoubleValueWithDefault(pathEdit, "hoisting_view_elevation_degrees", 30.0, missingItems);
+ config.PathEditing.RailViewDistanceMeters = GetDoubleValueWithDefault(pathEdit, "rail_view_distance_meters", 6.0, missingItems);
+ config.PathEditing.RailViewElevationDegrees = GetDoubleValueWithDefault(pathEdit, "rail_view_elevation_degrees", 30.0, missingItems);
}
}
diff --git a/src/Core/Config/SystemConfig.cs b/src/Core/Config/SystemConfig.cs
index 024d340..1c424f7 100644
--- a/src/Core/Config/SystemConfig.cs
+++ b/src/Core/Config/SystemConfig.cs
@@ -119,6 +119,26 @@ namespace NavisworksTransport.Core.Config
///
public double ArcSamplingStepMeters { get; set; }
+ ///
+ /// 吊装路径自动视角距离(米)
+ ///
+ public double HoistingViewDistanceMeters { get; set; }
+
+ ///
+ /// 吊装路径自动视角仰角(度)
+ ///
+ public double HoistingViewElevationDegrees { get; set; }
+
+ ///
+ /// Rail 路径自动视角距离(米)
+ ///
+ public double RailViewDistanceMeters { get; set; }
+
+ ///
+ /// Rail 路径自动视角仰角(度)
+ ///
+ public double RailViewElevationDegrees { get; set; }
+
// === 模型单位接口(用于内部计算) ===
///
@@ -160,6 +180,16 @@ namespace NavisworksTransport.Core.Config
/// 圆弧采样步长(模型单位)
///
public double ArcSamplingStep => ArcSamplingStepMeters * Utils.UnitsConverter.GetMetersToUnitsConversionFactor();
+
+ ///
+ /// 吊装路径自动视角距离(模型单位)
+ ///
+ public double HoistingViewDistance => HoistingViewDistanceMeters * Utils.UnitsConverter.GetMetersToUnitsConversionFactor();
+
+ ///
+ /// Rail 路径自动视角距离(模型单位)
+ ///
+ public double RailViewDistance => RailViewDistanceMeters * Utils.UnitsConverter.GetMetersToUnitsConversionFactor();
}
///
diff --git a/src/UI/WPF/ViewModels/LogisticsControlViewModel.cs b/src/UI/WPF/ViewModels/LogisticsControlViewModel.cs
index bfe3c2e..17b7089 100644
--- a/src/UI/WPF/ViewModels/LogisticsControlViewModel.cs
+++ b/src/UI/WPF/ViewModels/LogisticsControlViewModel.cs
@@ -240,6 +240,12 @@ namespace NavisworksTransport.UI.WPF.ViewModels
{
if (IsClipBoxEnabled)
{
+ if (_pathPlanningManager.PathEditState == PathEditState.Creating)
+ {
+ LogManager.Info("[剖面盒] 当前处于新建路径流程,跳过自动跟随");
+ return;
+ }
+
LogManager.Info("[剖面盒] 路径已切换,重新设置剖面盒聚焦到新路径");
EnableClipBoxForCurrentRoute();
}
@@ -418,4 +424,4 @@ namespace NavisworksTransport.UI.WPF.ViewModels
#endregion
}
-}
\ No newline at end of file
+}
diff --git a/src/UI/WPF/ViewModels/PathEditingViewModel.cs b/src/UI/WPF/ViewModels/PathEditingViewModel.cs
index 6e01e40..67fb46d 100644
--- a/src/UI/WPF/ViewModels/PathEditingViewModel.cs
+++ b/src/UI/WPF/ViewModels/PathEditingViewModel.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Drawing;
@@ -385,8 +385,13 @@ namespace NavisworksTransport.UI.WPF.ViewModels
_pathPlanningManager.SetCurrentRoute(coreRoute);
LogManager.Info($"UI路径切换:已同步PathPlanningManager的CurrentRoute到 {value.Name}");
+ // 新建路径流程里禁止自动改视角,避免打断建路径操作。
+ if (_pathPlanningManager.PathEditState == PathEditState.Creating)
+ {
+ LogManager.Info($"UI路径切换:当前处于新建路径流程,跳过自动视角调整: {value.Name}");
+ }
// 自动调整视角到路径中心(只有路径有点时才调整)
- if (coreRoute.Points.Count > 0)
+ else if (coreRoute.Points.Count > 0)
{
try
{
diff --git a/src/UI/WPF/ViewModels/SystemManagementViewModel.cs b/src/UI/WPF/ViewModels/SystemManagementViewModel.cs
index 4468645..4048f76 100644
--- a/src/UI/WPF/ViewModels/SystemManagementViewModel.cs
+++ b/src/UI/WPF/ViewModels/SystemManagementViewModel.cs
@@ -832,14 +832,13 @@ namespace NavisworksTransport.UI.WPF.ViewModels
{
try
{
- // 使用单例模式显示配置编辑器(非模态,避免独占焦点)
- // 不传递 owner,让 ShowEditor 自己通过 DialogHelper 处理
+ // 使用模态配置编辑器,确保键盘输入不会被 Navisworks 主窗口截获
var dialog = NavisworksTransport.UI.WPF.Views.ConfigEditorDialog.ShowEditor();
if (dialog != null)
{
- UpdateMainStatus("配置编辑器已打开");
- LogManager.Info("配置编辑器:已打开(非模态)");
+ UpdateMainStatus("配置编辑器已关闭");
+ LogManager.Info("配置编辑器:已打开并关闭(模态)");
}
else
{
diff --git a/src/UI/WPF/Views/ConfigEditorDialog.xaml.cs b/src/UI/WPF/Views/ConfigEditorDialog.xaml.cs
index f8b3638..73e6d56 100644
--- a/src/UI/WPF/Views/ConfigEditorDialog.xaml.cs
+++ b/src/UI/WPF/Views/ConfigEditorDialog.xaml.cs
@@ -2,13 +2,15 @@ using System;
using System.IO;
using System.Windows;
using System.Windows.Controls;
+using System.Windows.Input;
+using System.Windows.Threading;
using System.Diagnostics;
using NavisworksTransport.Core.Config;
namespace NavisworksTransport.UI.WPF.Views
{
///
- /// 配置编辑器对话框 - 非模态窗口,避免独占焦点
+ /// 配置编辑器对话框
///
public partial class ConfigEditorDialog : Window
{
@@ -297,7 +299,7 @@ namespace NavisworksTransport.UI.WPF.Views
}
///
- /// 窗口加载事件 - 处理激活逻辑避免独占焦点
+ /// 窗口初始化后,确保获得稳定的键盘焦点
///
protected override void OnSourceInitialized(EventArgs e)
{
@@ -305,11 +307,10 @@ namespace NavisworksTransport.UI.WPF.Views
try
{
- // 激活并前置窗口,但不独占焦点
- // 先置顶再取消置顶,让窗口前置但不系统级置顶
this.Activate();
this.Topmost = true;
this.Topmost = false;
+ Dispatcher.BeginInvoke(new Action(FocusEditorTextBox), DispatcherPriority.Input);
LogManager.Debug("配置编辑器对话框已激活并前置显示");
}
@@ -319,6 +320,15 @@ namespace NavisworksTransport.UI.WPF.Views
}
}
+ ///
+ /// 窗口内容渲染完成后再次请求焦点,避免宿主窗口抢占键盘
+ ///
+ protected override void OnContentRendered(EventArgs e)
+ {
+ base.OnContentRendered(e);
+ Dispatcher.BeginInvoke(new Action(FocusEditorTextBox), DispatcherPriority.Input);
+ }
+
///
/// 激活并显示已存在的窗口
///
@@ -332,10 +342,10 @@ namespace NavisworksTransport.UI.WPF.Views
this.WindowState = WindowState.Normal;
}
- // 激活并前置窗口(先置顶再取消置顶,避免独占焦点)
this.Activate();
this.Topmost = true;
this.Topmost = false;
+ FocusEditorTextBox();
LogManager.Info("配置编辑器对话框已激活并前置显示");
}
@@ -345,10 +355,29 @@ namespace NavisworksTransport.UI.WPF.Views
}
}
+ private void FocusEditorTextBox()
+ {
+ try
+ {
+ if (ConfigContentTextBox == null)
+ {
+ return;
+ }
+
+ ConfigContentTextBox.Focus();
+ Keyboard.Focus(ConfigContentTextBox);
+ ConfigContentTextBox.CaretIndex = ConfigContentTextBox.Text?.Length ?? 0;
+ }
+ catch (Exception ex)
+ {
+ LogManager.Debug($"配置编辑器焦点设置失败: {ex.Message}");
+ }
+ }
+
#region 静态工厂方法
///
- /// 创建并显示配置编辑器对话框(单例模式,非模态)
+ /// 创建并显示配置编辑器对话框(单例模式,模态)
///
/// 对话框实例
public static ConfigEditorDialog ShowEditor()
@@ -385,16 +414,19 @@ namespace NavisworksTransport.UI.WPF.Views
// 创建新的对话框实例
var dialog = new ConfigEditorDialog();
- // 使用 DialogHelper 安全地设置 owner
- NavisworksTransport.Utils.DialogHelper.SetOwnerSafely(dialog);
+ // 优先设置 WPF Owner,失败时回退到 Navisworks 主窗口句柄
+ if (!NavisworksTransport.Utils.DialogHelper.SetOwnerSafely(dialog))
+ {
+ NavisworksTransport.Utils.DialogHelper.SetWin32Owner(dialog);
+ }
// 设置为当前实例
_currentInstance = dialog;
- // 使用 Show() 非模态显示,避免独占焦点
- dialog.Show();
+ // 使用 ShowDialog() 确保键盘输入不会被宿主窗口吃掉
+ dialog.ShowDialog();
- LogManager.Info("新的配置编辑器对话框已显示(非模态)");
+ LogManager.Info("新的配置编辑器对话框已显示(模态)");
return dialog;
}
}
diff --git a/src/Utils/ViewpointHelper.cs b/src/Utils/ViewpointHelper.cs
index 08aea47..6b4c0b0 100644
--- a/src/Utils/ViewpointHelper.cs
+++ b/src/Utils/ViewpointHelper.cs
@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using Autodesk.Navisworks.Api;
+using NavisworksTransport.Core.Config;
using NavisworksTransport.Utils.CoordinateSystem;
namespace NavisworksTransport.Utils
@@ -22,18 +23,25 @@ namespace NavisworksTransport.Utils
CollisionCloseUp
}
+ internal enum PathCameraHorizontalMode
+ {
+ Top,
+ Side,
+ Rear
+ }
+
internal struct PathViewpointProfile
{
- public PathViewpointProfile(double distanceScale, double minDistanceMeters, double elevationDegrees)
+ public PathViewpointProfile(double cameraDistanceMeters, double elevationDegrees, PathCameraHorizontalMode horizontalMode)
{
- DistanceScale = distanceScale;
- MinDistanceMeters = minDistanceMeters;
+ CameraDistanceMeters = cameraDistanceMeters;
ElevationDegrees = elevationDegrees;
+ HorizontalMode = horizontalMode;
}
- public double DistanceScale { get; }
- public double MinDistanceMeters { get; }
+ public double CameraDistanceMeters { get; }
public double ElevationDegrees { get; }
+ public PathCameraHorizontalMode HorizontalMode { get; }
}
internal struct FocusViewpointProfile
@@ -146,8 +154,7 @@ namespace NavisworksTransport.Utils
Point3D startPoint = path.Points[0].Position;
Point3D endPoint = path.Points[path.Points.Count - 1].Position;
- BoundingBox3D focusBoundingBox = CreateBoundingBoxFromPoints(startPoint, endPoint);
- BoundingBox3D viewBoundingBox = CalculatePathBoundingBox(path);
+ BoundingBox3D focusBoundingBox = ResolveDirectionalFocusBoundingBox(path);
Point3D focusCenter = GetBoundingBoxCenter(focusBoundingBox);
var adapter = CoordinateSystemManager.Instance.CreateHostAdapter();
@@ -158,31 +165,40 @@ namespace NavisworksTransport.Utils
(float)(endPoint.Z - startPoint.Z));
Vector3 fallbackForward = ToVector3(ProjectVectorOntoPlane(doc.FrontRightTopViewVector, adapter.HostUpVector));
Vector3 horizontalForward = ResolveHorizontalPathForward(rawPathDirection, hostUp, fallbackForward);
- Vector3 cameraOffsetDirection = ResolveCameraOffsetDirection(path.PathType, hostUp, horizontalForward);
-
- double maxDimensionModel = GetMaxDimension(viewBoundingBox);
- double minDistanceModel = UnitsConverter.ConvertFromMeters(profile.MinDistanceMeters);
- double cameraDistance = Math.Max(maxDimensionModel * profile.DistanceScale, minDistanceModel);
+ Vector3 cameraOffsetDirection = ResolveCameraOffsetDirection(profile, hostUp, horizontalForward);
+ double cameraDistance = UnitsConverter.ConvertFromMeters(profile.CameraDistanceMeters);
Point3D cameraPosition = new Point3D(
focusCenter.X + cameraOffsetDirection.X * (float)cameraDistance,
focusCenter.Y + cameraOffsetDirection.Y * (float)cameraDistance,
focusCenter.Z + cameraOffsetDirection.Z * (float)cameraDistance);
- double expansionMargin = Math.Max(
- maxDimensionModel * (VIEW_BOUNDING_BOX_EXPANSION_FACTOR - 1.0) / 2.0,
- UnitsConverter.ConvertFromMeters(2.0));
-
- ApplyViewpointWithZoomBox(
+ ApplyViewpoint(
cameraPosition,
focusCenter,
new Vector3D(hostUp.X, hostUp.Y, hostUp.Z),
- viewBoundingBox,
- expansionMargin);
+ useAlignDirection: true);
LogManager.Info(
$"视角已调整完成: 类型={path.PathType}, 焦点=({focusCenter.X:F2},{focusCenter.Y:F2},{focusCenter.Z:F2}), " +
$"相机=({cameraPosition.X:F2},{cameraPosition.Y:F2},{cameraPosition.Z:F2}), " +
- $"距离={UnitsConverter.ConvertToMeters(cameraDistance):F2}米, elevation={profile.ElevationDegrees:F1}°");
+ $"距离={UnitsConverter.ConvertToMeters(cameraDistance):F2}米, elevation={profile.ElevationDegrees:F1}°, 模式={profile.HorizontalMode}");
+ }
+
+ internal static BoundingBox3D ResolveDirectionalFocusBoundingBox(PathRoute path)
+ {
+ if (path == null)
+ {
+ throw new ArgumentNullException(nameof(path));
+ }
+
+ if (path.Points == null || path.Points.Count == 0)
+ {
+ throw new ArgumentException("路径为空或没有路径点", nameof(path));
+ }
+
+ // 侧视路径应聚焦整条路径包络,而不是只看首尾点。
+ // 对吊装路径尤其重要:首尾点常在地面,若只取首尾中心,高处主路径会被挤到画面上半部。
+ return CalculatePathBoundingBox(path);
}
///
@@ -296,15 +312,25 @@ namespace NavisworksTransport.Utils
internal static PathViewpointProfile ResolvePathViewpointProfile(ViewpointStrategy strategy)
{
+ PathEditingConfig config = ConfigManager.Instance.Current?.PathEditing ?? new PathEditingConfig();
switch (strategy)
{
case ViewpointStrategy.PathHoistingSelection:
- return new PathViewpointProfile(distanceScale: 1.2, minDistanceMeters: 8.0, elevationDegrees: 22.0);
+ return new PathViewpointProfile(
+ cameraDistanceMeters: config.HoistingViewDistanceMeters > 0 ? config.HoistingViewDistanceMeters : 6.0,
+ elevationDegrees: config.HoistingViewElevationDegrees > 0 ? config.HoistingViewElevationDegrees : 30.0,
+ horizontalMode: PathCameraHorizontalMode.Side);
case ViewpointStrategy.PathRailSelection:
- return new PathViewpointProfile(distanceScale: 0.75, minDistanceMeters: 4.5, elevationDegrees: 12.0);
+ return new PathViewpointProfile(
+ cameraDistanceMeters: config.RailViewDistanceMeters > 0 ? config.RailViewDistanceMeters : 6.0,
+ elevationDegrees: config.RailViewElevationDegrees > 0 ? config.RailViewElevationDegrees : 30.0,
+ horizontalMode: PathCameraHorizontalMode.Side);
case ViewpointStrategy.PathGroundSelection:
default:
- return new PathViewpointProfile(distanceScale: 1.0, minDistanceMeters: 12.0, elevationDegrees: 90.0);
+ return new PathViewpointProfile(
+ cameraDistanceMeters: 12.0,
+ elevationDegrees: 90.0,
+ horizontalMode: PathCameraHorizontalMode.Top);
}
}
@@ -340,27 +366,25 @@ namespace NavisworksTransport.Utils
return Vector3.Normalize(ProjectOntoPlane(canonicalFallback, hostUp));
}
- internal static Vector3 ResolveCameraOffsetDirection(PathType pathType, Vector3 hostUp, Vector3 horizontalForward)
+ internal static Vector3 ResolveCameraOffsetDirection(PathViewpointProfile profile, Vector3 hostUp, Vector3 horizontalForward)
{
- PathViewpointProfile profile = ResolvePathViewpointProfile(pathType);
float elevationRadians = (float)(profile.ElevationDegrees * Math.PI / 180.0);
float horizontalWeight = (float)Math.Cos(elevationRadians);
float verticalWeight = (float)Math.Sin(elevationRadians);
Vector3 horizontalDirection;
- switch (pathType)
+ switch (profile.HorizontalMode)
{
- case PathType.Hoisting:
+ case PathCameraHorizontalMode.Rear:
horizontalDirection = -horizontalForward;
break;
- case PathType.Rail:
+ case PathCameraHorizontalMode.Side:
horizontalDirection = Vector3.Cross(hostUp, horizontalForward);
if (horizontalDirection.LengthSquared() <= 1e-8f)
{
horizontalDirection = -horizontalForward;
}
break;
- case PathType.Ground:
default:
horizontalDirection = hostUp;
break;
From 9be5d250e8df0064fdf6ff11c99eea1bd66e6d94 Mon Sep 17 00:00:00 2001
From: tian <11429339@qq.com>
Date: Thu, 2 Apr 2026 22:35:58 +0800
Subject: [PATCH 58/87] Restore tool focus by picking context
---
src/Core/PathInputMonitor.cs | 2 +-
src/Core/PathPlanningManager.cs | 27 +++++++++++++++++++++++++++
2 files changed, 28 insertions(+), 1 deletion(-)
diff --git a/src/Core/PathInputMonitor.cs b/src/Core/PathInputMonitor.cs
index 57a5525..04e119e 100644
--- a/src/Core/PathInputMonitor.cs
+++ b/src/Core/PathInputMonitor.cs
@@ -49,7 +49,7 @@ namespace NavisworksTransport
if (currentTool != Tool.CustomToolPlugin)
{
LogManager.Info($"[InputMonitor] 用户按空格键,当前工具为{currentTool},强制恢复ToolPlugin焦点");
- bool restored = pathManager.ForceReinitializeToolPlugin(subscribeToEvents: false);
+ bool restored = pathManager.RestoreToolPluginFocusForCurrentContext();
if (restored)
{
return true;
diff --git a/src/Core/PathPlanningManager.cs b/src/Core/PathPlanningManager.cs
index 94b5455..620187b 100644
--- a/src/Core/PathPlanningManager.cs
+++ b/src/Core/PathPlanningManager.cs
@@ -4562,6 +4562,33 @@ namespace NavisworksTransport
}
}
+ ///
+ /// 根据当前业务上下文恢复 ToolPlugin 焦点。
+ /// 普通手动建/编辑路径应恢复 PathPlanningManager 主链;
+ /// 自动路径、装配拾取、多层吊装等外部自管拾取模式则保持外部事件处理。
+ ///
+ public bool RestoreToolPluginFocusForCurrentContext()
+ {
+ try
+ {
+ bool shouldSubscribeToManagerEvents = !IsInAutoPathMode;
+ string contextLabel = shouldSubscribeToManagerEvents
+ ? "普通手动取点"
+ : "外部自管拾取";
+
+ LogManager.Info(
+ $"[工具插件恢复] 按当前上下文恢复焦点: 模式={contextLabel}, " +
+ $"PathEditState={PathEditState}, 订阅PathPlanningManager事件={shouldSubscribeToManagerEvents}");
+
+ return ForceReinitializeToolPlugin(shouldSubscribeToManagerEvents);
+ }
+ catch (Exception ex)
+ {
+ LogManager.Error($"[工具插件恢复] 按当前上下文恢复焦点失败: {ex.Message}", ex);
+ return false;
+ }
+ }
+
#endregion
#region 路径计算和保存
From 7058e5fd23655b4a05b7129a3d2e3dde0c7baf90 Mon Sep 17 00:00:00 2001
From: tian <11429339@qq.com>
Date: Thu, 2 Apr 2026 22:37:16 +0800
Subject: [PATCH 59/87] Add hoisting layer height editing
---
src/UI/WPF/Models/HoistingLevelItem.cs | 44 ++++
src/UI/WPF/ViewModels/PathEditingViewModel.cs | 232 +++++++++++++++++-
src/UI/WPF/Views/PathEditingView.xaml | 53 ++++
3 files changed, 324 insertions(+), 5 deletions(-)
diff --git a/src/UI/WPF/Models/HoistingLevelItem.cs b/src/UI/WPF/Models/HoistingLevelItem.cs
index c92e7ce..56d356b 100644
--- a/src/UI/WPF/Models/HoistingLevelItem.cs
+++ b/src/UI/WPF/Models/HoistingLevelItem.cs
@@ -2,6 +2,7 @@ using System;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using Autodesk.Navisworks.Api;
+using System.Collections.Generic;
namespace NavisworksTransport.UI.WPF.Models
{
@@ -53,4 +54,47 @@ namespace NavisworksTransport.UI.WPF.Models
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
+
+ ///
+ /// 吊装路径层高编辑项
+ ///
+ public class HoistingLayerEditItem : INotifyPropertyChanged
+ {
+ private int _displayIndex;
+ private double _heightInMeters;
+ private string _pointSummary;
+
+ public int DisplayIndex
+ {
+ get => _displayIndex;
+ set { _displayIndex = value; OnPropertyChanged(nameof(DisplayIndex)); }
+ }
+
+ ///
+ /// 相对起吊点的绝对高度(米)
+ ///
+ public double HeightInMeters
+ {
+ get => _heightInMeters;
+ set { _heightInMeters = value; OnPropertyChanged(nameof(HeightInMeters)); }
+ }
+
+ public string PointSummary
+ {
+ get => _pointSummary;
+ set { _pointSummary = value; OnPropertyChanged(nameof(PointSummary)); }
+ }
+
+ ///
+ /// 当前层关联的路径点 Id 列表
+ ///
+ public List PointIds { get; } = new List();
+
+ public event PropertyChangedEventHandler PropertyChanged;
+
+ protected virtual void OnPropertyChanged(string propertyName)
+ {
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
+ }
+ }
}
diff --git a/src/UI/WPF/ViewModels/PathEditingViewModel.cs b/src/UI/WPF/ViewModels/PathEditingViewModel.cs
index 67fb46d..c533ce7 100644
--- a/src/UI/WPF/ViewModels/PathEditingViewModel.cs
+++ b/src/UI/WPF/ViewModels/PathEditingViewModel.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Drawing;
@@ -332,6 +332,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
// 多层吊装模式
private bool _isMultiLevelHoistingMode = false;
private ObservableCollection _multiLevelItems = new ObservableCollection();
+ private ObservableCollection _hoistingLayerEditItems = new ObservableCollection();
private Point3D _multiLevelStartPoint;
private Point3D _multiLevelEndPoint;
private bool _hasMultiLevelStartPoint = false;
@@ -339,6 +340,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
private string _multiLevelStartPointText = "未选择";
private string _multiLevelEndPointText = "未选择";
private string _multiLevelStatsText = "层级数: 0 | 预估路径点数: 0";
+ private string _hoistingLayerEditHintText = "选择一条吊装路径后,可在这里按层编辑相对起点的绝对高度。";
private bool _isSelectingMultiLevelStartPoint = false;
private bool _isSelectingMultiLevelEndPoint = false;
private HoistingLevelItem _currentSelectingLevelTarget;
@@ -431,9 +433,11 @@ namespace NavisworksTransport.UI.WPF.ViewModels
// 🔥 路径类型改变时,检查是否可以使用路径线
OnPropertyChanged(nameof(CanUsePathLines));
OnPropertyChanged(nameof(IsRailRouteSelected));
+ OnPropertyChanged(nameof(IsHoistingRouteSelected));
OnPropertyChanged(nameof(SelectedRailMountMode));
OnPropertyChanged(nameof(SelectedRailNormalOffsetInMeters));
OnPropertyChanged(nameof(CanRepositionRailStartPoint));
+ OnPropertyChanged(nameof(CanApplyHoistingLayerHeights));
NotifyRailAssemblyCommandStateChanged();
if (!CanUsePathLines && ShowPathLines)
{
@@ -442,6 +446,8 @@ namespace NavisworksTransport.UI.WPF.ViewModels
LogManager.Info("[路径可视化] 切换到吊装/空轨路径,自动关闭路径线");
}
+ RefreshHoistingLayerEditItemsFromSelectedRoute();
+
// 实现路径选择时的可视化切换
UpdatePathVisualization();
}
@@ -493,6 +499,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
}
public bool IsRailRouteSelected => SelectedPathRoute?.PathType == PathType.Rail;
+ public bool IsHoistingRouteSelected => SelectedPathRoute?.PathType == PathType.Hoisting;
public ObservableCollection> RailMountModeOptions
{
@@ -906,6 +913,18 @@ namespace NavisworksTransport.UI.WPF.ViewModels
set => SetProperty(ref _multiLevelItems, value);
}
+ public ObservableCollection HoistingLayerEditItems
+ {
+ get => _hoistingLayerEditItems;
+ set => SetProperty(ref _hoistingLayerEditItems, value);
+ }
+
+ public string HoistingLayerEditHintText
+ {
+ get => _hoistingLayerEditHintText;
+ set => SetProperty(ref _hoistingLayerEditHintText, value);
+ }
+
public string MultiLevelStartPointText
{
get => _multiLevelStartPointText;
@@ -925,6 +944,9 @@ namespace NavisworksTransport.UI.WPF.ViewModels
}
public bool CanCreateMultiLevelPath => _hasMultiLevelStartPoint && _hasMultiLevelEndPoint && _multiLevelItems.Count > 0;
+ public bool CanApplyHoistingLayerHeights => IsHoistingRouteSelected &&
+ SelectedPathRoute != null &&
+ HoistingLayerEditItems.Count > 0;
#endregion
@@ -1326,6 +1348,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
public ICommand PickMultiLevelTargetCommand { get; private set; }
public ICommand CreateMultiLevelPathCommand { get; private set; }
public ICommand CancelMultiLevelHoistingCommand { get; private set; }
+ public ICommand ApplyHoistingLayerHeightsCommand { get; private set; }
#endregion
@@ -1977,6 +2000,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
AnalyzeAssemblyTerminalFaceForEditCommand = new RelayCommand(async () => await ExecuteAnalyzeAssemblyTerminalFaceAsync(RailAssemblyWorkflowMode.EditSelectedRail), () => CanAnalyzeAssemblyTerminalFaceForEdit);
DecreaseSelectedRailNormalOffsetCommand = new RelayCommand(() => AdjustSelectedRailNormalOffset(-RailNormalOffsetNudgeStepInMeters));
IncreaseSelectedRailNormalOffsetCommand = new RelayCommand(() => AdjustSelectedRailNormalOffset(RailNormalOffsetNudgeStepInMeters));
+ ApplyHoistingLayerHeightsCommand = new RelayCommand(async () => await ExecuteApplyHoistingLayerHeightsAsync(), () => CanApplyHoistingLayerHeights);
}
#endregion
@@ -2010,22 +2034,58 @@ namespace NavisworksTransport.UI.WPF.ViewModels
HasAssemblyTerminalObject = true;
AssemblyStartPointText = "未选择";
AssemblyTerminalObjectName = ModelItemAnalysisHelper.GetSafeDisplayName(selectedItem);
+ bool isEditMode = IsRailAssemblyEditMode(mode);
+
ResetAssemblyEndFaceAnalysisState();
- ResetAssemblyInstallationReferenceState();
+ if (!isEditMode)
+ {
+ ResetAssemblyInstallationReferenceState();
+ }
RefreshAssemblyTerminalObjectInfo();
ClearAssemblyAnchorMarker();
ClearAssemblyEndFaceAnalysisVisuals();
- ClearAssemblyInstallationReferenceVisuals();
+ if (!isEditMode)
+ {
+ ClearAssemblyInstallationReferenceVisuals();
+ }
NotifyRailAssemblyCommandStateChanged();
- // 新建路径时隐藏其他路径的辅助线
- ClearAssemblyReferenceLineVisuals();
+ if (isEditMode)
+ {
+ // 编辑态重选箱体后,只恢复可由当前 Rail 路径和终点箱体稳定重建的可视化。
+ // 安装参考面依赖安装拾取点、安装面 span 等会话态数据,这些数据当前未持久化到 PathRoute,
+ // 因此这里不尝试恢复安装参考面,避免后续误以为“少了一次刷新调用”是 bug。
+ RefreshSelectedPathVisualizationForAssemblyEdit();
+ RefreshAssemblyReferenceRodIfNeeded();
+ }
+ else
+ {
+ // 新建路径时隐藏其他路径的辅助线
+ ClearAssemblyReferenceLineVisuals();
+ }
UpdateMainStatus($"已捕获终点箱体: {AssemblyTerminalObjectName}");
LogManager.Info($"[直线装配] 已捕获终点箱体: {AssemblyTerminalObjectName}");
}, "捕获终点箱体");
}
+ private void RefreshSelectedPathVisualizationForAssemblyEdit()
+ {
+ if (_selectedPathRoute == null || _selectedPathRoute.Points.Count == 0 || _pathPlanningManager == null)
+ {
+ return;
+ }
+
+ var coreRoute = _pathPlanningManager.Routes?.FirstOrDefault(route => route.Id == _selectedPathRoute.Id)
+ ?? _pathPlanningManager.Routes?.FirstOrDefault(route => route.Name == _selectedPathRoute.Name);
+ if (coreRoute == null)
+ {
+ return;
+ }
+
+ _pathPlanningManager.DrawRouteVisualization(coreRoute, isAutoPath: false);
+ }
+
private void ClearNonGridPathVisualizations(string context)
{
if (PathPointRenderPlugin.Instance == null)
@@ -4572,6 +4632,163 @@ namespace NavisworksTransport.UI.WPF.ViewModels
return true;
}
+ private void RefreshHoistingLayerEditItemsFromSelectedRoute()
+ {
+ HoistingLayerEditItems.Clear();
+
+ var coreRoute = GetSelectedCoreRoute();
+ if (coreRoute == null || coreRoute.PathType != PathType.Hoisting || coreRoute.Points == null || coreRoute.Points.Count < 2)
+ {
+ HoistingLayerEditHintText = "选择一条吊装路径后,可在这里按层编辑相对起点的绝对高度。";
+ OnPropertyChanged(nameof(CanApplyHoistingLayerHeights));
+ return;
+ }
+
+ HostCoordinateAdapter hostAdapter = CoordinateSystemManager.Instance.CreateHostAdapter();
+ PathPoint hostStartPoint = coreRoute.Points.FirstOrDefault(point => point.Type == PathPointType.StartPoint) ?? coreRoute.Points.First();
+ double hostStartElevation = HoistingCoordinateHelper.GetElevation(hostStartPoint.Position, hostAdapter);
+ const double relativeHeightToleranceInMeters = 0.001;
+
+ var layerItems = new List();
+ foreach (var point in coreRoute.Points)
+ {
+ if (point == null || point.Type == PathPointType.StartPoint || point.Type == PathPointType.EndPoint)
+ {
+ continue;
+ }
+
+ double hostPointElevation = HoistingCoordinateHelper.GetElevation(point.Position, hostAdapter);
+ double relativeHeightInMeters = UnitsConverter.ConvertToMeters(hostPointElevation - hostStartElevation);
+ if (relativeHeightInMeters <= relativeHeightToleranceInMeters)
+ {
+ continue;
+ }
+
+ HoistingLayerEditItem existingLayer = layerItems.FirstOrDefault(item =>
+ Math.Abs(item.HeightInMeters - relativeHeightInMeters) <= relativeHeightToleranceInMeters);
+ if (existingLayer == null)
+ {
+ existingLayer = new HoistingLayerEditItem
+ {
+ HeightInMeters = Math.Round(relativeHeightInMeters, 3)
+ };
+ layerItems.Add(existingLayer);
+ }
+
+ if (!string.IsNullOrWhiteSpace(point.Id) && !existingLayer.PointIds.Contains(point.Id))
+ {
+ existingLayer.PointIds.Add(point.Id);
+ }
+ }
+
+ List orderedLayers = layerItems
+ .OrderByDescending(item => item.HeightInMeters)
+ .ToList();
+
+ for (int i = 0; i < orderedLayers.Count; i++)
+ {
+ orderedLayers[i].DisplayIndex = i + 1;
+ orderedLayers[i].PointSummary = $"关联点 {orderedLayers[i].PointIds.Count} 个";
+ HoistingLayerEditItems.Add(orderedLayers[i]);
+ }
+
+ HoistingLayerEditHintText = HoistingLayerEditItems.Count > 0
+ ? "层高按高到低显示;每层高度相对起吊点,且必须严格低于上一层、高于下一层。"
+ : "当前吊装路径未解析出可编辑的空中层。";
+ OnPropertyChanged(nameof(CanApplyHoistingLayerHeights));
+ }
+
+ private async Task ExecuteApplyHoistingLayerHeightsAsync()
+ {
+ await SafeExecuteAsync(() =>
+ {
+ var coreRoute = GetSelectedCoreRoute();
+ if (coreRoute == null || coreRoute.PathType != PathType.Hoisting)
+ {
+ return;
+ }
+
+ if (HoistingLayerEditItems.Count == 0)
+ {
+ UpdateMainStatus("当前吊装路径没有可编辑的空中层。");
+ return;
+ }
+
+ for (int i = 0; i < HoistingLayerEditItems.Count; i++)
+ {
+ HoistingLayerEditItem currentLayer = HoistingLayerEditItems[i];
+ if (currentLayer.HeightInMeters <= 0)
+ {
+ HandleInvalidHoistingLayerHeightInput($"第{currentLayer.DisplayIndex}层高度必须大于0。");
+ return;
+ }
+
+ if (i > 0 && currentLayer.HeightInMeters >= HoistingLayerEditItems[i - 1].HeightInMeters)
+ {
+ HandleInvalidHoistingLayerHeightInput($"第{currentLayer.DisplayIndex}层高度必须严格低于上一层。");
+ return;
+ }
+
+ if (i < HoistingLayerEditItems.Count - 1 && currentLayer.HeightInMeters <= HoistingLayerEditItems[i + 1].HeightInMeters)
+ {
+ HandleInvalidHoistingLayerHeightInput($"第{currentLayer.DisplayIndex}层高度必须严格高于下一层。");
+ return;
+ }
+ }
+
+ HostCoordinateAdapter hostAdapter = CoordinateSystemManager.Instance.CreateHostAdapter();
+ PathPoint hostStartPoint = coreRoute.Points.FirstOrDefault(point => point.Type == PathPointType.StartPoint) ?? coreRoute.Points.First();
+ double hostStartElevation = HoistingCoordinateHelper.GetElevation(hostStartPoint.Position, hostAdapter);
+
+ foreach (HoistingLayerEditItem layerItem in HoistingLayerEditItems)
+ {
+ double hostTargetElevation = hostStartElevation + UnitsConverter.ConvertFromMeters(layerItem.HeightInMeters);
+ foreach (string pointId in layerItem.PointIds)
+ {
+ PathPoint targetPoint = coreRoute.Points.FirstOrDefault(point => string.Equals(point.Id, pointId, StringComparison.Ordinal));
+ if (targetPoint != null)
+ {
+ targetPoint.Position = HoistingCoordinateHelper.SetElevation(targetPoint.Position, hostTargetElevation, hostAdapter);
+ }
+ }
+ }
+
+ PathPoint firstAerialPoint = coreRoute.Points.FirstOrDefault(point =>
+ point != null &&
+ point.Type != PathPointType.StartPoint &&
+ point.Type != PathPointType.EndPoint &&
+ HoistingCoordinateHelper.GetElevation(point.Position, hostAdapter) > hostStartElevation + UnitsConverter.ConvertFromMeters(0.001));
+ coreRoute.LiftHeight = firstAerialPoint != null
+ ? HoistingCoordinateHelper.GetElevation(firstAerialPoint.Position, hostAdapter) - hostStartElevation
+ : 0.0;
+ coreRoute.LastModified = DateTime.Now;
+ coreRoute.RecalculateRoute("更新吊装层高");
+
+ SyncObjectParametersToRenderPlugin();
+ _pathPlanningManager.DrawRouteVisualization(coreRoute, isAutoPath: false);
+ _pathPlanningManager.SavePathToDatabase(coreRoute);
+ _pathPlanningManager.RaiseVisualizationStateChanged();
+
+ if (SelectedPathRoute != null && SelectedPathRoute.Id == coreRoute.Id)
+ {
+ SyncPathViewModelFromCoreRoute(SelectedPathRoute, coreRoute, preserveSelection: true);
+ }
+
+ RefreshHoistingLayerEditItemsFromSelectedRoute();
+ UpdateMainStatus($"已应用 {HoistingLayerEditItems.Count} 个吊装层高。");
+ LogManager.Info($"[吊装层高] {coreRoute.Name}: 已应用 {HoistingLayerEditItems.Count} 个层高编辑项");
+ }, "应用吊装层高");
+ }
+
+ private void HandleInvalidHoistingLayerHeightInput(string message)
+ {
+ const string validationMessage = "每层高度必须低于上一层、高于下一层,也不能相等。";
+ LogManager.Warning($"[吊装层高] 用户输入无效: {message}");
+ RefreshHoistingLayerEditItemsFromSelectedRoute();
+ UpdateMainStatus(validationMessage);
+ MessageBox.Show(validationMessage, "层高输入无效", MessageBoxButton.OK, MessageBoxImage.Warning);
+ }
+
///
/// 更新吊装路径的关联点(UI层)
///
@@ -6579,6 +6796,11 @@ namespace NavisworksTransport.UI.WPF.ViewModels
LogManager.Info($"已更新路径点列表,当前点数: {pathViewModel.Points.Count}");
+ if (SelectedPathRoute == pathViewModel)
+ {
+ RefreshHoistingLayerEditItemsFromSelectedRoute();
+ }
+
// 更新真实路径可视化
UpdatePathVisualization();
}
diff --git a/src/UI/WPF/Views/PathEditingView.xaml b/src/UI/WPF/Views/PathEditingView.xaml
index 73e7b6d..dc7680a 100644
--- a/src/UI/WPF/Views/PathEditingView.xaml
+++ b/src/UI/WPF/Views/PathEditingView.xaml
@@ -371,6 +371,59 @@ NavisworksTransport 路径编辑页签视图 - 采用与动画控制和分层管
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
From e21e9347050c1d6197989e8a0167bb1312623efe Mon Sep 17 00:00:00 2001
From: tian <11429339@qq.com>
Date: Sat, 4 Apr 2026 10:58:29 +0800
Subject: [PATCH 60/87] Add rail preferred normal repair script
---
scripts/Fix-RailPreferredNormals.ps1 | 209 ++++++++++++++++++
src/Core/Animation/PathAnimationManager.cs | 4 +-
src/Core/PathPlanningModels.cs | 2 +-
src/UI/WPF/ViewModels/PathEditingViewModel.cs | 8 +-
.../CanonicalRailOffsetResolver.cs | 2 +-
src/Utils/RailPathPoseHelper.cs | 12 +-
6 files changed, 223 insertions(+), 14 deletions(-)
create mode 100644 scripts/Fix-RailPreferredNormals.ps1
diff --git a/scripts/Fix-RailPreferredNormals.ps1 b/scripts/Fix-RailPreferredNormals.ps1
new file mode 100644
index 0000000..d85a680
--- /dev/null
+++ b/scripts/Fix-RailPreferredNormals.ps1
@@ -0,0 +1,209 @@
+<#
+用法示例
+
+1. 批量修复当前目录下所有 XML 路径文件
+powershell -ExecutionPolicy Bypass -File "C:\Users\Tellme\apps\NavisworksTransport-rail-mount-modes\scripts\Fix-RailPreferredNormals.ps1" -Path "C:\Users\Tellme\Documents\NavisworksTransport\模型"
+
+2. 递归修复目录及其子目录中的所有 XML 路径文件
+powershell -ExecutionPolicy Bypass -File "C:\Users\Tellme\apps\NavisworksTransport-rail-mount-modes\scripts\Fix-RailPreferredNormals.ps1" -Path "C:\Users\Tellme\Documents\NavisworksTransport\模型" -Recurse
+
+3. 只修复单个 XML 路径文件
+powershell -ExecutionPolicy Bypass -File "C:\Users\Tellme\apps\NavisworksTransport-rail-mount-modes\scripts\Fix-RailPreferredNormals.ps1" -Path "C:\Users\Tellme\Documents\NavisworksTransport\模型\副本_人工_0331_133243.xml"
+
+脚本规则
+
+- 只处理 pathType="Rail" 且 railMountMode="OverRail" 的路径
+- 只修复 railPreferredNormalY < 0 的路径
+- 修复方式为将 railPreferredNormalX / Y / Z 三个分量一起取反
+- 原文件会被直接覆盖,同时自动保留一个同名 .bak 备份文件
+#>
+
+param(
+ [Parameter(Mandatory = $true)]
+ [string]$Path,
+
+ [switch]$Recurse
+)
+
+Set-StrictMode -Version Latest
+$ErrorActionPreference = 'Stop'
+
+function Format-NormalValue {
+ param(
+ [Parameter(Mandatory = $true)]
+ [double]$Value
+ )
+
+ return $Value.ToString("0.###", [System.Globalization.CultureInfo]::InvariantCulture)
+}
+
+function Test-ParseableDoubleText {
+ param(
+ [Parameter(Mandatory = $true)]
+ [string]$Text,
+
+ [ref]$Value
+ )
+
+ return [double]::TryParse(
+ $Text,
+ [System.Globalization.NumberStyles]::Float -bor [System.Globalization.NumberStyles]::AllowLeadingSign,
+ [System.Globalization.CultureInfo]::InvariantCulture,
+ $Value)
+}
+
+function Update-RouteNode {
+ param(
+ [Parameter(Mandatory = $true)]
+ [System.Xml.XmlElement]$RouteNode,
+
+ [Parameter(Mandatory = $true)]
+ [string]$SourcePath
+ )
+
+ if ($RouteNode.GetAttribute("pathType") -ne "Rail") {
+ return $false
+ }
+
+ if ($RouteNode.GetAttribute("railMountMode") -ne "OverRail") {
+ return $false
+ }
+
+ $xText = $RouteNode.GetAttribute("railPreferredNormalX")
+ $yText = $RouteNode.GetAttribute("railPreferredNormalY")
+ $zText = $RouteNode.GetAttribute("railPreferredNormalZ")
+
+ if ([string]::IsNullOrWhiteSpace($xText) -or
+ [string]::IsNullOrWhiteSpace($yText) -or
+ [string]::IsNullOrWhiteSpace($zText)) {
+ return $false
+ }
+
+ $x = 0.0
+ $y = 0.0
+ $z = 0.0
+ if (-not (Test-ParseableDoubleText -Text $xText -Value ([ref]$x)) -or
+ -not (Test-ParseableDoubleText -Text $yText -Value ([ref]$y)) -or
+ -not (Test-ParseableDoubleText -Text $zText -Value ([ref]$z))) {
+ throw "文件 '$SourcePath' 中存在无法解析的 railPreferredNormal 值。"
+ }
+
+ if ($y -ge 0) {
+ return $false
+ }
+
+ $RouteNode.SetAttribute("railPreferredNormalX", (Format-NormalValue -Value (-$x)))
+ $RouteNode.SetAttribute("railPreferredNormalY", (Format-NormalValue -Value (-$y)))
+ $RouteNode.SetAttribute("railPreferredNormalZ", (Format-NormalValue -Value (-$z)))
+ return $true
+}
+
+function Save-XmlDocument {
+ param(
+ [Parameter(Mandatory = $true)]
+ [xml]$Document,
+
+ [Parameter(Mandatory = $true)]
+ [string]$TargetPath
+ )
+
+ $settings = New-Object System.Xml.XmlWriterSettings
+ $settings.Indent = $true
+ $settings.IndentChars = " "
+ $settings.Encoding = New-Object System.Text.UTF8Encoding($false)
+ $settings.NewLineChars = "`r`n"
+ $settings.NewLineHandling = [System.Xml.NewLineHandling]::Replace
+
+ $writer = [System.Xml.XmlWriter]::Create($TargetPath, $settings)
+ try {
+ $Document.Save($writer)
+ }
+ finally {
+ $writer.Dispose()
+ }
+}
+
+function Update-XmlFile {
+ param(
+ [Parameter(Mandatory = $true)]
+ [string]$FilePath
+ )
+
+ [xml]$xml = Get-Content -LiteralPath $FilePath -Raw
+ $routeNodes = $xml.SelectNodes("//*[local-name()='Route']")
+ if ($null -eq $routeNodes -or $routeNodes.Count -eq 0) {
+ return [pscustomobject]@{
+ FilePath = $FilePath
+ Modified = $false
+ RouteCount = 0
+ }
+ }
+
+ $modifiedRouteCount = 0
+ foreach ($routeNode in $routeNodes) {
+ if (Update-RouteNode -RouteNode $routeNode -SourcePath $FilePath) {
+ $modifiedRouteCount++
+ }
+ }
+
+ if ($modifiedRouteCount -gt 0) {
+ $backupPath = "$FilePath.bak"
+ if (-not (Test-Path -LiteralPath $backupPath)) {
+ Copy-Item -LiteralPath $FilePath -Destination $backupPath
+ }
+
+ Save-XmlDocument -Document $xml -TargetPath $FilePath
+ }
+
+ return [pscustomobject]@{
+ FilePath = $FilePath
+ Modified = ($modifiedRouteCount -gt 0)
+ RouteCount = $modifiedRouteCount
+ }
+}
+
+$resolvedPath = Resolve-Path -LiteralPath $Path
+$item = Get-Item -LiteralPath $resolvedPath
+
+$files = @()
+if ($item.PSIsContainer) {
+ $childParams = @{
+ LiteralPath = $item.FullName
+ File = $true
+ }
+ if ($Recurse) {
+ $childParams["Recurse"] = $true
+ }
+
+ $files = Get-ChildItem @childParams | Where-Object { $_.Extension -ieq ".xml" }
+}
+else {
+ $files = @($item)
+}
+
+if ($files.Count -eq 0) {
+ Write-Host "未找到可处理的 XML 文件。"
+ exit 0
+}
+
+$results = @()
+foreach ($file in $files) {
+ $results += Update-XmlFile -FilePath $file.FullName
+}
+
+$modifiedFiles = @($results | Where-Object { $_.Modified })
+$modifiedRoutes = 0
+if ($modifiedFiles.Count -gt 0) {
+ $measure = $modifiedFiles | Measure-Object -Property RouteCount -Sum
+ if ($null -ne $measure -and $null -ne $measure.Sum) {
+ $modifiedRoutes = [int]$measure.Sum
+ }
+}
+
+Write-Host ("扫描文件: {0}" -f $results.Count)
+Write-Host ("修改文件: {0}" -f $modifiedFiles.Count)
+Write-Host ("修复路径: {0}" -f $modifiedRoutes)
+
+foreach ($result in $modifiedFiles) {
+ Write-Host ("已修复: {0} (路径数={1})" -f $result.FilePath, $result.RouteCount)
+}
diff --git a/src/Core/Animation/PathAnimationManager.cs b/src/Core/Animation/PathAnimationManager.cs
index 76eb2d2..54ad5b6 100644
--- a/src/Core/Animation/PathAnimationManager.cs
+++ b/src/Core/Animation/PathAnimationManager.cs
@@ -770,7 +770,7 @@ namespace NavisworksTransport.Core.Animation
else
{
startPosition = RailPathPoseHelper.ResolveObjectSpaceCenterPosition(_route, startPosition, previousPoint, nextPoint, objectHeight);
- LogManager.Debug($"[移动到起点] Rail路径调整: 参考点=({_pathPoints[0].X:F2},{_pathPoints[0].Y:F2},{_pathPoints[0].Z:F2}), 物体中心=({startPosition.X:F2},{startPosition.Y:F2},{startPosition.Z:F2}), 物体高度={objectHeight:F2}, 安装={_route.RailMountMode}, 对接={(PathRoute.IsTopPayloadAnchorForMountMode(_route.RailMountMode) ? "顶面对接" : "底面对接")}");
+ LogManager.Debug($"[移动到起点] Rail路径调整: 参考点=({_pathPoints[0].X:F2},{_pathPoints[0].Y:F2},{_pathPoints[0].Z:F2}), 物体中心=({startPosition.X:F2},{startPosition.Y:F2},{startPosition.Z:F2}), 物体高度={objectHeight:F2}, 安装={_route.RailMountMode}, 参考面={(PathRoute.IsTopReferenceFaceForMountMode(_route.RailMountMode) ? "顶面参考点" : "底面参考点")}");
}
if (TryCreateRailPathRotation(
@@ -1278,7 +1278,7 @@ namespace NavisworksTransport.Core.Animation
else
{
framePosition = RailPathPoseHelper.ResolveObjectSpaceCenterPosition(_route, framePosition, p1, p2, objectHeight);
- LogManager.Debug($"[Rail路径] 调整物体位置: 参考点=({p1.X:F2},{p1.Y:F2},{p1.Z:F2})->({p2.X:F2},{p2.Y:F2},{p2.Z:F2}), 物体中心=({framePosition.X:F2},{framePosition.Y:F2},{framePosition.Z:F2}), 安装={_route.RailMountMode}, 对接={(PathRoute.IsTopPayloadAnchorForMountMode(_route.RailMountMode) ? "顶面对接" : "底面对接")}");
+ LogManager.Debug($"[Rail路径] 调整物体位置: 参考点=({p1.X:F2},{p1.Y:F2},{p1.Z:F2})->({p2.X:F2},{p2.Y:F2},{p2.Z:F2}), 物体中心=({framePosition.X:F2},{framePosition.Y:F2},{framePosition.Z:F2}), 安装={_route.RailMountMode}, 参考面={(PathRoute.IsTopReferenceFaceForMountMode(_route.RailMountMode) ? "顶面参考点" : "底面参考点")}");
}
}
}
diff --git a/src/Core/PathPlanningModels.cs b/src/Core/PathPlanningModels.cs
index eabea26..2553df7 100644
--- a/src/Core/PathPlanningModels.cs
+++ b/src/Core/PathPlanningModels.cs
@@ -566,7 +566,7 @@ namespace NavisworksTransport
[Serializable]
public class PathRoute
{
- public static bool IsTopPayloadAnchorForMountMode(RailMountMode railMountMode)
+ public static bool IsTopReferenceFaceForMountMode(RailMountMode railMountMode)
{
return railMountMode == RailMountMode.UnderRail;
}
diff --git a/src/UI/WPF/ViewModels/PathEditingViewModel.cs b/src/UI/WPF/ViewModels/PathEditingViewModel.cs
index c533ce7..e8abb29 100644
--- a/src/UI/WPF/ViewModels/PathEditingViewModel.cs
+++ b/src/UI/WPF/ViewModels/PathEditingViewModel.cs
@@ -2943,16 +2943,16 @@ namespace NavisworksTransport.UI.WPF.ViewModels
private double GetAssemblyAnchorDirection()
{
- return PathRoute.IsTopPayloadAnchorForMountMode(AssemblyMountMode)
+ return PathRoute.IsTopReferenceFaceForMountMode(AssemblyMountMode)
? 1.0
: -1.0;
}
private string GetAssemblyAnchorText()
{
- return PathRoute.IsTopPayloadAnchorForMountMode(AssemblyMountMode)
- ? "顶面对接"
- : "底面对接";
+ return PathRoute.IsTopReferenceFaceForMountMode(AssemblyMountMode)
+ ? "顶面参考点"
+ : "底面参考点";
}
private sealed class AssemblyReferenceLine
diff --git a/src/Utils/CoordinateSystem/CanonicalRailOffsetResolver.cs b/src/Utils/CoordinateSystem/CanonicalRailOffsetResolver.cs
index eab2073..23edf3b 100644
--- a/src/Utils/CoordinateSystem/CanonicalRailOffsetResolver.cs
+++ b/src/Utils/CoordinateSystem/CanonicalRailOffsetResolver.cs
@@ -15,7 +15,7 @@ namespace NavisworksTransport.Utils.CoordinateSystem
// 路径点本身已经是安装参考点(终点锚点)语义。
// 动画跟踪中心只需要相对安装参考点沿轨道法向偏移半个高度,
// 不能叠加任何“参考线 -> 锚点”旧语义偏移,否则会把终点/逐帧参考点重复平移。
- double halfHeightOffset = PathRoute.IsTopPayloadAnchorForMountMode(route.RailMountMode)
+ double halfHeightOffset = PathRoute.IsTopReferenceFaceForMountMode(route.RailMountMode)
? -objectHeight / 2.0
: objectHeight / 2.0;
return halfHeightOffset + route.RailNormalOffset;
diff --git a/src/Utils/RailPathPoseHelper.cs b/src/Utils/RailPathPoseHelper.cs
index 27b6fb7..39b7776 100644
--- a/src/Utils/RailPathPoseHelper.cs
+++ b/src/Utils/RailPathPoseHelper.cs
@@ -50,7 +50,7 @@ namespace NavisworksTransport.Utils
return 0.0;
}
- if (PathRoute.IsTopPayloadAnchorForMountMode(route.RailMountMode))
+ if (PathRoute.IsTopReferenceFaceForMountMode(route.RailMountMode))
{
return -objectHeight;
}
@@ -60,8 +60,8 @@ namespace NavisworksTransport.Utils
///
/// 计算通行空间中心相对于 Rail 参考点的 Z 偏移(模型单位)。
- /// 顶面对接时,通行空间中心位于对接点下方半高;
- /// 底面对接时,通行空间中心位于对接点上方半高。
+ /// 路径参考点位于构件顶面时,通行空间中心位于参考点下方半高;
+ /// 路径参考点位于构件底面时,通行空间中心位于参考点上方半高。
///
public static double GetObjectSpaceCenterZOffset(PathRoute route, double objectSpaceHeight)
{
@@ -70,7 +70,7 @@ namespace NavisworksTransport.Utils
return 0.0;
}
- if (PathRoute.IsTopPayloadAnchorForMountMode(route.RailMountMode))
+ if (PathRoute.IsTopReferenceFaceForMountMode(route.RailMountMode))
{
return -objectSpaceHeight / 2.0;
}
@@ -605,7 +605,7 @@ namespace NavisworksTransport.Utils
private static double GetBottomOffsetMagnitude(PathRoute route, double objectHeight)
{
- double baseOffset = PathRoute.IsTopPayloadAnchorForMountMode(route.RailMountMode)
+ double baseOffset = PathRoute.IsTopReferenceFaceForMountMode(route.RailMountMode)
? -objectHeight
: 0.0;
return baseOffset + route.RailNormalOffset;
@@ -613,7 +613,7 @@ namespace NavisworksTransport.Utils
private static double GetObjectSpaceCenterOffsetMagnitude(PathRoute route, double objectSpaceHeight)
{
- double baseOffset = PathRoute.IsTopPayloadAnchorForMountMode(route.RailMountMode)
+ double baseOffset = PathRoute.IsTopReferenceFaceForMountMode(route.RailMountMode)
? -objectSpaceHeight / 2.0
: objectSpaceHeight / 2.0;
return baseOffset + route.RailNormalOffset;
From fb9dac9953f983e641c794346ff7383726e26568 Mon Sep 17 00:00:00 2001
From: tian <11429339@qq.com>
Date: Sun, 5 Apr 2026 01:36:24 +0800
Subject: [PATCH 61/87] =?UTF-8?q?=E6=81=A2=E5=A4=8D=E5=9C=B0=E9=9D=A2?=
=?UTF-8?q?=E8=B7=AF=E5=BE=84=E4=B8=BA3=E6=9C=8825=E5=8F=B7=E7=89=88?=
=?UTF-8?q?=E6=9C=AC?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.gitignore | 2 +
src/Core/Animation/PathAnimationManager.cs | 223 ++++++++-------------
src/Utils/ModelItemTransformHelper.cs | 116 +++++++++++
3 files changed, 201 insertions(+), 140 deletions(-)
diff --git a/.gitignore b/.gitignore
index 4373112..30977ea 100644
--- a/.gitignore
+++ b/.gitignore
@@ -15,3 +15,5 @@ navisworks_api/
*.exe
*.db
+
+.codex-temp
\ No newline at end of file
diff --git a/src/Core/Animation/PathAnimationManager.cs b/src/Core/Animation/PathAnimationManager.cs
index 54ad5b6..5c49d14 100644
--- a/src/Core/Animation/PathAnimationManager.cs
+++ b/src/Core/Animation/PathAnimationManager.cs
@@ -813,6 +813,7 @@ namespace NavisworksTransport.Core.Animation
$"[路径起点诊断] 起点应用后实际trackedPoint=({startAppliedPoint.X:F3},{startAppliedPoint.Y:F3},{startAppliedPoint.Z:F3}), " +
$"相对目标偏差=({startAppliedPoint.X - startPosition.X:F3},{startAppliedPoint.Y - startPosition.Y:F3},{startAppliedPoint.Z - startPosition.Z:F3})");
+ // 🔥 起点不再进行二次补偿,旋转偏移由 MoveItemIncrementallyToTrackedPositionAndRotation 内部处理
if (IsRealObjectMode && _route.PathType == PathType.Ground)
{
var startOffset = new Vector3D(
@@ -825,36 +826,20 @@ namespace NavisworksTransport.Core.Animation
startOffset.Z * startOffset.Z);
LogManager.Info(
- $"[路径起点补偿] 首次落位偏差=({startOffset.X:F3},{startOffset.Y:F3},{startOffset.Z:F3}), 长度={startOffsetLength:F3}");
+ $"[路径起点诊断] 落位偏差=({startOffset.X:F3},{startOffset.Y:F3},{startOffset.Z:F3}), 长度={startOffsetLength:F3} (仅记录,不二次补偿)");
- if (startOffsetLength > 1e-3)
- {
- _groundRealObjectStartCompensation = startOffset;
- _hasGroundRealObjectStartCompensation = true;
-
- UpdateObjectPosition(startPosition, planarRotation);
-
- var correctedAppliedPoint = GetTrackedObjectPosition(CurrentControlledObject ?? _animatedObject);
- LogManager.Info(
- $"[路径起点补偿] 补偿后实际trackedPoint=({correctedAppliedPoint.X:F3},{correctedAppliedPoint.Y:F3},{correctedAppliedPoint.Z:F3}), " +
- $"相对目标偏差=({correctedAppliedPoint.X - startPosition.X:F3},{correctedAppliedPoint.Y - startPosition.Y:F3},{correctedAppliedPoint.Z - startPosition.Z:F3})");
- }
- else
- {
- _groundRealObjectStartCompensation = new Vector3D(0, 0, 0);
- _hasGroundRealObjectStartCompensation = false;
- }
+ // 不再设置 _groundRealObjectStartCompensation,也不再调用二次 UpdateObjectPosition
+ _groundRealObjectStartCompensation = new Vector3D(0, 0, 0);
+ _hasGroundRealObjectStartCompensation = false;
}
if (IsRealObjectMode && _route.PathType == PathType.Ground)
{
_groundRealObjectBaseRotation = planarRotation;
- _hasGroundRealObjectBasePose = TryResolvePlanarRealObjectBaseYaw(PathType.Ground, out _groundRealObjectBaseYaw);
+ _hasGroundRealObjectBasePose = TryResolveGroundRealObjectBaseYaw(out _groundRealObjectBaseYaw);
LogManager.Info(
$"[Ground真实物体基姿态] {animatedObject.DisplayName} BaseYaw={_groundRealObjectBaseYaw * 180.0 / Math.PI:F2}°, " +
- $"已记录基姿态={_hasGroundRealObjectBasePose}, " +
- $"起点补偿=({_groundRealObjectStartCompensation.X:F3},{_groundRealObjectStartCompensation.Y:F3},{_groundRealObjectStartCompensation.Z:F3}), " +
- $"已启用补偿={_hasGroundRealObjectStartCompensation}");
+ $"已记录基姿态={_hasGroundRealObjectBasePose} (旋转偏移由逐帧方法内部处理)");
}
else if (IsRealObjectMode && _route.PathType == PathType.Hoisting)
{
@@ -2758,7 +2743,7 @@ namespace NavisworksTransport.Core.Animation
try
{
ModelItem controlledObject = CurrentControlledObject;
- bool usesRealObjectOverrideRotation = ShouldUseRealObjectOverrideRotation(IsRealObjectMode) && controlledObject != null;
+ bool isRailRealObject = _route?.PathType == PathType.Rail && IsRealObjectMode && controlledObject != null;
if (controlledObject == null)
{
return;
@@ -2782,67 +2767,43 @@ namespace NavisworksTransport.Core.Animation
Point3D currentPositionForTransform = _trackedPosition;
Rotation3D currentRotation;
- Rotation3D currentRotationForTransform;
- Rotation3D actualGeometryRotation = Rotation3D.Identity;
- bool hasActualGeometryRotation = false;
Rotation3D appliedTargetRotation = newRotation;
- Rotation3D targetRotationForTransform = newRotation;
Point3D appliedTargetPosition = newPosition;
- Rotation3D referenceRotation = IsRealObjectMode && _hasRealObjectReferenceRotation
- ? CoordinateSystemManager.Instance.CreateHostAdapter().FromHostQuaternionDirect(_realObjectReferenceRotation)
- : Rotation3D.Identity;
- Rotation3D transformRotation = controlledObject.Transform.Factor().Rotation;
- PathType currentPathType = _route?.PathType ?? PathType.Ground;
- currentRotation = ResolveCurrentRotationBaseline(
- currentPathType,
- IsRealObjectMode,
- _hasTrackedRotation,
- _trackedRotation,
- _hasRealObjectReferenceRotation,
- referenceRotation,
- transformRotation,
- hasActualGeometryRotation: false,
- actualGeometryRotation: Rotation3D.Identity);
+ if (_hasTrackedRotation)
+ {
+ currentRotation = _trackedRotation;
+ }
+ else if (IsRealObjectMode && _hasRealObjectReferenceRotation)
+ {
+ currentRotation = CoordinateSystemManager.Instance
+ .CreateHostAdapter()
+ .FromHostQuaternionDirect(_realObjectReferenceRotation);
+ }
+ else
+ {
+ currentRotation = controlledObject.Transform.Factor().Rotation;
+ }
try
{
var actualHostPosition = GetTrackedObjectPosition(controlledObject);
currentPositionForTransform = actualHostPosition;
- LogManager.Debug(
+ LogManager.Info(
$"[动画姿态入口] {controlledObject.DisplayName} 宿主即时读回点=({actualHostPosition.X:F3},{actualHostPosition.Y:F3},{actualHostPosition.Z:F3})");
if (IsRealObjectMode &&
- (_route?.PathType == PathType.Ground ||
- _route?.PathType == PathType.Hoisting ||
- _route?.PathType == PathType.Rail) &&
- ModelItemTransformHelper.TryGetCurrentGeometryRotation(controlledObject, out actualGeometryRotation))
+ (_route?.PathType == PathType.Ground || _route?.PathType == PathType.Hoisting) &&
+ ModelItemTransformHelper.TryGetCurrentGeometryRotation(controlledObject, out var actualGeometryRotation))
{
- hasActualGeometryRotation = true;
- currentRotation = ResolveCurrentRotationBaseline(
- currentPathType,
- IsRealObjectMode,
- _hasTrackedRotation,
- _trackedRotation,
- _hasRealObjectReferenceRotation,
- referenceRotation,
- transformRotation,
- hasActualGeometryRotation,
- actualGeometryRotation);
var trackedLinear = new Transform3D(currentRotation).Linear;
var actualLinear = new Transform3D(actualGeometryRotation).Linear;
- string poseTag = _route?.PathType == PathType.Rail
- ? "[Rail姿态基线诊断]"
- : "[平面姿态基线诊断]";
- string baselineLabel = usesRealObjectOverrideRotation
- ? "当前姿态基线"
- : "当前跟踪姿态";
- LogManager.Debug(
- $"{poseTag} {controlledObject.DisplayName} {baselineLabel}: " +
+ LogManager.Info(
+ $"[平面姿态基线诊断] {controlledObject.DisplayName} 跟踪旋转: " +
$"X=({trackedLinear.Get(0, 0):F4},{trackedLinear.Get(1, 0):F4},{trackedLinear.Get(2, 0):F4}), " +
$"Y=({trackedLinear.Get(0, 1):F4},{trackedLinear.Get(1, 1):F4},{trackedLinear.Get(2, 1):F4}), " +
$"Z=({trackedLinear.Get(0, 2):F4},{trackedLinear.Get(1, 2):F4},{trackedLinear.Get(2, 2):F4})");
- LogManager.Debug(
- $"{poseTag} {controlledObject.DisplayName} 实际几何旋转: " +
+ LogManager.Info(
+ $"[平面姿态基线诊断] {controlledObject.DisplayName} 实际几何旋转: " +
$"X=({actualLinear.Get(0, 0):F4},{actualLinear.Get(1, 0):F4},{actualLinear.Get(2, 0):F4}), " +
$"Y=({actualLinear.Get(0, 1):F4},{actualLinear.Get(1, 1):F4},{actualLinear.Get(2, 1):F4}), " +
$"Z=({actualLinear.Get(0, 2):F4},{actualLinear.Get(1, 2):F4},{actualLinear.Get(2, 2):F4})");
@@ -2852,93 +2813,54 @@ namespace NavisworksTransport.Core.Animation
{
LogManager.Warning($"[动画姿态入口] 读取宿主实际状态失败: {ex.Message}");
}
- currentRotationForTransform = currentRotation;
- if (usesRealObjectOverrideRotation)
- {
- if (ModelItemTransformHelper.TryGetCurrentOverrideRotation(controlledObject, out var currentOverrideRotation))
- {
- currentRotationForTransform = currentOverrideRotation;
- }
- else
- {
- currentRotationForTransform = Rotation3D.Identity;
- }
-
- if (!ModelItemTransformHelper.TryResolveOverrideRotationForFinalTarget(
- controlledObject,
- appliedTargetRotation,
- out targetRotationForTransform))
- {
- targetRotationForTransform = appliedTargetRotation;
- }
- }
-
- if (IsRealObjectMode &&
- _route?.PathType == PathType.Ground &&
- !_suppressGroundRealObjectCompensation &&
- _hasGroundRealObjectStartCompensation &&
- _hasGroundRealObjectBasePose &&
- TryRotateGroundRealObjectCompensation(appliedTargetRotation, out var rotatedCompensation))
- {
- appliedTargetPosition = new Point3D(
- newPosition.X - rotatedCompensation.X,
- newPosition.Y - rotatedCompensation.Y,
- newPosition.Z - rotatedCompensation.Z);
-
- LogManager.Debug(
- $"[Ground路径补偿] {controlledObject.DisplayName} 原目标点=({newPosition.X:F3},{newPosition.Y:F3},{newPosition.Z:F3}), " +
- $"起点补偿=({_groundRealObjectStartCompensation.X:F3},{_groundRealObjectStartCompensation.Y:F3},{_groundRealObjectStartCompensation.Z:F3}), " +
- $"旋转后补偿=({rotatedCompensation.X:F3},{rotatedCompensation.Y:F3},{rotatedCompensation.Z:F3}), " +
- $"应用目标点=({appliedTargetPosition.X:F3},{appliedTargetPosition.Y:F3},{appliedTargetPosition.Z:F3})");
- }
-
- var hostCurrentPoseLocalAxesLinear = new Transform3D(currentRotationForTransform).Linear;
- var hostTargetDisplayPoseLocalAxesLinear = new Transform3D(appliedTargetRotation).Linear;
- LogManager.Debug(
+ var currentLinear = new Transform3D(currentRotation).Linear;
+ var targetLinear = new Transform3D(appliedTargetRotation).Linear;
+ LogManager.Info(
$"[动画姿态入口] {controlledObject.DisplayName} 跟踪点=({_trackedPosition.X:F3},{_trackedPosition.Y:F3},{_trackedPosition.Z:F3}), " +
$"目标点=({appliedTargetPosition.X:F3},{appliedTargetPosition.Y:F3},{appliedTargetPosition.Z:F3})");
- string currentRotationLabel = usesRealObjectOverrideRotation
- ? "宿主覆盖姿态基线局部轴"
- : "宿主当前跟踪姿态局部轴";
- LogManager.Debug(
- $"[动画姿态入口] {controlledObject.DisplayName} {currentRotationLabel}: " +
- $"hostCurrentLocalXAxis=({hostCurrentPoseLocalAxesLinear.Get(0, 0):F4},{hostCurrentPoseLocalAxesLinear.Get(1, 0):F4},{hostCurrentPoseLocalAxesLinear.Get(2, 0):F4}), " +
- $"hostCurrentLocalYAxis=({hostCurrentPoseLocalAxesLinear.Get(0, 1):F4},{hostCurrentPoseLocalAxesLinear.Get(1, 1):F4},{hostCurrentPoseLocalAxesLinear.Get(2, 1):F4}), " +
- $"hostCurrentLocalZAxis=({hostCurrentPoseLocalAxesLinear.Get(0, 2):F4},{hostCurrentPoseLocalAxesLinear.Get(1, 2):F4},{hostCurrentPoseLocalAxesLinear.Get(2, 2):F4})");
- LogManager.Debug(
- $"[动画姿态入口] {controlledObject.DisplayName} 宿主目标显示姿态局部轴: " +
- $"hostTargetDisplayLocalXAxis=({hostTargetDisplayPoseLocalAxesLinear.Get(0, 0):F4},{hostTargetDisplayPoseLocalAxesLinear.Get(1, 0):F4},{hostTargetDisplayPoseLocalAxesLinear.Get(2, 0):F4}), " +
- $"hostTargetDisplayLocalYAxis=({hostTargetDisplayPoseLocalAxesLinear.Get(0, 1):F4},{hostTargetDisplayPoseLocalAxesLinear.Get(1, 1):F4},{hostTargetDisplayPoseLocalAxesLinear.Get(2, 1):F4}), " +
- $"hostTargetDisplayLocalZAxis=({hostTargetDisplayPoseLocalAxesLinear.Get(0, 2):F4},{hostTargetDisplayPoseLocalAxesLinear.Get(1, 2):F4},{hostTargetDisplayPoseLocalAxesLinear.Get(2, 2):F4})");
+ LogManager.Info(
+ $"[动画姿态入口] {controlledObject.DisplayName} 跟踪姿态: " +
+ $"X=({currentLinear.Get(0, 0):F4},{currentLinear.Get(1, 0):F4},{currentLinear.Get(2, 0):F4}), " +
+ $"Y=({currentLinear.Get(0, 1):F4},{currentLinear.Get(1, 1):F4},{currentLinear.Get(2, 1):F4}), " +
+ $"Z=({currentLinear.Get(0, 2):F4},{currentLinear.Get(1, 2):F4},{currentLinear.Get(2, 2):F4})");
+ LogManager.Info(
+ $"[动画姿态入口] {controlledObject.DisplayName} 目标姿态: " +
+ $"X=({targetLinear.Get(0, 0):F4},{targetLinear.Get(1, 0):F4},{targetLinear.Get(2, 0):F4}), " +
+ $"Y=({targetLinear.Get(0, 1):F4},{targetLinear.Get(1, 1):F4},{targetLinear.Get(2, 1):F4}), " +
+ $"Z=({targetLinear.Get(0, 2):F4},{targetLinear.Get(1, 2):F4},{targetLinear.Get(2, 2):F4})");
- if (usesRealObjectOverrideRotation)
+ // 🔥 Ground 路径使用带旋转偏移补偿的新方法
+ // Rail/Hoisting 保持原行为
+ if (_route?.PathType == PathType.Ground && IsRealObjectMode)
{
- var hostTargetOverridePoseLocalAxesLinear = new Transform3D(targetRotationForTransform).Linear;
- LogManager.Debug(
- $"[真实物体覆盖姿态] {controlledObject.DisplayName} 宿主目标覆盖姿态局部轴: " +
- $"hostTargetOverrideLocalXAxis=({hostTargetOverridePoseLocalAxesLinear.Get(0, 0):F4},{hostTargetOverridePoseLocalAxesLinear.Get(1, 0):F4},{hostTargetOverridePoseLocalAxesLinear.Get(2, 0):F4}), " +
- $"hostTargetOverrideLocalYAxis=({hostTargetOverridePoseLocalAxesLinear.Get(0, 1):F4},{hostTargetOverridePoseLocalAxesLinear.Get(1, 1):F4},{hostTargetOverridePoseLocalAxesLinear.Get(2, 1):F4}), " +
- $"hostTargetOverrideLocalZAxis=({hostTargetOverridePoseLocalAxesLinear.Get(0, 2):F4},{hostTargetOverridePoseLocalAxesLinear.Get(1, 2):F4},{hostTargetOverridePoseLocalAxesLinear.Get(2, 2):F4})");
+ ModelItemTransformHelper.MoveItemIncrementallyToTrackedPositionAndRotation(
+ controlledObject,
+ currentPositionForTransform,
+ currentRotation,
+ appliedTargetPosition,
+ appliedTargetRotation);
+ }
+ else
+ {
+ ModelItemTransformHelper.MoveItemIncrementallyToPositionAndRotation(
+ controlledObject,
+ currentPositionForTransform,
+ currentRotation,
+ appliedTargetPosition,
+ appliedTargetRotation);
}
-
- ModelItemTransformHelper.MoveItemIncrementallyToPositionAndRotation(
- controlledObject,
- currentPositionForTransform,
- currentRotationForTransform,
- appliedTargetPosition,
- targetRotationForTransform);
_trackedPosition = appliedTargetPosition;
_trackedRotation = appliedTargetRotation;
_hasTrackedRotation = true;
_currentYaw = ModelItemTransformHelper.GetYawFromRotation(appliedTargetRotation);
- if (usesRealObjectOverrideRotation)
+ if (isRailRealObject)
{
Point3D hostActualAfter = GetTrackedObjectPosition(controlledObject);
LogManager.Debug(
- $"[真实物体姿态应用] 宿主最终跟踪中心=({hostActualAfter.X:F3},{hostActualAfter.Y:F3},{hostActualAfter.Z:F3}), " +
+ $"[Rail姿态应用] 宿主最终跟踪中心=({hostActualAfter.X:F3},{hostActualAfter.Y:F3},{hostActualAfter.Z:F3}), " +
$"目标跟踪中心=({newPosition.X:F3},{newPosition.Y:F3},{newPosition.Z:F3}), " +
$"偏差=({hostActualAfter.X - newPosition.X:F3},{hostActualAfter.Y - newPosition.Y:F3},{hostActualAfter.Z - newPosition.Z:F3})");
}
@@ -4355,6 +4277,27 @@ namespace NavisworksTransport.Core.Animation
return true;
}
+ private bool TryResolveGroundRealObjectBaseYaw(out double yawRadians)
+ {
+ yawRadians = 0.0;
+ if (_route?.PathType != PathType.Ground ||
+ !IsRealObjectMode ||
+ _pathPoints == null ||
+ _pathPoints.Count < 2)
+ {
+ return false;
+ }
+
+ var hostType = CoordinateSystemManager.Instance.ResolvedType;
+ var hostPoints = new List(_pathPoints.Count);
+ for (int i = 0; i < _pathPoints.Count; i++)
+ {
+ hostPoints.Add(new Vector3((float)_pathPoints[i].X, (float)_pathPoints[i].Y, (float)_pathPoints[i].Z));
+ }
+
+ return PathTargetFrameResolver.TryResolvePlanarStartHostYaw(_route.PathType, hostPoints, hostType, out yawRadians);
+ }
+
private static double NormalizeRadians(double angle)
{
while (angle > Math.PI)
diff --git a/src/Utils/ModelItemTransformHelper.cs b/src/Utils/ModelItemTransformHelper.cs
index 52f858c..dd7f7b6 100644
--- a/src/Utils/ModelItemTransformHelper.cs
+++ b/src/Utils/ModelItemTransformHelper.cs
@@ -591,6 +591,122 @@ namespace NavisworksTransport.Utils
LogIncrementalTransformActual(item, targetPosition, targetRotation);
}
+ public static void MoveItemIncrementallyToTrackedPositionAndRotation(
+ ModelItem item,
+ Point3D currentTrackedPosition,
+ Rotation3D currentRotation,
+ Point3D targetTrackedPosition,
+ Rotation3D targetRotation)
+ {
+ if (item == null)
+ {
+ throw new ArgumentNullException(nameof(item));
+ }
+
+ var doc = Application.ActiveDocument;
+ var modelItems = new ModelItemCollection { item };
+
+ Rotation3D deltaRotation = BuildDeltaRotation(currentRotation, targetRotation);
+ var currentLinear = new Transform3D(currentRotation).Linear;
+ var targetLinear = new Transform3D(targetRotation).Linear;
+
+ LogManager.Info(
+ $"[模型增量补偿] {item.DisplayName} 当前trackedPoint=({currentTrackedPosition.X:F3},{currentTrackedPosition.Y:F3},{currentTrackedPosition.Z:F3}), " +
+ $"目标trackedPoint=({targetTrackedPosition.X:F3},{targetTrackedPosition.Y:F3},{targetTrackedPosition.Z:F3})");
+ // LogManager.Debug(
+ // $"[模型增量补偿] {item.DisplayName} 当前旋转: " +
+ // $"X=({currentLinear.Get(0, 0):F4},{currentLinear.Get(1, 0):F4},{currentLinear.Get(2, 0):F4}), " +
+ // $"Y=({currentLinear.Get(0, 1):F4},{currentLinear.Get(1, 1):F4},{currentLinear.Get(2, 1):F4}), " +
+ // $"Z=({currentLinear.Get(0, 2):F4},{currentLinear.Get(1, 2):F4},{currentLinear.Get(2, 2):F4})");
+ // LogManager.Debug(
+ // $"[模型增量补偿] {item.DisplayName} 目标旋转: " +
+ // $"X=({targetLinear.Get(0, 0):F4},{targetLinear.Get(1, 0):F4},{targetLinear.Get(2, 0):F4}), " +
+ // $"Y=({targetLinear.Get(0, 1):F4},{targetLinear.Get(1, 1):F4},{targetLinear.Get(2, 1):F4}), " +
+ // $"Z=({targetLinear.Get(0, 2):F4},{targetLinear.Get(1, 2):F4},{targetLinear.Get(2, 2):F4})");
+
+ // LogGeometryLevelTransforms(item, "[模型增量补偿应用前][GeometryAPI]");
+
+ // 第1步:将物体中心移到原点(此时几何中心在原点)
+ var toOrigin = Transform3D.CreateTranslation(new Vector3D(
+ -currentTrackedPosition.X,
+ -currentTrackedPosition.Y,
+ -currentTrackedPosition.Z));
+ doc.Models.OverridePermanentTransform(modelItems, toOrigin, false);
+
+ // 第2步:在原点旋转
+ var rotationOnlyComponents = Transform3D.CreateTranslation(new Vector3D(0, 0, 0)).Factor();
+ rotationOnlyComponents.Rotation = deltaRotation;
+ var rotationOnly = rotationOnlyComponents.Combine();
+ doc.Models.OverridePermanentTransform(modelItems, rotationOnly, false);
+
+ // 🔥 关键:读取旋转后、平移前的包围盒中心(相对于几何中心的偏移)
+ Point3D centerAfterRotation = item.BoundingBox()?.Center ?? new Point3D(0, 0, 0);
+
+ // 这就是目标旋转下,BoundingBox中心相对于几何中心的偏移
+ var rotationOffset = new Vector3D(
+ centerAfterRotation.X,
+ centerAfterRotation.Y,
+ centerAfterRotation.Z);
+ LogManager.Info(
+ $"[模型增量补偿] {item.DisplayName} 旋转后包围盒中心=({centerAfterRotation.X:F3},{centerAfterRotation.Y:F3},{centerAfterRotation.Z:F3}), " +
+ $"旋转偏移量=({rotationOffset.X:F3},{rotationOffset.Y:F3},{rotationOffset.Z:F3})");
+
+ // 🔥 关键修正:我们要几何中心对齐目标,而不是 BoundingBox 中心对齐目标
+ // rotationOffset = 旋转后 BoundingBox 中心相对于原点的位置
+ // 几何中心在原点,相对于 BoundingBox 中心的偏移 = -rotationOffset
+ // 所以 BoundingBox 中心应该位于 target + rotationOffset,才能让几何中心对齐 target
+ var adjustedTargetPosition = new Point3D(
+ targetTrackedPosition.X + rotationOffset.X,
+ targetTrackedPosition.Y + rotationOffset.Y,
+ targetTrackedPosition.Z + rotationOffset.Z);
+ LogManager.Info(
+ $"[模型增量补偿] {item.DisplayName} 调整后目标位置=({adjustedTargetPosition.X:F3},{adjustedTargetPosition.Y:F3},{adjustedTargetPosition.Z:F3}), " +
+ $"补偿量=(+{rotationOffset.X:F3},+{rotationOffset.Y:F3},+{rotationOffset.Z:F3})");
+
+ // 第3步:预落位到调整后的目标位置
+ var provisionalToTarget = Transform3D.CreateTranslation(new Vector3D(
+ adjustedTargetPosition.X,
+ adjustedTargetPosition.Y,
+ adjustedTargetPosition.Z));
+ doc.Models.OverridePermanentTransform(modelItems, provisionalToTarget, false);
+
+ Point3D actualTrackedPosition = item.BoundingBox()?.Center ?? new Point3D(0, 0, 0);
+ var correctionTranslation = new Vector3D(
+ adjustedTargetPosition.X - actualTrackedPosition.X,
+ adjustedTargetPosition.Y - actualTrackedPosition.Y,
+ adjustedTargetPosition.Z - actualTrackedPosition.Z);
+
+ LogManager.Info(
+ $"[模型增量补偿] {item.DisplayName} 预落位后实际trackedPoint=({actualTrackedPosition.X:F3},{actualTrackedPosition.Y:F3},{actualTrackedPosition.Z:F3}), " +
+ $"纠偏平移=({correctionTranslation.X:F3},{correctionTranslation.Y:F3},{correctionTranslation.Z:F3})");
+
+ if (Math.Abs(correctionTranslation.X) > 1e-6 ||
+ Math.Abs(correctionTranslation.Y) > 1e-6 ||
+ Math.Abs(correctionTranslation.Z) > 1e-6)
+ {
+ var correction = Transform3D.CreateTranslation(correctionTranslation);
+ doc.Models.OverridePermanentTransform(modelItems, correction, false);
+ LogManager.Info($"[模型增量补偿] {item.DisplayName} 已应用纠偏平移");
+ }
+ else
+ {
+ LogManager.Info($"[模型增量补偿] {item.DisplayName} 纠偏平移太小,跳过应用");
+ }
+
+ // 🔥 验证纠偏后的实际位置(包围盒中心应该偏离目标,但几何中心对齐目标)
+ Point3D finalTrackedPosition = item.BoundingBox()?.Center ?? new Point3D(0, 0, 0);
+ // 几何中心 = BoundingBox中心 - rotationOffset(因为rotationOffset是BoundingBox中心相对于几何中心的偏移)
+ var geometricCenterDeviation = new Vector3D(
+ targetTrackedPosition.X - (finalTrackedPosition.X - rotationOffset.X),
+ targetTrackedPosition.Y - (finalTrackedPosition.Y - rotationOffset.Y),
+ targetTrackedPosition.Z - (finalTrackedPosition.Z - rotationOffset.Z));
+ LogManager.Info(
+ $"[模型增量补偿] {item.DisplayName} 最终包围盒中心=({finalTrackedPosition.X:F3},{finalTrackedPosition.Y:F3},{finalTrackedPosition.Z:F3}), " +
+ $"几何中心偏差=({geometricCenterDeviation.X:F3},{geometricCenterDeviation.Y:F3},{geometricCenterDeviation.Z:F3})");
+
+ // LogIncrementalTransformActual(item, targetTrackedPosition, targetRotation);
+ }
+
///
/// 读取物体当前实际几何姿态。
/// 优先使用 ModelGeometry.ActiveTransform,因为 ModelItem.Transform 只反映原始设计变换。
From 348b3a75bfca9566d251f0b67087d21c1c0a3dfe Mon Sep 17 00:00:00 2001
From: tian <11429339@qq.com>
Date: Sun, 5 Apr 2026 14:03:41 +0800
Subject: [PATCH 62/87] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=9C=B0=E9=9D=A2?=
=?UTF-8?q?=E5=80=BE=E6=96=9C=E8=B7=AF=E5=BE=84=E5=81=8F=E7=A7=BB=E7=9A=84?=
=?UTF-8?q?=E9=97=AE=E9=A2=98?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../Utils/BoundingBoxGeometryUtilsTests.cs | 400 ++++++++++++++++++
src/Core/Animation/PathAnimationManager.cs | 37 +-
src/Utils/BoundingBoxGeometryUtils.cs | 61 +++
3 files changed, 497 insertions(+), 1 deletion(-)
create mode 100644 UnitTests/Utils/BoundingBoxGeometryUtilsTests.cs
diff --git a/UnitTests/Utils/BoundingBoxGeometryUtilsTests.cs b/UnitTests/Utils/BoundingBoxGeometryUtilsTests.cs
new file mode 100644
index 0000000..a0bc7c2
--- /dev/null
+++ b/UnitTests/Utils/BoundingBoxGeometryUtilsTests.cs
@@ -0,0 +1,400 @@
+using System;
+using Autodesk.Navisworks.Api;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using NavisworksTransport.Utils;
+
+namespace NavisworksTransport.UnitTests.Utils
+{
+ ///
+ /// BoundingBoxGeometryUtils 的单元测试
+ /// 重点验证旋转后包围盒中心偏移计算的数学正确性
+ /// 使用大尺寸和夸张比例,确保结果清晰可见
+ ///
+ [TestClass]
+ public class BoundingBoxGeometryUtilsTests
+ {
+ #region CalculateRotatedBoundingBoxCenterOffset Tests - 大尺寸非对称包围盒
+
+ ///
+ /// X方向超长的长方体(类似长沙发/长桌)绕Z轴旋转90度
+ /// 尺寸: 10000 x 100 x 50
+ /// 预期: X方向大偏移
+ ///
+ [TestMethod]
+ public void CalculateRotatedBoundingBoxCenterOffset_LongX_Rotate90AroundZ_LargeXOffset()
+ {
+ // X方向从 -5000 到 +5000(总长10000)
+ // Y方向从 -50 到 +50(总长100)
+ // Z方向从 -25 到 +25(总长50)
+ var bounds = new BoundingBox3D(
+ new Point3D(-5000, -50, -25),
+ new Point3D(5000, 50, 25));
+
+ // 原中心: (0, 0, 0)
+ // 8角点: (±5000, ±50, ±25)
+
+ // 绕Z轴旋转90度 (x'=-y, y'=x):
+ // (5000, 50) -> (-50, 5000)
+ // (5000, -50) -> (50, 5000)
+ // (-5000, 50) -> (-50, -5000)
+ // (-5000, -50) -> (50, -5000)
+ //
+ // 新X范围: [-50, 50](来自原Y范围)
+ // 新Y范围: [-5000, 5000](来自原X范围)
+ // 新中心: (0, 0, 0)
+ // 偏移: (0, 0, 0) - 等等,这不应该是0
+
+ // 啊不对,让我重新算:
+ // 原中心是 (0, 0, 0)
+ // 旋转后各角点分布在 [-50,50]x[-5000,5000]x[-25,25]
+ // 新AABB中心仍然是 (0, 0, 0)
+ // 所以偏移应该是 0?
+
+ // 等等,问题在于原中心是 (0,0,0),所以新中心也是 (0,0,0)
+ // 我需要测试非对称的,即原中心不在原点的情况
+
+ // 重新设计:让包围盒中心不在原点
+ // Min=(-1000, -50, -25), Max=(9000, 50, 25)
+ // 中心: (4000, 0, 0)
+ bounds = new BoundingBox3D(
+ new Point3D(-1000, -50, -25),
+ new Point3D(9000, 50, 25));
+
+ var rotation = new Rotation3D(new UnitVector3D(0, 0, 1), Math.PI / 2);
+
+ var offset = BoundingBoxGeometryUtils.CalculateRotatedBoundingBoxCenterOffset(bounds, rotation);
+
+ // 原中心: (4000, 0, 0)
+ // 8角点相对中心: (-5000,±50,±25), (5000,±50,±25)
+ //
+ // 旋转90度后:
+ // (-5000, 50) -> (-50, -5000)
+ // (-5000, -50) -> (50, -5000)
+ // (5000, 50) -> (-50, 5000)
+ // (5000, -50) -> (50, 5000)
+ //
+ // 新X范围: [-50, 50]
+ // 新Y范围: [-5000, 5000]
+ // 新中心: (0, 0, 0)
+ // 偏移: (0-4000, 0-0, 0-0) = (-4000, 0, 0)
+
+ Console.WriteLine($"LongX-Rotate90: offset=({offset.X:F1}, {offset.Y:F1}, {offset.Z:F1})");
+
+ Assert.AreEqual(-4000, offset.X, 1, "X方向大偏移应为-4000");
+ Assert.AreEqual(0, offset.Y, 1, "Y偏移应为0");
+ Assert.AreEqual(0, offset.Z, 0.1, "Z偏移应为0");
+ }
+
+ ///
+ /// Y方向超长的长方体绕X轴旋转90度
+ /// 尺寸: 100 x 10000 x 50
+ ///
+ [TestMethod]
+ public void CalculateRotatedBoundingBoxCenterOffset_LongY_Rotate90AroundX_LargeYOffset()
+ {
+ // Y方向从 -1000 到 +9000(总长10000,中心在4000)
+ // X方向从 -50 到 +50(总长100)
+ // Z方向从 -25 到 +25(总长50)
+ var bounds = new BoundingBox3D(
+ new Point3D(-50, -1000, -25),
+ new Point3D(50, 9000, 25));
+
+ // 绕X轴旋转90度 (y'=-z, z'=y)
+ var rotation = new Rotation3D(new UnitVector3D(1, 0, 0), Math.PI / 2);
+
+ var offset = BoundingBoxGeometryUtils.CalculateRotatedBoundingBoxCenterOffset(bounds, rotation);
+
+ // 原中心: (0, 4000, 0)
+ // 8角点相对中心: (±50, -5000, ±25), (±50, 5000, ±25)
+ //
+ // 旋转90度后:
+ // y' = -z, z' = y
+ // (50, -5000, 25) -> (50, -25, -5000)
+ // (50, -5000, -25) -> (50, 25, -5000)
+ // (50, 5000, 25) -> (50, -25, 5000)
+ // (50, 5000, -25) -> (50, 25, 5000)
+ // (-50, -5000, 25) -> (-50, -25, -5000)
+ // (-50, -5000, -25) -> (-50, 25, -5000)
+ // (-50, 5000, 25) -> (-50, -25, 5000)
+ // (-50, 5000, -25) -> (-50, 25, 5000)
+ //
+ // 新X范围: [-50, 50]
+ // 新Y范围: [-25, 25]
+ // 新Z范围: [-5000, 5000]
+ // 新中心: (0, 0, 0)
+ // 偏移: (0-0, 0-4000, 0-0) = (0, -4000, 0)
+
+ Console.WriteLine($"LongY-Rotate90: offset=({offset.X:F1}, {offset.Y:F1}, {offset.Z:F1})");
+
+ Assert.AreEqual(0, offset.X, 1, "X偏移应为0");
+ Assert.AreEqual(-4000, offset.Y, 1, "Y方向大偏移应为-4000");
+ Assert.AreEqual(0, offset.Z, 0.1, "Z偏移应为0");
+ }
+
+ ///
+ /// Z方向超长的长方体绕Y轴旋转90度
+ /// 尺寸: 100 x 100 x 10000
+ ///
+ [TestMethod]
+ public void CalculateRotatedBoundingBoxCenterOffset_LongZ_Rotate90AroundY_LargeZOffset()
+ {
+ // Z方向从 -1000 到 +9000(总长10000,中心在4000)
+ // X方向从 -50 到 +50(总长100)
+ // Y方向从 -50 到 +50(总长100)
+ var bounds = new BoundingBox3D(
+ new Point3D(-50, -50, -1000),
+ new Point3D(50, 50, 9000));
+
+ // 绕Y轴旋转90度 (z'=x, x'=-z) - 等等,让我确认旋转矩阵
+ // 绕Y轴旋转θ: x'=x*cosθ+z*sinθ, z'=-x*sinθ+z*cosθ
+ // 90度时: x'=z, z'=-x
+ var rotation = new Rotation3D(new UnitVector3D(0, 1, 0), Math.PI / 2);
+
+ var offset = BoundingBoxGeometryUtils.CalculateRotatedBoundingBoxCenterOffset(bounds, rotation);
+
+ // 原中心: (0, 0, 4000)
+ // 8角点相对中心: (±50,±50,-5000), (±50,±50,5000)
+ //
+ // 旋转90度后 (x'=z, z'=-x):
+ // (50, 50, -5000) -> (-5000, 50, -50)
+ // (50, 50, 5000) -> (5000, 50, -50)
+ // (-50, 50, -5000) -> (-5000, 50, 50)
+ // (-50, 50, 5000) -> (5000, 50, 50)
+ // 以及Y=-50的四个点...
+ //
+ // 新X范围: [-5000, 5000]
+ // 新Y范围: [-50, 50]
+ // 新Z范围: [-50, 50]
+ // 新中心: (0, 0, 0)
+ // 偏移: (0-0, 0-0, 0-4000) = (0, 0, -4000)
+
+ Console.WriteLine($"LongZ-Rotate90: offset=({offset.X:F1}, {offset.Y:F1}, {offset.Z:F1})");
+
+ Assert.AreEqual(0, offset.X, 1, "X偏移应为0");
+ Assert.AreEqual(0, offset.Y, 0.1, "Y偏移应为0");
+ Assert.AreEqual(-4000, offset.Z, 1, "Z方向大偏移应为-4000");
+ }
+
+ ///
+ /// 三轴都不同比例的长方体绕Z轴旋转45度
+ /// 尺寸: 1000 x 200 x 100
+ ///
+ [TestMethod]
+ public void CalculateRotatedBoundingBoxCenterOffset_AsymmetricXYZ_Rotate45AroundZ_MultipleOffsets()
+ {
+ // X: -500 ~ +500 (中心0,但非对称也可测)
+ // Y: -100 ~ +100 (中心0)
+ // Z: -50 ~ +50 (中心0)
+ // 等等,中心0的话偏移还是0...
+
+ // 改用非中心对称的
+ // X: -100 ~ +900 (总长1000,中心400)
+ // Y: -80 ~ +120 (总长200,中心20)
+ // Z: -30 ~ +70 (总长100,中心20)
+ var bounds = new BoundingBox3D(
+ new Point3D(-100, -80, -30),
+ new Point3D(900, 120, 70));
+
+ // 绕Z轴旋转45度
+ var rotation = new Rotation3D(new UnitVector3D(0, 0, 1), Math.PI / 4);
+
+ var offset = BoundingBoxGeometryUtils.CalculateRotatedBoundingBoxCenterOffset(bounds, rotation);
+
+ // 原中心: (400, 20, 20)
+ //
+ // 8角点相对中心:
+ // (-500,-100,-50), (500,-100,-50), (-500,100,-50), (500,100,-50)
+ // (-500,-100,50), (500,-100,50), (-500,100,50), (500,100,50)
+ //
+ // 旋转45度后 (x'=(x-y)/√2, y'=(x+y)/√2):
+ // (-500,-100) -> (-400/√2, -600/√2) ≈ (-283, -424)
+ // (500,-100) -> (600/√2, 400/√2) ≈ (424, 283)
+ // (-500,100) -> (-600/√2, -400/√2) ≈ (-424, -283)
+ // (500,100) -> (400/√2, 600/√2) ≈ (283, 424)
+ //
+ // 新X范围: [-424, 424]
+ // 新Y范围: [-424, 424]
+ // 新中心: (0, 0, 20) - Z不变
+ // 偏移: (0-400, 0-20, 20-20) = (-400, -20, 0)
+
+ Console.WriteLine($"Asymmetric-Rotate45: offset=({offset.X:F1}, {offset.Y:F1}, {offset.Z:F1})");
+
+ // 允许一定误差(因为手动计算是近似值)
+ Assert.IsTrue(Math.Abs(offset.X - (-400)) < 50, $"X偏移应约-400,实际是{offset.X:F1}");
+ Assert.IsTrue(Math.Abs(offset.Y - (-20)) < 50, $"Y偏移应约-20,实际是{offset.Y:F1}");
+ Assert.AreEqual(0, offset.Z, 0.1, "Z偏移应为0");
+ }
+
+ ///
+ /// 实际沙发案例放大版
+ /// 模拟沙发: 长2000,宽100,高80,中心偏移
+ ///
+ [TestMethod]
+ public void CalculateRotatedBoundingBoxCenterOffset_BigSofa_Rotate90AroundZ_RealisticCase()
+ {
+ // 沙发包围盒放大版
+ // X: -200 ~ +1800 (总长2000,中心800)
+ // Y: -50 ~ +50 (总长100,中心0)
+ // Z: 0 ~ +80 (总高80,中心40)
+ var bounds = new BoundingBox3D(
+ new Point3D(-200, -50, 0),
+ new Point3D(1800, 50, 80));
+
+ var rotation = new Rotation3D(new UnitVector3D(0, 0, 1), Math.PI / 2);
+
+ var offset = BoundingBoxGeometryUtils.CalculateRotatedBoundingBoxCenterOffset(bounds, rotation);
+
+ // 原中心: (800, 0, 40)
+ //
+ // 8角点相对中心:
+ // (-1000,-50,-40), (1000,-50,-40), (-1000,50,-40), (1000,50,-40)
+ // (-1000,-50,40), (1000,-50,40), (-1000,50,40), (1000,50,40)
+ //
+ // 旋转90度后 (x'=-y, y'=x):
+ // (-1000,-50) -> (50, -1000)
+ // (1000,-50) -> (50, 1000)
+ // (-1000,50) -> (-50, -1000)
+ // (1000,50) -> (-50, 1000)
+ //
+ // 新X范围: [-50, 50]
+ // 新Y范围: [-1000, 1000]
+ // 新中心: (0, 0, 40) - Z不变
+ // 偏移: (0-800, 0-0, 40-40) = (-800, 0, 0)
+
+ Console.WriteLine($"BigSofa-Rotate90: offset=({offset.X:F1}, {offset.Y:F1}, {offset.Z:F1})");
+
+ Assert.AreEqual(-800, offset.X, 10, "X方向偏移应为-800");
+ Assert.AreEqual(0, offset.Y, 10, "Y偏移应为0");
+ Assert.AreEqual(0, offset.Z, 0.1, "Z偏移应为0");
+ }
+
+ ///
+ /// X方向超长绕Z轴旋转45度 - 测试非90度旋转
+ /// 尺寸: 10000 x 100 x 50
+ ///
+ [TestMethod]
+ public void CalculateRotatedBoundingBoxCenterOffset_LongX_Rotate45AroundZ_DiagonalOffset()
+ {
+ // X方向从 -1000 到 +9000(总长10000,中心4000)
+ // Y方向从 -50 到 +50(总长100)
+ var bounds = new BoundingBox3D(
+ new Point3D(-1000, -50, -25),
+ new Point3D(9000, 50, 25));
+
+ // 绕Z轴旋转45度
+ var rotation = new Rotation3D(new UnitVector3D(0, 0, 1), Math.PI / 4);
+
+ var offset = BoundingBoxGeometryUtils.CalculateRotatedBoundingBoxCenterOffset(bounds, rotation);
+
+ // 原中心: (4000, 0, 0)
+ // 8角点相对中心: (-5000,±50,±25), (5000,±50,±25)
+ //
+ // 旋转45度后 (x'=(x-y)/√2, y'=(x+y)/√2):
+ // (-5000, 50) -> (-5050/√2, -4950/√2) ≈ (-3571, -3500)
+ // (-5000, -50) -> (-4950/√2, -5050/√2) ≈ (-3500, -3571)
+ // (5000, 50) -> (4950/√2, 5050/√2) ≈ (3500, 3571)
+ // (5000, -50) -> (5050/√2, 4950/√2) ≈ (3571, 3500)
+ //
+ // 新X范围: [-3571, 3571]
+ // 新Y范围: [-3571, 3571]
+ // 新中心: (0, 0, 0)
+ // 偏移: (0-4000, 0-0, 0-0) = (-4000, 0, 0)
+
+ Console.WriteLine($"LongX-Rotate45: offset=({offset.X:F1}, {offset.Y:F1}, {offset.Z:F1})");
+
+ // X方向应该有较大负偏移(接近-4000)
+ Assert.IsTrue(offset.X < -3500, $"45度旋转后X偏移应小于-3500,实际是{offset.X:F1}");
+ Assert.IsTrue(offset.X > -4500, $"45度旋转后X偏移应大于-4500,实际是{offset.X:F1}");
+ // Y方向偏移应该很小(因为原Y中心是0)
+ Assert.IsTrue(Math.Abs(offset.Y) < 100, $"45度旋转后Y偏移应接近0,实际是{offset.Y:F1}");
+ Assert.AreEqual(0, offset.Z, 0.1, "Z偏移应为0");
+ }
+
+ ///
+ /// XY方向都非对称的长方体绕Z轴旋转45度 - 测试双向偏移
+ /// 尺寸: 8000 x 4000 x 100
+ ///
+ [TestMethod]
+ public void CalculateRotatedBoundingBoxCenterOffset_XYBothAsymmetric_Rotate45AroundZ_BothOffsets()
+ {
+ // X方向从 -3000 到 +5000(总长8000,中心2000)
+ // Y方向从 -1000 到 +3000(总长4000,中心1000)
+ var bounds = new BoundingBox3D(
+ new Point3D(-3000, -1000, -50),
+ new Point3D(5000, 3000, 50));
+
+ // 绕Z轴旋转45度
+ var rotation = new Rotation3D(new UnitVector3D(0, 0, 1), Math.PI / 4);
+
+ var offset = BoundingBoxGeometryUtils.CalculateRotatedBoundingBoxCenterOffset(bounds, rotation);
+
+ // 原中心: (2000, 1000, 0)
+ // 8角点相对中心: (-5000,-2000,±50), (3000,-2000,±50), (-5000,2000,±50), (3000,2000,±50)
+ //
+ // 旋转45度后:
+ // (-5000, -2000) -> (-3000/√2, -7000/√2) ≈ (-2121, -4950)
+ // (3000, -2000) -> (5000/√2, 1000/√2) ≈ (3536, 707)
+ // (-5000, 2000) -> (-7000/√2, -3000/√2) ≈ (-4950, -2121)
+ // (3000, 2000) -> (1000/√2, 5000/√2) ≈ (707, 3536)
+ //
+ // 新X范围: [-4950, 3536]
+ // 新Y范围: [-4950, 3536]
+ // 新中心: ((-4950+3536)/2, (-4950+3536)/2, 0) = (-707, -707, 0)
+ // 偏移: (-707-2000, -707-1000, 0-0) = (-2707, -1707, 0)
+
+ Console.WriteLine($"XY-Asymmetric-Rotate45: offset=({offset.X:F1}, {offset.Y:F1}, {offset.Z:F1})");
+
+ // X方向应该有较大负偏移
+ Assert.IsTrue(offset.X < -2000, $"45度旋转后X偏移应小于-2000,实际是{offset.X:F1}");
+ Assert.IsTrue(offset.X > -3500, $"45度旋转后X偏移应大于-3500,实际是{offset.X:F1}");
+ // Y方向也应该有负偏移,但比X小
+ Assert.IsTrue(offset.Y < -1000, $"45度旋转后Y偏移应小于-1000,实际是{offset.Y:F1}");
+ Assert.IsTrue(offset.Y > -2500, $"45度旋转后Y偏移应大于-2500,实际是{offset.Y:F1}");
+ Assert.AreEqual(0, offset.Z, 0.1, "Z偏移应为0");
+ }
+
+ ///
+ /// 零旋转验证 - 无论什么形状,不旋转偏移都应为0
+ ///
+ [TestMethod]
+ public void CalculateRotatedBoundingBoxCenterOffset_NoRotation_ZeroOffset()
+ {
+ var bounds = new BoundingBox3D(
+ new Point3D(-1000, -100, -50),
+ new Point3D(9000, 100, 50));
+
+ var rotation = Rotation3D.Identity;
+
+ var offset = BoundingBoxGeometryUtils.CalculateRotatedBoundingBoxCenterOffset(bounds, rotation);
+
+ Console.WriteLine($"No-Rotation: offset=({offset.X:F1}, {offset.Y:F1}, {offset.Z:F1})");
+
+ Assert.AreEqual(0, offset.X, 1e-6, "无旋转时X偏移应为0");
+ Assert.AreEqual(0, offset.Y, 1e-6, "无旋转时Y偏移应为0");
+ Assert.AreEqual(0, offset.Z, 1e-6, "无旋转时Z偏移应为0");
+ }
+
+ ///
+ /// 180度旋转验证 - 包围盒应该不变(只是翻转)
+ ///
+ [TestMethod]
+ public void CalculateRotatedBoundingBoxCenterOffset_180DegreeRotation_ZeroOffset()
+ {
+ var bounds = new BoundingBox3D(
+ new Point3D(-1000, -100, -50),
+ new Point3D(9000, 100, 50));
+
+ var rotation = new Rotation3D(new UnitVector3D(0, 0, 1), Math.PI);
+
+ var offset = BoundingBoxGeometryUtils.CalculateRotatedBoundingBoxCenterOffset(bounds, rotation);
+
+ Console.WriteLine($"180-Rotation: offset=({offset.X:F1}, {offset.Y:F1}, {offset.Z:F1})");
+
+ Assert.AreEqual(0, offset.X, 1e-6, "180度旋转后X偏移应为0");
+ Assert.AreEqual(0, offset.Y, 1e-6, "180度旋转后Y偏移应为0");
+ Assert.AreEqual(0, offset.Z, 1e-6, "180度旋转后Z偏移应为0");
+ }
+
+ #endregion
+ }
+}
diff --git a/src/Core/Animation/PathAnimationManager.cs b/src/Core/Animation/PathAnimationManager.cs
index 5c49d14..61754d8 100644
--- a/src/Core/Animation/PathAnimationManager.cs
+++ b/src/Core/Animation/PathAnimationManager.cs
@@ -5562,7 +5562,15 @@ namespace NavisworksTransport.Core.Animation
if (frameData.HasCustomRotation)
{
- UpdateObjectPosition(frameData.Position, frameData.Rotation);
+ // 🔥 Ground路径动画播放:使用 _trackedPosition 计算增量,不重新读取包围盒
+ if (_route?.PathType == PathType.Ground && IsRealObjectMode)
+ {
+ ApplyGroundAnimationFrame(frameData.Position, frameData.Rotation);
+ }
+ else
+ {
+ UpdateObjectPosition(frameData.Position, frameData.Rotation);
+ }
}
else if (_route?.PathType == PathType.Ground || _route?.PathType == PathType.Hoisting)
{
@@ -5575,6 +5583,33 @@ namespace NavisworksTransport.Core.Animation
}
}
+ ///
+ /// 专门用于Ground路径动画帧播放。使用 _trackedPosition 作为当前位置,不重新读取包围盒。
+ ///
+ private void ApplyGroundAnimationFrame(Point3D targetPosition, Rotation3D targetRotation)
+ {
+ var controlledObject = CurrentControlledObject;
+ if (controlledObject == null) return;
+
+ // 使用 _trackedPosition 作为当前位置(几何中心),不重新读取包围盒
+ Point3D currentPosition = _trackedPosition;
+ Rotation3D currentRotation = _hasTrackedRotation ? _trackedRotation : Rotation3D.Identity;
+
+ // 简单增量变换,不使用包围盒
+ ModelItemTransformHelper.MoveItemIncrementallyToPositionAndRotation(
+ controlledObject,
+ currentPosition,
+ currentRotation,
+ targetPosition,
+ targetRotation);
+
+ // 更新跟踪状态
+ _trackedPosition = targetPosition;
+ _trackedRotation = targetRotation;
+ _hasTrackedRotation = true;
+ _currentYaw = ModelItemTransformHelper.GetYawFromRotation(targetRotation);
+ }
+
///
/// 更新FPS计数器(DispatcherTimer模式)
diff --git a/src/Utils/BoundingBoxGeometryUtils.cs b/src/Utils/BoundingBoxGeometryUtils.cs
index 8c29671..3fd3d42 100644
--- a/src/Utils/BoundingBoxGeometryUtils.cs
+++ b/src/Utils/BoundingBoxGeometryUtils.cs
@@ -272,5 +272,66 @@ namespace NavisworksTransport.Utils
new Point3D(minX, minY, minZ),
new Point3D(maxX, maxY, maxZ));
}
+
+ ///
+ /// 计算包围盒旋转后,新AABB中心相对于原中心的偏移。
+ /// 这是解决"旋转后BoundingBox中心漂移"问题的核心数学方法。
+ ///
+ /// [备注] 此方法仅供参考,当前代码中未使用。保留作为数学工具方法。
+ ///
+ /// 原始包围盒
+ /// 旋转(世界坐标系)
+ /// 新AABB中心相对于原中心的偏移量
+ public static Vector3D CalculateRotatedBoundingBoxCenterOffset(BoundingBox3D originalBounds, Rotation3D rotation)
+ {
+ // 原包围盒中心
+ Point3D originalCenter = new Point3D(
+ (originalBounds.Min.X + originalBounds.Max.X) / 2.0,
+ (originalBounds.Min.Y + originalBounds.Max.Y) / 2.0,
+ (originalBounds.Min.Z + originalBounds.Max.Z) / 2.0);
+
+ // 旋转矩阵
+ var linear = new Transform3D(rotation).Linear;
+
+ // 原始包围盒的8个角点(相对于原中心)
+ var corners = new[]
+ {
+ new Point3D(originalBounds.Min.X - originalCenter.X, originalBounds.Min.Y - originalCenter.Y, originalBounds.Min.Z - originalCenter.Z),
+ new Point3D(originalBounds.Max.X - originalCenter.X, originalBounds.Min.Y - originalCenter.Y, originalBounds.Min.Z - originalCenter.Z),
+ new Point3D(originalBounds.Min.X - originalCenter.X, originalBounds.Max.Y - originalCenter.Y, originalBounds.Min.Z - originalCenter.Z),
+ new Point3D(originalBounds.Max.X - originalCenter.X, originalBounds.Max.Y - originalCenter.Y, originalBounds.Min.Z - originalCenter.Z),
+ new Point3D(originalBounds.Min.X - originalCenter.X, originalBounds.Min.Y - originalCenter.Y, originalBounds.Max.Z - originalCenter.Z),
+ new Point3D(originalBounds.Max.X - originalCenter.X, originalBounds.Min.Y - originalCenter.Y, originalBounds.Max.Z - originalCenter.Z),
+ new Point3D(originalBounds.Min.X - originalCenter.X, originalBounds.Max.Y - originalCenter.Y, originalBounds.Max.Z - originalCenter.Z),
+ new Point3D(originalBounds.Max.X - originalCenter.X, originalBounds.Max.Y - originalCenter.Y, originalBounds.Max.Z - originalCenter.Z)
+ };
+
+ // 计算旋转后的角点,并求新AABB
+ double minX = double.MaxValue, minY = double.MaxValue, minZ = double.MaxValue;
+ double maxX = double.MinValue, maxY = double.MinValue, maxZ = double.MinValue;
+
+ foreach (var corner in corners)
+ {
+ // 应用旋转
+ double rx = linear.Get(0, 0) * corner.X + linear.Get(0, 1) * corner.Y + linear.Get(0, 2) * corner.Z;
+ double ry = linear.Get(1, 0) * corner.X + linear.Get(1, 1) * corner.Y + linear.Get(1, 2) * corner.Z;
+ double rz = linear.Get(2, 0) * corner.X + linear.Get(2, 1) * corner.Y + linear.Get(2, 2) * corner.Z;
+
+ minX = Math.Min(minX, rx);
+ minY = Math.Min(minY, ry);
+ minZ = Math.Min(minZ, rz);
+ maxX = Math.Max(maxX, rx);
+ maxY = Math.Max(maxY, ry);
+ maxZ = Math.Max(maxZ, rz);
+ }
+
+ // 新AABB中心(相对于原中心)
+ Vector3D newCenterOffset = new Vector3D(
+ (minX + maxX) / 2.0,
+ (minY + maxY) / 2.0,
+ (minZ + maxZ) / 2.0);
+
+ return newCenterOffset;
+ }
}
}
\ No newline at end of file
From 8a7563a4feca12e348bdca9806a812f9ff2f45e2 Mon Sep 17 00:00:00 2001
From: tian <11429339@qq.com>
Date: Mon, 6 Apr 2026 08:55:53 +0800
Subject: [PATCH 63/87] Clarify ground tracked-point semantics
---
AGENTS.md | 10 ++
.../RailPreservedPoseTests.cs | 17 +++
doc/working/current-engineering-state.md | 15 ++-
src/Core/Animation/PathAnimationManager.cs | 94 ++++++++------
src/Utils/ModelItemTransformHelper.cs | 116 ------------------
5 files changed, 94 insertions(+), 158 deletions(-)
diff --git a/AGENTS.md b/AGENTS.md
index 8851250..6eb1690 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -296,6 +296,16 @@ var rotation = new Rotation3D(qw, qx, qy, qz); // 错误
- 走平面姿态链
- 已禁止偷偷退回旧 `yaw` 方案
- 起点、逐帧、终点、通行空间,必须共享同一套尺寸语义
+- `Ground + 真实物体` 的业务跟踪点固定为**原始包围盒中心**
+- 旋转后的实时 `BoundingBox.Center` 只允许用于:
+ - 诊断旋转后漂移
+ - 计算当帧补偿
+ - 校验补偿结果
+- 不允许把实时 `BoundingBox.Center` 直接当成:
+ - 起点目标跟踪点
+ - 逐帧业务跟踪点
+ - 碰撞记录主语义跟踪点
+- 如果 `Ground` 出现“起点正确但越走越偏”或“起点偏了但拐弯后偏差反而减小”,优先检查是否把实时包围盒中心误当成了业务跟踪点
#### Rail
diff --git a/UnitTests/CoordinateSystem/RailPreservedPoseTests.cs b/UnitTests/CoordinateSystem/RailPreservedPoseTests.cs
index 1e8326a..f416c13 100644
--- a/UnitTests/CoordinateSystem/RailPreservedPoseTests.cs
+++ b/UnitTests/CoordinateSystem/RailPreservedPoseTests.cs
@@ -149,6 +149,23 @@ namespace NavisworksTransport.UnitTests.CoordinateSystem
Assert.IsFalse(PathAnimationManager.ShouldUseRealObjectOverrideRotation(false));
}
+ [TestMethod]
+ public void ShouldUseOriginalBoundingBoxCenterForBusinessTrackedPosition_ShouldEnableOnlyForGroundRealObjects()
+ {
+ Assert.IsTrue(PathAnimationManager.ShouldUseOriginalBoundingBoxCenterForBusinessTrackedPosition(
+ PathType.Ground,
+ isRealObjectMode: true));
+ Assert.IsFalse(PathAnimationManager.ShouldUseOriginalBoundingBoxCenterForBusinessTrackedPosition(
+ PathType.Ground,
+ isRealObjectMode: false));
+ Assert.IsFalse(PathAnimationManager.ShouldUseOriginalBoundingBoxCenterForBusinessTrackedPosition(
+ PathType.Hoisting,
+ isRealObjectMode: true));
+ Assert.IsFalse(PathAnimationManager.ShouldUseOriginalBoundingBoxCenterForBusinessTrackedPosition(
+ PathType.Rail,
+ isRealObjectMode: true));
+ }
+
[TestMethod]
public void ResolveHoistingPoseSourceLabel_ShouldDescribeCachedBasePose()
{
diff --git a/doc/working/current-engineering-state.md b/doc/working/current-engineering-state.md
index 7869750..48f90f7 100644
--- a/doc/working/current-engineering-state.md
+++ b/doc/working/current-engineering-state.md
@@ -111,8 +111,19 @@
- `AnimatedObjectTrackedPosition` 的唯一语义:
- 当前动画主链路使用的跟踪点位置。
-- 当前主链路已经切到:
- - 几何中心跟踪。
+- `Ground + 真实物体` 当前必须明确区分两类“中心”:
+ - 业务跟踪点应固定为物体**原始包围盒中心**
+ - 不是旋转后的实时包围盒中心
+- 实时包围盒中心的用途仅限于:
+ - 诊断旋转后漂移
+ - 计算当帧补偿
+ - 验证补偿结果
+- 不允许把实时包围盒中心直接回写成 `Ground` 路径的业务跟踪点定义。
+- 这条规则的直接影响范围包括:
+ - 起点落位
+ - 逐帧补偿
+ - 动画日志
+ - 碰撞与结果记录语义
- 如果后续动画跟踪点再变,数据库和碰撞结果语义必须同步更新。
### 4.4 虚拟物体资源
diff --git a/src/Core/Animation/PathAnimationManager.cs b/src/Core/Animation/PathAnimationManager.cs
index 61754d8..8cc6b3a 100644
--- a/src/Core/Animation/PathAnimationManager.cs
+++ b/src/Core/Animation/PathAnimationManager.cs
@@ -186,7 +186,7 @@ namespace NavisworksTransport.Core.Animation
private Transform3D _originalTransform;
private Point3D _originalCenter; // 存储部件的原始中心位置
- private Point3D _trackedPosition; // 动画管理器内部跟踪的位置状态,不等于宿主即时读回位置
+ private Point3D _trackedPosition; // 动画管理器内部业务跟踪点,不等于宿主即时读回的实时包围盒中心
private AnimationState _currentState = AnimationState.Idle;
private double _pausedProgress = 0.0; // 暂停时的进度(0-1之间)
private double _currentYaw = 0.0; // 当前偏航角(弧度)
@@ -484,7 +484,7 @@ namespace NavisworksTransport.Core.Animation
}
var originalBoundingBox = objectToRestore.BoundingBox();
- _trackedPosition = GetTrackedObjectPosition(objectToRestore);
+ _trackedPosition = ResolveInitialBusinessTrackedPosition(objectToRestore);
string objectName = isVirtual ? "虚拟物体" : objectToRestore.DisplayName;
_groundRealObjectStartCompensation = new Vector3D(0, 0, 0);
@@ -525,7 +525,7 @@ namespace NavisworksTransport.Core.Animation
{
_originalTransform = animatedObject.Transform;
_originalCenter = animatedObject.BoundingBox().Center;
- _trackedPosition = GetTrackedObjectPosition(animatedObject);
+ _trackedPosition = ResolveInitialBusinessTrackedPosition(animatedObject);
SyncTrackedRotationToObjectReference(animatedObject, isVirtualObject: false);
}
}
@@ -689,7 +689,7 @@ namespace NavisworksTransport.Core.Animation
ResetRealObjectReferenceRotation();
_originalTransform = animatedObject.Transform;
_originalCenter = animatedObject.BoundingBox().Center;
- _trackedPosition = GetTrackedObjectPosition(animatedObject);
+ _trackedPosition = ResolveInitialBusinessTrackedPosition(animatedObject);
if (isVirtualObject)
{
_currentYaw = ModelItemTransformHelper.GetYawFromTransform(_originalTransform);
@@ -808,12 +808,12 @@ namespace NavisworksTransport.Core.Animation
UpdateObjectPosition(startPosition, planarRotation);
LogManager.Info("[移动到起点] 地面/吊装路径已应用完整三维姿态");
- var startAppliedPoint = GetTrackedObjectPosition(CurrentControlledObject ?? _animatedObject);
+ var startAppliedPoint = GetLiveBoundingBoxCenter(CurrentControlledObject ?? _animatedObject);
LogManager.Info(
- $"[路径起点诊断] 起点应用后实际trackedPoint=({startAppliedPoint.X:F3},{startAppliedPoint.Y:F3},{startAppliedPoint.Z:F3}), " +
+ $"[路径起点诊断] 起点应用后实际包围盒中心=({startAppliedPoint.X:F3},{startAppliedPoint.Y:F3},{startAppliedPoint.Z:F3}), " +
$"相对目标偏差=({startAppliedPoint.X - startPosition.X:F3},{startAppliedPoint.Y - startPosition.Y:F3},{startAppliedPoint.Z - startPosition.Z:F3})");
- // 🔥 起点不再进行二次补偿,旋转偏移由 MoveItemIncrementallyToTrackedPositionAndRotation 内部处理
+ // Ground 起点不再进行二次补偿;业务跟踪点语义由 _trackedPosition 主链负责
if (IsRealObjectMode && _route.PathType == PathType.Ground)
{
var startOffset = new Vector3D(
@@ -875,9 +875,9 @@ namespace NavisworksTransport.Core.Animation
// 使用 UpdateObjectPosition 统一处理移动和旋转
UpdateObjectPosition(startPosition, yaw);
- var startAppliedPoint = GetTrackedObjectPosition(CurrentControlledObject ?? _animatedObject);
+ var startAppliedPoint = GetLiveBoundingBoxCenter(CurrentControlledObject ?? _animatedObject);
LogManager.Info(
- $"[路径起点诊断] 起点应用后实际trackedPoint=({startAppliedPoint.X:F3},{startAppliedPoint.Y:F3},{startAppliedPoint.Z:F3}), " +
+ $"[路径起点诊断] 起点应用后实际包围盒中心=({startAppliedPoint.X:F3},{startAppliedPoint.Y:F3},{startAppliedPoint.Z:F3}), " +
$"相对目标偏差=({startAppliedPoint.X - startPosition.X:F3},{startAppliedPoint.Y - startPosition.Y:F3},{startAppliedPoint.Z - startPosition.Z:F3})");
}
@@ -922,7 +922,7 @@ namespace NavisworksTransport.Core.Animation
ResetRealObjectReferenceRotation();
_originalTransform = animatedObject.Transform;
_originalCenter = animatedObject.BoundingBox().Center;
- _trackedPosition = GetTrackedObjectPosition(animatedObject);
+ _trackedPosition = ResolveInitialBusinessTrackedPosition(animatedObject);
if (isVirtualObject)
{
_currentYaw = ModelItemTransformHelper.GetYawFromTransform(_originalTransform);
@@ -989,11 +989,11 @@ namespace NavisworksTransport.Core.Animation
_groundRealObjectStartCompensation = new Vector3D(0, 0, 0);
_hasGroundRealObjectStartCompensation = false;
- var startAppliedPoint = GetTrackedObjectPosition(CurrentControlledObject ?? _animatedObject);
+ var startAppliedPoint = GetLiveBoundingBoxCenter(CurrentControlledObject ?? _animatedObject);
LogManager.Info(
$"[平移到起点] 已按终点原始位姿残差搬运到起点: 路径point0=({pathStartPoint.X:F3},{pathStartPoint.Y:F3},{pathStartPoint.Z:F3}), " +
$"目标trackedPoint=({startPosition.X:F3},{startPosition.Y:F3},{startPosition.Z:F3}), " +
- $"实际trackedPoint=({startAppliedPoint.X:F3},{startAppliedPoint.Y:F3},{startAppliedPoint.Z:F3}), 路径类型={_route.PathType.GetDisplayName()}");
+ $"实际包围盒中心=({startAppliedPoint.X:F3},{startAppliedPoint.Y:F3},{startAppliedPoint.Z:F3}), 路径类型={_route.PathType.GetDisplayName()}");
return true;
}
catch (Exception ex)
@@ -2786,8 +2786,11 @@ namespace NavisworksTransport.Core.Animation
try
{
- var actualHostPosition = GetTrackedObjectPosition(controlledObject);
- currentPositionForTransform = actualHostPosition;
+ var actualHostPosition = GetLiveBoundingBoxCenter(controlledObject);
+ if (!(IsRealObjectMode && _route?.PathType == PathType.Ground))
+ {
+ currentPositionForTransform = actualHostPosition;
+ }
LogManager.Info(
$"[动画姿态入口] {controlledObject.DisplayName} 宿主即时读回点=({actualHostPosition.X:F3},{actualHostPosition.Y:F3},{actualHostPosition.Z:F3})");
@@ -2830,26 +2833,12 @@ namespace NavisworksTransport.Core.Animation
$"Y=({targetLinear.Get(0, 1):F4},{targetLinear.Get(1, 1):F4},{targetLinear.Get(2, 1):F4}), " +
$"Z=({targetLinear.Get(0, 2):F4},{targetLinear.Get(1, 2):F4},{targetLinear.Get(2, 2):F4})");
- // 🔥 Ground 路径使用带旋转偏移补偿的新方法
- // Rail/Hoisting 保持原行为
- if (_route?.PathType == PathType.Ground && IsRealObjectMode)
- {
- ModelItemTransformHelper.MoveItemIncrementallyToTrackedPositionAndRotation(
- controlledObject,
- currentPositionForTransform,
- currentRotation,
- appliedTargetPosition,
- appliedTargetRotation);
- }
- else
- {
- ModelItemTransformHelper.MoveItemIncrementallyToPositionAndRotation(
- controlledObject,
- currentPositionForTransform,
- currentRotation,
- appliedTargetPosition,
- appliedTargetRotation);
- }
+ ModelItemTransformHelper.MoveItemIncrementallyToPositionAndRotation(
+ controlledObject,
+ currentPositionForTransform,
+ currentRotation,
+ appliedTargetPosition,
+ appliedTargetRotation);
_trackedPosition = appliedTargetPosition;
_trackedRotation = appliedTargetRotation;
@@ -2858,7 +2847,7 @@ namespace NavisworksTransport.Core.Animation
if (isRailRealObject)
{
- Point3D hostActualAfter = GetTrackedObjectPosition(controlledObject);
+ Point3D hostActualAfter = GetLiveBoundingBoxCenter(controlledObject);
LogManager.Debug(
$"[Rail姿态应用] 宿主最终跟踪中心=({hostActualAfter.X:F3},{hostActualAfter.Y:F3},{hostActualAfter.Z:F3}), " +
$"目标跟踪中心=({newPosition.X:F3},{newPosition.Y:F3},{newPosition.Z:F3}), " +
@@ -3797,7 +3786,7 @@ namespace NavisworksTransport.Core.Animation
{
_originalTransform = animatedObject.Transform;
_originalCenter = animatedObject.BoundingBox().Center;
- _trackedPosition = GetTrackedObjectPosition(animatedObject);
+ _trackedPosition = ResolveInitialBusinessTrackedPosition(animatedObject);
// 保持当前的 _currentYaw(因为物体可能已经被 MoveObjectToPathStart 旋转)
// 不要从 Transform 中提取,因为 Transform 返回的是原始值,不是当前值
@@ -3814,7 +3803,7 @@ namespace NavisworksTransport.Core.Animation
LogManager.Info($"[CreateAnimation] 动画已创建,状态设置为Ready");
}
- private Point3D GetTrackedObjectPosition(ModelItem item)
+ private Point3D GetLiveBoundingBoxCenter(ModelItem item)
{
if (item == null)
{
@@ -3825,6 +3814,24 @@ namespace NavisworksTransport.Core.Animation
return bounds?.Center ?? new Point3D(0, 0, 0);
}
+ private Point3D ResolveInitialBusinessTrackedPosition(ModelItem item)
+ {
+ if (item == null)
+ {
+ return new Point3D(0, 0, 0);
+ }
+
+ if (ShouldUseOriginalBoundingBoxCenterForBusinessTrackedPosition(
+ _route?.PathType ?? PathType.Ground,
+ IsRealObjectMode) &&
+ _originalCenter != null)
+ {
+ return _originalCenter;
+ }
+
+ return GetLiveBoundingBoxCenter(item);
+ }
+
private Point3D ResolveGroundTrackedCenter(Point3D groundContactPoint, double objectHeight)
{
var adapter = CoordinateSystemManager.Instance.CreateHostAdapter();
@@ -3892,7 +3899,7 @@ namespace NavisworksTransport.Core.Animation
return;
}
- Point3D currentTrackedCenter = GetTrackedObjectPosition(CurrentControlledObject ?? _animatedObject);
+ Point3D currentTrackedCenter = GetLiveBoundingBoxCenter(CurrentControlledObject ?? _animatedObject);
Point3D semanticTrackedCenter = RailPathPoseHelper.ResolveObjectSpaceCenterPosition(
_route,
referencePoint,
@@ -3979,6 +3986,13 @@ namespace NavisworksTransport.Core.Animation
return isRealObjectMode;
}
+ internal static bool ShouldUseOriginalBoundingBoxCenterForBusinessTrackedPosition(
+ PathType pathType,
+ bool isRealObjectMode)
+ {
+ return isRealObjectMode && pathType == PathType.Ground;
+ }
+
private void SyncTrackedRotationToDisplayedPose(ModelItem sourceObject)
{
if (sourceObject == null)
@@ -5584,14 +5598,14 @@ namespace NavisworksTransport.Core.Animation
}
///
- /// 专门用于Ground路径动画帧播放。使用 _trackedPosition 作为当前位置,不重新读取包围盒。
+ /// 专门用于Ground路径动画帧播放。使用业务跟踪点 _trackedPosition 作为当前位置,不读取实时包围盒中心回写主链。
///
private void ApplyGroundAnimationFrame(Point3D targetPosition, Rotation3D targetRotation)
{
var controlledObject = CurrentControlledObject;
if (controlledObject == null) return;
- // 使用 _trackedPosition 作为当前位置(几何中心),不重新读取包围盒
+ // 使用业务跟踪点 _trackedPosition 作为当前位置,不把实时包围盒中心混入主链
Point3D currentPosition = _trackedPosition;
Rotation3D currentRotation = _hasTrackedRotation ? _trackedRotation : Rotation3D.Identity;
diff --git a/src/Utils/ModelItemTransformHelper.cs b/src/Utils/ModelItemTransformHelper.cs
index dd7f7b6..52f858c 100644
--- a/src/Utils/ModelItemTransformHelper.cs
+++ b/src/Utils/ModelItemTransformHelper.cs
@@ -591,122 +591,6 @@ namespace NavisworksTransport.Utils
LogIncrementalTransformActual(item, targetPosition, targetRotation);
}
- public static void MoveItemIncrementallyToTrackedPositionAndRotation(
- ModelItem item,
- Point3D currentTrackedPosition,
- Rotation3D currentRotation,
- Point3D targetTrackedPosition,
- Rotation3D targetRotation)
- {
- if (item == null)
- {
- throw new ArgumentNullException(nameof(item));
- }
-
- var doc = Application.ActiveDocument;
- var modelItems = new ModelItemCollection { item };
-
- Rotation3D deltaRotation = BuildDeltaRotation(currentRotation, targetRotation);
- var currentLinear = new Transform3D(currentRotation).Linear;
- var targetLinear = new Transform3D(targetRotation).Linear;
-
- LogManager.Info(
- $"[模型增量补偿] {item.DisplayName} 当前trackedPoint=({currentTrackedPosition.X:F3},{currentTrackedPosition.Y:F3},{currentTrackedPosition.Z:F3}), " +
- $"目标trackedPoint=({targetTrackedPosition.X:F3},{targetTrackedPosition.Y:F3},{targetTrackedPosition.Z:F3})");
- // LogManager.Debug(
- // $"[模型增量补偿] {item.DisplayName} 当前旋转: " +
- // $"X=({currentLinear.Get(0, 0):F4},{currentLinear.Get(1, 0):F4},{currentLinear.Get(2, 0):F4}), " +
- // $"Y=({currentLinear.Get(0, 1):F4},{currentLinear.Get(1, 1):F4},{currentLinear.Get(2, 1):F4}), " +
- // $"Z=({currentLinear.Get(0, 2):F4},{currentLinear.Get(1, 2):F4},{currentLinear.Get(2, 2):F4})");
- // LogManager.Debug(
- // $"[模型增量补偿] {item.DisplayName} 目标旋转: " +
- // $"X=({targetLinear.Get(0, 0):F4},{targetLinear.Get(1, 0):F4},{targetLinear.Get(2, 0):F4}), " +
- // $"Y=({targetLinear.Get(0, 1):F4},{targetLinear.Get(1, 1):F4},{targetLinear.Get(2, 1):F4}), " +
- // $"Z=({targetLinear.Get(0, 2):F4},{targetLinear.Get(1, 2):F4},{targetLinear.Get(2, 2):F4})");
-
- // LogGeometryLevelTransforms(item, "[模型增量补偿应用前][GeometryAPI]");
-
- // 第1步:将物体中心移到原点(此时几何中心在原点)
- var toOrigin = Transform3D.CreateTranslation(new Vector3D(
- -currentTrackedPosition.X,
- -currentTrackedPosition.Y,
- -currentTrackedPosition.Z));
- doc.Models.OverridePermanentTransform(modelItems, toOrigin, false);
-
- // 第2步:在原点旋转
- var rotationOnlyComponents = Transform3D.CreateTranslation(new Vector3D(0, 0, 0)).Factor();
- rotationOnlyComponents.Rotation = deltaRotation;
- var rotationOnly = rotationOnlyComponents.Combine();
- doc.Models.OverridePermanentTransform(modelItems, rotationOnly, false);
-
- // 🔥 关键:读取旋转后、平移前的包围盒中心(相对于几何中心的偏移)
- Point3D centerAfterRotation = item.BoundingBox()?.Center ?? new Point3D(0, 0, 0);
-
- // 这就是目标旋转下,BoundingBox中心相对于几何中心的偏移
- var rotationOffset = new Vector3D(
- centerAfterRotation.X,
- centerAfterRotation.Y,
- centerAfterRotation.Z);
- LogManager.Info(
- $"[模型增量补偿] {item.DisplayName} 旋转后包围盒中心=({centerAfterRotation.X:F3},{centerAfterRotation.Y:F3},{centerAfterRotation.Z:F3}), " +
- $"旋转偏移量=({rotationOffset.X:F3},{rotationOffset.Y:F3},{rotationOffset.Z:F3})");
-
- // 🔥 关键修正:我们要几何中心对齐目标,而不是 BoundingBox 中心对齐目标
- // rotationOffset = 旋转后 BoundingBox 中心相对于原点的位置
- // 几何中心在原点,相对于 BoundingBox 中心的偏移 = -rotationOffset
- // 所以 BoundingBox 中心应该位于 target + rotationOffset,才能让几何中心对齐 target
- var adjustedTargetPosition = new Point3D(
- targetTrackedPosition.X + rotationOffset.X,
- targetTrackedPosition.Y + rotationOffset.Y,
- targetTrackedPosition.Z + rotationOffset.Z);
- LogManager.Info(
- $"[模型增量补偿] {item.DisplayName} 调整后目标位置=({adjustedTargetPosition.X:F3},{adjustedTargetPosition.Y:F3},{adjustedTargetPosition.Z:F3}), " +
- $"补偿量=(+{rotationOffset.X:F3},+{rotationOffset.Y:F3},+{rotationOffset.Z:F3})");
-
- // 第3步:预落位到调整后的目标位置
- var provisionalToTarget = Transform3D.CreateTranslation(new Vector3D(
- adjustedTargetPosition.X,
- adjustedTargetPosition.Y,
- adjustedTargetPosition.Z));
- doc.Models.OverridePermanentTransform(modelItems, provisionalToTarget, false);
-
- Point3D actualTrackedPosition = item.BoundingBox()?.Center ?? new Point3D(0, 0, 0);
- var correctionTranslation = new Vector3D(
- adjustedTargetPosition.X - actualTrackedPosition.X,
- adjustedTargetPosition.Y - actualTrackedPosition.Y,
- adjustedTargetPosition.Z - actualTrackedPosition.Z);
-
- LogManager.Info(
- $"[模型增量补偿] {item.DisplayName} 预落位后实际trackedPoint=({actualTrackedPosition.X:F3},{actualTrackedPosition.Y:F3},{actualTrackedPosition.Z:F3}), " +
- $"纠偏平移=({correctionTranslation.X:F3},{correctionTranslation.Y:F3},{correctionTranslation.Z:F3})");
-
- if (Math.Abs(correctionTranslation.X) > 1e-6 ||
- Math.Abs(correctionTranslation.Y) > 1e-6 ||
- Math.Abs(correctionTranslation.Z) > 1e-6)
- {
- var correction = Transform3D.CreateTranslation(correctionTranslation);
- doc.Models.OverridePermanentTransform(modelItems, correction, false);
- LogManager.Info($"[模型增量补偿] {item.DisplayName} 已应用纠偏平移");
- }
- else
- {
- LogManager.Info($"[模型增量补偿] {item.DisplayName} 纠偏平移太小,跳过应用");
- }
-
- // 🔥 验证纠偏后的实际位置(包围盒中心应该偏离目标,但几何中心对齐目标)
- Point3D finalTrackedPosition = item.BoundingBox()?.Center ?? new Point3D(0, 0, 0);
- // 几何中心 = BoundingBox中心 - rotationOffset(因为rotationOffset是BoundingBox中心相对于几何中心的偏移)
- var geometricCenterDeviation = new Vector3D(
- targetTrackedPosition.X - (finalTrackedPosition.X - rotationOffset.X),
- targetTrackedPosition.Y - (finalTrackedPosition.Y - rotationOffset.Y),
- targetTrackedPosition.Z - (finalTrackedPosition.Z - rotationOffset.Z));
- LogManager.Info(
- $"[模型增量补偿] {item.DisplayName} 最终包围盒中心=({finalTrackedPosition.X:F3},{finalTrackedPosition.Y:F3},{finalTrackedPosition.Z:F3}), " +
- $"几何中心偏差=({geometricCenterDeviation.X:F3},{geometricCenterDeviation.Y:F3},{geometricCenterDeviation.Z:F3})");
-
- // LogIncrementalTransformActual(item, targetTrackedPosition, targetRotation);
- }
-
///
/// 读取物体当前实际几何姿态。
/// 优先使用 ModelGeometry.ActiveTransform,因为 ModelItem.Transform 只反映原始设计变换。
From 042f30bf874adca5f3c4622dd951d70f8d0a7683 Mon Sep 17 00:00:00 2001
From: tian <11429339@qq.com>
Date: Mon, 6 Apr 2026 22:39:02 +0800
Subject: [PATCH 64/87] =?UTF-8?q?=E5=9C=B0=E9=9D=A2=E8=B7=AF=E5=BE=84?=
=?UTF-8?q?=E9=87=8D=E6=9E=84=E5=A4=B1=E8=B4=A5=EF=BC=8C=E7=95=99=E4=B8=8B?=
=?UTF-8?q?=E4=B8=80=E4=BA=9B=E6=95=99=E8=AE=AD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.agents/skills/nw-api/SKILL.md | 4 +-
AGENTS.md | 28 +-
...sworks-transform-api-official-reference.md | 1012 +++++++++++++++++
...-ground-remove-fragment-dependency-plan.md | 136 +++
4 files changed, 1174 insertions(+), 6 deletions(-)
create mode 100644 doc/working/2026-04-06-navisworks-transform-api-official-reference.md
create mode 100644 doc/working/2026-04-08-ground-remove-fragment-dependency-plan.md
diff --git a/.agents/skills/nw-api/SKILL.md b/.agents/skills/nw-api/SKILL.md
index c08dd29..0f96cd5 100644
--- a/.agents/skills/nw-api/SKILL.md
+++ b/.agents/skills/nw-api/SKILL.md
@@ -16,7 +16,9 @@ description: Navisworks API 开发助手,用于开发 Navisworks 插件。功
| NET API | `doc/navisworks_api/NET/documentation/NET API.chm` | CHM 帮助文件 |
| NET API HTML | `doc/navisworks_api/NET/documentation/NetAPIHtml/` | HTML 文档 |
-**HTML 文档入口**: `doc/navisworks_api/NET/documentation/NetAPIHtml/html/index.html`
+**推荐导航入口**: `doc/navisworks_api/NET/documentation/NetAPIHtml/index.html`
+
+**原始 HTML 文档入口**: `doc/navisworks_api/NET/documentation/NetAPIHtml/html/index.html`
### API 文档搜索方法
diff --git a/AGENTS.md b/AGENTS.md
index 6eb1690..745e39b 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -361,19 +361,37 @@ var rotation = new Rotation3D(qw, qx, qy, qz); // 错误
## 5. 开发原则
-### 5.1 不向后兼容
+### 5.1 彻底禁止 fallback
-项目只针对 Navisworks 2026。不要写旧版本兼容代码。
+这是当前项目的第一编码原则,优先级高于其他“先跑起来”的考虑。
-### 5.2 不要随意加 fallback
+不允许出现以下行为:
-不要为了“先跑起来”就:
+- 新姿态链失败时,静默退回旧姿态链
+- 新变换链失败时,静默退回旧变换链
+- 正确姿势/正确位置/正确尺寸语义拿不到时,用“差不多”的旧值、缓存值、默认值顶上
+- 只打印一条 warning,然后继续使用错误语义把流程跑完
+
+尤其禁止这类做法:
- 偷偷退回旧 `yaw`
- 偷偷用硬编码 `Z-up`
- 偷偷在错误时给默认值掩盖问题
+- 偷偷在新链失败时自动掉回旧链
+- 读不到当前实际几何旋转时,回退到 `_trackedRotation`
+- 读不到当前真实姿态时,回退到 `referenceRotation`
+- 读不到当前显示姿态时,回退到 `ModelItem.Transform`
-如果完整姿态链失败,应优先暴露问题并修根因。
+正确做法只有两种:
+
+1. 在进入新链前把前置条件补齐
+2. 直接暴露失败并修根因
+
+不允许把“旧链兜底”当成正式实现的一部分。
+
+### 5.2 不向后兼容
+
+项目只针对 Navisworks 2026。不要写旧版本兼容代码。
### 5.3 临时补丁不是正式实现
diff --git a/doc/working/2026-04-06-navisworks-transform-api-official-reference.md b/doc/working/2026-04-06-navisworks-transform-api-official-reference.md
new file mode 100644
index 0000000..a8be570
--- /dev/null
+++ b/doc/working/2026-04-06-navisworks-transform-api-official-reference.md
@@ -0,0 +1,1012 @@
+# Navisworks 变换 API 官方原始定义整理
+
+更新时间:2026-04-06
+
+本文只整理当前讨论中直接用到的 Navisworks .NET API 官方原始定义与语法。
+
+原则:
+
+- 正文尽量保留官方原始内容
+- 不混入项目内部“局部坐标系”“参考姿态”等二次解释
+- 如果官方示例目录中未找到对应 API 的直接示例,就如实记录“未找到直接示例”
+- 只在最后增加一段简短归纳
+
+---
+
+## 1. `DocumentModels.OverridePermanentTransform(...)`
+
+官方文档:
+
+- [M_Autodesk_Navisworks_Api_DocumentParts_DocumentModels_OverridePermanentTransform_3_131351c5.htm](/C:/Users/Tellme/apps/NavisworksTransport/doc/navisworks_api/NET/documentation/NetAPIHtml/html/M_Autodesk_Navisworks_Api_DocumentParts_DocumentModels_OverridePermanentTransform_3_131351c5.htm)
+
+官方标题:
+
+```text
+DocumentModels.OverridePermanentTransform Method
+```
+
+官方定义:
+
+```text
+Apply an incremental transform to a selection.
+```
+
+官方 C# 语法:
+
+```csharp
+public void OverridePermanentTransform(
+ IEnumerable items,
+ Transform3D transform,
+ bool updateModelTransform
+)
+```
+
+官方 Remarks:
+
+```text
+If the selection contains any files, and the updateModelTransform
+parameter is true, then instead of applying a transform to the
+fragments, the File Units and Transform will be updated.
+```
+
+官方示例情况:
+
+```text
+在官方 NET examples 目录中,未找到该方法的直接示例代码。
+```
+
+---
+
+## 2. `DocumentModels.ResetPermanentTransform(...)`
+
+官方文档:
+
+- [M_Autodesk_Navisworks_Api_DocumentParts_DocumentModels_ResetPermanentTransform_1_75193b86.htm](/C:/Users/Tellme/apps/NavisworksTransport/doc/navisworks_api/NET/documentation/NetAPIHtml/html/M_Autodesk_Navisworks_Api_DocumentParts_DocumentModels_ResetPermanentTransform_1_75193b86.htm)
+
+官方标题:
+
+```text
+DocumentModels.ResetPermanentTransform Method
+```
+
+官方定义:
+
+```text
+Reset incremental transforms for all model items contained in the selection.
+```
+
+官方 C# 语法:
+
+```csharp
+public void ResetPermanentTransform(
+ IEnumerable items
+)
+```
+
+官方示例情况:
+
+```text
+在官方 NET examples 目录中,未找到该方法的直接示例代码。
+```
+
+---
+
+## 3. `ModelGeometry.ActiveTransform`
+
+官方文档:
+
+- [P_Autodesk_Navisworks_Api_ModelGeometry_ActiveTransform.htm](/C:/Users/Tellme/apps/NavisworksTransport/doc/navisworks_api/NET/documentation/NetAPIHtml/html/P_Autodesk_Navisworks_Api_ModelGeometry_ActiveTransform.htm)
+
+官方标题:
+
+```text
+ModelGeometry.ActiveTransform Property
+```
+
+官方定义:
+
+```text
+Returns the currently active transform of the geometry.
+```
+
+官方 C# 语法:
+
+```csharp
+public Transform3D ActiveTransform { get; }
+```
+
+官方示例情况:
+
+```text
+在官方 NET examples 目录中,未找到该属性的直接示例代码。
+```
+
+---
+
+## 4. `Transform3D`
+
+官方文档:
+
+- [T_Autodesk_Navisworks_Api_Transform3D.htm](/C:/Users/Tellme/apps/NavisworksTransport/doc/navisworks_api/NET/documentation/NetAPIHtml/html/T_Autodesk_Navisworks_Api_Transform3D.htm)
+
+官方标题:
+
+```text
+Transform3D Class
+```
+
+官方定义:
+
+```text
+A generic transformation in 3D space.
+```
+
+官方 Remarks:
+
+```text
+Considered an immutable value type.
+```
+
+官方 C# 类型语法:
+
+```csharp
+public class Transform3D : NativeHandle
+```
+
+官方成员页中可见的相关构造/工厂:
+
+- `Transform3D(Rotation3D)`
+- `Transform3D(Matrix3, Vector3D)`
+- `Transform3D(Rotation3D, Vector3D)`
+- `CreateIdentity()`
+- `CreateTranslation(Vector3D)`
+
+来源:
+
+- [AllMembers_T_Autodesk_Navisworks_Api_Transform3D.htm](/C:/Users/Tellme/apps/NavisworksTransport/doc/navisworks_api/NET/documentation/NetAPIHtml/html/AllMembers_T_Autodesk_Navisworks_Api_Transform3D.htm)
+
+官方示例情况:
+
+```text
+在官方 NET examples 目录中,未找到该类型的直接示例代码。
+```
+
+---
+
+## 5. `Transform3D(Rotation3D)`
+
+官方文档:
+
+- [C_Autodesk_Navisworks_Api_Transform3D_ctor_1_73bf3bc1.htm](/C:/Users/Tellme/apps/NavisworksTransport/doc/navisworks_api/NET/documentation/NetAPIHtml/html/C_Autodesk_Navisworks_Api_Transform3D_ctor_1_73bf3bc1.htm)
+
+官方定义:
+
+```text
+Make a transform from a rotation (3D homogenous).
+```
+
+官方 C# 语法:
+
+```csharp
+public Transform3D(
+ Rotation3D rotation
+)
+```
+
+---
+
+## 6. `Transform3D(Rotation3D, Vector3D)`
+
+官方文档:
+
+- [C_Autodesk_Navisworks_Api_Transform3D_ctor_2_499970f8.htm](/C:/Users/Tellme/apps/NavisworksTransport/doc/navisworks_api/NET/documentation/NetAPIHtml/html/C_Autodesk_Navisworks_Api_Transform3D_ctor_2_499970f8.htm)
+
+官方定义:
+
+```text
+Make a transform from a rotation and a translation.
+```
+
+官方 C# 语法:
+
+```csharp
+public Transform3D(
+ Rotation3D rotation,
+ Vector3D translation
+)
+```
+
+---
+
+## 7. `Transform3D.CreateIdentity()`
+
+官方文档:
+
+- [M_Autodesk_Navisworks_Api_Transform3D_CreateIdentity.htm](/C:/Users/Tellme/apps/NavisworksTransport/doc/navisworks_api/NET/documentation/NetAPIHtml/html/M_Autodesk_Navisworks_Api_Transform3D_CreateIdentity.htm)
+
+官方定义:
+
+```text
+Create an identity transform.
+```
+
+官方 C# 语法:
+
+```csharp
+public static Transform3D CreateIdentity()
+```
+
+---
+
+## 8. `Transform3D.CreateTranslation(Vector3D)`
+
+官方文档:
+
+- [M_Autodesk_Navisworks_Api_Transform3D_CreateTranslation_1_aa2b59dc.htm](/C:/Users/Tellme/apps/NavisworksTransport/doc/navisworks_api/NET/documentation/NetAPIHtml/html/M_Autodesk_Navisworks_Api_Transform3D_CreateTranslation_1_aa2b59dc.htm)
+
+官方定义:
+
+```text
+Set matrix to be 3D homogenous translation matrix.
+```
+
+官方 C# 语法:
+
+```csharp
+public static Transform3D CreateTranslation(
+ Vector3D translation
+)
+```
+
+---
+
+## 9. `Rotation3D(UnitVector3D, Double)`
+
+官方文档:
+
+- [C_Autodesk_Navisworks_Api_Rotation3D_ctor_2_a6cb51d1.htm](/C:/Users/Tellme/apps/NavisworksTransport/doc/navisworks_api/NET/documentation/NetAPIHtml/html/C_Autodesk_Navisworks_Api_Rotation3D_ctor_2_a6cb51d1.htm)
+
+官方定义:
+
+```text
+Create a rotation as a rotation about an axis by an angle.
+```
+
+官方 C# 语法:
+
+```csharp
+public Rotation3D(
+ UnitVector3D axis,
+ double angle
+)
+```
+
+官方示例情况:
+
+```text
+在官方 NET examples 目录中,未找到该构造函数的直接示例代码。
+```
+
+---
+
+## 10. `Transform3D.Inverse()`
+
+官方文档:
+
+- [M_Autodesk_Navisworks_Api_Transform3D_Inverse.htm](/C:/Users/Tellme/apps/NavisworksTransport/doc/navisworks_api/NET/documentation/NetAPIHtml/html/M_Autodesk_Navisworks_Api_Transform3D_Inverse.htm)
+
+官方定义:
+
+```text
+Return inverse transformation.
+```
+
+官方 C# 语法:
+
+```csharp
+public Transform3D Inverse()
+```
+
+---
+
+## 11. `Transform3D.Multiply(Transform3D, Transform3D)`
+
+官方文档:
+
+- [M_Autodesk_Navisworks_Api_Transform3D_Multiply_2_141222c1.htm](/C:/Users/Tellme/apps/NavisworksTransport/doc/navisworks_api/NET/documentation/NetAPIHtml/html/M_Autodesk_Navisworks_Api_Transform3D_Multiply_2_141222c1.htm)
+
+官方定义:
+
+```text
+Multiply two transformations in the order supplied.
+```
+
+官方 C# 语法:
+
+```csharp
+public static Transform3D Multiply(
+ Transform3D left,
+ Transform3D right
+)
+```
+
+---
+
+## 12. 当前已用实验钉死的最小用法
+
+本节不是官方原文,而是基于上面这些官方 API 和本仓库 `ReadTransformTestCommand` 的实验结果整理出的最小结论。
+
+### 12.1 宿主世界轴直接转 90° 的 API 参数
+
+`90°` 在 API 中应写成弧度:
+
+```csharp
+Math.PI / 2.0
+```
+
+宿主世界轴应直接使用世界单位轴:
+
+```csharp
+var hostX = new UnitVector3D(1, 0, 0);
+var hostY = new UnitVector3D(0, 1, 0);
+var hostZ = new UnitVector3D(0, 0, 1);
+```
+
+对应的旋转对象写法:
+
+```csharp
+var rx90 = new Rotation3D(hostX, Math.PI / 2.0);
+var ry90 = new Rotation3D(hostY, Math.PI / 2.0);
+var rz90 = new Rotation3D(hostZ, Math.PI / 2.0);
+```
+
+如果要直接喂给 `OverridePermanentTransform(...)`:
+
+```csharp
+var transform = new Transform3D(ry90);
+doc.Models.OverridePermanentTransform(items, transform, false);
+```
+
+### 12.2 从当前姿态到目标姿态的纯旋转增量
+
+当前实验已经验证:
+
+```csharp
+deltaRotation = currentInverse * target
+```
+
+在 API 上应写成:
+
+```csharp
+var currentTransform = new Transform3D(currentRotation);
+var targetTransform = new Transform3D(targetRotation);
+var deltaTransform = Transform3D.Multiply(
+ currentTransform.Inverse(),
+ targetTransform);
+```
+
+已验证:
+
+- `currentInverse * target` 对
+- `target * currentInverse` 不对
+
+### 12.3 纯旋转增量的默认旋转中心
+
+当前实验已验证:
+
+- `OverridePermanentTransform(..., rotationOnly, false)` 的纯旋转增量默认绕宿主原点 `(0,0,0)` 生效
+
+因此:
+
+- 不能把纯旋转理解成“围绕当前业务跟踪点原地自转”
+- 对 `Ground + 真实物体`,旋转后必须再单独做位置重对齐
+
+---
+
+## 9. `Transform3D.Factor(Vector3D worldCenter)`
+
+官方文档:
+
+- [M_Autodesk_Navisworks_Api_Transform3D_Factor_1_aa2b59dc.htm](/C:/Users/Tellme/apps/NavisworksTransport/doc/navisworks_api/NET/documentation/NetAPIHtml/html/M_Autodesk_Navisworks_Api_Transform3D_Factor_1_aa2b59dc.htm)
+
+官方标题:
+
+```text
+Transform3D.Factor Method (Vector3D)
+```
+
+官方定义:
+
+```text
+Treat as homogenous 3D matrix and factor into scale orientation, scale, rotation and translation components.
+```
+
+官方 C# 语法:
+
+```csharp
+public Transform3DComponents Factor(
+ Vector3D worldCenter
+)
+```
+
+目前从官方原文可确认:
+
+- 该重载显式接收 `worldCenter`
+- 说明 `Transform3D` 的分解结果与指定的世界中心有关
+
+当前实验用途:
+
+- 用它比较“同一个旋转增量”在
+ - `worldCenter = (0,0,0)`
+ - `worldCenter = 基线BoundingBox.Center`
+- 两种情况下分解出来的 `Translation`
+- 以验证 Navisworks 增量旋转是否表现得像“绕宿主原点旋转”
+
+---
+
+## 9. `Transform3DComponents`
+
+官方文档:
+
+- [T_Autodesk_Navisworks_Api_Transform3DComponents.htm](/C:/Users/Tellme/apps/NavisworksTransport/doc/navisworks_api/NET/documentation/NetAPIHtml/html/T_Autodesk_Navisworks_Api_Transform3DComponents.htm)
+
+官方标题:
+
+```text
+Transform3DComponents Class
+```
+
+官方定义:
+
+```text
+Affine transform represented as individual components that combine to form complete transform. Immutable.
+```
+
+官方 Remarks:
+
+```text
+Affine transform represented as individual components that combine to form complete transform. Immutable. M = c' * so' * s * so * r * c * t * p, where so is scale orientation, s is scale, c is center, r is rotation and t is translation.
+```
+
+官方 C# 类型语法:
+
+```csharp
+public class Transform3DComponents : NativeHandle
+```
+
+官方示例情况:
+
+```text
+在官方 NET examples 目录中,未找到该类型的直接示例代码。
+```
+
+---
+
+## 10. `Transform3DComponents.Translation`
+
+官方文档:
+
+- [P_Autodesk_Navisworks_Api_Transform3DComponents_Translation.htm](/C:/Users/Tellme/apps/NavisworksTransport/doc/navisworks_api/NET/documentation/NetAPIHtml/html/P_Autodesk_Navisworks_Api_Transform3DComponents_Translation.htm)
+
+官方定义:
+
+```text
+The translation component of the transform.
+```
+
+官方 C# 语法:
+
+```csharp
+public Vector3D Translation { get; set; }
+```
+
+---
+
+## 11. `Transform3DComponents.Rotation`
+
+官方文档:
+
+- [P_Autodesk_Navisworks_Api_Transform3DComponents_Rotation.htm](/C:/Users/Tellme/apps/NavisworksTransport/doc/navisworks_api/NET/documentation/NetAPIHtml/html/P_Autodesk_Navisworks_Api_Transform3DComponents_Rotation.htm)
+
+官方定义:
+
+```text
+The rotation component of the transform.
+```
+
+官方 C# 语法:
+
+```csharp
+public Rotation3D Rotation { get; set; }
+```
+
+---
+
+## 12. `Transform3DComponents.Scale`
+
+官方文档:
+
+- [P_Autodesk_Navisworks_Api_Transform3DComponents_Scale.htm](/C:/Users/Tellme/apps/NavisworksTransport/doc/navisworks_api/NET/documentation/NetAPIHtml/html/P_Autodesk_Navisworks_Api_Transform3DComponents_Scale.htm)
+
+官方定义:
+
+```text
+The scale component of the transform.
+```
+
+官方 C# 语法:
+
+```csharp
+public Vector3D Scale { get; set; }
+```
+
+---
+
+## 13. `Transform3DComponents.Combine()`
+
+官方文档:
+
+- [M_Autodesk_Navisworks_Api_Transform3DComponents_Combine.htm](/C:/Users/Tellme/apps/NavisworksTransport/doc/navisworks_api/NET/documentation/NetAPIHtml/html/M_Autodesk_Navisworks_Api_Transform3DComponents_Combine.htm)
+
+官方定义:
+
+```text
+Combine components together into a composite transform.
+```
+
+官方 C# 语法:
+
+```csharp
+public Transform3D Combine()
+```
+
+---
+
+## 14. `Transform3D.Multiply(Transform3D, Transform3D)`
+
+官方文档:
+
+- [M_Autodesk_Navisworks_Api_Transform3D_Multiply_2_141222c1.htm](/C:/Users/Tellme/apps/NavisworksTransport/doc/navisworks_api/NET/documentation/NetAPIHtml/html/M_Autodesk_Navisworks_Api_Transform3D_Multiply_2_141222c1.htm)
+
+官方定义:
+
+```text
+Multiply two transforms in the order that the arguments are given, and return the result.
+```
+
+官方 C# 语法:
+
+```csharp
+public static Transform3D Multiply(
+ Transform3D leftTransform,
+ Transform3D rightTransform
+)
+```
+
+---
+
+## 15. `Transform3D.Factor()`
+
+官方文档:
+
+- [M_Autodesk_Navisworks_Api_Transform3D_Factor.htm](/C:/Users/Tellme/apps/NavisworksTransport/doc/navisworks_api/NET/documentation/NetAPIHtml/html/M_Autodesk_Navisworks_Api_Transform3D_Factor.htm)
+
+官方定义:
+
+```text
+Treat as homogenous 3D matrix and factor into scale orientation, scale, rotation and translation components.
+```
+
+官方 C# 语法:
+
+```csharp
+public Transform3DComponents Factor()
+```
+
+---
+
+## 16. `Transform3D.Inverse()`
+
+官方文档:
+
+- [M_Autodesk_Navisworks_Api_Transform3D_Inverse.htm](/C:/Users/Tellme/apps/NavisworksTransport/doc/navisworks_api/NET/documentation/NetAPIHtml/html/M_Autodesk_Navisworks_Api_Transform3D_Inverse.htm)
+
+官方定义:
+
+```text
+Return inverse of matrix. Matrix must be non-singular (non-zero determinant).
+```
+
+官方 C# 语法:
+
+```csharp
+public Transform3D Inverse()
+```
+
+官方异常说明:
+
+```text
+ObjectDisposedException
+Object has been Disposed
+```
+
+---
+
+## 17. `Transform3D.TranslateRight(Vector3D, Transform3D)`
+
+官方文档:
+
+- [M_Autodesk_Navisworks_Api_Transform3D_TranslateRight_2_9ab750ca.htm](/C:/Users/Tellme/apps/NavisworksTransport/doc/navisworks_api/NET/documentation/NetAPIHtml/html/M_Autodesk_Navisworks_Api_Transform3D_TranslateRight_2_9ab750ca.htm)
+
+官方定义:
+
+```text
+Translate a transform by a vector.
+```
+
+官方 C# 语法:
+
+```csharp
+public static Transform3D TranslateRight(
+ Vector3D translation,
+ Transform3D transform
+)
+```
+
+---
+
+## 18. `Rotation3D`
+
+官方文档:
+
+- [T_Autodesk_Navisworks_Api_Rotation3D.htm](/C:/Users/Tellme/apps/NavisworksTransport/doc/navisworks_api/NET/documentation/NetAPIHtml/html/T_Autodesk_Navisworks_Api_Rotation3D.htm)
+
+官方 C# 类型语法:
+
+```csharp
+public class Rotation3D : NativeHandle
+```
+
+---
+
+## 19. `Rotation3D(UnitVector3D, Double)`
+
+官方文档:
+
+- [C_Autodesk_Navisworks_Api_Rotation3D_ctor_2_afd1d818.htm](/C:/Users/Tellme/apps/NavisworksTransport/doc/navisworks_api/NET/documentation/NetAPIHtml/html/C_Autodesk_Navisworks_Api_Rotation3D_ctor_2_afd1d818.htm)
+
+官方定义:
+
+```text
+Creates rotation about given axis by angle in radians
+```
+
+官方 C# 语法:
+
+```csharp
+public Rotation3D(
+ UnitVector3D axis,
+ double angle
+)
+```
+
+---
+
+## 20. `Rotation3D.CreateFromEulerAngles(Double, Double, Double)`
+
+官方文档:
+
+- [M_Autodesk_Navisworks_Api_Rotation3D_CreateFromEulerAngles_3_d36b82d7.htm](/C:/Users/Tellme/apps/NavisworksTransport/doc/navisworks_api/NET/documentation/NetAPIHtml/html/M_Autodesk_Navisworks_Api_Rotation3D_CreateFromEulerAngles_3_d36b82d7.htm)
+
+官方定义:
+
+```text
+Creates a Euler angle rotation.
+Parameters are in radians.
+Rotation is created by combination of rotations about X, Y and Z axes.
+```
+
+官方 C# 语法:
+
+```csharp
+public static Rotation3D CreateFromEulerAngles(
+ double x,
+ double y,
+ double z
+)
+```
+
+---
+
+## 21. `Rotation3D.ToAxisAndAngle()`
+
+官方文档:
+
+- [M_Autodesk_Navisworks_Api_Rotation3D_ToAxisAndAngle.htm](/C:/Users/Tellme/apps/NavisworksTransport/doc/navisworks_api/NET/documentation/NetAPIHtml/html/M_Autodesk_Navisworks_Api_Rotation3D_ToAxisAndAngle.htm)
+
+官方定义:
+
+```text
+Calculates an axis and angle representation of this rotation.
+```
+
+官方 C# 语法:
+
+```csharp
+public AxisAndAngleResult ToAxisAndAngle()
+```
+
+---
+
+## 22. `Rotation3D.ToEulerAngles()`
+
+官方文档:
+
+- [M_Autodesk_Navisworks_Api_Rotation3D_ToEulerAngles.htm](/C:/Users/Tellme/apps/NavisworksTransport/doc/navisworks_api/NET/documentation/NetAPIHtml/html/M_Autodesk_Navisworks_Api_Rotation3D_ToEulerAngles.htm)
+
+官方定义:
+
+```text
+Calculates the Euler angles for this rotation.
+```
+
+官方 C# 语法:
+
+```csharp
+public EulerAngleResult ToEulerAngles()
+```
+
+---
+
+## 23. 由官方 API 原始定义直接能表达的增量求法
+
+这一节不引入项目术语,只把前面几条官方定义并列摆出:
+
+```text
+currentTransform = 当前生效的几何变换(ModelGeometry.ActiveTransform)
+targetTransform = 目标完整 Transform3D
+currentInverse = currentTransform.Inverse()
+incrementalTransform = Transform3D.Multiply(targetTransform, currentInverse)
+OverridePermanentTransform(..., incrementalTransform, ...)
+```
+
+这一段的依据完全来自前文官方定义:
+
+- `ActiveTransform` = Returns the currently active transform of the geometry.
+- `Inverse()` = Return inverse of matrix.
+- `Multiply(left, right)` = Multiply two transforms in the order that the arguments are given.
+- `OverridePermanentTransform(...)` = Apply an incremental transform to a selection.
+
+这里只说明“这些 API 可以这样直接串起来表达完整当前变换 -> 目标变换 -> 增量变换”,不额外添加项目侧解释。
+
+---
+
+## 24. 官方示例检索结果
+
+检索范围:
+
+```text
+C:\Users\Tellme\apps\NavisworksTransport\doc\navisworks_api\NET\examples\
+```
+
+检索关键词:
+
+```text
+OverridePermanentTransform
+ResetPermanentTransform
+ActiveTransform
+Transform3DComponents
+CreateTranslation
+new Transform3D(
+```
+
+检索结论:
+
+```text
+当前官方 NET examples 目录中,未找到这些 API 的直接示例代码。
+```
+
+因此本文中的“示例”部分不补写项目侧示例,只保留官方文档中的原始定义与语法。
+
+---
+
+## 25. 2026-04-06 Transform API 实验结论
+
+以下结论来自仓库内实验按钮:
+
+- [ReadTransformTestCommand.cs](/C:/Users/Tellme/apps/NavisworksTransport-rail-mount-modes/src/Commands/ReadTransformTestCommand.cs)
+
+实验对象:
+
+```text
+Chair Lounge Couch Double
+```
+
+实验日志位置:
+
+```text
+C:\ProgramData\Autodesk\Navisworks Manage 2026\plugins\TransportPlugin\logs\debug.log
+```
+
+### 25.1 纯旋转增量的真实行为
+
+实验结果表明:
+
+- 对真实物体调用
+ - `OverridePermanentTransform(..., rotationOnly, false)`
+- 其视觉效果等价于:
+ - **绕宿主原点 `(0,0,0)` 旋转**
+
+已验证的旋转:
+
+- `X + 90°`
+- `Y + 90°`
+- `Z + 90°`
+
+实验日志中,这三项都满足:
+
+```text
+按原点旋转预期Center == 实际Center
+误差 = (0, 0, 0)
+```
+
+因此当前可以视为已证实:
+
+- `.NET API` 下的纯旋转增量默认不是“围绕对象当前中心自转”
+- 而是“围绕宿主原点旋转”
+
+### 25.2 `Factor(worldCenter)` 的语义价值
+
+实验结果表明:
+
+- 对同一个纯旋转增量:
+ - `Factor(Vector3D.Zero)` 得到的 `Translation = (0,0,0)`
+ - `Factor(基线Center)` 会得到非零 `Translation`
+
+这说明:
+
+- `Transform3D` 的分解结果确实依赖 `worldCenter`
+- `Factor(worldCenter)` 可以用于分析“同一变换相对于不同世界中心的平移语义”
+
+但它只是:
+
+- **分解分析 API**
+
+不是:
+
+- “设置旋转中心”的构造 API
+
+### 25.3 “绕指定中心旋转”的三种 API 组合实验
+
+我们验证了三种做法,目标都是尝试复现 UI 中“指定变换中心后旋转”的效果。
+
+#### 做法 A:直接把 `T(center) * R * T(-center)` 当作增量
+
+结果:
+
+- 不成立
+- 对基线中心的误差为:
+
+```text
+(427.280, -492.199, 0.000)
+```
+
+#### 做法 B:完整目标左乘
+
+做法:
+
+```text
+target = centerRotation * current
+incremental = target * current.Inverse()
+```
+
+结果:
+
+- 与做法 A 等价
+- 同样不成立
+
+#### 做法 C:完整目标右乘
+
+做法:
+
+```text
+target = current * centerRotation
+incremental = target * current.Inverse()
+```
+
+结果:
+
+- 也不成立
+- 误差更大:
+
+```text
+(693.643, 0.000, -689.804)
+```
+
+### 25.4 当前可以正式采用的结论
+
+基于本轮实验,当前可以采用以下工程结论:
+
+1. `OverridePermanentTransform(..., false)` 的纯旋转增量,默认绕宿主原点旋转。
+2. 当前 `.NET API` 暴露的这套:
+ - `Transform3D.CreateTranslation`
+ - `Rotation3D`
+ - `Transform3D.Multiply`
+ - `OverridePermanentTransform`
+ 不能直接复现 UI 的“指定变换中心旋转”语义。
+3. 因此对 `Ground` 这类业务链,不能把问题继续建模成“通过 API 复刻 UI 中心旋转”。
+4. 对 `Ground` 更可靠的策略应是:
+ - 接受旋转默认绕宿主原点
+ - 再基于业务跟踪点语义做位置重对齐
+
+### 25.5 当前未证实的事项
+
+以下事项目前仍未在原始 API 文档中找到明确说明:
+
+1. UI 中“变换中心”是否有对应的 .NET API 可直接读写。
+2. UI 的“变换中心旋转”是否走的是另一套未公开的内部机制。
+3. 是否存在未被当前文档索引捕获的 COM API / UI API 可直接设置该中心。
+
+---
+
+## 26. 归纳
+
+基于以上官方原始定义,可以先得到一个非常克制的结论:
+
+1. `OverridePermanentTransform(...)` 的官方语义就是“应用增量变换”。
+2. `ResetPermanentTransform(...)` 的官方语义就是“重置这层增量变换”。
+3. `ModelGeometry.ActiveTransform` 表示“当前生效的几何变换”。
+4. `Transform3D` / `Transform3DComponents` 官方暴露的核心概念是:
+ - 平移
+ - 旋转
+ - 缩放
+5. `Transform3D.Multiply(...)` 官方明确说明“按参数给定顺序相乘”。
+6. `Transform3D.Inverse()` 官方明确是“返回矩阵逆”。
+7. `Rotation3D(UnitVector3D, Double)` 官方明确是“绕给定轴、按弧度创建旋转”。
+8. `Rotation3D.CreateFromEulerAngles(...)` 官方明确是“按 X/Y/Z 轴组合欧拉旋转,参数单位为弧度”。
+9. 这些官方定义里并没有把“局部业务坐标系解释”当成变换 API 的主语。
+
+因此,从官方原始定义可以直接得到一个非常具体的增量表达方式:
+
+- 当前变换:`ModelGeometry.ActiveTransform`
+- 目标变换:调用方构造的 `targetTransform`
+- 增量变换:`Transform3D.Multiply(targetTransform, currentTransform.Inverse())`
+- 应用:`OverridePermanentTransform(...)`
+
+因此,后续如果要重构真实物体变换工具,更合理的方向是:
+
+- 先把工具方法收成“宿主坐标系下的平移 / 旋转 / 缩放”
+- 再在需要时把它们组合成完整 `Transform3D`
+- 再用 `currentTransform.Inverse()` 和 `Transform3D.Multiply(...)` 求增量
+- 最后把这个增量变换交给 `OverridePermanentTransform(...)`
+
+而不是在变换工具层继续传播 `reference axis / local axis / hostUpLocalAxis` 之类的概念。
+
+---
+
+## 27. 2026-04-06 当天追加结论
+
+基于当天对真实物体、Ground 起点、逐帧、实验按钮的连续实验,可以再补充一条更直接的工程结论:
+
+1. 对 Navisworks 当前这套增量 API,真正稳定的做法只有**最简单的增量法**:
+ - 直接构造宿主坐标系下的旋转增量
+ - 直接构造宿主坐标系下的平移增量
+ - 按业务需要分步应用
+2. 对真实物体来说,旋转和平移不需要再引入“物体局部映射”来解释 API 行为。
+3. 也就是说,变换主语应始终是:
+ - 宿主坐标系下绕哪根轴旋转
+ - 宿主坐标系下平移多少
+4. 如果继续把问题建模成:
+ - 先解释物体局部轴
+ - 再把宿主旋转翻译成局部旋转
+ - 再去重建完整目标姿态
+ 这条链在真实物体上非常容易把问题复杂化,而且会引入额外歧义。
+5. 因此,后续 Ground 真实物体变换链的工程方向应当是:
+ - 只保留宿主坐标系增量变换
+ - 旋转和平移分开处理
+ - 不再在变换工具层传播“局部坐标系 / 局部轴映射”概念
+
+一句话总结:
+
+- **只有最简单的宿主坐标系增量法,才能尽量无副作用地做真实物体变换。**
+- **旋转和平移不需要物体局部映射,局部坐标的概念可以从这条变换链里彻底移除。**
diff --git a/doc/working/2026-04-08-ground-remove-fragment-dependency-plan.md b/doc/working/2026-04-08-ground-remove-fragment-dependency-plan.md
new file mode 100644
index 0000000..e99339b
--- /dev/null
+++ b/doc/working/2026-04-08-ground-remove-fragment-dependency-plan.md
@@ -0,0 +1,136 @@
+# Ground 去 Fragment 依赖实施方案
+
+更新时间:2026-04-08
+
+## 1. 这份方案现在只解决什么
+
+只解决一件事:
+
+- `Ground + 真实物体` 主链里,尽量去掉 `fragment` 参考姿态依赖
+
+只允许改动:
+
+- `PathAnimationManager.cs`
+- 必要时补少量日志
+
+明确不做:
+
+- 不新建大范围工具链
+- 不改 `Hoisting`
+- 不改 `Rail`
+- 不重写 `ModelItemTransformHelper`
+- 不删除 `RealObjectReferencePoseResolver`
+- 不做“整项目去 fragment”
+
+这份方案的目标是:**缩小修改范围,先把 Ground 主链收干净。**
+
+---
+
+## 2. 当前已确认的事实
+
+1. `Ground` 的变换更适合走最简单的宿主增量法:
+ - 宿主旋转增量
+ - 宿主平移增量
+2. `Ground` 这条链不应该再扩散 `local/reference/fragment` 概念。
+3. `fragment` 现在的问题,不在于“所有地方都要立刻删”,而在于:
+ - Ground 主链还会读它
+ - 导致姿态来源不稳定
+
+---
+
+## 3. 只保留的改造目标
+
+这轮只保留 3 个具体目标:
+
+1. `Ground` 初始化时,不再优先读 fragment 参考姿态
+2. `Ground` 平面姿态求解时,不再走 fragment 参考旋转入口
+3. `Ground` 不再允许 fragment planar fallback
+
+只要这 3 点做到,就算这一轮完成。
+
+---
+
+## 4. 当前 Ground 需要处理的入口
+
+### 4.1 初始化入口
+
+当前重点看:
+
+- `SyncTrackedRotationToObjectReference(...)`
+
+要求:
+
+- 当 `PathType == Ground` 且是真实物体时
+- 不再去走 `TryCaptureRealObjectReferenceRotation(...)`
+- 直接改用当前实际几何姿态,或现有非 fragment 入口
+
+### 4.2 平面姿态求解入口
+
+当前重点看:
+
+- `TryGetRealObjectReferenceRotation(...)`
+- `TryCreateReferenceBasedRealObjectPlanarPoseSolution(...)`
+
+要求:
+
+- `Ground` 不再从这里拿 fragment 参考旋转
+- `Ground` 单独走非 fragment 的姿态来源
+
+### 4.3 fallback 入口
+
+当前重点看:
+
+- `ShouldAllowFragmentPlanarFallback(PathType pathType)`
+
+要求:
+
+- `Ground` 改成和 `Hoisting` 一样,不再允许 fragment planar fallback
+
+---
+
+## 5. 实施顺序
+
+只按下面顺序做,不扩展:
+
+1. 先改 `Ground` 初始化入口
+2. 再改 `Ground` 平面姿态求解入口
+3. 最后关掉 `Ground` 的 fragment fallback
+
+每一步都要求:
+
+- 先看日志
+- 只改 `Ground`
+- 不顺手改别的路径
+
+---
+
+## 6. 验证标准
+
+这轮不追求“大而全测试矩阵”,只看 3 条:
+
+1. 起点
+ - `Ground + 真实物体` 到起点后不再读 fragment 姿态
+
+2. 逐帧
+ - `Ground` 播放时姿态来源不再依赖 fragment
+
+3. fallback
+ - `Ground` 关闭 fragment fallback 后,主链要么成功,要么明确报错
+ - 不允许再偷偷回退
+
+---
+
+## 7. 当前停止线
+
+如果做到下面这句话,就先停:
+
+- **Ground 主链不再依赖 fragment,但 Hoisting / Rail / 通用参考姿态系统暂时不动。**
+
+不要在这一轮里再继续追求:
+
+- 抽象统一工具类
+- 清理全部 reference/local 命名
+- 一次性删光 fragment 代码
+- 统一三类路径的所有姿态入口
+
+这些都属于下一轮的事。
From 405f72181143aa5667656ef6556cfd1327855445 Mon Sep 17 00:00:00 2001
From: tian <11429339@qq.com>
Date: Wed, 8 Apr 2026 23:06:42 +0800
Subject: [PATCH 65/87] Ground real-object incremental path updates
---
.../RailPreservedPoseTests.cs | 71 ++-
...-ground-remove-fragment-dependency-plan.md | 130 +++++-
src/Core/Animation/PathAnimationManager.cs | 420 ++++++++++++++++--
src/Utils/ModelItemTransformHelper.cs | 69 +++
4 files changed, 636 insertions(+), 54 deletions(-)
diff --git a/UnitTests/CoordinateSystem/RailPreservedPoseTests.cs b/UnitTests/CoordinateSystem/RailPreservedPoseTests.cs
index f416c13..97fe309 100644
--- a/UnitTests/CoordinateSystem/RailPreservedPoseTests.cs
+++ b/UnitTests/CoordinateSystem/RailPreservedPoseTests.cs
@@ -92,10 +92,79 @@ namespace NavisworksTransport.UnitTests.CoordinateSystem
public void ShouldAllowFragmentPlanarFallback_ShouldDisableHoisting()
{
Assert.IsFalse(PathAnimationManager.ShouldAllowFragmentPlanarFallback(PathType.Hoisting));
- Assert.IsTrue(PathAnimationManager.ShouldAllowFragmentPlanarFallback(PathType.Ground));
+ Assert.IsFalse(PathAnimationManager.ShouldAllowFragmentPlanarFallback(PathType.Ground));
Assert.IsTrue(PathAnimationManager.ShouldAllowFragmentPlanarFallback(PathType.Rail));
}
+ [TestMethod]
+ public void ShouldUseReferenceBasedRealObjectPlanarPose_ShouldDisableGroundAndHoisting()
+ {
+ Assert.IsFalse(PathAnimationManager.ShouldUseReferenceBasedRealObjectPlanarPose(PathType.Ground));
+ Assert.IsFalse(PathAnimationManager.ShouldUseReferenceBasedRealObjectPlanarPose(PathType.Hoisting));
+ Assert.IsTrue(PathAnimationManager.ShouldUseReferenceBasedRealObjectPlanarPose(PathType.Rail));
+ }
+
+ [TestMethod]
+ public void TryResolveDisplayedPlanarYawFromRotation_ShouldResolveYUpFromDisplayedXAxis()
+ {
+ Rotation3D rotation = new Rotation3D(
+ 0.0,
+ System.Math.Sin(-System.Math.PI / 4.0),
+ 0.0,
+ System.Math.Cos(-System.Math.PI / 4.0));
+
+ bool ok = PathAnimationManager.TryResolveDisplayedPlanarYawFromRotation(
+ rotation,
+ CoordinateSystemType.YUp,
+ out double yawRadians);
+
+ Assert.IsTrue(ok);
+ Assert.AreEqual(-System.Math.PI / 2.0, yawRadians, 1e-6);
+ }
+
+ [TestMethod]
+ public void ShouldUseGroundRealObjectPureIncrementFrames_ShouldEnableOnlyForGroundRealObjects()
+ {
+ Assert.IsTrue(PathAnimationManager.ShouldUseGroundRealObjectPureIncrementFrames(PathType.Ground, true));
+ Assert.IsFalse(PathAnimationManager.ShouldUseGroundRealObjectPureIncrementFrames(PathType.Ground, false));
+ Assert.IsFalse(PathAnimationManager.ShouldUseGroundRealObjectPureIncrementFrames(PathType.Hoisting, true));
+ Assert.IsFalse(PathAnimationManager.ShouldUseGroundRealObjectPureIncrementFrames(PathType.Rail, true));
+ }
+
+ [TestMethod]
+ public void TryResolveGroundHostPlanarYawFromFramePoints_ShouldResolveYUpHostYaw()
+ {
+ bool ok = PathAnimationManager.TryResolveGroundHostPlanarYawFromFramePoints(
+ new Point3D(0, 0, 0),
+ new Point3D(1, 0, 0),
+ new Point3D(1, 0, -1),
+ CoordinateSystemType.YUp,
+ out double yawRadians);
+
+ Assert.IsTrue(ok);
+ Assert.AreEqual(-System.Math.PI / 4.0, yawRadians, 1e-6);
+ }
+
+ [TestMethod]
+ public void ResolveGroundHostUpCorrectionRadians_ShouldUseYDegreesForYUp()
+ {
+ double radians = PathAnimationManager.ResolveGroundHostUpCorrectionRadians(
+ new LocalEulerRotationCorrection(10.0, 20.0, 30.0),
+ CoordinateSystemType.YUp);
+
+ Assert.AreEqual(20.0 * System.Math.PI / 180.0, radians, 1e-9);
+ }
+
+ [TestMethod]
+ public void ResolveGroundHostUpCorrectionRadians_ShouldUseZDegreesForZUp()
+ {
+ double radians = PathAnimationManager.ResolveGroundHostUpCorrectionRadians(
+ new LocalEulerRotationCorrection(10.0, 20.0, 30.0),
+ CoordinateSystemType.ZUp);
+
+ Assert.AreEqual(30.0 * System.Math.PI / 180.0, radians, 1e-9);
+ }
+
[TestMethod]
public void ResolveCurrentRotationBaseline_ShouldPreferActualGeometryForHoisting()
{
diff --git a/doc/working/2026-04-08-ground-remove-fragment-dependency-plan.md b/doc/working/2026-04-08-ground-remove-fragment-dependency-plan.md
index e99339b..1dad3c6 100644
--- a/doc/working/2026-04-08-ground-remove-fragment-dependency-plan.md
+++ b/doc/working/2026-04-08-ground-remove-fragment-dependency-plan.md
@@ -34,7 +34,15 @@
2. `Ground` 这条链不应该再扩散 `local/reference/fragment` 概念。
3. `fragment` 现在的问题,不在于“所有地方都要立刻删”,而在于:
- Ground 主链还会读它
- - 导致姿态来源不稳定
+ - 导致旋转来源不稳定
+4. `Ground` 这条链仍然有旋转目标,但这个旋转目标不再来自 `fragment` 参考姿态解释。
+5. `Ground` 后续只需要求:
+ - 路径在宿主平面里的方向
+ - 对应的宿主平面旋转量
+6. 也就是说,`Ground` 不再求“reference-based pose”,而只求:
+ - `hostForward`
+ - `hostUp`
+ - `host-planar rotation delta`
---
@@ -43,7 +51,7 @@
这轮只保留 3 个具体目标:
1. `Ground` 初始化时,不再优先读 fragment 参考姿态
-2. `Ground` 平面姿态求解时,不再走 fragment 参考旋转入口
+2. `Ground` 平面姿态求解时,不再走 fragment 参考旋转入口,而只求宿主平面旋转量
3. `Ground` 不再允许 fragment planar fallback
只要这 3 点做到,就算这一轮完成。
@@ -63,6 +71,7 @@
- 当 `PathType == Ground` 且是真实物体时
- 不再去走 `TryCaptureRealObjectReferenceRotation(...)`
- 直接改用当前实际几何姿态,或现有非 fragment 入口
+- 这一步的目的不是继续建立另一套“参考姿态定义”,而只是拿到当前增量起点
### 4.2 平面姿态求解入口
@@ -74,7 +83,11 @@
要求:
- `Ground` 不再从这里拿 fragment 参考旋转
-- `Ground` 单独走非 fragment 的姿态来源
+- `Ground` 不再继续求 reference-based pose
+- `Ground` 只求宿主平面旋转量:
+ - 路径 `hostForward`
+ - 宿主 `hostUp`
+ - 当前对象在宿主坐标系下要追加的平面旋转量
### 4.3 fallback 入口
@@ -94,7 +107,8 @@
1. 先改 `Ground` 初始化入口
2. 再改 `Ground` 平面姿态求解入口
-3. 最后关掉 `Ground` 的 fragment fallback
+3. 再改 `Ground` 尺寸/通行空间入口,避免它们继续偷偷走 `reference-based pose`
+4. 最后关掉 `Ground` 的 fragment fallback
每一步都要求:
@@ -102,6 +116,67 @@
- 只改 `Ground`
- 不顺手改别的路径
+### 5.1 2026-04-07 夜间分析结论
+
+第一刀已经证明:
+
+- `Ground` 初始化入口可以先切掉 fragment
+- 但这还不够
+
+这次日志暴露出的真正问题是:
+
+- `Ground` 虽然不再直接读 fragment
+- 但起点姿态求解仍然在走 `TryCreateRealObjectPlanarPoseSolution(...)`
+- 也就是仍然在走 `reference-based pose` 入口
+
+所以第二刀必须明确成:
+
+1. `Ground` 不再进入 `TryCreateRealObjectPlanarPoseSolution(...)`
+2. `Ground` 起点旋转改成:
+ - 当前显示旋转
+ - 当前宿主平面 yaw
+ - 路径目标宿主平面 yaw
+ - 基于这三者直接求目标旋转
+3. 也就是 `Ground` 只保留:
+ - 当前显示状态作为增量起点
+ - 路径方向作为目标方向
+ - 宿主平面旋转量作为唯一旋转语义
+
+### 5.2 现有链路与目标链路
+
+当前代码里,`Ground + 真实物体` 至少有 3 个入口会接触姿态:
+
+1. 起点
+ - `MoveObjectToPathStart(...)`
+ - `TryCreatePlanarPathRotationAtStart(...)`
+ - `TryCreateRealObjectPlanarRotationFromHostForward(...)`
+2. 逐帧
+ - `ApplyGroundAnimationFrame(...)`
+3. 尺寸/通行空间
+ - `TryCalculateCurrentRealObjectPlanarProjectedExtents(...)`
+
+这一轮的目标链路应该统一成:
+
+1. 初始化
+ - 当前显示姿态只用来拿“当前增量起点”
+ - 不再把它包装成 `referenceRotation`
+2. 起点/逐帧旋转
+ - 只求 `hostForward`
+ - 只求宿主平面旋转量
+ - 不再进入 `TryCreateRealObjectPlanarPoseSolution(...)`
+ - 起点应用层不再走 `currentRotation -> targetRotation -> deltaRotation`
+ - 改成直接施加宿主轴旋转增量,再补平移把 tracked point 拉回目标点
+3. 尺寸/通行空间
+ - 对 `Ground` 直接复用固定业务约定:
+ - `forward = PositiveX`
+ - `up = HostUp`
+ - 不再通过 `reference-based pose` 推导 `ModelAxisConvention`
+
+这 3 条链必须保持同一件事:
+
+- `Ground` 不再解释 reference pose
+- `Ground` 只解释“当前显示状态 + 路径方向 + 宿主平面旋转量”
+
---
## 6. 验证标准
@@ -112,9 +187,14 @@
- `Ground + 真实物体` 到起点后不再读 fragment 姿态
2. 逐帧
- - `Ground` 播放时姿态来源不再依赖 fragment
+ - `Ground` 播放时旋转来源不再依赖 fragment
+ - `Ground` 只根据路径方向继续求宿主平面旋转量
-3. fallback
+3. 尺寸/通行空间
+ - `Ground` 的 projected extents 不再因为 fragment/reference pose 缺失而失败
+ - `Ground` 的 `ModelAxisConvention` 不再来自 `TryCreateRealObjectPlanarPoseSolution(...)`
+
+4. fallback
- `Ground` 关闭 fragment fallback 后,主链要么成功,要么明确报错
- 不允许再偷偷回退
@@ -134,3 +214,41 @@
- 统一三类路径的所有姿态入口
这些都属于下一轮的事。
+
+---
+
+## 8. 2026-04-08 夜间新增:Ground 角度调整的最小实现策略
+
+在 `Ground + 真实物体` 这条新纯增量链上,角度调整不再通过“完整目标姿态重建”实现,而只允许走最单纯的宿主增量法:
+
+1. `up` 轴修正
+ - 不单独构造三维姿态
+ - 直接并入路径目标 `yaw`
+ - `YUp` 取 `YDegrees`
+ - `ZUp` 取 `ZDegrees`
+
+2. 非 `up` 轴修正
+ - 不与 `yaw` 一次性组合
+ - 只在起点落位后,围绕业务跟踪点按宿主轴逐次增量应用
+ - 当前约定:
+ - `YUp`:应用 `XDegrees`、`ZDegrees`
+ - `ZUp`:应用 `XDegrees`、`YDegrees`
+
+3. 逐帧播放
+ - 继续只吃“路径宿主平面角 + up 轴修正”
+ - 不在每帧重复叠加非 `up` 轴修正
+ - 非 `up` 轴修正应由起点姿态一次性建立,并在后续 `yaw` 增量中自然保持
+
+### 明早优先验证
+
+1. `Ground + 真实物体` 到起点
+ - 位置是否继续保持正确
+ - `X/Y/Z` 角度调整是否真正进入增量旋转链
+
+2. `Ground` 逐帧播放
+ - 转弯是否仍保持当前已修好的效果
+ - `up` 轴修正是否能跟随路径持续生效
+
+3. 非 `up` 轴修正
+ - 是否表现为“起点一次性按宿主轴旋转”
+ - 播放过程中不应每帧累加放大
diff --git a/src/Core/Animation/PathAnimationManager.cs b/src/Core/Animation/PathAnimationManager.cs
index 8cc6b3a..65cc77d 100644
--- a/src/Core/Animation/PathAnimationManager.cs
+++ b/src/Core/Animation/PathAnimationManager.cs
@@ -77,6 +77,7 @@ namespace NavisworksTransport.Core.Animation
public double Progress { get; set; } // 进度(0-1)
public Point3D Position { get; set; } // 该帧的位置
public double YawRadians { get; set; } // 绕Z轴的偏航角(弧度)
+ public double? GroundHostPlanarYawRadians { get; set; } // Ground真实物体逐帧宿主平面角
public Rotation3D Rotation { get; set; } // 可选的完整三维姿态
public bool HasCustomRotation { get; set; } // 是否使用完整三维姿态
public List Collisions { get; set; } // 该帧的碰撞结果
@@ -86,6 +87,7 @@ namespace NavisworksTransport.Core.Animation
{
Collisions = new List();
YawRadians = 0.0;
+ GroundHostPlanarYawRadians = null;
Rotation = Rotation3D.Identity;
HasCustomRotation = false;
}
@@ -738,8 +740,9 @@ namespace NavisworksTransport.Core.Animation
LogManager.Debug($"[移动到起点] 地面/空轨路径使用路径方向: {yaw * 180 / Math.PI:F2}度");
}
- // 旧 yaw 链仅保留给未进入完整三维姿态的遗留路径。
- if (!_objectRotationCorrection.IsZero)
+ // 旧 yaw 链仅保留给未进入 Ground 纯增量主链的遗留路径。
+ if (!_objectRotationCorrection.IsZero &&
+ !(_route.PathType == PathType.Ground && IsRealObjectMode))
{
double correctionRad = _objectRotationCorrection.ZDegrees * Math.PI / 180.0;
yaw += correctionRad;
@@ -802,7 +805,41 @@ namespace NavisworksTransport.Core.Animation
$"[路径起点诊断] 路径point0=({pathStartPoint.X:F3},{pathStartPoint.Y:F3},{pathStartPoint.Z:F3}), " +
$"起点目标trackedPoint=({startPosition.X:F3},{startPosition.Y:F3},{startPosition.Z:F3}), 路径类型={_route.PathType.GetDisplayName()}");
- if (_route.PathType != PathType.Rail &&
+ if (IsRealObjectMode &&
+ _route.PathType == PathType.Ground &&
+ TryApplyGroundStartIncrementalTransform(startPosition, out double groundTargetYawRadians))
+ {
+ LogManager.Info("[移动到起点] Ground 真实物体已应用纯增量旋转+平移");
+
+ var startAppliedPoint = GetLiveBoundingBoxCenter(CurrentControlledObject ?? _animatedObject);
+ LogManager.Info(
+ $"[路径起点诊断] 起点应用后实际包围盒中心=({startAppliedPoint.X:F3},{startAppliedPoint.Y:F3},{startAppliedPoint.Z:F3}), " +
+ $"相对目标偏差=({startAppliedPoint.X - startPosition.X:F3},{startAppliedPoint.Y - startPosition.Y:F3},{startAppliedPoint.Z - startPosition.Z:F3})");
+
+ var startOffset = new Vector3D(
+ startPosition.X - startAppliedPoint.X,
+ startPosition.Y - startAppliedPoint.Y,
+ startPosition.Z - startAppliedPoint.Z);
+ double startOffsetLength = Math.Sqrt(
+ startOffset.X * startOffset.X +
+ startOffset.Y * startOffset.Y +
+ startOffset.Z * startOffset.Z);
+
+ LogManager.Info(
+ $"[路径起点诊断] 落位偏差=({startOffset.X:F3},{startOffset.Y:F3},{startOffset.Z:F3}), 长度={startOffsetLength:F3} (仅记录,不二次补偿)");
+
+ _groundRealObjectBaseRotation = _trackedRotation;
+ _groundRealObjectBaseYaw = groundTargetYawRadians;
+ _hasGroundRealObjectBasePose = true;
+ _groundRealObjectStartCompensation = new Vector3D(0, 0, 0);
+ _hasGroundRealObjectStartCompensation = false;
+ _hasHoistingRealObjectBasePose = false;
+
+ LogManager.Info(
+ $"[Ground真实物体基姿态] {animatedObject.DisplayName} BaseYaw={_groundRealObjectBaseYaw * 180.0 / Math.PI:F2}°, " +
+ $"已记录基姿态={_hasGroundRealObjectBasePose} (纯增量链)");
+ }
+ else if (_route.PathType != PathType.Rail &&
TryCreatePlanarPathRotationAtStart(out var planarRotation))
{
UpdateObjectPosition(startPosition, planarRotation);
@@ -1336,7 +1373,23 @@ namespace NavisworksTransport.Core.Animation
Collisions = new List()
};
- if (ShouldPreservePathRotationForFrames(
+ if (ShouldUseGroundRealObjectPureIncrementFrames(_route.PathType, IsRealObjectMode))
+ {
+ if (!TryResolveGroundHostPlanarYawFromFramePoints(
+ previousFramePoint,
+ framePosition,
+ nextFramePoint,
+ CoordinateSystemManager.Instance.ResolvedType,
+ out double groundHostPlanarYawRadians))
+ {
+ throw new InvalidOperationException(
+ $"Ground真实物体第 {i} 帧未能解析宿主平面目标角。");
+ }
+
+ frame.GroundHostPlanarYawRadians = groundHostPlanarYawRadians;
+ frame.YawRadians = groundHostPlanarYawRadians;
+ }
+ else if (ShouldPreservePathRotationForFrames(
_route.PathType,
_objectStartPlacementMode,
_hasPathPreservedPoseRotation))
@@ -3947,7 +4000,76 @@ namespace NavisworksTransport.Core.Animation
internal static bool ShouldAllowFragmentPlanarFallback(PathType pathType)
{
- return pathType != PathType.Hoisting;
+ return pathType != PathType.Hoisting && pathType != PathType.Ground;
+ }
+
+ internal static bool ShouldUseReferenceBasedRealObjectPlanarPose(PathType pathType)
+ {
+ return pathType != PathType.Ground && pathType != PathType.Hoisting;
+ }
+
+ internal static bool ShouldUseGroundRealObjectPureIncrementFrames(PathType pathType, bool isRealObjectMode)
+ {
+ return pathType == PathType.Ground && isRealObjectMode;
+ }
+
+ internal static bool TryResolveDisplayedPlanarYawFromRotation(
+ Rotation3D rotation,
+ CoordinateSystemType hostType,
+ out double yawRadians)
+ {
+ yawRadians = 0.0;
+ if (rotation == null)
+ {
+ return false;
+ }
+
+ Matrix3 linear = new Transform3D(rotation).Linear;
+ Vector3 hostForward = new Vector3(
+ (float)linear.Get(0, 0),
+ (float)linear.Get(1, 0),
+ (float)linear.Get(2, 0));
+
+ return PathTargetFrameResolver.TryResolvePlanarHostYaw(hostForward, hostType, out yawRadians);
+ }
+
+ internal static double ResolveGroundHostUpCorrectionRadians(
+ LocalEulerRotationCorrection correction,
+ CoordinateSystemType hostType)
+ {
+ double degrees = hostType == CoordinateSystemType.YUp
+ ? correction.YDegrees
+ : correction.ZDegrees;
+ return degrees * Math.PI / 180.0;
+ }
+
+ internal static bool TryResolveGroundHostPlanarYawFromFramePoints(
+ Point3D previousPoint,
+ Point3D currentPoint,
+ Point3D nextPoint,
+ CoordinateSystemType hostType,
+ out double yawRadians)
+ {
+ yawRadians = 0.0;
+
+ Vector3 hostForward = new Vector3(
+ (float)(nextPoint.X - previousPoint.X),
+ (float)(nextPoint.Y - previousPoint.Y),
+ (float)(nextPoint.Z - previousPoint.Z));
+ if (hostForward.LengthSquared() < 1e-6f)
+ {
+ hostForward = new Vector3(
+ (float)(nextPoint.X - currentPoint.X),
+ (float)(nextPoint.Y - currentPoint.Y),
+ (float)(nextPoint.Z - currentPoint.Z));
+ }
+
+ if (hostForward.LengthSquared() < 1e-6f)
+ {
+ return false;
+ }
+
+ return PathTargetFrameResolver.TryResolvePlanarHostYaw(hostForward, hostType, out yawRadians);
}
internal static Rotation3D ResolveCurrentRotationBaseline(
@@ -4312,6 +4434,134 @@ namespace NavisworksTransport.Core.Animation
return PathTargetFrameResolver.TryResolvePlanarStartHostYaw(_route.PathType, hostPoints, hostType, out yawRadians);
}
+ private bool TryApplyGroundStartIncrementalTransform(Point3D targetTrackedPosition, out double targetYawRadians)
+ {
+ targetYawRadians = 0.0;
+
+ if (!IsRealObjectMode ||
+ _route?.PathType != PathType.Ground ||
+ CurrentControlledObject == null ||
+ _pathPoints == null ||
+ _pathPoints.Count < 2)
+ {
+ return false;
+ }
+
+ if (!PathTargetFrameResolver.TryResolvePlanarStartHostForward(
+ PathType.Ground,
+ _pathPoints,
+ out var hostForward))
+ {
+ LogManager.Error("[Ground纯增量起点] 无法解析路径起始方向。");
+ return false;
+ }
+
+ var adapter = CoordinateSystemManager.Instance.CreateHostAdapter();
+ if (!PathTargetFrameResolver.TryResolvePlanarHostYaw(hostForward, adapter.HostType, out targetYawRadians))
+ {
+ LogManager.Error("[Ground纯增量起点] 无法从路径方向解析宿主平面角。");
+ return false;
+ }
+
+ ApplyGroundNonUpAxisCorrectionsAtTrackedPosition(_trackedPosition, adapter.HostType);
+
+ Rotation3D currentRotation = Rotation3D.Identity;
+ if (!ModelItemTransformHelper.TryGetCurrentGeometryRotation(CurrentControlledObject, out currentRotation))
+ {
+ if (_hasTrackedRotation)
+ {
+ currentRotation = _trackedRotation;
+ }
+ else
+ {
+ LogManager.Error("[Ground纯增量起点] 无法读取当前显示旋转。");
+ return false;
+ }
+ }
+
+ if (!TryResolveDisplayedPlanarYawFromRotation(currentRotation, adapter.HostType, out double currentYawRadians))
+ {
+ LogManager.Error("[Ground纯增量起点] 无法从当前显示状态解析宿主平面角。");
+ return false;
+ }
+
+ double upAxisCorrectionRadians = ResolveGroundHostUpCorrectionRadians(
+ _objectRotationCorrection,
+ adapter.HostType);
+ targetYawRadians = NormalizeRadians(targetYawRadians + upAxisCorrectionRadians);
+ double deltaYawRadians = NormalizeRadians(targetYawRadians - currentYawRadians);
+ LogManager.Info(
+ $"[Ground纯增量起点] 当前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.MoveItemIncrementallyByAxisRotationAndTranslation(
+ CurrentControlledObject,
+ _trackedPosition,
+ adapter.HostUpVector3,
+ deltaYawRadians,
+ targetTrackedPosition);
+
+ _trackedPosition = targetTrackedPosition;
+
+ if (ModelItemTransformHelper.TryGetCurrentGeometryRotation(CurrentControlledObject, out var appliedRotation))
+ {
+ _trackedRotation = appliedRotation;
+ _hasTrackedRotation = true;
+ }
+ else
+ {
+ _trackedRotation = currentRotation;
+ _hasTrackedRotation = true;
+ }
+
+ _currentYaw = targetYawRadians;
+ LogHostActualPoseAxes(CurrentControlledObject, "[Ground纯增量起点应用后宿主姿态]", false);
+ return true;
+ }
+
+ private void ApplyGroundNonUpAxisCorrectionsAtTrackedPosition(
+ Point3D trackedPosition,
+ CoordinateSystemType hostType)
+ {
+ if (CurrentControlledObject == null || _objectRotationCorrection.IsZero)
+ {
+ return;
+ }
+
+ var correctionSteps = new List<(string AxisName, Vector3 HostAxis, double AngleRadians)>();
+ correctionSteps.Add(("X", Vector3.UnitX, _objectRotationCorrection.XDegrees * Math.PI / 180.0));
+
+ if (hostType == CoordinateSystemType.YUp)
+ {
+ correctionSteps.Add(("Z", Vector3.UnitZ, _objectRotationCorrection.ZDegrees * Math.PI / 180.0));
+ }
+ else
+ {
+ correctionSteps.Add(("Y", Vector3.UnitY, _objectRotationCorrection.YDegrees * Math.PI / 180.0));
+ }
+
+ foreach (var step in correctionSteps)
+ {
+ if (Math.Abs(step.AngleRadians) < 1e-9)
+ {
+ continue;
+ }
+
+ LogManager.Info(
+ $"[Ground纯增量起点角度修正] 宿主{step.AxisName}轴增量={step.AngleRadians * 180.0 / Math.PI:F2}°, " +
+ $"跟踪点=({trackedPosition.X:F3},{trackedPosition.Y:F3},{trackedPosition.Z:F3})");
+
+ ModelItemTransformHelper.MoveItemIncrementallyByAxisRotationAndTranslation(
+ CurrentControlledObject,
+ trackedPosition,
+ step.HostAxis,
+ step.AngleRadians,
+ trackedPosition);
+ }
+ }
+
private static double NormalizeRadians(double angle)
{
while (angle > Math.PI)
@@ -4509,7 +4759,7 @@ namespace NavisworksTransport.Core.Animation
convention = null;
extents = (0.0, 0.0, 0.0);
- bool requiresReferenceRotation = _route?.PathType != PathType.Hoisting;
+ bool requiresReferenceRotation = ShouldUseReferenceBasedRealObjectPlanarPose(_route?.PathType ?? PathType.Ground);
if (!IsRealObjectMode ||
(requiresReferenceRotation && !_hasRealObjectReferenceRotation) ||
_realObjectLength <= 0.0 ||
@@ -4526,31 +4776,22 @@ namespace NavisworksTransport.Core.Animation
}
if (_route?.PathType == PathType.Ground &&
- _hasGroundRealObjectBasePose &&
- TryCreateGroundRealObjectConstrainedRotationFromHostForward(hostForward, out var constrainedRotation))
+ TryCreateRealObjectPlanarRotationFromHostForward(hostForward, out var groundRotation))
{
- if (!TryCreateRealObjectPlanarPoseSolution(
- targetFrame.Forward,
- targetFrame.Up,
- out var constrainedSolution))
- {
- return false;
- }
-
- LocalAxisDirection constrainedSemanticUpAxis =
+ LocalAxisDirection groundSemanticUpAxis =
adapter.HostType == CoordinateSystemType.YUp
? LocalAxisDirection.PositiveY
: LocalAxisDirection.PositiveZ;
- convention = new ModelAxisConvention(constrainedSolution.SelectedReferenceAxisDirection, constrainedSemanticUpAxis);
+ convention = new ModelAxisConvention(LocalAxisDirection.PositiveX, groundSemanticUpAxis);
- Quaternion constrainedFinalHostQuaternion = Rotation3DToHostQuaternion(constrainedRotation);
+ Quaternion groundFinalHostQuaternion = Rotation3DToHostQuaternion(groundRotation);
extents = RealObjectProjectedExtentResolver.CalculateProjectedSemanticExtents(
convention,
_realObjectLength,
_realObjectWidth,
_realObjectHeight,
- constrainedSolution.BaselineRotation,
- constrainedFinalHostQuaternion,
+ groundFinalHostQuaternion,
+ groundFinalHostQuaternion,
targetFrame.Forward,
targetFrame.Up);
return true;
@@ -4961,6 +5202,54 @@ namespace NavisworksTransport.Core.Animation
return false;
}
+ var adapter = CoordinateSystemManager.Instance.CreateHostAdapter();
+ if (_route?.PathType == PathType.Ground)
+ {
+ Rotation3D actualRotation = Rotation3D.Identity;
+ bool hasActualRotation =
+ _animatedObject != null &&
+ ModelItemTransformHelper.TryGetCurrentGeometryRotation(_animatedObject, out actualRotation);
+
+ if (!hasActualRotation && _hasTrackedRotation)
+ {
+ actualRotation = _trackedRotation;
+ hasActualRotation = true;
+ }
+
+ if (!hasActualRotation)
+ {
+ LogManager.Error("[真实物体起点姿态] Ground 无法读取当前实际几何姿态,禁止回退到 reference-based pose。");
+ return false;
+ }
+
+ if (!TryResolveDisplayedPlanarYawFromRotation(actualRotation, adapter.HostType, out double currentYawRadians))
+ {
+ LogManager.Error("[真实物体起点姿态] Ground 无法从当前显示姿态解析宿主平面角。");
+ return false;
+ }
+
+ if (!PathTargetFrameResolver.TryResolvePlanarHostYaw(hostForward, adapter.HostType, out double targetYawRadians))
+ {
+ LogManager.Error("[真实物体起点姿态] Ground 无法从路径方向解析宿主平面角。");
+ return false;
+ }
+
+ Quaternion targetQuaternion = HoistingRealObjectPoseHelper.CreateRotationFromPlanarBasePose(
+ Rotation3DToHostQuaternion(actualRotation),
+ currentYawRadians,
+ targetYawRadians,
+ adapter.HostUpVector3);
+ rotation = adapter.FromHostQuaternionDirect(targetQuaternion);
+
+ Matrix4x4 targetLinear = Matrix4x4.CreateFromQuaternion(targetQuaternion);
+ LogManager.Debug(
+ $"[Ground宿主平面旋转] 当前Yaw={currentYawRadians * 180.0 / Math.PI:F2}°, 目标Yaw={targetYawRadians * 180.0 / Math.PI:F2}°, " +
+ $"hostXAxis=({targetLinear.M11:F4},{targetLinear.M21:F4},{targetLinear.M31:F4}), " +
+ $"hostYAxis=({targetLinear.M12:F4},{targetLinear.M22:F4},{targetLinear.M32:F4}), " +
+ $"hostZAxis=({targetLinear.M13:F4},{targetLinear.M23:F4},{targetLinear.M33:F4})");
+ return true;
+ }
+
if (!TryCreateRealObjectPlanarPoseSolution(
targetFrame.Forward,
targetFrame.Up,
@@ -4969,7 +5258,6 @@ namespace NavisworksTransport.Core.Animation
return false;
}
- var adapter = CoordinateSystemManager.Instance.CreateHostAdapter();
LocalEulerRotationCorrection localCorrection = ResolveRealObjectLocalRotationCorrection();
Quaternion hostComposedQuaternion = adapter.ComposeHostQuaternion(solution.BaselineRotation, localCorrection);
rotation = adapter.FromHostQuaternionDirect(hostComposedQuaternion);
@@ -5015,6 +5303,11 @@ namespace NavisworksTransport.Core.Animation
{
solution = null;
+ if (!ShouldUseReferenceBasedRealObjectPlanarPose(_route?.PathType ?? PathType.Ground))
+ {
+ return false;
+ }
+
if (!TryGetRealObjectReferenceRotation(out var referenceRotation))
{
return false;
@@ -5121,7 +5414,8 @@ namespace NavisworksTransport.Core.Animation
try
{
- if (TryCaptureRealObjectReferenceRotation(_animatedObject, out referenceRotation))
+ if (_route?.PathType != PathType.Ground &&
+ TryCaptureRealObjectReferenceRotation(_animatedObject, out referenceRotation))
{
return true;
}
@@ -5198,7 +5492,12 @@ namespace NavisworksTransport.Core.Animation
try
{
- Rotation3D fallbackRotation3D = sourceObject.Transform?.Factor().Rotation ?? Rotation3D.Identity;
+ Rotation3D fallbackRotation3D = Rotation3D.Identity;
+ if (!ModelItemTransformHelper.TryGetCurrentGeometryRotation(sourceObject, out fallbackRotation3D))
+ {
+ fallbackRotation3D = sourceObject.Transform?.Factor().Rotation ?? Rotation3D.Identity;
+ }
+
referenceRotation = new Quaternion(
(float)fallbackRotation3D.A,
(float)fallbackRotation3D.B,
@@ -5240,7 +5539,7 @@ namespace NavisworksTransport.Core.Animation
_hasRealObjectReferenceRotation = true;
LogManager.Warning(
- $"[真实物体参考姿态] {sourceObject.DisplayName} fragment 姿态不可用,已退回默认参考姿态继续业务流程: " +
+ $"[真实物体参考姿态] {sourceObject.DisplayName} 已改用非 fragment 参考姿态继续业务流程: " +
$"FallbackX=({_realObjectReferenceAxisX.X:F4},{_realObjectReferenceAxisX.Y:F4},{_realObjectReferenceAxisX.Z:F4}), " +
$"FallbackY=({_realObjectReferenceAxisY.X:F4},{_realObjectReferenceAxisY.Y:F4},{_realObjectReferenceAxisY.Z:F4}), " +
$"FallbackZ=({_realObjectReferenceAxisZ.X:F4},{_realObjectReferenceAxisZ.Y:F4},{_realObjectReferenceAxisZ.Z:F4}), " +
@@ -5279,6 +5578,23 @@ namespace NavisworksTransport.Core.Animation
return;
}
+ if (!isVirtualObject &&
+ _route?.PathType == PathType.Ground &&
+ ModelItemTransformHelper.TryGetCurrentGeometryRotation(sourceObject, out var groundActualGeometryRotation))
+ {
+ _trackedRotation = groundActualGeometryRotation;
+ _hasTrackedRotation = true;
+ _currentYaw = ModelItemTransformHelper.GetYawFromRotation(_trackedRotation);
+
+ var linear = new Transform3D(_trackedRotation).Linear;
+ LogManager.Info(
+ $"[真实物体当前姿态] {sourceObject.DisplayName} Ground 路径已改用实际几何姿态同步,不再读取 fragment 参考姿态: " +
+ $"X=({linear.Get(0, 0):F4},{linear.Get(1, 0):F4},{linear.Get(2, 0):F4}), " +
+ $"Y=({linear.Get(0, 1):F4},{linear.Get(1, 1):F4},{linear.Get(2, 1):F4}), " +
+ $"Z=({linear.Get(0, 2):F4},{linear.Get(1, 2):F4},{linear.Get(2, 2):F4})");
+ return;
+ }
+
if (!isVirtualObject && TryCaptureRealObjectReferenceRotation(sourceObject, out Quaternion referenceRotation))
{
var adapter = CoordinateSystemManager.Instance.CreateHostAdapter();
@@ -5574,17 +5890,14 @@ namespace NavisworksTransport.Core.Animation
double actualYaw = frameData.YawRadians;
- if (frameData.HasCustomRotation)
+ if (ShouldUseGroundRealObjectPureIncrementFrames(_route?.PathType ?? PathType.Ground, IsRealObjectMode) &&
+ frameData.GroundHostPlanarYawRadians.HasValue)
{
- // 🔥 Ground路径动画播放:使用 _trackedPosition 计算增量,不重新读取包围盒
- if (_route?.PathType == PathType.Ground && IsRealObjectMode)
- {
- ApplyGroundAnimationFrame(frameData.Position, frameData.Rotation);
- }
- else
- {
- UpdateObjectPosition(frameData.Position, frameData.Rotation);
- }
+ ApplyGroundAnimationFrame(frameData.Position, frameData.GroundHostPlanarYawRadians.Value);
+ }
+ else if (frameData.HasCustomRotation)
+ {
+ UpdateObjectPosition(frameData.Position, frameData.Rotation);
}
else if (_route?.PathType == PathType.Ground || _route?.PathType == PathType.Hoisting)
{
@@ -5600,28 +5913,41 @@ namespace NavisworksTransport.Core.Animation
///
/// 专门用于Ground路径动画帧播放。使用业务跟踪点 _trackedPosition 作为当前位置,不读取实时包围盒中心回写主链。
///
- private void ApplyGroundAnimationFrame(Point3D targetPosition, Rotation3D targetRotation)
+ private void ApplyGroundAnimationFrame(Point3D targetPosition, double targetYawRadians)
{
var controlledObject = CurrentControlledObject;
if (controlledObject == null) return;
- // 使用业务跟踪点 _trackedPosition 作为当前位置,不把实时包围盒中心混入主链
Point3D currentPosition = _trackedPosition;
- Rotation3D currentRotation = _hasTrackedRotation ? _trackedRotation : Rotation3D.Identity;
+ double currentYawRadians = _currentYaw;
+ targetYawRadians = NormalizeRadians(
+ targetYawRadians +
+ ResolveGroundHostUpCorrectionRadians(
+ _objectRotationCorrection,
+ CoordinateSystemManager.Instance.ResolvedType));
+ double deltaYawRadians = NormalizeRadians(targetYawRadians - currentYawRadians);
+ Vector3 hostUp = Vector3.Normalize(CoordinateSystemManager.Instance.CreateHostAdapter().HostUpVector3);
- // 简单增量变换,不使用包围盒
- ModelItemTransformHelper.MoveItemIncrementallyToPositionAndRotation(
+ LogManager.Debug(
+ $"[Ground纯增量逐帧] 当前Yaw={currentYawRadians * 180.0 / Math.PI:F2}°, " +
+ $"目标Yaw={targetYawRadians * 180.0 / Math.PI:F2}°, 增量Yaw={deltaYawRadians * 180.0 / Math.PI:F2}°, " +
+ $"当前跟踪点=({currentPosition.X:F3},{currentPosition.Y:F3},{currentPosition.Z:F3}), " +
+ $"目标跟踪点=({targetPosition.X:F3},{targetPosition.Y:F3},{targetPosition.Z:F3})");
+
+ ModelItemTransformHelper.MoveItemIncrementallyByAxisRotationAndTranslation(
controlledObject,
currentPosition,
- currentRotation,
- targetPosition,
- targetRotation);
+ hostUp,
+ deltaYawRadians,
+ targetPosition);
- // 更新跟踪状态
_trackedPosition = targetPosition;
- _trackedRotation = targetRotation;
- _hasTrackedRotation = true;
- _currentYaw = ModelItemTransformHelper.GetYawFromRotation(targetRotation);
+ if (ModelItemTransformHelper.TryGetCurrentGeometryRotation(controlledObject, out Rotation3D appliedRotation))
+ {
+ _trackedRotation = appliedRotation;
+ _hasTrackedRotation = true;
+ }
+ _currentYaw = targetYawRadians;
}
diff --git a/src/Utils/ModelItemTransformHelper.cs b/src/Utils/ModelItemTransformHelper.cs
index 52f858c..eb2f8d6 100644
--- a/src/Utils/ModelItemTransformHelper.cs
+++ b/src/Utils/ModelItemTransformHelper.cs
@@ -1,6 +1,7 @@
using System;
using Autodesk.Navisworks.Api;
using NavisworksTransport.Utils.CoordinateSystem;
+using System.Numerics;
namespace NavisworksTransport.Utils
{
@@ -591,6 +592,74 @@ namespace NavisworksTransport.Utils
LogIncrementalTransformActual(item, targetPosition, targetRotation);
}
+ ///
+ /// 直接对当前显示结果施加宿主轴旋转增量,再补一段平移把业务跟踪点拉回目标点。
+ /// 不先构造 targetRotation,也不从 current/target 姿态反推 deltaRotation。
+ ///
+ public static void MoveItemIncrementallyByAxisRotationAndTranslation(
+ ModelItem item,
+ Point3D currentPosition,
+ Vector3 hostAxis,
+ double deltaAngleRadians,
+ Point3D targetPosition)
+ {
+ if (item == null)
+ {
+ throw new ArgumentNullException(nameof(item));
+ }
+
+ if (hostAxis.LengthSquared() < 1e-12f)
+ {
+ throw new ArgumentException("hostAxis must be non-zero.", nameof(hostAxis));
+ }
+
+ Vector3 normalizedHostAxis = Vector3.Normalize(hostAxis);
+ Rotation3D deltaRotation = new Rotation3D(
+ new UnitVector3D(normalizedHostAxis.X, normalizedHostAxis.Y, normalizedHostAxis.Z),
+ deltaAngleRadians);
+ Matrix3 deltaLinear = new Transform3D(deltaRotation).Linear;
+ Point3D rotatedCurrentPosition = new Point3D(
+ deltaLinear.Get(0, 0) * currentPosition.X + deltaLinear.Get(0, 1) * currentPosition.Y + deltaLinear.Get(0, 2) * currentPosition.Z,
+ deltaLinear.Get(1, 0) * currentPosition.X + deltaLinear.Get(1, 1) * currentPosition.Y + deltaLinear.Get(1, 2) * currentPosition.Z,
+ deltaLinear.Get(2, 0) * currentPosition.X + deltaLinear.Get(2, 1) * currentPosition.Y + deltaLinear.Get(2, 2) * currentPosition.Z);
+
+ Vector3D compensatedTranslation = new Vector3D(
+ targetPosition.X - rotatedCurrentPosition.X,
+ targetPosition.Y - rotatedCurrentPosition.Y,
+ targetPosition.Z - rotatedCurrentPosition.Z);
+
+ LogManager.Debug(
+ $"[模型纯增量旋转平移] {item.DisplayName} 当前=({currentPosition.X:F3},{currentPosition.Y:F3},{currentPosition.Z:F3}), " +
+ $"目标=({targetPosition.X:F3},{targetPosition.Y:F3},{targetPosition.Z:F3}), " +
+ $"Axis=({normalizedHostAxis.X:F4},{normalizedHostAxis.Y:F4},{normalizedHostAxis.Z:F4}), " +
+ $"Angle={deltaAngleRadians * 180.0 / Math.PI:F2}°, " +
+ $"旋后当前点=({rotatedCurrentPosition.X:F3},{rotatedCurrentPosition.Y:F3},{rotatedCurrentPosition.Z:F3}), " +
+ $"平移=({compensatedTranslation.X:F3},{compensatedTranslation.Y:F3},{compensatedTranslation.Z:F3})");
+
+ var doc = Application.ActiveDocument;
+ var modelItems = new ModelItemCollection { item };
+ LogGeometryLevelTransforms(item, "[模型纯增量旋转平移应用前][GeometryAPI]");
+
+ var toOrigin = Transform3D.CreateTranslation(new Vector3D(
+ -currentPosition.X,
+ -currentPosition.Y,
+ -currentPosition.Z));
+ doc.Models.OverridePermanentTransform(modelItems, toOrigin, false);
+
+ var rotationOnlyComponents = Transform3D.CreateTranslation(new Vector3D(0, 0, 0)).Factor();
+ rotationOnlyComponents.Rotation = deltaRotation;
+ var rotationOnly = rotationOnlyComponents.Combine();
+ doc.Models.OverridePermanentTransform(modelItems, rotationOnly, false);
+
+ var toTarget = Transform3D.CreateTranslation(new Vector3D(
+ targetPosition.X,
+ targetPosition.Y,
+ targetPosition.Z));
+ doc.Models.OverridePermanentTransform(modelItems, toTarget, false);
+
+ LogGeometryLevelTransforms(item, "[模型纯增量旋转平移应用后][GeometryAPI]");
+ }
+
///
/// 读取物体当前实际几何姿态。
/// 优先使用 ModelGeometry.ActiveTransform,因为 ModelItem.Transform 只反映原始设计变换。
From 3892ddababe7d5dabf68f152cebac601ea64dd88 Mon Sep 17 00:00:00 2001
From: tian <11429339@qq.com>
Date: Thu, 9 Apr 2026 21:09:43 +0800
Subject: [PATCH 66/87] Stabilize ground incremental rotation and extent tests
---
NavisworksTransport.UnitTests.csproj | 1 +
.../RealObjectProjectedExtentResolverTests.cs | 79 ++++++
.../RotatedObjectExtentHelperTests.cs | 232 ++++++++++++++++++
src/Core/Animation/PathAnimationManager.cs | 49 +---
.../RealObjectProjectedExtentResolver.cs | 44 ++++
.../RotatedObjectExtentHelper.cs | 18 ++
6 files changed, 383 insertions(+), 40 deletions(-)
diff --git a/NavisworksTransport.UnitTests.csproj b/NavisworksTransport.UnitTests.csproj
index f26da39..701304c 100644
--- a/NavisworksTransport.UnitTests.csproj
+++ b/NavisworksTransport.UnitTests.csproj
@@ -73,6 +73,7 @@
+
diff --git a/UnitTests/CoordinateSystem/RealObjectProjectedExtentResolverTests.cs b/UnitTests/CoordinateSystem/RealObjectProjectedExtentResolverTests.cs
index 87fa470..595a088 100644
--- a/UnitTests/CoordinateSystem/RealObjectProjectedExtentResolverTests.cs
+++ b/UnitTests/CoordinateSystem/RealObjectProjectedExtentResolverTests.cs
@@ -51,6 +51,85 @@ namespace NavisworksTransport.UnitTests.CoordinateSystem
Assert.AreNotEqual(4.0, extents.upExtent, 1e-3, "双轴旋转后,Ground 的法线尺寸不应停留在旧高度。");
}
+ [TestMethod]
+ public void Ground_DisplayedHostAxesProjection_ShouldKeepXAxisLengthAndSwapXZAsAxesRotate()
+ {
+ Vector3 targetForward = Vector3.UnitX;
+ Vector3 targetUp = Vector3.UnitY;
+
+ var noCorrection = RealObjectProjectedExtentResolver.CalculateProjectedDisplayedExtents(
+ displayedAxisXSize: 6.0,
+ displayedAxisYSize: 2.0,
+ displayedAxisZSize: 4.0,
+ displayedHostAxisX: Vector3.UnitX,
+ displayedHostAxisY: Vector3.UnitZ,
+ displayedHostAxisZ: -Vector3.UnitY,
+ targetForward,
+ targetUp);
+
+ Assert.AreEqual(6.0, noCorrection.forwardExtent, 1e-5);
+ Assert.AreEqual(2.0, noCorrection.sideExtent, 1e-5);
+ Assert.AreEqual(4.0, noCorrection.upExtent, 1e-5);
+
+ var xRotated = RealObjectProjectedExtentResolver.CalculateProjectedDisplayedExtents(
+ displayedAxisXSize: 6.0,
+ displayedAxisYSize: 2.0,
+ displayedAxisZSize: 4.0,
+ displayedHostAxisX: Vector3.UnitX,
+ displayedHostAxisY: Vector3.UnitY,
+ displayedHostAxisZ: Vector3.UnitZ,
+ targetForward,
+ targetUp);
+
+ Assert.AreEqual(6.0, xRotated.forwardExtent, 1e-5);
+ Assert.AreEqual(4.0, xRotated.sideExtent, 1e-5);
+ Assert.AreEqual(2.0, xRotated.upExtent, 1e-5);
+
+ var zRotated = RealObjectProjectedExtentResolver.CalculateProjectedDisplayedExtents(
+ displayedAxisXSize: 6.0,
+ displayedAxisYSize: 2.0,
+ displayedAxisZSize: 4.0,
+ displayedHostAxisX: -Vector3.UnitZ,
+ displayedHostAxisY: Vector3.UnitX,
+ displayedHostAxisZ: -Vector3.UnitY,
+ targetForward,
+ targetUp);
+
+ Assert.AreEqual(2.0, zRotated.forwardExtent, 1e-5);
+ Assert.AreEqual(6.0, zRotated.sideExtent, 1e-5);
+ Assert.AreEqual(4.0, zRotated.upExtent, 1e-5);
+ }
+
+ [TestMethod]
+ public void Ground_DisplayedHostAxesProjection_ShouldIgnoreVerticalComponentInForward()
+ {
+ Vector3 targetUp = Vector3.UnitY;
+ Vector3 planarForward = Vector3.Normalize(new Vector3(1f, 0f, 1f));
+ Vector3 slopedForward = Vector3.Normalize(new Vector3(1f, 2f, 1f));
+
+ var planar = RealObjectProjectedExtentResolver.CalculateProjectedDisplayedExtents(
+ displayedAxisXSize: 6.0,
+ displayedAxisYSize: 2.0,
+ displayedAxisZSize: 4.0,
+ displayedHostAxisX: Vector3.UnitX,
+ displayedHostAxisY: Vector3.UnitZ,
+ displayedHostAxisZ: -Vector3.UnitY,
+ planarForward,
+ targetUp);
+
+ var sloped = RealObjectProjectedExtentResolver.CalculateProjectedDisplayedExtents(
+ displayedAxisXSize: 6.0,
+ displayedAxisYSize: 2.0,
+ displayedAxisZSize: 4.0,
+ displayedHostAxisX: Vector3.UnitX,
+ displayedHostAxisY: Vector3.UnitZ,
+ displayedHostAxisZ: -Vector3.UnitY,
+ slopedForward,
+ targetUp);
+
+ Assert.AreNotEqual(planar.forwardExtent, sloped.forwardExtent, 1e-3, "原始带竖直分量的路径方向会污染 Ground 尺寸投影。");
+ }
+
private static double ProjectExtent(
Vector3 localSize,
Vector3 rotatedLocalX,
diff --git a/UnitTests/CoordinateSystem/RotatedObjectExtentHelperTests.cs b/UnitTests/CoordinateSystem/RotatedObjectExtentHelperTests.cs
index e6785ae..ca93b9b 100644
--- a/UnitTests/CoordinateSystem/RotatedObjectExtentHelperTests.cs
+++ b/UnitTests/CoordinateSystem/RotatedObjectExtentHelperTests.cs
@@ -1,5 +1,6 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using NavisworksTransport.Utils.CoordinateSystem;
+using System;
using System.Numerics;
namespace NavisworksTransport.UnitTests.CoordinateSystem
@@ -86,5 +87,236 @@ namespace NavisworksTransport.UnitTests.CoordinateSystem
Assert.AreEqual(6.0, result.sideExtent, 1e-6);
Assert.AreEqual(2.0, result.upExtent, 1e-6);
}
+
+ [TestMethod]
+ public void Ground_YUp_HostY90_ShouldSwapForwardAndSide()
+ {
+ var result = RotatedObjectExtentHelper.CalculateGroundSemanticExtents(
+ CoordinateSystemType.YUp,
+ forwardSize: 6.0,
+ sideSize: 4.0,
+ upSize: 2.0,
+ hostCorrection: new LocalEulerRotationCorrection(0.0, 90.0, 0.0));
+
+ Assert.AreEqual(4.0, result.forwardExtent, 1e-6);
+ Assert.AreEqual(6.0, result.sideExtent, 1e-6);
+ Assert.AreEqual(2.0, result.upExtent, 1e-6);
+ }
+
+ [TestMethod]
+ public void Ground_YUp_HostY45_ShouldBlendForwardAndSide_AndKeepUp()
+ {
+ var result = RotatedObjectExtentHelper.CalculateGroundSemanticExtents(
+ CoordinateSystemType.YUp,
+ forwardSize: 6.0,
+ sideSize: 4.0,
+ upSize: 2.0,
+ hostCorrection: new LocalEulerRotationCorrection(0.0, 45.0, 0.0));
+
+ double expectedPlanar = 5.0 * Math.Sqrt(2.0);
+ Assert.AreEqual(expectedPlanar, result.forwardExtent, 1e-6);
+ Assert.AreEqual(expectedPlanar, result.sideExtent, 1e-6);
+ Assert.AreEqual(2.0, result.upExtent, 1e-6);
+ }
+
+ [TestMethod]
+ public void Ground_YUp_HostY135_ShouldBlendForwardAndSide_AndKeepUp()
+ {
+ var result = RotatedObjectExtentHelper.CalculateGroundSemanticExtents(
+ CoordinateSystemType.YUp,
+ forwardSize: 6.0,
+ sideSize: 4.0,
+ upSize: 2.0,
+ hostCorrection: new LocalEulerRotationCorrection(0.0, 135.0, 0.0));
+
+ double expectedPlanar = 5.0 * Math.Sqrt(2.0);
+ Assert.AreEqual(expectedPlanar, result.forwardExtent, 1e-6);
+ Assert.AreEqual(expectedPlanar, result.sideExtent, 1e-6);
+ Assert.AreEqual(2.0, result.upExtent, 1e-6);
+ }
+
+ [TestMethod]
+ public void Ground_YUp_HostY180_ShouldKeepSemanticExtents()
+ {
+ var result = RotatedObjectExtentHelper.CalculateGroundSemanticExtents(
+ CoordinateSystemType.YUp,
+ forwardSize: 6.0,
+ sideSize: 4.0,
+ upSize: 2.0,
+ hostCorrection: new LocalEulerRotationCorrection(0.0, 180.0, 0.0));
+
+ Assert.AreEqual(6.0, result.forwardExtent, 1e-6);
+ Assert.AreEqual(4.0, result.sideExtent, 1e-6);
+ Assert.AreEqual(2.0, result.upExtent, 1e-6);
+ }
+
+ [TestMethod]
+ public void Ground_YUp_HostY270_ShouldSwapForwardAndSide()
+ {
+ var result = RotatedObjectExtentHelper.CalculateGroundSemanticExtents(
+ CoordinateSystemType.YUp,
+ forwardSize: 6.0,
+ sideSize: 4.0,
+ upSize: 2.0,
+ hostCorrection: new LocalEulerRotationCorrection(0.0, 270.0, 0.0));
+
+ Assert.AreEqual(4.0, result.forwardExtent, 1e-6);
+ Assert.AreEqual(6.0, result.sideExtent, 1e-6);
+ Assert.AreEqual(2.0, result.upExtent, 1e-6);
+ }
+
+ [TestMethod]
+ public void Ground_YUp_HostX90_ShouldKeepForward_AndSwapSideWithUp()
+ {
+ var result = RotatedObjectExtentHelper.CalculateGroundSemanticExtents(
+ CoordinateSystemType.YUp,
+ forwardSize: 6.0,
+ sideSize: 4.0,
+ upSize: 2.0,
+ hostCorrection: new LocalEulerRotationCorrection(90.0, 0.0, 0.0));
+
+ Assert.AreEqual(6.0, result.forwardExtent, 1e-6);
+ Assert.AreEqual(2.0, result.sideExtent, 1e-6);
+ Assert.AreEqual(4.0, result.upExtent, 1e-6);
+ }
+
+ [TestMethod]
+ public void Ground_YUp_HostX45_ShouldKeepForward_AndBlendSideWithUp()
+ {
+ var result = RotatedObjectExtentHelper.CalculateGroundSemanticExtents(
+ CoordinateSystemType.YUp,
+ forwardSize: 6.0,
+ sideSize: 4.0,
+ upSize: 2.0,
+ hostCorrection: new LocalEulerRotationCorrection(45.0, 0.0, 0.0));
+
+ double expectedBlend = 3.0 * Math.Sqrt(2.0);
+ Assert.AreEqual(6.0, result.forwardExtent, 1e-6);
+ Assert.AreEqual(expectedBlend, result.sideExtent, 1e-6);
+ Assert.AreEqual(expectedBlend, result.upExtent, 1e-6);
+ }
+
+ [TestMethod]
+ public void Ground_YUp_HostX135_ShouldKeepForward_AndBlendSideWithUp()
+ {
+ var result = RotatedObjectExtentHelper.CalculateGroundSemanticExtents(
+ CoordinateSystemType.YUp,
+ forwardSize: 6.0,
+ sideSize: 4.0,
+ upSize: 2.0,
+ hostCorrection: new LocalEulerRotationCorrection(135.0, 0.0, 0.0));
+
+ double expectedBlend = 3.0 * Math.Sqrt(2.0);
+ Assert.AreEqual(6.0, result.forwardExtent, 1e-6);
+ Assert.AreEqual(expectedBlend, result.sideExtent, 1e-6);
+ Assert.AreEqual(expectedBlend, result.upExtent, 1e-6);
+ }
+
+ [TestMethod]
+ public void Ground_YUp_HostX180_ShouldKeepSemanticExtents()
+ {
+ var result = RotatedObjectExtentHelper.CalculateGroundSemanticExtents(
+ CoordinateSystemType.YUp,
+ forwardSize: 6.0,
+ sideSize: 4.0,
+ upSize: 2.0,
+ hostCorrection: new LocalEulerRotationCorrection(180.0, 0.0, 0.0));
+
+ Assert.AreEqual(6.0, result.forwardExtent, 1e-6);
+ Assert.AreEqual(4.0, result.sideExtent, 1e-6);
+ Assert.AreEqual(2.0, result.upExtent, 1e-6);
+ }
+
+ [TestMethod]
+ public void Ground_YUp_HostX270_ShouldKeepForward_AndSwapSideWithUp()
+ {
+ var result = RotatedObjectExtentHelper.CalculateGroundSemanticExtents(
+ CoordinateSystemType.YUp,
+ forwardSize: 6.0,
+ sideSize: 4.0,
+ upSize: 2.0,
+ hostCorrection: new LocalEulerRotationCorrection(270.0, 0.0, 0.0));
+
+ Assert.AreEqual(6.0, result.forwardExtent, 1e-6);
+ Assert.AreEqual(2.0, result.sideExtent, 1e-6);
+ Assert.AreEqual(4.0, result.upExtent, 1e-6);
+ }
+
+ [TestMethod]
+ public void Ground_YUp_HostZ90_ShouldPromoteUpToForward_AndKeepSide()
+ {
+ var result = RotatedObjectExtentHelper.CalculateGroundSemanticExtents(
+ CoordinateSystemType.YUp,
+ forwardSize: 6.0,
+ sideSize: 4.0,
+ upSize: 2.0,
+ hostCorrection: new LocalEulerRotationCorrection(0.0, 0.0, 90.0));
+
+ Assert.AreEqual(2.0, result.forwardExtent, 1e-6);
+ Assert.AreEqual(4.0, result.sideExtent, 1e-6);
+ Assert.AreEqual(6.0, result.upExtent, 1e-6);
+ }
+
+ [TestMethod]
+ public void Ground_YUp_HostZ45_ShouldBlendForwardWithUp_AndKeepSide()
+ {
+ var result = RotatedObjectExtentHelper.CalculateGroundSemanticExtents(
+ CoordinateSystemType.YUp,
+ forwardSize: 6.0,
+ sideSize: 4.0,
+ upSize: 2.0,
+ hostCorrection: new LocalEulerRotationCorrection(0.0, 0.0, 45.0));
+
+ double expectedBlend = 4.0 * Math.Sqrt(2.0);
+ Assert.AreEqual(expectedBlend, result.forwardExtent, 1e-6);
+ Assert.AreEqual(4.0, result.sideExtent, 1e-6);
+ Assert.AreEqual(expectedBlend, result.upExtent, 1e-6);
+ }
+
+ [TestMethod]
+ public void Ground_YUp_HostZ135_ShouldBlendForwardWithUp_AndKeepSide()
+ {
+ var result = RotatedObjectExtentHelper.CalculateGroundSemanticExtents(
+ CoordinateSystemType.YUp,
+ forwardSize: 6.0,
+ sideSize: 4.0,
+ upSize: 2.0,
+ hostCorrection: new LocalEulerRotationCorrection(0.0, 0.0, 135.0));
+
+ double expectedBlend = 4.0 * Math.Sqrt(2.0);
+ Assert.AreEqual(expectedBlend, result.forwardExtent, 1e-6);
+ Assert.AreEqual(4.0, result.sideExtent, 1e-6);
+ Assert.AreEqual(expectedBlend, result.upExtent, 1e-6);
+ }
+
+ [TestMethod]
+ public void Ground_YUp_HostZ180_ShouldKeepSemanticExtents()
+ {
+ var result = RotatedObjectExtentHelper.CalculateGroundSemanticExtents(
+ CoordinateSystemType.YUp,
+ forwardSize: 6.0,
+ sideSize: 4.0,
+ upSize: 2.0,
+ hostCorrection: new LocalEulerRotationCorrection(0.0, 0.0, 180.0));
+
+ Assert.AreEqual(6.0, result.forwardExtent, 1e-6);
+ Assert.AreEqual(4.0, result.sideExtent, 1e-6);
+ Assert.AreEqual(2.0, result.upExtent, 1e-6);
+ }
+
+ [TestMethod]
+ public void Ground_YUp_HostZ270_ShouldPromoteUpToForward_AndKeepSide()
+ {
+ var result = RotatedObjectExtentHelper.CalculateGroundSemanticExtents(
+ CoordinateSystemType.YUp,
+ forwardSize: 6.0,
+ sideSize: 4.0,
+ upSize: 2.0,
+ hostCorrection: new LocalEulerRotationCorrection(0.0, 0.0, 270.0));
+
+ Assert.AreEqual(2.0, result.forwardExtent, 1e-6);
+ Assert.AreEqual(4.0, result.sideExtent, 1e-6);
+ Assert.AreEqual(6.0, result.upExtent, 1e-6);
+ }
}
}
diff --git a/src/Core/Animation/PathAnimationManager.cs b/src/Core/Animation/PathAnimationManager.cs
index 65cc77d..fc3a74c 100644
--- a/src/Core/Animation/PathAnimationManager.cs
+++ b/src/Core/Animation/PathAnimationManager.cs
@@ -4465,30 +4465,11 @@ namespace NavisworksTransport.Core.Animation
ApplyGroundNonUpAxisCorrectionsAtTrackedPosition(_trackedPosition, adapter.HostType);
- Rotation3D currentRotation = Rotation3D.Identity;
- if (!ModelItemTransformHelper.TryGetCurrentGeometryRotation(CurrentControlledObject, out currentRotation))
- {
- if (_hasTrackedRotation)
- {
- currentRotation = _trackedRotation;
- }
- else
- {
- LogManager.Error("[Ground纯增量起点] 无法读取当前显示旋转。");
- return false;
- }
- }
-
- if (!TryResolveDisplayedPlanarYawFromRotation(currentRotation, adapter.HostType, out double currentYawRadians))
- {
- LogManager.Error("[Ground纯增量起点] 无法从当前显示状态解析宿主平面角。");
- return false;
- }
-
double upAxisCorrectionRadians = ResolveGroundHostUpCorrectionRadians(
_objectRotationCorrection,
adapter.HostType);
targetYawRadians = NormalizeRadians(targetYawRadians + upAxisCorrectionRadians);
+ double currentYawRadians = _currentYaw;
double deltaYawRadians = NormalizeRadians(targetYawRadians - currentYawRadians);
LogManager.Info(
$"[Ground纯增量起点] 当前Yaw={currentYawRadians * 180.0 / Math.PI:F2}°, " +
@@ -4510,11 +4491,6 @@ namespace NavisworksTransport.Core.Animation
_trackedRotation = appliedRotation;
_hasTrackedRotation = true;
}
- else
- {
- _trackedRotation = currentRotation;
- _hasTrackedRotation = true;
- }
_currentYaw = targetYawRadians;
LogHostActualPoseAxes(CurrentControlledObject, "[Ground纯增量起点应用后宿主姿态]", false);
@@ -4775,25 +4751,18 @@ namespace NavisworksTransport.Core.Animation
return false;
}
- if (_route?.PathType == PathType.Ground &&
- TryCreateRealObjectPlanarRotationFromHostForward(hostForward, out var groundRotation))
+ if (_route?.PathType == PathType.Ground)
{
- LocalAxisDirection groundSemanticUpAxis =
- adapter.HostType == CoordinateSystemType.YUp
- ? LocalAxisDirection.PositiveY
- : LocalAxisDirection.PositiveZ;
- convention = new ModelAxisConvention(LocalAxisDirection.PositiveX, groundSemanticUpAxis);
-
- Quaternion groundFinalHostQuaternion = Rotation3DToHostQuaternion(groundRotation);
- extents = RealObjectProjectedExtentResolver.CalculateProjectedSemanticExtents(
- convention,
+ convention = ModelAxisConvention.CreateDefaultForHost(adapter.HostType);
+ extents = RotatedObjectExtentHelper.CalculateGroundSemanticExtents(
+ adapter.HostType,
_realObjectLength,
_realObjectWidth,
_realObjectHeight,
- groundFinalHostQuaternion,
- groundFinalHostQuaternion,
- targetFrame.Forward,
- targetFrame.Up);
+ _objectRotationCorrection);
+ LogManager.Debug(
+ $"[Ground通行空间尺寸] 使用Ground宿主语义尺寸: " +
+ $"Forward={extents.forwardExtent:F3}, Side={extents.sideExtent:F3}, Up={extents.upExtent:F3}");
return true;
}
diff --git a/src/Utils/CoordinateSystem/RealObjectProjectedExtentResolver.cs b/src/Utils/CoordinateSystem/RealObjectProjectedExtentResolver.cs
index 1805346..c07af02 100644
--- a/src/Utils/CoordinateSystem/RealObjectProjectedExtentResolver.cs
+++ b/src/Utils/CoordinateSystem/RealObjectProjectedExtentResolver.cs
@@ -39,6 +39,50 @@ namespace NavisworksTransport.Utils.CoordinateSystem
ProjectExtent(localSize, rotatedLocalX, rotatedLocalY, rotatedLocalZ, normalizedTargetUp));
}
+ public static (double forwardExtent, double sideExtent, double upExtent) CalculateProjectedDisplayedExtents(
+ double displayedAxisXSize,
+ double displayedAxisYSize,
+ double displayedAxisZSize,
+ Vector3 displayedHostAxisX,
+ Vector3 displayedHostAxisY,
+ Vector3 displayedHostAxisZ,
+ Vector3 targetForward,
+ Vector3 targetUp)
+ {
+ Vector3 displayedSize = new Vector3(
+ (float)displayedAxisXSize,
+ (float)displayedAxisYSize,
+ (float)displayedAxisZSize);
+
+ Vector3 normalizedDisplayedHostAxisX = Vector3.Normalize(displayedHostAxisX);
+ Vector3 normalizedDisplayedHostAxisY = Vector3.Normalize(displayedHostAxisY);
+ Vector3 normalizedDisplayedHostAxisZ = Vector3.Normalize(displayedHostAxisZ);
+
+ Vector3 normalizedTargetForward = Vector3.Normalize(targetForward);
+ Vector3 normalizedTargetUp = Vector3.Normalize(targetUp);
+ Vector3 normalizedTargetSide = Vector3.Normalize(Vector3.Cross(normalizedTargetForward, normalizedTargetUp));
+
+ return (
+ ProjectExtent(
+ displayedSize,
+ normalizedDisplayedHostAxisX,
+ normalizedDisplayedHostAxisY,
+ normalizedDisplayedHostAxisZ,
+ normalizedTargetForward),
+ ProjectExtent(
+ displayedSize,
+ normalizedDisplayedHostAxisX,
+ normalizedDisplayedHostAxisY,
+ normalizedDisplayedHostAxisZ,
+ normalizedTargetSide),
+ ProjectExtent(
+ displayedSize,
+ normalizedDisplayedHostAxisX,
+ normalizedDisplayedHostAxisY,
+ normalizedDisplayedHostAxisZ,
+ normalizedTargetUp));
+ }
+
private static double ProjectExtent(
Vector3 localSize,
Vector3 rotatedLocalX,
diff --git a/src/Utils/CoordinateSystem/RotatedObjectExtentHelper.cs b/src/Utils/CoordinateSystem/RotatedObjectExtentHelper.cs
index 09d58b3..7637e54 100644
--- a/src/Utils/CoordinateSystem/RotatedObjectExtentHelper.cs
+++ b/src/Utils/CoordinateSystem/RotatedObjectExtentHelper.cs
@@ -8,6 +8,24 @@ namespace NavisworksTransport.Utils.CoordinateSystem
///
public static class RotatedObjectExtentHelper
{
+ public static (double forwardExtent, double sideExtent, double upExtent) CalculateGroundSemanticExtents(
+ CoordinateSystemType hostType,
+ double forwardSize,
+ double sideSize,
+ double upSize,
+ LocalEulerRotationCorrection hostCorrection)
+ {
+ var convention = ModelAxisConvention.CreateDefaultForHost(hostType);
+ var adapter = new HostCoordinateAdapter(hostType);
+ Quaternion correctionQuaternion = adapter.CreateHostRotationCorrection(hostCorrection);
+ return CalculateProjectedSemanticExtents(
+ convention,
+ forwardSize,
+ sideSize,
+ upSize,
+ correctionQuaternion);
+ }
+
public static (double forwardExtent, double sideExtent, double upExtent) CalculateProjectedSemanticExtents(
ModelAxisConvention convention,
double forwardSize,
From 4fbab52880582f7b9e98127762abb0e584aec898 Mon Sep 17 00:00:00 2001
From: tian <11429339@qq.com>
Date: Thu, 9 Apr 2026 23:08:59 +0800
Subject: [PATCH 67/87] clean legacy planar pose branches
---
.../PathTargetFrameResolverTests.cs | 19 +
.../RailPreservedPoseTests.cs | 24 +-
src/Core/Animation/PathAnimationManager.cs | 383 +++++-------------
3 files changed, 135 insertions(+), 291 deletions(-)
diff --git a/UnitTests/CoordinateSystem/PathTargetFrameResolverTests.cs b/UnitTests/CoordinateSystem/PathTargetFrameResolverTests.cs
index 6bfe5dc..8fe7ea9 100644
--- a/UnitTests/CoordinateSystem/PathTargetFrameResolverTests.cs
+++ b/UnitTests/CoordinateSystem/PathTargetFrameResolverTests.cs
@@ -76,6 +76,25 @@ namespace NavisworksTransport.UnitTests.CoordinateSystem
Assert.AreEqual(0.0, Vector3.Dot(frame.Forward, frame.Up), 1e-6);
}
+ [TestMethod]
+ public void Hoisting_StartForward_ShouldUseEditedRealPathHorizontalSegment_FromDebugLog()
+ {
+ var pathPoints = new List
+ {
+ new Vector3(-197.436f, 14.833f, 29.413f),
+ new Vector3(-197.436f, 31.240f, 29.413f),
+ new Vector3(-213.650f, 31.240f, 29.413f)
+ };
+
+ bool ok = PathTargetFrameResolver.TryResolvePlanarStartHostForward(
+ NavisworksTransport.PathType.Hoisting,
+ pathPoints,
+ out Vector3 hostForward);
+
+ Assert.IsTrue(ok);
+ AssertVector(Vector3.Normalize(hostForward), -1.0, 0.0, 0.0, 1e-6);
+ }
+
[TestMethod]
public void YUp_PlanarYaw_ShouldUseHostXZPlane_NotHostXY()
{
diff --git a/UnitTests/CoordinateSystem/RailPreservedPoseTests.cs b/UnitTests/CoordinateSystem/RailPreservedPoseTests.cs
index 97fe309..8382b75 100644
--- a/UnitTests/CoordinateSystem/RailPreservedPoseTests.cs
+++ b/UnitTests/CoordinateSystem/RailPreservedPoseTests.cs
@@ -123,12 +123,12 @@ namespace NavisworksTransport.UnitTests.CoordinateSystem
}
[TestMethod]
- public void ShouldUseGroundRealObjectPureIncrementFrames_ShouldEnableOnlyForGroundRealObjects()
+ public void ShouldUsePlanarRealObjectPureIncrementFrames_ShouldEnableOnlyForPlanarRealObjects()
{
- Assert.IsTrue(PathAnimationManager.ShouldUseGroundRealObjectPureIncrementFrames(PathType.Ground, true));
- Assert.IsFalse(PathAnimationManager.ShouldUseGroundRealObjectPureIncrementFrames(PathType.Ground, false));
- Assert.IsFalse(PathAnimationManager.ShouldUseGroundRealObjectPureIncrementFrames(PathType.Hoisting, true));
- Assert.IsFalse(PathAnimationManager.ShouldUseGroundRealObjectPureIncrementFrames(PathType.Rail, true));
+ Assert.IsTrue(PathAnimationManager.ShouldUsePlanarRealObjectPureIncrementFrames(PathType.Ground, true));
+ Assert.IsTrue(PathAnimationManager.ShouldUsePlanarRealObjectPureIncrementFrames(PathType.Hoisting, true));
+ Assert.IsFalse(PathAnimationManager.ShouldUsePlanarRealObjectPureIncrementFrames(PathType.Ground, false));
+ Assert.IsFalse(PathAnimationManager.ShouldUsePlanarRealObjectPureIncrementFrames(PathType.Rail, true));
}
[TestMethod]
@@ -146,9 +146,9 @@ namespace NavisworksTransport.UnitTests.CoordinateSystem
}
[TestMethod]
- public void ResolveGroundHostUpCorrectionRadians_ShouldUseYDegreesForYUp()
+ public void ResolvePlanarHostUpCorrectionRadians_ShouldUseYDegreesForYUp()
{
- double radians = PathAnimationManager.ResolveGroundHostUpCorrectionRadians(
+ double radians = PathAnimationManager.ResolvePlanarHostUpCorrectionRadians(
new LocalEulerRotationCorrection(10.0, 20.0, 30.0),
CoordinateSystemType.YUp);
@@ -156,9 +156,9 @@ namespace NavisworksTransport.UnitTests.CoordinateSystem
}
[TestMethod]
- public void ResolveGroundHostUpCorrectionRadians_ShouldUseZDegreesForZUp()
+ public void ResolvePlanarHostUpCorrectionRadians_ShouldUseZDegreesForZUp()
{
- double radians = PathAnimationManager.ResolveGroundHostUpCorrectionRadians(
+ double radians = PathAnimationManager.ResolvePlanarHostUpCorrectionRadians(
new LocalEulerRotationCorrection(10.0, 20.0, 30.0),
CoordinateSystemType.ZUp);
@@ -235,11 +235,5 @@ namespace NavisworksTransport.UnitTests.CoordinateSystem
isRealObjectMode: true));
}
- [TestMethod]
- public void ResolveHoistingPoseSourceLabel_ShouldDescribeCachedBasePose()
- {
- Assert.AreEqual("缓存基姿态", PathAnimationManager.ResolveHoistingPoseSourceLabel(true));
- Assert.AreEqual("实际几何姿态基线", PathAnimationManager.ResolveHoistingPoseSourceLabel(false));
- }
}
}
diff --git a/src/Core/Animation/PathAnimationManager.cs b/src/Core/Animation/PathAnimationManager.cs
index fc3a74c..5ef7fcc 100644
--- a/src/Core/Animation/PathAnimationManager.cs
+++ b/src/Core/Animation/PathAnimationManager.cs
@@ -77,7 +77,7 @@ namespace NavisworksTransport.Core.Animation
public double Progress { get; set; } // 进度(0-1)
public Point3D Position { get; set; } // 该帧的位置
public double YawRadians { get; set; } // 绕Z轴的偏航角(弧度)
- public double? GroundHostPlanarYawRadians { get; set; } // Ground真实物体逐帧宿主平面角
+ public double? PlanarHostYawRadians { get; set; } // 平面真实物体逐帧宿主平面角
public Rotation3D Rotation { get; set; } // 可选的完整三维姿态
public bool HasCustomRotation { get; set; } // 是否使用完整三维姿态
public List Collisions { get; set; } // 该帧的碰撞结果
@@ -87,7 +87,7 @@ namespace NavisworksTransport.Core.Animation
{
Collisions = new List();
YawRadians = 0.0;
- GroundHostPlanarYawRadians = null;
+ PlanarHostYawRadians = null;
Rotation = Rotation3D.Identity;
HasCustomRotation = false;
}
@@ -197,14 +197,6 @@ namespace NavisworksTransport.Core.Animation
private Rotation3D _groundRealObjectBaseRotation = Rotation3D.Identity;
private double _groundRealObjectBaseYaw = 0.0;
private bool _hasGroundRealObjectBasePose = false;
- private Rotation3D _hoistingRealObjectBaseRotation = Rotation3D.Identity;
- private double _hoistingRealObjectBaseYaw = 0.0;
- private bool _hasHoistingRealObjectBasePose = false;
- private Rotation3D _lastResolvedHoistingStartBaselineRotation = Rotation3D.Identity;
- private bool _hasLastResolvedHoistingStartBaselineRotation = false;
- private Vector3D _groundRealObjectStartCompensation = new Vector3D(0, 0, 0);
- private bool _hasGroundRealObjectStartCompensation = false;
- private bool _suppressGroundRealObjectCompensation = false;
private Vector3D _railPreservedPoseTrackedCenterOffset = new Vector3D(0, 0, 0);
private bool _hasRailPreservedPoseTrackedCenterOffset = false;
@@ -489,8 +481,6 @@ namespace NavisworksTransport.Core.Animation
_trackedPosition = ResolveInitialBusinessTrackedPosition(objectToRestore);
string objectName = isVirtual ? "虚拟物体" : objectToRestore.DisplayName;
- _groundRealObjectStartCompensation = new Vector3D(0, 0, 0);
- _hasGroundRealObjectStartCompensation = false;
_railPreservedPoseTrackedCenterOffset = new Vector3D(0, 0, 0);
_hasRailPreservedPoseTrackedCenterOffset = false;
LogManager.Info($"[归位] {objectName} 已彻底恢复到原始位置, yaw={_currentYaw:F3}");
@@ -719,34 +709,13 @@ namespace NavisworksTransport.Core.Animation
return false;
}
- // 计算朝向(使用前两个路径点的方向)
- double pathDirectionYaw = Math.Atan2(_pathPoints[1].Y - _pathPoints[0].Y, _pathPoints[1].X - _pathPoints[0].X);
- double yaw;
+ bool shouldUsePlanarPureIncrementStart =
+ IsRealObjectMode &&
+ (_route.PathType == PathType.Ground || _route.PathType == PathType.Hoisting);
- LogManager.Info($"[移动到起点] 路径方向yaw: {pathDirectionYaw * 180 / Math.PI:F2}度, 角度修正: {_objectRotationCorrection}");
-
- // 根据路径类型调整朝向
- if (_route.PathType == PathType.Hoisting)
+ if (shouldUsePlanarPureIncrementStart)
{
- // 吊装路径:使用水平吊运方向(与通行空间方向一致)
- // 从第2个路径点(提升点)到第3个路径点(平移终点)的水平方向
- yaw = Math.Atan2(_pathPoints[2].Y - _pathPoints[1].Y, _pathPoints[2].X - _pathPoints[1].X);
- LogManager.Debug($"[移动到起点] 吊装路径使用水平吊运方向: {yaw * 180 / Math.PI:F2}度");
- }
- else
- {
- // 地面路径和空轨路径:直接使用路径方向
- yaw = pathDirectionYaw;
- LogManager.Debug($"[移动到起点] 地面/空轨路径使用路径方向: {yaw * 180 / Math.PI:F2}度");
- }
-
- // 旧 yaw 链仅保留给未进入 Ground 纯增量主链的遗留路径。
- if (!_objectRotationCorrection.IsZero &&
- !(_route.PathType == PathType.Ground && IsRealObjectMode))
- {
- double correctionRad = _objectRotationCorrection.ZDegrees * Math.PI / 180.0;
- yaw += correctionRad;
- LogManager.Debug($"[移动到起点] 应用遗留Z轴修正: {_objectRotationCorrection.ZDegrees:F1}°, 修正后yaw: {yaw * 180 / Math.PI:F2}度");
+ LogManager.Info($"[移动到起点] {_route.PathType.GetDisplayName()} 真实物体起点改用纯增量姿态主链");
}
// 根据路径类型调整起点位置
@@ -805,11 +774,10 @@ namespace NavisworksTransport.Core.Animation
$"[路径起点诊断] 路径point0=({pathStartPoint.X:F3},{pathStartPoint.Y:F3},{pathStartPoint.Z:F3}), " +
$"起点目标trackedPoint=({startPosition.X:F3},{startPosition.Y:F3},{startPosition.Z:F3}), 路径类型={_route.PathType.GetDisplayName()}");
- if (IsRealObjectMode &&
- _route.PathType == PathType.Ground &&
- TryApplyGroundStartIncrementalTransform(startPosition, out double groundTargetYawRadians))
+ if (shouldUsePlanarPureIncrementStart &&
+ TryApplyPlanarRealObjectStartIncrementalTransform(_route.PathType, startPosition, out double planarTargetYawRadians))
{
- LogManager.Info("[移动到起点] Ground 真实物体已应用纯增量旋转+平移");
+ LogManager.Info($"[移动到起点] {_route.PathType.GetDisplayName()} 真实物体已应用纯增量旋转+平移");
var startAppliedPoint = GetLiveBoundingBoxCenter(CurrentControlledObject ?? _animatedObject);
LogManager.Info(
@@ -828,16 +796,25 @@ namespace NavisworksTransport.Core.Animation
LogManager.Info(
$"[路径起点诊断] 落位偏差=({startOffset.X:F3},{startOffset.Y:F3},{startOffset.Z:F3}), 长度={startOffsetLength:F3} (仅记录,不二次补偿)");
- _groundRealObjectBaseRotation = _trackedRotation;
- _groundRealObjectBaseYaw = groundTargetYawRadians;
- _hasGroundRealObjectBasePose = true;
- _groundRealObjectStartCompensation = new Vector3D(0, 0, 0);
- _hasGroundRealObjectStartCompensation = false;
- _hasHoistingRealObjectBasePose = false;
+ if (_route.PathType == PathType.Ground)
+ {
+ _groundRealObjectBaseRotation = _trackedRotation;
+ _groundRealObjectBaseYaw = planarTargetYawRadians;
+ _hasGroundRealObjectBasePose = true;
- LogManager.Info(
- $"[Ground真实物体基姿态] {animatedObject.DisplayName} BaseYaw={_groundRealObjectBaseYaw * 180.0 / Math.PI:F2}°, " +
- $"已记录基姿态={_hasGroundRealObjectBasePose} (纯增量链)");
+ LogManager.Info(
+ $"[Ground真实物体基姿态] {animatedObject.DisplayName} BaseYaw={_groundRealObjectBaseYaw * 180.0 / Math.PI:F2}°, " +
+ $"已记录基姿态={_hasGroundRealObjectBasePose} (纯增量链)");
+ }
+ else
+ {
+ _hasGroundRealObjectBasePose = false;
+ }
+ }
+ else if (shouldUsePlanarPureIncrementStart)
+ {
+ LogManager.Error($"[移动到起点] {_route.PathType.GetDisplayName()} 真实物体纯增量起点失败,已禁止退回旧姿态链。");
+ return false;
}
else if (_route.PathType != PathType.Rail &&
TryCreatePlanarPathRotationAtStart(out var planarRotation))
@@ -865,9 +842,6 @@ namespace NavisworksTransport.Core.Animation
LogManager.Info(
$"[路径起点诊断] 落位偏差=({startOffset.X:F3},{startOffset.Y:F3},{startOffset.Z:F3}), 长度={startOffsetLength:F3} (仅记录,不二次补偿)");
- // 不再设置 _groundRealObjectStartCompensation,也不再调用二次 UpdateObjectPosition
- _groundRealObjectStartCompensation = new Vector3D(0, 0, 0);
- _hasGroundRealObjectStartCompensation = false;
}
if (IsRealObjectMode && _route.PathType == PathType.Ground)
@@ -880,26 +854,10 @@ namespace NavisworksTransport.Core.Animation
}
else if (IsRealObjectMode && _route.PathType == PathType.Hoisting)
{
- if (_hasLastResolvedHoistingStartBaselineRotation)
- {
- _hoistingRealObjectBaseRotation = _lastResolvedHoistingStartBaselineRotation;
- }
- else if (!TryCreateHoistingPlanarBaseRotationAtStart(out _hoistingRealObjectBaseRotation))
- {
- _hoistingRealObjectBaseRotation = planarRotation;
- }
-
- _hasHoistingRealObjectBasePose = TryResolvePlanarRealObjectBaseYaw(PathType.Hoisting, out _hoistingRealObjectBaseYaw);
- LogManager.Debug(
- $"[Hoisting真实物体基姿态] {(animatedObject ?? _animatedObject)?.DisplayName} BaseYaw={_hoistingRealObjectBaseYaw * 180.0 / Math.PI:F2}°, " +
- $"已记录基姿态={_hasHoistingRealObjectBasePose}, 姿态将复用于预览/生成/播放");
}
else
{
_hasGroundRealObjectBasePose = false;
- _hasHoistingRealObjectBasePose = false;
- _groundRealObjectStartCompensation = new Vector3D(0, 0, 0);
- _hasGroundRealObjectStartCompensation = false;
}
}
else if (_route.PathType == PathType.Ground || _route.PathType == PathType.Hoisting)
@@ -907,19 +865,14 @@ namespace NavisworksTransport.Core.Animation
LogManager.Error("[移动到起点] 平面路径未能生成完整姿态,已禁止退回旧 yaw 链路");
return false;
}
- else
+ else if (_route.PathType == PathType.Rail)
{
- // 使用 UpdateObjectPosition 统一处理移动和旋转
- UpdateObjectPosition(startPosition, yaw);
-
- var startAppliedPoint = GetLiveBoundingBoxCenter(CurrentControlledObject ?? _animatedObject);
- LogManager.Info(
- $"[路径起点诊断] 起点应用后实际包围盒中心=({startAppliedPoint.X:F3},{startAppliedPoint.Y:F3},{startAppliedPoint.Z:F3}), " +
- $"相对目标偏差=({startAppliedPoint.X - startPosition.X:F3},{startAppliedPoint.Y - startPosition.Y:F3},{startAppliedPoint.Z - startPosition.Z:F3})");
+ LogManager.Error("[移动到起点] Rail路径未能生成完整三维姿态,已禁止退回旧 yaw 链路");
+ return false;
}
string pathTypeName = _route.PathType.GetDisplayName();
- LogManager.Info($"物体已初始化到路径起点并对齐朝向: pos=({startPosition.X:F2},{startPosition.Y:F2},{startPosition.Z:F2}), yaw={yaw:F3}rad, 路径类型={pathTypeName}");
+ LogManager.Info($"物体已初始化到路径起点并对齐朝向: pos=({startPosition.X:F2},{startPosition.Y:F2},{startPosition.Z:F2}), 路径类型={pathTypeName}");
return true;
}
catch (Exception ex)
@@ -1022,9 +975,6 @@ namespace NavisworksTransport.Core.Animation
UpdateObjectPosition(startPosition);
SyncTrackedRotationToDisplayedPose(CurrentControlledObject ?? _animatedObject);
_hasGroundRealObjectBasePose = false;
- _hasHoistingRealObjectBasePose = false;
- _groundRealObjectStartCompensation = new Vector3D(0, 0, 0);
- _hasGroundRealObjectStartCompensation = false;
var startAppliedPoint = GetLiveBoundingBoxCenter(CurrentControlledObject ?? _animatedObject);
LogManager.Info(
@@ -1373,21 +1323,22 @@ namespace NavisworksTransport.Core.Animation
Collisions = new List()
};
- if (ShouldUseGroundRealObjectPureIncrementFrames(_route.PathType, IsRealObjectMode))
+ if (ShouldUsePlanarRealObjectPureIncrementFrames(_route.PathType, IsRealObjectMode))
{
- if (!TryResolveGroundHostPlanarYawFromFramePoints(
+ double planarHostYawRadians = yawRadians;
+ if (_route.PathType == PathType.Ground &&
+ !TryResolveGroundHostPlanarYawFromFramePoints(
previousFramePoint,
framePosition,
nextFramePoint,
CoordinateSystemManager.Instance.ResolvedType,
- out double groundHostPlanarYawRadians))
+ out planarHostYawRadians))
{
throw new InvalidOperationException(
- $"Ground真实物体第 {i} 帧未能解析宿主平面目标角。");
+ $"{_route.PathType.GetDisplayName()}真实物体第 {i} 帧未能解析宿主平面目标角。");
}
- frame.GroundHostPlanarYawRadians = groundHostPlanarYawRadians;
- frame.YawRadians = groundHostPlanarYawRadians;
+ frame.PlanarHostYawRadians = planarHostYawRadians;
}
else if (ShouldPreservePathRotationForFrames(
_route.PathType,
@@ -1517,11 +1468,7 @@ namespace NavisworksTransport.Core.Animation
LogManager.Debug($"移动物体位置: {framePosition.X:F2},{framePosition.Y:F2},{framePosition.Z:F2}");
LogManager.Debug($"被撞物体位置:{GetObjectPosition(collider).X:F2},{GetObjectPosition(collider).Y:F2},{GetObjectPosition(collider).Z:F2}");
- double actualYawRadians = yawRadians;
- if (!frame.HasCustomRotation && !_objectRotationCorrection.IsZero)
- {
- actualYawRadians += _objectRotationCorrection.ZDegrees * Math.PI / 180.0;
- }
+ double actualYawRadians = ResolveAnimationFramePlaybackYawRadians(frame);
var collisionResult = new CollisionResult
{
@@ -1841,7 +1788,7 @@ namespace NavisworksTransport.Core.Animation
Math.Pow(currentPos.Y - firstFrame.Position.Y, 2)
);
- var yawDiff = Math.Abs(_currentYaw - firstFrame.YawRadians);
+ var yawDiff = Math.Abs(_currentYaw - ResolveAnimationFramePlaybackYawRadians(firstFrame));
if (yawDiff > Math.PI) yawDiff = 2 * Math.PI - yawDiff;
needsReset = distanceToStart > 0.1 || yawDiff > 0.01;
@@ -1923,7 +1870,9 @@ namespace NavisworksTransport.Core.Animation
// 🔥 确保物体在起点位置和朝向
// 注意:MoveObjectToPathStart 已在 StartAnimation 中调用,这里只是日志记录
- LogManager.Debug($"[动画开始] 物体应在起点: pos=({firstFrame.Position.X:F2},{firstFrame.Position.Y:F2}), yaw={firstFrame.YawRadians:F3}rad");
+ LogManager.Debug(
+ $"[动画开始] 物体应在起点: pos=({firstFrame.Position.X:F2},{firstFrame.Position.Y:F2}), " +
+ $"yaw={ResolveAnimationFramePlaybackYawRadians(firstFrame):F3}rad");
}
else
{
@@ -3085,22 +3034,13 @@ namespace NavisworksTransport.Core.Animation
$"Z=({linear.Get(0, 2):F4},{linear.Get(1, 2):F4},{linear.Get(2, 2):F4})");
}
- bool previousSuppressCompensation = _suppressGroundRealObjectCompensation;
- _suppressGroundRealObjectCompensation = true;
- try
+ if (_savedObjectHasCustomRotation)
{
- if (_savedObjectHasCustomRotation)
- {
- UpdateObjectPosition(_savedObjectPosition, _savedObjectRotation);
- }
- else
- {
- UpdateObjectPosition(_savedObjectPosition, _savedObjectYaw);
- }
+ UpdateObjectPosition(_savedObjectPosition, _savedObjectRotation);
}
- finally
+ else
{
- _suppressGroundRealObjectCompensation = previousSuppressCompensation;
+ UpdateObjectPosition(_savedObjectPosition, _savedObjectYaw);
}
_trackedPosition = _savedObjectPosition;
@@ -3299,7 +3239,7 @@ namespace NavisworksTransport.Core.Animation
}
else
{
- UpdateObjectPosition(frameData.Position, frameData.YawRadians);
+ UpdateObjectPosition(frameData.Position, ResolveAnimationFramePlaybackYawRadians(frameData));
}
// 更新碰撞高亮
@@ -4008,9 +3948,10 @@ namespace NavisworksTransport.Core.Animation
return pathType != PathType.Ground && pathType != PathType.Hoisting;
}
- internal static bool ShouldUseGroundRealObjectPureIncrementFrames(PathType pathType, bool isRealObjectMode)
+ internal static bool ShouldUsePlanarRealObjectPureIncrementFrames(PathType pathType, bool isRealObjectMode)
{
- return pathType == PathType.Ground && isRealObjectMode;
+ return isRealObjectMode &&
+ (pathType == PathType.Ground || pathType == PathType.Hoisting);
}
internal static bool TryResolveDisplayedPlanarYawFromRotation(
@@ -4033,7 +3974,7 @@ namespace NavisworksTransport.Core.Animation
return PathTargetFrameResolver.TryResolvePlanarHostYaw(hostForward, hostType, out yawRadians);
}
- internal static double ResolveGroundHostUpCorrectionRadians(
+ internal static double ResolvePlanarHostUpCorrectionRadians(
LocalEulerRotationCorrection correction,
CoordinateSystemType hostType)
{
@@ -4261,14 +4202,12 @@ namespace NavisworksTransport.Core.Animation
selectedAxisDirection = LocalAxisDirection.PositiveX;
selectedAxisLocal = Vector3.UnitX;
projectedForward = Vector3.Zero;
- string sourceLabel;
if (!TryResolveHoistingStartBaselinePose(
hostForward,
out baselineQuaternion,
out selectedAxisDirection,
out selectedAxisLocal,
- out projectedForward,
- out sourceLabel))
+ out projectedForward))
{
rotation = Rotation3D.Identity;
return false;
@@ -4278,15 +4217,13 @@ namespace NavisworksTransport.Core.Animation
Quaternion finalHostQuaternion = adapter.ComposeHostQuaternion(
baselineQuaternion,
_objectRotationCorrection);
- _lastResolvedHoistingStartBaselineRotation = adapter.FromHostQuaternionDirect(baselineQuaternion);
- _hasLastResolvedHoistingStartBaselineRotation = true;
rotation = adapter.FromHostQuaternionDirect(finalHostQuaternion);
Matrix4x4 hostBaselineLocalAxesLinear = Matrix4x4.CreateFromQuaternion(baselineQuaternion);
Matrix4x4 hostComposedLocalAxesLinear = Matrix4x4.CreateFromQuaternion(finalHostQuaternion);
LogManager.Debug(
- $"[真实物体平面前进轴] Hoisting 已改用{sourceLabel},选中对象轴={selectedAxisDirection}。");
+ $"[真实物体平面前进轴] Hoisting 已改用实际几何姿态基线,选中对象轴={selectedAxisDirection}。");
LogManager.Debug(
$"[真实物体起点姿态] 选中参考轴=({selectedAxisLocal.X:F3},{selectedAxisLocal.Y:F3},{selectedAxisLocal.Z:F3}), " +
$"投影前进=({projectedForward.X:F3},{projectedForward.Y:F3},{projectedForward.Z:F3}), " +
@@ -4305,8 +4242,7 @@ namespace NavisworksTransport.Core.Animation
out Quaternion baselineQuaternion,
out LocalAxisDirection selectedAxisDirection,
out Vector3 selectedAxisLocal,
- out Vector3 projectedForward,
- out string sourceLabel)
+ out Vector3 projectedForward)
{
baselineQuaternion = Quaternion.Identity;
selectedAxisDirection = _hasRealObjectPlanarSelectedForwardAxis
@@ -4324,41 +4260,6 @@ namespace NavisworksTransport.Core.Animation
projectedForward = planarProjectedForward.LengthSquared() > 1e-6f
? Vector3.Normalize(planarProjectedForward)
: Vector3.UnitX;
-
- if (_hasHoistingRealObjectBasePose)
- {
- sourceLabel = ResolveHoistingPoseSourceLabel(true);
- Quaternion cachedBaseQuaternion = Rotation3DToHostQuaternion(_hoistingRealObjectBaseRotation);
- if (!PathTargetFrameResolver.TryCreatePlanarHostFrame(
- projectedForward,
- CoordinateSystemManager.Instance.ResolvedType,
- out var currentFrame))
- {
- return false;
- }
-
- if (!PathTargetFrameResolver.TryResolvePlanarHostYaw(
- currentFrame.Forward,
- CoordinateSystemManager.Instance.ResolvedType,
- out double targetYawRadians))
- {
- return false;
- }
-
- baselineQuaternion = HoistingRealObjectPoseHelper.CreateRotationFromPlanarBasePose(
- cachedBaseQuaternion,
- _hoistingRealObjectBaseYaw,
- targetYawRadians,
- Vector3.Normalize(adapter.HostUpVector3));
- selectedAxisLocal = ResolveReferenceWorldAxisForLocalDirection(
- selectedAxisDirection,
- _realObjectReferenceAxisX,
- _realObjectReferenceAxisY,
- _realObjectReferenceAxisZ);
- return true;
- }
-
- sourceLabel = ResolveHoistingPoseSourceLabel(false);
Rotation3D sourceRotation;
if (_animatedObject == null ||
!ModelItemTransformHelper.TryGetCurrentGeometryRotation(_animatedObject, out sourceRotation))
@@ -4377,11 +4278,6 @@ namespace NavisworksTransport.Core.Animation
out projectedForward);
}
- internal static string ResolveHoistingPoseSourceLabel(bool hasHoistingBasePose)
- {
- return hasHoistingBasePose ? "缓存基姿态" : "实际几何姿态基线";
- }
-
private static Quaternion Rotation3DToHostQuaternion(Rotation3D rotation)
{
var linear = new Transform3D(rotation).Linear;
@@ -4393,26 +4289,6 @@ namespace NavisworksTransport.Core.Animation
return Quaternion.Normalize(Quaternion.CreateFromRotationMatrix(matrix));
}
- private bool TryRotateGroundRealObjectCompensation(Rotation3D targetRotation, out Vector3D rotatedCompensation)
- {
- rotatedCompensation = _groundRealObjectStartCompensation;
- if (!_hasGroundRealObjectBasePose || !_hasGroundRealObjectStartCompensation)
- {
- return false;
- }
-
- Quaternion baseQuaternion = Rotation3DToHostQuaternion(_groundRealObjectBaseRotation);
- Quaternion targetQuaternion = Rotation3DToHostQuaternion(targetRotation);
- Quaternion deltaQuaternion = Quaternion.Normalize(targetQuaternion * Quaternion.Inverse(baseQuaternion));
- Vector3 baseCompensation = new Vector3(
- (float)_groundRealObjectStartCompensation.X,
- (float)_groundRealObjectStartCompensation.Y,
- (float)_groundRealObjectStartCompensation.Z);
- Vector3 rotatedVector = Vector3.Transform(baseCompensation, deltaQuaternion);
- rotatedCompensation = new Vector3D(rotatedVector.X, rotatedVector.Y, rotatedVector.Z);
- return true;
- }
-
private bool TryResolveGroundRealObjectBaseYaw(out double yawRadians)
{
yawRadians = 0.0;
@@ -4434,12 +4310,16 @@ namespace NavisworksTransport.Core.Animation
return PathTargetFrameResolver.TryResolvePlanarStartHostYaw(_route.PathType, hostPoints, hostType, out yawRadians);
}
- private bool TryApplyGroundStartIncrementalTransform(Point3D targetTrackedPosition, out double targetYawRadians)
+ private bool TryApplyPlanarRealObjectStartIncrementalTransform(
+ PathType pathType,
+ Point3D targetTrackedPosition,
+ out double targetYawRadians)
{
targetYawRadians = 0.0;
if (!IsRealObjectMode ||
- _route?.PathType != PathType.Ground ||
+ (pathType != PathType.Ground && pathType != PathType.Hoisting) ||
+ _route?.PathType != pathType ||
CurrentControlledObject == null ||
_pathPoints == null ||
_pathPoints.Count < 2)
@@ -4448,31 +4328,31 @@ namespace NavisworksTransport.Core.Animation
}
if (!PathTargetFrameResolver.TryResolvePlanarStartHostForward(
- PathType.Ground,
+ pathType,
_pathPoints,
out var hostForward))
{
- LogManager.Error("[Ground纯增量起点] 无法解析路径起始方向。");
+ LogManager.Error($"[{pathType.GetDisplayName()}纯增量起点] 无法解析路径起始方向。");
return false;
}
var adapter = CoordinateSystemManager.Instance.CreateHostAdapter();
if (!PathTargetFrameResolver.TryResolvePlanarHostYaw(hostForward, adapter.HostType, out targetYawRadians))
{
- LogManager.Error("[Ground纯增量起点] 无法从路径方向解析宿主平面角。");
+ LogManager.Error($"[{pathType.GetDisplayName()}纯增量起点] 无法从路径方向解析宿主平面角。");
return false;
}
- ApplyGroundNonUpAxisCorrectionsAtTrackedPosition(_trackedPosition, adapter.HostType);
+ ApplyPlanarNonUpAxisCorrectionsAtTrackedPosition(pathType, _trackedPosition, adapter.HostType);
- double upAxisCorrectionRadians = ResolveGroundHostUpCorrectionRadians(
+ double upAxisCorrectionRadians = ResolvePlanarHostUpCorrectionRadians(
_objectRotationCorrection,
adapter.HostType);
targetYawRadians = NormalizeRadians(targetYawRadians + upAxisCorrectionRadians);
double currentYawRadians = _currentYaw;
double deltaYawRadians = NormalizeRadians(targetYawRadians - currentYawRadians);
LogManager.Info(
- $"[Ground纯增量起点] 当前Yaw={currentYawRadians * 180.0 / Math.PI:F2}°, " +
+ $"[{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})");
@@ -4493,11 +4373,12 @@ namespace NavisworksTransport.Core.Animation
}
_currentYaw = targetYawRadians;
- LogHostActualPoseAxes(CurrentControlledObject, "[Ground纯增量起点应用后宿主姿态]", false);
+ LogHostActualPoseAxes(CurrentControlledObject, $"[{pathType.GetDisplayName()}纯增量起点应用后宿主姿态]", false);
return true;
}
- private void ApplyGroundNonUpAxisCorrectionsAtTrackedPosition(
+ private void ApplyPlanarNonUpAxisCorrectionsAtTrackedPosition(
+ PathType pathType,
Point3D trackedPosition,
CoordinateSystemType hostType)
{
@@ -4526,7 +4407,7 @@ namespace NavisworksTransport.Core.Animation
}
LogManager.Info(
- $"[Ground纯增量起点角度修正] 宿主{step.AxisName}轴增量={step.AngleRadians * 180.0 / Math.PI:F2}°, " +
+ $"[{pathType.GetDisplayName()}纯增量起点角度修正] 宿主{step.AxisName}轴增量={step.AngleRadians * 180.0 / Math.PI:F2}°, " +
$"跟踪点=({trackedPosition.X:F3},{trackedPosition.Y:F3},{trackedPosition.Z:F3})");
ModelItemTransformHelper.MoveItemIncrementallyByAxisRotationAndTranslation(
@@ -4751,7 +4632,7 @@ namespace NavisworksTransport.Core.Animation
return false;
}
- if (_route?.PathType == PathType.Ground)
+ if (_route?.PathType == PathType.Ground || _route?.PathType == PathType.Hoisting)
{
convention = ModelAxisConvention.CreateDefaultForHost(adapter.HostType);
extents = RotatedObjectExtentHelper.CalculateGroundSemanticExtents(
@@ -4761,43 +4642,11 @@ namespace NavisworksTransport.Core.Animation
_realObjectHeight,
_objectRotationCorrection);
LogManager.Debug(
- $"[Ground通行空间尺寸] 使用Ground宿主语义尺寸: " +
+ $"[{_route.PathType.GetDisplayName()}通行空间尺寸] 使用宿主语义尺寸: " +
$"Forward={extents.forwardExtent:F3}, Side={extents.sideExtent:F3}, Up={extents.upExtent:F3}");
return true;
}
- if (_route?.PathType == PathType.Hoisting)
- {
- if (!TryResolveHoistingActualPose(
- hostForward,
- out var hoistingRotation,
- out var baselineQuaternion,
- out var selectedAxisDirection,
- out _,
- out _))
- {
- return false;
- }
-
- LocalAxisDirection hoistingSemanticUpAxis =
- adapter.HostType == CoordinateSystemType.YUp
- ? LocalAxisDirection.PositiveY
- : LocalAxisDirection.PositiveZ;
- convention = new ModelAxisConvention(selectedAxisDirection, hoistingSemanticUpAxis);
-
- Quaternion hoistingFinalHostQuaternion = Rotation3DToHostQuaternion(hoistingRotation);
- extents = RealObjectProjectedExtentResolver.CalculateProjectedSemanticExtents(
- convention,
- _realObjectLength,
- _realObjectWidth,
- _realObjectHeight,
- baselineQuaternion,
- hoistingFinalHostQuaternion,
- targetFrame.Forward,
- targetFrame.Up);
- return true;
- }
-
if (!TryCreateRealObjectPlanarPoseSolution(
targetFrame.Forward,
targetFrame.Up,
@@ -4995,43 +4844,6 @@ namespace NavisworksTransport.Core.Animation
return false;
}
- private bool TryCreateHoistingPlanarBaseRotationAtStart(out Rotation3D rotation)
- {
- rotation = Rotation3D.Identity;
-
- if (!IsRealObjectMode ||
- _route?.PathType != PathType.Hoisting ||
- _pathPoints == null ||
- _pathPoints.Count < 2)
- {
- return false;
- }
-
- if (!PathTargetFrameResolver.TryResolvePlanarStartHostForward(
- PathType.Hoisting,
- _pathPoints,
- out var hostForward))
- {
- return false;
- }
-
- if (!TryResolveHoistingStartBaselinePose(
- hostForward,
- out var baselineQuaternion,
- out _,
- out _,
- out _,
- out _))
- {
- return false;
- }
-
- rotation = CoordinateSystemManager.Instance
- .CreateHostAdapter()
- .FromHostQuaternionDirect(baselineQuaternion);
- return true;
- }
-
private bool TryCreatePlanarPathRotationForFrame(Point3D previousPoint, Point3D currentPoint, Point3D nextPoint, out Rotation3D rotation)
{
rotation = Rotation3D.Identity;
@@ -5613,11 +5425,6 @@ namespace NavisworksTransport.Core.Animation
_groundRealObjectBaseRotation = Rotation3D.Identity;
_groundRealObjectBaseYaw = 0.0;
_hasGroundRealObjectBasePose = false;
- _hoistingRealObjectBaseRotation = Rotation3D.Identity;
- _hoistingRealObjectBaseYaw = 0.0;
- _hasHoistingRealObjectBasePose = false;
- _lastResolvedHoistingStartBaselineRotation = Rotation3D.Identity;
- _hasLastResolvedHoistingStartBaselineRotation = false;
}
private LocalEulerRotationCorrection ResolveRealObjectLocalRotationCorrection()
@@ -5857,12 +5664,10 @@ namespace NavisworksTransport.Core.Animation
return;
}
- double actualYaw = frameData.YawRadians;
-
- if (ShouldUseGroundRealObjectPureIncrementFrames(_route?.PathType ?? PathType.Ground, IsRealObjectMode) &&
- frameData.GroundHostPlanarYawRadians.HasValue)
+ if (ShouldUsePlanarRealObjectPureIncrementFrames(_route?.PathType ?? PathType.Ground, IsRealObjectMode) &&
+ frameData.PlanarHostYawRadians.HasValue)
{
- ApplyGroundAnimationFrame(frameData.Position, frameData.GroundHostPlanarYawRadians.Value);
+ ApplyPlanarAnimationFrame(frameData.Position, frameData.PlanarHostYawRadians.Value);
}
else if (frameData.HasCustomRotation)
{
@@ -5875,14 +5680,39 @@ namespace NavisworksTransport.Core.Animation
}
else
{
- UpdateObjectPosition(frameData.Position, actualYaw);
+ UpdateObjectPosition(frameData.Position, ResolveAnimationFramePlaybackYawRadians(frameData));
}
}
+ private double ResolveAnimationFramePlaybackYawRadians(AnimationFrame frameData)
+ {
+ if (frameData == null)
+ {
+ return 0.0;
+ }
+
+ if (frameData.PlanarHostYawRadians.HasValue)
+ {
+ return NormalizeRadians(
+ frameData.PlanarHostYawRadians.Value +
+ ResolvePlanarHostUpCorrectionRadians(
+ _objectRotationCorrection,
+ CoordinateSystemManager.Instance.ResolvedType));
+ }
+
+ double resolvedYaw = frameData.YawRadians;
+ if (!frameData.HasCustomRotation && !_objectRotationCorrection.IsZero)
+ {
+ resolvedYaw += _objectRotationCorrection.ZDegrees * Math.PI / 180.0;
+ }
+
+ return NormalizeRadians(resolvedYaw);
+ }
+
///
/// 专门用于Ground路径动画帧播放。使用业务跟踪点 _trackedPosition 作为当前位置,不读取实时包围盒中心回写主链。
///
- private void ApplyGroundAnimationFrame(Point3D targetPosition, double targetYawRadians)
+ private void ApplyPlanarAnimationFrame(Point3D targetPosition, double targetYawRadians)
{
var controlledObject = CurrentControlledObject;
if (controlledObject == null) return;
@@ -5891,14 +5721,15 @@ namespace NavisworksTransport.Core.Animation
double currentYawRadians = _currentYaw;
targetYawRadians = NormalizeRadians(
targetYawRadians +
- ResolveGroundHostUpCorrectionRadians(
+ 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();
LogManager.Debug(
- $"[Ground纯增量逐帧] 当前Yaw={currentYawRadians * 180.0 / Math.PI:F2}°, " +
+ $"[{pathTypeName}纯增量逐帧] 当前Yaw={currentYawRadians * 180.0 / Math.PI:F2}°, " +
$"目标Yaw={targetYawRadians * 180.0 / Math.PI:F2}°, 增量Yaw={deltaYawRadians * 180.0 / Math.PI:F2}°, " +
$"当前跟踪点=({currentPosition.X:F3},{currentPosition.Y:F3},{currentPosition.Z:F3}), " +
$"目标跟踪点=({targetPosition.X:F3},{targetPosition.Y:F3},{targetPosition.Z:F3})");
From 144ee4e6b2c1adcb94c104dc47c02de8e79aafdc Mon Sep 17 00:00:00 2001
From: tian <11429339@qq.com>
Date: Thu, 9 Apr 2026 23:09:56 +0800
Subject: [PATCH 68/87] refine hoisting path fixes
---
...sworks-transform-api-official-reference.md | 134 ++++++++++++++++++
1 file changed, 134 insertions(+)
diff --git a/doc/working/2026-04-06-navisworks-transform-api-official-reference.md b/doc/working/2026-04-06-navisworks-transform-api-official-reference.md
index a8be570..7a8899b 100644
--- a/doc/working/2026-04-06-navisworks-transform-api-official-reference.md
+++ b/doc/working/2026-04-06-navisworks-transform-api-official-reference.md
@@ -1010,3 +1010,137 @@ incremental = target * current.Inverse()
- **只有最简单的宿主坐标系增量法,才能尽量无副作用地做真实物体变换。**
- **旋转和平移不需要物体局部映射,局部坐标的概念可以从这条变换链里彻底移除。**
+
+---
+
+## 28. 2026-04-09 Ground 真实物体增量链追加结论
+
+以下结论来自 2026-04-09 对 `Ground + 真实物体` 的连续回归:
+
+- 起点落位
+- `X / Y / Z` 角度调整
+- 逐帧动画
+- 通行空间尺寸
+
+以及同一对象在 Navisworks 中的实际可视结果与日志对照。
+
+### 28.1 Ground 角度调整的正确主语
+
+对 `Ground + 真实物体`,角度调整的正确主语仍然只有:
+
+- 宿主世界 `X` 轴
+- 宿主世界 `Y` 轴
+- 宿主世界 `Z` 轴
+
+也就是说:
+
+- `XDegrees` 就是绕宿主 `X` 轴的增量
+- `YDegrees` 就是绕宿主 `Y` 轴的增量
+- `ZDegrees` 就是绕宿主 `Z` 轴的增量
+
+不需要再额外解释:
+
+- 物体当前局部轴
+- 参考姿态
+- 基姿态
+- fragment 代表姿态
+- 局部轴业务映射
+
+### 28.2 角度调整必须按“单轴增量”逐次应用
+
+本轮实验已经验证:
+
+- `Ground` 的角度调整不能先重建“完整目标姿态”
+- 也不能把多个轴的旋转先合成为一份“总姿态”再去应用
+
+更稳定的工程做法是:
+
+1. 对当前物体直接叠加一个宿主轴旋转增量
+2. 一次只处理一个轴
+3. 非 `up` 轴修正做完后,再处理路径方向对应的 `yaw`
+4. 最后再做平移补偿 / tracked point 对齐
+
+一句话:
+
+- **Ground 真实物体的角度调整应实现为“直接对物体叠加宿主轴增量”,而不是“先解释姿态,再重建目标姿态”。**
+
+### 28.3 不要从当前显示姿态反解新的平面角
+
+本轮实际问题证明:
+
+- 对 `X / Z` 非 `up` 轴修正,如果先应用一次宿主轴增量
+- 再从修正后的当前显示姿态里重新反解 `currentYaw`
+- 很容易把非平面旋转的结果错误混入后续平面转向链
+
+对 `Ground` 这条链,更稳定的做法是:
+
+- 非 `up` 轴修正只负责自身的单轴增量旋转
+- 路径 `yaw` 仍然只作为“平面转向”处理
+- 不再把前一步的三维旋转结果重新解释成新的 `yaw` 起点
+
+### 28.4 Ground 通行空间的正确数学模型
+
+对 `Ground + 真实物体` 的通行空间,当前验证通过的最简单数学模型是:
+
+1. 先确定一组固定的宿主语义尺寸:
+ - `forward`
+ - `side`
+ - `up`
+2. 再只根据宿主轴角度修正,计算旋转后的:
+ - `forwardExtent`
+ - `sideExtent`
+ - `upExtent`
+3. 不再把这组三个尺寸二次投影到 `pathForward`
+
+原因是:
+
+- 渲染器本身已经会把:
+ - `along`
+ - `across`
+ - `normal`
+ 摆到路径上
+- 如果在尺寸链里再按路径方向投影一次,就会把路径方向重复计算
+
+因此对 `Ground` 通行空间,更合理的输入应当是:
+
+- 角度修正后的宿主语义尺寸
+
+而不是:
+
+- 再投影到路径方向后的尺寸
+
+### 28.5 当前已补齐的最小数学测试集合
+
+本轮已经在:
+
+- [RotatedObjectExtentHelperTests.cs](/C:/Users/Tellme/apps/NavisworksTransport-rail-mount-modes/UnitTests/CoordinateSystem/RotatedObjectExtentHelperTests.cs)
+
+补齐 `Ground / YUp` 下的宿主轴尺寸测试,包括:
+
+- `X / Y / Z`
+- `45 / 90 / 135 / 180 / 270`
+
+这些测试锁住的不是业务解释,而是最基础的数学结论:
+
+- 一个长方体在宿主语义尺寸下
+- 经宿主轴旋转后
+- `forward / side / up` 三个尺寸应如何变化
+
+### 28.6 当前可以正式采用的 Ground 工程约束
+
+对 `Ground + 真实物体`,当前可以正式采用以下工程约束:
+
+1. 起点、角度调整、逐帧动画都优先走宿主坐标系增量链。
+2. `X / Y / Z` 角度调整一次只处理一个宿主轴,不重建总姿态。
+3. `yaw` 只处理平面路径转向,不再承担三维姿态解释职责。
+4. 通行空间只消费“角度调整后的宿主语义尺寸”,不再重复按路径方向投影。
+5. 这条链不再传播:
+ - 基姿态
+ - 参考姿态
+ - 局部轴映射
+ - fragment 代表姿态
+ 这些概念作为变换主语。
+
+一句话总结:
+
+- **Ground 真实物体这条链,已经证明“宿主轴单轴增量 + 平面 yaw + 平移补偿”是目前最稳定、最可控的实现方式。**
From 6020bfa97957986748369a61dc7215f581775a10 Mon Sep 17 00:00:00 2001
From: tian <11429339@qq.com>
Date: Thu, 9 Apr 2026 23:28:12 +0800
Subject: [PATCH 69/87] fix hoisting descend point reuse
---
NavisworksTransport.UnitTests.csproj | 1 +
...hPlanningManagerHoistingCompletionTests.cs | 47 +++++++++++++++++++
src/Core/PathPlanningManager.cs | 42 ++++++++++-------
3 files changed, 73 insertions(+), 17 deletions(-)
create mode 100644 UnitTests/Core/PathPlanningManagerHoistingCompletionTests.cs
diff --git a/NavisworksTransport.UnitTests.csproj b/NavisworksTransport.UnitTests.csproj
index 701304c..d9d8cc5 100644
--- a/NavisworksTransport.UnitTests.csproj
+++ b/NavisworksTransport.UnitTests.csproj
@@ -54,6 +54,7 @@
+
diff --git a/UnitTests/Core/PathPlanningManagerHoistingCompletionTests.cs b/UnitTests/Core/PathPlanningManagerHoistingCompletionTests.cs
new file mode 100644
index 0000000..537f574
--- /dev/null
+++ b/UnitTests/Core/PathPlanningManagerHoistingCompletionTests.cs
@@ -0,0 +1,47 @@
+using System.Collections.Generic;
+using Autodesk.Navisworks.Api;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace NavisworksTransport.UnitTests.Core
+{
+ [TestClass]
+ public class PathPlanningManagerHoistingCompletionTests
+ {
+ [TestMethod]
+ public void ReuseLastHoistingPointAsDescendPoint_ShouldPreserveGeometryAndAvoidDuplicateSegment_FromRealLogCase()
+ {
+ var originalLastAerialPointPosition = new Point3D(-132.36, -16.89, -6.95);
+ var pathPoints = new List
+ {
+ new PathPoint(new Point3D(-140.00, -30.01, -6.95), "起吊点", PathPointType.StartPoint) { Index = 0, Direction = HoistingPointDirection.Vertical },
+ new PathPoint(new Point3D(-140.00, -16.89, -6.95), "提升点", PathPointType.WayPoint) { Index = 1, Direction = HoistingPointDirection.Vertical },
+ new PathPoint(new Point3D(-150.00, -16.89, -6.95), "路径点7", PathPointType.WayPoint) { Index = 2, Direction = HoistingPointDirection.Longitudinal },
+ new PathPoint(originalLastAerialPointPosition, "路径点8", PathPointType.WayPoint) { Index = 3, Direction = HoistingPointDirection.Longitudinal }
+ };
+
+ var finalGroundPoint = new Point3D(-132.36, -30.01, -6.95);
+ var originalLastPoint = pathPoints[3];
+
+ PathPoint descendPoint = PathPlanningManager.ReuseLastHoistingPointAsDescendPoint(pathPoints);
+ var endPoint = new PathPoint(finalGroundPoint, "落地点", PathPointType.EndPoint)
+ {
+ Index = pathPoints.Count,
+ Direction = HoistingPointDirection.Vertical
+ };
+ pathPoints.Add(endPoint);
+
+ Assert.AreEqual(5, pathPoints.Count);
+ Assert.AreSame(pathPoints[3], descendPoint);
+ Assert.AreSame(originalLastPoint, descendPoint);
+ Assert.AreEqual("下降点", pathPoints[3].Name);
+ Assert.AreEqual(PathPointType.WayPoint, pathPoints[3].Type);
+ Assert.AreEqual(HoistingPointDirection.Vertical, pathPoints[3].Direction);
+ Assert.AreSame(originalLastAerialPointPosition, pathPoints[3].Position);
+
+ Assert.AreEqual("落地点", pathPoints[4].Name);
+ Assert.AreEqual(PathPointType.EndPoint, pathPoints[4].Type);
+ Assert.AreEqual(HoistingPointDirection.Vertical, pathPoints[4].Direction);
+ Assert.AreSame(finalGroundPoint, pathPoints[4].Position);
+ }
+ }
+}
diff --git a/src/Core/PathPlanningManager.cs b/src/Core/PathPlanningManager.cs
index 620187b..71a2a03 100644
--- a/src/Core/PathPlanningManager.cs
+++ b/src/Core/PathPlanningManager.cs
@@ -1715,17 +1715,8 @@ namespace NavisworksTransport
var lastPoint = CurrentRoute.Points.Last();
LogManager.Info($"[吊装路径] 最后一个路径点: {lastPoint.Name}, 位置: ({lastPoint.Position.X:F2}, {lastPoint.Position.Y:F2}, {lastPoint.Position.Z:F2})");
- // 计算下一个索引
- int nextIndex = CurrentRoute.Points.Count;
-
- // 创建下降点:在落地点正上方,保持当前空中高程(沿宿主 up)
- var descendPoint = new PathPoint(
- lastPoint.Position,
- "下降点",
- PathPointType.WayPoint);
- descendPoint.Direction = HoistingPointDirection.Vertical;
- descendPoint.Index = nextIndex++;
- LogManager.Info($"[吊装路径] 已生成下降点(在终点正上方): ({descendPoint.Position.X:F2}, {descendPoint.Position.Y:F2}, {descendPoint.Position.Z:F2})");
+ var descendPoint = ReuseLastHoistingPointAsDescendPoint(CurrentRoute.Points);
+ LogManager.Info($"[吊装路径] 已将最后一个空中点复用为下降点(终点正上方): ({descendPoint.Position.X:F2}, {descendPoint.Position.Y:F2}, {descendPoint.Position.Z:F2})");
Point3D finalGroundPoint = _hoistingGroundIntermediatePoints.Count > 0
? _hoistingGroundIntermediatePoints[_hoistingGroundIntermediatePoints.Count - 1]
@@ -1737,15 +1728,12 @@ namespace NavisworksTransport
"落地点",
PathPointType.EndPoint);
endPoint.Direction = HoistingPointDirection.Vertical;
- endPoint.Index = nextIndex++;
+ endPoint.Index = CurrentRoute.Points.Count;
LogManager.Info($"[吊装路径] 已创建落地点(终点): ({endPoint.Position.X:F2}, {endPoint.Position.Y:F2}, {endPoint.Position.Z:F2})");
-
- // 添加下降点
- CurrentRoute.Points.Add(descendPoint);
-
+
// 添加落地点(终点)
CurrentRoute.Points.Add(endPoint);
- LogManager.Info($"[吊装路径] 已添加下降点和落地点,当前共 {CurrentRoute.Points.Count} 个路径点");
+ LogManager.Info($"[吊装路径] 已复用最后一个空中点作为下降点,并添加落地点,当前共 {CurrentRoute.Points.Count} 个路径点");
// 注意:不需要再次调用 OrthogonalizePath,因为添加的只是垂直线段
// 垂直线段不会形成斜线,所以不需要优化
@@ -5483,6 +5471,26 @@ namespace NavisworksTransport
return _activePathManager;
}
+ internal static PathPoint ReuseLastHoistingPointAsDescendPoint(IList points)
+ {
+ if (points == null)
+ {
+ throw new ArgumentNullException(nameof(points));
+ }
+
+ if (points.Count == 0)
+ {
+ throw new ArgumentException("吊装路径至少需要一个空中点才能复用为下降点。", nameof(points));
+ }
+
+ PathPoint descendPoint = points[points.Count - 1];
+ descendPoint.Name = "下降点";
+ descendPoint.Type = PathPointType.WayPoint;
+ descendPoint.Direction = HoistingPointDirection.Vertical;
+ descendPoint.Index = points.Count - 1;
+ return descendPoint;
+ }
+
#endregion
}
}
From da5f8a1ae11789a9222f4234261db668e003ff11 Mon Sep 17 00:00:00 2001
From: tian <11429339@qq.com>
Date: Thu, 9 Apr 2026 23:53:40 +0800
Subject: [PATCH 70/87] add ground object lift adjustment
---
NavisworksTransport.UnitTests.csproj | 2 +
.../GroundPassageSpaceOffsetTests.cs | 17 +++++
.../GroundPathObjectLiftOffsetTests.cs | 42 +++++++++++
.../ObjectStartPlacementRequestTests.cs | 15 +++-
src/Core/Animation/PathAnimationManager.cs | 74 ++++++++++++++++++-
src/Core/PathPointRenderPlugin.cs | 14 +++-
.../ViewModels/AnimationControlViewModel.cs | 41 ++++++----
src/UI/WPF/Views/AnimationControlView.xaml | 4 +-
src/UI/WPF/Views/EditRotationWindow.xaml | 31 +++++++-
src/UI/WPF/Views/EditRotationWindow.xaml.cs | 56 +++++++++++++-
.../ObjectStartPlacementRequest.cs | 29 ++++++--
11 files changed, 291 insertions(+), 34 deletions(-)
create mode 100644 UnitTests/CoordinateSystem/GroundPassageSpaceOffsetTests.cs
create mode 100644 UnitTests/CoordinateSystem/GroundPathObjectLiftOffsetTests.cs
diff --git a/NavisworksTransport.UnitTests.csproj b/NavisworksTransport.UnitTests.csproj
index d9d8cc5..959e016 100644
--- a/NavisworksTransport.UnitTests.csproj
+++ b/NavisworksTransport.UnitTests.csproj
@@ -64,6 +64,8 @@
+
+
diff --git a/UnitTests/CoordinateSystem/GroundPassageSpaceOffsetTests.cs b/UnitTests/CoordinateSystem/GroundPassageSpaceOffsetTests.cs
new file mode 100644
index 0000000..01ffc39
--- /dev/null
+++ b/UnitTests/CoordinateSystem/GroundPassageSpaceOffsetTests.cs
@@ -0,0 +1,17 @@
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using NavisworksTransport.Core;
+
+namespace NavisworksTransport.UnitTests.CoordinateSystem
+{
+ [TestClass]
+ public class GroundPassageSpaceOffsetTests
+ {
+ [TestMethod]
+ public void GroundPassageSpaceVerticalOffset_ShouldStayAtHalfHeight_WhenHeightAlreadyIncludesLift()
+ {
+ double verticalOffset = PathPointRenderPlugin.ResolveGroundPathObjectSpaceVerticalOffset(5.7);
+
+ Assert.AreEqual(2.85, verticalOffset, 1e-9);
+ }
+ }
+}
diff --git a/UnitTests/CoordinateSystem/GroundPathObjectLiftOffsetTests.cs b/UnitTests/CoordinateSystem/GroundPathObjectLiftOffsetTests.cs
new file mode 100644
index 0000000..04a2456
--- /dev/null
+++ b/UnitTests/CoordinateSystem/GroundPathObjectLiftOffsetTests.cs
@@ -0,0 +1,42 @@
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using NavisworksTransport.Core.Animation;
+using NavisworksTransport.Utils.CoordinateSystem;
+using System.Numerics;
+
+namespace NavisworksTransport.UnitTests.CoordinateSystem
+{
+ [TestClass]
+ public class GroundPathObjectLiftOffsetTests
+ {
+ [TestMethod]
+ public void YUp_GroundPathLift_ShouldOffsetAlongHostY()
+ {
+ Vector3 offset = PathAnimationManager.ResolveGroundPathObjectLiftOffsetVector(1.25, CoordinateSystemType.YUp);
+
+ AssertVector(offset, 0.0, 1.25, 0.0);
+ }
+
+ [TestMethod]
+ public void ZUp_GroundPathLift_ShouldOffsetAlongHostZ()
+ {
+ Vector3 offset = PathAnimationManager.ResolveGroundPathObjectLiftOffsetVector(1.25, CoordinateSystemType.ZUp);
+
+ AssertVector(offset, 0.0, 0.0, 1.25);
+ }
+
+ [TestMethod]
+ public void GroundPathLift_ShouldReturnZero_WhenLiftIsNotPositive()
+ {
+ Vector3 offset = PathAnimationManager.ResolveGroundPathObjectLiftOffsetVector(0.0, CoordinateSystemType.ZUp);
+
+ AssertVector(offset, 0.0, 0.0, 0.0);
+ }
+
+ private static void AssertVector(Vector3 actual, double x, double y, double z, double tolerance = 1e-6)
+ {
+ Assert.AreEqual(x, actual.X, tolerance);
+ Assert.AreEqual(y, actual.Y, tolerance);
+ Assert.AreEqual(z, actual.Z, tolerance);
+ }
+ }
+}
diff --git a/UnitTests/CoordinateSystem/ObjectStartPlacementRequestTests.cs b/UnitTests/CoordinateSystem/ObjectStartPlacementRequestTests.cs
index 8c14ad3..8ec72ff 100644
--- a/UnitTests/CoordinateSystem/ObjectStartPlacementRequestTests.cs
+++ b/UnitTests/CoordinateSystem/ObjectStartPlacementRequestTests.cs
@@ -14,6 +14,7 @@ namespace NavisworksTransport.UnitTests.CoordinateSystem
Assert.AreEqual(ObjectStartPlacementMode.PreserveInitialPose, request.PlacementMode);
Assert.IsTrue(request.PreserveInitialPose);
Assert.AreEqual(LocalEulerRotationCorrection.Zero, request.RotationCorrection);
+ Assert.AreEqual(0.0, request.VerticalLiftInMeters, 1e-9);
}
[TestMethod]
@@ -21,11 +22,23 @@ namespace NavisworksTransport.UnitTests.CoordinateSystem
{
var correction = new LocalEulerRotationCorrection(15.0, 30.0, 45.0);
- var request = ObjectStartPlacementRequest.CreateRotationCorrection(correction);
+ var request = ObjectStartPlacementRequest.CreateRotationCorrection(correction, 0.35);
Assert.AreEqual(ObjectStartPlacementMode.AlignToPathPose, request.PlacementMode);
Assert.IsFalse(request.PreserveInitialPose);
Assert.AreEqual(correction, request.RotationCorrection);
+ Assert.AreEqual(0.35, request.VerticalLiftInMeters, 1e-9);
+ }
+
+ [TestMethod]
+ public void TranslationOnlyRequest_ShouldKeepVerticalLift()
+ {
+ var request = ObjectStartPlacementRequest.CreateTranslationOnly(0.28);
+
+ Assert.AreEqual(ObjectStartPlacementMode.PreserveInitialPose, request.PlacementMode);
+ Assert.IsTrue(request.PreserveInitialPose);
+ Assert.AreEqual(LocalEulerRotationCorrection.Zero, request.RotationCorrection);
+ Assert.AreEqual(0.28, request.VerticalLiftInMeters, 1e-9);
}
}
}
diff --git a/src/Core/Animation/PathAnimationManager.cs b/src/Core/Animation/PathAnimationManager.cs
index 5ef7fcc..0758533 100644
--- a/src/Core/Animation/PathAnimationManager.cs
+++ b/src/Core/Animation/PathAnimationManager.cs
@@ -133,6 +133,7 @@ namespace NavisworksTransport.Core.Animation
// === 角度修正 ===
private LocalEulerRotationCorrection _objectRotationCorrection = LocalEulerRotationCorrection.Zero; // 物体绕宿主 X/Y/Z 轴的角度修正
+ private double _groundPathObjectLiftHeight; // 地面路径物体额外提升高度(模型单位)
// === 碰撞排除列表缓存管理(从LogisticsAnimationManager迁移)===
private ModelItem _currentCachedAnimationObject;
@@ -767,6 +768,7 @@ namespace NavisworksTransport.Core.Animation
{
// 地面路径:路径点在接触面上,动画跟踪点统一使用几何中心
startPosition = ResolveGroundTrackedCenter(startPosition, GetAnimatedObjectGroundContactHeight());
+ startPosition = ApplyGroundPathObjectLiftOffset(startPosition);
LogManager.Debug($"[移动到起点] 地面路径中心点=({startPosition.X:F2},{startPosition.Y:F2},{startPosition.Z:F2})");
}
@@ -970,6 +972,7 @@ namespace NavisworksTransport.Core.Animation
else
{
startPosition = ResolveGroundTrackedCenter(startPosition, GetAnimatedObjectGroundContactHeight());
+ startPosition = ApplyGroundPathObjectLiftOffset(startPosition);
}
UpdateObjectPosition(startPosition);
@@ -1293,10 +1296,13 @@ namespace NavisworksTransport.Core.Animation
}
framePosition = ResolveGroundTrackedCenter(framePosition, GetAnimatedObjectGroundContactHeight());
+ framePosition = ApplyGroundPathObjectLiftOffset(framePosition);
yawRadians = ComputeYawOnPath(i, allSampledPoints, edgeIndex, edgeProgress);
- previousFramePoint = ResolveGroundTrackedCenter(sampledPreviousPoint, GetAnimatedObjectGroundContactHeight());
- nextFramePoint = ResolveGroundTrackedCenter(sampledNextPoint, GetAnimatedObjectGroundContactHeight());
+ previousFramePoint = ApplyGroundPathObjectLiftOffset(
+ ResolveGroundTrackedCenter(sampledPreviousPoint, GetAnimatedObjectGroundContactHeight()));
+ nextFramePoint = ApplyGroundPathObjectLiftOffset(
+ ResolveGroundTrackedCenter(sampledNextPoint, GetAnimatedObjectGroundContactHeight()));
if ((_route.PathType == PathType.Ground || _route.PathType == PathType.Hoisting) &&
i > 0 &&
@@ -5461,6 +5467,34 @@ namespace NavisworksTransport.Core.Animation
}
}
+ public void ApplyObjectStartPlacementRequest(ObjectStartPlacementRequest request)
+ {
+ _objectStartPlacementMode = request.PlacementMode;
+ if (_objectStartPlacementMode != ObjectStartPlacementMode.PreserveInitialPose)
+ {
+ _hasPathPreservedPoseRotation = false;
+ }
+
+ _objectRotationCorrection = request.RotationCorrection;
+ _groundPathObjectLiftHeight = UnitsConverter.ConvertFromMeters(request.VerticalLiftInMeters);
+
+ LogManager.Info(
+ $"[起点摆放] 已应用物体调整请求: 模式={_objectStartPlacementMode}, 角度={_objectRotationCorrection}, " +
+ $"提升高度={request.VerticalLiftInMeters:F3}m");
+
+ if (_animatedObject != null && _pathPoints != null && _pathPoints.Count > 0)
+ {
+ try
+ {
+ MoveObjectToPathStartUsingCurrentPlacementMode();
+ }
+ catch (Exception ex)
+ {
+ LogManager.Error($"[起点摆放] 应用物体调整请求失败: {ex.Message}");
+ }
+ }
+ }
+
///
/// 保留给旧单轴调用方:按当前宿主 up 轴映射成三轴角度修正。
///
@@ -5479,6 +5513,12 @@ namespace NavisworksTransport.Core.Animation
LogManager.Debug($"[角度修正] 直接设置角度修正值: {_objectRotationCorrection}(不触发旋转)");
}
+ public void SetObjectStartVerticalLiftDirect(double verticalLiftInMeters)
+ {
+ _groundPathObjectLiftHeight = UnitsConverter.ConvertFromMeters(verticalLiftInMeters);
+ LogManager.Debug($"[起点摆放] 直接设置地面路径提升高度: {verticalLiftInMeters:F3}m");
+ }
+
public void SetObjectStartPlacementMode(ObjectStartPlacementMode placementMode)
{
_objectStartPlacementMode = placementMode;
@@ -5512,6 +5552,36 @@ namespace NavisworksTransport.Core.Animation
: new LocalEulerRotationCorrection(0.0, 0.0, rotationCorrection);
}
+ internal static Vector3 ResolveGroundPathObjectLiftOffsetVector(
+ double verticalLiftModelUnits,
+ CoordinateSystemType hostType)
+ {
+ if (verticalLiftModelUnits <= 0.0)
+ {
+ return Vector3.Zero;
+ }
+
+ var adapter = new HostCoordinateAdapter(hostType);
+ return Vector3.Normalize(adapter.HostUpVector3) * (float)verticalLiftModelUnits;
+ }
+
+ private Point3D ApplyGroundPathObjectLiftOffset(Point3D trackedCenter)
+ {
+ if (_route == null || _route.PathType != PathType.Ground || _groundPathObjectLiftHeight <= 0.0)
+ {
+ return trackedCenter;
+ }
+
+ Vector3 offset = ResolveGroundPathObjectLiftOffsetVector(
+ _groundPathObjectLiftHeight,
+ CoordinateSystemManager.Instance.ResolvedType);
+
+ return new Point3D(
+ trackedCenter.X + offset.X,
+ trackedCenter.Y + offset.Y,
+ trackedCenter.Z + offset.Z);
+ }
+
#region 动画实现方法
///
diff --git a/src/Core/PathPointRenderPlugin.cs b/src/Core/PathPointRenderPlugin.cs
index 5823d70..b7b5f87 100644
--- a/src/Core/PathPointRenderPlugin.cs
+++ b/src/Core/PathPointRenderPlugin.cs
@@ -1594,7 +1594,7 @@ namespace NavisworksTransport
if (visualization.PathRoute.PathType == NavisworksTransport.PathType.Ground)
{
// 地面路径:路径点是地面位置,通行空间底面在地面,中心需要向上偏移半个高度
- verticalOffset = height / 2.0;
+ verticalOffset = ResolveGroundPathObjectSpaceVerticalOffset(height);
}
else if (visualization.PathRoute.PathType == NavisworksTransport.PathType.Rail)
{
@@ -2314,7 +2314,12 @@ namespace NavisworksTransport
/// 沿路径方向的通行空间尺寸(模型单位)
/// 垂直段的高度(物体长度 + 2×安全间隙,模型单位)
/// 水平段的高度(物体高度 + 2×安全间隙,模型单位)
- public void SetPassageSpaceParameters(double passageAcrossPath, double passageNormalToPath, double passageAlongPath, double passageNormalToPathVertical, double passageNormalToPathHorizontal)
+ public void SetPassageSpaceParameters(
+ double passageAcrossPath,
+ double passageNormalToPath,
+ double passageAlongPath,
+ double passageNormalToPathVertical,
+ double passageNormalToPathHorizontal)
{
// 直接存储模型单位(调用方已转换)
_passageAlongPath = passageAlongPath;
@@ -2329,6 +2334,11 @@ namespace NavisworksTransport
RefreshNormalPaths();
}
+ internal static double ResolveGroundPathObjectSpaceVerticalOffset(double objectSpaceHeight)
+ {
+ return objectSpaceHeight / 2.0;
+ }
+
#region 颜色管理
///
diff --git a/src/UI/WPF/ViewModels/AnimationControlViewModel.cs b/src/UI/WPF/ViewModels/AnimationControlViewModel.cs
index 9d7e22e..54b2e65 100644
--- a/src/UI/WPF/ViewModels/AnimationControlViewModel.cs
+++ b/src/UI/WPF/ViewModels/AnimationControlViewModel.cs
@@ -357,6 +357,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
// 角度修正相关字段
private LocalEulerRotationCorrection _objectRotationCorrection = LocalEulerRotationCorrection.Zero; // 物体绕宿主 X/Y/Z 轴的角度修正
+ private double _objectGroundLiftHeightInMeters;
// 移动物体原始尺寸(米,在选择物体时保存)
private double _objectOriginalLength; // 物体原始长度(X方向)
@@ -1688,7 +1689,9 @@ namespace NavisworksTransport.UI.WPF.ViewModels
if (!isSameObject)
{
_pathAnimationManager?.SetObjectRotationCorrectionDirect(LocalEulerRotationCorrection.Zero);
+ _pathAnimationManager?.SetObjectStartVerticalLiftDirect(0.0);
ObjectRotationCorrection = LocalEulerRotationCorrection.Zero;
+ _objectGroundLiftHeightInMeters = 0.0;
LogManager.Debug("[选择物体] 已重置角度修正值为0(新物体)");
}
else
@@ -2124,6 +2127,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
_pathAnimationManager.RestoreObjectToCADPosition();
_pathAnimationManager.ClearAnimationResults();
_pathAnimationManager.SetObjectRotationCorrectionDirect(LocalEulerRotationCorrection.Zero);
+ _pathAnimationManager.SetObjectStartVerticalLiftDirect(0.0);
_pathAnimationManager.SetObjectStartPlacementMode(ObjectStartPlacementMode.AlignToPathPose);
LogManager.Info("已清除PathAnimationManager中的动画数据并归位物体");
}
@@ -2133,6 +2137,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
// 3. 重置角度修正值为0
ObjectRotationCorrection = LocalEulerRotationCorrection.Zero;
+ _objectGroundLiftHeightInMeters = 0.0;
LogManager.Debug("[清除物体] 已重置角度修正值为0");
// 4. 重置属性
@@ -2159,20 +2164,29 @@ namespace NavisworksTransport.UI.WPF.ViewModels
{
try
{
- var dialog = new Views.EditRotationWindow(_objectRotationCorrection);
+ bool enableGroundLift = CurrentPathRoute?.PathType == PathType.Ground;
+ var dialog = new Views.EditRotationWindow(
+ _objectRotationCorrection,
+ _objectGroundLiftHeightInMeters,
+ enableGroundLift);
if (dialog.ShowDialog() == true)
{
var placementRequest = dialog.AdjustmentRequest;
- if (placementRequest.PreserveInitialPose)
- {
- ApplyTranslationOnlyObjectPlacement();
- return;
- }
-
- _pathAnimationManager?.SetObjectStartPlacementMode(ObjectStartPlacementMode.AlignToPathPose);
+ _objectGroundLiftHeightInMeters = placementRequest.VerticalLiftInMeters;
+ _pathAnimationManager?.ApplyObjectStartPlacementRequest(placementRequest);
ObjectRotationCorrection = placementRequest.RotationCorrection;
- LogManager.Info($"物体角度修正已更新: {_objectRotationCorrection}");
- UpdateMainStatus($"物体角度修正: {_objectRotationCorrection}");
+
+ string liftSummary = enableGroundLift
+ ? $", 提升高度={_objectGroundLiftHeightInMeters:F3}m"
+ : string.Empty;
+ string statusSummary = placementRequest.PreserveInitialPose
+ ? $"物体已调整为平移模式{liftSummary}"
+ : $"物体角度修正: {_objectRotationCorrection}{liftSummary}";
+
+ LogManager.Info(
+ $"物体调整已更新: 模式={placementRequest.PlacementMode}, 角度={_objectRotationCorrection}, " +
+ $"提升高度={_objectGroundLiftHeightInMeters:F3}m");
+ UpdateMainStatus(statusSummary);
// 更新通行空间可视化(考虑旋转后的尺寸)
UpdatePassageSpaceVisualization();
@@ -2196,6 +2210,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
_objectRotationCorrection = LocalEulerRotationCorrection.Zero;
_pathAnimationManager.SetObjectRotationCorrectionDirect(LocalEulerRotationCorrection.Zero);
+ _pathAnimationManager.SetObjectStartVerticalLiftDirect(0.0);
_pathAnimationManager.SetObjectStartPlacementMode(ObjectStartPlacementMode.PreserveInitialPose);
OnPropertyChanged(nameof(ObjectRotationCorrection));
@@ -4463,7 +4478,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
// 地面路径:物体的长度方向(X轴,前进方向)朝向路径方向
// 通行空间沿路径方向 = X方向尺寸(长度),垂直于路径方向 = Y方向尺寸(宽度),高度上方加间隙(下方不需要,因为有地面)
passageAcrossPath = effectiveWidth + 2 * safetyMargin; // 旋转后宽度 + 2*间隙(垂直于路径方向)
- passageNormalToPath = effectiveHeight + safetyMargin; // 法线方向高度 + 间隙(法线方向,只有上方)
+ passageNormalToPath = effectiveHeight + (_objectGroundLiftHeightInMeters * metersToUnitsPassage) + safetyMargin; // 物体高度 + 提升高度 + 上方间隙
passageAlongPath = effectiveLength; // 旋转后长度(沿路径方向)
// 地面路径不需要分段参数
passageNormalToPathVertical = passageNormalToPath;
@@ -4507,7 +4522,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
// 物体的长度方向(X轴,前进方向)朝向路径方向
// 通行空间沿路径方向 = X方向尺寸(长度),垂直于路径方向 = Y方向尺寸(宽度),高度上方加间隙(下方不需要,因为有地面)
passageAcrossPath = effectiveWidth + 2 * safetyMargin; // 旋转后宽度 + 2*间隙(垂直于路径方向)
- passageNormalToPath = effectiveHeight + safetyMargin; // 局部up方向高度 + 间隙(法线方向,只有上方)
+ passageNormalToPath = effectiveHeight + (_objectGroundLiftHeightInMeters * metersToUnitsPassage) + safetyMargin; // 物体高度 + 提升高度 + 上方间隙
passageAlongPath = effectiveLength; // 旋转后长度(沿路径方向)
// 地面路径不需要分段参数
passageNormalToPathVertical = passageNormalToPath;
@@ -4551,7 +4566,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
passageAlongPath,
passageNormalToPathVertical,
passageNormalToPathHorizontal);
- LogManager.Debug($"[通行空间可视化] 已设置通行空间参数: 沿路径={passageAlongPath / metersToUnitsPassage:F2}m, 垂直路径={passageAcrossPath / metersToUnitsPassage:F2}m, 法线={passageNormalToPath / metersToUnitsPassage: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;
diff --git a/src/UI/WPF/Views/AnimationControlView.xaml b/src/UI/WPF/Views/AnimationControlView.xaml
index 0dee544..f6226aa 100644
--- a/src/UI/WPF/Views/AnimationControlView.xaml
+++ b/src/UI/WPF/Views/AnimationControlView.xaml
@@ -346,7 +346,7 @@ NavisworksTransport 检测动画页签视图 - 采用与类别设置和分层管
-
@@ -383,7 +383,7 @@ NavisworksTransport 检测动画页签视图 - 采用与类别设置和分层管
diff --git a/src/UI/WPF/Views/EditRotationWindow.xaml b/src/UI/WPF/Views/EditRotationWindow.xaml
index a1fe247..fb5b0d3 100644
--- a/src/UI/WPF/Views/EditRotationWindow.xaml
+++ b/src/UI/WPF/Views/EditRotationWindow.xaml
@@ -1,7 +1,7 @@
-
-
+
+
@@ -95,6 +95,31 @@
BorderThickness="1"/>
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/UI/WPF/Views/EditRotationWindow.xaml.cs b/src/UI/WPF/Views/EditRotationWindow.xaml.cs
index 9e1e55f..387d9ed 100644
--- a/src/UI/WPF/Views/EditRotationWindow.xaml.cs
+++ b/src/UI/WPF/Views/EditRotationWindow.xaml.cs
@@ -19,8 +19,10 @@ namespace NavisworksTransport.UI.WPF.Views
private double _rotationXDegrees;
private double _rotationYDegrees;
private double _rotationZDegrees;
+ private double _groundPathLiftHeightInMeters;
private RotationAxisTarget _activeAxisTarget = RotationAxisTarget.Z;
private ObjectStartPlacementRequest _adjustmentRequest;
+ private readonly bool _isGroundLiftEnabled;
public double RotationXDegrees
{
@@ -67,9 +69,31 @@ namespace NavisworksTransport.UI.WPF.Views
public LocalEulerRotationCorrection RotationCorrection =>
new LocalEulerRotationCorrection(RotationXDegrees, RotationYDegrees, RotationZDegrees);
+ public double GroundPathLiftHeightInMeters
+ {
+ get => _groundPathLiftHeightInMeters;
+ set
+ {
+ if (Math.Abs(_groundPathLiftHeightInMeters - value) > 1e-9)
+ {
+ _groundPathLiftHeightInMeters = value;
+ OnPropertyChanged();
+ }
+ }
+ }
+
+ public bool IsGroundLiftEnabled => _isGroundLiftEnabled;
+
+ public string GroundLiftHint => _isGroundLiftEnabled
+ ? "仅地面路径生效,用于模拟物体下方物流小车带来的整体抬升。"
+ : "提升高度当前仅对地面路径生效。";
+
public ObjectStartPlacementRequest AdjustmentRequest => _adjustmentRequest;
- public EditRotationWindow(LocalEulerRotationCorrection currentRotation)
+ public EditRotationWindow(
+ LocalEulerRotationCorrection currentRotation,
+ double currentGroundLiftHeightInMeters,
+ bool isGroundLiftEnabled)
{
try
{
@@ -77,7 +101,9 @@ namespace NavisworksTransport.UI.WPF.Views
RotationXDegrees = currentRotation.XDegrees;
RotationYDegrees = currentRotation.YDegrees;
RotationZDegrees = currentRotation.ZDegrees;
- _adjustmentRequest = ObjectStartPlacementRequest.CreateRotationCorrection(currentRotation);
+ _isGroundLiftEnabled = isGroundLiftEnabled;
+ GroundPathLiftHeightInMeters = currentGroundLiftHeightInMeters;
+ _adjustmentRequest = ObjectStartPlacementRequest.CreateRotationCorrection(currentRotation, currentGroundLiftHeightInMeters);
DataContext = this;
Loaded += (sender, args) => ZAxisTextBox.Focus();
}
@@ -145,14 +171,24 @@ namespace NavisworksTransport.UI.WPF.Views
private void OnConfirmClick(object sender, RoutedEventArgs e)
{
- _adjustmentRequest = ObjectStartPlacementRequest.CreateRotationCorrection(RotationCorrection);
+ if (!ValidateGroundLiftHeight())
+ {
+ return;
+ }
+
+ _adjustmentRequest = ObjectStartPlacementRequest.CreateRotationCorrection(RotationCorrection, GroundPathLiftHeightInMeters);
DialogResult = true;
Close();
}
private void OnTranslateClick(object sender, RoutedEventArgs e)
{
- _adjustmentRequest = ObjectStartPlacementRequest.TranslationOnly;
+ if (!ValidateGroundLiftHeight())
+ {
+ return;
+ }
+
+ _adjustmentRequest = ObjectStartPlacementRequest.CreateTranslationOnly(GroundPathLiftHeightInMeters);
DialogResult = true;
Close();
}
@@ -186,5 +222,17 @@ namespace NavisworksTransport.UI.WPF.Views
break;
}
}
+
+ private bool ValidateGroundLiftHeight()
+ {
+ if (GroundPathLiftHeightInMeters < 0.0)
+ {
+ MessageBox.Show("提升高度必须大于或等于 0。", "输入错误", MessageBoxButton.OK, MessageBoxImage.Warning);
+ GroundLiftTextBox.Focus();
+ return false;
+ }
+
+ return true;
+ }
}
}
diff --git a/src/Utils/CoordinateSystem/ObjectStartPlacementRequest.cs b/src/Utils/CoordinateSystem/ObjectStartPlacementRequest.cs
index a2617ca..ffeb517 100644
--- a/src/Utils/CoordinateSystem/ObjectStartPlacementRequest.cs
+++ b/src/Utils/CoordinateSystem/ObjectStartPlacementRequest.cs
@@ -11,31 +11,43 @@ namespace NavisworksTransport.Utils.CoordinateSystem
public struct ObjectStartPlacementRequest : IEquatable
{
public static readonly ObjectStartPlacementRequest TranslationOnly =
- new ObjectStartPlacementRequest(LocalEulerRotationCorrection.Zero, ObjectStartPlacementMode.PreserveInitialPose);
+ new ObjectStartPlacementRequest(LocalEulerRotationCorrection.Zero, ObjectStartPlacementMode.PreserveInitialPose, 0.0);
public ObjectStartPlacementRequest(
LocalEulerRotationCorrection rotationCorrection,
- ObjectStartPlacementMode placementMode)
+ ObjectStartPlacementMode placementMode,
+ double verticalLiftInMeters)
{
RotationCorrection = rotationCorrection;
PlacementMode = placementMode;
+ VerticalLiftInMeters = verticalLiftInMeters;
}
public LocalEulerRotationCorrection RotationCorrection { get; }
public ObjectStartPlacementMode PlacementMode { get; }
+ public double VerticalLiftInMeters { get; }
+
public bool PreserveInitialPose => PlacementMode == ObjectStartPlacementMode.PreserveInitialPose;
- public static ObjectStartPlacementRequest CreateRotationCorrection(LocalEulerRotationCorrection rotationCorrection)
+ public static ObjectStartPlacementRequest CreateRotationCorrection(
+ LocalEulerRotationCorrection rotationCorrection,
+ double verticalLiftInMeters = 0.0)
{
- return new ObjectStartPlacementRequest(rotationCorrection, ObjectStartPlacementMode.AlignToPathPose);
+ return new ObjectStartPlacementRequest(rotationCorrection, ObjectStartPlacementMode.AlignToPathPose, verticalLiftInMeters);
+ }
+
+ public static ObjectStartPlacementRequest CreateTranslationOnly(double verticalLiftInMeters = 0.0)
+ {
+ return new ObjectStartPlacementRequest(LocalEulerRotationCorrection.Zero, ObjectStartPlacementMode.PreserveInitialPose, verticalLiftInMeters);
}
public bool Equals(ObjectStartPlacementRequest other)
{
return RotationCorrection.Equals(other.RotationCorrection) &&
- PlacementMode == other.PlacementMode;
+ PlacementMode == other.PlacementMode &&
+ VerticalLiftInMeters.Equals(other.VerticalLiftInMeters);
}
public override bool Equals(object obj)
@@ -47,13 +59,16 @@ namespace NavisworksTransport.Utils.CoordinateSystem
{
unchecked
{
- return (RotationCorrection.GetHashCode() * 397) ^ (int)PlacementMode;
+ int hashCode = RotationCorrection.GetHashCode();
+ hashCode = (hashCode * 397) ^ (int)PlacementMode;
+ hashCode = (hashCode * 397) ^ VerticalLiftInMeters.GetHashCode();
+ return hashCode;
}
}
public override string ToString()
{
- return $"PlacementMode={PlacementMode}, Rotation={RotationCorrection}";
+ return $"PlacementMode={PlacementMode}, Rotation={RotationCorrection}, VerticalLiftInMeters={VerticalLiftInMeters:F3}";
}
}
}
From 1a9c9ecd212bfd921faa393e259ff7703a3f22db Mon Sep 17 00:00:00 2001
From: tian <11429339@qq.com>
Date: Fri, 10 Apr 2026 00:16:55 +0800
Subject: [PATCH 71/87] fix collision report save timing
---
UnitTests/Core/PathHelperTests.cs | 10 ++++++
.../GenerateCollisionReportCommand.cs | 22 +++++++------
.../ViewModels/CollisionReportViewModel.cs | 7 ++--
.../WPF/Views/CollisionReportDialog.xaml.cs | 33 ++++++-------------
src/Utils/CollisionReportHtmlGenerator.cs | 3 +-
src/Utils/PathHelper.cs | 14 ++++++++
6 files changed, 50 insertions(+), 39 deletions(-)
diff --git a/UnitTests/Core/PathHelperTests.cs b/UnitTests/Core/PathHelperTests.cs
index 36bf856..9949ea5 100644
--- a/UnitTests/Core/PathHelperTests.cs
+++ b/UnitTests/Core/PathHelperTests.cs
@@ -24,5 +24,15 @@ namespace NavisworksTransport.UnitTests.Core
Assert.AreEqual("副本_我的路径", duplicatedName);
}
+
+ [TestMethod]
+ public void GenerateCollisionReportFileName_ShouldUseUnifiedChinesePrefixAndTimestamp()
+ {
+ var now = new DateTime(2026, 4, 9, 23, 55, 1);
+
+ var fileName = PathHelper.GenerateCollisionReportFileName("人工_0409_235000", "html", now);
+
+ Assert.AreEqual("碰撞检测报告_人工_0409_235000_20260409_235501.html", fileName);
+ }
}
}
diff --git a/src/Commands/GenerateCollisionReportCommand.cs b/src/Commands/GenerateCollisionReportCommand.cs
index 1eb3f4e..181520f 100644
--- a/src/Commands/GenerateCollisionReportCommand.cs
+++ b/src/Commands/GenerateCollisionReportCommand.cs
@@ -602,19 +602,21 @@ namespace NavisworksTransport.Commands
ShowReport(result);
}
- // 🔥 自动导出HTML报告
- try
+ if (!_parameters.ShowDialog)
{
- string htmlPath = Utils.CollisionReportHtmlGenerator.AutoSaveHtmlReport(result);
- if (!string.IsNullOrEmpty(htmlPath))
+ try
{
- LogManager.Info($"HTML报告已自动导出: {htmlPath}");
+ string htmlPath = Utils.CollisionReportHtmlGenerator.AutoSaveHtmlReport(result);
+ if (!string.IsNullOrEmpty(htmlPath))
+ {
+ LogManager.Info($"HTML报告已自动导出: {htmlPath}");
+ }
+ }
+ catch (Exception ex)
+ {
+ LogManager.Error($"自动导出HTML报告失败: {ex.Message}");
+ // HTML导出失败不影响报告生成
}
- }
- catch (Exception ex)
- {
- LogManager.Error($"自动导出HTML报告失败: {ex.Message}");
- // HTML导出失败不影响报告生成
}
}
catch (OperationCanceledException)
diff --git a/src/UI/WPF/ViewModels/CollisionReportViewModel.cs b/src/UI/WPF/ViewModels/CollisionReportViewModel.cs
index 32e95f5..5349f7b 100644
--- a/src/UI/WPF/ViewModels/CollisionReportViewModel.cs
+++ b/src/UI/WPF/ViewModels/CollisionReportViewModel.cs
@@ -917,7 +917,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
Title = "导出碰撞报告",
Filter = "HTML报告 (*.html)|*.html|碰撞报告 (*.txt)|*.txt|所有文件 (*.*)|*.*",
DefaultExt = "html",
- FileName = $"NavisworksTransport_CollisionReport_{DateTime.Now:yyyyMMdd_HHmmss}"
+ FileName = PathHelper.GenerateCollisionReportFileName(CurrentReport?.PathName ?? PathName, "html")
};
if (saveFileDialog.ShowDialog() == true)
@@ -946,11 +946,10 @@ namespace NavisworksTransport.UI.WPF.ViewModels
{
try
{
- if (CurrentReport == null || !HasCollisions) return;
+ if (CurrentReport == null) return;
// 生成默认文件名和路径
- var sanitizedName = PathHelper.SanitizeFileName(CurrentReport.PathName ?? "未知路径");
- var fileName = $"NavisworksTransport_CollisionReport_{sanitizedName}_{DateTime.Now:yyyyMMdd_HHmmss}.html";
+ var fileName = PathHelper.GenerateCollisionReportFileName(CurrentReport.PathName, "html");
var reportDir = PathHelper.GetReportDirectory();
var filePath = Path.Combine(reportDir, fileName);
diff --git a/src/UI/WPF/Views/CollisionReportDialog.xaml.cs b/src/UI/WPF/Views/CollisionReportDialog.xaml.cs
index 7515ab0..21c60be 100644
--- a/src/UI/WPF/Views/CollisionReportDialog.xaml.cs
+++ b/src/UI/WPF/Views/CollisionReportDialog.xaml.cs
@@ -94,11 +94,8 @@ namespace NavisworksTransport.UI.WPF.Views
{
try
{
- LogManager.Info("用户关闭碰撞报告对话框,正在自动导出报告...");
-
- // 关闭前自动导出报告
+ LogManager.Info("用户关闭碰撞报告对话框,正在自动导出最终报告...");
await AutoExportReportAsync();
-
this.Close();
}
catch (Exception ex)
@@ -107,24 +104,6 @@ namespace NavisworksTransport.UI.WPF.Views
}
}
- ///
- /// 自动导出报告
- ///
- private async System.Threading.Tasks.Task AutoExportReportAsync()
- {
- try
- {
- if (_viewModel != null)
- {
- await _viewModel.AutoExportReportAsync();
- }
- }
- catch (Exception ex)
- {
- LogManager.Error($"自动导出报告失败: {ex.Message}");
- }
- }
-
///
/// 窗口关闭事件
///
@@ -169,6 +148,14 @@ namespace NavisworksTransport.UI.WPF.Views
}
}
+ private async System.Threading.Tasks.Task AutoExportReportAsync()
+ {
+ if (_viewModel != null)
+ {
+ await _viewModel.AutoExportReportAsync();
+ }
+ }
+
///
/// 窗口加载事件
///
@@ -470,4 +457,4 @@ namespace NavisworksTransport.UI.WPF.Views
#endregion
}
-}
\ No newline at end of file
+}
diff --git a/src/Utils/CollisionReportHtmlGenerator.cs b/src/Utils/CollisionReportHtmlGenerator.cs
index 6d5d50e..e8e5ffa 100644
--- a/src/Utils/CollisionReportHtmlGenerator.cs
+++ b/src/Utils/CollisionReportHtmlGenerator.cs
@@ -422,8 +422,7 @@ namespace NavisworksTransport.Utils
try
{
// 生成文件名
- string sanitizedName = PathHelper.SanitizeFileName(report.PathName ?? "未知路径");
- string fileName = PathHelper.GenerateTimestampedFileName($"collision_{sanitizedName}", "html");
+ string fileName = PathHelper.GenerateCollisionReportFileName(report.PathName, "html");
// 获取报告目录
string reportDir = PathHelper.GetReportDirectory();
diff --git a/src/Utils/PathHelper.cs b/src/Utils/PathHelper.cs
index c77a908..655d35a 100644
--- a/src/Utils/PathHelper.cs
+++ b/src/Utils/PathHelper.cs
@@ -84,6 +84,20 @@ namespace NavisworksTransport.Utils
return $"{prefix}_{timestamp}.{extension}";
}
+ ///
+ /// 生成碰撞检测报告文件名。
+ /// 格式:碰撞检测报告_{路径名}_{时间戳}.{extension}
+ ///
+ public static string GenerateCollisionReportFileName(string pathName, string extension, DateTime? now = null)
+ {
+ string sanitizedName = SanitizeFileName(pathName ?? "未知路径");
+ string normalizedExtension = string.IsNullOrWhiteSpace(extension)
+ ? "html"
+ : extension.Trim().TrimStart('.');
+ string timestamp = (now ?? DateTime.Now).ToString("yyyyMMdd_HHmmss");
+ return $"碰撞检测报告_{sanitizedName}_{timestamp}.{normalizedExtension}";
+ }
+
///
/// 生成复制路径名称。
/// 如果原名称以 MMdd_HHmmss 结尾,则重建时间戳;否则仅添加“副本_”前缀。
From 5a03c3aa6c027fcaa71d82cfed2c4656e46e46ab Mon Sep 17 00:00:00 2001
From: tian <11429339@qq.com>
Date: Fri, 10 Apr 2026 22:02:43 +0800
Subject: [PATCH 72/87] preserve planar collision pose rotations
---
.../RailPreservedPoseTests.cs | 33 ++++++++
src/Core/Animation/PathAnimationManager.cs | 83 ++++++++++++++++++-
2 files changed, 115 insertions(+), 1 deletion(-)
diff --git a/UnitTests/CoordinateSystem/RailPreservedPoseTests.cs b/UnitTests/CoordinateSystem/RailPreservedPoseTests.cs
index 8382b75..e8bb9a6 100644
--- a/UnitTests/CoordinateSystem/RailPreservedPoseTests.cs
+++ b/UnitTests/CoordinateSystem/RailPreservedPoseTests.cs
@@ -2,6 +2,7 @@ using Autodesk.Navisworks.Api;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using NavisworksTransport.Core.Animation;
using NavisworksTransport.Utils.CoordinateSystem;
+using System.Numerics;
namespace NavisworksTransport.UnitTests.CoordinateSystem
{
@@ -131,6 +132,38 @@ namespace NavisworksTransport.UnitTests.CoordinateSystem
Assert.IsFalse(PathAnimationManager.ShouldUsePlanarRealObjectPureIncrementFrames(PathType.Rail, true));
}
+ [TestMethod]
+ public void ApplyRecordedPlanarRealObjectFramePose_ShouldPreserveFullRotation_ForGroundCollisionLogCase()
+ {
+ var frame = new AnimationFrame();
+ double yawRadians = 115.85 * System.Math.PI / 180.0;
+
+ var hostLinear = new Matrix4x4(
+ -0.4360f, 0.0000f, -0.8999f, 0.0f,
+ -0.6364f, 0.7071f, 0.3083f, 0.0f,
+ 0.6364f, 0.7071f, -0.3083f, 0.0f,
+ 0.0f, 0.0f, 0.0f, 1.0f);
+ Quaternion hostQuaternion = Quaternion.Normalize(Quaternion.CreateFromRotationMatrix(hostLinear));
+ var rotation = new Rotation3D(hostQuaternion.X, hostQuaternion.Y, hostQuaternion.Z, hostQuaternion.W);
+
+ PathAnimationManager.ApplyRecordedPlanarRealObjectFramePose(frame, yawRadians, rotation);
+
+ Assert.IsTrue(frame.PlanarHostYawRadians.HasValue);
+ Assert.AreEqual(yawRadians, frame.PlanarHostYawRadians.Value, 1e-9);
+ Assert.IsTrue(frame.HasCustomRotation);
+
+ Matrix3 linear = new Transform3D(frame.Rotation).Linear;
+ Assert.AreEqual(-0.4360, linear.Get(0, 0), 5e-4);
+ Assert.AreEqual(-0.6364, linear.Get(1, 0), 5e-4);
+ Assert.AreEqual(0.6364, linear.Get(2, 0), 5e-4);
+ Assert.AreEqual(0.0000, linear.Get(0, 1), 5e-4);
+ Assert.AreEqual(0.7071, linear.Get(1, 1), 5e-4);
+ Assert.AreEqual(0.7071, linear.Get(2, 1), 5e-4);
+ Assert.AreEqual(-0.8999, linear.Get(0, 2), 5e-4);
+ Assert.AreEqual(0.3083, linear.Get(1, 2), 5e-4);
+ Assert.AreEqual(-0.3083, linear.Get(2, 2), 5e-4);
+ }
+
[TestMethod]
public void TryResolveGroundHostPlanarYawFromFramePoints_ShouldResolveYUpHostYaw()
{
diff --git a/src/Core/Animation/PathAnimationManager.cs b/src/Core/Animation/PathAnimationManager.cs
index 0758533..8d1a838 100644
--- a/src/Core/Animation/PathAnimationManager.cs
+++ b/src/Core/Animation/PathAnimationManager.cs
@@ -1344,7 +1344,21 @@ namespace NavisworksTransport.Core.Animation
$"{_route.PathType.GetDisplayName()}真实物体第 {i} 帧未能解析宿主平面目标角。");
}
- frame.PlanarHostYawRadians = planarHostYawRadians;
+ if (!TryResolveRecordedPlanarRealObjectFrameRotation(
+ _route.PathType,
+ previousFramePoint,
+ framePosition,
+ nextFramePoint,
+ out var recordedRotation))
+ {
+ throw new InvalidOperationException(
+ $"{_route.PathType.GetDisplayName()}真实物体第 {i} 帧未能记录完整姿态。");
+ }
+
+ ApplyRecordedPlanarRealObjectFramePose(
+ frame,
+ planarHostYawRadians,
+ recordedRotation);
}
else if (ShouldPreservePathRotationForFrames(
_route.PathType,
@@ -3960,6 +3974,26 @@ namespace NavisworksTransport.Core.Animation
(pathType == PathType.Ground || pathType == PathType.Hoisting);
}
+ internal static void ApplyRecordedPlanarRealObjectFramePose(
+ AnimationFrame frame,
+ double planarHostYawRadians,
+ Rotation3D recordedRotation)
+ {
+ if (frame == null)
+ {
+ throw new ArgumentNullException(nameof(frame));
+ }
+
+ if (recordedRotation == null)
+ {
+ throw new ArgumentNullException(nameof(recordedRotation));
+ }
+
+ frame.PlanarHostYawRadians = planarHostYawRadians;
+ frame.Rotation = recordedRotation;
+ frame.HasCustomRotation = true;
+ }
+
internal static bool TryResolveDisplayedPlanarYawFromRotation(
Rotation3D rotation,
CoordinateSystemType hostType,
@@ -4195,6 +4229,53 @@ namespace NavisworksTransport.Core.Animation
return true;
}
+ private bool TryResolveRecordedPlanarRealObjectFrameRotation(
+ PathType pathType,
+ Point3D previousFramePoint,
+ Point3D currentFramePoint,
+ Point3D nextFramePoint,
+ out Rotation3D rotation)
+ {
+ rotation = Rotation3D.Identity;
+
+ if (!IsRealObjectMode)
+ {
+ return false;
+ }
+
+ if (pathType == PathType.Ground)
+ {
+ return TryCreateGroundRealObjectConstrainedRotation(
+ previousFramePoint,
+ currentFramePoint,
+ nextFramePoint,
+ out rotation);
+ }
+
+ if (pathType == PathType.Hoisting)
+ {
+ Vector3 hostForward = new Vector3(
+ (float)(nextFramePoint.X - previousFramePoint.X),
+ (float)(nextFramePoint.Y - previousFramePoint.Y),
+ (float)(nextFramePoint.Z - previousFramePoint.Z));
+
+ if (hostForward.LengthSquared() <= 1e-6f)
+ {
+ return false;
+ }
+
+ return TryResolveHoistingActualPose(
+ hostForward,
+ out rotation,
+ out _,
+ out _,
+ out _,
+ out _);
+ }
+
+ return false;
+ }
+
private bool TryResolveHoistingActualPose(
Vector3 hostForward,
out Rotation3D rotation,
From a252dad46a03ae3f3abf353b29e90cf47e56de50 Mon Sep 17 00:00:00 2001
From: tian <11429339@qq.com>
Date: Fri, 10 Apr 2026 22:23:46 +0800
Subject: [PATCH 73/87] remove pose fallbacks and fix script compatibility
---
scripts/Fix-RailPreferredNormals.ps1 | 30 ++++---
src/Core/Animation/PathAnimationManager.cs | 97 +++-------------------
2 files changed, 28 insertions(+), 99 deletions(-)
diff --git a/scripts/Fix-RailPreferredNormals.ps1 b/scripts/Fix-RailPreferredNormals.ps1
index d85a680..990809f 100644
--- a/scripts/Fix-RailPreferredNormals.ps1
+++ b/scripts/Fix-RailPreferredNormals.ps1
@@ -1,3 +1,11 @@
+[CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Medium')]
+param(
+ [Parameter(Mandatory = $true)]
+ [string]$Path,
+
+ [switch]$Recurse
+)
+
<#
用法示例
@@ -18,13 +26,6 @@ powershell -ExecutionPolicy Bypass -File "C:\Users\Tellme\apps\NavisworksTranspo
- 原文件会被直接覆盖,同时自动保留一个同名 .bak 备份文件
#>
-param(
- [Parameter(Mandatory = $true)]
- [string]$Path,
-
- [switch]$Recurse
-)
-
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
@@ -130,7 +131,7 @@ function Update-XmlFile {
)
[xml]$xml = Get-Content -LiteralPath $FilePath -Raw
- $routeNodes = $xml.SelectNodes("//*[local-name()='Route']")
+ $routeNodes = @($xml.SelectNodes("//*[local-name()='Route']"))
if ($null -eq $routeNodes -or $routeNodes.Count -eq 0) {
return [pscustomobject]@{
FilePath = $FilePath
@@ -148,11 +149,13 @@ function Update-XmlFile {
if ($modifiedRouteCount -gt 0) {
$backupPath = "$FilePath.bak"
- if (-not (Test-Path -LiteralPath $backupPath)) {
- Copy-Item -LiteralPath $FilePath -Destination $backupPath
- }
+ if ($PSCmdlet.ShouldProcess($FilePath, "修复 railPreferredNormal 并写回 XML")) {
+ if (-not (Test-Path -LiteralPath $backupPath)) {
+ Copy-Item -LiteralPath $FilePath -Destination $backupPath
+ }
- Save-XmlDocument -Document $xml -TargetPath $FilePath
+ Save-XmlDocument -Document $xml -TargetPath $FilePath
+ }
}
return [pscustomobject]@{
@@ -175,7 +178,7 @@ if ($item.PSIsContainer) {
$childParams["Recurse"] = $true
}
- $files = Get-ChildItem @childParams | Where-Object { $_.Extension -ieq ".xml" }
+ $files = @(Get-ChildItem @childParams | Where-Object { $_.Extension -ieq ".xml" })
}
else {
$files = @($item)
@@ -191,6 +194,7 @@ foreach ($file in $files) {
$results += Update-XmlFile -FilePath $file.FullName
}
+$results = @($results)
$modifiedFiles = @($results | Where-Object { $_.Modified })
$modifiedRoutes = 0
if ($modifiedFiles.Count -gt 0) {
diff --git a/src/Core/Animation/PathAnimationManager.cs b/src/Core/Animation/PathAnimationManager.cs
index 8d1a838..1eef1cd 100644
--- a/src/Core/Animation/PathAnimationManager.cs
+++ b/src/Core/Animation/PathAnimationManager.cs
@@ -5280,21 +5280,19 @@ namespace NavisworksTransport.Core.Animation
return false;
}
- try
+ if (_route?.PathType == PathType.Ground)
{
- if (_route?.PathType != PathType.Ground &&
- TryCaptureRealObjectReferenceRotation(_animatedObject, out referenceRotation))
- {
- return true;
- }
+ return false;
+ }
- return TryCaptureFallbackRealObjectReferenceRotation(_animatedObject, out referenceRotation);
- }
- catch (Exception ex)
+ if (!TryCaptureRealObjectReferenceRotation(_animatedObject, out referenceRotation))
{
- LogManager.Warning($"[真实物体参考姿态] 读取 fragment 参考位姿失败,改用默认姿态: {ex.Message}");
- return TryCaptureFallbackRealObjectReferenceRotation(_animatedObject, out referenceRotation);
+ string objectName = _animatedObject?.DisplayName ?? "未知对象";
+ LogManager.Error($"[真实物体参考姿态] {objectName} 无法获取 fragment 参考姿态,已禁止回退默认姿态。");
+ return false;
}
+
+ return true;
}
private bool TryCaptureRealObjectReferenceRotation(ModelItem sourceObject, out Quaternion referenceRotation)
@@ -5349,78 +5347,6 @@ namespace NavisworksTransport.Core.Animation
}
}
- private bool TryCaptureFallbackRealObjectReferenceRotation(ModelItem sourceObject, out Quaternion referenceRotation)
- {
- referenceRotation = Quaternion.Identity;
-
- if (sourceObject == null)
- {
- return false;
- }
-
- try
- {
- Rotation3D fallbackRotation3D = Rotation3D.Identity;
- if (!ModelItemTransformHelper.TryGetCurrentGeometryRotation(sourceObject, out fallbackRotation3D))
- {
- fallbackRotation3D = sourceObject.Transform?.Factor().Rotation ?? Rotation3D.Identity;
- }
-
- referenceRotation = new Quaternion(
- (float)fallbackRotation3D.A,
- (float)fallbackRotation3D.B,
- (float)fallbackRotation3D.C,
- (float)fallbackRotation3D.D);
-
- if (referenceRotation.LengthSquared() < 1e-6f)
- {
- referenceRotation = Quaternion.Identity;
- }
- else
- {
- referenceRotation = Quaternion.Normalize(referenceRotation);
- }
-
- _realObjectReferenceRotation = referenceRotation;
- _realObjectReferenceAxisX = Vector3.Normalize(Vector3.Transform(Vector3.UnitX, referenceRotation));
- _realObjectReferenceAxisY = Vector3.Normalize(Vector3.Transform(Vector3.UnitY, referenceRotation));
- _realObjectReferenceAxisZ = Vector3.Normalize(Vector3.Transform(Vector3.UnitZ, referenceRotation));
- _realObjectReferenceHostWorldXAxisLocalAxis = LocalAxisDirection.PositiveX;
- _realObjectReferenceHostWorldYAxisLocalAxis = LocalAxisDirection.PositiveY;
- _realObjectReferenceHostWorldZAxisLocalAxis = LocalAxisDirection.PositiveZ;
- _realObjectReferenceHostSemanticXAxisLocalAxis = LocalAxisDirection.PositiveX;
-
- var adapter = CoordinateSystemManager.Instance.CreateHostAdapter();
- if (adapter.HostType == CoordinateSystemType.YUp)
- {
- _realObjectReferenceHostSemanticYAxisLocalAxis = LocalAxisDirection.PositiveY;
- _realObjectReferenceHostSemanticZAxisLocalAxis = LocalAxisDirection.PositiveZ;
- _realObjectReferenceHostUpLocalAxis = LocalAxisDirection.PositiveY;
- }
- else
- {
- _realObjectReferenceHostSemanticYAxisLocalAxis = LocalAxisDirection.PositiveY;
- _realObjectReferenceHostSemanticZAxisLocalAxis = LocalAxisDirection.PositiveZ;
- _realObjectReferenceHostUpLocalAxis = LocalAxisDirection.PositiveZ;
- }
-
- _hasRealObjectReferenceRotation = true;
-
- LogManager.Warning(
- $"[真实物体参考姿态] {sourceObject.DisplayName} 已改用非 fragment 参考姿态继续业务流程: " +
- $"FallbackX=({_realObjectReferenceAxisX.X:F4},{_realObjectReferenceAxisX.Y:F4},{_realObjectReferenceAxisX.Z:F4}), " +
- $"FallbackY=({_realObjectReferenceAxisY.X:F4},{_realObjectReferenceAxisY.Y:F4},{_realObjectReferenceAxisY.Z:F4}), " +
- $"FallbackZ=({_realObjectReferenceAxisZ.X:F4},{_realObjectReferenceAxisZ.Y:F4},{_realObjectReferenceAxisZ.Z:F4}), " +
- $"宿主Up对应本地轴={_realObjectReferenceHostUpLocalAxis}");
- return true;
- }
- catch (Exception ex)
- {
- LogManager.Error($"[真实物体参考姿态] {sourceObject.DisplayName} 默认参考姿态回退失败: {ex.Message}");
- return false;
- }
- }
-
private void SyncTrackedRotationToObjectReference(ModelItem sourceObject, bool isVirtualObject)
{
if (sourceObject == null)
@@ -5472,9 +5398,8 @@ namespace NavisworksTransport.Core.Animation
return;
}
- _currentYaw = ModelItemTransformHelper.GetYawFromTransform(sourceObject.Transform);
- _trackedRotation = sourceObject.Transform.Factor().Rotation;
- _hasTrackedRotation = true;
+ throw new InvalidOperationException(
+ $"[真实物体参考姿态] {sourceObject.DisplayName} 无法同步参考姿态:未能读取当前实际几何姿态,fragment 代表姿态也不可用,已禁止回退 Transform。");
}
private static Rotation3D CreateVirtualObjectReferenceRotation()
From 18b530ef5f3423b69bc5fc3c8407aa4df6fbc1ba Mon Sep 17 00:00:00 2001
From: tian <11429339@qq.com>
Date: Sat, 11 Apr 2026 09:04:03 +0800
Subject: [PATCH 74/87] Unify WPF numeric textbox editing behavior
---
TransportPlugin.csproj | 1 +
.../WPF/Converters/EditableNumberConverter.cs | 127 ++++++++++++++++++
src/UI/WPF/Views/AnimationControlView.xaml | 3 +-
src/UI/WPF/Views/EditCoordinatesWindow.xaml | 11 +-
.../WPF/Views/EditCoordinatesWindow.xaml.cs | 57 +++++++-
src/UI/WPF/Views/EditRotationWindow.xaml | 10 +-
src/UI/WPF/Views/EditRotationWindow.xaml.cs | 60 ++++++++-
src/UI/WPF/Views/ModelSettingsView.xaml | 7 +-
src/UI/WPF/Views/PathEditingView.xaml | 21 +--
src/UI/WPF/Views/TimeTagDialog.xaml | 8 +-
10 files changed, 269 insertions(+), 36 deletions(-)
create mode 100644 src/UI/WPF/Converters/EditableNumberConverter.cs
diff --git a/TransportPlugin.csproj b/TransportPlugin.csproj
index 3cd6540..4bbb6e9 100644
--- a/TransportPlugin.csproj
+++ b/TransportPlugin.csproj
@@ -307,6 +307,7 @@
+
diff --git a/src/UI/WPF/Converters/EditableNumberConverter.cs b/src/UI/WPF/Converters/EditableNumberConverter.cs
new file mode 100644
index 0000000..ee7886a
--- /dev/null
+++ b/src/UI/WPF/Converters/EditableNumberConverter.cs
@@ -0,0 +1,127 @@
+using System;
+using System.Globalization;
+using System.Windows.Data;
+
+namespace NavisworksTransport.UI.WPF.Converters
+{
+ ///
+ /// Keeps numeric TextBox editing humane: incomplete text is allowed while editing,
+ /// and only complete numeric values are written back to the bound source.
+ ///
+ public class EditableNumberConverter : IValueConverter
+ {
+ public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ if (value == null)
+ {
+ return string.Empty;
+ }
+
+ string format = parameter as string;
+ if (string.IsNullOrWhiteSpace(format))
+ {
+ return System.Convert.ToString(value, culture);
+ }
+
+ if (value is IFormattable formattable)
+ {
+ return formattable.ToString(format, culture);
+ }
+
+ return System.Convert.ToString(value, culture);
+ }
+
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ string text = value as string;
+ if (IsIntermediateText(text))
+ {
+ throw new FormatException("请输入有效的数字。");
+ }
+
+ text = text.Trim();
+ Type actualTargetType = Nullable.GetUnderlyingType(targetType) ?? targetType;
+
+ if (TryParseNumber(text, culture, actualTargetType, out object parsedValue))
+ {
+ return parsedValue;
+ }
+
+ throw new FormatException("请输入有效的数字。");
+ }
+
+ public static bool IsIntermediateText(string text)
+ {
+ if (string.IsNullOrWhiteSpace(text))
+ {
+ return true;
+ }
+
+ text = text.Trim();
+
+ return text == "-" ||
+ text == "+" ||
+ text == "." ||
+ text == "-." ||
+ text == "+.";
+ }
+
+ public static bool TryParseNumber(string text, CultureInfo culture, Type targetType, out object parsedValue)
+ {
+ NumberStyles styles = NumberStyles.Float | NumberStyles.AllowThousands;
+
+ if (targetType == typeof(double))
+ {
+ if (TryParseDouble(text, styles, culture, out double doubleValue))
+ {
+ parsedValue = doubleValue;
+ return true;
+ }
+ }
+ else if (targetType == typeof(float))
+ {
+ if (TryParseDouble(text, styles, culture, out double doubleValue))
+ {
+ parsedValue = (float)doubleValue;
+ return true;
+ }
+ }
+ else if (targetType == typeof(decimal))
+ {
+ if (decimal.TryParse(text, styles, culture, out decimal decimalValue) ||
+ decimal.TryParse(text, styles, CultureInfo.InvariantCulture, out decimalValue))
+ {
+ parsedValue = decimalValue;
+ return true;
+ }
+ }
+ else if (targetType == typeof(int))
+ {
+ if (int.TryParse(text, NumberStyles.Integer, culture, out int intValue) ||
+ int.TryParse(text, NumberStyles.Integer, CultureInfo.InvariantCulture, out intValue))
+ {
+ parsedValue = intValue;
+ return true;
+ }
+ }
+ else if (targetType == typeof(long))
+ {
+ if (long.TryParse(text, NumberStyles.Integer, culture, out long longValue) ||
+ long.TryParse(text, NumberStyles.Integer, CultureInfo.InvariantCulture, out longValue))
+ {
+ parsedValue = longValue;
+ return true;
+ }
+ }
+
+ parsedValue = null;
+ return false;
+ }
+
+ private static bool TryParseDouble(string text, NumberStyles styles, CultureInfo culture, out double value)
+ {
+ return double.TryParse(text, styles, culture, out value) ||
+ double.TryParse(text, styles, CultureInfo.InvariantCulture, out value);
+ }
+ }
+}
diff --git a/src/UI/WPF/Views/AnimationControlView.xaml b/src/UI/WPF/Views/AnimationControlView.xaml
index f6226aa..eb9efcb 100644
--- a/src/UI/WPF/Views/AnimationControlView.xaml
+++ b/src/UI/WPF/Views/AnimationControlView.xaml
@@ -27,6 +27,7 @@ NavisworksTransport 检测动画页签视图 - 采用与类别设置和分层管
+