Checkpoint animation rotation refactor state

This commit is contained in:
tian 2026-03-23 21:00:02 +08:00
parent 1cf4fe5967
commit d52b1aef08
23 changed files with 1015 additions and 238 deletions

View File

@ -308,6 +308,19 @@ double distance = GeometryHelper.Distance(pointA, pointB);
- 需要坐标转换?→ 使用 `CoordinateConverter` - 需要坐标转换?→ 使用 `CoordinateConverter`
- 需要路径相关工具?→ 使用 `PathHelper` - 需要路径相关工具?→ 使用 `PathHelper`
#### 5. 临时定位代码必须回收
为定位问题临时加入的兜底调用、强制刷新、额外同步、绕过分支等代码,如果后续确认**不是真正根因**,在根因修复后必须恢复或删除,不能把这类临时补丁长期保留在正式代码中。
```csharp
// ❌ 错误:为掩盖问题长期保留的强制调用
DoNormalUpdate();
ForceRefreshAgain(); // 只是为了“看起来好了”
// ✅ 正确:先定位根因,再移除临时补丁
DoNormalUpdate();
```
### 单位系统 - 极其重要 ### 单位系统 - 极其重要
**所有网格地图和路径规划计算必须使用模型单位,严禁混用米制单位。** **所有网格地图和路径规划计算必须使用模型单位,严禁混用米制单位。**

View File

@ -334,11 +334,14 @@
<Compile Include="src\Utils\CoordinateSystem\CoordinateSystemType.cs" /> <Compile Include="src\Utils\CoordinateSystem\CoordinateSystemType.cs" />
<Compile Include="src\Utils\CoordinateSystem\ICoordinateSystem.cs" /> <Compile Include="src\Utils\CoordinateSystem\ICoordinateSystem.cs" />
<Compile Include="src\Utils\CoordinateSystem\CanonicalBounds3.cs" /> <Compile Include="src\Utils\CoordinateSystem\CanonicalBounds3.cs" />
<Compile Include="src\Utils\CoordinateSystem\LocalAxisPoseBuilder.cs" />
<Compile Include="src\Utils\CoordinateSystem\LocalEulerRotationCorrection.cs" />
<Compile Include="src\Utils\CoordinateSystem\CanonicalPlanarPoseBuilder.cs" /> <Compile Include="src\Utils\CoordinateSystem\CanonicalPlanarPoseBuilder.cs" />
<Compile Include="src\Utils\CoordinateSystem\CanonicalRailOffsetResolver.cs" /> <Compile Include="src\Utils\CoordinateSystem\CanonicalRailOffsetResolver.cs" />
<Compile Include="src\Utils\CoordinateSystem\CanonicalRailPoseBuilder.cs" /> <Compile Include="src\Utils\CoordinateSystem\CanonicalRailPoseBuilder.cs" />
<Compile Include="src\Utils\CoordinateSystem\CanonicalTrackedPositionResolver.cs" /> <Compile Include="src\Utils\CoordinateSystem\CanonicalTrackedPositionResolver.cs" />
<Compile Include="src\Utils\CoordinateSystem\ObjectSpaceOrientationHelper.cs" /> <Compile Include="src\Utils\CoordinateSystem\ObjectSpaceOrientationHelper.cs" />
<Compile Include="src\Utils\CoordinateSystem\RotatedObjectExtentHelper.cs" />
<Compile Include="src\Utils\CoordinateSystem\HostCoordinateAdapter.cs" /> <Compile Include="src\Utils\CoordinateSystem\HostCoordinateAdapter.cs" />
<Compile Include="src\Utils\CoordinateSystem\HoistingCoordinateHelper.cs" /> <Compile Include="src\Utils\CoordinateSystem\HoistingCoordinateHelper.cs" />
<Compile Include="src\Utils\CoordinateSystem\LocalAxisDirection.cs" /> <Compile Include="src\Utils\CoordinateSystem\LocalAxisDirection.cs" />

View File

@ -1,5 +1,6 @@
using Microsoft.VisualStudio.TestTools.UnitTesting; using Microsoft.VisualStudio.TestTools.UnitTesting;
using NavisworksTransport.Utils.CoordinateSystem; using NavisworksTransport.Utils.CoordinateSystem;
using System;
using System.Numerics; using System.Numerics;
namespace NavisworksTransport.UnitTests.CoordinateSystem namespace NavisworksTransport.UnitTests.CoordinateSystem
@ -55,6 +56,52 @@ namespace NavisworksTransport.UnitTests.CoordinateSystem
Assert.AreEqual(1.0, linear.M33, 1e-6); Assert.AreEqual(1.0, linear.M33, 1e-6);
} }
[TestMethod]
public void ZeroLocalEulerCorrection_ShouldMatchCurrentPlanarBaseline()
{
var convention = ModelAxisConvention.CreateDefaultForHost(CoordinateSystemType.ZUp);
Assert.IsTrue(CanonicalPlanarPoseBuilder.TryCreateQuaternionFromForward(
new Vector3(5, 0, 0),
Vector3.UnitZ,
convention,
out Quaternion baselineRotation));
Assert.IsTrue(CanonicalPlanarPoseBuilder.TryCreateQuaternionFromForward(
new Vector3(5, 0, 0),
Vector3.UnitZ,
convention,
LocalEulerRotationCorrection.Zero,
out Quaternion correctedRotation));
AssertQuaternionEquivalent(baselineRotation, correctedRotation);
}
[TestMethod]
public void LocalEulerCorrection_ShouldAlignCorrectedLocalForwardWithPlanarForward()
{
var convention = ModelAxisConvention.CreateDefaultForHost(CoordinateSystemType.ZUp);
var correction = new LocalEulerRotationCorrection(0.0, 0.0, 90.0);
var adapter = new HostCoordinateAdapter(CoordinateSystemType.ZUp);
Quaternion correctionQuaternion = adapter.CreateCanonicalRotationCorrection(correction);
Assert.IsTrue(CanonicalPlanarPoseBuilder.TryCreateQuaternionFromForward(
new Vector3(5, 0, 0),
Vector3.UnitZ,
convention,
correctionQuaternion,
out Quaternion rotation));
LocalAxisPoseBuilder.ApplyLocalPreRotation(convention, correctionQuaternion, out var correctedLocalForward, out var correctedLocalUp);
Matrix4x4 linear = Matrix4x4.CreateFromQuaternion(rotation);
Vector3 mappedForward = TransformLocalVector(linear, correctedLocalForward);
Vector3 mappedUp = TransformLocalVector(linear, correctedLocalUp);
AssertVector(mappedForward, 1, 0, 0);
AssertVector(mappedUp, 0, 0, 1);
}
private static void AssertColumn(Matrix4x4 matrix, int column, double x, double y, double z) private static void AssertColumn(Matrix4x4 matrix, int column, double x, double y, double z)
{ {
switch (column) switch (column)
@ -79,5 +126,32 @@ namespace NavisworksTransport.UnitTests.CoordinateSystem
break; break;
} }
} }
private static void AssertVector(Vector3 actual, double x, double y, double z)
{
Assert.AreEqual(x, actual.X, 1e-6);
Assert.AreEqual(y, actual.Y, 1e-6);
Assert.AreEqual(z, actual.Z, 1e-6);
}
private static void AssertQuaternionEquivalent(Quaternion expected, Quaternion actual)
{
double dot = Math.Abs(
expected.X * actual.X +
expected.Y * actual.Y +
expected.Z * actual.Z +
expected.W * actual.W);
Assert.AreEqual(1.0, dot, 1e-6);
}
private static Vector3 TransformLocalVector(Matrix4x4 linear, Vector3 localVector)
{
Vector3 world = new Vector3(
linear.M11 * localVector.X + linear.M12 * localVector.Y + linear.M13 * localVector.Z,
linear.M21 * localVector.X + linear.M22 * localVector.Y + linear.M23 * localVector.Z,
linear.M31 * localVector.X + linear.M32 * localVector.Y + linear.M33 * localVector.Z);
return Vector3.Normalize(world);
}
} }
} }

View File

@ -137,6 +137,34 @@ namespace NavisworksTransport.UnitTests.CoordinateSystem
AssertVector(mappedUp, 0, 0, 1); AssertVector(mappedUp, 0, 0, 1);
} }
[TestMethod]
public void LocalEulerCorrection_ShouldAlignCorrectedLocalForwardWithRailTangent()
{
var convention = ModelAxisConvention.CreateRailAssetConvention();
var correction = new LocalEulerRotationCorrection(0.0, 90.0, 0.0);
var adapter = new HostCoordinateAdapter(CoordinateSystemType.ZUp);
Quaternion correctionQuaternion = adapter.CreateCanonicalRotationCorrection(correction);
Assert.IsTrue(CanonicalRailPoseBuilder.TryCreateQuaternion(
new Vector3(0, 0, 0),
new Vector3(1, 0, 0),
new Vector3(2, 0, 0),
Vector3.UnitZ,
convention,
null,
correctionQuaternion,
out Quaternion rotation));
LocalAxisPoseBuilder.ApplyLocalPreRotation(convention, correctionQuaternion, out var correctedLocalForward, out var correctedLocalUp);
Matrix4x4 linear = Matrix4x4.CreateFromQuaternion(rotation);
Vector3 mappedForward = TransformLocalVector(linear, correctedLocalForward);
Vector3 mappedUp = TransformLocalVector(linear, correctedLocalUp);
AssertVector(mappedForward, 1, 0, 0);
AssertVector(mappedUp, 0, 0, 1);
}
[TestMethod] [TestMethod]
public void PreferredNormal_ShouldOverrideCanonicalUpForRailFrame() public void PreferredNormal_ShouldOverrideCanonicalUpForRailFrame()
{ {

View File

@ -1,6 +1,7 @@
using Microsoft.VisualStudio.TestTools.UnitTesting; using Microsoft.VisualStudio.TestTools.UnitTesting;
using NavisworksTransport.Utils.CoordinateSystem; using NavisworksTransport.Utils.CoordinateSystem;
using System.Numerics; using System.Numerics;
using System;
using Autodesk.Navisworks.Api; using Autodesk.Navisworks.Api;
namespace NavisworksTransport.UnitTests.CoordinateSystem namespace NavisworksTransport.UnitTests.CoordinateSystem
@ -143,6 +144,20 @@ namespace NavisworksTransport.UnitTests.CoordinateSystem
Assert.AreEqual(0.0000, hostLinear.M33, 1e-3); Assert.AreEqual(0.0000, hostLinear.M33, 1e-3);
} }
[TestMethod]
public void YUp_HostRotationCorrection_ShouldMapHostYAxisToCanonicalUp()
{
var adapter = new HostCoordinateAdapter(CoordinateSystemType.YUp);
var correction = new LocalEulerRotationCorrection(0.0, 90.0, 0.0);
Quaternion canonicalCorrection = adapter.CreateCanonicalRotationCorrection(correction);
Vector3 rotatedForward = Vector3.Transform(Vector3.UnitX, canonicalCorrection);
Assert.AreEqual(0.0, rotatedForward.X, 1e-6);
Assert.AreEqual(1.0, rotatedForward.Y, 1e-6);
Assert.AreEqual(0.0, rotatedForward.Z, 1e-6);
}
private static void AssertPoint(Vector3 point, double x, double y, double z) private static void AssertPoint(Vector3 point, double x, double y, double z)
{ {
Assert.AreEqual(x, point.X, 1e-9); Assert.AreEqual(x, point.X, 1e-9);

View File

@ -36,5 +36,21 @@ namespace NavisworksTransport.UnitTests.CoordinateSystem
Assert.AreEqual(0.0, up.Y, 1e-6); Assert.AreEqual(0.0, up.Y, 1e-6);
Assert.AreEqual(0.0, up.Z, 1e-6); Assert.AreEqual(0.0, up.Z, 1e-6);
} }
[TestMethod]
public void TryCalculateAxes_ShouldReturnFalse_ForZeroLengthSegment()
{
var segmentDirection = Vector3.Zero;
var upReference = new Vector3(0f, 1f, 0f);
bool ok = ObjectSpaceOrientationHelper.TryCalculateAxes(
segmentDirection,
upReference,
out var axes);
Assert.IsFalse(ok);
Assert.AreEqual(Vector3.Zero, axes.right);
Assert.AreEqual(Vector3.Zero, axes.up);
}
} }
} }

View File

@ -7,6 +7,21 @@ powershell -NoProfile -ExecutionPolicy Bypass -Command ^
"$ErrorActionPreference = 'Stop';" ^ "$ErrorActionPreference = 'Stop';" ^
"$targetDir = [System.IO.Path]::GetFullPath('%TARGET_DIR%');" ^ "$targetDir = [System.IO.Path]::GetFullPath('%TARGET_DIR%');" ^
"$buildDir = [System.IO.Path]::GetFullPath('%BUILD_DIR%');" ^ "$buildDir = [System.IO.Path]::GetFullPath('%BUILD_DIR%');" ^
"$deployDllNames = @(" ^
" 'TransportPlugin.dll'," ^
" 'geometry4Sharp.dll'," ^
" 'Newtonsoft.Json.dll'," ^
" 'Roy-T.AStar.dll'," ^
" 'SQLite.Interop.dll'," ^
" 'System.Data.SQLite.dll'," ^
" 'Tomlyn.dll'" ^
");" ^
"$stalePluginFiles = @(" ^
" 'Autodesk.Navisworks.Api.dll'," ^
" 'NavisworksTransport.UnitTests.dll'," ^
" 'Microsoft.VisualStudio.TestPlatform.TestFramework.dll'," ^
" 'Microsoft.VisualStudio.TestPlatform.TestFramework.Extensions.dll'" ^
");" ^
"function Test-FileUnlocked([string]$path) {" ^ "function Test-FileUnlocked([string]$path) {" ^
" if (-not (Test-Path $path)) { return $true }" ^ " if (-not (Test-Path $path)) { return $true }" ^
" try {" ^ " try {" ^
@ -39,14 +54,22 @@ powershell -NoProfile -ExecutionPolicy Bypass -Command ^
"foreach ($keyFile in $keyTargetFiles) { Wait-FileUnlocked $keyFile $deadline }" ^ "foreach ($keyFile in $keyTargetFiles) { Wait-FileUnlocked $keyFile $deadline }" ^
"" ^ "" ^
"New-Item -ItemType Directory -Force -Path $targetDir | Out-Null;" ^ "New-Item -ItemType Directory -Force -Path $targetDir | Out-Null;" ^
"Copy-Item (Join-Path $buildDir '*.dll') $targetDir -Force;" ^ "foreach ($staleFileName in $stalePluginFiles) {" ^
" $stalePath = Join-Path $targetDir $staleFileName;" ^
" if (Test-Path $stalePath) { Remove-Item $stalePath -Force }" ^
"}" ^
"foreach ($dllName in $deployDllNames) {" ^
" $sourceDll = Join-Path $buildDir $dllName;" ^
" if (-not (Test-Path $sourceDll)) { throw ('Deployment source file missing: ' + $sourceDll) }" ^
" Copy-Item $sourceDll $targetDir -Force;" ^
"}" ^
"if (Test-Path (Join-Path $buildDir 'resources')) {" ^ "if (Test-Path (Join-Path $buildDir 'resources')) {" ^
" New-Item -ItemType Directory -Force -Path (Join-Path $targetDir 'resources') | Out-Null;" ^ " New-Item -ItemType Directory -Force -Path (Join-Path $targetDir 'resources') | Out-Null;" ^
" Copy-Item (Join-Path $buildDir 'resources\\*') (Join-Path $targetDir 'resources') -Recurse -Force;" ^ " Copy-Item (Join-Path $buildDir 'resources\\*') (Join-Path $targetDir 'resources') -Recurse -Force;" ^
"}" ^ "}" ^
"" ^ "" ^
"$sourceFiles = @();" ^ "$sourceFiles = @();" ^
"$sourceFiles += Get-ChildItem $buildDir -File -Filter '*.dll';" ^ "foreach ($dllName in $deployDllNames) { $sourceFiles += Get-Item (Join-Path $buildDir $dllName) }" ^
"if (Test-Path (Join-Path $buildDir 'resources')) {" ^ "if (Test-Path (Join-Path $buildDir 'resources')) {" ^
" $sourceFiles += Get-ChildItem (Join-Path $buildDir 'resources') -File;" ^ " $sourceFiles += Get-ChildItem (Join-Path $buildDir 'resources') -File;" ^
"}" ^ "}" ^

