From aabf263f0da5dfcd40450ebd419eaaa19fb78e49 Mon Sep 17 00:00:00 2001 From: tian <11429339@qq.com> Date: Wed, 25 Mar 2026 11:18:03 +0800 Subject: [PATCH] Fix host rotation adapter split for real and virtual objects --- NavisworksTransport.UnitTests.csproj | 2 + Properties/AssemblyInfo.cs | 3 +- TransportPlugin.csproj | 5 + .../FragmentDefaultUpContextTests.cs | 47 +++++ .../VirtualGroundPoseCharacterizationTests.cs | 120 +++++++++++ .../VirtualObjectModelTransformTests.cs | 100 +++++++++ resources/unit_cube_yup.nwc | Bin 0 -> 1806 bytes resources/unit_cube_yup.obj | 26 +++ src/Core/Animation/PathAnimationManager.cs | 190 ++++++++++++------ src/Core/VirtualObjectManager.cs | 22 +- .../FragmentDefaultUpContext.cs | 43 +++- .../CoordinateSystem/HostCoordinateAdapter.cs | 76 +++++++ src/Utils/ModelItemTransformHelper.cs | 148 +++++++++++++- 13 files changed, 693 insertions(+), 89 deletions(-) create mode 100644 UnitTests/CoordinateSystem/FragmentDefaultUpContextTests.cs create mode 100644 UnitTests/CoordinateSystem/VirtualGroundPoseCharacterizationTests.cs create mode 100644 UnitTests/CoordinateSystem/VirtualObjectModelTransformTests.cs create mode 100644 resources/unit_cube_yup.nwc create mode 100644 resources/unit_cube_yup.obj 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 0000000000000000000000000000000000000000..47289cdd24b1170ccafc876bab75ff0c0c831ede GIT binary patch literal 1806 zcma)7X;_m768=I+`!G0VEvN z&|pwRPTMX70nxHn4k;i(4!IVjU9O^Tp_P>zvT^;h{nz<5$Mej*^UgdoCf>}zfJ7cE z;;*X=F@%1p# zlMc-9B|ORt`GOU?Uq~=%jxi<*Ia8qqQz={1tDi8ZV`KoXi!oF+K-OZstfIsg>;F7p zGV?m{NEnZ^Q4!pBd(5=wo?lYKw}}7%0faJY1f4E|&RqaoAq-74JZmqc(eWsPX~L{r zkd_-c0qjp~g5l98qT$i;(I*JW(ZP++*kqYnH(_6?)SC-e&-#X(psDe9po0ar0qUTg zH5ipSqJzidb^HLpm~8PvC}T#_>2&qENbm`y8-Q0qoK!A)1^@sW0OTV8Kb7eM;uX)a zxxRQGuYv|FZs@shzgk|knp_k40P_0-_OHH4@l5*9u%@({?(7?Im1Y>~q@6#ne+EcGa(?DM5C9b{M4cMAVm?x%%f$ zU0Q)&gLn|o1Ad*Q6||Xu0eo#hvS2l-!lk0+{{S*o=sh5e^B@hz2mqI0b0h5oC7TN) zRWCRDN7O)Pgvcl3YlP?ymm{2N&&qVs=pFQ=VLhpXFD&p1A6%ZE2i81-3<3G0nISqN z*sx;kf32}V>0at;ceM68{q(d=i!F4N16b>s4S(cIRWJ-5`CG^B10GEP$-pq1Y>vDI zC7B>=A@U<3%^j$-J-Xw^0}mYODAMhgBk%5cOrkXB0mS;zBwyT-yAHrbe?5CuD%J<; z8D?liC#dRc643sTK0~D<0X3maTNO@d&C|ruaOPr7Peos!Pk|>EjTZk-#XiU4Ul987 zx)`qk3w$rZd>H37Ffgo85EK@W5={Lw5~cpxRyC!M9K;Tb@=WEi{bN&E?98yR6w9HR z(%4^|SW<4m+s2m;*x3HKA2ZZ}+z<>&D4Sg<}@nD$}qJtXE;#jRr*!T zlVzHrI$frh~9VqzF=J_?O(c^bEtYr?lV#KsV)3FP2LZC1R=4g!)F#-pt*9jOFRbeyg5{D*IjM-^3WKQ zuWb(imFrfEe^`!_;ts7&BGPRQcZCaUc%+4LjJc6j+~gCiQR6vjilgizmE(qlE**!j zHx9YByL?wnc1^CS?d;IEN-&{W3yXH(c-v87v)F-lEV=0O@BmnI(87${=gjMZ@`P5l z=B`)XaIKN;!|UTM0cfX*mnlI^m9#=I#*aXaMlER z8k%|7Z7c}kszd2Co#-*n%`UHtb)N;g2Q5hQH5~_XLK&4h14w9+lAZtTV537NXEZ*Q z+!s&h`}%U;Y?H@5C{fe_WkKWMQt$#xvB(O9CEd*z!`D5Yle;aOS}gkMr!m771MT+9-m6CDg~BP5oVUF;Ju${NRJnD>@{8cBUH zH;q}7eVM;_#mMOW#`nRO9OK`#Av<=_)n{e}fp*|QZk*P#4z>!*ds1sO$NO{pslQG4yjmbq}^ 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}"); + } + } + /// /// 将物体移动到指定位置和朝向,同时保持缩放比例 /// 专为虚拟物体设计,避免缩放被覆盖