diff --git a/UnitTests/CoordinateSystem/AlignToGroundMinCrossSectionYawSearcherTests.cs b/UnitTests/CoordinateSystem/AlignToGroundMinCrossSectionYawSearcherTests.cs
index 0220b1a..5d46eb2 100644
--- a/UnitTests/CoordinateSystem/AlignToGroundMinCrossSectionYawSearcherTests.cs
+++ b/UnitTests/CoordinateSystem/AlignToGroundMinCrossSectionYawSearcherTests.cs
@@ -219,5 +219,206 @@ namespace NavisworksTransport.UnitTests.CoordinateSystem
// 立方体任何 yaw 面积相同,只要返回有效四元数即可
Assert.AreEqual(1.0, Quaternion.Normalize(q).Length(), 1e-5);
}
+
+ // ----- ComposeHostCorrection:扣除 CAD yaw + pathYaw,避免重复旋转 -----
+
+ ///
+ /// PathAnimationManager 增量链语义闭环验证:
+ /// finalQ = qup(pathYaw - currentYaw) * CreateHostRotationCorrection(correction) * cadQ
+ /// 应等于 overrideQ * cadQ(贴地时实际施加的最终姿态)
+ /// cadQ=Identity 时 currentYaw=0,简化为:
+ /// qup(pathYaw) * correctionHostQ == overrideQ
+ ///
+ private static void AssertIncrementalChainClosure(
+ Quaternion overrideQ,
+ Quaternion cadQ,
+ double pathYawRadians,
+ HostCoordinateAdapter adapter,
+ double tolerance = 1e-3)
+ {
+ LocalEulerRotationCorrection correction =
+ AlignToGroundMinCrossSectionYawSearcher.ComposeHostCorrection(
+ overrideQ, cadQ, pathYawRadians, adapter);
+
+ Quaternion correctionHostQ = adapter.CreateHostRotationCorrection(correction);
+
+ // currentYaw 从 cadQ 提取(与 ComposeHostCorrection 内部一致)
+ Matrix4x4 cadLinear = Matrix4x4.CreateFromQuaternion(cadQ);
+ Vector3 cadFwdHost = new Vector3(cadLinear.M11, cadLinear.M21, cadLinear.M31);
+ Vector3 cadFwdCanon = adapter.ToCanonicalVector3(cadFwdHost);
+ cadFwdCanon.Z = 0f;
+ double currentYaw = cadFwdCanon.LengthSquared() > 1e-9f
+ ? Math.Atan2(Vector3.Normalize(cadFwdCanon).Y, Vector3.Normalize(cadFwdCanon).X)
+ : 0.0;
+
+ // 增量链:finalQ = qup(pathYaw - currentYaw) * correctionHostQ * cadQ
+ Quaternion deltaQ = Quaternion.CreateFromAxisAngle(
+ adapter.HostUpVector3, (float)(pathYawRadians - currentYaw));
+ Quaternion reconstructedFinal = Quaternion.Normalize(deltaQ * correctionHostQ * cadQ);
+ Quaternion expectedFinal = Quaternion.Normalize(overrideQ * cadQ);
+
+ for (int axis = 0; axis < 3; axis++)
+ {
+ Vector3 v = axis == 0 ? Vector3.UnitX : (axis == 1 ? Vector3.UnitY : Vector3.UnitZ);
+ Vector3 expected = Vector3.Normalize(Vector3.Transform(v, expectedFinal));
+ Vector3 actual = Vector3.Normalize(Vector3.Transform(v, reconstructedFinal));
+ double dot = Vector3.Dot(expected, actual);
+ Assert.IsTrue(Math.Abs(dot - 1.0) < tolerance,
+ $"axis {axis}: 增量链闭环偏差,dot={dot:F6}");
+ }
+ }
+
+ [TestMethod]
+ public void ComposeCorrection_CadIdentity_PurePathYaw_ReturnsZeroCorrection_ZUp()
+ {
+ // CAD=Identity(currentYaw=0), overrideQ=纯 pathYaw, correction 应为 0
+ var adapter = new HostCoordinateAdapter(CoordinateSystemType.ZUp);
+ double pathYaw = Deg(30.0);
+ Quaternion cadQ = Quaternion.Identity;
+ Quaternion overrideQ = Quaternion.CreateFromAxisAngle(adapter.HostUpVector3, (float)pathYaw);
+
+ LocalEulerRotationCorrection correction =
+ AlignToGroundMinCrossSectionYawSearcher.ComposeHostCorrection(
+ overrideQ, cadQ, pathYaw, adapter);
+
+ Assert.AreEqual(0.0, correction.XDegrees, 1e-3, $"X={correction.XDegrees:F4}");
+ Assert.AreEqual(0.0, correction.YDegrees, 1e-3, $"Y(up)={correction.YDegrees:F4}");
+ Assert.AreEqual(0.0, correction.ZDegrees, 1e-3, $"Z(nonUp)={correction.ZDegrees:F4}");
+ }
+
+ [TestMethod]
+ public void ComposeCorrection_CadIdentity_PurePathYaw_ReturnsZeroCorrection_YUp()
+ {
+ var adapter = new HostCoordinateAdapter(CoordinateSystemType.YUp);
+ double pathYaw = Deg(-15.62);
+ Quaternion cadQ = Quaternion.Identity;
+ Quaternion overrideQ = Quaternion.CreateFromAxisAngle(adapter.HostUpVector3, (float)pathYaw);
+
+ LocalEulerRotationCorrection correction =
+ AlignToGroundMinCrossSectionYawSearcher.ComposeHostCorrection(
+ overrideQ, cadQ, pathYaw, adapter);
+
+ Assert.AreEqual(0.0, correction.XDegrees, 1e-3, $"X={correction.XDegrees:F4}");
+ Assert.AreEqual(0.0, correction.YDegrees, 1e-3, $"Y(up)={correction.YDegrees:F4}");
+ Assert.AreEqual(0.0, correction.ZDegrees, 1e-3, $"Z(nonUp)={correction.ZDegrees:F4}");
+ }
+
+ [TestMethod]
+ public void ComposeCorrection_CadIdentity_FaceDownPlusPathYaw_Closure_YUp()
+ {
+ // CAD=Identity, overrideQ = pathYawQ * faceDown
+ // 增量链闭环验证
+ var adapter = new HostCoordinateAdapter(CoordinateSystemType.YUp);
+ double pathYaw = Deg(-15.62);
+ Quaternion cadQ = Quaternion.Identity;
+ Quaternion faceDown = Quaternion.CreateFromAxisAngle(Vector3.UnitX, (float)Deg(15.0));
+ Quaternion pathYawQ = Quaternion.CreateFromAxisAngle(adapter.HostUpVector3, (float)pathYaw);
+ Quaternion overrideQ = Quaternion.Normalize(pathYawQ * faceDown);
+
+ AssertIncrementalChainClosure(overrideQ, cadQ, pathYaw, adapter);
+ }
+
+ [TestMethod]
+ public void ComposeCorrection_CadIdentity_SearchedYawPlusFaceDown_Closure_YUp()
+ {
+ // 模拟真实贴地:CAD=Identity, Search 出 overrideQ, 验证增量链闭环
+ var adapter = new HostCoordinateAdapter(CoordinateSystemType.YUp);
+ Quaternion cadQ = Quaternion.Identity;
+ Quaternion faceDown = Quaternion.CreateFromAxisAngle(Vector3.UnitX, (float)Deg(25.0));
+ Vector3 pathFwd = new Vector3(0.96f, 0.0f, -0.28f);
+
+ Quaternion overrideQ = AlignToGroundMinCrossSectionYawSearcher.Search(
+ faceDown, cadQ, sizeX: 8.0, sizeY: 4.0, sizeZ: 2.0,
+ hostPathForward: pathFwd, adapter: adapter);
+
+ PathTargetFrameResolver.TryResolvePlanarHostYaw(pathFwd, CoordinateSystemType.YUp, out double pathYaw);
+
+ AssertIncrementalChainClosure(overrideQ, cadQ, pathYaw, adapter, tolerance: 2e-3);
+ }
+
+ [TestMethod]
+ public void ComposeCorrection_SkewedCad_IncrementalChainClosure_YUp()
+ {
+ // CAD 姿态天然歪斜(带 yaw),验证增量链仍闭环
+ // 这是日志中 25"x25" 的场景:cadQ 自带 -124° yaw
+ var adapter = new HostCoordinateAdapter(CoordinateSystemType.YUp);
+ // 构造 CAD 姿态:绕 host Y(up) 转 -124° + 一些倾斜
+ Quaternion cadQ = Quaternion.Normalize(
+ Quaternion.CreateFromAxisAngle(Vector3.UnitY, (float)Deg(-124.0)) *
+ Quaternion.CreateFromAxisAngle(Vector3.UnitX, (float)Deg(10.0)));
+ double pathYaw = Deg(-15.62);
+ // overrideQ = searchedYawQ * faceDown(贴地搜索结果)
+ Quaternion faceDown = Quaternion.CreateFromAxisAngle(Vector3.UnitX, (float)Deg(20.0));
+ Quaternion searchedYawQ = Quaternion.CreateFromAxisAngle(adapter.HostUpVector3, (float)Deg(-57.0));
+ Quaternion overrideQ = Quaternion.Normalize(searchedYawQ * faceDown);
+
+ AssertIncrementalChainClosure(overrideQ, cadQ, pathYaw, adapter, tolerance: 2e-3);
+ }
+
+ [TestMethod]
+ public void HostQuaternionToCanonical_HostUpRotation_MapsToCanonicalZRotation_YUp()
+ {
+ // YUp: host 绕 Y(up) 转 θ → canonical 绕 Z(up) 转 θ
+ var adapter = new HostCoordinateAdapter(CoordinateSystemType.YUp);
+ double theta = Deg(37.0);
+ Quaternion hostQ = Quaternion.CreateFromAxisAngle(Vector3.UnitY, (float)theta);
+
+ Quaternion canonQ = AlignToGroundMinCrossSectionYawSearcher.HostQuaternionToCanonical(hostQ, adapter);
+
+ // canonical 下应是绕 Z 转 θ
+ Quaternion expected = Quaternion.CreateFromAxisAngle(Vector3.UnitZ, (float)theta);
+ AssertQuaternionsEqual(expected, canonQ);
+ }
+
+ [TestMethod]
+ public void HostQuaternionToCanonical_HostXRotation_MapsToCanonicalXRotation_YUp()
+ {
+ // YUp: host 绕 X(forward) 转 θ → canonical 绕 X(forward) 转 θ(X 轴两坐标系相同)
+ var adapter = new HostCoordinateAdapter(CoordinateSystemType.YUp);
+ double theta = Deg(42.0);
+ Quaternion hostQ = Quaternion.CreateFromAxisAngle(Vector3.UnitX, (float)theta);
+
+ Quaternion canonQ = AlignToGroundMinCrossSectionYawSearcher.HostQuaternionToCanonical(hostQ, adapter);
+
+ Quaternion expected = Quaternion.CreateFromAxisAngle(Vector3.UnitX, (float)theta);
+ AssertQuaternionsEqual(expected, canonQ);
+ }
+
+ [TestMethod]
+ public void HostQuaternionToCanonical_Identity_StaysIdentity_YUp()
+ {
+ // Identity 在两坐标系都应是 Identity(相似变换的不动点)
+ var adapter = new HostCoordinateAdapter(CoordinateSystemType.YUp);
+ Quaternion canonQ = AlignToGroundMinCrossSectionYawSearcher.HostQuaternionToCanonical(
+ Quaternion.Identity, adapter);
+ AssertQuaternionsEqual(Quaternion.Identity, canonQ);
+ }
+
+ [TestMethod]
+ public void HostQuaternionToCanonical_ZUp_IsIdentity()
+ {
+ // ZUp: canonical=host,转换应返回原四元数
+ var adapter = new HostCoordinateAdapter(CoordinateSystemType.ZUp);
+ Quaternion original = Quaternion.CreateFromAxisAngle(
+ new Vector3(0.3f, 0.5f, 0.8f), (float)Deg(55.0f));
+ original = Quaternion.Normalize(original);
+
+ Quaternion canonQ = AlignToGroundMinCrossSectionYawSearcher.HostQuaternionToCanonical(original, adapter);
+ AssertQuaternionsEqual(original, canonQ);
+ }
+
+ private static void AssertQuaternionsEqual(Quaternion expected, Quaternion actual, double tolerance = 1e-4)
+ {
+ // 四元数有双覆盖性(q 和 -q 表示同一旋转),比较作用后的轴
+ for (int axis = 0; axis < 3; axis++)
+ {
+ Vector3 v = axis == 0 ? Vector3.UnitX : (axis == 1 ? Vector3.UnitY : Vector3.UnitZ);
+ Vector3 e = Vector3.Normalize(Vector3.Transform(v, expected));
+ Vector3 a = Vector3.Normalize(Vector3.Transform(v, actual));
+ double dot = Vector3.Dot(e, a);
+ Assert.IsTrue(Math.Abs(dot - 1.0) < tolerance,
+ $"axis {axis}: expected≈actual 失败,dot={dot:F6}");
+ }
+ }
}
}
diff --git a/src/Core/Animation/PathAnimationManager.cs b/src/Core/Animation/PathAnimationManager.cs
index 83e99a4..680fb3c 100644
--- a/src/Core/Animation/PathAnimationManager.cs
+++ b/src/Core/Animation/PathAnimationManager.cs
@@ -5613,6 +5613,62 @@ namespace NavisworksTransport.Core.Animation
LogManager.Debug($"[起点摆放] 直接设置地面路径上下偏移: {verticalLiftInMeters:F3}m");
}
+ ///
+ /// 给定 correction,计算重建后使物体底边贴合目标高度所需的 lift(米单位)。
+ /// 与自动调整的 ComputedLiftOffsetModelUnits 语义一致:
+ /// neededLift = targetBottom - objectBottomAfterRebuild
+ /// 实际应用 correction 重建后测量底边(与自动调整同方法)。
+ /// 测量后用新 correction + 新 lift 重建,使物体停留在正确的贴地等价姿态
+ /// (与确认按钮后的姿态一致),而不是恢复到贴地前的旧姿态。
+ ///
+ public double ComputeGroundLiftForCorrection(
+ LocalEulerRotationCorrection correction,
+ double targetLiftInMeters,
+ ModelItem item)
+ {
+ if (_route?.PathType != PathType.Ground || _pathPoints == null || _pathPoints.Count < 1 || item == null)
+ {
+ return targetLiftInMeters;
+ }
+
+ var hst = CoordinateSystemManager.Instance.ResolvedType;
+ var savedMode = _objectStartPlacementMode;
+ double computedLiftMeters = targetLiftInMeters;
+
+ try
+ {
+ // 用 correction + lift=0 重建,测量底边
+ var measureRequest = ObjectStartPlacementRequest.CreateRotationCorrection(correction, 0);
+ ApplyObjectStartPlacementRequest(measureRequest);
+ BoundingBox3D measureBounds = item.BoundingBox();
+ double objectBottomWorld = hst == CoordinateSystemType.YUp
+ ? measureBounds.Min.Y : measureBounds.Min.Z;
+ double pathStartHeight = hst == CoordinateSystemType.YUp
+ ? _pathPoints[0].Y : _pathPoints[0].Z;
+ double targetBottom = pathStartHeight
+ + UnitsConverter.ConvertFromMeters(targetLiftInMeters);
+ double neededLiftModelUnits = targetBottom - objectBottomWorld;
+ computedLiftMeters = Math.Round(UnitsConverter.ConvertToMeters(neededLiftModelUnits), 3);
+
+ LogManager.Debug(
+ $"[贴合地面] 计算上下偏移: 重建后底边={objectBottomWorld:F3}, 目标底边={targetBottom:F3}, " +
+ $"pathStart={pathStartHeight:F3}, neededLift={computedLiftMeters:F3}m");
+ }
+ finally
+ {
+ // 用新 correction + 新 lift 完整重建(归位 CAD → 增量链旋转+平移),
+ // 使物体停留在与确认按钮一致的贴地等价姿态。
+ // 必须用 skipCadRestore=false:skipCadRestore=true 会清零 correction,
+ // 只施加 yaw 增量而不施加 X/Z 旋转,导致姿态缺失俯仰/翻滚。
+ var applyRequest = ObjectStartPlacementRequest.CreateRotationCorrection(
+ correction, computedLiftMeters);
+ _objectStartPlacementMode = savedMode;
+ ApplyObjectStartPlacementRequest(applyRequest);
+ }
+
+ return computedLiftMeters;
+ }
+
public void SetObjectStartPlacementMode(ObjectStartPlacementMode placementMode)
{
_objectStartPlacementMode = placementMode;
diff --git a/src/UI/WPF/ViewModels/AnimationControlViewModel.cs b/src/UI/WPF/ViewModels/AnimationControlViewModel.cs
index bdd53a5..94a2303 100644
--- a/src/UI/WPF/ViewModels/AnimationControlViewModel.cs
+++ b/src/UI/WPF/ViewModels/AnimationControlViewModel.cs
@@ -2303,6 +2303,8 @@ namespace NavisworksTransport.UI.WPF.ViewModels
// (与"自动调整"同目标,但贴地已固定俯仰/翻滚,只剩 yaw 单自由度)
var routePoints = CurrentPathRoute?.Points;
double? searchedYawDegreesForLog = null;
+ double pathYawRadians = 0.0;
+ bool hasPathYaw = false;
if ((CurrentPathRoute?.PathType == PathType.Ground || CurrentPathRoute?.PathType == PathType.Hoisting)
&& routePoints != null && routePoints.Count >= 2)
{
@@ -2319,8 +2321,11 @@ namespace NavisworksTransport.UI.WPF.ViewModels
break;
}
}
- if (hostForward.LengthSquared() > 1e-8f)
+ if (hostForward.LengthSquared() > 1e-8f
+ && PathTargetFrameResolver.TryResolvePlanarHostYaw(
+ hostForward, CoordinateSystemManager.Instance.ResolvedType, out pathYawRadians))
{
+ hasPathYaw = true;
double metersToUnits = UnitsConverter.GetMetersToUnitsConversionFactor();
double sizeX, sizeY, sizeZ;
if (UseVirtualObject)
@@ -2372,11 +2377,39 @@ namespace NavisworksTransport.UI.WPF.ViewModels
hst == CoordinateSystemType.YUp ? ps.Z - newCenter.Z : groundLevel - objectBottom);
doc.Models.OverridePermanentTransform(modelItems, Transform3D.CreateTranslation(delta), false);
- var resultCorrection = ObjectPassageProjectionOptimizer.CanonicalQuaternionToHostEulerCorrection(overrideQ);
+ // 把绝对贴地姿态转成"相对于 baseline 的 host 欧拉修正"。
+ // PathAnimationManager 增量链从 CAD 姿态出发,需要扣除 CAD 自带 yaw + pathYaw。
+ LocalEulerRotationCorrection resultCorrection;
+ if (hasPathYaw)
+ {
+ resultCorrection = AlignToGroundMinCrossSectionYawSearcher.ComposeHostCorrection(
+ overrideQ, cadQ, pathYawRadians, adapter);
+ }
+ else
+ {
+ // 无路径 yaw(纯垂直路径或无路径):直接转 overrideQ,
+ // 无 baseline 可扣除,correction 等价于绝对姿态。
+ Quaternion correctionCanonQ = AlignToGroundMinCrossSectionYawSearcher.HostQuaternionToCanonical(
+ overrideQ, adapter);
+ resultCorrection = ObjectPassageProjectionOptimizer.CanonicalQuaternionToHostEulerCorrection(
+ correctionCanonQ, adapter.HostType);
+ }
_pathAnimationManager?.SetObjectRotationCorrectionDirect(resultCorrection);
_objectRotationCorrection = resultCorrection;
OnPropertyChanged(nameof(ObjectRotationCorrection));
+ // 回写上下偏移:贴地时用实际包围盒贴地,但确认/动画后用 correction 重建姿态,
+ // 旋转后物体底边位置会变。与自动调整同方法:应用 correction 重建→测量底边→算 lift,
+ // 测量后恢复贴地姿态。
+ if (CurrentPathRoute?.PathType == PathType.Ground && CurrentPathRoute.Points?.Count > 0
+ && _pathAnimationManager != null)
+ {
+ double computedLift = _pathAnimationManager.ComputeGroundLiftForCorrection(
+ resultCorrection, _objectGroundLiftHeightInMeters, item);
+ _objectGroundLiftHeightInMeters = computedLift;
+ _pathAnimationManager.SetObjectStartVerticalLiftDirect(_objectGroundLiftHeightInMeters);
+ }
+
LogManager.Info(
$"[贴合地面] faceNormal=({faceNormal.X:F4},{faceNormal.Y:F4},{faceNormal.Z:F4}) " +
$"cadWorldN=({cadWorldN.X:F4},{cadWorldN.Y:F4},{cadWorldN.Z:F4}) t=({t.X:F4},{t.Y:F4},{t.Z:F4}) " +
diff --git a/src/Utils/CoordinateSystem/AlignToGroundMinCrossSectionYawSearcher.cs b/src/Utils/CoordinateSystem/AlignToGroundMinCrossSectionYawSearcher.cs
index 1acdffb..bd77d21 100644
--- a/src/Utils/CoordinateSystem/AlignToGroundMinCrossSectionYawSearcher.cs
+++ b/src/Utils/CoordinateSystem/AlignToGroundMinCrossSectionYawSearcher.cs
@@ -127,5 +127,83 @@ namespace NavisworksTransport.Utils.CoordinateSystem
return width * height;
}
+
+ ///
+ /// 把贴地搜索后的绝对宿主姿态(overrideQ)转成"相对于路径 baseline 的 host 欧拉修正",
+ /// 与自动调整返回的 correction 语义一致,供动画系统在 baseline 之上叠加。
+ ///
+ /// 关键:PathAnimationManager 增量链从 CAD 姿态出发:
+ /// finalQ = qup(pathYaw + YDeg - currentYaw) * qnonUp(ZDeg) * qx(XDeg) * cadQ
+ /// 贴地时 finalQ = overrideQ * cadQ,所以:
+ /// qup(pathYaw + YDeg - currentYaw) * qnonUp(ZDeg) * qx(XDeg) = overrideQ
+ /// 而 CreateHostRotationCorrection(correction) = qup(YDeg) * qnonUp(ZDeg) * qx(XDeg),所以:
+ /// qup(pathYaw - currentYaw) * CreateHostRotationCorrection(correction) = overrideQ
+ /// => correctionHostQ = qup(currentYaw - pathYaw) * overrideQ
+ ///
+ /// currentYaw 是 CAD 姿态的 canonical yaw,从 cadQ 提取(与 GetYawFromRotation 一致)。
+ public static LocalEulerRotationCorrection ComposeHostCorrection(
+ Quaternion absoluteHostQ,
+ Quaternion cadRotation,
+ double pathYawRadians,
+ HostCoordinateAdapter adapter)
+ {
+ if (adapter == null)
+ {
+ throw new ArgumentNullException(nameof(adapter));
+ }
+
+ double currentYaw = ExtractCanonicalYaw(cadRotation, adapter);
+ Quaternion preRotation = Quaternion.CreateFromAxisAngle(
+ adapter.HostUpVector3, (float)(currentYaw - pathYawRadians));
+ Quaternion correctionHostQ = Quaternion.Normalize(preRotation * absoluteHostQ);
+
+ Quaternion correctionCanonQ = HostQuaternionToCanonical(correctionHostQ, adapter);
+ return ObjectPassageProjectionOptimizer.CanonicalQuaternionToHostEulerCorrection(
+ correctionCanonQ, adapter.HostType);
+ }
+
+ ///
+ /// 从宿主空间四元数提取 canonical yaw(Atan2(canonicalForward.Y, canonicalForward.X))。
+ /// 与 ModelItemTransformHelper.GetYawFromRotation 一致:取 forward 轴(X列),转 canonical,投影 XY 平面。
+ ///
+ private static double ExtractCanonicalYaw(Quaternion hostQ, HostCoordinateAdapter adapter)
+ {
+ Matrix4x4 hostLinear = Matrix4x4.CreateFromQuaternion(hostQ);
+ Vector3 hostForward = new Vector3(hostLinear.M11, hostLinear.M21, hostLinear.M31);
+ Vector3 canonFwd = adapter.ToCanonicalVector3(hostForward);
+ canonFwd.Z = 0f;
+ if (canonFwd.LengthSquared() < 1e-9f)
+ {
+ return 0.0;
+ }
+ canonFwd = Vector3.Normalize(canonFwd);
+ return Math.Atan2(canonFwd.Y, canonFwd.X);
+ }
+
+ ///
+ /// 宿主空间四元数 → canonical 空间四元数。
+ /// 旋转的坐标系转换必须用相似变换 R_canon = M⁻¹ · R_host · M
+ /// (M = canonical→host 坐标变换矩阵,正交故 M⁻¹ = Mᵀ)。
+ /// 不能用列向量映射——那只在"基向量定义"场景成立,会破坏 Identity 不变性。
+ ///
+ public static Quaternion HostQuaternionToCanonical(Quaternion hostQ, HostCoordinateAdapter adapter)
+ {
+ Matrix4x4 hostLinear = Matrix4x4.CreateFromQuaternion(hostQ);
+
+ // M: canonical→host 坐标变换(列 = canonical 基在 host 中的表示)
+ Vector3 mColX = adapter.FromCanonicalVector3(Vector3.UnitX);
+ Vector3 mColY = adapter.FromCanonicalVector3(Vector3.UnitY);
+ Vector3 mColZ = adapter.FromCanonicalVector3(Vector3.UnitZ);
+ Matrix4x4 m = new Matrix4x4(
+ mColX.X, mColY.X, mColZ.X, 0f,
+ mColX.Y, mColY.Y, mColZ.Y, 0f,
+ mColX.Z, mColY.Z, mColZ.Z, 0f,
+ 0f, 0f, 0f, 1f);
+ // M 正交,M⁻¹ = Mᵀ
+ Matrix4x4 mInv = Matrix4x4.Transpose(m);
+ // R_canon = M⁻¹ · R_host · M
+ Matrix4x4 canonLinear = Matrix4x4.Multiply(Matrix4x4.Multiply(mInv, hostLinear), m);
+ return Quaternion.Normalize(Quaternion.CreateFromRotationMatrix(canonLinear));
+ }
}
}
diff --git a/src/Utils/CoordinateSystem/ObjectPassageProjectionOptimizer.cs b/src/Utils/CoordinateSystem/ObjectPassageProjectionOptimizer.cs
index 29acaee..7a8d9b5 100644
--- a/src/Utils/CoordinateSystem/ObjectPassageProjectionOptimizer.cs
+++ b/src/Utils/CoordinateSystem/ObjectPassageProjectionOptimizer.cs
@@ -360,6 +360,15 @@ namespace NavisworksTransport.Utils.CoordinateSystem
}
public static LocalEulerRotationCorrection CanonicalQuaternionToHostEulerCorrection(Quaternion q)
+ {
+ return CanonicalQuaternionToHostEulerCorrection(q, CoordinateSystemManager.Instance.ResolvedType);
+ }
+
+ ///
+ /// 将 canonical 空间的四元数分解为宿主欧拉修正 (X, Y=up, Z=nonUp)。
+ /// 接受显式 hostType 参数,避免依赖全局 CoordinateSystemManager 状态,便于单测。
+ ///
+ public static LocalEulerRotationCorrection CanonicalQuaternionToHostEulerCorrection(Quaternion q, CoordinateSystemType hostType)
{
double qw = q.W, qx = q.X, qy = q.Y, qz = q.Z;
@@ -375,8 +384,6 @@ namespace NavisworksTransport.Utils.CoordinateSystem
double cosy_cosp = 1.0 - 2.0 * (qy * qy + qz * qz);
double yaw_rad = Math.Atan2(siny_cosp, cosy_cosp);
- // Canonical (ZUp) → Host: 使用 CoordinateSystemManager 桥接
- var hostType = CoordinateSystemManager.Instance.ResolvedType;
if (hostType == CoordinateSystemType.ZUp)
{
return new LocalEulerRotationCorrection(