View File

@ -41,6 +41,7 @@
- Navisworks 世界坐标统一视为**外部坐标** - Navisworks 世界坐标统一视为**外部坐标**
- 程序内部统一使用一套**规范内部坐标** - 程序内部统一使用一套**规范内部坐标**
- 工程语义使用独立的**业务基准坐标** - 工程语义使用独立的**业务基准坐标**
- 只有插件自带资源才允许引入**资产坐标系**
- 坐标转换只发生在少数边界入口 - 坐标转换只发生在少数边界入口
- 业务逻辑禁止直接依赖宿主坐标语义 - 业务逻辑禁止直接依赖宿主坐标语义
@ -95,6 +96,7 @@ flowchart LR
特点: 特点:
- 是程序的输入/输出坐标 - 是程序的输入/输出坐标
- UI 文本框、对话框、日志、鼠标拾取结果,统一按宿主坐标系解释和显示
- 可能来自 `Y-up` 项目,也可能来自 `Z-up` 项目 - 可能来自 `Y-up` 项目,也可能来自 `Z-up` 项目
- 不应直接当成内部计算坐标 - 不应直接当成内部计算坐标
@ -118,6 +120,29 @@ flowchart LR
- 项目 up 方向的业务解释 - 项目 up 方向的业务解释
- 终端安装参考面 - 终端安装参考面
- 轨道参考面 - 轨道参考面
### 3.2.1 术语约束
后续文档、代码注释和日志中,统一使用以下说法:
- **宿主坐标系Host Space**
- 指 Navisworks 文档坐标系
- `Y-up` / `Z-up` 的判断只属于这一层
- **内部坐标系Canonical Space**
- 指程序内部统一使用的 `Z-up` 坐标系
- 仅供内部计算使用,禁止直接暴露给 UI
- **资产坐标系Asset Space**
- 只用于插件自带资源
- 当前明确只有两类:`虚拟物体` 与 `单位圆柱体(参考杆资源)`
- 用于描述这些资源文件自身的 `Forward/Up/Side` 轴约定
禁止继续使用含糊的“本地坐标系”说法,因为它容易混淆:
- 宿主坐标系
- 内部坐标系
- 资产坐标系
后续凡是 UI 输入/输出,必须明确是**宿主坐标系**语义;凡是姿态或几何内部计算,必须明确是**内部坐标系**语义;凡是虚拟物体/单位圆柱体资源朝向,必须明确是**资产坐标系**语义。
- 业务锚点 - 业务锚点
这层不能偷用世界原点,也不能偷用宿主世界轴。 这层不能偷用世界原点,也不能偷用宿主世界轴。
@ -154,24 +179,35 @@ flowchart LR
都必须基于这组定义来解释,不能继续偷用“世界 Z 就是 up”的旧假设。 都必须基于这组定义来解释,不能继续偷用“世界 Z 就是 up”的旧假设。
### 3.4 模型局部轴约定是独立层 ### 3.4 资产坐标系是独立层
除了宿主坐标系和内部统一坐标系,还必须区分**模型局部轴约定**。 除了宿主坐标系和内部统一坐标系,还必须区分**资产坐标系**。
注意,这一层**不是所有模型都有**。当前项目里只有插件自带资源需要它:
- 虚拟物体资源
- 单位圆柱体参考杆资源
这是另一层独立定义,至少包含: 这是另一层独立定义,至少包含:
- `LocalForwardAxis` - `AssetForwardAxis`
- `LocalUpAxis` - `AssetUpAxis`
例如: 例如:
- 虚拟物体资源通常按程序约定构建,可能是 `Local X = Forward, Local Z = Up` - 虚拟物体资源通常按程序约定构建,可能是 `Asset X = Forward, Asset Z = Up`
- 真实模型如果来自 `Y-up` 项目,则很可能是 `Local X = Forward, Local Y = Up` - 单位圆柱体参考杆资源当前约定为 `Asset X = 杆轴方向, Asset Z = 截面 Up`
而对于客户真实模型:
- 不单独引入“资产坐标系”概念
- UI 和数据输入输出仍只认宿主坐标系
- 内部算法如需统一姿态,先进入 Canonical Space再叠加业务参考信息
这意味着: 这意味着:
- 即使宿主坐标系转换已经正确 - 即使宿主坐标系转换已经正确
- 如果模型局部轴约定没有显式处理 - 如果资产坐标系语义没有显式处理
- 动画和姿态仍然会出现“路径对了、通行空间对了、模型自己站歪了”的现象 - 动画和姿态仍然会出现“路径对了、通行空间对了、模型自己站歪了”的现象
因此,后续姿态系统必须显式区分: 因此,后续姿态系统必须显式区分:
@ -179,7 +215,7 @@ flowchart LR
1. 宿主坐标系定义 1. 宿主坐标系定义
2. 内部统一坐标系定义 2. 内部统一坐标系定义
3. 业务基准定义 3. 业务基准定义
4. 模型局部轴约定 4. 资产坐标系约定(仅限虚拟物体和单位圆柱体等插件自带资源)
### 3.5 Rail 局部坐标系与动画跟踪点 ### 3.5 Rail 局部坐标系与动画跟踪点
@ -599,6 +635,8 @@ WPF 插件的最终部署,必须依赖完整主项目构建产物,而不应
- **Navisworks 世界坐标统一视为外部坐标** - **Navisworks 世界坐标统一视为外部坐标**
- **程序内部统一使用 Canonical SpaceZ-up** - **程序内部统一使用 Canonical SpaceZ-up**
- **业务基准点单独建模** - **业务基准点单独建模**
- **UI 输入输出统一使用宿主坐标系**
- **资产坐标系只属于插件自带资源,不得泛化成“所有模型都有本地轴”**
- **坐标转换只发生在边界层** - **坐标转换只发生在边界层**
这是一条更接近业界三维软件成熟做法的路线。 这是一条更接近业界三维软件成熟做法的路线。

View File

@ -2,9 +2,14 @@
## 功能点 ## 功能点
### [2026/3/20]
1. [x] 优化实现Yup模型兼容
2. [x] (功能)运动物体在起点支持三个方向的旋转
### [2026/3/18] ### [2026/3/18]
1. [x](功能)增加轨上路径(角度可倾斜) 1. [x] (功能)增加终端仿真需要的轨上和轨下路径(角度可倾斜)
### [2026/3/11] ### [2026/3/11]

View File

