diff --git a/AGENTS.md b/AGENTS.md
index 9a438af..b58d67a 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -172,6 +172,20 @@ var rotation = new Rotation3D(qw, qx, qy, qz); // 错误
- 可以视为直接生活在宿主坐标系里
- 角度调整对话框里的 `X/Y/Z`,对真实物体应按**宿主坐标系**消费
+#### 真实物体参考姿态
+
+- 真实物体不要再直接依赖 `ModelItem.Transform.Factor().Rotation`
+ - 对很多 Revit 导入件,这个值是单位旋转,不代表真实显示姿态
+- 真实物体参考姿态应优先来自 fragment 代表姿态
+- `Fragment默认Up` 的用途是:
+ - 把 fragment 参考框架解释成当前宿主坐标语义下的真实姿态
+ - 不是仅仅挑一个“竖直候选轴”
+- 在真实物体链路里,必须先完成“fragment 姿态解释”,再谈:
+ - 前进方向
+ - 路径对齐
+ - 角度调整
+- `_trackedRotation` 对真实物体必须优先跟踪“解释后的真实参考姿态”,不能回退成 `Transform` 的单位旋转
+
#### 虚拟物体
- 有明确资产坐标系
@@ -194,6 +208,15 @@ var rotation = new Rotation3D(qw, qx, qy, qz); // 错误
- 不能像平面路径那样在宿主空间随意补旋转
- 必须并入 `canonical -> rail pose` 链
- `Rail 0°` 基线必须稳定,不能被角度修正逻辑污染
+- `Rail` 真实物体不应再退回默认 `PositiveX / PositiveY`
+- 三类路径都必须先复用同一个“对象姿态解释层”:
+ - `Ground / Hoisting`
+ - `forward = 路径方向`
+ - `up = 宿主 up`
+ - `Rail`
+ - `forward = rail 切向`
+ - `up = rail 法向 / preferred normal`
+- 统一的是“对象参考姿态来源与解释方式”,不是把三类路径都强行改成同一个 `up`
### 4.4 通行空间与物体姿态的关系
@@ -209,6 +232,10 @@ var rotation = new Rotation3D(qw, qx, qy, qz); // 错误
- 是否一条链用了“原始高度”
- 另一条链用了“旋转后法线尺寸”
+- `Rail` 真实物体尤其要检查:
+ - 起点/逐帧中心偏移是否仍在用原始 `objectHeight`
+ - 通行空间是否仍在用旧 `RailAssetConvention`
+ - 通行空间、路径偏移、最终姿态是否共享同一套“最终姿态尺寸语义”
### 4.5 终端安装仿真
diff --git a/NavisworksTransport.UnitTests.csproj b/NavisworksTransport.UnitTests.csproj
index dcde989..21c8463 100644
--- a/NavisworksTransport.UnitTests.csproj
+++ b/NavisworksTransport.UnitTests.csproj
@@ -64,6 +64,8 @@
+
+
diff --git a/TransportPlugin.csproj b/TransportPlugin.csproj
index e2b3465..4d8bfaa 100644
--- a/TransportPlugin.csproj
+++ b/TransportPlugin.csproj
@@ -343,6 +343,8 @@
+
+
diff --git a/UnitTests/CoordinateSystem/HostCoordinateAdapterTests.cs b/UnitTests/CoordinateSystem/HostCoordinateAdapterTests.cs
index c206309..3ea652f 100644
--- a/UnitTests/CoordinateSystem/HostCoordinateAdapterTests.cs
+++ b/UnitTests/CoordinateSystem/HostCoordinateAdapterTests.cs
@@ -305,6 +305,63 @@ namespace NavisworksTransport.UnitTests.CoordinateSystem
AssertAxis(linear, 3, Vector3.Transform(baselineZ, hostCorrection));
}
+ [TestMethod]
+ public void YUp_ComposeHostQuaternion_ForRailBaseline_ShouldRotateAxesAroundHostXAxis()
+ {
+ var adapter = new HostCoordinateAdapter(CoordinateSystemType.YUp);
+ Vector3 baselineX = new Vector3(-0.9900f, 0.1403f, -0.0163f);
+ Vector3 baselineY = new Vector3(0.1403f, 0.9901f, 0.0000f);
+ Vector3 baselineZ = new Vector3(0.0161f, -0.0023f, -0.9999f);
+ Quaternion baseline = CreateQuaternionFromAxes(baselineX, baselineY, baselineZ);
+
+ var correction = new LocalEulerRotationCorrection(90.0, 0.0, 0.0);
+ Quaternion composed = adapter.ComposeHostQuaternion(baseline, correction);
+ Quaternion hostCorrection = adapter.CreateHostRotationCorrection(correction);
+ Matrix4x4 linear = Matrix4x4.CreateFromQuaternion(composed);
+
+ AssertAxis(linear, 1, Vector3.Transform(baselineX, hostCorrection));
+ AssertAxis(linear, 2, Vector3.Transform(baselineY, hostCorrection));
+ AssertAxis(linear, 3, Vector3.Transform(baselineZ, hostCorrection));
+ }
+
+ [TestMethod]
+ public void YUp_ComposeHostQuaternion_ForRailBaseline_ShouldRotateAxesAroundHostYAxis()
+ {
+ var adapter = new HostCoordinateAdapter(CoordinateSystemType.YUp);
+ Vector3 baselineX = new Vector3(-0.9900f, 0.1403f, -0.0163f);
+ Vector3 baselineY = new Vector3(0.1403f, 0.9901f, 0.0000f);
+ Vector3 baselineZ = new Vector3(0.0161f, -0.0023f, -0.9999f);
+ Quaternion baseline = CreateQuaternionFromAxes(baselineX, baselineY, baselineZ);
+
+ var correction = new LocalEulerRotationCorrection(0.0, 90.0, 0.0);
+ Quaternion composed = adapter.ComposeHostQuaternion(baseline, correction);
+ Quaternion hostCorrection = adapter.CreateHostRotationCorrection(correction);
+ Matrix4x4 linear = Matrix4x4.CreateFromQuaternion(composed);
+
+ AssertAxis(linear, 1, Vector3.Transform(baselineX, hostCorrection));
+ AssertAxis(linear, 2, Vector3.Transform(baselineY, hostCorrection));
+ AssertAxis(linear, 3, Vector3.Transform(baselineZ, hostCorrection));
+ }
+
+ [TestMethod]
+ public void YUp_ComposeHostQuaternion_ForRailBaseline_ShouldRotateAxesAroundHostZAxis()
+ {
+ var adapter = new HostCoordinateAdapter(CoordinateSystemType.YUp);
+ Vector3 baselineX = new Vector3(-0.9900f, 0.1403f, -0.0163f);
+ Vector3 baselineY = new Vector3(0.1403f, 0.9901f, 0.0000f);
+ Vector3 baselineZ = new Vector3(0.0161f, -0.0023f, -0.9999f);
+ Quaternion baseline = CreateQuaternionFromAxes(baselineX, baselineY, baselineZ);
+
+ var correction = new LocalEulerRotationCorrection(0.0, 0.0, 90.0);
+ Quaternion composed = adapter.ComposeHostQuaternion(baseline, correction);
+ Quaternion hostCorrection = adapter.CreateHostRotationCorrection(correction);
+ Matrix4x4 linear = Matrix4x4.CreateFromQuaternion(composed);
+
+ AssertAxis(linear, 1, Vector3.Transform(baselineX, hostCorrection));
+ AssertAxis(linear, 2, Vector3.Transform(baselineY, hostCorrection));
+ AssertAxis(linear, 3, Vector3.Transform(baselineZ, hostCorrection));
+ }
+
private static void AssertPoint(Vector3 point, double x, double y, double z)
{
Assert.AreEqual(x, point.X, 1e-9);
diff --git a/UnitTests/CoordinateSystem/RealObjectRailAxisConventionResolverTests.cs b/UnitTests/CoordinateSystem/RealObjectRailAxisConventionResolverTests.cs
new file mode 100644
index 0000000..27284da
--- /dev/null
+++ b/UnitTests/CoordinateSystem/RealObjectRailAxisConventionResolverTests.cs
@@ -0,0 +1,89 @@
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using NavisworksTransport.Utils.CoordinateSystem;
+using System.Numerics;
+
+namespace NavisworksTransport.UnitTests.CoordinateSystem
+{
+ [TestClass]
+ public class RealObjectRailAxisConventionResolverTests
+ {
+ [TestMethod]
+ public void YUp_InterpretedReferencePose_ShouldChoosePositiveXAndPositiveYForRail()
+ {
+ Vector3 referenceAxisX = new Vector3(-1.0f, 0.0f, 0.0f);
+ Vector3 referenceAxisY = new Vector3(0.0f, 1.0f, 0.0f);
+ Vector3 referenceAxisZ = new Vector3(0.0f, 0.0f, -1.0f);
+ Vector3 desiredForward = Vector3.Normalize(new Vector3(-0.8987f, 0.0f, 0.4386f));
+
+ bool ok = RealObjectRailAxisConventionResolver.TryResolve(
+ referenceAxisX,
+ referenceAxisY,
+ referenceAxisZ,
+ desiredForward,
+ CoordinateSystemType.YUp,
+ out ModelAxisConvention convention,
+ out LocalAxisDirection selectedForwardAxis,
+ out Vector3 selectedForwardWorldAxis);
+
+ Assert.IsTrue(ok);
+ Assert.AreEqual(LocalAxisDirection.PositiveX, selectedForwardAxis);
+ Assert.AreEqual(LocalAxisDirection.PositiveX, convention.ForwardAxis);
+ Assert.AreEqual(LocalAxisDirection.PositiveY, convention.UpAxis);
+ AssertVector(selectedForwardWorldAxis, -1.0, 0.0, 0.0);
+ }
+
+ [TestMethod]
+ public void ZUp_InterpretedReferencePose_ShouldChoosePositiveXAndPositiveZForRail()
+ {
+ Vector3 referenceAxisX = new Vector3(-1.0f, 0.0f, 0.0f);
+ Vector3 referenceAxisY = new Vector3(0.0f, 1.0f, 0.0f);
+ Vector3 referenceAxisZ = new Vector3(0.0f, 0.0f, 1.0f);
+ Vector3 desiredForward = Vector3.Normalize(new Vector3(-0.9f, 0.3f, 0.0f));
+
+ bool ok = RealObjectRailAxisConventionResolver.TryResolve(
+ referenceAxisX,
+ referenceAxisY,
+ referenceAxisZ,
+ desiredForward,
+ CoordinateSystemType.ZUp,
+ out ModelAxisConvention convention,
+ out LocalAxisDirection selectedForwardAxis,
+ out _);
+
+ Assert.IsTrue(ok);
+ Assert.AreEqual(LocalAxisDirection.PositiveX, selectedForwardAxis);
+ Assert.AreEqual(LocalAxisDirection.PositiveX, convention.ForwardAxis);
+ Assert.AreEqual(LocalAxisDirection.PositiveZ, convention.UpAxis);
+ }
+
+ [TestMethod]
+ public void YUp_ShouldNotSelectYAxisFamilyAsForwardCandidate()
+ {
+ Vector3 referenceAxisX = new Vector3(1.0f, 0.0f, 0.0f);
+ Vector3 referenceAxisY = new Vector3(0.0f, 1.0f, 0.0f);
+ Vector3 referenceAxisZ = new Vector3(0.0f, 0.0f, 1.0f);
+ Vector3 desiredForward = Vector3.Normalize(new Vector3(0.0f, 1.0f, 0.01f));
+
+ bool ok = RealObjectRailAxisConventionResolver.TryResolve(
+ referenceAxisX,
+ referenceAxisY,
+ referenceAxisZ,
+ desiredForward,
+ CoordinateSystemType.YUp,
+ out _,
+ out LocalAxisDirection selectedForwardAxis,
+ out _);
+
+ Assert.IsTrue(ok);
+ Assert.AreNotEqual(LocalAxisDirection.PositiveY, selectedForwardAxis);
+ Assert.AreNotEqual(LocalAxisDirection.NegativeY, selectedForwardAxis);
+ }
+
+ private static void AssertVector(Vector3 actual, double x, double y, double z, double tolerance = 1e-6)
+ {
+ Assert.AreEqual(x, actual.X, tolerance);
+ Assert.AreEqual(y, actual.Y, tolerance);
+ Assert.AreEqual(z, actual.Z, tolerance);
+ }
+ }
+}
diff --git a/UnitTests/CoordinateSystem/RealObjectRailExtentResolverTests.cs b/UnitTests/CoordinateSystem/RealObjectRailExtentResolverTests.cs
new file mode 100644
index 0000000..7e1c0bb
--- /dev/null
+++ b/UnitTests/CoordinateSystem/RealObjectRailExtentResolverTests.cs
@@ -0,0 +1,142 @@
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using NavisworksTransport.Utils.CoordinateSystem;
+using System;
+using System.Numerics;
+
+namespace NavisworksTransport.UnitTests.CoordinateSystem
+{
+ [TestClass]
+ public class RealObjectRailExtentResolverTests
+ {
+ [TestMethod]
+ public void YUp_ZeroCorrection_ShouldKeepResolvedRailUpExtent()
+ {
+ Vector3 referenceAxisX = new Vector3(-1.0f, 0.0f, 0.0f);
+ Vector3 referenceAxisY = new Vector3(0.0f, 1.0f, 0.0f);
+ Vector3 referenceAxisZ = new Vector3(0.0f, 0.0f, -1.0f);
+ Vector3 desiredForward = Vector3.Normalize(new Vector3(-0.8987f, 0.0f, 0.4386f));
+ Quaternion baseline = Quaternion.Identity;
+
+ bool ok = RealObjectRailExtentResolver.TryResolveProjectedSemanticExtents(
+ referenceAxisX,
+ referenceAxisY,
+ referenceAxisZ,
+ desiredForward,
+ CoordinateSystemType.YUp,
+ forwardSize: 6.0,
+ sideSize: 2.0,
+ upSize: 4.0,
+ baseline,
+ baseline,
+ out ModelAxisConvention convention,
+ out var extents);
+
+ Assert.IsTrue(ok);
+ Assert.AreEqual(LocalAxisDirection.PositiveX, convention.ForwardAxis);
+ Assert.AreEqual(LocalAxisDirection.PositiveY, convention.UpAxis);
+ Assert.AreEqual(6.0, extents.forwardExtent, 1e-6);
+ Assert.AreEqual(2.0, extents.sideExtent, 1e-6);
+ Assert.AreEqual(4.0, extents.upExtent, 1e-6);
+ }
+
+ [TestMethod]
+ public void YUp_HostY90_ShouldKeepResolvedRailUpExtentAndSwapForwardSide()
+ {
+ Vector3 referenceAxisX = new Vector3(-1.0f, 0.0f, 0.0f);
+ Vector3 referenceAxisY = new Vector3(0.0f, 1.0f, 0.0f);
+ Vector3 referenceAxisZ = new Vector3(0.0f, 0.0f, -1.0f);
+ Vector3 desiredForward = Vector3.Normalize(new Vector3(-0.8987f, 0.0f, 0.4386f));
+ Quaternion baseline = Quaternion.Identity;
+ var adapter = new HostCoordinateAdapter(CoordinateSystemType.YUp);
+ Quaternion final = adapter.ComposeHostQuaternion(
+ baseline,
+ new LocalEulerRotationCorrection(0.0, 90.0, 0.0));
+
+ bool ok = RealObjectRailExtentResolver.TryResolveProjectedSemanticExtents(
+ referenceAxisX,
+ referenceAxisY,
+ referenceAxisZ,
+ desiredForward,
+ CoordinateSystemType.YUp,
+ forwardSize: 6.0,
+ sideSize: 2.0,
+ upSize: 4.0,
+ baseline,
+ final,
+ out _,
+ out var extents);
+
+ Assert.IsTrue(ok);
+ Assert.AreEqual(2.0, extents.forwardExtent, 1e-6);
+ Assert.AreEqual(6.0, extents.sideExtent, 1e-6);
+ Assert.AreEqual(4.0, extents.upExtent, 1e-6);
+ }
+
+ [TestMethod]
+ public void YUp_DualAxisCorrection_WithRailBaseline_ShouldProjectAgainstFinalRailPose()
+ {
+ Vector3 referenceAxisX = new Vector3(-1.0f, 0.0f, 0.0f);
+ Vector3 referenceAxisY = new Vector3(0.0f, 1.0f, 0.0f);
+ Vector3 referenceAxisZ = new Vector3(0.0f, 0.0f, -1.0f);
+ Vector3 desiredForward = Vector3.Normalize(new Vector3(-0.8987f, 0.0f, 0.4386f));
+
+ Quaternion baseline = Quaternion.Normalize(Quaternion.CreateFromRotationMatrix(new Matrix4x4(
+ 0.9900f, -0.1403f, -0.0161f, 0f,
+ 0.1403f, 0.9901f, -0.0023f, 0f,
+ 0.0163f, 0.0000f, 0.9999f, 0f,
+ 0f, 0f, 0f, 1f)));
+
+ var adapter = new HostCoordinateAdapter(CoordinateSystemType.YUp);
+ Quaternion final = adapter.ComposeHostQuaternion(
+ baseline,
+ new LocalEulerRotationCorrection(0.0, 90.0, 90.0));
+
+ bool ok = RealObjectRailExtentResolver.TryResolveProjectedSemanticExtents(
+ referenceAxisX,
+ referenceAxisY,
+ referenceAxisZ,
+ desiredForward,
+ CoordinateSystemType.YUp,
+ forwardSize: 6.0,
+ sideSize: 2.0,
+ upSize: 4.0,
+ baseline,
+ final,
+ out ModelAxisConvention convention,
+ out var extents);
+
+ Assert.IsTrue(ok);
+ Assert.AreEqual(LocalAxisDirection.PositiveX, convention.ForwardAxis);
+ Assert.AreEqual(LocalAxisDirection.PositiveY, convention.UpAxis);
+
+ Vector3 localSize = convention.CreateScaleVector3(6.0, 2.0, 4.0);
+ Vector3 rotatedLocalX = Vector3.Normalize(Vector3.Transform(Vector3.UnitX, final));
+ Vector3 rotatedLocalY = Vector3.Normalize(Vector3.Transform(Vector3.UnitY, final));
+ Vector3 rotatedLocalZ = Vector3.Normalize(Vector3.Transform(Vector3.UnitZ, final));
+ Vector3 targetForward = Vector3.Normalize(Vector3.Transform(convention.ForwardUnitVector, baseline));
+ Vector3 targetUp = Vector3.Normalize(Vector3.Transform(convention.UpUnitVector, baseline));
+ Vector3 targetSide = Vector3.Normalize(Vector3.Cross(targetForward, targetUp));
+
+ double expectedForward = ProjectExtent(localSize, rotatedLocalX, rotatedLocalY, rotatedLocalZ, targetForward);
+ double expectedSide = ProjectExtent(localSize, rotatedLocalX, rotatedLocalY, rotatedLocalZ, targetSide);
+ double expectedUp = ProjectExtent(localSize, rotatedLocalX, rotatedLocalY, rotatedLocalZ, targetUp);
+
+ Assert.AreEqual(expectedForward, extents.forwardExtent, 1e-5);
+ Assert.AreEqual(expectedSide, extents.sideExtent, 1e-5);
+ Assert.AreEqual(expectedUp, extents.upExtent, 1e-5);
+ Assert.AreNotEqual(4.0, extents.upExtent, 1e-3, "双轴旋转后,法向尺寸不应仍停留在单轴结果。");
+ }
+
+ private static double ProjectExtent(
+ Vector3 localSize,
+ Vector3 rotatedLocalX,
+ Vector3 rotatedLocalY,
+ Vector3 rotatedLocalZ,
+ Vector3 targetAxis)
+ {
+ return Math.Abs(Vector3.Dot(rotatedLocalX, targetAxis)) * localSize.X +
+ Math.Abs(Vector3.Dot(rotatedLocalY, targetAxis)) * localSize.Y +
+ Math.Abs(Vector3.Dot(rotatedLocalZ, targetAxis)) * localSize.Z;
+ }
+ }
+}
diff --git a/doc/working/current-engineering-state.md b/doc/working/current-engineering-state.md
index 9b0e59c..548662f 100644
--- a/doc/working/current-engineering-state.md
+++ b/doc/working/current-engineering-state.md
@@ -1,6 +1,6 @@
# 当前工程状态
-更新时间:2026-03-23
+更新时间:2026-03-25
## 1. 当前稳定状态
@@ -46,6 +46,24 @@
## 4. 当前必须记住的根因与规则
+### 4.0 真实物体参考姿态与 Fragment默认Up
+
+- 真实物体不能再默认使用 `ModelItem.Transform` 或包围盒正交框架当“原始姿态”。
+- 当前稳定做法是:
+ - 先从 fragment 统计得到代表姿态
+ - 再用当前文档级 `Fragment默认Up` 解释这组 fragment 参考轴
+ - 最终得到当前宿主语义下的真实参考姿态
+- `Fragment默认Up` 的真正用途不是“找一个竖直候选轴”,而是:
+ - 解释 fragment 参考姿态的原始 `Y/Z` 语义
+ - 把 fragment 参考姿态转换成当前宿主坐标语义下可用的真实姿态
+- 当前文档如果 `Fragment默认Up` 设错,会直接导致:
+ - 真实物体起点姿态错误
+ - Ground / Hoisting / Rail 的参考姿态解释错误
+ - 后续角度修正、通行空间、路径贴合全部建立在错误姿态上
+- 当前规则:
+ - 先解释真实物体参考姿态,再做路径对齐
+ - 不能跳过这一步,直接拿 fragment 世界轴去猜业务姿态
+
### 4.1 Rotation / 矩阵语义
- `Rotation3D(a, b, c, d)` 的参数顺序是四元数 `x, y, z, w`。
@@ -162,6 +180,37 @@
2. 再验证修正后 `forward` 是否仍沿 rail 切向
3. 最后再看 Navisworks 应用层是否正确按三步增量姿态落位
+### 4.8 Rail 真实物体必须复用统一姿态解释层
+
+- `Rail` 不应再单独退回默认 `PositiveX / PositiveY` 轴约定。
+- 当前稳定原则:
+ - `Ground / Hoisting / Rail` 三类路径的真实物体,必须先共用同一个“真实参考姿态解释层”
+ - 路径类型的差异只体现在目标路径框架:
+ - `Ground / Hoisting`:`up = 宿主 up`
+ - `Rail`:`up = rail 法向 / preferred normal`
+- 当前 `Rail` 真实物体稳定链路:
+ 1. fragment 代表姿态
+ 2. `Fragment默认Up` 解释后的真实参考姿态
+ 3. `Rail` 路径框架(切向 / 法向)
+ 4. 宿主 `X/Y/Z` 角度修正
+- 不要再让 `Rail` 真实物体单独维护一套与 `Ground / Hoisting` 不同的对象姿态来源。
+
+### 4.9 Rail 真实物体的通行空间与路径偏移
+
+- `Rail` 真实物体的通行空间、起点中心偏移、逐帧法向偏移,必须共享“最终姿态”的同一套尺寸语义。
+- 当前稳定规则:
+ - 不能只根据“宿主轴角度修正”单独投影尺寸
+ - 必须同时吃到:
+ - `Rail` 基姿态
+ - 宿主 `X/Y/Z` 角度修正后的最终姿态
+- 否则会出现典型错误:
+ - 物体本体姿态正确,但起点偏离路径
+ - 单轴旋转时通行空间看起来对,双轴旋转后只跟上一个角度
+ - `Rail` 真实物体通行空间与物体本体方向不一致
+- 当前消费规则:
+ - `AnimationControlViewModel.UpdatePassageSpaceVisualization()` 中,真实物体的 `Rail` 和 `Hoisting` 必须分开消费
+ - `Rail` 真实物体不能再复用 `Hoisting` 的法向/分段参数语义
+
## 5. 当前保留的日志策略
- 保留:
diff --git a/src/Core/Animation/PathAnimationManager.cs b/src/Core/Animation/PathAnimationManager.cs
index fa76673..23e5198 100644
--- a/src/Core/Animation/PathAnimationManager.cs
+++ b/src/Core/Animation/PathAnimationManager.cs
@@ -707,19 +707,16 @@ namespace NavisworksTransport.Core.Animation
}
else if (_route.PathType == PathType.Rail)
{
- double objectHeight = GetAnimatedObjectHeight();
Point3D previousPoint = _pathPoints[0];
Point3D nextPoint = _pathPoints.Count > 1 ? _pathPoints[1] : _pathPoints[0];
+ double objectHeight = GetAnimatedObjectRailNormalExtent(previousPoint, _pathPoints[0], nextPoint);
startPosition = RailPathPoseHelper.ResolveObjectSpaceCenterPosition(_route, startPosition, previousPoint, nextPoint, objectHeight);
LogManager.Debug($"[移动到起点] Rail路径调整: 参考点=({_pathPoints[0].X:F2},{_pathPoints[0].Y:F2},{_pathPoints[0].Z:F2}), 物体中心=({startPosition.X:F2},{startPosition.Y:F2},{startPosition.Z:F2}), 物体高度={objectHeight:F2}, 安装={_route.RailMountMode}, 对接={(PathRoute.IsTopPayloadAnchorForMountMode(_route.RailMountMode) ? "顶面对接" : "底面对接")}");
- if (RailPathPoseHelper.TryCreateRailRotation(
- _route,
+ if (TryCreateRailPathRotation(
previousPoint,
_pathPoints[0],
nextPoint,
- GetCurrentRailModelAxisConvention(),
- _objectRotationCorrection,
out var railRotation))
{
var railLinearTransform = new Transform3D(railRotation).Linear;
@@ -964,7 +961,7 @@ namespace NavisworksTransport.Core.Animation
// 🔥 空中路径:根据路径类型和线段索引调整物体位置
double objectHeight = _route.PathType == PathType.Rail
- ? GetAnimatedObjectHeight()
+ ? GetAnimatedObjectRailNormalExtent(p1, framePosition, p2)
: GetAnimatedObjectGroundContactHeight();
if (_route.PathType == PathType.Hoisting)
@@ -1079,13 +1076,10 @@ namespace NavisworksTransport.Core.Animation
};
if (_route.PathType == PathType.Rail &&
- RailPathPoseHelper.TryCreateRailRotation(
- _route,
+ TryCreateRailPathRotation(
previousFramePoint,
framePosition,
nextFramePoint,
- GetCurrentRailModelAxisConvention(),
- _objectRotationCorrection,
out var railRotation))
{
frame.Rotation = railRotation;
@@ -1941,7 +1935,7 @@ namespace NavisworksTransport.Core.Animation
Point3D expectedTrackedEndPoint;
if (_route.PathType == PathType.Rail)
{
- double objectHeight = GetAnimatedObjectHeight();
+ double objectHeight = GetAnimatedObjectRailNormalExtent(previousAnchorPoint, terminalAnchorPoint, terminalAnchorPoint);
expectedTrackedEndPoint = RailPathPoseHelper.ResolveObjectSpaceCenterPosition(
_route,
terminalAnchorPoint,
@@ -3510,19 +3504,227 @@ namespace NavisworksTransport.Core.Animation
return new Vector3((float)point.X, (float)point.Y, (float)point.Z);
}
- private ModelAxisConvention GetCurrentRailModelAxisConvention()
+ private bool TryGetCurrentRailModelAxisConvention(
+ Point3D previousPoint,
+ Point3D currentPoint,
+ Point3D nextPoint,
+ out ModelAxisConvention convention)
{
+ convention = null;
+
if (IsVirtualObjectMode)
{
- return ModelAxisConvention.CreateVirtualObjectAssetConvention();
+ convention = ModelAxisConvention.CreateVirtualObjectAssetConvention();
+ return true;
}
var adapter = CoordinateSystemManager.Instance.CreateHostAdapter();
- var convention = ModelAxisConvention.CreateDefaultForHost(adapter.HostType);
+ if (IsRealObjectMode && _hasRealObjectReferenceRotation)
+ {
+ Vector3 desiredForward = new Vector3(
+ (float)(nextPoint.X - previousPoint.X),
+ (float)(nextPoint.Y - previousPoint.Y),
+ (float)(nextPoint.Z - previousPoint.Z));
+
+ if (desiredForward.LengthSquared() < 1e-6f)
+ {
+ desiredForward = new Vector3(
+ (float)(nextPoint.X - currentPoint.X),
+ (float)(nextPoint.Y - currentPoint.Y),
+ (float)(nextPoint.Z - currentPoint.Z));
+ }
+
+ if (RealObjectRailAxisConventionResolver.TryResolve(
+ _realObjectReferenceAxisX,
+ _realObjectReferenceAxisY,
+ _realObjectReferenceAxisZ,
+ desiredForward,
+ adapter.HostType,
+ out convention,
+ out var selectedForwardAxis,
+ out var selectedForwardWorldAxis))
+ {
+ LogManager.Info(
+ $"[Rail姿态修正] Host={adapter.HostType}, 虚拟物体={IsVirtualObjectMode}, " +
+ $"真实物体参考姿态生效, Forward={convention.ForwardAxis}, Up={convention.UpAxis}, " +
+ $"选中Forward轴={selectedForwardAxis}, 选中Forward世界轴=({selectedForwardWorldAxis.X:F4},{selectedForwardWorldAxis.Y:F4},{selectedForwardWorldAxis.Z:F4})");
+ return true;
+ }
+ }
+
+ convention = ModelAxisConvention.CreateDefaultForHost(adapter.HostType);
LogManager.Info(
$"[Rail姿态修正] Host={adapter.HostType}, 虚拟物体={IsVirtualObjectMode}, " +
- $"Forward={convention.ForwardAxis}, Up={convention.UpAxis}");
- return convention;
+ $"Forward={convention.ForwardAxis}, Up={convention.UpAxis}, 来源=默认轴约定");
+ return true;
+ }
+
+ private bool TryCalculateCurrentRealObjectRailProjectedExtents(
+ Point3D previousPoint,
+ Point3D currentPoint,
+ Point3D nextPoint,
+ out ModelAxisConvention convention,
+ out (double forwardExtent, double sideExtent, double upExtent) extents)
+ {
+ convention = null;
+ extents = (0.0, 0.0, 0.0);
+
+ if (!IsRealObjectMode ||
+ !_hasRealObjectReferenceRotation ||
+ _realObjectLength <= 0.0 ||
+ _realObjectWidth <= 0.0 ||
+ _realObjectHeight <= 0.0)
+ {
+ return false;
+ }
+
+ var adapter = CoordinateSystemManager.Instance.CreateHostAdapter();
+ Vector3 desiredForward = new Vector3(
+ (float)(nextPoint.X - previousPoint.X),
+ (float)(nextPoint.Y - previousPoint.Y),
+ (float)(nextPoint.Z - previousPoint.Z));
+
+ if (desiredForward.LengthSquared() < 1e-6f)
+ {
+ desiredForward = new Vector3(
+ (float)(nextPoint.X - currentPoint.X),
+ (float)(nextPoint.Y - currentPoint.Y),
+ (float)(nextPoint.Z - currentPoint.Z));
+ }
+
+ if (!TryGetCurrentRailModelAxisConvention(previousPoint, currentPoint, nextPoint, out var railConvention))
+ {
+ return false;
+ }
+
+ if (!RailPathPoseHelper.TryCreateRailRotation(
+ _route,
+ previousPoint,
+ currentPoint,
+ nextPoint,
+ railConvention,
+ LocalEulerRotationCorrection.Zero,
+ out var baselineRotation))
+ {
+ return false;
+ }
+
+ Quaternion baselineHostQuaternion = new Quaternion(
+ (float)baselineRotation.A,
+ (float)baselineRotation.B,
+ (float)baselineRotation.C,
+ (float)baselineRotation.D);
+ Quaternion finalHostQuaternion = adapter.ComposeHostQuaternion(
+ baselineHostQuaternion,
+ _objectRotationCorrection);
+
+ return RealObjectRailExtentResolver.TryResolveProjectedSemanticExtents(
+ _realObjectReferenceAxisX,
+ _realObjectReferenceAxisY,
+ _realObjectReferenceAxisZ,
+ desiredForward,
+ adapter.HostType,
+ _realObjectLength,
+ _realObjectWidth,
+ _realObjectHeight,
+ baselineHostQuaternion,
+ finalHostQuaternion,
+ out convention,
+ out extents);
+ }
+
+ public bool TryGetCurrentRouteRealObjectRailProjectedExtents(
+ out double forwardExtent,
+ out double sideExtent,
+ out double upExtent)
+ {
+ forwardExtent = 0.0;
+ sideExtent = 0.0;
+ upExtent = 0.0;
+
+ if (_route == null || _route.PathType != PathType.Rail || _pathPoints == null || _pathPoints.Count == 0)
+ {
+ return false;
+ }
+
+ Point3D previousPoint = _pathPoints[0];
+ Point3D currentPoint = _pathPoints[0];
+ Point3D nextPoint = _pathPoints.Count > 1 ? _pathPoints[1] : _pathPoints[0];
+
+ if (!TryCalculateCurrentRealObjectRailProjectedExtents(
+ previousPoint,
+ currentPoint,
+ nextPoint,
+ out _,
+ out var extents))
+ {
+ return false;
+ }
+
+ forwardExtent = extents.forwardExtent;
+ sideExtent = extents.sideExtent;
+ upExtent = extents.upExtent;
+ return true;
+ }
+
+ private bool TryCreateRailPathRotation(
+ Point3D previousPoint,
+ Point3D currentPoint,
+ Point3D nextPoint,
+ out Rotation3D rotation)
+ {
+ rotation = Rotation3D.Identity;
+
+ if (!TryGetCurrentRailModelAxisConvention(previousPoint, currentPoint, nextPoint, out var railConvention))
+ {
+ return false;
+ }
+
+ if (IsRealObjectMode)
+ {
+ if (!RailPathPoseHelper.TryCreateRailRotation(
+ _route,
+ previousPoint,
+ currentPoint,
+ nextPoint,
+ railConvention,
+ LocalEulerRotationCorrection.Zero,
+ out var baselineRotation))
+ {
+ return false;
+ }
+
+ var adapter = CoordinateSystemManager.Instance.CreateHostAdapter();
+ Quaternion baselineHostQuaternion = new Quaternion(
+ (float)baselineRotation.A,
+ (float)baselineRotation.B,
+ (float)baselineRotation.C,
+ (float)baselineRotation.D);
+ Quaternion composedHostQuaternion = adapter.ComposeHostQuaternion(
+ baselineHostQuaternion,
+ _objectRotationCorrection);
+ rotation = adapter.FromHostQuaternion(composedHostQuaternion);
+
+ Matrix4x4 baselineLinear = Matrix4x4.CreateFromQuaternion(baselineHostQuaternion);
+ Matrix4x4 composedLinear = Matrix4x4.CreateFromQuaternion(composedHostQuaternion);
+ LogManager.Info(
+ $"[Rail真实物体角度修正] BaselineX=({baselineLinear.M11:F4},{baselineLinear.M21:F4},{baselineLinear.M31:F4}), " +
+ $"BaselineY=({baselineLinear.M12:F4},{baselineLinear.M22:F4},{baselineLinear.M32:F4}), " +
+ $"BaselineZ=({baselineLinear.M13:F4},{baselineLinear.M23:F4},{baselineLinear.M33:F4}), " +
+ $"HostComposeX=({composedLinear.M11:F4},{composedLinear.M21:F4},{composedLinear.M31:F4}), " +
+ $"HostComposeY=({composedLinear.M12:F4},{composedLinear.M22:F4},{composedLinear.M32:F4}), " +
+ $"HostComposeZ=({composedLinear.M13:F4},{composedLinear.M23:F4},{composedLinear.M33:F4})");
+ return true;
+ }
+
+ return RailPathPoseHelper.TryCreateRailRotation(
+ _route,
+ previousPoint,
+ currentPoint,
+ nextPoint,
+ railConvention,
+ _objectRotationCorrection,
+ out rotation);
}
private ModelAxisConvention GetCurrentModelAxisConvention()
@@ -3627,6 +3829,27 @@ namespace NavisworksTransport.Core.Animation
return true;
}
+ private double GetAnimatedObjectRailNormalExtent(
+ Point3D previousPoint,
+ Point3D currentPoint,
+ Point3D nextPoint)
+ {
+ if (TryCalculateCurrentRealObjectRailProjectedExtents(
+ previousPoint,
+ currentPoint,
+ nextPoint,
+ out var convention,
+ out var extents))
+ {
+ LogManager.Debug(
+ $"[Rail法向尺寸] 真实物体有效尺寸: Forward={extents.forwardExtent:F3}, Side={extents.sideExtent:F3}, Up={extents.upExtent:F3}, " +
+ $"ForwardAxis={convention.ForwardAxis}, UpAxis={convention.UpAxis}, 角度={_objectRotationCorrection}");
+ return extents.upExtent;
+ }
+
+ return GetAnimatedObjectHeight();
+ }
+
private bool TryCreatePlanarPathRotationFromHostForward(Vector3D hostForward, out Rotation3D rotation)
{
rotation = Rotation3D.Identity;
diff --git a/src/UI/WPF/ViewModels/AnimationControlViewModel.cs b/src/UI/WPF/ViewModels/AnimationControlViewModel.cs
index 635d6b2..da3b565 100644
--- a/src/UI/WPF/ViewModels/AnimationControlViewModel.cs
+++ b/src/UI/WPF/ViewModels/AnimationControlViewModel.cs
@@ -1713,9 +1713,11 @@ namespace NavisworksTransport.UI.WPF.ViewModels
_pathAnimationManager?.SetRealObjectDimensions(objectLength, objectWidth, objectHeight);
LogManager.Debug($"[选择物体] 保存原始尺寸: 长度={_objectOriginalLength:F2}m, 宽度={_objectOriginalWidth:F2}m, 高度={_objectOriginalHeight:F2}m");
- // 3. 设置新物体(会触发UpdatePassageSpaceVisualization)
- SelectedAnimatedObject = newObject;
+ // 3. 先注册到动画管理器,再设置 SelectedAnimatedObject。
+ // SelectedAnimatedObject 的 setter 会立即触发 MoveAnimatedObjectToPathStart();
+ // 如果先移动、后 SetAnimatedObject,会把“已摆到起点后的当前姿态”再次当成参考姿态缓存。
_pathAnimationManager?.SetAnimatedObject(newObject);
+ SelectedAnimatedObject = newObject;
LogManager.Info($"已选择移动物体: {SelectedAnimatedObject.DisplayName}");
// 选择实体物体意味着切换到实体模式,避免后续起点同步仍走虚拟物体分支。
@@ -2160,16 +2162,18 @@ namespace NavisworksTransport.UI.WPF.ViewModels
LogManager.Info("已清除PathAnimationManager中的动画数据并归位物体");
}
- // 2. 重置角度修正值为0
+ // 2. 先清空选择,避免角度修正归零时又触发“移动到起点”链路。
+ SelectedAnimatedObject = null;
+
+ // 3. 重置角度修正值为0
ObjectRotationCorrection = LocalEulerRotationCorrection.Zero;
LogManager.Debug("[清除物体] 已重置角度修正值为0");
- // 3. 重置属性
- SelectedAnimatedObject = null;
+ // 4. 重置属性
UpdateAnimatedObjectInfo();
UpdateCanGenerateAnimation();
- // 4. 清理高亮
+ // 5. 清理高亮
ModelHighlightHelper.ClearCollisionHighlights();
LogManager.Info("移动物体已完全清除并归位");
@@ -4339,6 +4343,20 @@ namespace NavisworksTransport.UI.WPF.ViewModels
upSize = _objectOriginalHeight * metersToUnits;
var adapter = CoordinateSystemManager.Instance.CreateHostAdapter();
+ if (CurrentPathRoute?.PathType == NavisworksTransport.PathType.Rail &&
+ _pathAnimationManager != null &&
+ _pathAnimationManager.TryGetCurrentRouteRealObjectRailProjectedExtents(
+ out double resolvedForward,
+ out double resolvedSide,
+ out double resolvedUp))
+ {
+ double unitsToMetersForRail = UnitsConverter.GetUnitsToMetersConversionFactor();
+ LogManager.Debug(
+ $"[角度修正] Rail真实物体复用路径姿态尺寸: Forward={resolvedForward * unitsToMetersForRail:F2}m, " +
+ $"Side={resolvedSide * unitsToMetersForRail:F2}m, Up={resolvedUp * unitsToMetersForRail:F2}m, 角度={_objectRotationCorrection}");
+ return (resolvedForward, resolvedSide, resolvedUp);
+ }
+
axisConvention = CurrentPathRoute?.PathType == NavisworksTransport.PathType.Rail
? ModelAxisConvention.CreateRailAssetConvention()
: ModelAxisConvention.CreateDefaultForHost(adapter.HostType);
@@ -4442,9 +4460,9 @@ namespace NavisworksTransport.UI.WPF.ViewModels
else if (SelectedAnimatedObject != null)
{
// 根据路径类型确定通行空间的尺寸
- if (CurrentPathRoute.PathType == NavisworksTransport.PathType.Rail || CurrentPathRoute.PathType == NavisworksTransport.PathType.Hoisting)
+ if (CurrentPathRoute.PathType == NavisworksTransport.PathType.Hoisting)
{
- // 空中路径(空轨或吊装):
+ // 吊装路径:
// 高度上下都加间隙
passageAcrossPath = effectiveWidth + 2 * safetyMargin; // 旋转后宽度 + 2*间隙(垂直于路径方向)
passageNormalToPath = effectiveHeight + 2 * safetyMargin; // 局部up方向高度 + 2*间隙(法线方向)
@@ -4467,7 +4485,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
passageNormalToPathHorizontal = passageNormalToPath;
LogManager.Debug($"[通行空间可视化] 地面路径使用物体尺寸 ({SelectedAnimatedObject.DisplayName}): 有效长度={effectiveLength / metersToUnitsPassage:F2}m, 有效宽度={effectiveWidth / metersToUnitsPassage:F2}m, 有效高度={effectiveHeight / metersToUnitsPassage:F2}m, 通行空间沿路径={passageAlongPath / metersToUnitsPassage:F2}m, 垂直路径={passageAcrossPath / metersToUnitsPassage:F2}m, 法线={passageNormalToPath / metersToUnitsPassage:F2}m");
}
- else // Rail
+ else if (CurrentPathRoute.PathType == NavisworksTransport.PathType.Rail)
{
// 空轨路径(可能有坡度):
// 物体的长度方向(X轴,前进方向)朝向路径方向
@@ -4482,6 +4500,14 @@ namespace NavisworksTransport.UI.WPF.ViewModels
passageNormalToPathHorizontal = passageNormalToPath;
LogManager.Debug($"[通行空间可视化] 空轨路径使用物体尺寸 ({SelectedAnimatedObject.DisplayName}): 有效长度={effectiveLength / metersToUnitsPassage:F2}m, 有效宽度={effectiveWidth / metersToUnitsPassage:F2}m, 有效高度={effectiveHeight / metersToUnitsPassage:F2}m, 通行空间沿路径={passageAlongPath / metersToUnitsPassage:F2}m, 垂直路径={passageAcrossPath / metersToUnitsPassage:F2}m, 法线={passageNormalToPath / metersToUnitsPassage:F2}m");
}
+ else
+ {
+ passageAcrossPath = effectiveWidth + 2 * safetyMargin;
+ passageNormalToPath = effectiveHeight + safetyMargin;
+ passageAlongPath = effectiveLength;
+ passageNormalToPathVertical = passageNormalToPath;
+ passageNormalToPathHorizontal = passageNormalToPath;
+ }
}
else
{
diff --git a/src/UI/WPF/ViewModels/SystemManagementViewModel.cs b/src/UI/WPF/ViewModels/SystemManagementViewModel.cs
index 7695501..a2c1ba7 100644
--- a/src/UI/WPF/ViewModels/SystemManagementViewModel.cs
+++ b/src/UI/WPF/ViewModels/SystemManagementViewModel.cs
@@ -28,6 +28,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
private readonly UIStateManager _uiStateManager;
private readonly HashSet _documentRotationHintShown = new HashSet(StringComparer.OrdinalIgnoreCase);
+ private readonly HashSet _documentRotationHintPending = new HashSet(StringComparer.OrdinalIgnoreCase);
// 日志管理字段
private ObservableCollection _logLevels;
@@ -506,7 +507,9 @@ namespace NavisworksTransport.UI.WPF.ViewModels
private void ShowRootModelRotationHintIfNeeded()
{
string documentKey = GetCurrentDocumentKey();
- if (string.IsNullOrWhiteSpace(documentKey) || _documentRotationHintShown.Contains(documentKey))
+ if (string.IsNullOrWhiteSpace(documentKey) ||
+ _documentRotationHintShown.Contains(documentKey) ||
+ _documentRotationHintPending.Contains(documentKey))
{
return;
}
@@ -522,13 +525,51 @@ namespace NavisworksTransport.UI.WPF.ViewModels
return;
}
- _documentRotationHintShown.Add(documentKey);
- LogManager.Info($"[模型整体旋转提示] {hintMessage}");
- System.Windows.MessageBox.Show(
- hintMessage,
- "坐标系提示",
- System.Windows.MessageBoxButton.OK,
- System.Windows.MessageBoxImage.Information);
+ _documentRotationHintPending.Add(documentKey);
+ _ = Task.Run(async () =>
+ {
+ try
+ {
+ await Task.Delay(1500).ConfigureAwait(false);
+ _uiStateManager?.QueueUIUpdate(() =>
+ {
+ try
+ {
+ string currentDocumentKey = GetCurrentDocumentKey();
+ if (!string.Equals(documentKey, currentDocumentKey, StringComparison.OrdinalIgnoreCase))
+ {
+ _documentRotationHintPending.Remove(documentKey);
+ return;
+ }
+
+ if (_documentRotationHintShown.Contains(documentKey))
+ {
+ _documentRotationHintPending.Remove(documentKey);
+ return;
+ }
+
+ _documentRotationHintShown.Add(documentKey);
+ _documentRotationHintPending.Remove(documentKey);
+ LogManager.Info($"[模型整体旋转提示] {hintMessage}");
+ System.Windows.MessageBox.Show(
+ hintMessage,
+ "坐标系提示",
+ System.Windows.MessageBoxButton.OK,
+ System.Windows.MessageBoxImage.Information);
+ }
+ catch (Exception ex)
+ {
+ _documentRotationHintPending.Remove(documentKey);
+ LogManager.Warning($"[模型整体旋转提示] 延后弹出失败: {ex.Message}");
+ }
+ });
+ }
+ catch (Exception ex)
+ {
+ _documentRotationHintPending.Remove(documentKey);
+ LogManager.Warning($"[模型整体旋转提示] 延后任务失败: {ex.Message}");
+ }
+ });
}
private static bool TryGetRootModelRotationHint(Document doc, out string hintMessage)
diff --git a/src/Utils/CoordinateSystem/RealObjectRailAxisConventionResolver.cs b/src/Utils/CoordinateSystem/RealObjectRailAxisConventionResolver.cs
new file mode 100644
index 0000000..6bf4f7c
--- /dev/null
+++ b/src/Utils/CoordinateSystem/RealObjectRailAxisConventionResolver.cs
@@ -0,0 +1,132 @@
+using System;
+using System.Numerics;
+
+namespace NavisworksTransport.Utils.CoordinateSystem
+{
+ ///
+ /// 根据“已解释为当前宿主语义”的真实物体参考姿态,解析 Rail 路径应使用的局部轴约定。
+ ///
+ /// 语义:
+ /// - 真实物体没有资产坐标系,但 fragment 参考姿态经过 Fragment默认Up 解释后,
+ /// 可以得到“当前宿主语义下”的真实参考三轴。
+ /// - Rail 与 Ground/Hoisting 的统一点在于:都先解释对象姿态,再进入路径框架。
+ /// - Rail 的特殊性只在路径框架:forward 来自 rail 切向,up 来自 rail 法向。
+ /// - 本解析器只负责回答:对这个真实物体来说,哪个局部 forward 轴最接近当前路径 forward;
+ /// up 轴始终采用当前宿主语义对应的轴族(YUp => +Y,ZUp => +Z)。
+ ///
+ public static class RealObjectRailAxisConventionResolver
+ {
+ private const float AxisEpsilon = 1e-6f;
+
+ public static bool TryResolve(
+ Vector3 referenceAxisX,
+ Vector3 referenceAxisY,
+ Vector3 referenceAxisZ,
+ Vector3 desiredForward,
+ CoordinateSystemType hostType,
+ out ModelAxisConvention convention,
+ out LocalAxisDirection selectedForwardAxis,
+ out Vector3 selectedForwardWorldAxis)
+ {
+ convention = null;
+ selectedForwardAxis = LocalAxisDirection.PositiveX;
+ selectedForwardWorldAxis = Vector3.Zero;
+
+ if (!TryNormalize(desiredForward, out Vector3 normalizedForward))
+ {
+ return false;
+ }
+
+ if (!TryNormalize(referenceAxisX, out Vector3 axisX) ||
+ !TryNormalize(referenceAxisY, out Vector3 axisY) ||
+ !TryNormalize(referenceAxisZ, out Vector3 axisZ))
+ {
+ return false;
+ }
+
+ LocalAxisDirection semanticUpAxis =
+ hostType == CoordinateSystemType.YUp
+ ? LocalAxisDirection.PositiveY
+ : LocalAxisDirection.PositiveZ;
+
+ Candidate best = Candidate.Invalid;
+ foreach (Candidate candidate in EnumerateForwardCandidates(axisX, axisY, axisZ, semanticUpAxis))
+ {
+ float score = Vector3.Dot(candidate.WorldAxis, normalizedForward);
+ if (best.IsInvalid || score > best.Score)
+ {
+ best = new Candidate(candidate.Axis, candidate.WorldAxis, score);
+ }
+ }
+
+ if (best.IsInvalid)
+ {
+ return false;
+ }
+
+ convention = new ModelAxisConvention(best.Axis, semanticUpAxis);
+ selectedForwardAxis = best.Axis;
+ selectedForwardWorldAxis = best.WorldAxis;
+ return true;
+ }
+
+ private static Candidate[] EnumerateForwardCandidates(
+ Vector3 axisX,
+ Vector3 axisY,
+ Vector3 axisZ,
+ LocalAxisDirection semanticUpAxis)
+ {
+ switch (semanticUpAxis)
+ {
+ case LocalAxisDirection.PositiveY:
+ return new[]
+ {
+ new Candidate(LocalAxisDirection.PositiveX, axisX, 0f),
+ new Candidate(LocalAxisDirection.NegativeX, -axisX, 0f),
+ new Candidate(LocalAxisDirection.PositiveZ, axisZ, 0f),
+ new Candidate(LocalAxisDirection.NegativeZ, -axisZ, 0f)
+ };
+
+ case LocalAxisDirection.PositiveZ:
+ default:
+ return new[]
+ {
+ new Candidate(LocalAxisDirection.PositiveX, axisX, 0f),
+ new Candidate(LocalAxisDirection.NegativeX, -axisX, 0f),
+ new Candidate(LocalAxisDirection.PositiveY, axisY, 0f),
+ new Candidate(LocalAxisDirection.NegativeY, -axisY, 0f)
+ };
+ }
+ }
+
+ private static bool TryNormalize(Vector3 value, out Vector3 normalized)
+ {
+ if (value.LengthSquared() < AxisEpsilon)
+ {
+ normalized = Vector3.Zero;
+ return false;
+ }
+
+ normalized = Vector3.Normalize(value);
+ return true;
+ }
+
+ private readonly struct Candidate
+ {
+ public static Candidate Invalid => new Candidate(LocalAxisDirection.PositiveX, Vector3.Zero, float.MinValue);
+
+ public LocalAxisDirection Axis { get; }
+ public Vector3 WorldAxis { get; }
+ public float Score { get; }
+
+ public bool IsInvalid => Score == float.MinValue;
+
+ public Candidate(LocalAxisDirection axis, Vector3 worldAxis, float score)
+ {
+ Axis = axis;
+ WorldAxis = worldAxis;
+ Score = score;
+ }
+ }
+ }
+}
diff --git a/src/Utils/CoordinateSystem/RealObjectRailExtentResolver.cs b/src/Utils/CoordinateSystem/RealObjectRailExtentResolver.cs
new file mode 100644
index 0000000..2406a20
--- /dev/null
+++ b/src/Utils/CoordinateSystem/RealObjectRailExtentResolver.cs
@@ -0,0 +1,77 @@
+using System;
+using System.Numerics;
+
+namespace NavisworksTransport.Utils.CoordinateSystem
+{
+ ///
+ /// 真实物体在 Rail 路径下的尺寸语义解析。
+ /// 先根据解释后的真实参考姿态解析 rail 轴约定,再使用:
+ /// 1. rail 基姿态(零角度时对象与路径框架对齐后的宿主姿态)
+ /// 2. 最终姿态(rail 基姿态再叠加宿主轴角度修正)
+ /// 共同计算沿 rail forward/side/normal 的有效尺寸。
+ ///
+ /// 关键点:
+ /// - 这里不能只吃“角度修正”本身;否则双轴旋转时会漏掉 rail 基姿态语义。
+ /// - 目标语义轴来自 rail 基姿态,而不是宿主世界轴。
+ ///
+ public static class RealObjectRailExtentResolver
+ {
+ public static bool TryResolveProjectedSemanticExtents(
+ Vector3 referenceAxisX,
+ Vector3 referenceAxisY,
+ Vector3 referenceAxisZ,
+ Vector3 desiredForward,
+ CoordinateSystemType hostType,
+ double forwardSize,
+ double sideSize,
+ double upSize,
+ Quaternion baselineHostQuaternion,
+ Quaternion finalHostQuaternion,
+ out ModelAxisConvention convention,
+ out (double forwardExtent, double sideExtent, double upExtent) extents)
+ {
+ convention = null;
+ extents = (0.0, 0.0, 0.0);
+
+ if (!RealObjectRailAxisConventionResolver.TryResolve(
+ referenceAxisX,
+ referenceAxisY,
+ referenceAxisZ,
+ desiredForward,
+ hostType,
+ out convention,
+ out _,
+ out _))
+ {
+ return false;
+ }
+
+ Vector3 localSize = convention.CreateScaleVector3(forwardSize, sideSize, upSize);
+ Vector3 rotatedLocalX = Vector3.Normalize(Vector3.Transform(Vector3.UnitX, finalHostQuaternion));
+ Vector3 rotatedLocalY = Vector3.Normalize(Vector3.Transform(Vector3.UnitY, finalHostQuaternion));
+ Vector3 rotatedLocalZ = Vector3.Normalize(Vector3.Transform(Vector3.UnitZ, finalHostQuaternion));
+
+ Vector3 targetForward = Vector3.Normalize(Vector3.Transform(convention.ForwardUnitVector, baselineHostQuaternion));
+ Vector3 targetUp = Vector3.Normalize(Vector3.Transform(convention.UpUnitVector, baselineHostQuaternion));
+ Vector3 targetSide = Vector3.Normalize(Vector3.Cross(targetForward, targetUp));
+
+ extents = (
+ ProjectExtent(localSize, rotatedLocalX, rotatedLocalY, rotatedLocalZ, targetForward),
+ ProjectExtent(localSize, rotatedLocalX, rotatedLocalY, rotatedLocalZ, targetSide),
+ ProjectExtent(localSize, rotatedLocalX, rotatedLocalY, rotatedLocalZ, targetUp));
+ return true;
+ }
+
+ private static double ProjectExtent(
+ Vector3 localSize,
+ Vector3 rotatedLocalX,
+ Vector3 rotatedLocalY,
+ Vector3 rotatedLocalZ,
+ Vector3 targetAxis)
+ {
+ return Math.Abs(Vector3.Dot(rotatedLocalX, targetAxis)) * localSize.X +
+ Math.Abs(Vector3.Dot(rotatedLocalY, targetAxis)) * localSize.Y +
+ Math.Abs(Vector3.Dot(rotatedLocalZ, targetAxis)) * localSize.Z;
+ }
+ }
+}