diff --git a/AGENTS.md b/AGENTS.md
index d5d4b7e..9e55c6a 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -308,6 +308,19 @@ double distance = GeometryHelper.Distance(pointA, pointB);
- 需要坐标转换?→ 使用 `CoordinateConverter`
- 需要路径相关工具?→ 使用 `PathHelper`
+#### 5. 临时定位代码必须回收
+
+为定位问题临时加入的兜底调用、强制刷新、额外同步、绕过分支等代码,如果后续确认**不是真正根因**,在根因修复后必须恢复或删除,不能把这类临时补丁长期保留在正式代码中。
+
+```csharp
+// ❌ 错误:为掩盖问题长期保留的强制调用
+DoNormalUpdate();
+ForceRefreshAgain(); // 只是为了“看起来好了”
+
+// ✅ 正确:先定位根因,再移除临时补丁
+DoNormalUpdate();
+```
+
### 单位系统 - 极其重要
**所有网格地图和路径规划计算必须使用模型单位,严禁混用米制单位。**
diff --git a/TransportPlugin.csproj b/TransportPlugin.csproj
index af09c90..0b5454e 100644
--- a/TransportPlugin.csproj
+++ b/TransportPlugin.csproj
@@ -334,11 +334,14 @@
+
+
+
diff --git a/UnitTests/CoordinateSystem/CanonicalPlanarPoseBuilderTests.cs b/UnitTests/CoordinateSystem/CanonicalPlanarPoseBuilderTests.cs
index b0b6b0a..89b058c 100644
--- a/UnitTests/CoordinateSystem/CanonicalPlanarPoseBuilderTests.cs
+++ b/UnitTests/CoordinateSystem/CanonicalPlanarPoseBuilderTests.cs
@@ -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);
+ }
}
}
diff --git a/UnitTests/CoordinateSystem/CanonicalRailPoseBuilderTests.cs b/UnitTests/CoordinateSystem/CanonicalRailPoseBuilderTests.cs
index deef6f7..74b6405 100644
--- a/UnitTests/CoordinateSystem/CanonicalRailPoseBuilderTests.cs
+++ b/UnitTests/CoordinateSystem/CanonicalRailPoseBuilderTests.cs
@@ -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()
{
diff --git a/UnitTests/CoordinateSystem/HostCoordinateAdapterTests.cs b/UnitTests/CoordinateSystem/HostCoordinateAdapterTests.cs
index 3d54d44..9f77b59 100644
--- a/UnitTests/CoordinateSystem/HostCoordinateAdapterTests.cs
+++ b/UnitTests/CoordinateSystem/HostCoordinateAdapterTests.cs
@@ -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);
diff --git a/UnitTests/CoordinateSystem/ObjectSpaceOrientationHelperTests.cs b/UnitTests/CoordinateSystem/ObjectSpaceOrientationHelperTests.cs
index a15baf4..f60f624 100644
--- a/UnitTests/CoordinateSystem/ObjectSpaceOrientationHelperTests.cs
+++ b/UnitTests/CoordinateSystem/ObjectSpaceOrientationHelperTests.cs
@@ -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);
+ }
}
}
diff --git a/deploy-plugin.bat b/deploy-plugin.bat
index 9c39165..0c5a3cd 100644
--- a/deploy-plugin.bat
+++ b/deploy-plugin.bat
@@ -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;" ^
"}" ^
diff --git a/doc/design/2026/coordinate-system-canonical-space-design.md b/doc/design/2026/coordinate-system-canonical-space-design.md
index 7aa02af..9c06e51 100644
--- a/doc/design/2026/coordinate-system-canonical-space-design.md
+++ b/doc/design/2026/coordinate-system-canonical-space-design.md
@@ -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 Space(Z-up)**
- **业务基准点单独建模**
+- **UI 输入输出统一使用宿主坐标系**
+- **资产坐标系只属于插件自带资源,不得泛化成“所有模型都有本地轴”**
- **坐标转换只发生在边界层**
这是一条更接近业界三维软件成熟做法的路线。
diff --git a/doc/requirement/todo_features.md b/doc/requirement/todo_features.md
index 17d59b3..b886dc7 100644
--- a/doc/requirement/todo_features.md
+++ b/doc/requirement/todo_features.md
@@ -2,9 +2,14 @@
## 功能点
+### [2026/3/20]
+
+1. [x] (优化)实现Yup模型兼容
+2. [x] (功能)运动物体在起点支持三个方向的旋转
+
### [2026/3/18]
-1. [x](功能)增加轨上路径(角度可倾斜)
+1. [x] (功能)增加终端仿真需要的轨上和轨下路径(角度可倾斜)
### [2026/3/11]
diff --git a/src/Core/Animation/PathAnimationManager.cs b/src/Core/Animation/PathAnimationManager.cs
index e6ef4fa..3650993 100644
--- a/src/Core/Animation/PathAnimationManager.cs
+++ b/src/Core/Animation/PathAnimationManager.cs
@@ -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 _animationHashToRecordId = new Dictionary(); // 动画配置哈希到检测记录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;
+
///
/// 当前正在进行动画的对象
///
@@ -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;
+ }
}
///
@@ -467,7 +492,9 @@ namespace NavisworksTransport.Core.Animation
///
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
///
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);
- }
-
///
- /// 设置物体角度修正值(度,顺时针)
+ /// 设置物体绕宿主 X/Y/Z 轴的角度修正
///
- /// 角度修正值(度)
- 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
}
///
- /// 直接设置物体角度修正值(不触发物体旋转)
+ /// 保留给旧单轴调用方:按当前宿主 up 轴映射成三轴角度修正。
///
- /// 角度修正值(度)
- public void SetObjectRotationCorrectionDirect(double rotationCorrection)
+ public void SetObjectRotationCorrection(double rotationCorrection)
+ {
+ SetObjectRotationCorrection(CreateLegacyUpAxisCorrection(rotationCorrection));
+ }
+
+ ///
+ /// 直接设置物体绕宿主 X/Y/Z 轴的角度修正(不触发物体旋转)
+ ///
+ public void SetObjectRotationCorrectionDirect(LocalEulerRotationCorrection rotationCorrection)
{
_objectRotationCorrection = rotationCorrection;
- LogManager.Debug($"[角度修正] 直接设置角度修正值: {_objectRotationCorrection:F1}°(不触发旋转)");
+ LogManager.Debug($"[角度修正] 直接设置角度修正值: {_objectRotationCorrection}(不触发旋转)");
+ }
+
+ ///
+ /// 保留给旧单轴调用方:按当前宿主 up 轴映射成三轴角度修正。
+ ///
+ 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("|");
// 包含手工检测对象列表(确保手工指定模式的目标变化时重新检测)
diff --git a/src/Core/PathPointRenderPlugin.cs b/src/Core/PathPointRenderPlugin.cs
index 13983d2..a06c3d2 100644
--- a/src/Core/PathPointRenderPlugin.cs
+++ b/src/Core/PathPointRenderPlugin.cs
@@ -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;
diff --git a/src/Core/VirtualObjectManager.cs b/src/Core/VirtualObjectManager.cs
index f8e7647..311d93b 100644
--- a/src/Core/VirtualObjectManager.cs
+++ b/src/Core/VirtualObjectManager.cs
@@ -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(
diff --git a/src/UI/WPF/ViewModels/AnimationControlViewModel.cs b/src/UI/WPF/ViewModels/AnimationControlViewModel.cs
index 59c3aa1..ffd37f2 100644
--- a/src/UI/WPF/ViewModels/AnimationControlViewModel.cs
+++ b/src/UI/WPF/ViewModels/AnimationControlViewModel.cs
@@ -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
}
///
- /// 物体角度修正值(度,顺时针)
+ /// 物体绕宿主 X/Y/Z 轴的角度修正。
///
- 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
}
///
- /// 计算旋转后的有效宽度和长度
+ /// 计算本地三轴预旋转后的有效尺寸
/// 返回值为模型单位(与Navisworks API一致)
///
- /// (有效宽度, 有效长度) - 模型单位
- 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)
{
- 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;
}
///
@@ -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}");
diff --git a/src/UI/WPF/Views/EditRotationWindow.xaml b/src/UI/WPF/Views/EditRotationWindow.xaml
index f23922e..5eda71f 100644
--- a/src/UI/WPF/Views/EditRotationWindow.xaml
+++ b/src/UI/WPF/Views/EditRotationWindow.xaml
@@ -1,7 +1,7 @@
-
+
-
-
+
+
-
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+