@ -88,12 +88,19 @@ namespace NavisworksTransport.Core.Animation
} }
} }
internal enum AnimatedObjectMode
{
None,
RealObject,
VirtualObject
}
public class PathAnimationManager public class PathAnimationManager
{ {
private static PathAnimationManager _instance; private static PathAnimationManager _instance;
private static readonly Dictionary<string, int> _animationHashToRecordId = new Dictionary<string, int>(); // 动画配置哈希到检测记录ID的映射 private static readonly Dictionary<string, int> _animationHashToRecordId = new Dictionary<string, int>(); // 动画配置哈希到检测记录ID的映射
private ModelItem _animatedObject; private ModelItem _animatedObject;
private bool _isVirtualObject = false; // 是否使用虚拟物体 private AnimatedObjectMode _animatedObjectMode = AnimatedObjectMode.None;
private double _virtualObjectLength = 0; // 虚拟物体长度(模型单位) private double _virtualObjectLength = 0; // 虚拟物体长度(模型单位)
private double _virtualObjectWidth = 0; // 虚拟物体宽度(模型单位) private double _virtualObjectWidth = 0; // 虚拟物体宽度(模型单位)
private double _virtualObjectHeight = 0; // 虚拟物体高度(模型单位) private double _virtualObjectHeight = 0; // 虚拟物体高度(模型单位)
@ -105,7 +112,7 @@ namespace NavisworksTransport.Core.Animation
private bool _manualCollisionOverrideEnabled = false; private bool _manualCollisionOverrideEnabled = false;
// === 角度修正 === // === 角度修正 ===
private double _objectRotationCorrection = 0.0; // 物体角度修正值(度,顺时针) private LocalEulerRotationCorrection _objectRotationCorrection = LocalEulerRotationCorrection.Zero; // 物体绕宿主 X/Y/Z 轴的角度修正
// === 碰撞排除列表缓存管理从LogisticsAnimationManager迁移=== // === 碰撞排除列表缓存管理从LogisticsAnimationManager迁移===
private ModelItem _currentCachedAnimationObject; private ModelItem _currentCachedAnimationObject;
@ -182,6 +189,12 @@ namespace NavisworksTransport.Core.Animation
private bool _savedObjectHasCustomRotation = false; private bool _savedObjectHasCustomRotation = false;
private bool _hasSavedObjectState = false; private bool _hasSavedObjectState = false;
private bool IsVirtualObjectMode => _animatedObjectMode == AnimatedObjectMode.VirtualObject;
private bool IsRealObjectMode => _animatedObjectMode == AnimatedObjectMode.RealObject;
private ModelItem CurrentControlledObject =>
IsVirtualObjectMode ? VirtualObjectManager.Instance.CurrentVirtualObject : _animatedObject;
/// <summary> /// <summary>
/// 当前正在进行动画的对象 /// 当前正在进行动画的对象
/// </summary> /// </summary>
@ -397,7 +410,7 @@ namespace NavisworksTransport.Core.Animation
// 🔥 支持虚拟物体的恢复 // 🔥 支持虚拟物体的恢复
ModelItem objectToRestore = null; ModelItem objectToRestore = null;
bool isVirtual = _isVirtualObject; bool isVirtual = IsVirtualObjectMode;
if (isVirtual) if (isVirtual)
{ {
@ -458,7 +471,19 @@ namespace NavisworksTransport.Core.Animation
public void SetAnimatedObject(ModelItem animatedObject) public void SetAnimatedObject(ModelItem animatedObject)
{ {
_animatedObject = animatedObject; _animatedObject = animatedObject;
_isVirtualObject = (animatedObject == null); _animatedObjectMode = animatedObject == null
? AnimatedObjectMode.None
: AnimatedObjectMode.RealObject;
if (animatedObject != null)
{
_originalTransform = animatedObject.Transform;
_originalCenter = animatedObject.BoundingBox().Center;
_trackedPosition = GetTrackedObjectPosition(animatedObject);
_currentYaw = ModelItemTransformHelper.GetYawFromTransform(_originalTransform);
_trackedRotation = _originalTransform.Factor().Rotation;
_hasTrackedRotation = true;
}
} }
/// <summary> /// <summary>
@ -467,7 +492,9 @@ namespace NavisworksTransport.Core.Animation
/// </summary> /// </summary>
public void SetVirtualObjectParameters(bool isVirtualObject, double length, double width, double height) public void SetVirtualObjectParameters(bool isVirtualObject, double length, double width, double height)
{ {
_isVirtualObject = isVirtualObject; _animatedObjectMode = isVirtualObject
? AnimatedObjectMode.VirtualObject
: (_animatedObject != null ? AnimatedObjectMode.RealObject : AnimatedObjectMode.None);
_virtualObjectLength = length; // 模型单位 _virtualObjectLength = length; // 模型单位
_virtualObjectWidth = width; // 模型单位 _virtualObjectWidth = width; // 模型单位
_virtualObjectHeight = height; // 模型单位 _virtualObjectHeight = height; // 模型单位
@ -592,8 +619,8 @@ namespace NavisworksTransport.Core.Animation
{ {
// 🔥 重要先恢复物体到原始状态CAD位置和原始朝向确保每次计算都从相同的状态开始 // 🔥 重要先恢复物体到原始状态CAD位置和原始朝向确保每次计算都从相同的状态开始
// 这样可以避免之前旋转导致的包围盒尺寸计算不准确的问题 // 这样可以避免之前旋转导致的包围盒尺寸计算不准确的问题
// 注意:也要处理虚拟物体(_isVirtualObject为true时_animatedObject可能为null // 注意:也要处理虚拟物体(当前控制对象可能不在 _animatedObject 中
if (_animatedObject != null || _isVirtualObject) if (CurrentControlledObject != null || IsVirtualObjectMode)
{ {
RestoreObjectToCADPosition(); RestoreObjectToCADPosition();
LogManager.Info($"[移动到起点] 已恢复物体到原始状态, _currentYaw={_currentYaw * 180 / Math.PI:F2}°"); LogManager.Info($"[移动到起点] 已恢复物体到原始状态, _currentYaw={_currentYaw * 180 / Math.PI:F2}°");
@ -603,6 +630,12 @@ namespace NavisworksTransport.Core.Animation
if (animatedObject != null) if (animatedObject != null)
{ {
_animatedObject = animatedObject; _animatedObject = animatedObject;
bool isVirtualObject =
VirtualObjectManager.Instance.IsVirtualObjectActive &&
ReferenceEquals(animatedObject, VirtualObjectManager.Instance.CurrentVirtualObject);
_animatedObjectMode = isVirtualObject
? AnimatedObjectMode.VirtualObject
: AnimatedObjectMode.RealObject;
_originalTransform = animatedObject.Transform; _originalTransform = animatedObject.Transform;
_originalCenter = animatedObject.BoundingBox().Center; _originalCenter = animatedObject.BoundingBox().Center;
_trackedPosition = GetTrackedObjectPosition(animatedObject); _trackedPosition = GetTrackedObjectPosition(animatedObject);
@ -611,6 +644,9 @@ namespace NavisworksTransport.Core.Animation
_currentYaw = ModelItemTransformHelper.GetYawFromTransform(_originalTransform); _currentYaw = ModelItemTransformHelper.GetYawFromTransform(_originalTransform);
_trackedRotation = _originalTransform.Factor().Rotation; _trackedRotation = _originalTransform.Factor().Rotation;
_hasTrackedRotation = true; _hasTrackedRotation = true;
LogManager.Info(
$"[移动到起点] 更新动画对象内部状态: 对象={animatedObject.DisplayName}, 虚拟物体={IsVirtualObjectMode}");
} }
if (pathPoints != null) if (pathPoints != null)
@ -629,7 +665,7 @@ namespace NavisworksTransport.Core.Animation
double pathDirectionYaw = Math.Atan2(_pathPoints[1].Y - _pathPoints[0].Y, _pathPoints[1].X - _pathPoints[0].X); double pathDirectionYaw = Math.Atan2(_pathPoints[1].Y - _pathPoints[0].Y, _pathPoints[1].X - _pathPoints[0].X);
double yaw; double yaw;
LogManager.Info($"[移动到起点] 路径方向yaw: {pathDirectionYaw * 180 / Math.PI:F2}度, 角度修正: {_objectRotationCorrection:F1}"); LogManager.Info($"[移动到起点] 路径方向yaw: {pathDirectionYaw * 180 / Math.PI:F2}度, 角度修正: {_objectRotationCorrection}");
// 根据路径类型调整朝向 // 根据路径类型调整朝向
if (_route.PathType == PathType.Hoisting) if (_route.PathType == PathType.Hoisting)
@ -646,12 +682,12 @@ namespace NavisworksTransport.Core.Animation
LogManager.Debug($"[移动到起点] 地面/空轨路径使用路径方向: {yaw * 180 / Math.PI:F2}度"); LogManager.Debug($"[移动到起点] 地面/空轨路径使用路径方向: {yaw * 180 / Math.PI:F2}度");
} }
// 应用角度修正值(顺时针,转换为弧度) // 旧 yaw 链仅保留给未进入完整三维姿态的遗留路径。
if (_objectRotationCorrection != 0.0) if (!_objectRotationCorrection.IsZero)
{ {
double correctionRad = _objectRotationCorrection * Math.PI / 180.0; double correctionRad = _objectRotationCorrection.ZDegrees * Math.PI / 180.0;
yaw += correctionRad; yaw += correctionRad;
LogManager.Debug($"[移动到起点] 应用角度修正: {_objectRotationCorrection:F1}°, 修正后yaw: {yaw * 180 / Math.PI:F2}度"); LogManager.Debug($"[移动到起点] 应用遗留Z轴修正: {_objectRotationCorrection.ZDegrees:F1}°, 修正后yaw: {yaw * 180 / Math.PI:F2}度");
} }
// 根据路径类型调整起点位置 // 根据路径类型调整起点位置
@ -1147,9 +1183,9 @@ namespace NavisworksTransport.Core.Animation
LogManager.Debug($"被撞物体位置:{GetObjectPosition(collider).X:F2},{GetObjectPosition(collider).Y:F2},{GetObjectPosition(collider).Z:F2}"); LogManager.Debug($"被撞物体位置:{GetObjectPosition(collider).X:F2},{GetObjectPosition(collider).Y:F2},{GetObjectPosition(collider).Z:F2}");
double actualYawRadians = yawRadians; double actualYawRadians = yawRadians;
if (!frame.HasCustomRotation && _objectRotationCorrection != 0.0) if (!frame.HasCustomRotation && !_objectRotationCorrection.IsZero)
{ {
actualYawRadians += _objectRotationCorrection * Math.PI / 180.0; actualYawRadians += _objectRotationCorrection.ZDegrees * Math.PI / 180.0;
} }
var collisionResult = new CollisionResult var collisionResult = new CollisionResult
@ -1548,7 +1584,7 @@ namespace NavisworksTransport.Core.Animation
{ {
var firstFrame = _animationFrames[0]; var firstFrame = _animationFrames[0];
LogManager.Debug($"[动画开始] _currentYaw之前={_currentYaw * 180 / Math.PI:F2}度, _isVirtualObject={_isVirtualObject}"); LogManager.Debug($"[动画开始] _currentYaw之前={_currentYaw * 180 / Math.PI:F2}度, 模式={_animatedObjectMode}");
// 🔥 确保物体在起点位置和朝向 // 🔥 确保物体在起点位置和朝向
// 注意MoveObjectToPathStart 已在 StartAnimation 中调用,这里只是日志记录 // 注意MoveObjectToPathStart 已在 StartAnimation 中调用,这里只是日志记录
@ -1829,7 +1865,7 @@ namespace NavisworksTransport.Core.Animation
_detectionTolerance, _detectionTolerance,
_currentRouteId, _currentRouteId,
_animatedObject, _animatedObject,
_isVirtualObject, IsVirtualObjectMode,
_animationFrameRate, _animationFrameRate,
_animationDuration, _animationDuration,
_virtualObjectLength, _virtualObjectLength,
@ -2411,8 +2447,8 @@ namespace NavisworksTransport.Core.Animation
{ {
try try
{ {
bool isRailRealObject = _route?.PathType == PathType.Rail && !_isVirtualObject && _animatedObject != null; bool isRailRealObject = _route?.PathType == PathType.Rail && IsRealObjectMode && _animatedObject != null;
if (_isVirtualObject) if (IsVirtualObjectMode)
{ {
VirtualObjectManager.Instance.MoveVirtualObject(newPosition, newRotation); VirtualObjectManager.Instance.MoveVirtualObject(newPosition, newRotation);
} }
@ -2475,7 +2511,7 @@ namespace NavisworksTransport.Core.Animation
} }
else if (_route?.PathType == PathType.Ground || _route?.PathType == PathType.Hoisting) else if (_route?.PathType == PathType.Ground || _route?.PathType == PathType.Hoisting)
{ {
LogHostActualPoseAxes(_isVirtualObject ? VirtualObjectManager.Instance.CurrentVirtualObject : _animatedObject, "[平面姿态应用后宿主姿态]", false); LogHostActualPoseAxes(CurrentControlledObject, "[平面姿态应用后宿主姿态]", false);
} }
} }
catch (Exception ex) catch (Exception ex)
@ -2688,7 +2724,7 @@ namespace NavisworksTransport.Core.Animation
return false; return false;
} }
if (_isVirtualObject) if (IsVirtualObjectMode)
{ {
return VirtualObjectManager.Instance.IsVirtualObjectActive && return VirtualObjectManager.Instance.IsVirtualObjectActive &&
ReferenceEquals(VirtualObjectManager.Instance.CurrentVirtualObject, obj); ReferenceEquals(VirtualObjectManager.Instance.CurrentVirtualObject, obj);
@ -2723,7 +2759,7 @@ namespace NavisworksTransport.Core.Animation
try try
{ {
var originalAnimatedObject = _animatedObject; var originalAnimatedObject = _animatedObject;
if (!_isVirtualObject) if (!IsVirtualObjectMode)
{ {
_animatedObject = obj; _animatedObject = obj;
} }
@ -3243,7 +3279,7 @@ namespace NavisworksTransport.Core.Animation
/// </summary> /// </summary>
private double GetAnimatedObjectHeight() private double GetAnimatedObjectHeight()
{ {
if (_isVirtualObject) if (IsVirtualObjectMode)
{ {
return _virtualObjectHeight; return _virtualObjectHeight;
} }
@ -3334,7 +3370,7 @@ namespace NavisworksTransport.Core.Animation
_pathName = pathName; _pathName = pathName;
_currentRouteId = routeId; _currentRouteId = routeId;
_isVirtualObject = isVirtualObject; // 设置是否使用虚拟物体 _animatedObjectMode = isVirtualObject ? AnimatedObjectMode.VirtualObject : AnimatedObjectMode.RealObject;
_virtualObjectLength = virtualObjectLength; // 模型单位 _virtualObjectLength = virtualObjectLength; // 模型单位
_virtualObjectWidth = virtualObjectWidth; // 模型单位 _virtualObjectWidth = virtualObjectWidth; // 模型单位
_virtualObjectHeight = virtualObjectHeight; // 模型单位 _virtualObjectHeight = virtualObjectHeight; // 模型单位
@ -3351,12 +3387,12 @@ namespace NavisworksTransport.Core.Animation
// 保持当前的 _currentYaw因为物体可能已经被 MoveObjectToPathStart 旋转) // 保持当前的 _currentYaw因为物体可能已经被 MoveObjectToPathStart 旋转)
// 不要从 Transform 中提取,因为 Transform 返回的是原始值,不是当前值 // 不要从 Transform 中提取,因为 Transform 返回的是原始值,不是当前值
LogManager.Debug($"[CreateAnimation] 保持_currentYaw={_currentYaw * 180 / Math.PI:F2}度不变, _isVirtualObject={_isVirtualObject}"); LogManager.Debug($"[CreateAnimation] 保持_currentYaw={_currentYaw * 180 / Math.PI:F2}度不变, 模式={_animatedObjectMode}");
} }
// 设置动画参数并预计算动画帧 // 设置动画参数并预计算动画帧
// 注意物体应该在调用CreateAnimation之前已经通过MoveObjectToPathStart旋转到起点 // 注意物体应该在调用CreateAnimation之前已经通过MoveObjectToPathStart旋转到起点
LogManager.Debug($"[CreateAnimation开始] _currentYaw之前={_currentYaw * 180 / Math.PI:F2}度, _isVirtualObject={_isVirtualObject}"); LogManager.Debug($"[CreateAnimation开始] _currentYaw之前={_currentYaw * 180 / Math.PI:F2}度, 模式={_animatedObjectMode}");
SetupAnimation(animatedObject, durationSeconds, _route); SetupAnimation(animatedObject, durationSeconds, _route);
LogManager.Debug($"[CreateAnimation结束] _currentYaw之后={_currentYaw * 180 / Math.PI:F2}度"); LogManager.Debug($"[CreateAnimation结束] _currentYaw之后={_currentYaw * 180 / Math.PI:F2}度");
// 设置动画状态为Ready动画已生成可以播放 // 设置动画状态为Ready动画已生成可以播放
@ -3405,7 +3441,7 @@ namespace NavisworksTransport.Core.Animation
private ModelAxisConvention GetCurrentRailModelAxisConvention() private ModelAxisConvention GetCurrentRailModelAxisConvention()
{ {
if (_isVirtualObject) if (IsVirtualObjectMode)
{ {
return ModelAxisConvention.CreateVirtualObjectAssetConvention(); return ModelAxisConvention.CreateVirtualObjectAssetConvention();
} }
@ -3413,14 +3449,14 @@ namespace NavisworksTransport.Core.Animation
var adapter = CoordinateSystemManager.Instance.CreateHostAdapter(); var adapter = CoordinateSystemManager.Instance.CreateHostAdapter();
var convention = ModelAxisConvention.CreateDefaultForHost(adapter.HostType); var convention = ModelAxisConvention.CreateDefaultForHost(adapter.HostType);
LogManager.Info( LogManager.Info(
$"[Rail姿态修正] Host={adapter.HostType}, 虚拟物体={_isVirtualObject}, " + $"[Rail姿态修正] Host={adapter.HostType}, 虚拟物体={IsVirtualObjectMode}, " +
$"Forward={convention.ForwardAxis}, Up={convention.UpAxis}"); $"Forward={convention.ForwardAxis}, Up={convention.UpAxis}");
return convention; return convention;
} }
private ModelAxisConvention GetCurrentModelAxisConvention() private ModelAxisConvention GetCurrentModelAxisConvention()
{ {
if (_isVirtualObject) if (IsVirtualObjectMode)
{ {
return ModelAxisConvention.CreateVirtualObjectAssetConvention(); return ModelAxisConvention.CreateVirtualObjectAssetConvention();
} }
@ -3487,6 +3523,7 @@ namespace NavisworksTransport.Core.Animation
Vector3 canonicalPrevious = ToNumerics(adapter.ToCanonicalPoint(previousPoint)); Vector3 canonicalPrevious = ToNumerics(adapter.ToCanonicalPoint(previousPoint));
Vector3 canonicalCurrent = ToNumerics(adapter.ToCanonicalPoint(currentPoint)); Vector3 canonicalCurrent = ToNumerics(adapter.ToCanonicalPoint(currentPoint));
Vector3 canonicalNext = ToNumerics(adapter.ToCanonicalPoint(nextPoint)); Vector3 canonicalNext = ToNumerics(adapter.ToCanonicalPoint(nextPoint));
Quaternion correctionQuaternion = adapter.CreateCanonicalRotationCorrection(_objectRotationCorrection);
Vector3 forward = canonicalNext - canonicalPrevious; Vector3 forward = canonicalNext - canonicalPrevious;
if (forward.LengthSquared() < 1e-6f) if (forward.LengthSquared() < 1e-6f)
@ -3494,12 +3531,11 @@ namespace NavisworksTransport.Core.Animation
forward = canonicalNext - canonicalCurrent; forward = canonicalNext - canonicalCurrent;
} }
forward = ApplyPlanarRotationCorrection(forward);
if (!CanonicalPlanarPoseBuilder.TryCreateQuaternionFromForward( if (!CanonicalPlanarPoseBuilder.TryCreateQuaternionFromForward(
forward, forward,
HostCoordinateAdapter.CanonicalUpVector3, HostCoordinateAdapter.CanonicalUpVector3,
convention, convention,
correctionQuaternion,
out var quaternion)) out var quaternion))
{ {
return false; return false;
@ -3516,12 +3552,12 @@ namespace NavisworksTransport.Core.Animation
var adapter = CoordinateSystemManager.Instance.CreateHostAdapter(); var adapter = CoordinateSystemManager.Instance.CreateHostAdapter();
var convention = GetCurrentModelAxisConvention(); var convention = GetCurrentModelAxisConvention();
Vector3 canonicalForward = adapter.ToCanonicalVector3(new Vector3((float)hostForward.X, (float)hostForward.Y, (float)hostForward.Z)); Vector3 canonicalForward = adapter.ToCanonicalVector3(new Vector3((float)hostForward.X, (float)hostForward.Y, (float)hostForward.Z));
canonicalForward = ApplyPlanarRotationCorrection(canonicalForward); Quaternion correctionQuaternion = adapter.CreateCanonicalRotationCorrection(_objectRotationCorrection);
if (!CanonicalPlanarPoseBuilder.TryCreateQuaternionFromForward( if (!CanonicalPlanarPoseBuilder.TryCreateQuaternionFromForward(
canonicalForward, canonicalForward,
HostCoordinateAdapter.CanonicalUpVector3, HostCoordinateAdapter.CanonicalUpVector3,
convention, convention,
correctionQuaternion,
out var quaternion)) out var quaternion))
{ {
return false; return false;
@ -3531,25 +3567,10 @@ namespace NavisworksTransport.Core.Animation
return true; return true;
} }
private Vector3 ApplyPlanarRotationCorrection(Vector3 canonicalForward)
{
if (Math.Abs(_objectRotationCorrection) < 1e-9)
{
return canonicalForward;
}
float correctionRadians = (float)(_objectRotationCorrection * Math.PI / 180.0);
Quaternion correction = Quaternion.CreateFromAxisAngle(
Vector3.Normalize(HostCoordinateAdapter.CanonicalUpVector3),
correctionRadians);
return Vector3.Transform(canonicalForward, correction);
}
/// <summary> /// <summary>
/// 设置物体角度修正值(度,顺时针) /// 设置物体绕宿主 X/Y/Z 轴的角度修正
/// </summary> /// </summary>
/// <param name="rotationCorrection">角度修正值(度)</param> public void SetObjectRotationCorrection(LocalEulerRotationCorrection rotationCorrection)
public void SetObjectRotationCorrection(double rotationCorrection)
{ {
_objectRotationCorrection = rotationCorrection; _objectRotationCorrection = rotationCorrection;
@ -3560,7 +3581,7 @@ namespace NavisworksTransport.Core.Animation
{ {
// 重新计算并应用朝向 // 重新计算并应用朝向
MoveObjectToPathStart(); MoveObjectToPathStart();
LogManager.Info($"[角度修正] 物体角度已更新: {_objectRotationCorrection:F1}°"); LogManager.Info($"[角度修正] 物体角度已更新: {_objectRotationCorrection}");
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -3570,13 +3591,36 @@ namespace NavisworksTransport.Core.Animation
} }
/// <summary> /// <summary>
/// 直接设置物体角度修正值(不触发物体旋转) /// 保留给旧单轴调用方:按当前宿主 up 轴映射成三轴角度修正。
/// </summary> /// </summary>
/// <param name="rotationCorrection">角度修正值(度)</param> public void SetObjectRotationCorrection(double rotationCorrection)
public void SetObjectRotationCorrectionDirect(double rotationCorrection) {
SetObjectRotationCorrection(CreateLegacyUpAxisCorrection(rotationCorrection));
}
/// <summary>
/// 直接设置物体绕宿主 X/Y/Z 轴的角度修正(不触发物体旋转)
/// </summary>
public void SetObjectRotationCorrectionDirect(LocalEulerRotationCorrection rotationCorrection)
{ {
_objectRotationCorrection = rotationCorrection; _objectRotationCorrection = rotationCorrection;
LogManager.Debug($"[角度修正] 直接设置角度修正值: {_objectRotationCorrection:F1}°(不触发旋转)"); LogManager.Debug($"[角度修正] 直接设置角度修正值: {_objectRotationCorrection}(不触发旋转)");
}
/// <summary>
/// 保留给旧单轴调用方:按当前宿主 up 轴映射成三轴角度修正。
/// </summary>
public void SetObjectRotationCorrectionDirect(double rotationCorrection)
{
SetObjectRotationCorrectionDirect(CreateLegacyUpAxisCorrection(rotationCorrection));
}
private LocalEulerRotationCorrection CreateLegacyUpAxisCorrection(double rotationCorrection)
{
var adapter = CoordinateSystemManager.Instance.CreateHostAdapter();
return adapter.HostType == CoordinateSystemType.YUp
? new LocalEulerRotationCorrection(0.0, rotationCorrection, 0.0)
: new LocalEulerRotationCorrection(0.0, 0.0, rotationCorrection);
} }
#region #region
@ -3732,11 +3776,6 @@ namespace NavisworksTransport.Core.Animation
} }
double actualYaw = frameData.YawRadians; double actualYaw = frameData.YawRadians;
if (_objectRotationCorrection != 0.0)
{
double correctionRad = _objectRotationCorrection * Math.PI / 180.0;
actualYaw += correctionRad;
}
if (frameData.HasCustomRotation) if (frameData.HasCustomRotation)
{ {
@ -3858,7 +3897,7 @@ namespace NavisworksTransport.Core.Animation
sb.Append("|"); sb.Append("|");
// 包含角度修正(确保角度修正改变时重新检测) // 包含角度修正(确保角度修正改变时重新检测)
sb.Append($"RotationCorrection:{_objectRotationCorrection:F2}deg"); sb.Append($"RotationCorrection:{_objectRotationCorrection}");
sb.Append("|"); sb.Append("|");
// 包含手工检测对象列表(确保手工指定模式的目标变化时重新检测) // 包含手工检测对象列表(确保手工指定模式的目标变化时重新检测)

View File

@ -1886,6 +1886,19 @@ namespace NavisworksTransport
{ {
var startPoint = sortedPoints[i]; var startPoint = sortedPoints[i];
var endPoint = sortedPoints[i + 1]; var endPoint = sortedPoints[i + 1];
double segmentDx = endPoint.Position.X - startPoint.Position.X;
double segmentDy = endPoint.Position.Y - startPoint.Position.Y;
double segmentDz = endPoint.Position.Z - startPoint.Position.Z;
double segmentLength = Math.Sqrt(
segmentDx * segmentDx +
segmentDy * segmentDy +
segmentDz * segmentDz);
if (segmentLength < 0.001)
{
LogManager.Debug($"[路径渲染] 跳过零长度路径段: 索引={i}, 路径={visualization.PathRoute?.Name}");
continue;
}
// 对于吊装路径,判断是否是垂直段和起吊/下降段 // 对于吊装路径,判断是否是垂直段和起吊/下降段
bool isVerticalSegment = false; bool isVerticalSegment = false;
@ -1911,11 +1924,7 @@ namespace NavisworksTransport
// 沿宿主 up 判断垂直段,避免把世界 Z 误当成向上轴 // 沿宿主 up 判断垂直段,避免把世界 Z 误当成向上轴
var hostUp = GetHostUpVector(); var hostUp = GetHostUpVector();
double dx = endPoint.Position.X - startPoint.Position.X; double upDelta = segmentDx * hostUp.X + segmentDy * hostUp.Y + segmentDz * hostUp.Z;
double dy = endPoint.Position.Y - startPoint.Position.Y;
double dz = endPoint.Position.Z - startPoint.Position.Z;
double upDelta = dx * hostUp.X + dy * hostUp.Y + dz * hostUp.Z;
double segmentLength = Math.Sqrt(dx * dx + dy * dy + dz * dz);
double horizontalDist = Math.Sqrt(Math.Max(0.0, segmentLength * segmentLength - upDelta * upDelta)); double horizontalDist = Math.Sqrt(Math.Max(0.0, segmentLength * segmentLength - upDelta * upDelta));
// 如果沿宿主 up 的变化占主导(垂直段),或者是第一个/最后一个线段 // 如果沿宿主 up 的变化占主导(垂直段),或者是第一个/最后一个线段
bool isZDominant = Math.Abs(upDelta) > horizontalDist * 2.0; bool isZDominant = Math.Abs(upDelta) > horizontalDist * 2.0;

View File

@ -213,7 +213,9 @@ namespace NavisworksTransport.Core
try try
{ {
LogVirtualObjectGeometry("[虚拟物体姿态] 应用前"); LogVirtualObjectGeometry("[虚拟物体姿态] 应用前");
ModelItemTransformHelper.MoveItemToPositionAndRotation(_virtualObjectModelItem, position, rotation); // 虚拟物体当前尺寸是通过永久变换中的缩放实现的。
// 应用完整姿态时必须保留当前缩放,否则会破坏虚拟物体的资产姿态和几何语义。
ModelItemTransformHelper.MoveItemToPositionAndRotationWithCurrentScale(_virtualObjectModelItem, position, rotation);
var actualBounds = _virtualObjectModelItem.BoundingBox(); var actualBounds = _virtualObjectModelItem.BoundingBox();
Point3D actualCenter = actualBounds?.Center ?? new Point3D(0, 0, 0); Point3D actualCenter = actualBounds?.Center ?? new Point3D(0, 0, 0);
LogManager.Info( LogManager.Info(

View File

@ -4,6 +4,7 @@ using System.Collections.ObjectModel;
using System.Collections.Specialized; using System.Collections.Specialized;
using System.ComponentModel; using System.ComponentModel;
using System.Linq; using System.Linq;
using System.Numerics;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Windows; using System.Windows;
@ -355,7 +356,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
private double _safetyMarginInMeters; // 检测间隙(米),从路径编辑同步 private double _safetyMarginInMeters; // 检测间隙(米),从路径编辑同步
// 角度修正相关字段 // 角度修正相关字段
private double _objectRotationCorrection; // 物体角度修正值(度,顺时针) private LocalEulerRotationCorrection _objectRotationCorrection = LocalEulerRotationCorrection.Zero; // 物体绕宿主 X/Y/Z 轴的角度修正
// 移动物体原始尺寸(米,在选择物体时保存) // 移动物体原始尺寸(米,在选择物体时保存)
private double _objectOriginalLength; // 物体原始长度X方向 private double _objectOriginalLength; // 物体原始长度X方向
@ -708,8 +709,8 @@ namespace NavisworksTransport.UI.WPF.ViewModels
VirtualObjectLengthInMeters, VirtualObjectWidthInMeters, VirtualObjectHeightInMeters); VirtualObjectLengthInMeters, VirtualObjectWidthInMeters, VirtualObjectHeightInMeters);
// 重置角度修正值(虚拟物体重新选择时重置) // 重置角度修正值(虚拟物体重新选择时重置)
ObjectRotationCorrection = 0.0; ObjectRotationCorrection = LocalEulerRotationCorrection.Zero;
LogManager.Debug($"[切换虚拟物体] 已重置角度修正值为0"); LogManager.Debug("[切换虚拟物体] 已重置角度修正值为0");
// 当切换到虚拟物体模式时,根据路径类型打开通行空间可视化 // 当切换到虚拟物体模式时,根据路径类型打开通行空间可视化
UpdatePassageSpaceVisualization(); UpdatePassageSpaceVisualization();
@ -792,9 +793,9 @@ namespace NavisworksTransport.UI.WPF.ViewModels
} }
/// <summary> /// <summary>
/// 物体角度修正值(度,顺时针) /// 物体绕宿主 X/Y/Z 轴的角度修正。
/// </summary> /// </summary>
public double ObjectRotationCorrection public LocalEulerRotationCorrection ObjectRotationCorrection
{ {
get => _objectRotationCorrection; get => _objectRotationCorrection;
set set
@ -1261,8 +1262,8 @@ namespace NavisworksTransport.UI.WPF.ViewModels
SelectAnimatedObjectCommand = new RelayCommand(() => SelectAnimatedObjectCommand = new RelayCommand(() =>
{ {
// 重置角度修正值(每个物体的旋转独立) // 重置角度修正值(每个物体的旋转独立)
ObjectRotationCorrection = 0.0; ObjectRotationCorrection = LocalEulerRotationCorrection.Zero;
LogManager.Debug($"[选择物体] 已重置角度修正值为0"); LogManager.Debug("[选择物体] 已重置角度修正值为0");
ExecuteSelectAnimatedObject(); ExecuteSelectAnimatedObject();
}); });
ClearAnimatedObjectCommand = new RelayCommand(ExecuteClearAnimatedObject, () => HasSelectedAnimatedObject); ClearAnimatedObjectCommand = new RelayCommand(ExecuteClearAnimatedObject, () => HasSelectedAnimatedObject);
@ -1714,19 +1715,30 @@ namespace NavisworksTransport.UI.WPF.ViewModels
// 3. 设置新物体会触发UpdatePassageSpaceVisualization // 3. 设置新物体会触发UpdatePassageSpaceVisualization
SelectedAnimatedObject = newObject; SelectedAnimatedObject = newObject;
_pathAnimationManager?.SetAnimatedObject(newObject);
LogManager.Info($"已选择移动物体: {SelectedAnimatedObject.DisplayName}"); LogManager.Info($"已选择移动物体: {SelectedAnimatedObject.DisplayName}");
// 选择实体物体意味着切换到实体模式,避免后续起点同步仍走虚拟物体分支。
if (UseVirtualObject)
{
UseVirtualObject = false;
LogManager.Debug("[选择物体] 已切换到实体物体模式");
}
// 只有选择不同的物体时,才重置角度修正值 // 只有选择不同的物体时,才重置角度修正值
if (!isSameObject) if (!isSameObject)
{ {
// 重置 ViewModel 中的角度修正值(会自动同步到 PathAnimationManager // 重置 ViewModel 中的角度修正值(会自动同步到 PathAnimationManager
ObjectRotationCorrection = 0.0; ObjectRotationCorrection = LocalEulerRotationCorrection.Zero;
LogManager.Debug($"[选择物体] 已重置角度修正值为0度新物体"); LogManager.Debug("[选择物体] 已重置角度修正值为0新物体");
} }
else else
{ {
LogManager.Debug($"[选择物体] 保持当前角度修正值(同一物体)"); LogManager.Debug($"[选择物体] 保持当前角度修正值(同一物体)");
} }
// 显式同步一次到当前路径起点,避免依赖 setter 分支导致真实物体停留在 CAD 原位。
MoveAnimatedObjectToPathStart();
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -2151,8 +2163,8 @@ namespace NavisworksTransport.UI.WPF.ViewModels
} }
// 2. 重置角度修正值为0 // 2. 重置角度修正值为0
ObjectRotationCorrection = 0.0; ObjectRotationCorrection = LocalEulerRotationCorrection.Zero;
LogManager.Debug("[清除物体] 已重置角度修正值为0"); LogManager.Debug("[清除物体] 已重置角度修正值为0");
// 3. 重置属性 // 3. 重置属性
SelectedAnimatedObject = null; SelectedAnimatedObject = null;
@ -2182,9 +2194,9 @@ namespace NavisworksTransport.UI.WPF.ViewModels
var dialog = new Views.EditRotationWindow(_objectRotationCorrection); var dialog = new Views.EditRotationWindow(_objectRotationCorrection);
if (dialog.ShowDialog() == true) if (dialog.ShowDialog() == true)
{ {
ObjectRotationCorrection = dialog.RotationAngle; ObjectRotationCorrection = dialog.RotationCorrection;
LogManager.Info($"物体角度修正已更新: {_objectRotationCorrection:F1}°"); LogManager.Info($"物体角度修正已更新: {_objectRotationCorrection}");
UpdateMainStatus($"物体角度修正: {_objectRotationCorrection:F1}°"); UpdateMainStatus($"物体角度修正: {_objectRotationCorrection}");
// 应用角度修正到物体 // 应用角度修正到物体
UpdateObjectRotation(); UpdateObjectRotation();
@ -2211,7 +2223,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
{ {
// 将角度修正传递给动画管理器 // 将角度修正传递给动画管理器
_pathAnimationManager.SetObjectRotationCorrection(_objectRotationCorrection); _pathAnimationManager.SetObjectRotationCorrection(_objectRotationCorrection);
LogManager.Debug($"[角度修正] 已更新物体旋转: {_objectRotationCorrection:F1}°"); LogManager.Debug($"[角度修正] 已更新物体旋转: {_objectRotationCorrection}");
} }
} }
catch (Exception ex) catch (Exception ex)
@ -3191,7 +3203,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
IsVirtualObject = UseVirtualObject, IsVirtualObject = UseVirtualObject,
AnimatedObjectName = UseVirtualObject ? "虚拟物体" : SelectedAnimatedObject?.DisplayName, AnimatedObjectName = UseVirtualObject ? "虚拟物体" : SelectedAnimatedObject?.DisplayName,
DetectAllObjects = !IsManualCollisionTargetEnabled, DetectAllObjects = !IsManualCollisionTargetEnabled,
ObjectRotationCorrection = _objectRotationCorrection, ObjectRotationCorrection = _objectRotationCorrection.ZDegrees,
Description = $"{scenarioName}于 {DateTime.Now:yyyy-MM-dd HH:mm:ss}", Description = $"{scenarioName}于 {DateTime.Now:yyyy-MM-dd HH:mm:ss}",
TestName = testName, TestName = testName,
TestTime = DateTime.Now, TestTime = DateTime.Now,
@ -3356,7 +3368,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
UseVirtualObject ? VirtualObjectLengthInMeters * factor : (double?)null, // 米 → 模型单位 UseVirtualObject ? VirtualObjectLengthInMeters * factor : (double?)null, // 米 → 模型单位
UseVirtualObject ? VirtualObjectWidthInMeters * factor : (double?)null, UseVirtualObject ? VirtualObjectWidthInMeters * factor : (double?)null,
UseVirtualObject ? VirtualObjectHeightInMeters * factor : (double?)null, UseVirtualObject ? VirtualObjectHeightInMeters * factor : (double?)null,
_objectRotationCorrection, _objectRotationCorrection.ZDegrees,
IsManualCollisionTargetEnabled IsManualCollisionTargetEnabled
); );
@ -4302,54 +4314,56 @@ namespace NavisworksTransport.UI.WPF.ViewModels
} }
/// <summary> /// <summary>
/// 计算旋转后的有效宽度和长度 /// 计算本地三轴预旋转后的有效尺寸
/// 返回值为模型单位与Navisworks API一致 /// 返回值为模型单位与Navisworks API一致
/// </summary> /// </summary>
/// <returns>(有效宽度, 有效长度) - 模型单位</returns> /// <returns>(沿路径, 垂直路径, 法线) - 模型单位</returns>
private (double effectiveWidth, double effectiveLength) CalculateRotatedDimensions() private (double effectiveAlongPath, double effectiveAcrossPath, double effectiveNormalToPath) CalculateRotatedDimensions()
{ {
double baseLengthMeters, baseWidthMeters; double forwardSize;
double sideSize;
double upSize;
ModelAxisConvention axisConvention;
if (UseVirtualObject) if (UseVirtualObject)
{ {
baseLengthMeters = VirtualObjectLengthInMeters; double metersToUnits = UnitsConverter.GetMetersToUnitsConversionFactor();
baseWidthMeters = VirtualObjectWidthInMeters; forwardSize = VirtualObjectLengthInMeters * metersToUnits;
sideSize = VirtualObjectWidthInMeters * metersToUnits;
upSize = VirtualObjectHeightInMeters * metersToUnits;
axisConvention = ModelAxisConvention.CreateVirtualObjectAssetConvention();
} }
else if (SelectedAnimatedObject != null) else if (SelectedAnimatedObject != null)
{ {
// 使用保存的原始尺寸(米),避免物体旋转后包围盒尺寸变化 double metersToUnits = UnitsConverter.GetMetersToUnitsConversionFactor();
baseLengthMeters = _objectOriginalLength; forwardSize = _objectOriginalLength * metersToUnits;
baseWidthMeters = _objectOriginalWidth; sideSize = _objectOriginalWidth * metersToUnits;
upSize = _objectOriginalHeight * metersToUnits;
var adapter = CoordinateSystemManager.Instance.CreateHostAdapter();
axisConvention = CurrentPathRoute?.PathType == NavisworksTransport.PathType.Rail
? ModelAxisConvention.CreateRailAssetConvention()
: ModelAxisConvention.CreateDefaultForHost(adapter.HostType);
} }
else else
{ {
return (0, 0); return (0, 0, 0);
} }
// 转换为模型单位 Quaternion correctionQuaternion = CoordinateSystemManager.Instance.CreateHostAdapter()
var metersToUnits = UnitsConverter.GetMetersToUnitsConversionFactor(); .CreateCanonicalRotationCorrection(_objectRotationCorrection);
double baseLength = baseLengthMeters * metersToUnits; var result = RotatedObjectExtentHelper.CalculateProjectedSemanticExtents(
double baseWidth = baseWidthMeters * metersToUnits; axisConvention,
forwardSize,
sideSize,
upSize,
correctionQuaternion);
double unitsToMeters = UnitsConverter.GetUnitsToMetersConversionFactor();
LogManager.Debug(
$"[角度修正] 旋转后尺寸: 原始({forwardSize * unitsToMeters:F2}m×{sideSize * unitsToMeters:F2}m×{upSize * unitsToMeters:F2}m) -> " +
$"有效({result.forwardExtent * unitsToMeters:F2}m×{result.sideExtent * unitsToMeters:F2}m×{result.upExtent * unitsToMeters:F2}m), 角度={_objectRotationCorrection}");
// 如果没有角度修正,直接返回原始尺寸 return result;
if (_objectRotationCorrection == 0.0)
{
return (baseWidth, baseLength);
}
// 计算旋转后的有效尺寸(投影)
double rotationRad = _objectRotationCorrection * Math.PI / 180.0;
double cos = Math.Abs(Math.Cos(rotationRad));
double sin = Math.Abs(Math.Sin(rotationRad));
// 旋转后的宽度 = |宽度*cos| + |长度*sin|
double effectiveWidth = baseWidth * cos + baseLength * sin;
// 旋转后的长度 = |长度*cos| + |宽度*sin|
double effectiveLength = baseLength * cos + baseWidth * sin;
LogManager.Debug($"[角度修正] 旋转后尺寸: 原始({baseLengthMeters:F2}m×{baseWidthMeters:F2}m) -> 有效({effectiveLength / metersToUnits:F2}m×{effectiveWidth / metersToUnits:F2}m), 角度={_objectRotationCorrection:F1}°");
return (effectiveWidth, effectiveLength);
} }
/// <summary> /// <summary>
@ -4368,7 +4382,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
} }
// 计算旋转后的有效尺寸(考虑角度修正) // 计算旋转后的有效尺寸(考虑角度修正)
(double effectiveWidth, double effectiveLength) = CalculateRotatedDimensions(); (double effectiveLength, double effectiveWidth, double effectiveHeight) = CalculateRotatedDimensions();
var renderPlugin = PathPointRenderPlugin.Instance; var renderPlugin = PathPointRenderPlugin.Instance;
if (renderPlugin == null) if (renderPlugin == null)
@ -4383,8 +4397,6 @@ namespace NavisworksTransport.UI.WPF.ViewModels
// 将安全间隙从米转换为模型单位 // 将安全间隙从米转换为模型单位
var metersToUnitsPassage = UnitsConverter.GetMetersToUnitsConversionFactor(); var metersToUnitsPassage = UnitsConverter.GetMetersToUnitsConversionFactor();
double safetyMargin = _safetyMarginInMeters * metersToUnitsPassage; double safetyMargin = _safetyMarginInMeters * metersToUnitsPassage;
double virtualObjectHeight = VirtualObjectHeightInMeters * metersToUnitsPassage;
if (UseVirtualObject) if (UseVirtualObject)
{ {
// 虚拟物体的xyz定义X方向 = 长度前进方向Y方向 = 宽度侧面Z方向 = 高度 // 虚拟物体的xyz定义X方向 = 长度前进方向Y方向 = 宽度侧面Z方向 = 高度
@ -4392,11 +4404,11 @@ namespace NavisworksTransport.UI.WPF.ViewModels
{ {
// 空中路径(空轨或吊装):高度上下都加间隙 // 空中路径(空轨或吊装):高度上下都加间隙
passageAcrossPath = effectiveWidth + 2 * safetyMargin; // 旋转后宽度 + 2*间隙(垂直于路径方向) passageAcrossPath = effectiveWidth + 2 * safetyMargin; // 旋转后宽度 + 2*间隙(垂直于路径方向)
passageNormalToPath = virtualObjectHeight + 2 * safetyMargin; // Z方向高度+ 2*间隙(法线方向) passageNormalToPath = effectiveHeight + 2 * safetyMargin; // 法线方向高度 + 2*间隙
passageAlongPath = effectiveLength; // 旋转后长度(沿路径方向) passageAlongPath = effectiveLength; // 旋转后长度(沿路径方向)
// 吊装路径分段参数 // 吊装路径分段参数
passageNormalToPathVertical = effectiveLength + 2 * safetyMargin; // 垂直段:物体长度 + 2*间隙 passageNormalToPathVertical = effectiveLength + 2 * safetyMargin; // 垂直段:物体长度 + 2*间隙
passageNormalToPathHorizontal = virtualObjectHeight + 2 * safetyMargin; // 水平段:物体高度 + 2*间隙 passageNormalToPathHorizontal = effectiveHeight + 2 * safetyMargin; // 水平段:物体高度 + 2*间隙
LogManager.Debug($"[通行空间可视化] 空中路径使用虚拟物体尺寸: 有效长度={effectiveLength / metersToUnitsPassage:F2}m, 有效宽度={effectiveWidth / metersToUnitsPassage:F2}m, 通行空间沿路径={passageAlongPath / metersToUnitsPassage:F2}m, 垂直路径={passageAcrossPath / metersToUnitsPassage:F2}m, 法线={passageNormalToPath / metersToUnitsPassage:F2}m"); LogManager.Debug($"[通行空间可视化] 空中路径使用虚拟物体尺寸: 有效长度={effectiveLength / metersToUnitsPassage:F2}m, 有效宽度={effectiveWidth / metersToUnitsPassage:F2}m, 通行空间沿路径={passageAlongPath / metersToUnitsPassage:F2}m, 垂直路径={passageAcrossPath / metersToUnitsPassage:F2}m, 法线={passageNormalToPath / metersToUnitsPassage:F2}m");
} }
else if (CurrentPathRoute.PathType == NavisworksTransport.PathType.Ground) else if (CurrentPathRoute.PathType == NavisworksTransport.PathType.Ground)
@ -4404,7 +4416,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
// 地面路径物体的长度方向X轴前进方向朝向路径方向 // 地面路径物体的长度方向X轴前进方向朝向路径方向
// 通行空间沿路径方向 = X方向尺寸长度垂直于路径方向 = Y方向尺寸宽度高度上方加间隙下方不需要因为有地面 // 通行空间沿路径方向 = X方向尺寸长度垂直于路径方向 = Y方向尺寸宽度高度上方加间隙下方不需要因为有地面
passageAcrossPath = effectiveWidth + 2 * safetyMargin; // 旋转后宽度 + 2*间隙(垂直于路径方向) passageAcrossPath = effectiveWidth + 2 * safetyMargin; // 旋转后宽度 + 2*间隙(垂直于路径方向)
passageNormalToPath = virtualObjectHeight + safetyMargin; // Z方向高度+ 间隙(法线方向,只有上方) passageNormalToPath = effectiveHeight + safetyMargin; // 法线方向高度 + 间隙(法线方向,只有上方)
passageAlongPath = effectiveLength; // 旋转后长度(沿路径方向) passageAlongPath = effectiveLength; // 旋转后长度(沿路径方向)
// 地面路径不需要分段参数 // 地面路径不需要分段参数
passageNormalToPathVertical = passageNormalToPath; passageNormalToPathVertical = passageNormalToPath;
@ -4419,7 +4431,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
// - 轨下安装:顶面贴路径,底面留间隙 // - 轨下安装:顶面贴路径,底面留间隙
// - 轨上安装:底面贴路径,顶面留间隙 // - 轨上安装:底面贴路径,顶面留间隙
passageAcrossPath = effectiveWidth + 2 * safetyMargin; // 旋转后宽度 + 2*间隙(垂直于路径方向) passageAcrossPath = effectiveWidth + 2 * safetyMargin; // 旋转后宽度 + 2*间隙(垂直于路径方向)
passageNormalToPath = virtualObjectHeight + safetyMargin; // 法线方向单侧留间隙 passageNormalToPath = effectiveHeight + safetyMargin; // 法线方向单侧留间隙
passageAlongPath = effectiveLength; // 旋转后长度(沿路径方向) passageAlongPath = effectiveLength; // 旋转后长度(沿路径方向)
// 空轨路径不需要分段参数 // 空轨路径不需要分段参数
passageNormalToPathVertical = passageNormalToPath; passageNormalToPathVertical = passageNormalToPath;
@ -4429,26 +4441,18 @@ namespace NavisworksTransport.UI.WPF.ViewModels
} }
else if (SelectedAnimatedObject != null) else if (SelectedAnimatedObject != null)
{ {
// 使用选择物体的局部轴语义尺寸(模型单位)
var bbox = SelectedAnimatedObject.BoundingBox();
var adapter = CoordinateSystemManager.Instance.CreateHostAdapter();
var axisConvention = ModelAxisConvention.CreateDefaultForHost(adapter.HostType);
double sizeAlongPath = axisConvention.GetForwardSize(bbox);
double sizeAcrossPath = axisConvention.GetSideSize(bbox);
double sizeNormalToPath = axisConvention.GetUpSize(bbox);
// 根据路径类型确定通行空间的尺寸 // 根据路径类型确定通行空间的尺寸
if (CurrentPathRoute.PathType == NavisworksTransport.PathType.Rail || CurrentPathRoute.PathType == NavisworksTransport.PathType.Hoisting) if (CurrentPathRoute.PathType == NavisworksTransport.PathType.Rail || CurrentPathRoute.PathType == NavisworksTransport.PathType.Hoisting)
{ {
// 空中路径(空轨或吊装): // 空中路径(空轨或吊装):
// 高度上下都加间隙 // 高度上下都加间隙
passageAcrossPath = effectiveWidth + 2 * safetyMargin; // 旋转后宽度 + 2*间隙(垂直于路径方向) passageAcrossPath = effectiveWidth + 2 * safetyMargin; // 旋转后宽度 + 2*间隙(垂直于路径方向)
passageNormalToPath = sizeNormalToPath + 2 * safetyMargin; // 局部up方向高度 + 2*间隙(法线方向) passageNormalToPath = effectiveHeight + 2 * safetyMargin; // 局部up方向高度 + 2*间隙(法线方向)
passageAlongPath = effectiveLength; // 旋转后长度(沿路径方向) passageAlongPath = effectiveLength; // 旋转后长度(沿路径方向)
// 吊装路径分段参数 // 吊装路径分段参数
passageNormalToPathVertical = effectiveLength + 2 * safetyMargin; // 垂直段:物体长度 + 2*间隙 passageNormalToPathVertical = effectiveLength + 2 * safetyMargin; // 垂直段:物体长度 + 2*间隙
passageNormalToPathHorizontal = sizeNormalToPath + 2 * safetyMargin; // 水平段:物体高度 + 2*间隙 passageNormalToPathHorizontal = effectiveHeight + 2 * safetyMargin; // 水平段:物体高度 + 2*间隙
LogManager.Debug($"[通行空间可视化] 空中路径使用物体尺寸 ({SelectedAnimatedObject.DisplayName}): 局部长度={sizeAlongPath / metersToUnitsPassage:F2}m, 局部宽度={sizeAcrossPath / metersToUnitsPassage:F2}m, 局部高度={sizeNormalToPath / metersToUnitsPassage:F2}m, 有效长度={effectiveLength / metersToUnitsPassage:F2}m, 有效宽度={effectiveWidth / metersToUnitsPassage:F2}m, 通行空间沿路径={passageAlongPath / metersToUnitsPassage:F2}m, 垂直路径={passageAcrossPath / metersToUnitsPassage:F2}m, 法线={passageNormalToPath / metersToUnitsPassage:F2}m"); LogManager.Debug($"[通行空间可视化] 空中路径使用物体尺寸 ({SelectedAnimatedObject.DisplayName}): 有效长度={effectiveLength / metersToUnitsPassage:F2}m, 有效宽度={effectiveWidth / metersToUnitsPassage:F2}m, 有效高度={effectiveHeight / metersToUnitsPassage:F2}m, 通行空间沿路径={passageAlongPath / metersToUnitsPassage:F2}m, 垂直路径={passageAcrossPath / metersToUnitsPassage:F2}m, 法线={passageNormalToPath / metersToUnitsPassage:F2}m");
} }
else if (CurrentPathRoute.PathType == NavisworksTransport.PathType.Ground) else if (CurrentPathRoute.PathType == NavisworksTransport.PathType.Ground)
{ {
@ -4456,12 +4460,12 @@ namespace NavisworksTransport.UI.WPF.ViewModels
// 物体的长度方向X轴前进方向朝向路径方向 // 物体的长度方向X轴前进方向朝向路径方向
// 通行空间沿路径方向 = X方向尺寸长度垂直于路径方向 = Y方向尺寸宽度高度上方加间隙下方不需要因为有地面 // 通行空间沿路径方向 = X方向尺寸长度垂直于路径方向 = Y方向尺寸宽度高度上方加间隙下方不需要因为有地面
passageAcrossPath = effectiveWidth + 2 * safetyMargin; // 旋转后宽度 + 2*间隙(垂直于路径方向) passageAcrossPath = effectiveWidth + 2 * safetyMargin; // 旋转后宽度 + 2*间隙(垂直于路径方向)
passageNormalToPath = sizeNormalToPath + safetyMargin; // 局部up方向高度 + 间隙(法线方向,只有上方) passageNormalToPath = effectiveHeight + safetyMargin; // 局部up方向高度 + 间隙(法线方向,只有上方)
passageAlongPath = effectiveLength; // 旋转后长度(沿路径方向) passageAlongPath = effectiveLength; // 旋转后长度(沿路径方向)
// 地面路径不需要分段参数 // 地面路径不需要分段参数
passageNormalToPathVertical = passageNormalToPath; passageNormalToPathVertical = passageNormalToPath;
passageNormalToPathHorizontal = passageNormalToPath; passageNormalToPathHorizontal = passageNormalToPath;
LogManager.Debug($"[通行空间可视化] 地面路径使用物体尺寸 ({SelectedAnimatedObject.DisplayName}): 局部长度={sizeAlongPath / metersToUnitsPassage:F2}m, 局部宽度={sizeAcrossPath / metersToUnitsPassage:F2}m, 局部高度={sizeNormalToPath / metersToUnitsPassage:F2}m, 有效长度={effectiveLength / metersToUnitsPassage:F2}m, 有效宽度={effectiveWidth / metersToUnitsPassage:F2}m, 通行空间沿路径={passageAlongPath / metersToUnitsPassage:F2}m, 垂直路径={passageAcrossPath / metersToUnitsPassage:F2}m, 法线={passageNormalToPath / metersToUnitsPassage:F2}m"); LogManager.Debug($"[通行空间可视化] 地面路径使用物体尺寸 ({SelectedAnimatedObject.DisplayName}): 有效长度={effectiveLength / metersToUnitsPassage:F2}m, 有效宽度={effectiveWidth / metersToUnitsPassage:F2}m, 有效高度={effectiveHeight / metersToUnitsPassage:F2}m, 通行空间沿路径={passageAlongPath / metersToUnitsPassage:F2}m, 垂直路径={passageAcrossPath / metersToUnitsPassage:F2}m, 法线={passageNormalToPath / metersToUnitsPassage:F2}m");
} }
else // Rail else // Rail
{ {
@ -4472,11 +4476,11 @@ namespace NavisworksTransport.UI.WPF.ViewModels
// - 轨下安装:顶面贴路径,底面留间隙 // - 轨下安装:顶面贴路径,底面留间隙
// - 轨上安装:底面贴路径,顶面留间隙 // - 轨上安装:底面贴路径,顶面留间隙
passageAcrossPath = effectiveWidth + 2 * safetyMargin; // 旋转后宽度 + 2*间隙(垂直于路径方向) passageAcrossPath = effectiveWidth + 2 * safetyMargin; // 旋转后宽度 + 2*间隙(垂直于路径方向)
passageNormalToPath = sizeNormalToPath + safetyMargin; // 局部up方向高度 + 单侧间隙(法线方向) passageNormalToPath = effectiveHeight + safetyMargin; // 局部up方向高度 + 单侧间隙(法线方向)
passageAlongPath = effectiveLength; // 旋转后长度(沿路径方向) passageAlongPath = effectiveLength; // 旋转后长度(沿路径方向)
passageNormalToPathVertical = passageNormalToPath; passageNormalToPathVertical = passageNormalToPath;
passageNormalToPathHorizontal = passageNormalToPath; passageNormalToPathHorizontal = passageNormalToPath;
LogManager.Debug($"[通行空间可视化] 空轨路径使用物体尺寸 ({SelectedAnimatedObject.DisplayName}): 局部长度={sizeAlongPath / metersToUnitsPassage:F2}m, 局部宽度={sizeAcrossPath / metersToUnitsPassage:F2}m, 局部高度={sizeNormalToPath / metersToUnitsPassage:F2}m, 有效长度={effectiveLength / metersToUnitsPassage:F2}m, 有效宽度={effectiveWidth / metersToUnitsPassage:F2}m, 通行空间沿路径={passageAlongPath / metersToUnitsPassage:F2}m, 垂直路径={passageAcrossPath / metersToUnitsPassage:F2}m, 法线={passageNormalToPath / metersToUnitsPassage:F2}m"); LogManager.Debug($"[通行空间可视化] 空轨路径使用物体尺寸 ({SelectedAnimatedObject.DisplayName}): 有效长度={effectiveLength / metersToUnitsPassage:F2}m, 有效宽度={effectiveWidth / metersToUnitsPassage:F2}m, 有效高度={effectiveHeight / metersToUnitsPassage:F2}m, 通行空间沿路径={passageAlongPath / metersToUnitsPassage:F2}m, 垂直路径={passageAcrossPath / metersToUnitsPassage:F2}m, 法线={passageNormalToPath / metersToUnitsPassage:F2}m");
} }
} }
else else
@ -4950,8 +4954,8 @@ namespace NavisworksTransport.UI.WPF.ViewModels
} }
// 重置角度修正值为0 // 重置角度修正值为0
ObjectRotationCorrection = 0.0; ObjectRotationCorrection = LocalEulerRotationCorrection.Zero;
LogManager.Debug("[重置状态] 已重置角度修正值为0"); LogManager.Debug("[重置状态] 已重置角度修正值为0");
// 清空选中对象 // 清空选中对象
SelectedAnimatedObject = null; SelectedAnimatedObject = null;
@ -5261,7 +5265,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
DetectAllObjects = !IsManualCollisionTargetEnabled, DetectAllObjects = !IsManualCollisionTargetEnabled,
// 角度修正配置 // 角度修正配置
ObjectRotationCorrection = _objectRotationCorrection, ObjectRotationCorrection = _objectRotationCorrection.ZDegrees,
// 🔥 关联的检测记录ID // 🔥 关联的检测记录ID
DetectionRecordId = detectionRecordId DetectionRecordId = detectionRecordId
@ -5346,7 +5350,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
$"虚拟物体: {queueItem.IsVirtualObject}, " + $"虚拟物体: {queueItem.IsVirtualObject}, " +
$"帧率: {queueItem.FrameRate}, " + $"帧率: {queueItem.FrameRate}, " +
$"时长: {queueItem.DurationSeconds:F2}秒, " + $"时长: {queueItem.DurationSeconds:F2}秒, " +
$"角度修正: {queueItem.ObjectRotationCorrection:F1}°, " + $"角度修正: {_objectRotationCorrection}, " +
$"ID: {itemId}"); $"ID: {itemId}");
UpdateMainStatus($"已添加到批处理队列: {CurrentPathRoute.Name}"); UpdateMainStatus($"已添加到批处理队列: {CurrentPathRoute.Name}");

View File

@ -1,7 +1,7 @@
<Window x:Class="NavisworksTransport.UI.WPF.Views.EditRotationWindow" <Window x:Class="NavisworksTransport.UI.WPF.Views.EditRotationWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="调整物体角度" Height="320" Width="400" Title="调整物体角度" Height="360" Width="430"
WindowStartupLocation="CenterScreen" WindowStartupLocation="CenterScreen"
ResizeMode="NoResize" ResizeMode="NoResize"
ShowInTaskbar="False" ShowInTaskbar="False"
@ -26,23 +26,67 @@
<Border Grid.Row="0" Background="#FFF8FBFF" BorderBrush="#FFD4E7FF" BorderThickness="0,0,0,1" Padding="20,15"> <Border Grid.Row="0" Background="#FFF8FBFF" BorderBrush="#FFD4E7FF" BorderThickness="0,0,0,1" Padding="20,15">
<StackPanel> <StackPanel>
<TextBlock Text="调整物体角度" FontWeight="SemiBold" FontSize="14" Foreground="#FF2B579A"/> <TextBlock Text="调整物体角度" FontWeight="SemiBold" FontSize="14" Foreground="#FF2B579A"/>
<TextBlock Text="调整物体在路径起点时的水平旋转角度(顺时针)" FontSize="10" Foreground="#FF666666" Margin="0,3,0,0"/> <TextBlock Text="设置物体在路径起点处绕宿主 X/Y/Z 轴的旋转角度" FontSize="10" Foreground="#FF666666" Margin="0,3,0,0"/>
</StackPanel> </StackPanel>
</Border> </Border>
<!-- 输入区域 --> <!-- 输入区域 -->
<Border Grid.Row="1" Padding="20,15"> <Border Grid.Row="1" Padding="20,15">
<StackPanel> <StackPanel>
<!-- 角度输入 --> <!-- X轴 -->
<Grid Margin="0,0,0,12"> <Grid Margin="0,0,0,8">
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="80"/> <ColumnDefinition Width="110"/>
<ColumnDefinition Width="*"/> <ColumnDefinition Width="*"/>
<ColumnDefinition Width="50"/> <ColumnDefinition Width="50"/>
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<TextBlock Text="旋转角度:" VerticalAlignment="Center" FontSize="11" Foreground="#FF333333"/> <TextBlock Text="绕 X 轴:" VerticalAlignment="Center" FontSize="11" Foreground="#FF333333"/>
<TextBox Grid.Column="1" <TextBox Grid.Column="1"
Text="{Binding RotationAngle, StringFormat=0.0, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" x:Name="XAxisTextBox"
GotFocus="OnAxisTextBoxGotFocus"
Text="{Binding RotationXDegrees, StringFormat=0.0, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
Height="28"
VerticalContentAlignment="Center"
Padding="8,0"
FontSize="11"
BorderBrush="#FFCCCCCC"
BorderThickness="1"/>
<TextBlock Grid.Column="2" Text="°" VerticalAlignment="Center" FontSize="11" Foreground="#FF666666" Margin="8,0,0,0"/>
</Grid>
<!-- Y轴 -->
<Grid Margin="0,0,0,8">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="110"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="50"/>
</Grid.ColumnDefinitions>
<TextBlock Text="绕 Y 轴:" VerticalAlignment="Center" FontSize="11" Foreground="#FF333333"/>
<TextBox Grid.Column="1"
x:Name="YAxisTextBox"
GotFocus="OnAxisTextBoxGotFocus"
Text="{Binding RotationYDegrees, StringFormat=0.0, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
Height="28"
VerticalContentAlignment="Center"
Padding="8,0"
FontSize="11"
BorderBrush="#FFCCCCCC"
BorderThickness="1"/>
<TextBlock Grid.Column="2" Text="°" VerticalAlignment="Center" FontSize="11" Foreground="#FF666666" Margin="8,0,0,0"/>
</Grid>
<!-- Z轴 -->
<Grid Margin="0,0,0,12">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="110"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="50"/>
</Grid.ColumnDefinitions>
<TextBlock Text="绕 Z 轴:" VerticalAlignment="Center" FontSize="11" Foreground="#FF333333"/>
<TextBox Grid.Column="1"
x:Name="ZAxisTextBox"
GotFocus="OnAxisTextBoxGotFocus"
Text="{Binding RotationZDegrees, StringFormat=0.0, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
Height="28" Height="28"
VerticalContentAlignment="Center" VerticalContentAlignment="Center"
Padding="8,0" Padding="8,0"
@ -53,7 +97,7 @@
</Grid> </Grid>
<!-- 快捷按钮 --> <!-- 快捷按钮 -->
<TextBlock Text="快捷角度:" FontSize="10" Foreground="#FF666666" Margin="0,0,0,8"/> <TextBlock Text="快捷角度(作用于当前焦点输入框):" FontSize="10" Foreground="#FF666666" Margin="0,0,0,8"/>
<StackPanel Orientation="Horizontal"> <StackPanel Orientation="Horizontal">
<Button Content="0°" <Button Content="0°"
Click="OnQuickAngleClick" Click="OnQuickAngleClick"

View File

@ -2,33 +2,80 @@ using System;
using System.ComponentModel; using System.ComponentModel;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Windows; using System.Windows;
using System.Windows.Controls;
using NavisworksTransport.Utils.CoordinateSystem;
namespace NavisworksTransport.UI.WPF.Views namespace NavisworksTransport.UI.WPF.Views
{ {
public partial class EditRotationWindow : Window, INotifyPropertyChanged public partial class EditRotationWindow : Window, INotifyPropertyChanged
{ {
private double _rotationAngle; private enum RotationAxisTarget
public double RotationAngle
{ {
get => _rotationAngle; X,
Y,
Z
}
private double _rotationXDegrees;
private double _rotationYDegrees;
private double _rotationZDegrees;
private RotationAxisTarget _activeAxisTarget = RotationAxisTarget.Z;
public double RotationXDegrees
{
get => _rotationXDegrees;
set set
{ {
if (_rotationAngle != value) if (_rotationXDegrees != value)
{ {
_rotationAngle = value; _rotationXDegrees = value;
OnPropertyChanged(); OnPropertyChanged();
OnPropertyChanged(nameof(RotationCorrection));
} }
} }
} }
public EditRotationWindow(double currentAngle) public double RotationYDegrees
{
get => _rotationYDegrees;
set
{
if (_rotationYDegrees != value)
{
_rotationYDegrees = value;
OnPropertyChanged();
OnPropertyChanged(nameof(RotationCorrection));
}
}
}
public double RotationZDegrees
{
get => _rotationZDegrees;
set
{
if (_rotationZDegrees != value)
{
_rotationZDegrees = value;
OnPropertyChanged();
OnPropertyChanged(nameof(RotationCorrection));
}
}
}
public LocalEulerRotationCorrection RotationCorrection =>
new LocalEulerRotationCorrection(RotationXDegrees, RotationYDegrees, RotationZDegrees);
public EditRotationWindow(LocalEulerRotationCorrection currentRotation)
{ {
try try
{ {
InitializeComponent(); InitializeComponent();
RotationAngle = currentAngle; RotationXDegrees = currentRotation.XDegrees;
RotationYDegrees = currentRotation.YDegrees;
RotationZDegrees = currentRotation.ZDegrees;
DataContext = this; DataContext = this;
Loaded += (sender, args) => ZAxisTextBox.Focus();
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -39,34 +86,57 @@ namespace NavisworksTransport.UI.WPF.Views
private void OnQuickAngleClick(object sender, RoutedEventArgs e) private void OnQuickAngleClick(object sender, RoutedEventArgs e)
{ {
if (sender is System.Windows.Controls.Button button) if (sender is Button button)
{ {
LogManager.Debug($"[快捷角度] 点击按钮: {button.Content}, Tag类型: {button.Tag?.GetType()}, Tag值: {button.Tag}"); LogManager.Debug($"[快捷角度] 点击按钮: {button.Content}, Tag类型: {button.Tag?.GetType()}, Tag值: {button.Tag}");
if (button.Tag != null) if (button.Tag != null)
{ {
double? parsedAngle = null;
if (button.Tag is int intTag) if (button.Tag is int intTag)
{ {
RotationAngle = intTag; parsedAngle = intTag;
LogManager.Debug($"[快捷角度] 设置角度为: {intTag}°");
} }
else if (button.Tag is string stringTag && double.TryParse(stringTag, out double angle)) else if (button.Tag is string stringTag && double.TryParse(stringTag, out double angle))
{ {
RotationAngle = angle; parsedAngle = angle;
LogManager.Debug($"[快捷角度] 设置角度为: {angle}°");
} }
else if (button.Tag is double doubleTag) else if (button.Tag is double doubleTag)
{ {
RotationAngle = doubleTag; parsedAngle = doubleTag;
LogManager.Debug($"[快捷角度] 设置角度为: {doubleTag}°"); }
if (parsedAngle.HasValue)
{
ApplyQuickAngle(parsedAngle.Value);
LogManager.Debug($"[快捷角度] 设置 {_activeAxisTarget} 轴角度为: {parsedAngle.Value}°");
} }
} }
} }
} }
private void OnAxisTextBoxGotFocus(object sender, RoutedEventArgs e)
{
if (ReferenceEquals(sender, XAxisTextBox))
{
_activeAxisTarget = RotationAxisTarget.X;
return;
}
if (ReferenceEquals(sender, YAxisTextBox))
{
_activeAxisTarget = RotationAxisTarget.Y;
return;
}
_activeAxisTarget = RotationAxisTarget.Z;
}
private void OnResetClick(object sender, RoutedEventArgs e) private void OnResetClick(object sender, RoutedEventArgs e)
{ {
RotationAngle = 0.0; RotationXDegrees = 0.0;
RotationYDegrees = 0.0;
RotationZDegrees = 0.0;
} }
private void OnConfirmClick(object sender, RoutedEventArgs e) private void OnConfirmClick(object sender, RoutedEventArgs e)
@ -87,5 +157,22 @@ namespace NavisworksTransport.UI.WPF.Views
{ {
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
} }
private void ApplyQuickAngle(double angle)
{
switch (_activeAxisTarget)
{
case RotationAxisTarget.X:
RotationXDegrees = angle;
break;
case RotationAxisTarget.Y:
RotationYDegrees = angle;
break;
case RotationAxisTarget.Z:
default:
RotationZDegrees = angle;
break;
}
}
} }
} }

View File

@ -35,6 +35,36 @@ namespace NavisworksTransport.Utils.CoordinateSystem
Vector3 canonicalUp, Vector3 canonicalUp,
ModelAxisConvention convention, ModelAxisConvention convention,
out Quaternion rotation) out Quaternion rotation)
{
return TryCreateQuaternionFromForward(
desiredForward,
canonicalUp,
convention,
LocalEulerRotationCorrection.Zero,
out rotation);
}
public static bool TryCreateQuaternionFromForward(
Vector3 desiredForward,
Vector3 canonicalUp,
ModelAxisConvention convention,
LocalEulerRotationCorrection correction,
out Quaternion rotation)
{
return TryCreateQuaternionFromForward(
desiredForward,
canonicalUp,
convention,
correction.CreateQuaternion(Vector3.UnitX, Vector3.UnitY, Vector3.UnitZ),
out rotation);
}
public static bool TryCreateQuaternionFromForward(
Vector3 desiredForward,
Vector3 canonicalUp,
ModelAxisConvention convention,
Quaternion correctionQuaternion,
out Quaternion rotation)
{ {
if (convention == null) if (convention == null)
{ {
@ -50,7 +80,27 @@ namespace NavisworksTransport.Utils.CoordinateSystem
} }
Vector3 normalizedForward = Vector3.Normalize(projectedForward); Vector3 normalizedForward = Vector3.Normalize(projectedForward);
rotation = convention.CreateQuaternion(normalizedForward, Vector3.Normalize(canonicalUp)); Vector3 normalizedUp = Vector3.Normalize(canonicalUp);
if (Math.Abs(correctionQuaternion.X) < 1e-9f &&
Math.Abs(correctionQuaternion.Y) < 1e-9f &&
Math.Abs(correctionQuaternion.Z) < 1e-9f &&
Math.Abs(correctionQuaternion.W - 1.0f) < 1e-9f)
{
rotation = convention.CreateQuaternion(normalizedForward, normalizedUp);
return true;
}
LocalAxisPoseBuilder.ApplyLocalPreRotation(
convention,
correctionQuaternion,
out var correctedLocalForward,
out var correctedLocalUp);
rotation = LocalAxisPoseBuilder.CreateQuaternionFromLocalAxes(
correctedLocalForward,
correctedLocalUp,
normalizedForward,
normalizedUp);
return true; return true;
} }
} }

View File

@ -161,7 +161,27 @@ namespace NavisworksTransport.Utils.CoordinateSystem
canonicalUp, canonicalUp,
convention, convention,
null, null,
localUpRotationDegrees, Quaternion.CreateFromAxisAngle(Vector3.Normalize(convention.UpUnitVector), (float)(localUpRotationDegrees * Math.PI / 180.0)),
out rotation);
}
public static bool TryCreateQuaternion(
Vector3 previousPoint,
Vector3 currentPoint,
Vector3 nextPoint,
Vector3 canonicalUp,
ModelAxisConvention convention,
LocalEulerRotationCorrection correction,
out Quaternion rotation)
{
return TryCreateQuaternion(
previousPoint,
currentPoint,
nextPoint,
canonicalUp,
convention,
null,
correction.CreateQuaternion(Vector3.UnitX, Vector3.UnitY, Vector3.UnitZ),
out rotation); out rotation);
} }
@ -174,6 +194,27 @@ namespace NavisworksTransport.Utils.CoordinateSystem
Vector3? preferredNormal, Vector3? preferredNormal,
double localUpRotationDegrees, double localUpRotationDegrees,
out Quaternion rotation) out Quaternion rotation)
{
return TryCreateQuaternion(
previousPoint,
currentPoint,
nextPoint,
canonicalUp,
convention,
preferredNormal,
Quaternion.CreateFromAxisAngle(Vector3.Normalize(convention.UpUnitVector), (float)(localUpRotationDegrees * Math.PI / 180.0)),
out rotation);
}
public static bool TryCreateQuaternion(
Vector3 previousPoint,
Vector3 currentPoint,
Vector3 nextPoint,
Vector3 canonicalUp,
ModelAxisConvention convention,
Vector3? preferredNormal,
Quaternion correctionQuaternion,
out Quaternion rotation)
{ {
rotation = Quaternion.Identity; rotation = Quaternion.Identity;
@ -195,67 +236,22 @@ namespace NavisworksTransport.Utils.CoordinateSystem
return false; return false;
} }
if (Math.Abs(localUpRotationDegrees) < 1e-9) if (Math.Abs(correctionQuaternion.X) < 1e-9f &&
Math.Abs(correctionQuaternion.Y) < 1e-9f &&
Math.Abs(correctionQuaternion.Z) < 1e-9f &&
Math.Abs(correctionQuaternion.W - 1.0f) < 1e-9f)
{ {
rotation = convention.CreateQuaternion(forward, up); rotation = convention.CreateQuaternion(forward, up);
return true; return true;
} }
float correctionRadians = (float)(localUpRotationDegrees * Math.PI / 180.0); LocalAxisPoseBuilder.ApplyLocalPreRotation(
Quaternion localPreRotation = Quaternion.CreateFromAxisAngle( convention,
Vector3.Normalize(convention.UpUnitVector), correctionQuaternion,
correctionRadians); out var correctedLocalForward,
out var correctedLocalUp);
Vector3 correctedLocalForward = Vector3.Normalize(Vector3.Transform(convention.ForwardUnitVector, localPreRotation)); rotation = LocalAxisPoseBuilder.CreateQuaternionFromLocalAxes(correctedLocalForward, correctedLocalUp, forward, up);
Vector3 correctedLocalUp = Vector3.Normalize(Vector3.Transform(convention.UpUnitVector, localPreRotation));
rotation = CreateQuaternionFromLocalAxes(correctedLocalForward, correctedLocalUp, forward, up);
return true; return true;
} }
private static Quaternion CreateQuaternionFromLocalAxes(
Vector3 localForward,
Vector3 localUp,
Vector3 worldForward,
Vector3 worldUp)
{
Vector3 normalizedLocalForward = Vector3.Normalize(localForward);
Vector3 normalizedLocalUp = Vector3.Normalize(localUp);
Vector3 normalizedWorldForward = Vector3.Normalize(worldForward);
Vector3 normalizedWorldUp = Vector3.Normalize(worldUp);
Vector3 localRemaining = Vector3.Normalize(Vector3.Cross(normalizedLocalForward, normalizedLocalUp));
Vector3 worldRemaining = Vector3.Normalize(Vector3.Cross(normalizedWorldForward, normalizedWorldUp));
Vector3 worldX = MapOriginalLocalAxis(Vector3.UnitX, normalizedLocalForward, normalizedLocalUp, localRemaining, normalizedWorldForward, normalizedWorldUp, worldRemaining);
Vector3 worldY = MapOriginalLocalAxis(Vector3.UnitY, normalizedLocalForward, normalizedLocalUp, localRemaining, normalizedWorldForward, normalizedWorldUp, worldRemaining);
Vector3 worldZ = MapOriginalLocalAxis(Vector3.UnitZ, normalizedLocalForward, normalizedLocalUp, localRemaining, normalizedWorldForward, normalizedWorldUp, worldRemaining);
Matrix4x4 linear = 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);
return Quaternion.CreateFromRotationMatrix(linear);
}
private static Vector3 MapOriginalLocalAxis(
Vector3 originalAxis,
Vector3 localForward,
Vector3 localUp,
Vector3 localRemaining,
Vector3 worldForward,
Vector3 worldUp,
Vector3 worldRemaining)
{
float forwardWeight = Vector3.Dot(originalAxis, localForward);
float upWeight = Vector3.Dot(originalAxis, localUp);
float remainingWeight = Vector3.Dot(originalAxis, localRemaining);
return Vector3.Normalize(
forwardWeight * worldForward +
upWeight * worldUp +
remainingWeight * worldRemaining);
}
} }
} }

