From 3fd184934bc72d2e59aeac6e7e5067b4cc3a6f9d Mon Sep 17 00:00:00 2001 From: tian <11429339@qq.com> Date: Sat, 21 Mar 2026 06:10:34 +0800 Subject: [PATCH] Establish canonical coordinate framework with tests --- NavisworksTransport.UnitTests.csproj | 69 +++ TransportPlugin.csproj | 7 + TransportPlugin.sln | 10 + .../CanonicalRailPoseBuilderTests.cs | 92 ++++ .../HostCoordinateAdapterTests.cs | 69 +++ .../ModelAxisConventionTests.cs | 87 +++ .../ProjectReferenceFrameTests.cs | 38 ++ UnitTests/Properties/AssemblyInfo.cs | 15 + ...oordinate-system-canonical-space-design.md | 503 ++++++++++++++++++ doc/guide/design_principles.md | 292 ++++++++++ ...tem-canonical-space-implementation-plan.md | 362 +++++++++++++ run-unit-tests.bat | 50 ++ src/Core/Animation/PathAnimationManager.cs | 42 +- src/Core/PathPointRenderPlugin.cs | 171 +++--- src/PathPlanning/ChannelBasedGridBuilder.cs | 6 +- src/UI/WPF/ViewModels/PathEditingViewModel.cs | 114 +++- src/UI/WPF/Views/PathEditingView.xaml | 66 ++- .../CoordinateSystem/CanonicalBounds3.cs | 20 + .../CanonicalRailPoseBuilder.cs | 83 +++ .../CoordinateSystemManager.cs | 35 ++ .../CoordinateSystem/HostCoordinateAdapter.cs | 274 ++++++++++ .../CoordinateSystem/LocalAxisDirection.cs | 18 + .../CoordinateSystem/ModelAxisConvention.cs | 233 ++++++++ .../CoordinateSystem/ProjectReferenceFrame.cs | 97 ++++ src/Utils/ModelItemTransformHelper.cs | 53 ++ src/Utils/RailPathPoseHelper.cs | 179 +++++-- 26 files changed, 2809 insertions(+), 176 deletions(-) create mode 100644 NavisworksTransport.UnitTests.csproj create mode 100644 UnitTests/CoordinateSystem/CanonicalRailPoseBuilderTests.cs create mode 100644 UnitTests/CoordinateSystem/HostCoordinateAdapterTests.cs create mode 100644 UnitTests/CoordinateSystem/ModelAxisConventionTests.cs create mode 100644 UnitTests/CoordinateSystem/ProjectReferenceFrameTests.cs create mode 100644 UnitTests/Properties/AssemblyInfo.cs create mode 100644 doc/design/2026/coordinate-system-canonical-space-design.md create mode 100644 doc/working/coordinate-system-canonical-space-implementation-plan.md create mode 100644 run-unit-tests.bat create mode 100644 src/Utils/CoordinateSystem/CanonicalBounds3.cs create mode 100644 src/Utils/CoordinateSystem/CanonicalRailPoseBuilder.cs create mode 100644 src/Utils/CoordinateSystem/HostCoordinateAdapter.cs create mode 100644 src/Utils/CoordinateSystem/LocalAxisDirection.cs create mode 100644 src/Utils/CoordinateSystem/ModelAxisConvention.cs create mode 100644 src/Utils/CoordinateSystem/ProjectReferenceFrame.cs diff --git a/NavisworksTransport.UnitTests.csproj b/NavisworksTransport.UnitTests.csproj new file mode 100644 index 0000000..c58c383 --- /dev/null +++ b/NavisworksTransport.UnitTests.csproj @@ -0,0 +1,69 @@ + + + + + Debug + x64 + {7DDB41A4-A10B-4EA4-A658-0D5A9178A1F5} + Library + Properties + NavisworksTransport.UnitTests + NavisworksTransport.UnitTests + v4.8 + 512 + true + + + true + bin\x64\Debug\ + DEBUG;TRACE + full + x64 + 7.3 + prompt + + + bin\x64\Release\ + TRACE + true + pdbonly + x64 + 7.3 + prompt + + + + ..\..\..\..\Program Files\Autodesk\Navisworks Manage 2026\Autodesk.Navisworks.Api.dll + True + + + packages\MSTest.TestFramework.3.0.4\lib\net462\Microsoft.VisualStudio.TestPlatform.TestFramework.dll + + + packages\MSTest.TestFramework.3.0.4\lib\net462\Microsoft.VisualStudio.TestPlatform.TestFramework.Extensions.dll + + + + + + + + + + + + + + + + + + + + + {1A0124F6-3DEB-4153-8760-F568AD9393EE} + TransportPlugin + + + + diff --git a/TransportPlugin.csproj b/TransportPlugin.csproj index 79d391a..9db0354 100644 --- a/TransportPlugin.csproj +++ b/TransportPlugin.csproj @@ -63,6 +63,7 @@ + packages\Newtonsoft.Json.13.0.3\lib\net45\Newtonsoft.Json.dll @@ -330,6 +331,12 @@ + + + + + + diff --git a/TransportPlugin.sln b/TransportPlugin.sln index 7b0594e..3f2571b 100644 --- a/TransportPlugin.sln +++ b/TransportPlugin.sln @@ -5,6 +5,8 @@ VisualStudioVersion = 17.14.36203.30 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TransportPlugin", "TransportPlugin.csproj", "{1A0124F6-3DEB-4153-8760-F568AD9393EE}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NavisworksTransport.UnitTests", "NavisworksTransport.UnitTests.csproj", "{7DDB41A4-A10B-4EA4-A658-0D5A9178A1F5}" +EndProject Project("{54435603-DBB4-11D2-8724-00A0C9A8B90C}") = "TransportPlugin.Setup", "..\TransportPlugin.Setup\TransportPlugin.Setup.vdproj", "{E1955F72-A686-9398-1C6A-936493D9211F}" EndProject Global @@ -23,6 +25,14 @@ Global {1A0124F6-3DEB-4153-8760-F568AD9393EE}.Release|Any CPU.Build.0 = Release|Any CPU {1A0124F6-3DEB-4153-8760-F568AD9393EE}.Release|x64.ActiveCfg = Release|x64 {1A0124F6-3DEB-4153-8760-F568AD9393EE}.Release|x64.Build.0 = Release|x64 + {7DDB41A4-A10B-4EA4-A658-0D5A9178A1F5}.Debug|Any CPU.ActiveCfg = Debug|x64 + {7DDB41A4-A10B-4EA4-A658-0D5A9178A1F5}.Debug|Any CPU.Build.0 = Debug|x64 + {7DDB41A4-A10B-4EA4-A658-0D5A9178A1F5}.Debug|x64.ActiveCfg = Debug|x64 + {7DDB41A4-A10B-4EA4-A658-0D5A9178A1F5}.Debug|x64.Build.0 = Debug|x64 + {7DDB41A4-A10B-4EA4-A658-0D5A9178A1F5}.Release|Any CPU.ActiveCfg = Release|x64 + {7DDB41A4-A10B-4EA4-A658-0D5A9178A1F5}.Release|Any CPU.Build.0 = Release|x64 + {7DDB41A4-A10B-4EA4-A658-0D5A9178A1F5}.Release|x64.ActiveCfg = Release|x64 + {7DDB41A4-A10B-4EA4-A658-0D5A9178A1F5}.Release|x64.Build.0 = Release|x64 {E1955F72-A686-9398-1C6A-936493D9211F}.Debug|Any CPU.ActiveCfg = Debug {E1955F72-A686-9398-1C6A-936493D9211F}.Debug|x64.ActiveCfg = Debug {E1955F72-A686-9398-1C6A-936493D9211F}.Debug|x64.Build.0 = Debug diff --git a/UnitTests/CoordinateSystem/CanonicalRailPoseBuilderTests.cs b/UnitTests/CoordinateSystem/CanonicalRailPoseBuilderTests.cs new file mode 100644 index 0000000..b337260 --- /dev/null +++ b/UnitTests/CoordinateSystem/CanonicalRailPoseBuilderTests.cs @@ -0,0 +1,92 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using NavisworksTransport.Utils.CoordinateSystem; +using System.Numerics; + +namespace NavisworksTransport.UnitTests.CoordinateSystem +{ + [TestClass] + public class CanonicalRailPoseBuilderTests + { + [TestMethod] + public void StraightCanonicalPath_ZUpConvention_ShouldProduceIdentityLikeBasis() + { + bool ok = CanonicalRailPoseBuilder.TryCreateQuaternion( + new Vector3(0, 0, 0), + new Vector3(1, 0, 0), + new Vector3(2, 0, 0), + Vector3.UnitZ, + ModelAxisConvention.CreateDefaultForHost(CoordinateSystemType.ZUp), + out Quaternion rotation); + + Assert.IsTrue(ok); + Matrix4x4 linear = Matrix4x4.CreateFromQuaternion(rotation); + AssertColumn(linear, 0, 1, 0, 0); + AssertColumn(linear, 1, 0, 1, 0); + AssertColumn(linear, 2, 0, 0, 1); + } + + [TestMethod] + public void StraightCanonicalPath_YUpConvention_ShouldMapLocalYToCanonicalUp() + { + bool ok = CanonicalRailPoseBuilder.TryCreateQuaternion( + new Vector3(0, 0, 0), + new Vector3(1, 0, 0), + new Vector3(2, 0, 0), + Vector3.UnitZ, + ModelAxisConvention.CreateDefaultForHost(CoordinateSystemType.YUp), + out Quaternion rotation); + + Assert.IsTrue(ok); + Matrix4x4 linear = Matrix4x4.CreateFromQuaternion(rotation); + AssertColumn(linear, 0, 1, 0, 0); + AssertColumn(linear, 1, 0, 0, 1); + AssertColumn(linear, 2, 0, -1, 0); + } + + [TestMethod] + public void SlopedPath_ShouldKeepForwardAlignedWithPathAndUpOrthogonalized() + { + bool ok = CanonicalRailPoseBuilder.TryCreateBasis( + new Vector3(0, 0, 0), + new Vector3(1, 1, 1), + new Vector3(2, 2, 2), + Vector3.UnitZ, + out Vector3 forward, + out Vector3 lateral, + out Vector3 up); + + Assert.IsTrue(ok); + Assert.AreEqual(1.0, forward.Length(), 1e-6); + Assert.AreEqual(1.0, lateral.Length(), 1e-6); + Assert.AreEqual(1.0, up.Length(), 1e-6); + Assert.AreEqual(0.0, Vector3.Dot(forward, up), 1e-6); + Assert.AreEqual(0.0, Vector3.Dot(forward, lateral), 1e-6); + Assert.AreEqual(0.0, Vector3.Dot(lateral, up), 1e-6); + } + + private static void AssertColumn(Matrix4x4 matrix, int column, double x, double y, double z) + { + switch (column) + { + case 0: + Assert.AreEqual(x, matrix.M11, 1e-6); + Assert.AreEqual(y, matrix.M21, 1e-6); + Assert.AreEqual(z, matrix.M31, 1e-6); + break; + case 1: + Assert.AreEqual(x, matrix.M12, 1e-6); + Assert.AreEqual(y, matrix.M22, 1e-6); + Assert.AreEqual(z, matrix.M32, 1e-6); + break; + case 2: + Assert.AreEqual(x, matrix.M13, 1e-6); + Assert.AreEqual(y, matrix.M23, 1e-6); + Assert.AreEqual(z, matrix.M33, 1e-6); + break; + default: + Assert.Fail("Only first 3 columns are valid."); + break; + } + } + } +} diff --git a/UnitTests/CoordinateSystem/HostCoordinateAdapterTests.cs b/UnitTests/CoordinateSystem/HostCoordinateAdapterTests.cs new file mode 100644 index 0000000..0a97569 --- /dev/null +++ b/UnitTests/CoordinateSystem/HostCoordinateAdapterTests.cs @@ -0,0 +1,69 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using NavisworksTransport.Utils.CoordinateSystem; +using System.Numerics; + +namespace NavisworksTransport.UnitTests.CoordinateSystem +{ + [TestClass] + public class HostCoordinateAdapterTests + { + [TestMethod] + public void ZUp_PointRoundTrip_ShouldRemainUnchanged() + { + var adapter = new HostCoordinateAdapter(CoordinateSystemType.ZUp); + var hostPoint = new Vector3(1.5f, -2.5f, 3.5f); + + Vector3 canonicalPoint = adapter.ToCanonicalPoint3(hostPoint); + Vector3 roundTripPoint = adapter.FromCanonicalPoint3(canonicalPoint); + + AssertPoint(roundTripPoint, 1.5, -2.5, 3.5); + } + + [TestMethod] + public void YUp_PointAndVectorRoundTrip_ShouldMatchExpectedMapping() + { + var adapter = new HostCoordinateAdapter(CoordinateSystemType.YUp); + var hostPoint = new Vector3(1.0f, 2.0f, 3.0f); + var hostVector = new Vector3(4.0f, 5.0f, 6.0f); + + Vector3 canonicalPoint = adapter.ToCanonicalPoint3(hostPoint); + Vector3 canonicalVector = adapter.ToCanonicalVector3(hostVector); + + AssertPoint(canonicalPoint, 1.0, -3.0, 2.0); + AssertVector(canonicalVector, 4.0, -6.0, 5.0); + + Vector3 restoredPoint = adapter.FromCanonicalPoint3(canonicalPoint); + Vector3 restoredVector = adapter.FromCanonicalVector3(canonicalVector); + + AssertPoint(restoredPoint, 1.0, 2.0, 3.0); + AssertVector(restoredVector, 4.0, 5.0, 6.0); + } + + [TestMethod] + public void YUp_BoundsRoundTrip_ShouldPreserveBounds() + { + var adapter = new HostCoordinateAdapter(CoordinateSystemType.YUp); + var hostBounds = new CanonicalBounds3(new Vector3(-2.0f, 1.0f, 3.0f), new Vector3(5.0f, 7.0f, 11.0f)); + + CanonicalBounds3 canonicalBounds = adapter.ToCanonicalBounds3(hostBounds); + CanonicalBounds3 restoredBounds = adapter.FromCanonicalBounds3(canonicalBounds); + + AssertPoint(restoredBounds.Min, -2.0, 1.0, 3.0); + AssertPoint(restoredBounds.Max, 5.0, 7.0, 11.0); + } + + private static void AssertPoint(Vector3 point, double x, double y, double z) + { + Assert.AreEqual(x, point.X, 1e-9); + Assert.AreEqual(y, point.Y, 1e-9); + Assert.AreEqual(z, point.Z, 1e-9); + } + + private static void AssertVector(Vector3 vector, double x, double y, double z) + { + Assert.AreEqual(x, vector.X, 1e-9); + Assert.AreEqual(y, vector.Y, 1e-9); + Assert.AreEqual(z, vector.Z, 1e-9); + } + } +} diff --git a/UnitTests/CoordinateSystem/ModelAxisConventionTests.cs b/UnitTests/CoordinateSystem/ModelAxisConventionTests.cs new file mode 100644 index 0000000..74f16e0 --- /dev/null +++ b/UnitTests/CoordinateSystem/ModelAxisConventionTests.cs @@ -0,0 +1,87 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using NavisworksTransport.Utils.CoordinateSystem; +using System.Numerics; + +namespace NavisworksTransport.UnitTests.CoordinateSystem +{ + [TestClass] + public class ModelAxisConventionTests + { + [TestMethod] + public void DefaultForYUp_ShouldUseXForwardAndYUp() + { + ModelAxisConvention convention = ModelAxisConvention.CreateDefaultForHost(CoordinateSystemType.YUp); + + Assert.AreEqual(LocalAxisDirection.PositiveX, convention.ForwardAxis); + Assert.AreEqual(LocalAxisDirection.PositiveY, convention.UpAxis); + } + + [TestMethod] + public void DefaultForZUp_ShouldUseXForwardAndZUp() + { + ModelAxisConvention convention = ModelAxisConvention.CreateDefaultForHost(CoordinateSystemType.ZUp); + + Assert.AreEqual(LocalAxisDirection.PositiveX, convention.ForwardAxis); + Assert.AreEqual(LocalAxisDirection.PositiveZ, convention.UpAxis); + } + + [TestMethod] + public void YUpConvention_CreateLinearTransform_ShouldMapLocalYToWorldUp() + { + var convention = new ModelAxisConvention(LocalAxisDirection.PositiveX, LocalAxisDirection.PositiveY); + Matrix4x4 linear = convention.CreateLinearTransform3(new Vector3(1, 0, 0), new Vector3(0, 0, 1)); + + AssertColumn(linear, 0, 1, 0, 0); + AssertColumn(linear, 1, 0, 0, 1); + AssertColumn(linear, 2, 0, -1, 0); + } + + [TestMethod] + public void ZUpConvention_CreateLinearTransform_ShouldMapLocalZToWorldUp() + { + var convention = new ModelAxisConvention(LocalAxisDirection.PositiveX, LocalAxisDirection.PositiveZ); + Matrix4x4 linear = convention.CreateLinearTransform3(new Vector3(1, 0, 0), new Vector3(0, 0, 1)); + + AssertColumn(linear, 0, 1, 0, 0); + AssertColumn(linear, 1, 0, 1, 0); + AssertColumn(linear, 2, 0, 0, 1); + } + + [TestMethod] + public void YUpConvention_CreateRotation_ShouldAlignLocalAxesToRequestedWorldAxes() + { + var convention = new ModelAxisConvention(LocalAxisDirection.PositiveX, LocalAxisDirection.PositiveY); + Quaternion rotation = convention.CreateQuaternion(new Vector3(0, 1, 0), new Vector3(0, 0, 1)); + Matrix4x4 linear = Matrix4x4.CreateFromQuaternion(rotation); + + AssertColumn(linear, 0, 0, 1, 0); + AssertColumn(linear, 1, 0, 0, 1); + AssertColumn(linear, 2, 1, 0, 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-6); + Assert.AreEqual(y, matrix.M21, 1e-6); + Assert.AreEqual(z, matrix.M31, 1e-6); + break; + case 1: + Assert.AreEqual(x, matrix.M12, 1e-6); + Assert.AreEqual(y, matrix.M22, 1e-6); + Assert.AreEqual(z, matrix.M32, 1e-6); + break; + case 2: + Assert.AreEqual(x, matrix.M13, 1e-6); + Assert.AreEqual(y, matrix.M23, 1e-6); + Assert.AreEqual(z, matrix.M33, 1e-6); + break; + default: + Assert.Fail("Only first 3 columns are valid."); + break; + } + } + } +} diff --git a/UnitTests/CoordinateSystem/ProjectReferenceFrameTests.cs b/UnitTests/CoordinateSystem/ProjectReferenceFrameTests.cs new file mode 100644 index 0000000..755de5f --- /dev/null +++ b/UnitTests/CoordinateSystem/ProjectReferenceFrameTests.cs @@ -0,0 +1,38 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using NavisworksTransport.Utils.CoordinateSystem; +using System.Numerics; + +namespace NavisworksTransport.UnitTests.CoordinateSystem +{ + [TestClass] + public class ProjectReferenceFrameTests + { + [TestMethod] + public void DefaultForYUp_ShouldUseCanonicalUpAndYUpModelConvention() + { + ProjectReferenceFrame frame = ProjectReferenceFrame.CreateDefault(CoordinateSystemType.YUp); + + Assert.AreEqual(0.0, frame.SphereCenterInCanonical3.X, 1e-9); + Assert.AreEqual(0.0, frame.SphereCenterInCanonical3.Y, 1e-9); + Assert.AreEqual(0.0, frame.SphereCenterInCanonical3.Z, 1e-9); + Assert.AreEqual(0.0, frame.ProjectUpInCanonical3.X, 1e-9); + Assert.AreEqual(0.0, frame.ProjectUpInCanonical3.Y, 1e-9); + Assert.AreEqual(1.0, frame.ProjectUpInCanonical3.Z, 1e-9); + Assert.AreEqual(LocalAxisDirection.PositiveX, frame.DefaultModelAxisConvention.ForwardAxis); + Assert.AreEqual(LocalAxisDirection.PositiveY, frame.DefaultModelAxisConvention.UpAxis); + } + + [TestMethod] + public void Constructor_ShouldNormalizeProjectUp() + { + var frame = new ProjectReferenceFrame( + new Vector3(1, 2, 3), + new Vector3(0, 0, 10), + ModelAxisConvention.CreateDefaultForHost(CoordinateSystemType.ZUp)); + + Assert.AreEqual(0.0, frame.ProjectUpInCanonical3.X, 1e-9); + Assert.AreEqual(0.0, frame.ProjectUpInCanonical3.Y, 1e-9); + Assert.AreEqual(1.0, frame.ProjectUpInCanonical3.Z, 1e-9); + } + } +} diff --git a/UnitTests/Properties/AssemblyInfo.cs b/UnitTests/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..06b09da --- /dev/null +++ b/UnitTests/Properties/AssemblyInfo.cs @@ -0,0 +1,15 @@ +using System.Reflection; +using System.Runtime.InteropServices; + +[assembly: AssemblyTitle("NavisworksTransport.UnitTests")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("NavisworksTransport.UnitTests")] +[assembly: AssemblyCopyright("Copyright © 2026")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] +[assembly: ComVisible(false)] +[assembly: Guid("8f8a2fd7-c7e5-4185-95a4-740a2bf6130f")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/doc/design/2026/coordinate-system-canonical-space-design.md b/doc/design/2026/coordinate-system-canonical-space-design.md new file mode 100644 index 0000000..4d15f77 --- /dev/null +++ b/doc/design/2026/coordinate-system-canonical-space-design.md @@ -0,0 +1,503 @@ +# 坐标系统一架构设计方案 + +## 1. 背景 + +当前项目运行在 Navisworks 宿主环境中,程序直接读取和写入的都是 Navisworks 世界坐标: + +- `Point3D` +- `Vector3D` +- `BoundingBox3D` +- `Transform3D` +- 鼠标点击拾取点 +- `Document.UpVector` + +这些坐标对程序来说属于**外部坐标**。 + +项目目前的问题不是“完全没有坐标系抽象”,而是: + +- 有一部分代码已经开始抽象 `Y-up / Z-up` +- 另一部分代码仍然直接写死世界 `Z` +- 还有一部分代码把世界原点直接当作业务球心 + +结果是三种语义混在一起: + +1. Navisworks 外部坐标语义 +2. 程序内部几何计算语义 +3. 工程业务基准语义(球心、安装基准、轨道参考面) + +这会导致: + +- `Y-up` / `Z-up` 项目切换后功能不一致 +- 终端安装仿真把世界原点误当成球心 +- Rail 姿态和渲染层偷用世界 `Z` +- 不同模块对同一个点的解释不一致 + +因此,需要建立一套更成熟、更清晰的坐标系架构。 + +## 2. 设计目标 + +本方案的目标不是“到处加 `if (isYUp)`”,而是建立一个标准的分层架构: + +- Navisworks 世界坐标统一视为**外部坐标** +- 程序内部统一使用一套**规范内部坐标** +- 工程语义使用独立的**业务基准坐标** +- 坐标转换只发生在少数边界入口 +- 业务逻辑禁止直接依赖宿主坐标语义 + +## 3. 总体方案 + +### 3.1 内部统一坐标 + +程序内部统一使用: + +- **Canonical Space(规范内部坐标)** +- **固定为 `Z-up`** + +选择 `Z-up` 的原因: + +1. 当前项目大量成熟逻辑本身就是按 `Z-up` 语义构建的 +2. 动画、渲染、yaw 语义、很多 helper 都更接近 `Z-up` +3. 以 `Z-up` 作为内部标准,改造成本低于整体改成 `Y-up` + +注意: + +- 这只是程序内部选择 +- 不代表客户模型必须是 `Z-up` +- 客户仍可继续使用 `Y-up` 项目 + +### 3.2 三层坐标语义 + +```mermaid +flowchart LR + A["Navisworks Host Space\n外部坐标"] --> B["Host Coordinate Adapter\n边界适配层"] + B --> C["Canonical Space (Z-up)\n内部统一坐标"] + C --> D["Project Reference Frame\n业务基准坐标"] + + C --> E["路径规划/几何计算"] + C --> F["Rail 姿态/动画"] + C --> G["碰撞检测/结果恢复"] + C --> H["渲染/辅助线/通行空间"] + + D --> I["球心"] + D --> J["终端安装基准"] + D --> K["轨道参考面"] + + C --> B + B --> L["Navisworks 输出\n渲染/移动/截图恢复"] +``` + +三层职责如下: + +#### A. Navisworks Host Space(外部坐标) + +宿主 API 直接提供的坐标。 + +特点: + +- 是程序的输入/输出坐标 +- 可能来自 `Y-up` 项目,也可能来自 `Z-up` 项目 +- 不应直接当成内部计算坐标 + +#### B. Canonical Space(内部统一坐标) + +程序内部唯一允许进行几何计算、路径计算、姿态计算的坐标空间。 + +特点: + +- 固定为 `Z-up` +- 只解决坐标轴和方向语义统一问题 +- 不承载业务基准含义 + +#### C. Project Reference Frame(业务基准坐标) + +建立在内部统一坐标基础上的工程语义层。 + +负责表达: + +- 球心 +- 项目 up 方向的业务解释 +- 终端安装参考面 +- 轨道参考面 +- 业务锚点 + +这层不能偷用世界原点,也不能偷用宿主世界轴。 + +### 3.3 坐标系定义必须包含的语义 + +在本项目中,`Y-up` / `Z-up` 不能只被理解成“点坐标怎么换算”。 + +一个完整的坐标系定义,至少必须显式包含: + +- `UpAxis` +- `ElevationAxis` +- `HorizontalPlane` +- 必要时的 `Handedness` + +因此: + +- `Y-up` 的含义是: + - `Y` 为 up 轴 + - `Y` 为高程轴 + - `XZ` 为水平平面 +- `Z-up` 的含义是: + - `Z` 为 up 轴 + - `Z` 为高程轴 + - `XY` 为水平平面 + +后续凡是出现: + +- 高度 +- 底面 +- 顶面 +- 通行空间高度轴 +- 俯仰/法向 + +都必须基于这组定义来解释,不能继续偷用“世界 Z 就是 up”的旧假设。 + +### 3.4 模型局部轴约定是独立层 + +除了宿主坐标系和内部统一坐标系,还必须区分**模型局部轴约定**。 + +这是另一层独立定义,至少包含: + +- `LocalForwardAxis` +- `LocalUpAxis` + +例如: + +- 虚拟物体资源通常按程序约定构建,可能是 `Local X = Forward, Local Z = Up` +- 真实模型如果来自 `Y-up` 项目,则很可能是 `Local X = Forward, Local Y = Up` + +这意味着: + +- 即使宿主坐标系转换已经正确 +- 如果模型局部轴约定没有显式处理 +- 动画和姿态仍然会出现“路径对了、通行空间对了、模型自己站歪了”的现象 + +因此,后续姿态系统必须显式区分: + +1. 宿主坐标系定义 +2. 内部统一坐标系定义 +3. 业务基准定义 +4. 模型局部轴约定 + +## 4. 核心设计原则 + +### 4.1 Navisworks 世界坐标统一视为外部坐标 + +这是本方案的第一条硬规则。 + +对程序而言,以下数据统一视为外部输入: + +- `Point3D` +- `Vector3D` +- `BoundingBox3D` +- `Transform3D` +- `ModelItem.BoundingBox()` +- `Document.UpVector` +- 鼠标点击拾取点 + +无论当前 NWD/NWC 是客户原始模型,还是预先转换保存后的模型, +**只要是从 Navisworks API 读出来的,它对程序来说就是外部坐标。** + +### 4.1.1 外部坐标不等于内部语义 + +外部坐标虽然来自 Navisworks,但不能直接拿来推导内部业务语义。 + +特别是以下概念必须先经过坐标定义解释: + +- 哪个轴是 up +- 哪个轴是高程 +- 哪个平面是水平面 +- 一个 `BoundingBox` 的“底面”到底是哪一面 + +也就是说: + +- 外部坐标是输入 +- 坐标定义决定如何解释这个输入 +- 业务代码不得跳过这一步 + +### 4.2 程序内部一律只认 Canonical Space + +业务层不得直接消费 Navisworks 坐标。 + +禁止这样做: + +```csharp +// ❌ 错误:直接拿宿主包围盒结果开始做业务计算 +var bounds = item.BoundingBox(); +var center = bounds.Center; +var direction = new Vector3D(center.X, center.Y, center.Z); +``` + +正确做法应当是: + +```csharp +// ✅ 正确:先通过边界适配层转换到内部统一坐标 +var hostBounds = item.BoundingBox(); +var bounds = adapter.ToCanonicalBounds(hostBounds); +var center = bounds.Center; +``` + +### 4.3 坐标系层只解决轴变换,不解决业务语义 + +坐标适配层只负责: + +- 外部坐标到内部坐标的变换 +- 内部坐标到外部坐标的逆变换 +- up 轴、高程轴、水平面定义 + +不负责: + +- 球心是否在 `(0,0,0)` +- 终端安装是否指向球心 +- 顶面/底面对接如何定义 +- 轨道参考面在哪里 + +这些都属于业务基准层。 + +### 4.4 项目基准点必须显式配置或显式求解 + +像“球心”这类工程点不属于坐标系本身。 + +不能因为某一批模型里球心恰好在世界原点,就长期把它写死。 + +应使用以下来源之一: + +- 项目配置 +- 明确的设计资料 +- 多条向心轴线拟合 +- 其他明确的业务求解方式 + +### 4.5 不在业务代码中散落 `Y-up / Z-up` 分支 + +不推荐: + +```csharp +// ❌ 错误 +if (isYUp) +{ + ... +} +else +{ + ... +} +``` + +推荐: + +- 边界适配层统一处理 `Host -> Canonical` +- 业务层只消费 Canonical 数据 + +### 4.6 渲染几何也必须完成坐标转换 + +坐标系改造不能只停留在: + +- 点坐标转换 +- 中心点偏移 +- 业务锚点转换 + +对于以下可视化对象,还必须同步改造它们的**局部几何轴构造**: + +- 通行空间长方体 +- 辅助线杆体 +- 圆形/圆柱标记 +- 切向、法向、侧向相关的渲染面片 + +否则会出现一种典型错误: + +- 对象中心点已经在正确位置 +- 但渲染几何仍然按世界 `Z-up` 去构造 `right/up/height` +- 最终看起来仍然像 `Z-up`,即使业务点位已经是对的 + +本项目已经出现过这一类问题: + +- `Y-up` 模型中,通行空间中心点偏移已经正确 +- 但长方体本体仍然按世界 `XY + Z` 构造 +- 导致通行空间整体看起来仍是 `Z-up` + +因此,渲染层必须遵守以下规则: + +1. 凡是依赖 `up/right/forward/normal` 的渲染几何,必须基于统一坐标语义构造局部轴。 +2. 不允许只修改“中心点/偏移量”而保留旧的世界轴构造公式。 +3. 如果渲染输出面向 Navisworks 宿主,则应先在 Canonical Space 中完成几何语义计算,再转换回宿主坐标输出。 +4. 对长方体、圆柱体这类实体渲染,必须同时验证: + - 中心点是否正确 + - 局部高度轴是否正确 + - 局部侧向轴是否正确 + - 法向/截面方向是否正确 + +## 5. 推荐的技术方案 + +### 5.1 使用完整变换矩阵作为适配基础 + +不推荐继续停留在: + +- `GetElevation()` +- `GetHorizontalCoords()` +- `CreatePoint()` + +这类偏二维、局部的接口上。 + +对三维动画、Rail 姿态、碰撞恢复来说,更成熟的方案是: + +- 使用完整的空间变换对象 +- 以矩阵/旋转+平移为核心 + +即: + +- `ExternalToCanonical` +- `CanonicalToExternal` + +这两套变换应成为边界层的基础能力。 + +### 5.2 建议新增的核心组件 + +#### 1. `HostCoordinateAdapter` + +职责: + +- 统一处理 Navisworks 外部坐标到 Canonical Space 的转换 +- 统一处理 Canonical Space 到 Navisworks 外部坐标的反向转换 + +建议能力: + +- `ToCanonicalPoint(...)` +- `ToCanonicalVector(...)` +- `ToCanonicalBounds(...)` +- `ToCanonicalTransform(...)` +- `FromCanonicalPoint(...)` +- `FromCanonicalVector(...)` +- `FromCanonicalBounds(...)` +- `FromCanonicalTransform(...)` + +#### 2. `CanonicalTransform` + +职责: + +- 封装 `ExternalToCanonical / CanonicalToExternal` +- 提供点、向量、姿态、包围盒的统一变换 + +#### 3. `ProjectReferenceFrame` + +职责: + +- 管理业务基准语义 + +建议包含: + +- `SphereCenterInCanonical` +- `ProjectUpInCanonical` +- `RailReferencePlane` +- `AssemblyReferenceFrame` + +### 5.3 业务层只消费 Canonical 对象 + +终端安装、Rail、动画、碰撞、渲染层不应直接使用: + +- Navisworks `Point3D` +- Navisworks `BoundingBox3D` +- Navisworks `Transform3D` + +而应通过适配层先转成内部统一对象后再计算。 + +## 6. 输入输出边界 + +### 6.1 必须拦截的输入入口 + +以下都是必须拦截的宿主输入点: + +- 鼠标点击获取路径点 +- `ModelItem.BoundingBox()` +- `ModelItem.Transform` +- `Geometry.BoundingBox` +- `Document.UpVector` +- `PathClickToolPlugin` 等交互工具返回的点 + +这些点一旦进入业务逻辑,就应先转换到 Canonical Space。 + +### 6.2 必须反向转换的输出入口 + +以下都是必须回写到宿主时做反向转换的输出点: + +- 3D 渲染点、线、法向 +- 动画对象位置与姿态 +- 碰撞点恢复 +- 碰撞报告截图定位 +- 辅助线/参考杆/通行空间可视化 + +## 7. 当前项目中的优先改造模块 + +以下模块优先级最高,因为它们既参与三维姿态,又直接暴露了世界坐标假设问题: + +### 7.1 必改 + +- [PathEditingViewModel.cs](/C:/Users/Tellme/apps/NavisworksTransport-rail-mount-modes/src/UI/WPF/ViewModels/PathEditingViewModel.cs) + - 当前终端安装仿真里仍把世界原点当球心 +- [RailPathPoseHelper.cs](/C:/Users/Tellme/apps/NavisworksTransport-rail-mount-modes/src/Utils/RailPathPoseHelper.cs) + - 当前仍把世界 `Z` 当 `worldUp` +- [PathPointRenderPlugin.cs](/C:/Users/Tellme/apps/NavisworksTransport-rail-mount-modes/src/Core/PathPointRenderPlugin.cs) + - 当前大量渲染法向仍写死 `(0,0,1)` +- [PathAnimationManager.cs](/C:/Users/Tellme/apps/NavisworksTransport-rail-mount-modes/src/Core/Animation/PathAnimationManager.cs) + - 需要逐步统一输入输出边界 +- [CollisionSceneHelper.cs](/C:/Users/Tellme/apps/NavisworksTransport-rail-mount-modes/src/Utils/CollisionSceneHelper.cs) + - 需要确保碰撞恢复只使用内部语义 + +### 7.2 次改 + +- 自动路径规划相关的高度、坡度、网格构建 +- 历史二维 `yaw` 辅助逻辑 +- 旧视图辅助和截图辅助 + +这些部分可以后续逐步纳入统一架构,不必阻塞当前终端安装与 Rail 主线。 + +## 8. 迁移策略 + +不建议一次性全项目重构。 + +建议按以下顺序推进: + +### 阶段 1 + +建立边界层: + +- `HostCoordinateAdapter` +- `CanonicalTransform` +- `ProjectReferenceFrame` + +### 阶段 2 + +先接入以下主线功能: + +- 终端安装仿真 +- Rail 姿态 +- 动画播放 +- 碰撞恢复 +- 辅助线/通行空间渲染 + +### 阶段 3 + +再逐步改造: + +- 自动路径规划 +- 高度检测 +- 坡度分析 +- 旧二维路径辅助逻辑 + +## 9. 关键结论 + +本项目如果要长期稳定支持 `Y-up` 和 `Z-up` 项目,正确方向不是: + +- 到处散落 `if (isYUp)` +- 强行要求客户把模型先转成 `Z-up` +- 继续混用世界原点和业务球心 + +而应该是: + +- **Navisworks 世界坐标统一视为外部坐标** +- **程序内部统一使用 Canonical Space(Z-up)** +- **业务基准点单独建模** +- **坐标转换只发生在边界层** + +这是一条更接近业界三维软件成熟做法的路线。 diff --git a/doc/guide/design_principles.md b/doc/guide/design_principles.md index f05e188..a88d76a 100644 --- a/doc/guide/design_principles.md +++ b/doc/guide/design_principles.md @@ -259,11 +259,303 @@ private static void TryRecoverComponents() ### 适用场景 +## 18. 坐标系分层原则 + +### 问题描述 + +项目已经同时面对以下三类不同语义的坐标: + +- Navisworks 世界坐标 +- 程序内部计算坐标 +- 工程业务基准坐标(如球心、安装基准、轨道参考面) + +如果这三层语义混在一起使用,就会出现典型问题: + +- 把世界原点误当成业务球心 +- 把世界 `Z` 误当成项目唯一的 up 方向 +- 一部分代码按源模型坐标算,一部分代码按转换后坐标算 +- Y-up / Z-up 项目切换后,路径、姿态、渲染结果彼此不一致 + +### 设计原则 + +#### 18.1 必须明确区分三层坐标语义 + +```mermaid +flowchart LR + A["Navisworks 世界坐标\n(模型原始坐标, 可能是 Y-up 或 Z-up 语义)"] --> B["内部统一坐标\n(程序所有计算都使用)"] + B --> C["业务基准坐标\n(球心、安装方向、轨道参考面)"] + + C --> D["终端安装仿真"] + C --> E["Rail 姿态/动画"] + C --> F["碰撞检测/结果恢复"] + + B --> G["路径规划/网格/几何运算"] + B --> H["渲染/可视化"] + + I["UI 输入/日志/坐标显示"] <-->|按需转换| B +``` + +三层语义的职责如下: + +- **Navisworks 世界坐标** + - 这是 API 直接返回的坐标、包围盒、变换矩阵 + - 反映模型当前在 Navisworks 场景中的真实位置 + - 不能自动等同于业务上的球心、安装参考面或统一 up 方向 + +- **内部统一坐标** + - 程序内部所有几何计算、路径规划、姿态计算应尽量统一使用这一层 + - 这一层负责消化源模型的 Y-up / Z-up 差异 + - 这一层只处理坐标轴和方向语义,不处理业务规则 + +- **业务基准坐标** + - 用来表达球心、安装基准点、轨道参考面等工程语义 + - 这一层不应偷用世界原点或世界某个固定轴 + - 业务基准点应显式配置或显式计算,不得隐式假设 + +#### 18.2 坐标系层只负责轴语义转换,不负责业务解释 + +坐标系抽象层应只回答以下问题: + +- 当前项目 up 方向是什么 +- 高程轴是哪一轴 +- 水平面由哪两轴组成 +- 点和向量如何在源模型坐标与内部统一坐标之间转换 + +坐标系层不应负责以下业务问题: + +- 球心是否在 `(0,0,0)` +- 终端安装参考线是否指向球心 +- 顶面/底面对接如何定义 +- 轨道参考面偏移量是多少 + +这些都属于业务基准层。 + +#### 18.3 业务逻辑不得直接写死世界原点和世界 Z + +以下写法都应视为高风险设计: + +```csharp +// ❌ 错误:把世界原点直接当成业务球心 +Vector3D direction = new Vector3D(centerPoint.X, centerPoint.Y, centerPoint.Z); + +// ❌ 错误:把世界Z直接当成项目up +var worldUp = new Vector3D(0, 0, 1); + +// ❌ 错误:把Min.Z / Max.Z直接当成统一的高程语义 +double top = bounds.Max.Z; +double bottom = bounds.Min.Z; +``` + +更合理的写法应当是: + +```csharp +// ✅ 正确:球心来自业务基准配置或业务计算 +Vector3D direction = centerPoint - sphereCenter; + +// ✅ 正确:up方向来自坐标系抽象或对象自身姿态 +Vector3D worldUp = CoordinateSystemManager.Instance.Current.UpVector; + +// ✅ 正确:高程语义来自坐标系抽象 +double elevation = coordinateSystem.GetElevation(point); +``` + +#### 18.4 程序内部应优先统一计算坐标,再按需转换回用户语义 + +当客户项目坚持使用 Y-up 坐标时,不应要求客户先把模型硬转成 Z-up 再继续使用。 + +更合理的做法是: + +- 保留客户熟悉的输入输出坐标语义 +- 程序内部统一转换到内部计算坐标 +- 计算完成后,再把需要展示给用户的数据映射回客户语义 + +也就是说: + +- **客户层**可以是 Y-up +- **内部计算层**可以统一成程序更容易处理的一套坐标 +- **显示/日志层**再根据需要转回客户语义 + +#### 18.5 不要在全项目到处散落 Y-up / Z-up 条件分支 + +不推荐这种扩散式兼容写法: + +```csharp +// ❌ 错误:在业务逻辑中到处散落坐标系分支 +if (isYUp) +{ + // ... +} +else +{ + // ... +} +``` + +更推荐的方式是: + +- 在坐标系层统一封装 `UpVector`、`GetElevation()`、`GetHorizontalCoords()` 等能力 +- 在业务层统一消费抽象结果 +- 让路径、姿态、渲染尽量只面对“内部统一坐标” + +#### 18.6 业务基准点必须单独配置或显式求解 + +像“球心”这类点,不属于坐标系本身的一部分,必须单独处理。 + +例如: + +- 球心可以来自项目配置 +- 或来自多条已知向心轴线的拟合结果 +- 或来自设计资料中的明确基准点 + +但不能因为过去某批模型里球心正好在世界原点,就长期把它写死。 + +### 适用场景 + +- 终端安装仿真 +- Rail 三维姿态与动画 +- 路径规划中的高程与水平平面计算 +- 多坐标系项目的输入、显示与日志输出 + - 所有对外接口方法 - 事件处理器 - 后台任务执行 - Navisworks API调用 +## 19. 框架先行与测试先行原则 + +### 问题描述 + +当功能涉及到底层几何语义、坐标系语义、局部轴约定或姿态矩阵时,如果直接在业务链路里一边试一边改,通常会出现这些问题: + +- 虚拟物体和真实模型的语义被混用 +- 一处补丁修好,另一条链路被带坏 +- 日志越来越多,但问题边界越来越模糊 +- 业务代码里充满临时补偿,最终难以维护 + +这类问题的根因往往不是业务流程本身,而是底层框架语义没有先被验证。 + +### 设计原则 + +#### 19.1 先抽离框架,再接业务 + +对于以下类型的问题,不应先改业务代码: + +- 坐标系转换 +- 局部轴约定 +- 姿态构造 +- 纯几何补偿 +- 宿主坐标与内部统一坐标之间的映射 + +正确顺序应当是: + +1. 先抽出纯框架层 +2. 用最小测试验证框架层 +3. 通过后再接入业务模块 + +错误示例: + +```csharp +// ❌ 错误:尚未验证坐标/姿态语义,直接改动画和渲染链路 +if (isYUp) +{ + rotation = routeRotation * someCorrection; +} +else +{ + rotation = routeRotation; +} +``` + +正确示例: + +```csharp +// ✅ 正确:先让纯框架层定义并验证语义 +Quaternion rotation = canonicalPoseBuilder.CreateQuaternion( + canonicalForward, + canonicalUp, + modelAxisConvention); + +// 业务层只消费已验证的结果 +frame.Rotation = ToNavisworksRotation(rotation); +``` + +#### 19.2 可测试的核心框架不得直接依赖宿主 API + +凡是需要单元测试验证的核心几何框架,不应直接绑定 Navisworks `Point3D`、`Vector3D`、`BoundingBox3D` 这类宿主类型。 + +原因是: + +- 宿主 API 在脱离 Navisworks 进程时通常无法初始化 +- 会导致测试只能在宿主环境里间接验证 +- 这样测试粒度太粗,定位问题非常慢 + +更合理的方式是: + +- 框架核心使用纯数学类型 + - 例如 `System.Numerics.Vector3` + - 例如 `System.Numerics.Matrix4x4` + - 例如 `System.Numerics.Quaternion` +- 只有边界适配层才接触 Navisworks API + +也就是说: + +- **纯数学层**:可单元测试 +- **宿主适配层**:负责包装和转换 +- **业务层**:消费已验证结果 + +#### 19.3 先验证“语义”,再验证“效果” + +这类框架测试的重点不是先看动画画面,而是先验证最小语义: + +- Y-up 到 Canonical Z-up 的点/向量转换是否正确 +- 本地 `X-forward / Y-up` 与 `X-forward / Z-up` 的轴约定是否正确 +- 给定世界前进方向和上方向,生成的姿态矩阵列向量是否正确 +- 包围盒和参考点经过往返转换是否保持一致 + +只有这些最小语义先对,业务画面才值得继续看。 + +#### 19.4 业务接入应尽量只做“选择约定”,不做“临时补偿” + +当框架层已经定义了: + +- 宿主坐标系 +- 内部统一坐标 +- 模型局部轴约定 + +那么业务层最理想的职责应该只是: + +- 选择当前对象使用哪一种约定 +- 调用框架生成姿态或坐标 + +而不是在业务层继续做: + +- `+90°` +- `-90°` +- “如果 Y-up 就补一下” +- “如果真实模型就再扭一下” + +这些都属于典型的补丁式开发,会掩盖框架问题。 + +#### 19.5 一旦测试表明框架不纯,必须先回到框架层 + +如果在测试阶段发现: + +- 纯框架层还依赖宿主 API +- 测试无法脱离 Navisworks 运行 +- 同一个姿态语义在框架和业务里各算一遍 + +就不应该继续改业务代码,而应立即回到框架层收口。 + +这比继续在业务链路里加日志、加补偿更重要。 + +### 适用场景 + +- 坐标系改造 +- Y-up / Z-up 兼容 +- Rail 三维姿态 +- 真实模型与虚拟物体局部轴约定统一 +- 碰撞恢复姿态语义 + ## 3. 内存管理与性能优化 ### 问题描述 diff --git a/doc/working/coordinate-system-canonical-space-implementation-plan.md b/doc/working/coordinate-system-canonical-space-implementation-plan.md new file mode 100644 index 0000000..6e67f8c --- /dev/null +++ b/doc/working/coordinate-system-canonical-space-implementation-plan.md @@ -0,0 +1,362 @@ +# 坐标系统一架构实施清单 + +## 1. 文档目的 + +本文档基于 +[coordinate-system-canonical-space-design.md](/C:/Users/Tellme/apps/NavisworksTransport-rail-mount-modes/doc/design/2026/coordinate-system-canonical-space-design.md) +中的方案,拆解出一份可执行的实施清单。 + +目标不是一次性全项目重构,而是: + +- 先建立“外部坐标 -> 内部统一坐标 -> 业务基准坐标”的边界 +- 再优先改造当前最重要的非自动路径规划功能 +- 逐步把历史 `Z-up` 假设收口 + +## 2. 总体原则 + +### 2.1 本轮范围 + +当前优先范围: + +- 终端安装仿真 +- Rail 三维姿态 +- 动画播放 +- 碰撞检测与碰撞恢复 +- 辅助线、通行空间、碰撞点相关渲染 + +当前明确**不优先**处理: + +- 自动路径规划 +- 高度检测 +- 坡度分析 +- 旧网格高度/通道高度相关算法 + +### 2.2 内部统一坐标 + +当前实施目标固定为: + +- **内部统一坐标 = Canonical Space = Z-up** + +### 2.3 外部坐标定义 + +当前实施中,统一定义: + +- **Navisworks API 返回的世界坐标 = 外部坐标** + +即: + +- `Point3D` +- `Vector3D` +- `BoundingBox3D` +- `Transform3D` +- `Document.UpVector` +- `ModelItem.BoundingBox()` +- 鼠标点击拾取点 + +都先视为外部输入。 + +## 3. 里程碑划分 + +### M1. 建立坐标边界层 + +目标: + +- 引入统一的宿主坐标适配器 +- 不再在业务代码里直接解释 Navisworks 坐标 + +### M2. 接入终端安装仿真 + +目标: + +- 终端安装仿真不再直接依赖世界原点和世界 `Z` +- 球心与项目 up 都走统一边界层/业务基准层 + +### M3. 接入 Rail 三维姿态 + +目标: + +- Rail 姿态和动画只认内部统一坐标 +- 不再在姿态 helper 中写死 `(0,0,1)` + +### M4. 接入碰撞恢复和可视化 + +目标: + +- 碰撞报告、碰撞点恢复、自动截图、辅助线渲染统一使用同一套内部语义 + +## 4. 任务清单 + +## 4.1 M1 建立坐标边界层 + +### Task 1. 新增 `HostCoordinateAdapter` + +目标: + +- 建立 Navisworks 外部坐标与内部统一坐标之间的唯一适配入口 + +建议职责: + +- `ToCanonicalPoint(...)` +- `ToCanonicalVector(...)` +- `ToCanonicalBounds(...)` +- `ToCanonicalTransform(...)` +- `FromCanonicalPoint(...)` +- `FromCanonicalVector(...)` +- `FromCanonicalBounds(...)` +- `FromCanonicalTransform(...)` + +建议要求: + +- 统一使用完整三维变换语义 +- 不再停留在 `GetElevation()/GetHorizontalCoords()` 这种偏二维接口 +- 点、向量、包围盒、旋转、变换都必须有明确变换规则 +- 坐标定义中必须显式暴露: + - `UpAxis` + - `ElevationAxis` + - `HorizontalPlane` + +验收: + +- 对同一组 Navisworks 输入,适配结果在 Y-up 和 Z-up 项目中都能稳定输出 Canonical Space 数据 + +### Task 2. 新增 `ProjectReferenceFrame` + +目标: + +- 承载业务基准点,不再把业务基准混进坐标系层 + +建议职责: + +- `SphereCenterInCanonical` +- `ProjectUpInCanonical` +- 终端安装参考方向 +- 轨道参考面 + +验收: + +- 程序内不再把世界原点直接当球心使用 + +### Task 3. 约束边界:禁止业务代码直接解释宿主坐标 + +目标: + +- 收口“谁可以直接碰 Navisworks 坐标” + +需要收口的入口: + +- 鼠标点击取点 +- `ModelItem.BoundingBox()` +- `Transform3D` +- `Document.UpVector` +- 3D 渲染输入 +- 动画对象位姿输出 + +验收: + +- 新功能主链路中,业务代码不再直接从 `BoundingBox().Center` 开始做业务推导 + +## 4.2 M2 接入终端安装仿真 + +### Task 4. 改造 `PathEditingViewModel` + +文件: + +- [PathEditingViewModel.cs](/C:/Users/Tellme/apps/NavisworksTransport-rail-mount-modes/src/UI/WPF/ViewModels/PathEditingViewModel.cs) + +目标: + +- 终端安装辅助线全部改为基于 Canonical Space + ProjectReferenceFrame + +重点修改点: + +- `BuildAssemblyReferenceLine()` +- `GetAssemblyTerminalAnchorPoint()` +- 与辅助线、终点锚点、参考方向相关的计算 + +必须消除: + +- `Vector3D direction = new Vector3D(centerPoint.X, centerPoint.Y, centerPoint.Z);` +- 直接把世界原点当球心 + +验收: + +- Y-up 项目中,若球心配置正确,辅助线方向与业务预期一致 +- Z-up 项目中,行为保持不退化 + +### Task 5. 明确 UI 和日志的坐标语义 + +目标: + +- 终端安装相关 UI、日志输出不混淆内部坐标和外部坐标 + +建议: + +- 内部计算使用 Canonical +- 对用户显示时,明确是否转回 Navisworks 外部坐标 + +验收: + +- 日志中坐标语义一致,不再出现“内部算的是一种,打印的是另一种” + +## 4.3 M3 接入 Rail 三维姿态 + +### Task 6. 改造 `RailPathPoseHelper` + +文件: + +- [RailPathPoseHelper.cs](/C:/Users/Tellme/apps/NavisworksTransport-rail-mount-modes/src/Utils/RailPathPoseHelper.cs) + +目标: + +- Rail 姿态计算统一基于 Canonical Space + +必须消除: + +- `new Vector3D(0, 0, 1)` +- `worldUp = new Vector3D(0, 0, 1)` + +替换方式: + +- 改用适配层提供的 Canonical up +- 或由 `ProjectReferenceFrame` 提供项目参考 up + +验收: + +- Y-up 项目与 Z-up 项目中,Rail 参考方向一致、俯仰/侧倾逻辑一致 + +### Task 7. 清理 Rail 动画输入边界 + +涉及文件: + +- [PathAnimationManager.cs](/C:/Users/Tellme/apps/NavisworksTransport-rail-mount-modes/src/Core/Animation/PathAnimationManager.cs) +- [VirtualObjectManager.cs](/C:/Users/Tellme/apps/NavisworksTransport-rail-mount-modes/src/Core/VirtualObjectManager.cs) +- [ModelItemTransformHelper.cs](/C:/Users/Tellme/apps/NavisworksTransport-rail-mount-modes/src/Utils/ModelItemTransformHelper.cs) + +目标: + +- 动画内部只消费 Canonical 姿态 +- 输出到 Navisworks 时再做反向转换 + +验收: + +- Rail 虚拟物体与真实模型在 Y-up / Z-up 项目中的姿态语义一致 + +### Task 7.1 明确模型局部轴约定 + +目标: + +- 将“模型本地哪个轴代表 forward/up”从隐含假设提升为显式定义 + +需要覆盖: + +- 虚拟物体资源 +- 终端安装辅助杆资源 +- 真实模型在 `Y-up` / `Z-up` 项目中的默认局部轴约定 + +最低要求: + +- 至少明确 `LocalForwardAxis` +- 至少明确 `LocalUpAxis` + +验收: + +- 不再出现“路径方向和通行空间都正确,但真实模型仍按错误本地 up 站立”的现象 +- `Y-up` 真实模型与 `Z-up` 真实模型都能通过显式局部轴约定接入 Rail 姿态链路 + +## 4.4 M4 接入碰撞恢复和渲染 + +### Task 8. 改造碰撞恢复链路 + +涉及文件: + +- [CollisionSceneHelper.cs](/C:/Users/Tellme/apps/NavisworksTransport-rail-mount-modes/src/Utils/CollisionSceneHelper.cs) +- [GenerateCollisionReportCommand.cs](/C:/Users/Tellme/apps/NavisworksTransport-rail-mount-modes/src/Commands/GenerateCollisionReportCommand.cs) +- [ClashDetectiveIntegration.cs](/C:/Users/Tellme/apps/NavisworksTransport-rail-mount-modes/src/Core/Collision/ClashDetectiveIntegration.cs) + +目标: + +- 碰撞点恢复、报告截图、碰撞点回看全部使用统一的内部姿态语义 + +验收: + +- Rail 碰撞报告恢复位置和动画实际姿态一致 +- 二维路径不被三维逻辑污染 + +### Task 9. 改造渲染层 + +文件: + +- [PathPointRenderPlugin.cs](/C:/Users/Tellme/apps/NavisworksTransport-rail-mount-modes/src/Core/PathPointRenderPlugin.cs) +- [AssemblyReferencePathManager.cs](/C:/Users/Tellme/apps/NavisworksTransport-rail-mount-modes/src/Core/AssemblyReferencePathManager.cs) + +目标: + +- 辅助线、通行空间、点标记、碰撞点渲染,不再直接写死 `Normal = (0,0,1)` +- 渲染层不仅要改“中心点/偏移量”,还要统一渲染几何的局部轴语义 + +验收: + +- Y-up 项目中,渲染法向和可视化朝向不再依赖世界 Z +- Y-up 项目中,默认通行空间和 `SP` 模式通行空间的高度轴与宿主 up 一致 +- 不再出现“中心点位置正确,但长方体/杆体本体仍按 Z-up 构造”的现象 + +实施提示: + +- 长方体、圆柱体、辅助杆这类几何渲染,必须同时检查: + - 中心点是否正确 + - `right/up/height` 局部轴是否正确 + - `Normal` 是否仍偷用世界 `Z` +- 不能只修改 `ApplyVerticalOffset(...)` 或中心点偏移,而保留旧的 `XY + Z` 轴构造公式 + +## 5. 已知问题与注意事项 + +### 5.1 当前不应优先展开的模块 + +以下模块虽然也存在坐标系写死问题,但暂不作为本轮主线: + +- [ChannelHeightDetector.cs](/C:/Users/Tellme/apps/NavisworksTransport-rail-mount-modes/src/PathPlanning/ChannelHeightDetector.cs) +- [SlopeAnalyzer.cs](/C:/Users/Tellme/apps/NavisworksTransport-rail-mount-modes/src/PathPlanning/SlopeAnalyzer.cs) +- [OptimizedHeightCalculator.cs](/C:/Users/Tellme/apps/NavisworksTransport-rail-mount-modes/src/PathPlanning/OptimizedHeightCalculator.cs) + +原因: + +- 它们主要服务于自动路径规划 +- 当前项目主线优先级不在这里 + +### 5.2 不能依赖 fallback 掩盖问题 + +实施过程中禁止: + +- 三维路径恢复失败时偷偷退回 `yaw` +- 外部坐标转换失败时偷偷按世界 `Z` 继续算 +- 业务基准点缺失时默认球心为 `(0,0,0)` + +必须让错误显式暴露。 + +## 6. 当前实施优先顺序 + +建议实际推进顺序如下: + +1. `HostCoordinateAdapter` +2. `ProjectReferenceFrame` +3. `PathEditingViewModel` +4. `RailPathPoseHelper` +5. `PathAnimationManager` +6. `CollisionSceneHelper` +7. `PathPointRenderPlugin` + +## 7. 完成判定 + +当以下条件同时满足时,可认为本轮非自动路径规划坐标系统一改造达到阶段目标: + +- 终端安装仿真在 Y-up / Z-up 项目中语义一致 +- Rail 三维姿态在 Y-up / Z-up 项目中语义一致 +- 动画播放与碰撞恢复姿态一致 +- 碰撞报告截图与实际动画姿态一致 +- 主链路业务代码中不再直接写死世界原点和世界 `Z` + +## 8. 备注 + +本清单服务于“先稳定非自动路径规划主线”的目标,不代表自动路径规划相关模块不重要。 + +后续如要继续推进全项目坐标系统一,应单独启动自动路径规划相关的第二阶段清理计划。 diff --git a/run-unit-tests.bat b/run-unit-tests.bat new file mode 100644 index 0000000..b5df301 --- /dev/null +++ b/run-unit-tests.bat @@ -0,0 +1,50 @@ +@echo off +setlocal + +echo Building NavisworksTransport.UnitTests... + +set MSBUILD_PATH="C:\Program Files\Microsoft Visual Studio\2022\Community\MSBuild\Current\Bin\MSBuild.exe" +if not exist %MSBUILD_PATH% ( + set MSBUILD_PATH="C:\Program Files\Microsoft Visual Studio\2022\Professional\MSBuild\Current\Bin\MSBuild.exe" +) +if not exist %MSBUILD_PATH% ( + echo MSBuild not found. Please install Visual Studio 2022 or Build Tools. + exit /b 1 +) + +set VSTEST_PATH="C:\Program Files\Microsoft Visual Studio\2022\Community\Common7\IDE\Extensions\TestPlatform\vstest.console.exe" +if not exist %VSTEST_PATH% ( + set VSTEST_PATH="C:\Program Files\Microsoft Visual Studio\2022\Professional\Common7\IDE\Extensions\TestPlatform\vstest.console.exe" +) +if not exist %VSTEST_PATH% ( + echo vstest.console.exe not found. Please install Visual Studio 2022 test tools. + exit /b 1 +) + +set TEST_ADAPTER_PATH="packages\MSTest.TestAdapter.3.0.4\build\net462" +if not exist %TEST_ADAPTER_PATH% ( + echo MSTest adapter path not found: %TEST_ADAPTER_PATH% + exit /b 1 +) + +set NAVISWORKS_PATH=C:\Program Files\Autodesk\Navisworks Manage 2026 +if not exist "%NAVISWORKS_PATH%\Autodesk.Navisworks.Api.dll" ( + echo Navisworks API path not found: %NAVISWORKS_PATH% + exit /b 1 +) + +set PATH=%NAVISWORKS_PATH%;%PATH% + +%MSBUILD_PATH% "NavisworksTransport.UnitTests.csproj" /p:Configuration=Release /p:Platform=x64 /verbosity:minimal +if errorlevel 1 ( + echo Unit test build failed! + exit /b 1 +) + +%VSTEST_PATH% "bin\x64\Release\NavisworksTransport.UnitTests.dll" /Platform:x64 /TestAdapterPath:%TEST_ADAPTER_PATH% +if errorlevel 1 ( + echo Unit tests failed! + exit /b 1 +) + +echo Unit tests passed! diff --git a/src/Core/Animation/PathAnimationManager.cs b/src/Core/Animation/PathAnimationManager.cs index 1f53517..5f2a693 100644 --- a/src/Core/Animation/PathAnimationManager.cs +++ b/src/Core/Animation/PathAnimationManager.cs @@ -8,6 +8,7 @@ using Autodesk.Navisworks.Api.Clash; using NavisworksTransport.Core.Config; using NavisworksTransport.Core.Spatial; using NavisworksTransport.Utils; +using NavisworksTransport.Utils.CoordinateSystem; using NavisApplication = Autodesk.Navisworks.Api.Application; namespace NavisworksTransport.Core.Animation @@ -653,7 +654,12 @@ namespace NavisworksTransport.Core.Animation startPosition = RailPathPoseHelper.ResolveBottomPosition(_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) ? "顶面对接" : "底面对接")}, 偏移={_route.RailReferenceToAnchorOffset:F2}"); - if (RailPathPoseHelper.TryCreateRailRotation(previousPoint, _pathPoints[0], nextPoint, out var railRotation)) + if (RailPathPoseHelper.TryCreateRailRotation( + previousPoint, + _pathPoints[0], + nextPoint, + GetCurrentRailModelAxisConvention(), + out var railRotation)) { var railLinearTransform = new Transform3D(railRotation).Linear; LogManager.Info( @@ -966,7 +972,12 @@ namespace NavisworksTransport.Core.Animation }; if (_route.PathType == PathType.Rail && - RailPathPoseHelper.TryCreateRailRotation(previousFramePoint, framePosition, nextFramePoint, out var railRotation)) + RailPathPoseHelper.TryCreateRailRotation( + previousFramePoint, + framePosition, + nextFramePoint, + GetCurrentRailModelAxisConvention(), + out var railRotation)) { frame.Rotation = railRotation; frame.HasCustomRotation = true; @@ -2963,7 +2974,8 @@ namespace NavisworksTransport.Core.Animation } var boundingBox = _animatedObject.BoundingBox(); - return boundingBox.Max.Z - boundingBox.Min.Z; + var adapter = CoordinateSystemManager.Instance.CreateHostAdapter(); + return ModelItemTransformHelper.GetHostHeight(boundingBox, adapter); } /// @@ -3077,12 +3089,34 @@ namespace NavisworksTransport.Core.Animation var bounds = item.BoundingBox(); if (useStableRailAnchor) { - return ModelItemTransformHelper.GetStableBottomAnchorPoint(bounds, item.Transform); + var adapter = CoordinateSystemManager.Instance.CreateHostAdapter(); + return ModelItemTransformHelper.GetHostBottomAnchorPoint(bounds, adapter); } return new Point3D(bounds.Center.X, bounds.Center.Y, bounds.Min.Z); } + /// + /// Rail 路径的目标姿态以“本地 X 为前进方向、本地 Z 为上方向”为约定。 + /// 真实模型在 Y-up 宿主里通常仍保持“本地 Y 为上方向”的资源语义, + /// 因此需要补一个基准修正:绕本地 X 轴 +90°,使模型的 Y-up 语义映射到 Rail 的 Z-up 期望语义。 + /// 虚拟物体资源本身按 Z-up 构建,不需要此修正。 + /// + private ModelAxisConvention GetCurrentRailModelAxisConvention() + { + if (_isVirtualObject) + { + return ModelAxisConvention.CreateDefaultForHost(CoordinateSystemType.ZUp); + } + + var adapter = CoordinateSystemManager.Instance.CreateHostAdapter(); + var convention = ModelAxisConvention.CreateDefaultForHost(adapter.HostType); + LogManager.Info( + $"[Rail姿态修正] Host={adapter.HostType}, 虚拟物体={_isVirtualObject}, " + + $"Forward={convention.ForwardAxis}, Up={convention.UpAxis}"); + return convention; + } + /// /// 设置物体角度修正值(度,顺时针) /// diff --git a/src/Core/PathPointRenderPlugin.cs b/src/Core/PathPointRenderPlugin.cs index 3bdc5f6..ba45fb2 100644 --- a/src/Core/PathPointRenderPlugin.cs +++ b/src/Core/PathPointRenderPlugin.cs @@ -5,6 +5,7 @@ using Autodesk.Navisworks.Api; using Autodesk.Navisworks.Api.Plugins; using NavisworksTransport.Core.Config; using NavisworksTransport.Utils; +using NavisworksTransport.Utils.CoordinateSystem; using static NavisworksTransport.Core.Config.ConfigManager; namespace NavisworksTransport @@ -938,7 +939,7 @@ namespace NavisworksTransport return new CircleMarker { Center = marker.Position, - Normal = new Vector3D(0, 0, 1), + Normal = GetHostUpVector(), Radius = GetRadiusForGridVisualization(), Color = style.Color, Alpha = style.Alpha, @@ -1373,7 +1374,7 @@ namespace NavisworksTransport var grayEndMarker = new CircleMarker { Center = originalEndPoint, - Normal = new Vector3D(0, 0, 1), + Normal = GetHostUpVector(), Radius = GetRadiusForPointType(PathPointType.EndPoint), Color = unreachedStyle.Color, // 未到达终点颜色 Alpha = unreachedStyle.Alpha, // 透明度,统一管理 @@ -1615,17 +1616,10 @@ namespace NavisworksTransport } else if (_showObjectSpace && Math.Abs(verticalOffset) > 0.001) { - // 通行空间模式且有垂直偏移时,沿Z轴平移(确保只有垂直偏移,没有水平偏移) - adjustedStartPoint = new Point3D( - startPoint.X, - startPoint.Y, - startPoint.Z + verticalOffset - ); - adjustedEndPoint = new Point3D( - endPoint.X, - endPoint.Y, - endPoint.Z + verticalOffset - ); + // 非 Rail 通行空间必须沿宿主 up 偏移,不能写死世界 Z。 + var hostUp = GetHostUpVector(); + adjustedStartPoint = ApplyVerticalOffset(startPoint, hostUp, verticalOffset); + adjustedEndPoint = ApplyVerticalOffset(endPoint, hostUp, verticalOffset); } else { @@ -1718,29 +1712,18 @@ namespace NavisworksTransport } else if (_showObjectSpace && Math.Abs(verticalOffset) > 0.001) { - // 通行空间模式且有垂直偏移时,沿Z轴平移(确保只有垂直偏移,没有水平偏移) - adjustedStartPoint = new Point3D( - startPoint.X, - startPoint.Y, - startPoint.Z + verticalOffset - ); - adjustedEndPoint = new Point3D( - endPoint.X, - endPoint.Y, - endPoint.Z + verticalOffset - ); + // 非 Rail 通行空间必须沿宿主 up 偏移,不能写死世界 Z。 + var hostUp = GetHostUpVector(); + adjustedStartPoint = ApplyVerticalOffset(startPoint, hostUp, verticalOffset); + adjustedEndPoint = ApplyVerticalOffset(endPoint, hostUp, verticalOffset); - // 对 SampledPoints 应用垂直偏移(沿Z轴) + // 对 SampledPoints 应用同一宿主 up 方向偏移。 adjustedSampledPoints = new List(); if (edge.SampledPoints != null) { foreach (var point in edge.SampledPoints) { - adjustedSampledPoints.Add(new Point3D( - point.X, - point.Y, - point.Z + verticalOffset - )); + adjustedSampledPoints.Add(ApplyVerticalOffset(point, hostUp, verticalOffset)); } } } @@ -1810,20 +1793,26 @@ namespace NavisworksTransport segmentDirection.Z / segmentLength ); - // 计算垂直向量(宽度方向) - // 使用方向向量与Z轴的叉积来计算right向量,确保right向量在水平面上 - var right = new Vector3D(normalizedDirection.Y, -normalizedDirection.X, 0); + // 使用宿主 up 方向构造水平 right 向量,避免把世界 Z 写死到 Y-up 项目里。 + var hostUp = GetHostUpVector(); + var right = Cross(normalizedDirection, hostUp); - // 如果方向向量垂直于XY平面(吊装路径),则使用水平段的方向向量来计算right - if (Math.Abs(normalizedDirection.Z) > 0.9) + // 如果方向向量接近宿主 up(垂直段),则使用水平段方向重新构造 right。 + double parallelToUp = Math.Abs( + normalizedDirection.X * hostUp.X + + normalizedDirection.Y * hostUp.Y + + normalizedDirection.Z * hostUp.Z); + if (parallelToUp > 0.9) { if (horizontalDirection != null) { - right = new Vector3D(horizontalDirection.Y, -horizontalDirection.X, 0); + right = Cross(horizontalDirection, hostUp); } else { - right = new Vector3D(1, 0, 0); + right = Math.Abs(hostUp.X) < 0.9 + ? Normalize(Cross(hostUp, new Vector3D(1, 0, 0))) + : Normalize(Cross(hostUp, new Vector3D(0, 1, 0))); } } @@ -1973,18 +1962,10 @@ namespace NavisworksTransport } else if (_showObjectSpace && Math.Abs(verticalOffset) > 0.001) { - // 通行空间模式且有垂直偏移时 - // 所有空中路径:统一沿Z轴向下偏移,顶面中心对齐路径点 - adjustedStartPoint = new Point3D( - startPoint.Position.X, - startPoint.Position.Y, - startPoint.Position.Z + verticalOffset - ); - adjustedEndPoint = new Point3D( - endPoint.Position.X, - endPoint.Position.Y, - endPoint.Position.Z + verticalOffset - ); + // 非 Rail 通行空间统一沿宿主 up 方向偏移,不能再默认世界 Z。 + var hostUp = GetHostUpVector(); + adjustedStartPoint = ApplyVerticalOffset(startPoint.Position, hostUp, verticalOffset); + adjustedEndPoint = ApplyVerticalOffset(endPoint.Position, hostUp, verticalOffset); } else { @@ -2068,35 +2049,43 @@ namespace NavisworksTransport return point; } - // 归一化up向量 - var upLength = Math.Sqrt(up.X * up.X + up.Y * up.Y + up.Z * up.Z); - if (upLength < 0.001) + var normalizedUp = Normalize(up); + if (normalizedUp.X == 0.0 && normalizedUp.Y == 0.0 && normalizedUp.Z == 0.0) { return point; } - var normalizedUp = new Vector3D(up.X / upLength, up.Y / upLength, up.Z / upLength); - // 计算up向量在垂直平面上的投影(只保留Z分量) - // 这样可以确保偏移只在垂直方向上,不会产生水平偏移 - var verticalComponent = new Vector3D(0, 0, normalizedUp.Z); + return new Point3D( + point.X + normalizedUp.X * verticalOffset, + point.Y + normalizedUp.Y * verticalOffset, + point.Z + normalizedUp.Z * verticalOffset + ); + } - // 计算垂直分量的长度 - var verticalComponentLength = Math.Abs(verticalComponent.Z); + private static Vector3D GetHostUpVector() + { + var adapter = CoordinateSystemManager.Instance.CreateHostAdapter(); + return Normalize(adapter.FromCanonicalVector(HostCoordinateAdapter.CanonicalUp)); + } - if (verticalComponentLength < 0.001) + private static Vector3D Cross(Vector3D a, Vector3D b) + { + return new Vector3D( + a.Y * b.Z - a.Z * b.Y, + a.Z * b.X - a.X * b.Z, + a.X * b.Y - a.Y * b.X); + } + + private static Vector3D Normalize(Vector3D vector) + { + double lengthSquared = vector.X * vector.X + vector.Y * vector.Y + vector.Z * vector.Z; + if (lengthSquared < 1e-9) { - // 如果up向量几乎是水平的,则直接使用纯Z方向 - return new Point3D(point.X, point.Y, point.Z + verticalOffset); + return new Vector3D(0, 0, 0); } - // 沿着垂直分量方向平移 - // 需要调整偏移量,因为垂直分量的长度可能不是1 - var adjustedOffset = verticalOffset * (Math.Sign(verticalComponent.Z) / verticalComponentLength); - return new Point3D( - point.X, - point.Y, - point.Z + adjustedOffset - ); + double length = Math.Sqrt(lengthSquared); + return new Vector3D(vector.X / length, vector.Y / length, vector.Z / length); } /// @@ -2125,7 +2114,7 @@ namespace NavisworksTransport var tsMarker = new SquareMarker { Center = edge.Trajectory.Ts, - Normal = new Vector3D(0, 0, 1), + Normal = GetHostUpVector(), Size = tangentRadius * 2, // 直径 Height = tangentHeight, // 高度 Color = tangentColor, @@ -2139,7 +2128,7 @@ namespace NavisworksTransport var teMarker = new SquareMarker { Center = edge.Trajectory.Te, - Normal = new Vector3D(0, 0, 1), + Normal = GetHostUpVector(), Size = tangentRadius * 2, // 直径 Height = tangentHeight, // 高度 Color = tangentColor, @@ -2222,7 +2211,7 @@ namespace NavisworksTransport return new CircleMarker { Center = point.Position, - Normal = new Vector3D(0, 0, 1), + Normal = GetHostUpVector(), Radius = isGridVisualization ? GetRadiusForGridVisualization() : GetRadiusForPointType(point.Type), Color = isGridVisualization ? gridStyle.Color : GetColorForPointType(point.Type), Alpha = isGridVisualization ? gridStyle.Alpha : 1.0, @@ -2745,21 +2734,24 @@ namespace NavisworksTransport // 归一化方向向量 direction = new Vector3D(direction.X / length, direction.Y / length, direction.Z / length); - // 计算垂直向量(在XY平面内垂直于方向向量) - var right = new Vector3D(-direction.Y, direction.X, 0); + var hostUp = GetHostUpVector(); + var right = Cross(direction, hostUp); - // 如果方向向量垂直于XY平面(吊装路径),则使用水平段的方向向量来计算right - if (Math.Abs(direction.Z) > 0.9) + double parallelToUp = Math.Abs( + direction.X * hostUp.X + + direction.Y * hostUp.Y + + direction.Z * hostUp.Z); + if (parallelToUp > 0.9) { if (horizontalDirection != null) { - // 垂直段的 right 向量应该垂直于水平段的方向向量 - right = new Vector3D(horizontalDirection.Y, -horizontalDirection.X, 0); + right = Cross(horizontalDirection, hostUp); } else { - // 如果没有提供水平段方向向量,使用默认值 - right = new Vector3D(1, 0, 0); + right = Math.Abs(hostUp.X) < 0.9 + ? Normalize(Cross(hostUp, new Vector3D(1, 0, 0))) + : Normalize(Cross(hostUp, new Vector3D(0, 1, 0))); } } @@ -2837,21 +2829,24 @@ namespace NavisworksTransport direction = new Vector3D(direction.X / directionLength, direction.Y / directionLength, direction.Z / directionLength); - // 计算垂直向量(在XY平面内垂直于方向向量) - var right = new Vector3D(-direction.Y, direction.X, 0); + var hostUp = GetHostUpVector(); + var right = Cross(direction, hostUp); - // 如果方向向量垂直于XY平面(吊装路径),则使用水平段的方向向量来计算right - if (Math.Abs(direction.Z) > 0.9) + double parallelToUp = Math.Abs( + direction.X * hostUp.X + + direction.Y * hostUp.Y + + direction.Z * hostUp.Z); + if (parallelToUp > 0.9) { if (horizontalDirection != null) { - // 垂直段的 right 向量应该垂直于水平段的方向向量 - right = new Vector3D(-horizontalDirection.Y, horizontalDirection.X, 0); + right = Cross(horizontalDirection, hostUp); } else { - // 如果没有提供水平段方向向量,使用默认值 - right = new Vector3D(1, 0, 0); + right = Math.Abs(hostUp.X) < 0.9 + ? Normalize(Cross(hostUp, new Vector3D(1, 0, 0))) + : Normalize(Cross(hostUp, new Vector3D(0, 1, 0))); } } diff --git a/src/PathPlanning/ChannelBasedGridBuilder.cs b/src/PathPlanning/ChannelBasedGridBuilder.cs index 3d82c76..4c2a29c 100644 --- a/src/PathPlanning/ChannelBasedGridBuilder.cs +++ b/src/PathPlanning/ChannelBasedGridBuilder.cs @@ -101,7 +101,9 @@ namespace NavisworksTransport.PathPlanning LogManager.Info($"[通道网格构建器] 通道总边界: {FormatBounds(totalBounds)}"); // 3. 创建网格地图 - var gridMap = new GridMap(totalBounds, gridSize); + // 必须继续传递当前构建器绑定的坐标系,避免同一轮流程里 + // ChannelBasedGridBuilder、GridMap、ChannelHeightDetector 各自重新抓取 Current。 + var gridMap = new GridMap(totalBounds, gridSize, _coordinateSystem); // 4. 为每个通道生成精确投影 var processedChannels = new List(); @@ -397,7 +399,7 @@ namespace NavisworksTransport.PathPlanning var emptyBounds = new BoundingBox3D(new Point3D(0, 0, 0), new Point3D(0, 0, 0)); return new ChannelCoverage { - GridMap = new GridMap(emptyBounds, 1.0), + GridMap = new GridMap(emptyBounds, 1.0, _coordinateSystem), ChannelItems = new List(), TotalBounds = emptyBounds }; diff --git a/src/UI/WPF/ViewModels/PathEditingViewModel.cs b/src/UI/WPF/ViewModels/PathEditingViewModel.cs index cdbeba0..ce55ad8 100644 --- a/src/UI/WPF/ViewModels/PathEditingViewModel.cs +++ b/src/UI/WPF/ViewModels/PathEditingViewModel.cs @@ -18,6 +18,7 @@ using NavisworksTransport.Commands; using NavisworksTransport.UI.WPF.Views; using NavisworksTransport.UI.WPF.Models; using NavisworksTransport.Utils; +using NavisworksTransport.Utils.CoordinateSystem; using NavisworksTransport; namespace NavisworksTransport.UI.WPF.ViewModels @@ -122,11 +123,17 @@ namespace NavisworksTransport.UI.WPF.ViewModels private const double DefaultAssemblyReferenceRodLengthInMeters = 20.0; private static readonly double DefaultAssemblyReferenceRodDiameterInMeters = CalculateDefaultAssemblyReferenceRodDiameterInMeters(); private const double DefaultAssemblyAnchorVerticalOffsetInMeters = 0.0; + private const double DefaultAssemblySphereCenterX = 0.0; + private const double DefaultAssemblySphereCenterY = 0.0; + private const double DefaultAssemblySphereCenterZ = 0.0; private string _assemblyTerminalObjectName = "未选择"; private string _assemblyTerminalObjectInfo = "请选择终点处已安装箱体"; private double _assemblyReferenceRodLengthInMeters = DefaultAssemblyReferenceRodLengthInMeters; private double _assemblyReferenceRodDiameterInMeters = DefaultAssemblyReferenceRodDiameterInMeters; private double _assemblyAnchorVerticalOffsetInMeters = DefaultAssemblyAnchorVerticalOffsetInMeters; + private double _assemblySphereCenterX = DefaultAssemblySphereCenterX; + private double _assemblySphereCenterY = DefaultAssemblySphereCenterY; + private double _assemblySphereCenterZ = DefaultAssemblySphereCenterZ; private string _assemblyStartPointText = "未选择"; private RailMountMode _assemblyMountMode = RailMountMode.UnderRail; private bool _hasAssemblyTerminalObject; @@ -365,6 +372,45 @@ namespace NavisworksTransport.UI.WPF.ViewModels } } + public double AssemblySphereCenterX + { + get => _assemblySphereCenterX; + set + { + if (SetProperty(ref _assemblySphereCenterX, value) && HasAssemblyTerminalObject) + { + RefreshAssemblyTerminalObjectInfo(); + RefreshAssemblyReferenceRodIfNeeded(); + } + } + } + + public double AssemblySphereCenterY + { + get => _assemblySphereCenterY; + set + { + if (SetProperty(ref _assemblySphereCenterY, value) && HasAssemblyTerminalObject) + { + RefreshAssemblyTerminalObjectInfo(); + RefreshAssemblyReferenceRodIfNeeded(); + } + } + } + + public double AssemblySphereCenterZ + { + get => _assemblySphereCenterZ; + set + { + if (SetProperty(ref _assemblySphereCenterZ, value) && HasAssemblyTerminalObject) + { + RefreshAssemblyTerminalObjectInfo(); + RefreshAssemblyReferenceRodIfNeeded(); + } + } + } + public bool HasAssemblyTerminalObject { get => _hasAssemblyTerminalObject; @@ -1461,16 +1507,21 @@ namespace NavisworksTransport.UI.WPF.ViewModels throw new InvalidOperationException("终点箱体未设置或已失效,无法计算对接点"); } + HostCoordinateAdapter adapter = CoordinateSystemManager.Instance.CreateHostAdapter(); + var bounds = _assemblyTerminalObject.BoundingBox(); - var center = bounds.Center; - Vector3D up = ModelItemTransformHelper.GetUpDirectionFromTransform(_assemblyTerminalObject.Transform); + Point3D canonicalCenter = adapter.ToCanonicalPoint(bounds.Center); + Vector3D canonicalUp = adapter.ToCanonicalVector( + ModelItemTransformHelper.GetUpDirectionFromTransform(_assemblyTerminalObject.Transform)); double direction = GetAssemblyAnchorDirection(); double verticalOffset = UnitsConverter.ConvertFromMeters(AssemblyAnchorVerticalOffsetInMeters); - return new Point3D( - center.X + up.X * verticalOffset * direction, - center.Y + up.Y * verticalOffset * direction, - center.Z + up.Z * verticalOffset * direction); + Point3D canonicalAnchorPoint = new Point3D( + canonicalCenter.X + canonicalUp.X * verticalOffset * direction, + canonicalCenter.Y + canonicalUp.Y * verticalOffset * direction, + canonicalCenter.Z + canonicalUp.Z * verticalOffset * direction); + + return adapter.FromCanonicalPoint(canonicalAnchorPoint); } private AssemblyReferenceLine BuildAssemblyReferenceLine() @@ -1480,28 +1531,38 @@ namespace NavisworksTransport.UI.WPF.ViewModels throw new InvalidOperationException("终点箱体未设置或已失效,无法生成装配参考线"); } + HostCoordinateAdapter adapter = CoordinateSystemManager.Instance.CreateHostAdapter(); + ProjectReferenceFrame projectFrame = CreateAssemblyProjectReferenceFrame(adapter); + BoundingBox3D bounds = _assemblyTerminalObject.BoundingBox(); Point3D centerPoint = bounds.Center; Point3D endPoint = GetAssemblyTerminalAnchorPoint(); - Vector3D direction = new Vector3D(centerPoint.X, centerPoint.Y, centerPoint.Z); + + Point3D canonicalCenterPoint = adapter.ToCanonicalPoint(centerPoint); + Point3D canonicalEndPoint = adapter.ToCanonicalPoint(endPoint); + Vector3D direction = new Vector3D( + canonicalCenterPoint.X - projectFrame.SphereCenterInCanonical.X, + canonicalCenterPoint.Y - projectFrame.SphereCenterInCanonical.Y, + canonicalCenterPoint.Z - projectFrame.SphereCenterInCanonical.Z); double directionLengthSquared = direction.X * direction.X + direction.Y * direction.Y + direction.Z * direction.Z; if (directionLengthSquared < 1e-9) { - throw new InvalidOperationException("箱体中心与世界原点重合,无法生成装配参考线方向"); + throw new InvalidOperationException("箱体中心与项目球心重合,无法生成装配参考线方向"); } direction = direction.Normalize(); double rodLength = UnitsConverter.ConvertFromMeters(AssemblyReferenceRodLengthInMeters); - Point3D startPoint = new Point3D( - endPoint.X + direction.X * rodLength, - endPoint.Y + direction.Y * rodLength, - endPoint.Z + direction.Z * rodLength); + Point3D canonicalStartPoint = new Point3D( + canonicalEndPoint.X + direction.X * rodLength, + canonicalEndPoint.Y + direction.Y * rodLength, + canonicalEndPoint.Z + direction.Z * rodLength); + Point3D startPoint = adapter.FromCanonicalPoint(canonicalStartPoint); LogManager.Info( $"[直线装配] 参考线已计算,终点锚点=({endPoint.X:F2}, {endPoint.Y:F2}, {endPoint.Z:F2}), " + $"箱体中心=({centerPoint.X:F2}, {centerPoint.Y:F2}, {centerPoint.Z:F2}), " + $"参考线外端=({startPoint.X:F2}, {startPoint.Y:F2}, {startPoint.Z:F2}), " + - $"原点到箱体中心方向=({direction.X:F3}, {direction.Y:F3}, {direction.Z:F3})"); + $"球心到箱体中心方向(内部坐标)=({direction.X:F3}, {direction.Y:F3}, {direction.Z:F3})"); return new AssemblyReferenceLine(startPoint, endPoint, direction); } @@ -1743,17 +1804,40 @@ namespace NavisworksTransport.UI.WPF.ViewModels return; } + HostCoordinateAdapter adapter = CoordinateSystemManager.Instance.CreateHostAdapter(); + ProjectReferenceFrame projectFrame = CreateAssemblyProjectReferenceFrame(adapter); + Point3D centerPoint = _assemblyTerminalObject.BoundingBox().Center; + Point3D sphereCenterPoint = adapter.FromCanonicalPoint(projectFrame.SphereCenterInCanonical); renderPlugin.RenderRailBaseline( AssemblyCenterGuideLinePathId, - new List { Point3D.Origin, centerPoint }, + new List { sphereCenterPoint, centerPoint }, RenderStyleName.AssemblyGuideLine); LogManager.Info( - $"[直线装配] 已渲染原点到箱体中心基准线: 原点=(0.00, 0.00, 0.00), " + + $"[直线装配] 已渲染球心到箱体中心基准线: 球心=({sphereCenterPoint.X:F2}, {sphereCenterPoint.Y:F2}, {sphereCenterPoint.Z:F2}), " + $"箱体中心=({centerPoint.X:F2}, {centerPoint.Y:F2}, {centerPoint.Z:F2})"); } + private ProjectReferenceFrame CreateAssemblyProjectReferenceFrame(HostCoordinateAdapter adapter) + { + if (adapter == null) + { + throw new ArgumentNullException(nameof(adapter)); + } + + Point3D hostSphereCenter = new Point3D( + AssemblySphereCenterX, + AssemblySphereCenterY, + AssemblySphereCenterZ); + + Point3D canonicalSphereCenter = adapter.ToCanonicalPoint(hostSphereCenter); + return new ProjectReferenceFrame( + canonicalSphereCenter, + HostCoordinateAdapter.CanonicalUp, + ModelAxisConvention.CreateDefaultForHost(adapter.HostType)); + } + private void ClearAssemblyAnchorMarker() { var renderPlugin = PathPointRenderPlugin.Instance; diff --git a/src/UI/WPF/Views/PathEditingView.xaml b/src/UI/WPF/Views/PathEditingView.xaml index bc48783..6c14832 100644 --- a/src/UI/WPF/Views/PathEditingView.xaml +++ b/src/UI/WPF/Views/PathEditingView.xaml @@ -328,11 +328,11 @@ NavisworksTransport 路径编辑页签视图 - 采用与动画控制和分层管 DisplayMemberPath="DisplayName" ToolTip="选择直线装配路径使用轨上安装还是轨下安装"/> - - - - - + + + + + - - - - + Style="{StaticResource StatusTextStyle}" + VerticalAlignment="Center" + TextWrapping="Wrap"/> + + + + + + + + + + + + + + + + + + public struct CanonicalBounds3 + { + public Vector3 Min { get; } + public Vector3 Max { get; } + + public CanonicalBounds3(Vector3 min, Vector3 max) + { + Min = Vector3.Min(min, max); + Max = Vector3.Max(min, max); + } + } +} diff --git a/src/Utils/CoordinateSystem/CanonicalRailPoseBuilder.cs b/src/Utils/CoordinateSystem/CanonicalRailPoseBuilder.cs new file mode 100644 index 0000000..a4f82b3 --- /dev/null +++ b/src/Utils/CoordinateSystem/CanonicalRailPoseBuilder.cs @@ -0,0 +1,83 @@ +using System; +using System.Numerics; + +namespace NavisworksTransport.Utils.CoordinateSystem +{ + /// + /// 在内部 Canonical Space 中构造 Rail 姿态。 + /// 这一层只处理纯数学切向/法向/局部轴约定,不依赖 Navisworks API。 + /// + public static class CanonicalRailPoseBuilder + { + private const float TangentEpsilon = 1e-6f; + + public static bool TryCreateBasis( + Vector3 previousPoint, + Vector3 currentPoint, + Vector3 nextPoint, + Vector3 canonicalUp, + out Vector3 forward, + out Vector3 lateral, + out Vector3 up) + { + forward = default; + lateral = default; + up = default; + + Vector3 tangent = nextPoint - previousPoint; + if (tangent.LengthSquared() < TangentEpsilon) + { + tangent = nextPoint - currentPoint; + } + + if (tangent.LengthSquared() < TangentEpsilon) + { + return false; + } + + forward = Vector3.Normalize(tangent); + Vector3 projectedUp = canonicalUp - Vector3.Dot(canonicalUp, forward) * forward; + if (projectedUp.LengthSquared() < TangentEpsilon) + { + projectedUp = Math.Abs(forward.Z) < 0.9f + ? Vector3.UnitZ + : Vector3.UnitY; + projectedUp = projectedUp - Vector3.Dot(projectedUp, forward) * forward; + } + + if (projectedUp.LengthSquared() < TangentEpsilon) + { + return false; + } + + up = Vector3.Normalize(projectedUp); + lateral = Vector3.Normalize(Vector3.Cross(up, forward)); + up = Vector3.Normalize(Vector3.Cross(forward, lateral)); + return true; + } + + public static bool TryCreateQuaternion( + Vector3 previousPoint, + Vector3 currentPoint, + Vector3 nextPoint, + Vector3 canonicalUp, + ModelAxisConvention convention, + out Quaternion rotation) + { + rotation = Quaternion.Identity; + + if (convention == null) + { + throw new ArgumentNullException(nameof(convention)); + } + + if (!TryCreateBasis(previousPoint, currentPoint, nextPoint, canonicalUp, out var forward, out _, out var up)) + { + return false; + } + + rotation = convention.CreateQuaternion(forward, up); + return true; + } + } +} diff --git a/src/Utils/CoordinateSystem/CoordinateSystemManager.cs b/src/Utils/CoordinateSystem/CoordinateSystemManager.cs index 9779940..05655d9 100644 --- a/src/Utils/CoordinateSystem/CoordinateSystemManager.cs +++ b/src/Utils/CoordinateSystem/CoordinateSystemManager.cs @@ -37,6 +37,12 @@ namespace NavisworksTransport.Utils.CoordinateSystem /// public CoordinateSystemType ConfiguredType => _configuredType; + /// + /// 解析后的宿主坐标系类型。 + /// AutoDetect 模式下返回当前实际检测结果。 + /// + public CoordinateSystemType ResolvedType => GetResolvedType(); + #endregion #region 构造函数 @@ -123,6 +129,14 @@ namespace NavisworksTransport.Utils.CoordinateSystem return $"类型: {_current.Type}, 向上向量: ({_current.UpVector.X:F2}, {_current.UpVector.Y:F2}, {_current.UpVector.Z:F2})"; } + /// + /// 获取当前宿主坐标到 Canonical Space 的统一适配器。 + /// + public HostCoordinateAdapter CreateHostAdapter() + { + return new HostCoordinateAdapter(GetResolvedType()); + } + #endregion #region 私有方法 @@ -193,6 +207,27 @@ namespace NavisworksTransport.Utils.CoordinateSystem } } + /// + /// 获取当前解析后的宿主坐标系类型。 + /// + public CoordinateSystemType GetResolvedType() + { + if (_current == null) + { + return CoordinateSystemType.ZUp; + } + + switch (_current.Type) + { + case CoordinateSystemType.YUp: + return CoordinateSystemType.YUp; + case CoordinateSystemType.ZUp: + return CoordinateSystemType.ZUp; + default: + return CoordinateSystemType.ZUp; + } + } + #endregion } } diff --git a/src/Utils/CoordinateSystem/HostCoordinateAdapter.cs b/src/Utils/CoordinateSystem/HostCoordinateAdapter.cs new file mode 100644 index 0000000..4fb862f --- /dev/null +++ b/src/Utils/CoordinateSystem/HostCoordinateAdapter.cs @@ -0,0 +1,274 @@ +using System; +using System.Collections.Generic; +using System.Numerics; +using Autodesk.Navisworks.Api; + +namespace NavisworksTransport.Utils.CoordinateSystem +{ + /// + /// Navisworks 外部坐标与内部 Canonical Space 之间的统一适配器。 + /// + /// 约定: + /// - 外部坐标 = Navisworks API 返回的世界坐标 + /// - 内部坐标 = Canonical Space,固定为 Z-up + /// + /// 当前仅支持: + /// - Host Z-up -> Canonical Z-up(恒等) + /// - Host Y-up -> Canonical Z-up(绕 X 轴 +90° 的等价变换) + /// + public sealed class HostCoordinateAdapter + { + /// + /// Canonical Space 固定为 Z-up。 + /// + public static readonly Vector3 CanonicalUpVector3 = new Vector3(0f, 0f, 1f); + public static readonly Vector3D CanonicalUp = new Vector3D(0, 0, 1); + + /// + /// 当前宿主坐标系类型。 + /// + public CoordinateSystemType HostType { get; } + + /// + /// 宿主坐标系中“向上轴”的索引。 + /// Y-up => 1, Z-up => 2。 + /// + public int HostUpAxisIndex => HostType == CoordinateSystemType.YUp ? 1 : 2; + + /// + /// 宿主坐标系中的单位 up 向量。 + /// + public Vector3 HostUpVector3 => FromCanonicalVector3(CanonicalUpVector3); + public Vector3D HostUpVector => FromCanonicalVector(CanonicalUp); + + public HostCoordinateAdapter(CoordinateSystemType hostType) + { + if (hostType == CoordinateSystemType.AutoDetect) + { + throw new ArgumentException("HostCoordinateAdapter 不接受 AutoDetect,必须传入明确的宿主坐标系。", nameof(hostType)); + } + + HostType = hostType; + } + + /// + /// 从当前坐标系管理器创建适配器。 + /// + public static HostCoordinateAdapter CreateFromManager() + { + var hostType = CoordinateSystemManager.Instance.ConfiguredType == CoordinateSystemType.AutoDetect + ? CoordinateSystemManager.Instance.GetResolvedType() + : CoordinateSystemManager.Instance.ConfiguredType; + + return new HostCoordinateAdapter(hostType); + } + + public Point3D ToCanonicalPoint(Point3D hostPoint) + { + if (hostPoint == null) + { + throw new ArgumentNullException(nameof(hostPoint)); + } + + return ToPoint3D(ToCanonicalPoint3(ToVector3(hostPoint))); + } + + public Point3D FromCanonicalPoint(Point3D canonicalPoint) + { + if (canonicalPoint == null) + { + throw new ArgumentNullException(nameof(canonicalPoint)); + } + + return ToPoint3D(FromCanonicalPoint3(ToVector3(canonicalPoint))); + } + + public Vector3D ToCanonicalVector(Vector3D hostVector) + { + if (hostVector == null) + { + throw new ArgumentNullException(nameof(hostVector)); + } + + return ToVector3D(ToCanonicalVector3(ToNumericsVector(hostVector))); + } + + public Vector3D FromCanonicalVector(Vector3D canonicalVector) + { + if (canonicalVector == null) + { + throw new ArgumentNullException(nameof(canonicalVector)); + } + + return ToVector3D(FromCanonicalVector3(ToNumericsVector(canonicalVector))); + } + + public BoundingBox3D ToCanonicalBounds(BoundingBox3D hostBounds) + { + if (hostBounds == null) + { + throw new ArgumentNullException(nameof(hostBounds)); + } + + CanonicalBounds3 canonicalBounds = ToCanonicalBounds3(new CanonicalBounds3( + ToVector3(hostBounds.Min), + ToVector3(hostBounds.Max))); + return new BoundingBox3D(ToPoint3D(canonicalBounds.Min), ToPoint3D(canonicalBounds.Max)); + } + + public BoundingBox3D FromCanonicalBounds(BoundingBox3D canonicalBounds) + { + if (canonicalBounds == null) + { + throw new ArgumentNullException(nameof(canonicalBounds)); + } + + CanonicalBounds3 hostBounds = FromCanonicalBounds3(new CanonicalBounds3( + ToVector3(canonicalBounds.Min), + ToVector3(canonicalBounds.Max))); + return new BoundingBox3D(ToPoint3D(hostBounds.Min), ToPoint3D(hostBounds.Max)); + } + + public Vector3 ToCanonicalPoint3(Vector3 hostPoint) + { + switch (HostType) + { + case CoordinateSystemType.ZUp: + return hostPoint; + case CoordinateSystemType.YUp: + return new Vector3(hostPoint.X, -hostPoint.Z, hostPoint.Y); + default: + throw new InvalidOperationException($"不支持的宿主坐标系: {HostType}"); + } + } + + public Vector3 FromCanonicalPoint3(Vector3 canonicalPoint) + { + switch (HostType) + { + case CoordinateSystemType.ZUp: + return canonicalPoint; + case CoordinateSystemType.YUp: + return new Vector3(canonicalPoint.X, canonicalPoint.Z, -canonicalPoint.Y); + default: + throw new InvalidOperationException($"不支持的宿主坐标系: {HostType}"); + } + } + + public Vector3 ToCanonicalVector3(Vector3 hostVector) + { + return ToCanonicalPoint3(hostVector); + } + + public Vector3 FromCanonicalVector3(Vector3 canonicalVector) + { + return FromCanonicalPoint3(canonicalVector); + } + + public CanonicalBounds3 ToCanonicalBounds3(CanonicalBounds3 hostBounds) + { + return RebuildBounds3(hostBounds, ToCanonicalPoint3); + } + + public CanonicalBounds3 FromCanonicalBounds3(CanonicalBounds3 canonicalBounds) + { + return RebuildBounds3(canonicalBounds, FromCanonicalPoint3); + } + + private static BoundingBox3D RebuildBounds(BoundingBox3D sourceBounds, Func pointTransform) + { + var corners = GetCorners(sourceBounds); + var transformedCorners = new List(corners.Count); + + foreach (var corner in corners) + { + transformedCorners.Add(pointTransform(corner)); + } + + double minX = double.MaxValue; + double minY = double.MaxValue; + double minZ = double.MaxValue; + double maxX = double.MinValue; + double maxY = double.MinValue; + double maxZ = double.MinValue; + + foreach (var point in transformedCorners) + { + minX = Math.Min(minX, point.X); + minY = Math.Min(minY, point.Y); + minZ = Math.Min(minZ, point.Z); + maxX = Math.Max(maxX, point.X); + maxY = Math.Max(maxY, point.Y); + maxZ = Math.Max(maxZ, point.Z); + } + + return new BoundingBox3D(new Point3D(minX, minY, minZ), new Point3D(maxX, maxY, maxZ)); + } + + private static CanonicalBounds3 RebuildBounds3(CanonicalBounds3 sourceBounds, Func pointTransform) + { + var corners = GetCorners3(sourceBounds); + Vector3 min = new Vector3(float.MaxValue, float.MaxValue, float.MaxValue); + Vector3 max = new Vector3(float.MinValue, float.MinValue, float.MinValue); + + foreach (var corner in corners) + { + Vector3 point = pointTransform(corner); + min = Vector3.Min(min, point); + max = Vector3.Max(max, point); + } + + return new CanonicalBounds3(min, max); + } + + private static List GetCorners(BoundingBox3D bounds) + { + return new List + { + new Point3D(bounds.Min.X, bounds.Min.Y, bounds.Min.Z), + new Point3D(bounds.Max.X, bounds.Min.Y, bounds.Min.Z), + new Point3D(bounds.Min.X, bounds.Max.Y, bounds.Min.Z), + new Point3D(bounds.Max.X, bounds.Max.Y, bounds.Min.Z), + new Point3D(bounds.Min.X, bounds.Min.Y, bounds.Max.Z), + new Point3D(bounds.Max.X, bounds.Min.Y, bounds.Max.Z), + new Point3D(bounds.Min.X, bounds.Max.Y, bounds.Max.Z), + new Point3D(bounds.Max.X, bounds.Max.Y, bounds.Max.Z) + }; + } + + private static List GetCorners3(CanonicalBounds3 bounds) + { + return new List + { + new Vector3(bounds.Min.X, bounds.Min.Y, bounds.Min.Z), + new Vector3(bounds.Max.X, bounds.Min.Y, bounds.Min.Z), + new Vector3(bounds.Min.X, bounds.Max.Y, bounds.Min.Z), + new Vector3(bounds.Max.X, bounds.Max.Y, bounds.Min.Z), + new Vector3(bounds.Min.X, bounds.Min.Y, bounds.Max.Z), + new Vector3(bounds.Max.X, bounds.Min.Y, bounds.Max.Z), + new Vector3(bounds.Min.X, bounds.Max.Y, bounds.Max.Z), + new Vector3(bounds.Max.X, bounds.Max.Y, bounds.Max.Z) + }; + } + + private static Vector3 ToVector3(Point3D point) + { + return new Vector3((float)point.X, (float)point.Y, (float)point.Z); + } + + private static Vector3 ToNumericsVector(Vector3D vector) + { + return new Vector3((float)vector.X, (float)vector.Y, (float)vector.Z); + } + + private static Point3D ToPoint3D(Vector3 point) + { + return new Point3D(point.X, point.Y, point.Z); + } + + private static Vector3D ToVector3D(Vector3 vector) + { + return new Vector3D(vector.X, vector.Y, vector.Z); + } + } +} diff --git a/src/Utils/CoordinateSystem/LocalAxisDirection.cs b/src/Utils/CoordinateSystem/LocalAxisDirection.cs new file mode 100644 index 0000000..79e061a --- /dev/null +++ b/src/Utils/CoordinateSystem/LocalAxisDirection.cs @@ -0,0 +1,18 @@ +using Autodesk.Navisworks.Api; + +namespace NavisworksTransport.Utils.CoordinateSystem +{ + /// + /// 模型局部轴方向定义。 + /// 用于显式表达本地哪个轴代表 forward/up。 + /// + public enum LocalAxisDirection + { + PositiveX, + PositiveY, + PositiveZ, + NegativeX, + NegativeY, + NegativeZ + } +} diff --git a/src/Utils/CoordinateSystem/ModelAxisConvention.cs b/src/Utils/CoordinateSystem/ModelAxisConvention.cs new file mode 100644 index 0000000..824979d --- /dev/null +++ b/src/Utils/CoordinateSystem/ModelAxisConvention.cs @@ -0,0 +1,233 @@ +using System; +using System.Numerics; +using Autodesk.Navisworks.Api; + +namespace NavisworksTransport.Utils.CoordinateSystem +{ + /// + /// 模型局部轴约定。 + /// 显式定义模型本地哪个轴是前进方向、哪个轴是上方向。 + /// + public sealed class ModelAxisConvention + { + private readonly Vector3 _forwardUnitVector; + private readonly Vector3 _upUnitVector; + + public LocalAxisDirection ForwardAxis { get; } + public LocalAxisDirection UpAxis { get; } + + public ModelAxisConvention(LocalAxisDirection forwardAxis, LocalAxisDirection upAxis) + { + ForwardAxis = forwardAxis; + UpAxis = upAxis; + + _forwardUnitVector = GetAxisVector3(forwardAxis); + _upUnitVector = GetAxisVector3(upAxis); + float dot = Vector3.Dot(_forwardUnitVector, _upUnitVector); + if (Math.Abs(dot) > 1e-6) + { + throw new ArgumentException("ForwardAxis 和 UpAxis 不能平行或重合。"); + } + } + + public Vector3 ForwardUnitVector => _forwardUnitVector; + public Vector3 UpUnitVector => _upUnitVector; + public Vector3D ForwardVector => ToNavVector(_forwardUnitVector); + public Vector3D UpVector => ToNavVector(_upUnitVector); + + /// + /// 根据模型局部轴约定,直接构造“本地 forward/up 对齐到目标世界 forward/up”的线性姿态。 + /// 返回矩阵的 3 列分别表示模型本地 +X/+Y/+Z 在世界中的方向。 + /// + public Matrix3 CreateLinearTransform(Vector3D worldForward, Vector3D worldUp) + { + Matrix4x4 linear = CreateLinearTransform3(ToNumericsVector(worldForward), ToNumericsVector(worldUp)); + return ToNavMatrix3(linear); + } + + public Rotation3D CreateRotation(Vector3D worldForward, Vector3D worldUp) + { + Quaternion quaternion = CreateQuaternion(ToNumericsVector(worldForward), ToNumericsVector(worldUp)); + return new Rotation3D(quaternion.X, quaternion.Y, quaternion.Z, quaternion.W); + } + + public Matrix4x4 CreateLinearTransform3(Vector3 worldForward, Vector3 worldUp) + { + Vector3 normalizedWorldForward = Normalize(worldForward); + Vector3 normalizedWorldUp = Normalize(worldUp); + Vector3 worldRemaining = Normalize(Vector3.Cross(normalizedWorldForward, normalizedWorldUp)); + + Vector3 localRemaining = Normalize(Vector3.Cross(_forwardUnitVector, _upUnitVector)); + + Vector3 worldX = ResolveWorldAxisForLocalPositiveAxis( + 0, + normalizedWorldForward, + normalizedWorldUp, + worldRemaining, + localRemaining); + Vector3 worldY = ResolveWorldAxisForLocalPositiveAxis( + 1, + normalizedWorldForward, + normalizedWorldUp, + worldRemaining, + localRemaining); + Vector3 worldZ = ResolveWorldAxisForLocalPositiveAxis( + 2, + normalizedWorldForward, + normalizedWorldUp, + worldRemaining, + localRemaining); + + return new Matrix4x4( + worldX.X, worldY.X, worldZ.X, 0f, + worldX.Y, worldY.Y, worldZ.Y, 0f, + worldX.Z, worldY.Z, worldZ.Z, 0f, + 0f, 0f, 0f, 1f); + } + + public Quaternion CreateQuaternion(Vector3 worldForward, Vector3 worldUp) + { + return Quaternion.CreateFromRotationMatrix(CreateLinearTransform3(worldForward, worldUp)); + } + + /// + /// 当前项目里,真实模型默认遵循宿主 up 语义: + /// - Z-up 宿主 => 本地 X 前进,本地 Z 向上 + /// - Y-up 宿主 => 本地 X 前进,本地 Y 向上 + /// + public static ModelAxisConvention CreateDefaultForHost(CoordinateSystemType hostType) + { + switch (hostType) + { + case CoordinateSystemType.YUp: + return new ModelAxisConvention(LocalAxisDirection.PositiveX, LocalAxisDirection.PositiveY); + case CoordinateSystemType.ZUp: + default: + return new ModelAxisConvention(LocalAxisDirection.PositiveX, LocalAxisDirection.PositiveZ); + } + } + + private static Vector3 GetAxisVector3(LocalAxisDirection axis) + { + switch (axis) + { + case LocalAxisDirection.PositiveX: return new Vector3(1f, 0f, 0f); + case LocalAxisDirection.PositiveY: return new Vector3(0f, 1f, 0f); + case LocalAxisDirection.PositiveZ: return new Vector3(0f, 0f, 1f); + case LocalAxisDirection.NegativeX: return new Vector3(-1f, 0f, 0f); + case LocalAxisDirection.NegativeY: return new Vector3(0f, -1f, 0f); + case LocalAxisDirection.NegativeZ: return new Vector3(0f, 0f, -1f); + default: + throw new ArgumentOutOfRangeException(nameof(axis), axis, null); + } + } + + private Vector3 ResolveWorldAxisForLocalPositiveAxis( + int localAxisIndex, + Vector3 worldForward, + Vector3 worldUp, + Vector3 worldRemaining, + Vector3 localRemaining) + { + if (MatchesAxis(ForwardAxis, localAxisIndex, out int forwardSign)) + { + return Scale(worldForward, forwardSign); + } + + if (MatchesAxis(UpAxis, localAxisIndex, out int upSign)) + { + return Scale(worldUp, upSign); + } + + LocalAxisDirection remainingAxis = GetAxisDirection(localRemaining); + if (MatchesAxis(remainingAxis, localAxisIndex, out int remainingSign)) + { + return Scale(worldRemaining, remainingSign); + } + + throw new InvalidOperationException("无法为模型局部轴构造世界姿态矩阵。"); + } + + private static bool MatchesAxis(LocalAxisDirection axis, int localAxisIndex, out int sign) + { + switch (axis) + { + case LocalAxisDirection.PositiveX: + sign = 1; + return localAxisIndex == 0; + case LocalAxisDirection.NegativeX: + sign = -1; + return localAxisIndex == 0; + case LocalAxisDirection.PositiveY: + sign = 1; + return localAxisIndex == 1; + case LocalAxisDirection.NegativeY: + sign = -1; + return localAxisIndex == 1; + case LocalAxisDirection.PositiveZ: + sign = 1; + return localAxisIndex == 2; + case LocalAxisDirection.NegativeZ: + sign = -1; + return localAxisIndex == 2; + default: + throw new ArgumentOutOfRangeException(nameof(axis), axis, null); + } + } + + private static LocalAxisDirection GetAxisDirection(Vector3 vector) + { + if (IsApproximately(vector, 1, 0, 0)) return LocalAxisDirection.PositiveX; + if (IsApproximately(vector, -1, 0, 0)) return LocalAxisDirection.NegativeX; + if (IsApproximately(vector, 0, 1, 0)) return LocalAxisDirection.PositiveY; + if (IsApproximately(vector, 0, -1, 0)) return LocalAxisDirection.NegativeY; + if (IsApproximately(vector, 0, 0, 1)) return LocalAxisDirection.PositiveZ; + if (IsApproximately(vector, 0, 0, -1)) return LocalAxisDirection.NegativeZ; + + throw new InvalidOperationException("模型局部第三轴不是标准正交轴,无法确定局部轴约定。"); + } + + private static bool IsApproximately(Vector3 vector, double x, double y, double z) + { + return Math.Abs(vector.X - x) < 1e-6 && + Math.Abs(vector.Y - y) < 1e-6 && + Math.Abs(vector.Z - z) < 1e-6; + } + + private static Vector3 Scale(Vector3 vector, int sign) + { + return sign == 1 + ? vector + : new Vector3(-vector.X, -vector.Y, -vector.Z); + } + + private static Vector3 Normalize(Vector3 vector) + { + float lengthSquared = vector.LengthSquared(); + if (lengthSquared < 1e-12f) + { + throw new InvalidOperationException("无法为模型局部轴构造有效的正交侧向。"); + } + + return Vector3.Normalize(vector); + } + + private static Matrix3 ToNavMatrix3(Matrix4x4 matrix) + { + return new Matrix3( + matrix.M11, matrix.M12, matrix.M13, + matrix.M21, matrix.M22, matrix.M23, + matrix.M31, matrix.M32, matrix.M33); + } + + private static Vector3 ToNumericsVector(Vector3D vector) + { + return new Vector3((float)vector.X, (float)vector.Y, (float)vector.Z); + } + + private static Vector3D ToNavVector(Vector3 vector) + { + return new Vector3D(vector.X, vector.Y, vector.Z); + } + } +} diff --git a/src/Utils/CoordinateSystem/ProjectReferenceFrame.cs b/src/Utils/CoordinateSystem/ProjectReferenceFrame.cs new file mode 100644 index 0000000..e144c6d --- /dev/null +++ b/src/Utils/CoordinateSystem/ProjectReferenceFrame.cs @@ -0,0 +1,97 @@ +using System; +using System.Numerics; +using Autodesk.Navisworks.Api; + +namespace NavisworksTransport.Utils.CoordinateSystem +{ + /// + /// 工程业务基准框架。 + /// + /// 该对象建立在 Canonical Space 之上,用来承载: + /// - 球心 + /// - 项目 up 方向 + /// - 后续的终端安装基准和轨道参考面 + /// + /// 注意: + /// - 这不是宿主坐标系定义 + /// - 这不是 Navisworks 世界坐标 + /// - 这是业务语义层 + /// + public sealed class ProjectReferenceFrame + { + private readonly Vector3 _sphereCenterInCanonical; + private readonly Vector3 _projectUpInCanonical; + + /// + /// Canonical Space 中的球心坐标。 + /// + public Vector3 SphereCenterInCanonical3 => _sphereCenterInCanonical; + public Point3D SphereCenterInCanonical => new Point3D(_sphereCenterInCanonical.X, _sphereCenterInCanonical.Y, _sphereCenterInCanonical.Z); + + /// + /// Canonical Space 中的项目上方向。 + /// 当前固定与 Canonical Space 一致,为 (0,0,1)。 + /// + public Vector3 ProjectUpInCanonical3 => _projectUpInCanonical; + public Vector3D ProjectUpInCanonical => new Vector3D(_projectUpInCanonical.X, _projectUpInCanonical.Y, _projectUpInCanonical.Z); + + /// + /// 真实模型默认局部轴约定。 + /// + public ModelAxisConvention DefaultModelAxisConvention { get; } + + public ProjectReferenceFrame( + Point3D sphereCenterInCanonical, + Vector3D projectUpInCanonical, + ModelAxisConvention defaultModelAxisConvention) + : this( + new Vector3((float)sphereCenterInCanonical.X, (float)sphereCenterInCanonical.Y, (float)sphereCenterInCanonical.Z), + new Vector3((float)projectUpInCanonical.X, (float)projectUpInCanonical.Y, (float)projectUpInCanonical.Z), + defaultModelAxisConvention) + { + } + + public ProjectReferenceFrame( + Vector3 sphereCenterInCanonical, + Vector3 projectUpInCanonical, + ModelAxisConvention defaultModelAxisConvention) + { + if (projectUpInCanonical.LengthSquared() < 1e-12f) + { + throw new ArgumentException("项目 up 方向不能为零向量。", nameof(projectUpInCanonical)); + } + + if (defaultModelAxisConvention == null) + { + throw new ArgumentNullException(nameof(defaultModelAxisConvention)); + } + + _sphereCenterInCanonical = sphereCenterInCanonical; + _projectUpInCanonical = Vector3.Normalize(projectUpInCanonical); + DefaultModelAxisConvention = defaultModelAxisConvention; + } + + /// + /// 创建默认业务基准框架。 + /// + /// 说明: + /// - 当前只是 M1 骨架,先显式使用原点和 CanonicalUp + /// - 后续应改为来自项目配置或显式求解,不应长期依赖该默认值 + /// + public static ProjectReferenceFrame CreateDefault() + { + return CreateDefault(CoordinateSystemType.ZUp); + } + + /// + /// 按宿主坐标系创建默认业务基准框架。 + /// + public static ProjectReferenceFrame CreateDefault(CoordinateSystemType hostType) + { + return new ProjectReferenceFrame( + Vector3.Zero, + HostCoordinateAdapter.CanonicalUpVector3, + ModelAxisConvention.CreateDefaultForHost(hostType)); + } + } +} diff --git a/src/Utils/ModelItemTransformHelper.cs b/src/Utils/ModelItemTransformHelper.cs index b49342a..e32b4db 100644 --- a/src/Utils/ModelItemTransformHelper.cs +++ b/src/Utils/ModelItemTransformHelper.cs @@ -1,5 +1,6 @@ using System; using Autodesk.Navisworks.Api; +using NavisworksTransport.Utils.CoordinateSystem; namespace NavisworksTransport.Utils { @@ -136,6 +137,58 @@ namespace NavisworksTransport.Utils bounds.Center.Z - up.Z * halfHeight); } + /// + /// 使用宿主坐标系的 up 轴,从世界 AABB 中提取“底部锚点”。 + /// 适用于真实模型仍保持宿主坐标语义(如 Y-up 模型)的场景。 + /// + public static Point3D GetHostBottomAnchorPoint(BoundingBox3D bounds, HostCoordinateAdapter adapter) + { + if (bounds == null) + { + return new Point3D(0, 0, 0); + } + + if (adapter == null) + { + throw new ArgumentNullException(nameof(adapter)); + } + + switch (adapter.HostUpAxisIndex) + { + case 1: + return new Point3D(bounds.Center.X, bounds.Min.Y, bounds.Center.Z); + case 2: + default: + return new Point3D(bounds.Center.X, bounds.Center.Y, bounds.Min.Z); + } + } + + /// + /// 使用宿主坐标系的 up 轴,从世界 AABB 中提取高度。 + /// 适用于真实模型仍保持宿主坐标语义(如 Y-up 模型)的场景。 + /// + public static double GetHostHeight(BoundingBox3D bounds, HostCoordinateAdapter adapter) + { + if (bounds == null) + { + return 0.0; + } + + if (adapter == null) + { + throw new ArgumentNullException(nameof(adapter)); + } + + switch (adapter.HostUpAxisIndex) + { + case 1: + return bounds.Max.Y - bounds.Min.Y; + case 2: + default: + return bounds.Max.Z - bounds.Min.Z; + } + } + /// /// 根据世界轴对齐包围盒和物体变换,估算物体在局部坐标系中的尺寸。 /// 适用于刚性箱体类对象,用于顶/底面对接点推导。 diff --git a/src/Utils/RailPathPoseHelper.cs b/src/Utils/RailPathPoseHelper.cs index 9bd81be..378b09f 100644 --- a/src/Utils/RailPathPoseHelper.cs +++ b/src/Utils/RailPathPoseHelper.cs @@ -1,5 +1,7 @@ using System; +using System.Numerics; using Autodesk.Navisworks.Api; +using NavisworksTransport.Utils.CoordinateSystem; namespace NavisworksTransport.Utils { @@ -81,11 +83,17 @@ namespace NavisworksTransport.Utils /// public static Point3D ResolveBottomPosition(PathRoute route, Point3D referencePoint, double objectHeight) { - double bottomOffset = GetBottomZOffset(route, objectHeight); - return new Point3D( - referencePoint.X, - referencePoint.Y, - referencePoint.Z + bottomOffset); + var adapter = CoordinateSystemManager.Instance.CreateHostAdapter(); + var canonicalReferencePoint = adapter.ToCanonicalPoint(referencePoint); + var canonicalUp = HostCoordinateAdapter.CanonicalUp; + double bottomOffset = GetBottomOffsetMagnitude(route, objectHeight); + + var canonicalBottomPoint = new Point3D( + canonicalReferencePoint.X + canonicalUp.X * bottomOffset, + canonicalReferencePoint.Y + canonicalUp.Y * bottomOffset, + canonicalReferencePoint.Z + canonicalUp.Z * bottomOffset); + + return adapter.FromCanonicalPoint(canonicalBottomPoint); } /// @@ -109,13 +117,17 @@ namespace NavisworksTransport.Utils return ResolveBottomPosition(route, referencePoint, objectHeight); } + var adapter = CoordinateSystemManager.Instance.CreateHostAdapter(); + var canonicalReferencePoint = adapter.ToCanonicalPoint(referencePoint); var normal = ResolveRailNormal(previousPoint, referencePoint, nextPoint); double bottomOffset = GetBottomOffsetMagnitude(route, objectHeight); - return new Point3D( - referencePoint.X + normal.X * bottomOffset, - referencePoint.Y + normal.Y * bottomOffset, - referencePoint.Z + normal.Z * bottomOffset); + var canonicalBottomPoint = new Point3D( + canonicalReferencePoint.X + normal.X * bottomOffset, + canonicalReferencePoint.Y + normal.Y * bottomOffset, + canonicalReferencePoint.Z + normal.Z * bottomOffset); + + return adapter.FromCanonicalPoint(canonicalBottomPoint); } /// @@ -137,17 +149,23 @@ namespace NavisworksTransport.Utils if (route.RailPathDefinitionMode == RailPathDefinitionMode.LegacySuspensionPoint) { - return new Point3D( - referencePoint.X, - referencePoint.Y, - referencePoint.Z + centerOffset); + var adapter = CoordinateSystemManager.Instance.CreateHostAdapter(); + var canonicalReferencePoint = adapter.ToCanonicalPoint(referencePoint); + var canonicalCenterPoint = new Point3D( + canonicalReferencePoint.X, + canonicalReferencePoint.Y, + canonicalReferencePoint.Z + centerOffset); + return adapter.FromCanonicalPoint(canonicalCenterPoint); } + var adapterForRail = CoordinateSystemManager.Instance.CreateHostAdapter(); + var canonicalReferencePointForRail = adapterForRail.ToCanonicalPoint(referencePoint); var normal = ResolveRailNormal(previousPoint, referencePoint, nextPoint); - return new Point3D( - referencePoint.X + normal.X * centerOffset, - referencePoint.Y + normal.Y * centerOffset, - referencePoint.Z + normal.Z * centerOffset); + var canonicalCenterPointForRail = new Point3D( + canonicalReferencePointForRail.X + normal.X * centerOffset, + canonicalReferencePointForRail.Y + normal.Y * centerOffset, + canonicalReferencePointForRail.Z + normal.Z * centerOffset); + return adapterForRail.FromCanonicalPoint(canonicalCenterPointForRail); } /// @@ -160,49 +178,72 @@ namespace NavisworksTransport.Utils Point3D currentPoint, Point3D nextPoint, out Matrix3 linearTransform) + { + return TryCreateRailLinearTransform( + previousPoint, + currentPoint, + nextPoint, + ModelAxisConvention.CreateDefaultForHost(CoordinateSystemType.ZUp), + out linearTransform); + } + + /// + /// 根据轨道路径切向和法向创建 Rail 构件完整三维姿态。 + /// 通过模型局部轴约定显式指定本地哪个轴代表 forward/up。 + /// + public static bool TryCreateRailLinearTransform( + Point3D previousPoint, + Point3D currentPoint, + Point3D nextPoint, + ModelAxisConvention axisConvention, + out Matrix3 linearTransform) { linearTransform = null; - var tangent = ResolveTangent(previousPoint, currentPoint, nextPoint); - double tangentLengthSquared = tangent.X * tangent.X + tangent.Y * tangent.Y + tangent.Z * tangent.Z; - if (tangentLengthSquared < TangentEpsilon) + if (axisConvention == null) + { + throw new ArgumentNullException(nameof(axisConvention)); + } + + var adapter = CoordinateSystemManager.Instance.CreateHostAdapter(); + var canonicalPreviousPoint = ToNumerics(adapter.ToCanonicalPoint(previousPoint)); + var canonicalCurrentPoint = ToNumerics(adapter.ToCanonicalPoint(currentPoint)); + var canonicalNextPoint = ToNumerics(adapter.ToCanonicalPoint(nextPoint)); + + if (!CanonicalRailPoseBuilder.TryCreateBasis( + canonicalPreviousPoint, + canonicalCurrentPoint, + canonicalNextPoint, + HostCoordinateAdapter.CanonicalUpVector3, + out var canonicalForward, + out var canonicalLateral, + out var canonicalUp)) { return false; } - tangent = Normalize(tangent); - var normal = Normalize(ResolveRailNormal(previousPoint, currentPoint, nextPoint)); - var lateral = Normalize(Cross(normal, tangent)); + Quaternion canonicalRotation = axisConvention.CreateQuaternion(canonicalForward, canonicalUp); + Matrix4x4 canonicalLinear = Matrix4x4.CreateFromQuaternion(canonicalRotation); - double lateralLengthSquared = lateral.X * lateral.X + lateral.Y * lateral.Y + lateral.Z * lateral.Z; - if (lateralLengthSquared < TangentEpsilon) - { - Vector3D fallbackUp = Math.Abs(tangent.Z) < 0.9 - ? new Vector3D(0, 0, 1) - : new Vector3D(0, 1, 0); - lateral = Normalize(Cross(fallbackUp, tangent)); - lateralLengthSquared = lateral.X * lateral.X + lateral.Y * lateral.Y + lateral.Z * lateral.Z; - if (lateralLengthSquared < TangentEpsilon) - { - return false; - } - } - - normal = Normalize(Cross(tangent, lateral)); + var hostXAxis = Normalize(adapter.FromCanonicalVector(ToNavVector(new Vector3(canonicalLinear.M11, canonicalLinear.M21, canonicalLinear.M31)))); + var hostYAxis = Normalize(adapter.FromCanonicalVector(ToNavVector(new Vector3(canonicalLinear.M12, canonicalLinear.M22, canonicalLinear.M32)))); + var hostZAxis = Normalize(adapter.FromCanonicalVector(ToNavVector(new Vector3(canonicalLinear.M13, canonicalLinear.M23, canonicalLinear.M33)))); // Navisworks 在 Transform3D(Matrix3, ...) 中按列读取局部基向量: // 第 1 列 = 本地 X 轴在世界中的方向 // 第 2 列 = 本地 Y 轴在世界中的方向 // 第 3 列 = 本地 Z 轴在世界中的方向 linearTransform = new Matrix3( - tangent.X, lateral.X, normal.X, - tangent.Y, lateral.Y, normal.Y, - tangent.Z, lateral.Z, normal.Z); + hostXAxis.X, hostYAxis.X, hostZAxis.X, + hostXAxis.Y, hostYAxis.Y, hostZAxis.Y, + hostXAxis.Z, hostYAxis.Z, hostZAxis.Z); LogManager.Info( - $"[Rail姿态] 切向=({tangent.X:F4},{tangent.Y:F4},{tangent.Z:F4}), " + - $"侧向=({lateral.X:F4},{lateral.Y:F4},{lateral.Z:F4}), " + - $"法向=({normal.X:F4},{normal.Y:F4},{normal.Z:F4})"); + $"[Rail姿态] Canonical切向=({canonicalForward.X:F4},{canonicalForward.Y:F4},{canonicalForward.Z:F4}), " + + $"Canonical侧向=({canonicalLateral.X:F4},{canonicalLateral.Y:F4},{canonicalLateral.Z:F4}), " + + $"Canonical法向=({canonicalUp.X:F4},{canonicalUp.Y:F4},{canonicalUp.Z:F4}), " + + $"Host法向=({hostZAxis.X:F4},{hostZAxis.Y:F4},{hostZAxis.Z:F4}), " + + $"模型Forward={axisConvention.ForwardAxis}, 模型Up={axisConvention.UpAxis}"); return true; } @@ -215,11 +256,26 @@ namespace NavisworksTransport.Utils Point3D currentPoint, Point3D nextPoint, out Rotation3D rotation) + { + return TryCreateRailRotation( + previousPoint, + currentPoint, + nextPoint, + ModelAxisConvention.CreateDefaultForHost(CoordinateSystemType.ZUp), + out rotation); + } + + public static bool TryCreateRailRotation( + Point3D previousPoint, + Point3D currentPoint, + Point3D nextPoint, + ModelAxisConvention axisConvention, + out Rotation3D rotation) { rotation = Rotation3D.Identity; LogRotationConstructorConventionOnce(); - if (!TryCreateRailLinearTransform(previousPoint, currentPoint, nextPoint, out var linearTransform)) + if (!TryCreateRailLinearTransform(previousPoint, currentPoint, nextPoint, axisConvention, out var linearTransform)) { return false; } @@ -362,7 +418,7 @@ namespace NavisworksTransport.Utils if (tangentLengthSquared < TangentEpsilon) { - return new Vector3D(0, 0, 1); + return HostCoordinateAdapter.CanonicalUp; } tangent = new Vector3D( @@ -370,7 +426,7 @@ namespace NavisworksTransport.Utils tangent.Y / Math.Sqrt(tangentLengthSquared), tangent.Z / Math.Sqrt(tangentLengthSquared)); - var worldUp = new Vector3D(0, 0, 1); + var worldUp = HostCoordinateAdapter.CanonicalUp; double projection = worldUp.X * tangent.X + worldUp.Y * tangent.Y + worldUp.Z * tangent.Z; var normal = new Vector3D( worldUp.X - projection * tangent.X, @@ -380,7 +436,7 @@ namespace NavisworksTransport.Utils double normalLengthSquared = normal.X * normal.X + normal.Y * normal.Y + normal.Z * normal.Z; if (normalLengthSquared < TangentEpsilon) { - return new Vector3D(0, 0, 1); + return HostCoordinateAdapter.CanonicalUp; } double normalLength = Math.Sqrt(normalLengthSquared); @@ -392,18 +448,23 @@ namespace NavisworksTransport.Utils private static Vector3D ResolveTangent(Point3D previousPoint, Point3D currentPoint, Point3D nextPoint) { + var adapter = CoordinateSystemManager.Instance.CreateHostAdapter(); + var canonicalPreviousPoint = adapter.ToCanonicalPoint(previousPoint); + var canonicalCurrentPoint = adapter.ToCanonicalPoint(currentPoint); + var canonicalNextPoint = adapter.ToCanonicalPoint(nextPoint); + var tangent = new Vector3D( - nextPoint.X - previousPoint.X, - nextPoint.Y - previousPoint.Y, - nextPoint.Z - previousPoint.Z); + canonicalNextPoint.X - canonicalPreviousPoint.X, + canonicalNextPoint.Y - canonicalPreviousPoint.Y, + canonicalNextPoint.Z - canonicalPreviousPoint.Z); double tangentLengthSquared = tangent.X * tangent.X + tangent.Y * tangent.Y + tangent.Z * tangent.Z; if (tangentLengthSquared < TangentEpsilon) { tangent = new Vector3D( - nextPoint.X - currentPoint.X, - nextPoint.Y - currentPoint.Y, - nextPoint.Z - currentPoint.Z); + canonicalNextPoint.X - canonicalCurrentPoint.X, + canonicalNextPoint.Y - canonicalCurrentPoint.Y, + canonicalNextPoint.Z - canonicalCurrentPoint.Z); } return tangent; @@ -428,5 +489,15 @@ namespace NavisworksTransport.Utils a.Z * b.X - a.X * b.Z, a.X * b.Y - a.Y * b.X); } + + private static Vector3 ToNumerics(Point3D point) + { + return new Vector3((float)point.X, (float)point.Y, (float)point.Z); + } + + private static Vector3D ToNavVector(Vector3 vector) + { + return new Vector3D(vector.X, vector.Y, vector.Z); + } } }