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

View File

@ -334,11 +334,14 @@
<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\LocalAxisPoseBuilder.cs" />
<Compile Include="src\Utils\CoordinateSystem\LocalEulerRotationCorrection.cs" />
<Compile Include="src\Utils\CoordinateSystem\CanonicalPlanarPoseBuilder.cs" />
<Compile Include="src\Utils\CoordinateSystem\CanonicalRailOffsetResolver.cs" />
<Compile Include="src\Utils\CoordinateSystem\CanonicalRailPoseBuilder.cs" />
<Compile Include="src\Utils\CoordinateSystem\CanonicalTrackedPositionResolver.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\HoistingCoordinateHelper.cs" />
<Compile Include="src\Utils\CoordinateSystem\LocalAxisDirection.cs" />

View File

@ -1,5 +1,6 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using NavisworksTransport.Utils.CoordinateSystem;
using System;
using System.Numerics;
namespace NavisworksTransport.UnitTests.CoordinateSystem
@ -55,6 +56,52 @@ namespace NavisworksTransport.UnitTests.CoordinateSystem
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)
{
switch (column)
@ -79,5 +126,32 @@ namespace NavisworksTransport.UnitTests.CoordinateSystem
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);
}
[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]
public void PreferredNormal_ShouldOverrideCanonicalUpForRailFrame()
{

View File

@ -1,6 +1,7 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using NavisworksTransport.Utils.CoordinateSystem;
using System.Numerics;
using System;
using Autodesk.Navisworks.Api;
namespace NavisworksTransport.UnitTests.CoordinateSystem
@ -143,6 +144,20 @@ namespace NavisworksTransport.UnitTests.CoordinateSystem
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)
{
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.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';" ^
"$targetDir = [System.IO.Path]::GetFullPath('%TARGET_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) {" ^
" if (-not (Test-Path $path)) { return $true }" ^
" try {" ^
@ -39,14 +54,22 @@ powershell -NoProfile -ExecutionPolicy Bypass -Command ^
"foreach ($keyFile in $keyTargetFiles) { Wait-FileUnlocked $keyFile $deadline }" ^
"" ^
"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')) {" ^
" New-Item -ItemType Directory -Force -Path (Join-Path $targetDir 'resources') | Out-Null;" ^
" Copy-Item (Join-Path $buildDir 'resources\\*') (Join-Path $targetDir 'resources') -Recurse -Force;" ^
"}" ^
"" ^
"$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')) {" ^
" $sourceFiles += Get-ChildItem (Join-Path $buildDir 'resources') -File;" ^
"}" ^

View File

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

View File

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

View File

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

View File

@ -1886,6 +1886,19 @@ namespace NavisworksTransport
{
var startPoint = sortedPoints[i];
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;
@ -1911,11 +1924,7 @@ namespace NavisworksTransport
// 沿宿主 up 判断垂直段,避免把世界 Z 误当成向上轴
var hostUp = GetHostUpVector();
double dx = endPoint.Position.X - startPoint.Position.X;
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 upDelta = segmentDx * hostUp.X + segmentDy * hostUp.Y + segmentDz * hostUp.Z;
double horizontalDist = Math.Sqrt(Math.Max(0.0, segmentLength * segmentLength - upDelta * upDelta));
// 如果沿宿主 up 的变化占主导(垂直段),或者是第一个/最后一个线段
bool isZDominant = Math.Abs(upDelta) > horizontalDist * 2.0;

View File

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

View File

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

View File

@ -1,7 +1,7 @@
<Window x:Class="NavisworksTransport.UI.WPF.Views.EditRotationWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="调整物体角度" Height="320" Width="400"
Title="调整物体角度" Height="360" Width="430"
WindowStartupLocation="CenterScreen"
ResizeMode="NoResize"
ShowInTaskbar="False"
@ -26,23 +26,67 @@
<Border Grid.Row="0" Background="#FFF8FBFF" BorderBrush="#FFD4E7FF" BorderThickness="0,0,0,1" Padding="20,15">
<StackPanel>
<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>
</Border>
<!-- 输入区域 -->
<Border Grid.Row="1" Padding="20,15">
<StackPanel>
<!-- 角度输入 -->
<Grid Margin="0,0,0,12">
<!-- X轴 -->
<Grid Margin="0,0,0,8">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="80"/>
<ColumnDefinition Width="110"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="50"/>
</Grid.ColumnDefinitions>
<TextBlock Text="旋转角度:" VerticalAlignment="Center" FontSize="11" Foreground="#FF333333"/>
<TextBlock Text="绕 X 轴:" VerticalAlignment="Center" FontSize="11" Foreground="#FF333333"/>
<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"
VerticalContentAlignment="Center"
Padding="8,0"
@ -53,7 +97,7 @@
</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">
<Button Content="0°"
Click="OnQuickAngleClick"

View File

@ -2,33 +2,80 @@ using System;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Windows;
using System.Windows.Controls;
using NavisworksTransport.Utils.CoordinateSystem;
namespace NavisworksTransport.UI.WPF.Views
{
public partial class EditRotationWindow : Window, INotifyPropertyChanged
{
private double _rotationAngle;
public double RotationAngle
private enum RotationAxisTarget
{
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
{
if (_rotationAngle != value)
if (_rotationXDegrees != value)
{
_rotationAngle = value;
_rotationXDegrees = value;
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
{
InitializeComponent();
RotationAngle = currentAngle;
RotationXDegrees = currentRotation.XDegrees;
RotationYDegrees = currentRotation.YDegrees;
RotationZDegrees = currentRotation.ZDegrees;
DataContext = this;
Loaded += (sender, args) => ZAxisTextBox.Focus();
}
catch (Exception ex)
{
@ -39,34 +86,57 @@ namespace NavisworksTransport.UI.WPF.Views
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}");
if (button.Tag != null)
{
double? parsedAngle = null;
if (button.Tag is int intTag)
{
RotationAngle = intTag;
LogManager.Debug($"[快捷角度] 设置角度为: {intTag}°");
parsedAngle = intTag;
}
else if (button.Tag is string stringTag && double.TryParse(stringTag, out double angle))
{
RotationAngle = angle;
LogManager.Debug($"[快捷角度] 设置角度为: {angle}°");
parsedAngle = angle;
}
else if (button.Tag is double doubleTag)
{
RotationAngle = doubleTag;
LogManager.Debug($"[快捷角度] 设置角度为: {doubleTag}°");
parsedAngle = 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)
{
RotationAngle = 0.0;
RotationXDegrees = 0.0;
RotationYDegrees = 0.0;
RotationZDegrees = 0.0;
}
private void OnConfirmClick(object sender, RoutedEventArgs e)
@ -87,5 +157,22 @@ namespace NavisworksTransport.UI.WPF.Views
{
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,
ModelAxisConvention convention,
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)
{
@ -50,7 +80,27 @@ namespace NavisworksTransport.Utils.CoordinateSystem
}
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;
}
}

View File

@ -161,7 +161,27 @@ namespace NavisworksTransport.Utils.CoordinateSystem
canonicalUp,
convention,
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);
}
@ -174,6 +194,27 @@ namespace NavisworksTransport.Utils.CoordinateSystem
Vector3? preferredNormal,
double localUpRotationDegrees,
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;
@ -195,67 +236,22 @@ namespace NavisworksTransport.Utils.CoordinateSystem
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);
return true;
}
float correctionRadians = (float)(localUpRotationDegrees * Math.PI / 180.0);
Quaternion localPreRotation = Quaternion.CreateFromAxisAngle(
Vector3.Normalize(convention.UpUnitVector),
correctionRadians);
Vector3 correctedLocalForward = Vector3.Normalize(Vector3.Transform(convention.ForwardUnitVector, localPreRotation));
Vector3 correctedLocalUp = Vector3.Normalize(Vector3.Transform(convention.UpUnitVector, localPreRotation));
rotation = CreateQuaternionFromLocalAxes(correctedLocalForward, correctedLocalUp, forward, up);
LocalAxisPoseBuilder.ApplyLocalPreRotation(
convention,
correctionQuaternion,
out var correctedLocalForward,
out var correctedLocalUp);
rotation = LocalAxisPoseBuilder.CreateQuaternionFromLocalAxes(correctedLocalForward, correctedLocalUp, forward, up);
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);
}
/// <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(
double m00, double m01, double m02,
double m10, double m11, double m12,
@ -340,5 +361,10 @@ namespace NavisworksTransport.Utils.CoordinateSystem
{
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 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(
Point3D startPoint,
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,
double localUpRotationDegrees,
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;
@ -245,6 +264,7 @@ namespace NavisworksTransport.Utils
var canonicalForward = frame.Forward;
var canonicalLateral = frame.Lateral;
var canonicalUp = frame.Normal;
Quaternion correctionQuaternion = adapter.CreateCanonicalRotationCorrection(correction);
Quaternion canonicalRotation = CanonicalRailPoseBuilder.TryCreateQuaternion(
canonicalPreviousPoint,
canonicalCurrentPoint,
@ -252,7 +272,7 @@ namespace NavisworksTransport.Utils
HostCoordinateAdapter.CanonicalUpVector3,
axisConvention,
ResolvePreferredNormal(route),
localUpRotationDegrees,
correctionQuaternion,
out var correctedRotation)
? correctedRotation
: axisConvention.CreateQuaternion(canonicalForward, canonicalUp);
@ -276,7 +296,7 @@ namespace NavisworksTransport.Utils
$"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}, 本地预旋转={localUpRotationDegrees:F1}°");
$"模型Forward={axisConvention.ForwardAxis}, 模型Up={axisConvention.UpAxis}, 宿主轴旋转={correction}");
return true;
}
@ -356,6 +376,25 @@ namespace NavisworksTransport.Utils
ModelAxisConvention axisConvention,
double localUpRotationDegrees,
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;
LogRotationConstructorConventionOnce();
@ -366,7 +405,7 @@ namespace NavisworksTransport.Utils
currentPoint,
nextPoint,
axisConvention,
localUpRotationDegrees,
correction,
out var linearTransform))
{
return false;