Establish canonical coordinate framework with tests

This commit is contained in:
tian 2026-03-21 06:10:34 +08:00
parent a651b69459
commit 3fd184934b
26 changed files with 2809 additions and 176 deletions

View File

@ -0,0 +1,69 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">x64</Platform>
<ProjectGuid>{7DDB41A4-A10B-4EA4-A658-0D5A9178A1F5}</ProjectGuid>
<OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>NavisworksTransport.UnitTests</RootNamespace>
<AssemblyName>NavisworksTransport.UnitTests</AssemblyName>
<TargetFrameworkVersion>v4.8</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<Deterministic>true</Deterministic>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'">
<DebugSymbols>true</DebugSymbols>
<OutputPath>bin\x64\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<DebugType>full</DebugType>
<PlatformTarget>x64</PlatformTarget>
<LangVersion>7.3</LangVersion>
<ErrorReport>prompt</ErrorReport>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'">
<OutputPath>bin\x64\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<Optimize>true</Optimize>
<DebugType>pdbonly</DebugType>
<PlatformTarget>x64</PlatformTarget>
<LangVersion>7.3</LangVersion>
<ErrorReport>prompt</ErrorReport>
</PropertyGroup>
<ItemGroup>
<Reference Include="Autodesk.Navisworks.Api">
<HintPath>..\..\..\..\Program Files\Autodesk\Navisworks Manage 2026\Autodesk.Navisworks.Api.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="Microsoft.VisualStudio.TestPlatform.TestFramework">
<HintPath>packages\MSTest.TestFramework.3.0.4\lib\net462\Microsoft.VisualStudio.TestPlatform.TestFramework.dll</HintPath>
</Reference>
<Reference Include="Microsoft.VisualStudio.TestPlatform.TestFramework.Extensions">
<HintPath>packages\MSTest.TestFramework.3.0.4\lib\net462\Microsoft.VisualStudio.TestPlatform.TestFramework.Extensions.dll</HintPath>
</Reference>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Numerics" />
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Data" />
<Reference Include="System.Net.Http" />
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
<Compile Include="UnitTests\CoordinateSystem\HostCoordinateAdapterTests.cs" />
<Compile Include="UnitTests\CoordinateSystem\CanonicalRailPoseBuilderTests.cs" />
<Compile Include="UnitTests\CoordinateSystem\ModelAxisConventionTests.cs" />
<Compile Include="UnitTests\CoordinateSystem\ProjectReferenceFrameTests.cs" />
<Compile Include="UnitTests\Properties\AssemblyInfo.cs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="TransportPlugin.csproj">
<Project>{1A0124F6-3DEB-4153-8760-F568AD9393EE}</Project>
<Name>TransportPlugin</Name>
</ProjectReference>
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>

View File

@ -63,6 +63,7 @@
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Drawing" />
<Reference Include="System.Numerics" />
<Reference Include="System.Web.Extensions" />
<Reference Include="Newtonsoft.Json">
<HintPath>packages\Newtonsoft.Json.13.0.3\lib\net45\Newtonsoft.Json.dll</HintPath>
@ -330,6 +331,12 @@
<!-- Coordinate System -->
<Compile Include="src\Utils\CoordinateSystem\CoordinateSystemType.cs" />
<Compile Include="src\Utils\CoordinateSystem\ICoordinateSystem.cs" />
<Compile Include="src\Utils\CoordinateSystem\CanonicalBounds3.cs" />
<Compile Include="src\Utils\CoordinateSystem\CanonicalRailPoseBuilder.cs" />
<Compile Include="src\Utils\CoordinateSystem\HostCoordinateAdapter.cs" />
<Compile Include="src\Utils\CoordinateSystem\LocalAxisDirection.cs" />
<Compile Include="src\Utils\CoordinateSystem\ModelAxisConvention.cs" />
<Compile Include="src\Utils\CoordinateSystem\ProjectReferenceFrame.cs" />
<Compile Include="src\Utils\CoordinateSystem\ZUpCoordinateSystem.cs" />
<Compile Include="src\Utils\CoordinateSystem\YUpCoordinateSystem.cs" />
<Compile Include="src\Utils\CoordinateSystem\CoordinateSystemManager.cs" />

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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")]

