From 138eb43a67ecdbf42abfa65763d7a8304ad2e763 Mon Sep 17 00:00:00 2001 From: tian <11429339@qq.com> Date: Wed, 25 Mar 2026 00:11:07 +0800 Subject: [PATCH] Unify real-object rail pose interpretation --- AGENTS.md | 27 ++ NavisworksTransport.UnitTests.csproj | 2 + TransportPlugin.csproj | 2 + .../HostCoordinateAdapterTests.cs | 57 ++++ ...alObjectRailAxisConventionResolverTests.cs | 89 ++++++ .../RealObjectRailExtentResolverTests.cs | 142 ++++++++++ doc/working/current-engineering-state.md | 51 +++- src/Core/Animation/PathAnimationManager.cs | 255 ++++++++++++++++-- .../ViewModels/AnimationControlViewModel.cs | 44 ++- .../ViewModels/SystemManagementViewModel.cs | 57 +++- .../RealObjectRailAxisConventionResolver.cs | 132 +++++++++ .../RealObjectRailExtentResolver.cs | 77 ++++++ 12 files changed, 901 insertions(+), 34 deletions(-) create mode 100644 UnitTests/CoordinateSystem/RealObjectRailAxisConventionResolverTests.cs create mode 100644 UnitTests/CoordinateSystem/RealObjectRailExtentResolverTests.cs create mode 100644 src/Utils/CoordinateSystem/RealObjectRailAxisConventionResolver.cs create mode 100644 src/Utils/CoordinateSystem/RealObjectRailExtentResolver.cs 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; + } + } +}