diff --git a/NavisworksTransport.UnitTests.csproj b/NavisworksTransport.UnitTests.csproj index b109f54..17555ba 100644 --- a/NavisworksTransport.UnitTests.csproj +++ b/NavisworksTransport.UnitTests.csproj @@ -72,6 +72,8 @@ + + diff --git a/Properties/AssemblyInfo.cs b/Properties/AssemblyInfo.cs index 1020f6e..0edda3e 100644 --- a/Properties/AssemblyInfo.cs +++ b/Properties/AssemblyInfo.cs @@ -31,4 +31,5 @@ using System.Runtime.InteropServices; // // 主版本和次版本手动维护,Build和Revision在编译时自动更新 [assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] \ No newline at end of file +[assembly: AssemblyFileVersion("1.0.0.0")] +[assembly: InternalsVisibleTo("NavisworksTransport.UnitTests")] diff --git a/TransportPlugin.csproj b/TransportPlugin.csproj index f126d00..63a7404 100644 --- a/TransportPlugin.csproj +++ b/TransportPlugin.csproj @@ -522,6 +522,11 @@ PreserveNewest resources\unit_cube.nwc + + + PreserveNewest + resources\unit_cube_yup.nwc + PreserveNewest diff --git a/UnitTests/CoordinateSystem/FragmentDefaultUpContextTests.cs b/UnitTests/CoordinateSystem/FragmentDefaultUpContextTests.cs new file mode 100644 index 0000000..17b135b --- /dev/null +++ b/UnitTests/CoordinateSystem/FragmentDefaultUpContextTests.cs @@ -0,0 +1,47 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using NavisworksTransport.Utils.CoordinateSystem; + +namespace NavisworksTransport.UnitTests.CoordinateSystem +{ + [TestClass] + public class FragmentDefaultUpContextTests + { + [TestInitialize] + public void SetUp() + { + FragmentDefaultUpContext.ResetForTests(); + } + + [TestMethod] + public void GetOrCreateDocumentKey_ReusesFirstStableKey_WhenDocumentMetadataTemporarilyChanges() + { + const int runtimeDocumentId = 42; + + string initialKey = FragmentDefaultUpContext.GetOrCreateDocumentKey( + runtimeDocumentId, + @"C:\models\floor2.nwd", + "Floor2"); + + string transientKey = FragmentDefaultUpContext.GetOrCreateDocumentKey( + runtimeDocumentId, + string.Empty, + "无标题"); + + Assert.AreEqual(initialKey, transientKey); + Assert.AreEqual("file:C:\\models\\floor2.nwd", transientKey); + } + + [TestMethod] + public void GetOrCreateDocumentKey_UsesRuntimeScopedFallback_WhenFileNameIsUnavailable() + { + const int runtimeDocumentId = 7; + + string key = FragmentDefaultUpContext.GetOrCreateDocumentKey( + runtimeDocumentId, + string.Empty, + "无标题"); + + Assert.AreEqual("runtime:7|title:无标题", key); + } + } +} diff --git a/UnitTests/CoordinateSystem/VirtualGroundPoseCharacterizationTests.cs b/UnitTests/CoordinateSystem/VirtualGroundPoseCharacterizationTests.cs new file mode 100644 index 0000000..f94d79a --- /dev/null +++ b/UnitTests/CoordinateSystem/VirtualGroundPoseCharacterizationTests.cs @@ -0,0 +1,120 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using NavisworksTransport.Utils.CoordinateSystem; +using System; +using System.Numerics; +using Autodesk.Navisworks.Api; + +namespace NavisworksTransport.UnitTests.CoordinateSystem +{ + [TestClass] + public class VirtualGroundPoseCharacterizationTests + { + [TestMethod] + public void YUp_GroundVirtualPose_WithYUpVirtualAsset_ShouldUseHostYAsUp() + { + var adapter = new HostCoordinateAdapter(CoordinateSystemType.YUp); + var convention = ModelAxisConvention.CreateDefaultForHost(CoordinateSystemType.YUp); + + Vector3 hostForward = Vector3.Normalize(new Vector3(-0.8987f, 0.0f, 0.4386f)); + Vector3 canonicalForward = adapter.ToCanonicalVector3(hostForward); + + bool ok = CanonicalPlanarPoseBuilder.TryCreateQuaternionFromForward( + canonicalForward, + HostCoordinateAdapter.CanonicalUpVector3, + convention, + out Quaternion canonicalRotation); + + Assert.IsTrue(ok); + + Matrix4x4 hostLinear = adapter.FromCanonicalLinearTransform( + Matrix4x4.CreateFromQuaternion(canonicalRotation)); + + AssertColumn(hostLinear, 0, -0.8987, 0.0000, 0.4386); + AssertColumn(hostLinear, 1, 0.0000, 1.0000, 0.0000); + AssertColumn(hostLinear, 2, -0.4386, 0.0000, -0.8987); + } + + [TestMethod] + public void YUp_GroundVirtualPose_WithYUpVirtualAsset_ShouldMatchHostYUpDefaultPlanarConvention() + { + var adapter = new HostCoordinateAdapter(CoordinateSystemType.YUp); + Vector3 hostForward = Vector3.Normalize(new Vector3(-0.8987f, 0.0f, 0.4386f)); + Vector3 canonicalForward = adapter.ToCanonicalVector3(hostForward); + + Assert.IsTrue(CanonicalPlanarPoseBuilder.TryCreateQuaternionFromForward( + canonicalForward, + HostCoordinateAdapter.CanonicalUpVector3, + ModelAxisConvention.CreateDefaultForHost(CoordinateSystemType.YUp), + out Quaternion assetRotation)); + + Assert.IsTrue(CanonicalPlanarPoseBuilder.TryCreateQuaternionFromForward( + canonicalForward, + HostCoordinateAdapter.CanonicalUpVector3, + ModelAxisConvention.CreateDefaultForHost(CoordinateSystemType.YUp), + out Quaternion hostDefaultRotation)); + + Matrix4x4 assetHostLinear = adapter.FromCanonicalLinearTransform( + Matrix4x4.CreateFromQuaternion(assetRotation)); + Matrix4x4 hostDefaultLinear = adapter.FromCanonicalLinearTransform( + Matrix4x4.CreateFromQuaternion(hostDefaultRotation)); + + Assert.AreEqual(1.0, assetHostLinear.M22, 1e-3, "YUp 虚拟物体资源应把 local Y 对到宿主 up。"); + Assert.AreEqual(hostDefaultLinear.M11, assetHostLinear.M11, 1e-3); + Assert.AreEqual(hostDefaultLinear.M21, assetHostLinear.M21, 1e-3); + Assert.AreEqual(hostDefaultLinear.M31, assetHostLinear.M31, 1e-3); + Assert.AreEqual(hostDefaultLinear.M12, assetHostLinear.M12, 1e-3); + Assert.AreEqual(hostDefaultLinear.M22, assetHostLinear.M22, 1e-3); + Assert.AreEqual(hostDefaultLinear.M32, assetHostLinear.M32, 1e-3); + Assert.AreEqual(hostDefaultLinear.M13, assetHostLinear.M13, 1e-3); + Assert.AreEqual(hostDefaultLinear.M23, assetHostLinear.M23, 1e-3); + Assert.AreEqual(hostDefaultLinear.M33, assetHostLinear.M33, 1e-3); + } + + [TestMethod] + public void VirtualObjectReferencePose_ShouldMatchSelectedVirtualAssetConvention() + { + var yUpAdapter = new HostCoordinateAdapter(CoordinateSystemType.YUp); + var zUpAdapter = new HostCoordinateAdapter(CoordinateSystemType.ZUp); + var yUpConvention = ModelAxisConvention.CreateDefaultForHost(CoordinateSystemType.YUp); + var zUpConvention = ModelAxisConvention.CreateVirtualObjectAssetConvention(); + + Matrix4x4 yUpLinear = Matrix4x4.CreateFromQuaternion( + yUpConvention.CreateQuaternion(new Vector3(1, 0, 0), yUpAdapter.HostUpVector3)); + Matrix4x4 zUpLinear = Matrix4x4.CreateFromQuaternion( + zUpConvention.CreateQuaternion(new Vector3(1, 0, 0), zUpAdapter.HostUpVector3)); + + AssertColumn(yUpLinear, 0, 1.0, 0.0, 0.0); + AssertColumn(yUpLinear, 1, 0.0, 1.0, 0.0); + AssertColumn(yUpLinear, 2, 0.0, 0.0, 1.0); + + AssertColumn(zUpLinear, 0, 1.0, 0.0, 0.0); + AssertColumn(zUpLinear, 1, 0.0, 1.0, 0.0); + AssertColumn(zUpLinear, 2, 0.0, 0.0, 1.0); + } + + private static void AssertColumn(Matrix4x4 matrix, int column, double x, double y, double z) + { + switch (column) + { + case 0: + Assert.AreEqual(x, matrix.M11, 1e-3); + Assert.AreEqual(y, matrix.M21, 1e-3); + Assert.AreEqual(z, matrix.M31, 1e-3); + break; + case 1: + Assert.AreEqual(x, matrix.M12, 1e-3); + Assert.AreEqual(y, matrix.M22, 1e-3); + Assert.AreEqual(z, matrix.M32, 1e-3); + break; + case 2: + Assert.AreEqual(x, matrix.M13, 1e-3); + Assert.AreEqual(y, matrix.M23, 1e-3); + Assert.AreEqual(z, matrix.M33, 1e-3); + break; + default: + Assert.Fail("Only first 3 columns are valid."); + break; + } + } + } +} diff --git a/UnitTests/CoordinateSystem/VirtualObjectModelTransformTests.cs b/UnitTests/CoordinateSystem/VirtualObjectModelTransformTests.cs new file mode 100644 index 0000000..e571818 --- /dev/null +++ b/UnitTests/CoordinateSystem/VirtualObjectModelTransformTests.cs @@ -0,0 +1,100 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using NavisworksTransport.Core; + +namespace NavisworksTransport.UnitTests.CoordinateSystem +{ + [TestClass] + public class VirtualObjectModelTransformTests + { + [TestMethod] + public void BuildModelTransformPreservingScale_ShouldKeepExistingScaleAndReplaceRotationTranslation() + { + const double scaleX = 4.9213; + const double scaleY = 3.2808; + const double scaleZ = 6.5617; + var targetRotation = new QuaternionData(0.0, 0.7071068, 0.0, 0.7071068); + + VirtualObjectManager.ModelTransformComponents result = VirtualObjectManager.CreateModelTransformPreservingScale( + scaleX, + scaleY, + scaleZ, + -177.129, + 18.114, + -2.793, + targetRotation.ToRotation3D()); + + Assert.AreEqual(scaleX, result.ScaleX, 1e-4); + Assert.AreEqual(scaleY, result.ScaleY, 1e-4); + Assert.AreEqual(scaleZ, result.ScaleZ, 1e-4); + AssertRotationEquivalent(targetRotation, result.Rotation); + Assert.AreEqual(-177.129, result.TranslationX, 1e-6); + Assert.AreEqual(18.114, result.TranslationY, 1e-6); + Assert.AreEqual(-2.793, result.TranslationZ, 1e-6); + } + + [TestMethod] + public void BuildModelTransformPreservingScale_ResetPose_ShouldOnlyClearRotationAndTranslation() + { + VirtualObjectManager.ModelTransformComponents result = VirtualObjectManager.CreateModelTransformPreservingScale( + 12.0, + 5.0, + 7.0, + 0.0, + 0.0, + 0.0, + QuaternionData.Identity.ToRotation3D()); + + Assert.AreEqual(12.0, result.ScaleX, 1e-6); + Assert.AreEqual(5.0, result.ScaleY, 1e-6); + Assert.AreEqual(7.0, result.ScaleZ, 1e-6); + AssertRotationEquivalent(QuaternionData.Identity, result.Rotation); + Assert.AreEqual(0.0, result.TranslationX, 1e-6); + Assert.AreEqual(0.0, result.TranslationY, 1e-6); + Assert.AreEqual(0.0, result.TranslationZ, 1e-6); + } + + private static void AssertRotationEquivalent(QuaternionData expected, Autodesk.Navisworks.Api.Rotation3D actual) + { + bool same = + NearlyEqual(expected.X, actual.A) && + NearlyEqual(expected.Y, actual.B) && + NearlyEqual(expected.Z, actual.C) && + NearlyEqual(expected.W, actual.D); + bool negatedSame = + NearlyEqual(expected.X, -actual.A) && + NearlyEqual(expected.Y, -actual.B) && + NearlyEqual(expected.Z, -actual.C) && + NearlyEqual(expected.W, -actual.D); + + Assert.IsTrue(same || negatedSame, "Rotation3D quaternion should match target rotation up to sign."); + } + + private static bool NearlyEqual(double left, double right) + { + return System.Math.Abs(left - right) < 1e-6; + } + + private struct QuaternionData + { + public QuaternionData(double x, double y, double z, double w) + { + X = x; + Y = y; + Z = z; + W = w; + } + + public double X { get; } + public double Y { get; } + public double Z { get; } + public double W { get; } + + public Autodesk.Navisworks.Api.Rotation3D ToRotation3D() + { + return new Autodesk.Navisworks.Api.Rotation3D(X, Y, Z, W); + } + + public static QuaternionData Identity => new QuaternionData(0, 0, 0, 1); + } + } +} diff --git a/resources/unit_cube_yup.nwc b/resources/unit_cube_yup.nwc new file mode 100644 index 0000000..47289cd Binary files /dev/null and b/resources/unit_cube_yup.nwc differ diff --git a/resources/unit_cube_yup.obj b/resources/unit_cube_yup.obj new file mode 100644 index 0000000..6829a52 --- /dev/null +++ b/resources/unit_cube_yup.obj @@ -0,0 +1,26 @@ +# unit cube for virtual object (Y-up) +# dimensions: 1.0 x 1.0 x 1.0 +# centered at origin +# local +X = forward, local +Y = up, local +Z = side +# when Navisworks imports OBJ as centimeters, generated NWC becomes 0.01m base size +o unit_cube_yup +v -0.500000 -0.500000 -0.500000 +v 0.500000 -0.500000 -0.500000 +v 0.500000 0.500000 -0.500000 +v -0.500000 0.500000 -0.500000 +v -0.500000 -0.500000 0.500000 +v 0.500000 -0.500000 0.500000 +v 0.500000 0.500000 0.500000 +v -0.500000 0.500000 0.500000 +f 1 2 3 +f 1 3 4 +f 5 8 7 +f 5 7 6 +f 1 5 6 +f 1 6 2 +f 2 6 7 +f 2 7 3 +f 3 7 8 +f 3 8 4 +f 4 8 5 +f 4 5 1 diff --git a/src/Core/Animation/PathAnimationManager.cs b/src/Core/Animation/PathAnimationManager.cs index da2e878..29a3e05 100644 --- a/src/Core/Animation/PathAnimationManager.cs +++ b/src/Core/Animation/PathAnimationManager.cs @@ -449,7 +449,16 @@ namespace NavisworksTransport.Core.Animation // 2. 重置内部跟踪变量(同步到原始状态) // 注意:ResetPermanentTransform 后物体的 Transform 属性会自动变回原始值 - SyncTrackedRotationToObjectReference(objectToRestore, isVirtual); + if (isVirtual) + { + _currentYaw = ModelItemTransformHelper.GetYawFromTransform(objectToRestore.Transform); + _trackedRotation = objectToRestore.Transform.Factor().Rotation; + _hasTrackedRotation = true; + } + else + { + SyncTrackedRotationToObjectReference(objectToRestore, isVirtualObject: false); + } var originalBoundingBox = objectToRestore.BoundingBox(); _trackedPosition = GetTrackedObjectPosition(objectToRestore); @@ -652,7 +661,16 @@ namespace NavisworksTransport.Core.Animation _originalTransform = animatedObject.Transform; _originalCenter = animatedObject.BoundingBox().Center; _trackedPosition = GetTrackedObjectPosition(animatedObject); - SyncTrackedRotationToObjectReference(animatedObject, isVirtualObject); + if (isVirtualObject) + { + _currentYaw = ModelItemTransformHelper.GetYawFromTransform(_originalTransform); + _trackedRotation = _originalTransform.Factor().Rotation; + _hasTrackedRotation = true; + } + else + { + SyncTrackedRotationToObjectReference(animatedObject, isVirtualObject: false); + } LogManager.Info( $"[移动到起点] 更新动画对象内部状态: 对象={animatedObject.DisplayName}, 虚拟物体={IsVirtualObjectMode}"); @@ -2452,75 +2470,89 @@ namespace NavisworksTransport.Core.Animation { try { - bool isRailRealObject = _route?.PathType == PathType.Rail && IsRealObjectMode && _animatedObject != null; - if (IsVirtualObjectMode) - { - VirtualObjectManager.Instance.MoveVirtualObject(newPosition, newRotation); - } - 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})"); - } - catch (Exception ex) - { - LogManager.Warning($"[动画姿态入口] 读取宿主实际状态失败: {ex.Message}"); - } - var currentLinear = new Transform3D(currentRotation).Linear; - var targetLinear = new Transform3D(newRotation).Linear; - 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=({targetLinear.Get(0, 0):F4},{targetLinear.Get(1, 0):F4},{targetLinear.Get(2, 0):F4}), " + - $"Y=({targetLinear.Get(0, 1):F4},{targetLinear.Get(1, 1):F4},{targetLinear.Get(2, 1):F4}), " + - $"Z=({targetLinear.Get(0, 2):F4},{targetLinear.Get(1, 2):F4},{targetLinear.Get(2, 2):F4})"); - ModelItemTransformHelper.MoveItemIncrementallyToPositionAndRotation( - _animatedObject, - currentPositionForTransform, - currentRotation, - newPosition, - newRotation); - } - else + ModelItem controlledObject = CurrentControlledObject; + bool isRailRealObject = _route?.PathType == PathType.Rail && IsRealObjectMode && controlledObject != null; + if (controlledObject == null) { return; } + if (IsVirtualObjectMode) + { + VirtualObjectManager.Instance.MoveVirtualObject(newPosition, newRotation); + _trackedPosition = newPosition; + _trackedRotation = newRotation; + _hasTrackedRotation = true; + _currentYaw = ModelItemTransformHelper.GetYawFromRotation(newRotation); + + if (_route?.PathType == PathType.Ground || _route?.PathType == PathType.Hoisting) + { + LogHostActualPoseAxes(CurrentControlledObject, "[平面姿态应用后宿主姿态]", false); + } + + return; + } + + Point3D currentPositionForTransform = _trackedPosition; + Rotation3D currentRotation; + Rotation3D appliedTargetRotation = newRotation; + if (_hasTrackedRotation) + { + currentRotation = _trackedRotation; + } + else if (IsRealObjectMode && _hasRealObjectReferenceRotation) + { + currentRotation = CoordinateSystemManager.Instance + .CreateHostAdapter() + .FromHostQuaternionDirect(_realObjectReferenceRotation); + } + else + { + currentRotation = controlledObject.Transform.Factor().Rotation; + } + + try + { + var actualHostPosition = GetTrackedObjectPosition(controlledObject); + currentPositionForTransform = actualHostPosition; + LogManager.Info( + $"[动画姿态入口] {controlledObject.DisplayName} 宿主即时读回点=({actualHostPosition.X:F3},{actualHostPosition.Y:F3},{actualHostPosition.Z:F3})"); + } + catch (Exception ex) + { + LogManager.Warning($"[动画姿态入口] 读取宿主实际状态失败: {ex.Message}"); + } + + var currentLinear = new Transform3D(currentRotation).Linear; + var targetLinear = new Transform3D(appliedTargetRotation).Linear; + LogManager.Info( + $"[动画姿态入口] {controlledObject.DisplayName} 跟踪点=({_trackedPosition.X:F3},{_trackedPosition.Y:F3},{_trackedPosition.Z:F3}), " + + $"目标点=({newPosition.X:F3},{newPosition.Y:F3},{newPosition.Z:F3})"); + LogManager.Info( + $"[动画姿态入口] {controlledObject.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( + $"[动画姿态入口] {controlledObject.DisplayName} 目标姿态: " + + $"X=({targetLinear.Get(0, 0):F4},{targetLinear.Get(1, 0):F4},{targetLinear.Get(2, 0):F4}), " + + $"Y=({targetLinear.Get(0, 1):F4},{targetLinear.Get(1, 1):F4},{targetLinear.Get(2, 1):F4}), " + + $"Z=({targetLinear.Get(0, 2):F4},{targetLinear.Get(1, 2):F4},{targetLinear.Get(2, 2):F4})"); + ModelItemTransformHelper.MoveItemIncrementallyToPositionAndRotation( + controlledObject, + currentPositionForTransform, + currentRotation, + newPosition, + appliedTargetRotation); + _trackedPosition = newPosition; - _trackedRotation = newRotation; + _trackedRotation = appliedTargetRotation; _hasTrackedRotation = true; - _currentYaw = ModelItemTransformHelper.GetYawFromRotation(newRotation); + _currentYaw = ModelItemTransformHelper.GetYawFromRotation(appliedTargetRotation); if (isRailRealObject) { - Point3D hostActualAfter = GetTrackedObjectPosition(_animatedObject); + Point3D hostActualAfter = GetTrackedObjectPosition(controlledObject); LogManager.Debug( $"[Rail姿态应用] 宿主最终跟踪中心=({hostActualAfter.X:F3},{hostActualAfter.Y:F3},{hostActualAfter.Z:F3}), " + $"目标跟踪中心=({newPosition.X:F3},{newPosition.Y:F3},{newPosition.Z:F3}), " + @@ -3782,7 +3814,7 @@ namespace NavisworksTransport.Core.Animation Quaternion composedHostQuaternion = adapter.ComposeHostQuaternion( baselineHostQuaternion, _objectRotationCorrection); - rotation = adapter.FromHostQuaternion(composedHostQuaternion); + rotation = adapter.FromHostQuaternionDirect(composedHostQuaternion); Matrix4x4 baselineLinear = Matrix4x4.CreateFromQuaternion(baselineHostQuaternion); Matrix4x4 composedLinear = Matrix4x4.CreateFromQuaternion(composedHostQuaternion); @@ -3958,6 +3990,7 @@ namespace NavisworksTransport.Core.Animation return true; } + private bool TryCreateRealObjectPlanarRotationFromHostForward(Vector3 hostForward, out Rotation3D rotation) { rotation = Rotation3D.Identity; @@ -3980,7 +4013,7 @@ namespace NavisworksTransport.Core.Animation var adapter = CoordinateSystemManager.Instance.CreateHostAdapter(); Quaternion hostComposedQuaternion = adapter.ComposeHostQuaternion(solution.BaselineRotation, _objectRotationCorrection); - rotation = adapter.FromHostQuaternion(hostComposedQuaternion); + rotation = adapter.FromHostQuaternionDirect(hostComposedQuaternion); Matrix4x4 baselineLinear = Matrix4x4.CreateFromQuaternion(solution.BaselineRotation); Matrix4x4 hostComposedLinear = Matrix4x4.CreateFromQuaternion(hostComposedQuaternion); @@ -4192,10 +4225,25 @@ namespace NavisworksTransport.Core.Animation return; } + if (isVirtualObject) + { + _trackedRotation = CreateVirtualObjectReferenceRotation(); + _hasTrackedRotation = true; + _currentYaw = ModelItemTransformHelper.GetYawFromRotation(_trackedRotation); + + var linear = new Transform3D(_trackedRotation).Linear; + LogManager.Info( + $"[虚拟物体参考姿态] {sourceObject.DisplayName} 使用资产约定基姿态: " + + $"X=({linear.Get(0, 0):F4},{linear.Get(1, 0):F4},{linear.Get(2, 0):F4}), " + + $"Y=({linear.Get(0, 1):F4},{linear.Get(1, 1):F4},{linear.Get(2, 1):F4}), " + + $"Z=({linear.Get(0, 2):F4},{linear.Get(1, 2):F4},{linear.Get(2, 2):F4})"); + return; + } + if (!isVirtualObject && TryCaptureRealObjectReferenceRotation(sourceObject, out Quaternion referenceRotation)) { var adapter = CoordinateSystemManager.Instance.CreateHostAdapter(); - _trackedRotation = adapter.FromHostQuaternion(referenceRotation); + _trackedRotation = adapter.FromHostQuaternionDirect(referenceRotation); _hasTrackedRotation = true; _currentYaw = ModelItemTransformHelper.GetYawFromRotation(_trackedRotation); return; @@ -4206,6 +4254,18 @@ namespace NavisworksTransport.Core.Animation _hasTrackedRotation = true; } + private static Rotation3D CreateVirtualObjectReferenceRotation() + { + var adapter = CoordinateSystemManager.Instance.CreateHostAdapter(); + var convention = GetVirtualObjectAssetConvention(); + return convention.CreateRotation(new Vector3D(1, 0, 0), adapter.HostUpVector); + } + + private static ModelAxisConvention GetVirtualObjectAssetConvention() + { + return ModelAxisConvention.CreateVirtualObjectAssetConvention(); + } + private void ResetRealObjectReferenceRotation() { _realObjectReferenceRotation = Quaternion.Identity; diff --git a/src/Core/VirtualObjectManager.cs b/src/Core/VirtualObjectManager.cs index 311d93b..dc9ae85 100644 --- a/src/Core/VirtualObjectManager.cs +++ b/src/Core/VirtualObjectManager.cs @@ -94,7 +94,6 @@ namespace NavisworksTransport.Core var doc = Application.ActiveDocument; var unitCubePath = GetUnitCubeFilePath(); - if (string.IsNullOrEmpty(unitCubePath) || !File.Exists(unitCubePath)) { throw new FileNotFoundException($"找不到单位立方体文件: {unitCubePath}"); @@ -213,9 +212,7 @@ namespace NavisworksTransport.Core try { LogVirtualObjectGeometry("[虚拟物体姿态] 应用前"); - // 虚拟物体当前尺寸是通过永久变换中的缩放实现的。 - // 应用完整姿态时必须保留当前缩放,否则会破坏虚拟物体的资产姿态和几何语义。 - ModelItemTransformHelper.MoveItemToPositionAndRotationWithCurrentScale(_virtualObjectModelItem, position, rotation); + ModelItemTransformHelper.MoveItemToPositionAndRotation(_virtualObjectModelItem, position, rotation); var actualBounds = _virtualObjectModelItem.BoundingBox(); Point3D actualCenter = actualBounds?.Center ?? new Point3D(0, 0, 0); LogManager.Info( @@ -239,20 +236,15 @@ namespace NavisworksTransport.Core { var doc = Application.ActiveDocument; var modelItems = new ModelItemCollection { _virtualObjectModelItem }; - - // 获取当前缩放 var currentTransform = _virtualObjectModelItem.Transform; var currentComponents = currentTransform.Factor(); var currentScale = currentComponents.Scale; - // 重置到CAD原始状态 doc.Models.ResetPermanentTransform(modelItems); - // 重新应用缩放 var identity = Transform3D.CreateTranslation(new Vector3D(0, 0, 0)); var components = identity.Factor(); components.Scale = currentScale; - Transform3D newTransform = components.Combine(); doc.Models.OverridePermanentTransform(modelItems, newTransform, false); @@ -280,18 +272,15 @@ namespace NavisworksTransport.Core _currentWidthMeters = widthMeters; _currentHeightMeters = heightMeters; - // 获取当前位置和旋转 + // 应用新缩放 + ScaleVirtualObject(lengthMeters, widthMeters, heightMeters); + var currentTransform = _virtualObjectModelItem.Transform; var currentComponents = currentTransform.Factor(); var currentTranslation = currentComponents.Translation; var currentRotation = currentComponents.Rotation; - // 应用新缩放 - ScaleVirtualObject(lengthMeters, widthMeters, heightMeters); - - // 如果之前有位置/旋转,重新应用 bool hasTranslation = currentTranslation.X != 0 || currentTranslation.Y != 0 || currentTranslation.Z != 0; - // 使用 ToAxisAndAngle 检查是否有旋转 var rotationResult = currentRotation.ToAxisAndAngle(); bool hasRotation = Math.Abs(rotationResult.Angle) > 0.001; if (hasTranslation || hasRotation) @@ -300,7 +289,7 @@ namespace NavisworksTransport.Core var newComponents = newTransform.Factor(); newComponents.Translation = currentTranslation; newComponents.Rotation = currentRotation; - + Transform3D combinedTransform = newComponents.Combine(); var doc = Application.ActiveDocument; var modelItems = new ModelItemCollection { _virtualObjectModelItem }; @@ -332,7 +321,6 @@ namespace NavisworksTransport.Core lengthMeters / baseSizeMeters, widthMeters / baseSizeMeters, heightMeters / baseSizeMeters); - Transform3D newTransform = transformComponents.Combine(); doc.Models.SetModelUnitsAndTransform(_virtualObjectModel, _virtualObjectModel.Units, newTransform, false); diff --git a/src/Utils/CoordinateSystem/FragmentDefaultUpContext.cs b/src/Utils/CoordinateSystem/FragmentDefaultUpContext.cs index 32ab907..c7eadc4 100644 --- a/src/Utils/CoordinateSystem/FragmentDefaultUpContext.cs +++ b/src/Utils/CoordinateSystem/FragmentDefaultUpContext.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Concurrent; +using System.Runtime.CompilerServices; using Autodesk.Navisworks.Api; namespace NavisworksTransport.Utils.CoordinateSystem @@ -13,6 +14,9 @@ namespace NavisworksTransport.Utils.CoordinateSystem private static readonly ConcurrentDictionary DocumentFragmentDefaultUpAxes = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); + private static readonly ConcurrentDictionary RuntimeDocumentKeys = + new ConcurrentDictionary(); + public static string GetCurrentDocumentKey(Document doc = null) { doc = doc ?? Autodesk.Navisworks.Api.Application.ActiveDocument; @@ -21,12 +25,39 @@ namespace NavisworksTransport.Utils.CoordinateSystem return string.Empty; } - if (!string.IsNullOrWhiteSpace(doc.FileName)) + int runtimeDocumentId = RuntimeHelpers.GetHashCode(doc); + return GetOrCreateDocumentKey(runtimeDocumentId, doc.FileName, doc.Title); + } + + internal static string GetOrCreateDocumentKey(int runtimeDocumentId, string fileName, string title) + { + if (runtimeDocumentId <= 0) { - return doc.FileName; + return string.Empty; } - return doc.Title ?? string.Empty; + if (RuntimeDocumentKeys.TryGetValue(runtimeDocumentId, out string existingKey) && + !string.IsNullOrWhiteSpace(existingKey)) + { + return existingKey; + } + + string resolvedKey; + if (!string.IsNullOrWhiteSpace(fileName)) + { + resolvedKey = $"file:{fileName}"; + } + else if (!string.IsNullOrWhiteSpace(title)) + { + resolvedKey = $"runtime:{runtimeDocumentId}|title:{title}"; + } + else + { + resolvedKey = $"runtime:{runtimeDocumentId}"; + } + + RuntimeDocumentKeys[runtimeDocumentId] = resolvedKey; + return resolvedKey; } public static string GetCurrentHostUpAxis(Document doc = null) @@ -86,5 +117,11 @@ namespace NavisworksTransport.Utils.CoordinateSystem return string.Equals(axisName, "Y", StringComparison.OrdinalIgnoreCase) || string.Equals(axisName, "Z", StringComparison.OrdinalIgnoreCase); } + + internal static void ResetForTests() + { + DocumentFragmentDefaultUpAxes.Clear(); + RuntimeDocumentKeys.Clear(); + } } } diff --git a/src/Utils/CoordinateSystem/HostCoordinateAdapter.cs b/src/Utils/CoordinateSystem/HostCoordinateAdapter.cs index ef2bf29..f95f366 100644 --- a/src/Utils/CoordinateSystem/HostCoordinateAdapter.cs +++ b/src/Utils/CoordinateSystem/HostCoordinateAdapter.cs @@ -179,6 +179,21 @@ namespace NavisworksTransport.Utils.CoordinateSystem } public Rotation3D FromCanonicalRotation(Quaternion canonicalRotation) + { + return FromCanonicalRotationLegacy(canonicalRotation); + } + + public Rotation3D FromCanonicalRotationLegacy(Quaternion canonicalRotation) + { + Matrix4x4 canonicalLinear = Matrix4x4.CreateFromQuaternion(canonicalRotation); + Matrix4x4 hostLinear = FromCanonicalLinearTransform(canonicalLinear); + return CreateRotationFromLinearComponents( + hostLinear.M11, hostLinear.M12, hostLinear.M13, + hostLinear.M21, hostLinear.M22, hostLinear.M23, + hostLinear.M31, hostLinear.M32, hostLinear.M33); + } + + public Rotation3D FromCanonicalRotationDirect(Quaternion canonicalRotation) { Matrix4x4 canonicalLinear = Matrix4x4.CreateFromQuaternion(canonicalRotation); Matrix4x4 hostLinear = FromCanonicalLinearTransform(canonicalLinear); @@ -196,6 +211,20 @@ namespace NavisworksTransport.Utils.CoordinateSystem /// 这里不做任何轴交换、符号翻转或重新解释,只负责把已经确定好的宿主四元数写入 Navisworks。 /// public Rotation3D FromHostQuaternion(Quaternion hostRotation) + { + return FromHostQuaternionLegacy(hostRotation); + } + + public Rotation3D FromHostQuaternionLegacy(Quaternion hostRotation) + { + Matrix4x4 hostLinear = Matrix4x4.CreateFromQuaternion(hostRotation); + return CreateRotationFromLinearComponents( + hostLinear.M11, hostLinear.M12, hostLinear.M13, + hostLinear.M21, hostLinear.M22, hostLinear.M23, + hostLinear.M31, hostLinear.M32, hostLinear.M33); + } + + public Rotation3D FromHostQuaternionDirect(Quaternion hostRotation) { Quaternion normalizedHostRotation = Quaternion.Normalize(hostRotation); return new Rotation3D( @@ -276,6 +305,53 @@ namespace NavisworksTransport.Utils.CoordinateSystem return Quaternion.Normalize(qz * qy * qx); } + private static Rotation3D CreateRotationFromLinearComponents( + double m00, double m01, double m02, + double m10, double m11, double m12, + double m20, double m21, double m22) + { + double qx; + double qy; + double qz; + double qw; + + double trace = m00 + m11 + m22; + if (trace > 0.0) + { + double s = Math.Sqrt(trace + 1.0) * 2.0; + qw = 0.25 * s; + qx = (m21 - m12) / s; + qy = (m02 - m20) / s; + qz = (m10 - m01) / s; + } + else if (m00 > m11 && m00 > m22) + { + double s = Math.Sqrt(1.0 + m00 - m11 - m22) * 2.0; + qw = (m21 - m12) / s; + qx = 0.25 * s; + qy = (m01 + m10) / s; + qz = (m02 + m20) / s; + } + else if (m11 > m22) + { + double s = Math.Sqrt(1.0 + m11 - m00 - m22) * 2.0; + qw = (m02 - m20) / s; + qx = (m01 + m10) / s; + qy = 0.25 * s; + qz = (m12 + m21) / s; + } + else + { + double s = Math.Sqrt(1.0 + m22 - m00 - m11) * 2.0; + qw = (m10 - m01) / s; + qx = (m02 + m20) / s; + qy = (m12 + m21) / s; + qz = 0.25 * s; + } + + return new Rotation3D(qx, qy, qz, qw); + } + public CanonicalBounds3 ToCanonicalBounds3(CanonicalBounds3 hostBounds) { return RebuildBounds3(hostBounds, ToCanonicalPoint3); diff --git a/src/Utils/ModelItemTransformHelper.cs b/src/Utils/ModelItemTransformHelper.cs index d76da64..58460e5 100644 --- a/src/Utils/ModelItemTransformHelper.cs +++ b/src/Utils/ModelItemTransformHelper.cs @@ -589,6 +589,41 @@ namespace NavisworksTransport.Utils LogIncrementalTransformActual(item, targetPosition, targetRotation); } + /// + /// 读取物体当前实际几何姿态。 + /// 优先使用 ModelGeometry.ActiveTransform,因为 ModelItem.Transform 只反映原始设计变换。 + /// + public static bool TryGetCurrentGeometryRotation(ModelItem item, out Rotation3D rotation) + { + rotation = Rotation3D.Identity; + if (item == null) + { + return false; + } + + try + { + ModelGeometry geometry = item.FindFirstGeometry() ?? item.Geometry; + if (geometry?.ActiveTransform == null) + { + return false; + } + + rotation = geometry.ActiveTransform.Factor().Rotation; + return true; + } + catch (Exception ex) + { + LogManager.Warning($"[当前几何姿态] 读取 ActiveTransform 失败: {ex.Message}"); + return false; + } + } + + public static void LogCurrentGeometryTransformsForDebug(ModelItem item, string prefix) + { + LogGeometryLevelTransforms(item, prefix); + } + private static void LogIncrementalTransformActual(ModelItem item, Point3D targetPosition, Rotation3D targetRotation) { try @@ -614,6 +649,8 @@ namespace NavisworksTransport.Utils $"期望X=({targetLinear.Get(0, 0):F4},{targetLinear.Get(1, 0):F4},{targetLinear.Get(2, 0):F4}), " + $"期望Y=({targetLinear.Get(0, 1):F4},{targetLinear.Get(1, 1):F4},{targetLinear.Get(2, 1):F4}), " + $"期望Z=({targetLinear.Get(0, 2):F4},{targetLinear.Get(1, 2):F4},{targetLinear.Get(2, 2):F4})"); + + LogGeometryLevelTransforms(item, "[模型增量姿态应用后][GeometryAPI]"); } catch (Exception ex) { @@ -792,19 +829,21 @@ namespace NavisworksTransport.Utils var doc = Application.ActiveDocument; var modelItems = new ModelItemCollection { item }; Vector3D currentScale = new Vector3D(1, 1, 1); + ModelGeometry geometry = item.FindFirstGeometry() ?? item.Geometry; if (preserveCurrentScale) { - var currentComponents = item.Transform.Factor(); + var currentBaseTransform = geometry?.PermanentOverrideTransform ?? item.Transform; + var currentComponents = currentBaseTransform.Factor(); currentScale = currentComponents.Scale; } doc.Models.ResetPermanentTransform(modelItems); - var originalTransform = item.Transform; + var originalTransform = geometry?.OriginalTransform ?? item.Transform; var originalComponents = originalTransform.Factor(); var originalRotation = originalComponents.Rotation; - var originalBounds = item.BoundingBox(); + var originalBounds = geometry?.BoundingBox ?? item.BoundingBox(); var originalCenterPos = originalBounds.Center; Rotation3D deltaRotation = BuildDeltaRotation(originalRotation, targetRotation); @@ -840,6 +879,7 @@ namespace NavisworksTransport.Utils LogAbsoluteTransformDiagnostics(item, originalRotation, targetRotation, deltaRotation, originalCenterPos, targetPosition, translation); doc.Models.OverridePermanentTransform(modelItems, transform, false); + LogAbsoluteTransformActual(item, preserveCurrentScale, currentScale, targetPosition, targetRotation); } private static Rotation3D BuildDeltaRotation(Rotation3D originalRotation, Rotation3D targetRotation) @@ -893,6 +933,108 @@ namespace NavisworksTransport.Utils } } + private static void LogAbsoluteTransformActual( + ModelItem item, + bool preserveCurrentScale, + Vector3D currentScale, + Point3D targetPosition, + Rotation3D targetRotation) + { + try + { + Transform3D actualTransform = item.Transform; + var actualComponents = actualTransform.Factor(); + var actualRotation = actualComponents.Rotation; + var actualScale = actualComponents.Scale; + var actualLinear = new Transform3D(actualRotation).Linear; + var targetLinear = new Transform3D(targetRotation).Linear; + var actualBounds = item.BoundingBox(); + Point3D actualCenter = actualBounds?.Center ?? new Point3D(0, 0, 0); + + LogManager.Info( + $"[模型姿态应用后] {item.DisplayName} PreserveScale={preserveCurrentScale}, " + + $"输入Scale=({currentScale.X:F4},{currentScale.Y:F4},{currentScale.Z:F4}), " + + $"实际Scale=({actualScale.X:F4},{actualScale.Y:F4},{actualScale.Z:F4}), " + + $"目标中心=({targetPosition.X:F3},{targetPosition.Y:F3},{targetPosition.Z:F3}), " + + $"实际中心=({actualCenter.X:F3},{actualCenter.Y:F3},{actualCenter.Z:F3})"); + + LogManager.Info( + $"[模型姿态应用后] {item.DisplayName} 目标Transform轴: " + + $"X=({targetLinear.Get(0, 0):F4},{targetLinear.Get(1, 0):F4},{targetLinear.Get(2, 0):F4}), " + + $"Y=({targetLinear.Get(0, 1):F4},{targetLinear.Get(1, 1):F4},{targetLinear.Get(2, 1):F4}), " + + $"Z=({targetLinear.Get(0, 2):F4},{targetLinear.Get(1, 2):F4},{targetLinear.Get(2, 2):F4})"); + + LogManager.Info( + $"[模型姿态应用后] {item.DisplayName} 实际Transform轴: " + + $"X=({actualLinear.Get(0, 0):F4},{actualLinear.Get(1, 0):F4},{actualLinear.Get(2, 0):F4}), " + + $"Y=({actualLinear.Get(0, 1):F4},{actualLinear.Get(1, 1):F4},{actualLinear.Get(2, 1):F4}), " + + $"Z=({actualLinear.Get(0, 2):F4},{actualLinear.Get(1, 2):F4},{actualLinear.Get(2, 2):F4})"); + + LogGeometryLevelTransforms(item, "[模型姿态应用后][GeometryAPI]"); + } + catch (Exception ex) + { + LogManager.Warning($"[模型姿态应用后] 输出诊断日志失败: {ex.Message}"); + } + } + + private static void LogGeometryLevelTransforms(ModelItem item, string prefix) + { + if (item == null) + { + return; + } + + try + { + ModelGeometry geometry = item.FindFirstGeometry() ?? item.Geometry; + if (geometry == null) + { + LogManager.Info($"{prefix} Geometry=null"); + return; + } + + LogManager.Info( + $"{prefix} GeometryType={geometry.GetType().FullName}, " + + $"FragmentCount={geometry.FragmentCount}, " + + $"BoundsCenter=({geometry.BoundingBox.Center.X:F3},{geometry.BoundingBox.Center.Y:F3},{geometry.BoundingBox.Center.Z:F3})"); + + LogTransformProperty(geometry.OriginalTransform, $"{prefix} OriginalTransform"); + LogTransformProperty(geometry.PermanentOverrideTransform, $"{prefix} PermanentOverrideTransform"); + LogTransformProperty(geometry.PermanentTransform, $"{prefix} PermanentTransform"); + LogTransformProperty(geometry.ActiveTransform, $"{prefix} ActiveTransform"); + } + catch (Exception ex) + { + LogManager.Warning($"{prefix} 读取几何层当前变换失败: {ex.Message}"); + } + } + + private static void LogTransformProperty(Transform3D transform, string prefix) + { + if (transform == null) + { + LogManager.Info($"{prefix}=null"); + return; + } + + try + { + var linear = transform.Linear; + var components = transform.Factor(); + LogManager.Info( + $"{prefix}: " + + $"X=({linear.Get(0, 0):F4},{linear.Get(1, 0):F4},{linear.Get(2, 0):F4}), " + + $"Y=({linear.Get(0, 1):F4},{linear.Get(1, 1):F4},{linear.Get(2, 1):F4}), " + + $"Z=({linear.Get(0, 2):F4},{linear.Get(1, 2):F4},{linear.Get(2, 2):F4}), " + + $"T=({components.Translation.X:F3},{components.Translation.Y:F3},{components.Translation.Z:F3})"); + } + catch (Exception ex) + { + LogManager.Warning($"{prefix} 读取失败: {ex.Message}"); + } + } + /// /// 将物体移动到指定位置和朝向,同时保持缩放比例 /// 专为虚拟物体设计,避免缩放被覆盖