View File

@ -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 SpaceZ-up**
- **业务基准点单独建模**
- **坐标转换只发生在边界层**
这是一条更接近业界三维软件成熟做法的路线。

View File

@ -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. 内存管理与性能优化
### 问题描述

View File

@ -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. 备注
本清单服务于“先稳定非自动路径规划主线”的目标,不代表自动路径规划相关模块不重要。
后续如要继续推进全项目坐标系统一,应单独启动自动路径规划相关的第二阶段清理计划。

50
run-unit-tests.bat Normal file
View File

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

View File

@ -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);
}
/// <summary>
@ -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);
}
/// <summary>
/// Rail 路径的目标姿态以“本地 X 为前进方向、本地 Z 为上方向”为约定。
/// 真实模型在 Y-up 宿主里通常仍保持“本地 Y 为上方向”的资源语义,
/// 因此需要补一个基准修正:绕本地 X 轴 +90°使模型的 Y-up 语义映射到 Rail 的 Z-up 期望语义。
/// 虚拟物体资源本身按 Z-up 构建,不需要此修正。
/// </summary>
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;
}
/// <summary>
/// 设置物体角度修正值(度,顺时针)
/// </summary>

View File

@ -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<Point3D>();
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);
// 计算垂直分量的长度
var verticalComponentLength = Math.Abs(verticalComponent.Z);
if (verticalComponentLength < 0.001)
{
// 如果up向量几乎是水平的则直接使用纯Z方向
return new Point3D(point.X, point.Y, point.Z + verticalOffset);
return new Point3D(
point.X + normalizedUp.X * verticalOffset,
point.Y + normalizedUp.Y * verticalOffset,
point.Z + normalizedUp.Z * verticalOffset
);
}
// 沿着垂直分量方向平移
// 需要调整偏移量因为垂直分量的长度可能不是1
var adjustedOffset = verticalOffset * (Math.Sign(verticalComponent.Z) / verticalComponentLength);
return new Point3D(
point.X,
point.Y,
point.Z + adjustedOffset
);
private static Vector3D GetHostUpVector()
{
var adapter = CoordinateSystemManager.Instance.CreateHostAdapter();
return Normalize(adapter.FromCanonicalVector(HostCoordinateAdapter.CanonicalUp));
}
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)
{
return new Vector3D(0, 0, 0);
}
double length = Math.Sqrt(lengthSquared);
return new Vector3D(vector.X / length, vector.Y / length, vector.Z / length);
}
/// <summary>
@ -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)));
}
}

View File

@ -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<ModelItem>();
@ -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<ModelItem>(),
TotalBounds = emptyBounds
};

View File

@ -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> { Point3D.Origin, centerPoint },
new List<Point3D> { 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;

View File

@ -352,6 +352,46 @@ NavisworksTransport 路径编辑页签视图 - 采用与动画控制和分层管
VerticalAlignment="Center"
TextWrapping="Wrap"/>
</Grid>
<Grid Margin="0,6,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Grid.Column="0"
Content="球心坐标:"
Style="{StaticResource ParameterLabelStyle}"
VerticalAlignment="Center"/>
<TextBox Grid.Column="1"
Text="{Binding AssemblySphereCenterX, StringFormat=0.0###}"
Style="{StaticResource ParameterInputStyle}"/>
<Label Grid.Column="2"
Content="X"
Style="{StaticResource UnitLabelStyle}"/>
<TextBox Grid.Column="3"
Text="{Binding AssemblySphereCenterY, StringFormat=0.0###}"
Style="{StaticResource ParameterInputStyle}"/>
<Label Grid.Column="4"
Content="Y"
Style="{StaticResource UnitLabelStyle}"/>
<TextBox Grid.Column="5"
Text="{Binding AssemblySphereCenterZ, StringFormat=0.0###}"
Style="{StaticResource ParameterInputStyle}"/>
<Label Grid.Column="6"
Content="Z"
Style="{StaticResource UnitLabelStyle}"/>
<TextBlock Grid.Column="7"
Margin="8,0,0,0"
Text="使用 Navisworks 当前模型坐标,默认 0,0,0"
Style="{StaticResource StatusTextStyle}"
VerticalAlignment="Center"
TextWrapping="Wrap"/>
</Grid>
<Grid Margin="0,6,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>

View File

@ -0,0 +1,20 @@
using System.Numerics;
namespace NavisworksTransport.Utils.CoordinateSystem
{
/// <summary>
/// 内部 Canonical Space 使用的纯数学轴对齐包围盒。
/// 不依赖 Navisworks API便于单元测试。
/// </summary>
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);
}
}
}