View File

@ -188,6 +188,27 @@ namespace NavisworksTransport.Utils.CoordinateSystem
hostLinear.M31, hostLinear.M32, hostLinear.M33); hostLinear.M31, hostLinear.M32, hostLinear.M33);
} }
/// <summary>
/// 将宿主 X/Y/Z 三轴旋转角转换为内部 Canonical Space 中的旋转四元数。
/// 约定:先绕宿主 X再绕宿主 Y最后绕宿主 Z。
/// </summary>
public Quaternion CreateCanonicalRotationCorrection(LocalEulerRotationCorrection correction)
{
if (correction.IsZero)
{
return Quaternion.Identity;
}
Vector3 canonicalHostX = Vector3.Normalize(ToCanonicalVector3(Vector3.UnitX));
Vector3 canonicalHostY = Vector3.Normalize(ToCanonicalVector3(Vector3.UnitY));
Vector3 canonicalHostZ = Vector3.Normalize(ToCanonicalVector3(Vector3.UnitZ));
Quaternion qx = Quaternion.CreateFromAxisAngle(canonicalHostX, DegreesToRadians(correction.XDegrees));
Quaternion qy = Quaternion.CreateFromAxisAngle(canonicalHostY, DegreesToRadians(correction.YDegrees));
Quaternion qz = Quaternion.CreateFromAxisAngle(canonicalHostZ, DegreesToRadians(correction.ZDegrees));
return Quaternion.Normalize(qz * qy * qx);
}
private static Rotation3D CreateRotationFromLinearComponents( private static Rotation3D CreateRotationFromLinearComponents(
double m00, double m01, double m02, double m00, double m01, double m02,
double m10, double m11, double m12, double m10, double m11, double m12,
@ -340,5 +361,10 @@ namespace NavisworksTransport.Utils.CoordinateSystem
{ {
return new Vector3D(vector.X, vector.Y, vector.Z); return new Vector3D(vector.X, vector.Y, vector.Z);
} }
private static float DegreesToRadians(double degrees)
{
return (float)(degrees * Math.PI / 180.0);
}
} }
} }

