Fix host rotation adapter split for real and virtual objects

This commit is contained in:
tian 2026-03-25 11:18:03 +08:00
parent 029c7e37ad
commit aabf263f0d
13 changed files with 693 additions and 89 deletions

View File

@ -72,6 +72,8 @@
<Compile Include="UnitTests\CoordinateSystem\PathTargetFrameResolverTests.cs" />
<Compile Include="UnitTests\CoordinateSystem\AssemblyEndFaceAnalyzerTests.cs" />
<Compile Include="UnitTests\CoordinateSystem\AssemblyInstallationReferenceBuilderTests.cs" />
<Compile Include="UnitTests\CoordinateSystem\FragmentDefaultUpContextTests.cs" />
<Compile Include="UnitTests\CoordinateSystem\VirtualGroundPoseCharacterizationTests.cs" />
<Compile Include="UnitTests\Properties\AssemblyInfo.cs" />
</ItemGroup>
<ItemGroup>

View File

@ -31,4 +31,5 @@ using System.Runtime.InteropServices;
//
// 主版本和次版本手动维护Build和Revision在编译时自动更新
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]
[assembly: InternalsVisibleTo("NavisworksTransport.UnitTests")]

View File

@ -522,6 +522,11 @@
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<Link>resources\unit_cube.nwc</Link>
</None>
<!-- Unit Cube YUp NWC Model File -->
<None Include="resources\unit_cube_yup.nwc">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<Link>resources\unit_cube_yup.nwc</Link>
</None>
<!-- Unit Cylinder NWC Model File -->
<None Include="resources\unit_cylinder.nwc">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>

View File

@ -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);
}
}
}

View File

@ -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;
}
}
}
}

View File

@ -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);
}
}
}

BIN
resources/unit_cube_yup.nwc Normal file

Binary file not shown.

View File

@ -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

View File

@ -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;

View File

@ -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);

View File

@ -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<string, string> DocumentFragmentDefaultUpAxes =
new ConcurrentDictionary<string, string>(StringComparer.OrdinalIgnoreCase);
private static readonly ConcurrentDictionary<int, string> RuntimeDocumentKeys =
new ConcurrentDictionary<int, string>();
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();
}
}
}

View File

@ -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。
/// </summary>
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);

View File

@ -589,6 +589,41 @@ namespace NavisworksTransport.Utils
LogIncrementalTransformActual(item, targetPosition, targetRotation);
}
/// <summary>
/// 读取物体当前实际几何姿态。
/// 优先使用 ModelGeometry.ActiveTransform因为 ModelItem.Transform 只反映原始设计变换。
/// </summary>
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}");
}
}
/// <summary>
/// 将物体移动到指定位置和朝向,同时保持缩放比例
/// 专为虚拟物体设计,避免缩放被覆盖