Establish canonical coordinate framework with tests
This commit is contained in:
parent
a651b69459
commit
3fd184934b
69
NavisworksTransport.UnitTests.csproj
Normal file
69
NavisworksTransport.UnitTests.csproj
Normal 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>
|
||||
@ -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" />
|
||||
|
||||
@ -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
|
||||
|
||||
92
UnitTests/CoordinateSystem/CanonicalRailPoseBuilderTests.cs
Normal file
92
UnitTests/CoordinateSystem/CanonicalRailPoseBuilderTests.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
69
UnitTests/CoordinateSystem/HostCoordinateAdapterTests.cs
Normal file
69
UnitTests/CoordinateSystem/HostCoordinateAdapterTests.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
87
UnitTests/CoordinateSystem/ModelAxisConventionTests.cs
Normal file
87
UnitTests/CoordinateSystem/ModelAxisConventionTests.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
38
UnitTests/CoordinateSystem/ProjectReferenceFrameTests.cs
Normal file
38
UnitTests/CoordinateSystem/ProjectReferenceFrameTests.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
15
UnitTests/Properties/AssemblyInfo.cs
Normal file
15
UnitTests/Properties/AssemblyInfo.cs
Normal 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")]
|
||||
503
doc/design/2026/coordinate-system-canonical-space-design.md
Normal file
503
doc/design/2026/coordinate-system-canonical-space-design.md
Normal 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 Space(Z-up)**
|
||||
- **业务基准点单独建模**
|
||||
- **坐标转换只发生在边界层**
|
||||
|
||||
这是一条更接近业界三维软件成熟做法的路线。
|
||||
@ -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. 内存管理与性能优化
|
||||
|
||||
### 问题描述
|
||||
|
||||
@ -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
50
run-unit-tests.bat
Normal 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!
|
||||
@ -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>
|
||||
|
||||
@ -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)));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
};
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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"/>
|
||||
|
||||
20
src/Utils/CoordinateSystem/CanonicalBounds3.cs
Normal file
20
src/Utils/CoordinateSystem/CanonicalBounds3.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
83
src/Utils/CoordinateSystem/CanonicalRailPoseBuilder.cs
Normal file
83
src/Utils/CoordinateSystem/CanonicalRailPoseBuilder.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
274
src/Utils/CoordinateSystem/HostCoordinateAdapter.cs
Normal file
274
src/Utils/CoordinateSystem/HostCoordinateAdapter.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
18
src/Utils/CoordinateSystem/LocalAxisDirection.cs
Normal file
18
src/Utils/CoordinateSystem/LocalAxisDirection.cs
Normal 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
|
||||
}
|
||||
}
|
||||
233
src/Utils/CoordinateSystem/ModelAxisConvention.cs
Normal file
233
src/Utils/CoordinateSystem/ModelAxisConvention.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
97
src/Utils/CoordinateSystem/ProjectReferenceFrame.cs
Normal file
97
src/Utils/CoordinateSystem/ProjectReferenceFrame.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
/// 根据世界轴对齐包围盒和物体变换,估算物体在局部坐标系中的尺寸。
|
||||
/// 适用于刚性箱体类对象,用于顶/底面对接点推导。
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user