View File

@ -0,0 +1,76 @@
using System;
using System.Numerics;
namespace NavisworksTransport.Utils.CoordinateSystem
{
/// <summary>
/// 根据模型局部轴和世界目标 forward/up 构造姿态。
/// </summary>
public static class LocalAxisPoseBuilder
{
public static Quaternion CreateQuaternionFromLocalAxes(
Vector3 localForward,
Vector3 localUp,
Vector3 worldForward,
Vector3 worldUp)
{
Vector3 normalizedLocalForward = Vector3.Normalize(localForward);
Vector3 normalizedLocalUp = Vector3.Normalize(localUp);
Vector3 normalizedWorldForward = Vector3.Normalize(worldForward);
Vector3 normalizedWorldUp = Vector3.Normalize(worldUp);
Vector3 localRemaining = Vector3.Normalize(Vector3.Cross(normalizedLocalForward, normalizedLocalUp));
Vector3 worldRemaining = Vector3.Normalize(Vector3.Cross(normalizedWorldForward, normalizedWorldUp));
Vector3 worldX = MapOriginalLocalAxis(Vector3.UnitX, normalizedLocalForward, normalizedLocalUp, localRemaining, normalizedWorldForward, normalizedWorldUp, worldRemaining);
Vector3 worldY = MapOriginalLocalAxis(Vector3.UnitY, normalizedLocalForward, normalizedLocalUp, localRemaining, normalizedWorldForward, normalizedWorldUp, worldRemaining);
Vector3 worldZ = MapOriginalLocalAxis(Vector3.UnitZ, normalizedLocalForward, normalizedLocalUp, localRemaining, normalizedWorldForward, normalizedWorldUp, worldRemaining);
Matrix4x4 linear = 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);
return Quaternion.CreateFromRotationMatrix(linear);
}
public static void ApplyLocalPreRotation(
ModelAxisConvention convention,
Quaternion correctionQuaternion,
out Vector3 correctedLocalForward,
out Vector3 correctedLocalUp)
{
if (Math.Abs(correctionQuaternion.X) < 1e-9f &&
Math.Abs(correctionQuaternion.Y) < 1e-9f &&
Math.Abs(correctionQuaternion.Z) < 1e-9f &&
Math.Abs(correctionQuaternion.W - 1.0f) < 1e-9f)
{
correctedLocalForward = convention.ForwardUnitVector;
correctedLocalUp = convention.UpUnitVector;
return;
}
correctedLocalForward = Vector3.Normalize(Vector3.Transform(convention.ForwardUnitVector, correctionQuaternion));
correctedLocalUp = Vector3.Normalize(Vector3.Transform(convention.UpUnitVector, correctionQuaternion));
}
private static Vector3 MapOriginalLocalAxis(
Vector3 originalAxis,
Vector3 localForward,
Vector3 localUp,
Vector3 localRemaining,
Vector3 worldForward,
Vector3 worldUp,
Vector3 worldRemaining)
{
float forwardWeight = Vector3.Dot(originalAxis, localForward);
float upWeight = Vector3.Dot(originalAxis, localUp);
float remainingWeight = Vector3.Dot(originalAxis, localRemaining);
return Vector3.Normalize(
forwardWeight * worldForward +
upWeight * worldUp +
remainingWeight * worldRemaining);
}
}
}

