From d52b1aef082912d637175f1bc18b2b7bc0000fe0 Mon Sep 17 00:00:00 2001 From: tian <11429339@qq.com> Date: Mon, 23 Mar 2026 21:00:02 +0800 Subject: [PATCH] Checkpoint animation rotation refactor state --- AGENTS.md | 13 ++ TransportPlugin.csproj | 3 + .../CanonicalPlanarPoseBuilderTests.cs | 74 ++++++++ .../CanonicalRailPoseBuilderTests.cs | 28 +++ .../HostCoordinateAdapterTests.cs | 15 ++ .../ObjectSpaceOrientationHelperTests.cs | 16 ++ deploy-plugin.bat | 27 ++- ...oordinate-system-canonical-space-design.md | 54 +++++- doc/requirement/todo_features.md | 7 +- src/Core/Animation/PathAnimationManager.cs | 159 +++++++++++------- src/Core/PathPointRenderPlugin.cs | 19 ++- src/Core/VirtualObjectManager.cs | 4 +- .../ViewModels/AnimationControlViewModel.cs | 156 ++++++++--------- src/UI/WPF/Views/EditRotationWindow.xaml | 60 ++++++- src/UI/WPF/Views/EditRotationWindow.xaml.cs | 121 +++++++++++-- .../CanonicalPlanarPoseBuilder.cs | 52 +++++- .../CanonicalRailPoseBuilder.cs | 108 ++++++------ .../CoordinateSystem/HostCoordinateAdapter.cs | 26 +++ .../CoordinateSystem/LocalAxisPoseBuilder.cs | 76 +++++++++ .../LocalEulerRotationCorrection.cs | 111 ++++++++++++ .../ObjectSpaceOrientationHelper.cs | 21 +++ .../RotatedObjectExtentHelper.cs | 58 +++++++ src/Utils/RailPathPoseHelper.cs | 45 ++++- 23 files changed, 1015 insertions(+), 238 deletions(-) create mode 100644 src/Utils/CoordinateSystem/LocalAxisPoseBuilder.cs create mode 100644 src/Utils/CoordinateSystem/LocalEulerRotationCorrection.cs create mode 100644 src/Utils/CoordinateSystem/RotatedObjectExtentHelper.cs 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 @@  - + - - + + - + - + + + + + + + + + + + + + + + + + + + + + + + + + - +