View File

@ -0,0 +1,83 @@
using System;
using System.Numerics;
namespace NavisworksTransport.Utils.CoordinateSystem
{
/// <summary>
/// 在内部 Canonical Space 中构造 Rail 姿态。
/// 这一层只处理纯数学切向/法向/局部轴约定,不依赖 Navisworks API。
/// </summary>
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;
}
}
}

View File

@ -37,6 +37,12 @@ namespace NavisworksTransport.Utils.CoordinateSystem
/// </summary>
public CoordinateSystemType ConfiguredType => _configuredType;
/// <summary>
/// 解析后的宿主坐标系类型。
/// AutoDetect 模式下返回当前实际检测结果。
/// </summary>
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})";
}
/// <summary>
/// 获取当前宿主坐标到 Canonical Space 的统一适配器。
/// </summary>
public HostCoordinateAdapter CreateHostAdapter()
{
return new HostCoordinateAdapter(GetResolvedType());
}
#endregion
#region
@ -193,6 +207,27 @@ namespace NavisworksTransport.Utils.CoordinateSystem
}
}
/// <summary>
/// 获取当前解析后的宿主坐标系类型。
/// </summary>
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
}
}

View File

@ -0,0 +1,274 @@
using System;
using System.Collections.Generic;
using System.Numerics;
using Autodesk.Navisworks.Api;
namespace NavisworksTransport.Utils.CoordinateSystem
{
/// <summary>
/// Navisworks 外部坐标与内部 Canonical Space 之间的统一适配器。
///
/// 约定:
/// - 外部坐标 = Navisworks API 返回的世界坐标
/// - 内部坐标 = Canonical Space固定为 Z-up
///
/// 当前仅支持:
/// - Host Z-up -> Canonical Z-up恒等
/// - Host Y-up -> Canonical Z-up绕 X 轴 +90° 的等价变换)
/// </summary>
public sealed class HostCoordinateAdapter
{
/// <summary>
/// Canonical Space 固定为 Z-up。
/// </summary>
public static readonly Vector3 CanonicalUpVector3 = new Vector3(0f, 0f, 1f);
public static readonly Vector3D CanonicalUp = new Vector3D(0, 0, 1);
/// <summary>
/// 当前宿主坐标系类型。
/// </summary>
public CoordinateSystemType HostType { get; }
/// <summary>
/// 宿主坐标系中“向上轴”的索引。
/// Y-up => 1, Z-up => 2。
/// </summary>
public int HostUpAxisIndex => HostType == CoordinateSystemType.YUp ? 1 : 2;
/// <summary>
/// 宿主坐标系中的单位 up 向量。
/// </summary>
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;
}
/// <summary>
/// 从当前坐标系管理器创建适配器。
/// </summary>
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<Point3D, Point3D> pointTransform)
{
var corners = GetCorners(sourceBounds);
var transformedCorners = new List<Point3D>(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<Vector3, Vector3> 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<Point3D> GetCorners(BoundingBox3D bounds)
{
return new List<Point3D>
{
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<Vector3> GetCorners3(CanonicalBounds3 bounds)
{
return new List<Vector3>
{
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);
}
}
}

View File

@ -0,0 +1,18 @@
using Autodesk.Navisworks.Api;
namespace NavisworksTransport.Utils.CoordinateSystem
{
/// <summary>
/// 模型局部轴方向定义。
/// 用于显式表达本地哪个轴代表 forward/up。
/// </summary>
public enum LocalAxisDirection
{
PositiveX,
PositiveY,
PositiveZ,
NegativeX,
NegativeY,
NegativeZ
}
}

View File

@ -0,0 +1,233 @@
using System;
using System.Numerics;
using Autodesk.Navisworks.Api;
namespace NavisworksTransport.Utils.CoordinateSystem
{
/// <summary>
/// 模型局部轴约定。
/// 显式定义模型本地哪个轴是前进方向、哪个轴是上方向。
/// </summary>
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);
/// <summary>
/// 根据模型局部轴约定,直接构造“本地 forward/up 对齐到目标世界 forward/up”的线性姿态。
/// 返回矩阵的 3 列分别表示模型本地 +X/+Y/+Z 在世界中的方向。
/// </summary>
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));
}
/// <summary>
/// 当前项目里,真实模型默认遵循宿主 up 语义:
/// - Z-up 宿主 => 本地 X 前进,本地 Z 向上
/// - Y-up 宿主 => 本地 X 前进,本地 Y 向上
/// </summary>
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);
}
}
}

