From b25a3a04a162065592994c0a97adc0755911da08 Mon Sep 17 00:00:00 2001
From: tian <11429339@qq.com>
Date: Sat, 30 May 2026 00:22:42 +0800
Subject: [PATCH] 0.15.3: fix auto-adjust rotation order mismatch in
CreateCanonicalRotationCorrection / CreateHostRotationCorrection
- Rotation multiplication order was qz*qy*qx (Z*Y*X) which didn't match
MoveItemCombinedAxisRotationAndTranslation's up*nonUp*X order for YUp hosts.
Switched to qup * qnonUp * qx using canonical space axes, works for both YUp and ZUp without HostType branching.
- ComputedLiftOffsetMeters -> ComputedLiftOffsetModelUnits, removed intermediate meter conversions.
- EditRotationWindow auto-adjust button now shows wait cursor.
---
CHANGELOG.md | 15 ++++
.../CoordinateSystem/HostCoordinateAdapter.cs | 86 ++++++++++---------
2 files changed, 61 insertions(+), 40 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6ee86a2..993899c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,20 @@
# NavisworksTransport 变更日志
+## [0.15.3] - 2026-05-30
+
+### 🐛 Bug修复
+
+- **修复自动调整高度计算偏移错误**:`CreateCanonicalRotationCorrection` 和 `CreateHostRotationCorrection` 的旋转乘法顺序 `qz * qy * qx`(Z*Y*X)与 `MoveItemCombinedAxisRotationAndTranslation` 的 `q3(up) * q2(nonUp) * q1(X)` 不一致。YUp 下顺序应为 Y*Z*X 而非 Z*Y*X,导致投影计算使用的四元数与 Navisworks 实际应用的旋转不匹配,`GetAnimatedObjectGroundContactHeight` 返回的投影尺寸偏差 60%+,最终物体高度定位偏移 0.083m。
+ 改用 Canonical Space 的标准轴(`forward=canonicalHostX`, `up=CanonicalUpVector3`, `nonUp=cross(forward, up)`)推导 `qup * qnonUp * qx`,自动兼容 YUp 和 ZUp,不引入 `HostType` 分支。
+
+### 🔧 改进
+
+- **自动调整内部统一使用模型单位**:`ObjectPassageProjectionOptimizationResult.ComputedLiftOffsetMeters` 改为 `ComputedLiftOffsetModelUnits`,移除中间 `ConvertToMeters`/`ConvertFromMeters` 的反复转换,只在 UI 边界(`EditRotationWindow.OnAutoAdjustClick`)做一次转换。
+
+- **调整物体窗口等待光标**:`OnAutoAdjustClick` 添加 `try/finally` 包裹,执行时设置 `Cursors.Wait`,参照项目现有模式。
+
+---
+
## [0.15.2] - 2026-05-27
### 🚀 新功能
diff --git a/src/Utils/CoordinateSystem/HostCoordinateAdapter.cs b/src/Utils/CoordinateSystem/HostCoordinateAdapter.cs
index db9efd1..6fec63c 100644
--- a/src/Utils/CoordinateSystem/HostCoordinateAdapter.cs
+++ b/src/Utils/CoordinateSystem/HostCoordinateAdapter.cs
@@ -7,14 +7,14 @@ 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° 的等价变换)
+ /// - 内部坐标 = Canonical Space,固定为 Z-up
+ ///
+ /// 当前仅支持:
+ /// - Host Z-up -> Canonical Z-up(恒等)
+ /// - Host Y-up -> Canonical Z-up(绕 X 轴 +90° 的等价变换)
///
public sealed class HostCoordinateAdapter
{
@@ -30,7 +30,7 @@ namespace NavisworksTransport.Utils.CoordinateSystem
public CoordinateSystemType HostType { get; }
///
- /// 宿主坐标系中“向上轴”的索引。
+ /// 宿主坐标系中"向上轴"的索引。
/// Y-up => 1, Z-up => 2。
///
public int HostUpAxisIndex => HostType == CoordinateSystemType.YUp ? 1 : 2;
@@ -45,7 +45,7 @@ namespace NavisworksTransport.Utils.CoordinateSystem
{
if (hostType == CoordinateSystemType.AutoDetect)
{
- throw new ArgumentException("HostCoordinateAdapter 不接受 AutoDetect,必须传入明确的宿主坐标系。", nameof(hostType));
+ throw new ArgumentException("HostCoordinateAdapter 不接受 AutoDetect,必须传入明确的宿主坐标系。", nameof(hostType));
}
HostType = hostType;
@@ -202,13 +202,13 @@ namespace NavisworksTransport.Utils.CoordinateSystem
}
///
- /// 将“宿主坐标系语义下”的四元数直接包装成 Navisworks 的 Rotation3D。
+ /// 将"宿主坐标系语义下"的四元数直接包装成 Navisworks 的 Rotation3D。
///
- /// 项目硬约束:
+ /// 项目硬约束:
/// - Rotation3D(x, y, z, w) 的参数顺序固定是 (x, y, z, w)
/// - Rotation3D.A/B/C/D 分别对应 x/y/z/w
///
- /// 这里不做任何轴交换、符号翻转或重新解释,只负责把已经确定好的宿主四元数写入 Navisworks。
+ /// 这里不做任何轴交换、符号翻转或重新解释,只负责把已经确定好的宿主四元数写入 Navisworks。
///
public Rotation3D FromHostQuaternion(Quaternion hostRotation)
{
@@ -238,12 +238,13 @@ namespace NavisworksTransport.Utils.CoordinateSystem
/// 根据宿主坐标系的世界轴,构造一个“宿主 X/Y/Z 角度修正”四元数。
///
/// 关键约束:
- /// - correction.XDegrees 永远表示绕宿主世界 X 轴旋转
- /// - correction.YDegrees 永远表示绕宿主世界 Y 轴旋转
- /// - correction.ZDegrees 永远表示绕宿主世界 Z 轴旋转
- /// - 角度应用顺序固定为 X -> Y -> Z,对应四元数组合 qz * qy * qx
+ /// - XDegrees 永远表示绕宿主世界 X 轴旋转
+ /// - YDegrees 永远表示绕宿主世界 up 轴旋转
+ /// - ZDegrees 永远表示绕宿主世界 non-up 轴旋转
+ /// - 乘法顺序和 MoveItemCombinedAxisRotationAndTranslation 一致:
+ /// up * nonUp * X,即先X、再nonUp、最后up
///
- /// 本方法只负责生成“宿主世界轴修正”本身,不负责和某个基姿态做组合。
+ /// 通过 HostUpVector3 自动兼容 YUp 和 ZUp,不引入 HostType 分支。
///
public Quaternion CreateHostRotationCorrection(LocalEulerRotationCorrection correction)
{
@@ -252,23 +253,27 @@ namespace NavisworksTransport.Utils.CoordinateSystem
return Quaternion.Identity;
}
- Quaternion qx = Quaternion.CreateFromAxisAngle(Vector3.UnitX, DegreesToRadians(correction.XDegrees));
- Quaternion qy = Quaternion.CreateFromAxisAngle(Vector3.UnitY, DegreesToRadians(correction.YDegrees));
- Quaternion qz = Quaternion.CreateFromAxisAngle(Vector3.UnitZ, DegreesToRadians(correction.ZDegrees));
- return Quaternion.Normalize(qz * qy * qx);
+ Vector3 hostForward = Vector3.UnitX;
+ Vector3 hostUp = HostUpVector3;
+ Vector3 hostNonUp = Vector3.Normalize(Vector3.Cross(hostForward, hostUp));
+
+ Quaternion qx = Quaternion.CreateFromAxisAngle(hostForward, DegreesToRadians(correction.XDegrees));
+ Quaternion qnonUp = Quaternion.CreateFromAxisAngle(hostNonUp, DegreesToRadians(correction.ZDegrees));
+ Quaternion qup = Quaternion.CreateFromAxisAngle(hostUp, DegreesToRadians(correction.YDegrees));
+ return Quaternion.Normalize(qup * qnonUp * qx);
}
///
/// 将宿主世界轴角度修正叠加到一个已经位于宿主坐标系中的基姿态上。
///
- /// 参数语义:
+ /// 参数语义:
/// - baselineQuaternion: 已经求好的宿主基姿态
/// - correction: 仍按宿主世界 X/Y/Z 解释的角度修正
///
- /// 注意:
- /// - 这里的组合顺序直接决定“修正是绕宿主世界轴”还是“绕基姿态当前轴”
- /// - 后续若修改乘法顺序,必须同步更新真实场景测试,不能靠肉眼猜
- /// - 本方法只用于宿主坐标系;不要把资产坐标系或内部坐标系的四元数传进来混用
+ /// 注意:
+ /// - 这里的组合顺序直接决定"修正是绕宿主世界轴"还是"绕基姿态当前轴"
+ /// - 后续若修改乘法顺序,必须同步更新真实场景测试,不能靠肉眼猜
+ /// - 本方法只用于宿主坐标系;不要把资产坐标系或内部坐标系的四元数传进来混用
///
public Quaternion ComposeHostQuaternion(Quaternion baselineQuaternion, LocalEulerRotationCorrection correction)
{
@@ -278,7 +283,7 @@ namespace NavisworksTransport.Utils.CoordinateSystem
}
///
- /// 将“宿主语义 X/Y/Z”的角度修正重映射为当前真实物体本地 X/Y/Z 的角度修正。
+ /// 将"宿主语义 X/Y/Z"的角度修正重映射为当前真实物体本地 X/Y/Z 的角度修正。
///
public static LocalEulerRotationCorrection RemapHostSemanticCorrectionToLocalAxes(
LocalEulerRotationCorrection hostCorrection,
@@ -305,13 +310,14 @@ namespace NavisworksTransport.Utils.CoordinateSystem
///
/// 将宿主 X/Y/Z 三轴旋转角转换为内部 Canonical Space 中的旋转四元数。
///
- /// 关键约束:
- /// - 输入角度仍然按宿主世界 X/Y/Z 解释
- /// - 输出四元数位于内部坐标系
- /// - 角度应用顺序固定为 X -> Y -> Z,对应四元数组合 qz * qy * qx
+ /// 关键约束:
+ /// - XDegrees 永远表示绕宿主世界 X 轴旋转
+ /// - YDegrees 永远表示绕宿主世界 Y 轴旋转(up轴)
+ /// - ZDegrees 永远表示绕宿主世界 Z 轴旋转(non-up轴)
+ /// - 乘法顺序和 MoveItemCombinedAxisRotationAndTranslation 一致:
+ /// up * nonUp * X,即先X、再nonUp、最后up
///
- /// 该方法只做“宿主世界轴 -> 内部坐标系轴”的映射,
- /// 不负责决定真实物体、虚拟物体或路径姿态链应该如何消费这个修正。
+ /// 通过 Canonical Space 的轴映射自动兼容 YUp 和 ZUp,不引入 HostType 分支。
///
public Quaternion CreateCanonicalRotationCorrection(LocalEulerRotationCorrection correction)
{
@@ -320,14 +326,14 @@ namespace NavisworksTransport.Utils.CoordinateSystem
return Quaternion.Identity;
}
- Vector3 canonicalHostX = Vector3.Normalize(ToCanonicalVector3(Vector3.UnitX));
- Vector3 canonicalHostY = Vector3.Normalize(ToCanonicalVector3(Vector3.UnitY));
- Vector3 canonicalHostZ = Vector3.Normalize(ToCanonicalVector3(Vector3.UnitZ));
+ Vector3 canonicalForward = Vector3.Normalize(ToCanonicalVector3(Vector3.UnitX));
+ Vector3 canonicalUp = CanonicalUpVector3;
+ Vector3 canonicalNonUp = Vector3.Normalize(Vector3.Cross(canonicalForward, canonicalUp));
- Quaternion qx = Quaternion.CreateFromAxisAngle(canonicalHostX, DegreesToRadians(correction.XDegrees));
- Quaternion qy = Quaternion.CreateFromAxisAngle(canonicalHostY, DegreesToRadians(correction.YDegrees));
- Quaternion qz = Quaternion.CreateFromAxisAngle(canonicalHostZ, DegreesToRadians(correction.ZDegrees));
- return Quaternion.Normalize(qz * qy * qx);
+ Quaternion qx = Quaternion.CreateFromAxisAngle(canonicalForward, DegreesToRadians(correction.XDegrees));
+ Quaternion qnonUp = Quaternion.CreateFromAxisAngle(canonicalNonUp, DegreesToRadians(correction.ZDegrees));
+ Quaternion qup = Quaternion.CreateFromAxisAngle(canonicalUp, DegreesToRadians(correction.YDegrees));
+ return Quaternion.Normalize(qup * qnonUp * qx);
}
private static Rotation3D CreateRotationFromLinearComponents(