View File

@ -0,0 +1,111 @@
using System;
using System.Globalization;
using System.Numerics;
namespace NavisworksTransport.Utils.CoordinateSystem
{
/// <summary>
/// 三轴旋转修正角(单位:度)。
/// 该结构本身只保存 X/Y/Z 三个角度值,不直接绑定宿主/内部/资产语义。
/// </summary>
public struct LocalEulerRotationCorrection : IEquatable<LocalEulerRotationCorrection>
{
public static readonly LocalEulerRotationCorrection Zero = new LocalEulerRotationCorrection(0.0, 0.0, 0.0);
public LocalEulerRotationCorrection(double xDegrees, double yDegrees, double zDegrees)
{
XDegrees = xDegrees;
YDegrees = yDegrees;
ZDegrees = zDegrees;
}
public double XDegrees { get; }
public double YDegrees { get; }
public double ZDegrees { get; }
public bool IsZero =>
Math.Abs(XDegrees) < 1e-9 &&
Math.Abs(YDegrees) < 1e-9 &&
Math.Abs(ZDegrees) < 1e-9;
public Quaternion CreateQuaternion(Vector3 axisX, Vector3 axisY, Vector3 axisZ)
{
Quaternion qx = Quaternion.CreateFromAxisAngle(Vector3.Normalize(axisX), DegreesToRadians(XDegrees));
Quaternion qy = Quaternion.CreateFromAxisAngle(Vector3.Normalize(axisY), DegreesToRadians(YDegrees));
Quaternion qz = Quaternion.CreateFromAxisAngle(Vector3.Normalize(axisZ), DegreesToRadians(ZDegrees));
return Quaternion.Normalize(qz * qy * qx);
}
public static LocalEulerRotationCorrection FromConventionUpDegrees(ModelAxisConvention convention, double upDegrees)
{
if (convention == null)
{
throw new ArgumentNullException(nameof(convention));
}
switch (convention.UpAxis)
{
case LocalAxisDirection.PositiveX:
case LocalAxisDirection.NegativeX:
return new LocalEulerRotationCorrection(upDegrees, 0.0, 0.0);
case LocalAxisDirection.PositiveY:
case LocalAxisDirection.NegativeY:
return new LocalEulerRotationCorrection(0.0, upDegrees, 0.0);
case LocalAxisDirection.PositiveZ:
case LocalAxisDirection.NegativeZ:
default:
return new LocalEulerRotationCorrection(0.0, 0.0, upDegrees);
}
}
public override string ToString()
{
return string.Format(
CultureInfo.InvariantCulture,
"X={0:F1}°,Y={1:F1}°,Z={2:F1}°",
XDegrees,
YDegrees,
ZDegrees);
}
public bool Equals(LocalEulerRotationCorrection other)
{
return XDegrees.Equals(other.XDegrees) &&
YDegrees.Equals(other.YDegrees) &&
ZDegrees.Equals(other.ZDegrees);
}
public override bool Equals(object obj)
{
return obj is LocalEulerRotationCorrection other && Equals(other);
}
public override int GetHashCode()
{
unchecked
{
int hash = 17;
hash = hash * 31 + XDegrees.GetHashCode();
hash = hash * 31 + YDegrees.GetHashCode();
hash = hash * 31 + ZDegrees.GetHashCode();
return hash;
}
}
public static bool operator ==(LocalEulerRotationCorrection left, LocalEulerRotationCorrection right)
{
return left.Equals(right);
}
public static bool operator !=(LocalEulerRotationCorrection left, LocalEulerRotationCorrection right)
{
return !left.Equals(right);
}
private static float DegreesToRadians(double degrees)
{
return (float)(degrees * Math.PI / 180.0);
}
}
}