View File

@ -0,0 +1,97 @@
using System;
using System.Numerics;
using Autodesk.Navisworks.Api;
namespace NavisworksTransport.Utils.CoordinateSystem
{
/// <summary>
/// 工程业务基准框架。
///
/// 该对象建立在 Canonical Space 之上,用来承载:
/// - 球心
/// - 项目 up 方向
/// - 后续的终端安装基准和轨道参考面
///
/// 注意:
/// - 这不是宿主坐标系定义
/// - 这不是 Navisworks 世界坐标
/// - 这是业务语义层
/// </summary>
public sealed class ProjectReferenceFrame
{
private readonly Vector3 _sphereCenterInCanonical;
private readonly Vector3 _projectUpInCanonical;
/// <summary>
/// Canonical Space 中的球心坐标。
/// </summary>
public Vector3 SphereCenterInCanonical3 => _sphereCenterInCanonical;
public Point3D SphereCenterInCanonical => new Point3D(_sphereCenterInCanonical.X, _sphereCenterInCanonical.Y, _sphereCenterInCanonical.Z);
/// <summary>
/// Canonical Space 中的项目上方向。
/// 当前固定与 Canonical Space 一致,为 (0,0,1)。
/// </summary>
public Vector3 ProjectUpInCanonical3 => _projectUpInCanonical;
public Vector3D ProjectUpInCanonical => new Vector3D(_projectUpInCanonical.X, _projectUpInCanonical.Y, _projectUpInCanonical.Z);
/// <summary>
/// 真实模型默认局部轴约定。
/// </summary>
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;
}
/// <summary>
/// 创建默认业务基准框架。
///
/// 说明:
/// - 当前只是 M1 骨架,先显式使用原点和 CanonicalUp
/// - 后续应改为来自项目配置或显式求解,不应长期依赖该默认值
/// </summary>
public static ProjectReferenceFrame CreateDefault()
{
return CreateDefault(CoordinateSystemType.ZUp);
}
/// <summary>
/// 按宿主坐标系创建默认业务基准框架。
/// </summary>
public static ProjectReferenceFrame CreateDefault(CoordinateSystemType hostType)
{
return new ProjectReferenceFrame(
Vector3.Zero,
HostCoordinateAdapter.CanonicalUpVector3,
ModelAxisConvention.CreateDefaultForHost(hostType));
}
}
}

View File

@ -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);
}
/// <summary>
/// 使用宿主坐标系的 up 轴,从世界 AABB 中提取“底部锚点”。
/// 适用于真实模型仍保持宿主坐标语义(如 Y-up 模型)的场景。
/// </summary>
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);
}
}
/// <summary>
/// 使用宿主坐标系的 up 轴,从世界 AABB 中提取高度。
/// 适用于真实模型仍保持宿主坐标语义(如 Y-up 模型)的场景。
/// </summary>
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;
}
}
/// <summary>
/// 根据世界轴对齐包围盒和物体变换,估算物体在局部坐标系中的尺寸。
/// 适用于刚性箱体类对象,用于顶/底面对接点推导。

View File

@ -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
/// </summary>
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);
}
/// <summary>
@ -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);
}
/// <summary>
@ -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);
}
/// <summary>
@ -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);
}
/// <summary>
/// 根据轨道路径切向和法向创建 Rail 构件完整三维姿态。
/// 通过模型局部轴约定显式指定本地哪个轴代表 forward/up。
/// </summary>
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);
}
}
}