fix: 贴合地面后正确回写 correction 和 lift,确认/动画不再偏移

贴地后 overrideQ 是绝对旋转,但 _objectRotationCorrection 语义是
相对于 baseline(pathYaw) 的增量,且 PathAnimationManager 增量链从
CAD 姿态出发:finalQ = qup(pathYaw-currentYaw) * correctionHostQ * cadQ。
之前直接转 overrideQ 导致确认/动画时重复旋转。

核心改动:
- ComposeHostCorrection 接受 cadQ,从中提取 currentYaw,
  correctionHostQ = qup(currentYaw - pathYaw) * overrideQ
- HostQuaternionToCanonical 用相似变换 R_canon = M^-1 * R_host * M
  (非列向量映射,保证 Identity 不变性)
- CanonicalQuaternionToHostEulerCorrection 加 hostType 参数重载,
  去除全局 CoordinateSystemManager 依赖
- ComputeGroundLiftForCorrection:与自动调整同方法实测底边算 lift,
  finally 用新 correction+lift 完整重建(skipCadRestore=false),
  使物体停留在贴地等价姿态
- 新增 5 个增量链闭环单测(含 CAD 歪斜场景)
This commit is contained in:
tian 2026-06-24 08:24:38 +08:00
parent cf6b2ef959
commit 66e68c6bd1
5 changed files with 379 additions and 4 deletions

View File

@ -219,5 +219,206 @@ namespace NavisworksTransport.UnitTests.CoordinateSystem
// 立方体任何 yaw 面积相同,只要返回有效四元数即可
Assert.AreEqual(1.0, Quaternion.Normalize(q).Length(), 1e-5);
}
// ----- ComposeHostCorrection扣除 CAD yaw + pathYaw避免重复旋转 -----
/// <summary>
/// PathAnimationManager 增量链语义闭环验证:
/// finalQ = qup(pathYaw - currentYaw) * CreateHostRotationCorrection(correction) * cadQ
/// 应等于 overrideQ * cadQ贴地时实际施加的最终姿态
/// cadQ=Identity 时 currentYaw=0简化为
/// qup(pathYaw) * correctionHostQ == overrideQ
/// </summary>
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}");
}
}
}
}

View File

@ -5613,6 +5613,62 @@ namespace NavisworksTransport.Core.Animation
LogManager.Debug($"[起点摆放] 直接设置地面路径上下偏移: {verticalLiftInMeters:F3}m");
}
/// <summary>
/// 给定 correction计算重建后使物体底边贴合目标高度所需的 lift米单位
/// 与自动调整的 ComputedLiftOffsetModelUnits 语义一致:
/// neededLift = targetBottom - objectBottomAfterRebuild
/// 实际应用 correction 重建后测量底边(与自动调整同方法)。
/// 测量后用新 correction + 新 lift 重建,使物体停留在正确的贴地等价姿态
/// (与确认按钮后的姿态一致),而不是恢复到贴地前的旧姿态。
/// </summary>
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=falseskipCadRestore=true 会清零 correction
// 只施加 yaw 增量而不施加 X/Z 旋转,导致姿态缺失俯仰/翻滚。
var applyRequest = ObjectStartPlacementRequest.CreateRotationCorrection(
correction, computedLiftMeters);
_objectStartPlacementMode = savedMode;
ApplyObjectStartPlacementRequest(applyRequest);
}
return computedLiftMeters;
}
public void SetObjectStartPlacementMode(ObjectStartPlacementMode placementMode)
{
_objectStartPlacementMode = placementMode;

View File

@ -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}) " +

View File

@ -127,5 +127,83 @@ namespace NavisworksTransport.Utils.CoordinateSystem
return width * height;
}
/// <summary>
/// 把贴地搜索后的绝对宿主姿态(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);
}
/// <summary>
/// 从宿主空间四元数提取 canonical yawAtan2(canonicalForward.Y, canonicalForward.X))。
/// 与 ModelItemTransformHelper.GetYawFromRotation 一致:取 forward 轴(X列),转 canonical投影 XY 平面。
/// </summary>
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);
}
/// <summary>
/// 宿主空间四元数 → canonical 空间四元数。
/// 旋转的坐标系转换必须用相似变换 R_canon = M⁻¹ · R_host · M
/// M = canonical→host 坐标变换矩阵,正交故 M⁻¹ = Mᵀ
/// 不能用列向量映射——那只在"基向量定义"场景成立,会破坏 Identity 不变性。
/// </summary>
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));
}
}
}

View File

@ -360,6 +360,15 @@ namespace NavisworksTransport.Utils.CoordinateSystem
}
public static LocalEulerRotationCorrection CanonicalQuaternionToHostEulerCorrection(Quaternion q)
{
return CanonicalQuaternionToHostEulerCorrection(q, CoordinateSystemManager.Instance.ResolvedType);
}
/// <summary>
/// 将 canonical 空间的四元数分解为宿主欧拉修正 (X, Y=up, Z=nonUp)。
/// 接受显式 hostType 参数,避免依赖全局 CoordinateSystemManager 状态,便于单测。
/// </summary>
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(