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(