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] 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 旋转集合中提取代表旋转。 ///