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}");
+ }
+ }
+
///
/// 将物体移动到指定位置和朝向,同时保持缩放比例
/// 专为虚拟物体设计,避免缩放被覆盖