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