View File

@ -6,6 +6,27 @@ namespace NavisworksTransport.Utils.CoordinateSystem
{ {
public static class ObjectSpaceOrientationHelper public static class ObjectSpaceOrientationHelper
{ {
public static bool TryCalculateAxes(
Vector3 segmentDirection,
Vector3 upReference,
out (Vector3 right, Vector3 up) axes,
Vector3? horizontalDirection = null)
{
double segmentLength = Math.Sqrt(
segmentDirection.X * segmentDirection.X +
segmentDirection.Y * segmentDirection.Y +
segmentDirection.Z * segmentDirection.Z);
if (segmentLength < 1e-9)
{
axes = (Vector3.Zero, Vector3.Zero);
return false;
}
axes = CalculateAxes(segmentDirection, upReference, horizontalDirection);
return true;
}
public static (Vector3D right, Vector3D up) CalculateAxes( public static (Vector3D right, Vector3D up) CalculateAxes(
Point3D startPoint, Point3D startPoint,
Point3D endPoint, Point3D endPoint,

View File

@ -0,0 +1,58 @@
using System;
using System.Numerics;
namespace NavisworksTransport.Utils.CoordinateSystem
{
/// <summary>
/// 计算物体本地预旋转后,在 forward/side/up 语义下的投影尺寸。
/// </summary>
public static class RotatedObjectExtentHelper
{
public static (double forwardExtent, double sideExtent, double upExtent) CalculateProjectedSemanticExtents(
ModelAxisConvention convention,
double forwardSize,
double sideSize,
double upSize,
Quaternion correctionQuaternion)
{
if (convention == null)
{
throw new ArgumentNullException(nameof(convention));
}
if (Math.Abs(correctionQuaternion.X) < 1e-9f &&
Math.Abs(correctionQuaternion.Y) < 1e-9f &&
Math.Abs(correctionQuaternion.Z) < 1e-9f &&
Math.Abs(correctionQuaternion.W - 1.0f) < 1e-9f)
{
return (forwardSize, sideSize, upSize);
}
Vector3 localSize = convention.CreateScaleVector3(forwardSize, sideSize, upSize);
Vector3 rotatedLocalX = Vector3.Normalize(Vector3.Transform(Vector3.UnitX, correctionQuaternion));
Vector3 rotatedLocalY = Vector3.Normalize(Vector3.Transform(Vector3.UnitY, correctionQuaternion));
Vector3 rotatedLocalZ = Vector3.Normalize(Vector3.Transform(Vector3.UnitZ, correctionQuaternion));
Vector3 semanticForward = Vector3.Normalize(convention.ForwardUnitVector);
Vector3 semanticUp = Vector3.Normalize(convention.UpUnitVector);
Vector3 semanticSide = Vector3.Normalize(Vector3.Cross(semanticForward, semanticUp));
return (
ProjectExtent(localSize, rotatedLocalX, rotatedLocalY, rotatedLocalZ, semanticForward),
ProjectExtent(localSize, rotatedLocalX, rotatedLocalY, rotatedLocalZ, semanticSide),
ProjectExtent(localSize, rotatedLocalX, rotatedLocalY, rotatedLocalZ, semanticUp));
}
private static double ProjectExtent(
Vector3 localSize,
Vector3 rotatedLocalX,
Vector3 rotatedLocalY,
Vector3 rotatedLocalZ,
Vector3 targetAxis)
{
return Math.Abs(Vector3.Dot(rotatedLocalX, targetAxis)) * localSize.X +
Math.Abs(Vector3.Dot(rotatedLocalY, targetAxis)) * localSize.Y +
Math.Abs(Vector3.Dot(rotatedLocalZ, targetAxis)) * localSize.Z;
}
}
}

View File

@ -218,6 +218,25 @@ namespace NavisworksTransport.Utils
ModelAxisConvention axisConvention, ModelAxisConvention axisConvention,
double localUpRotationDegrees, double localUpRotationDegrees,
out Matrix3 linearTransform) out Matrix3 linearTransform)
{
return TryCreateRailLinearTransform(
route,
previousPoint,
currentPoint,
nextPoint,
axisConvention,
LocalEulerRotationCorrection.FromConventionUpDegrees(axisConvention, localUpRotationDegrees),
out linearTransform);
}
public static bool TryCreateRailLinearTransform(
PathRoute route,
Point3D previousPoint,
Point3D currentPoint,
Point3D nextPoint,
ModelAxisConvention axisConvention,
LocalEulerRotationCorrection correction,
out Matrix3 linearTransform)
{ {
linearTransform = null; linearTransform = null;
@ -245,6 +264,7 @@ namespace NavisworksTransport.Utils
var canonicalForward = frame.Forward; var canonicalForward = frame.Forward;
var canonicalLateral = frame.Lateral; var canonicalLateral = frame.Lateral;
var canonicalUp = frame.Normal; var canonicalUp = frame.Normal;
Quaternion correctionQuaternion = adapter.CreateCanonicalRotationCorrection(correction);
Quaternion canonicalRotation = CanonicalRailPoseBuilder.TryCreateQuaternion( Quaternion canonicalRotation = CanonicalRailPoseBuilder.TryCreateQuaternion(
canonicalPreviousPoint, canonicalPreviousPoint,
canonicalCurrentPoint, canonicalCurrentPoint,
@ -252,7 +272,7 @@ namespace NavisworksTransport.Utils
HostCoordinateAdapter.CanonicalUpVector3, HostCoordinateAdapter.CanonicalUpVector3,
axisConvention, axisConvention,
ResolvePreferredNormal(route), ResolvePreferredNormal(route),
localUpRotationDegrees, correctionQuaternion,
out var correctedRotation) out var correctedRotation)
? correctedRotation ? correctedRotation
: axisConvention.CreateQuaternion(canonicalForward, canonicalUp); : axisConvention.CreateQuaternion(canonicalForward, canonicalUp);
@ -276,7 +296,7 @@ namespace NavisworksTransport.Utils
$"Canonical侧向=({canonicalLateral.X:F4},{canonicalLateral.Y:F4},{canonicalLateral.Z:F4}), " + $"Canonical侧向=({canonicalLateral.X:F4},{canonicalLateral.Y:F4},{canonicalLateral.Z:F4}), " +
$"Canonical法向=({canonicalUp.X:F4},{canonicalUp.Y:F4},{canonicalUp.Z:F4}), " + $"Canonical法向=({canonicalUp.X:F4},{canonicalUp.Y:F4},{canonicalUp.Z:F4}), " +
$"Host法向=({hostZAxis.X:F4},{hostZAxis.Y:F4},{hostZAxis.Z:F4}), " + $"Host法向=({hostZAxis.X:F4},{hostZAxis.Y:F4},{hostZAxis.Z:F4}), " +
$"模型Forward={axisConvention.ForwardAxis}, 模型Up={axisConvention.UpAxis}, 本地预旋转={localUpRotationDegrees:F1}°"); $"模型Forward={axisConvention.ForwardAxis}, 模型Up={axisConvention.UpAxis}, 宿主轴旋转={correction}");
return true; return true;
} }
@ -356,6 +376,25 @@ namespace NavisworksTransport.Utils
ModelAxisConvention axisConvention, ModelAxisConvention axisConvention,
double localUpRotationDegrees, double localUpRotationDegrees,
out Rotation3D rotation) out Rotation3D rotation)
{
return TryCreateRailRotation(
route,
previousPoint,
currentPoint,
nextPoint,
axisConvention,
LocalEulerRotationCorrection.FromConventionUpDegrees(axisConvention, localUpRotationDegrees),
out rotation);
}
public static bool TryCreateRailRotation(
PathRoute route,
Point3D previousPoint,
Point3D currentPoint,
Point3D nextPoint,
ModelAxisConvention axisConvention,
LocalEulerRotationCorrection correction,
out Rotation3D rotation)
{ {
rotation = Rotation3D.Identity; rotation = Rotation3D.Identity;
LogRotationConstructorConventionOnce(); LogRotationConstructorConventionOnce();
@ -366,7 +405,7 @@ namespace NavisworksTransport.Utils
currentPoint, currentPoint,
nextPoint, nextPoint,
axisConvention, axisConvention,
localUpRotationDegrees, correction,
out var linearTransform)) out var linearTransform))
{ {
return false; return false;