From 3a5693a453deb53244a3b707350a6b4a19ae29f2 Mon Sep 17 00:00:00 2001 From: tian <11429339@qq.com> Date: Thu, 26 Mar 2026 01:01:01 +0800 Subject: [PATCH] Add rail mount placement controls and persistence --- NavisworksTransport.UnitTests.csproj | 1 + TransportPlugin.csproj | 1 + .../CanonicalRailOffsetResolverTests.cs | 21 + .../ObjectStartPlacementRequestTests.cs | 31 + UnitTests/Core/PathPersistenceTests.cs | 5 +- doc/design/2026/NavisworksAPI使用方法.md | 946 ++++++------------ doc/working/current-engineering-state.md | 54 +- src/Core/Animation/PathAnimationManager.cs | 149 ++- src/Core/PathDataManager.cs | 10 + src/Core/PathDatabase.cs | 17 +- src/Core/PathPlanningManager.cs | 1 + src/Core/PathPlanningModels.cs | 9 + .../ViewModels/AnimationControlViewModel.cs | 44 +- src/UI/WPF/ViewModels/PathEditingViewModel.cs | 54 +- src/UI/WPF/Views/EditRotationWindow.xaml | 22 +- src/UI/WPF/Views/EditRotationWindow.xaml.cs | 12 + src/UI/WPF/Views/PathEditingView.xaml | 332 +++--- .../CanonicalRailOffsetResolver.cs | 3 +- .../ObjectStartPlacementRequest.cs | 59 ++ src/Utils/RailPathPoseHelper.cs | 6 +- 20 files changed, 1002 insertions(+), 775 deletions(-) create mode 100644 UnitTests/CoordinateSystem/ObjectStartPlacementRequestTests.cs create mode 100644 src/Utils/CoordinateSystem/ObjectStartPlacementRequest.cs diff --git a/NavisworksTransport.UnitTests.csproj b/NavisworksTransport.UnitTests.csproj index 17555ba..30ef294 100644 --- a/NavisworksTransport.UnitTests.csproj +++ b/NavisworksTransport.UnitTests.csproj @@ -58,6 +58,7 @@ + diff --git a/TransportPlugin.csproj b/TransportPlugin.csproj index f126d00..fb6e518 100644 --- a/TransportPlugin.csproj +++ b/TransportPlugin.csproj @@ -336,6 +336,7 @@ + diff --git a/UnitTests/CoordinateSystem/CanonicalRailOffsetResolverTests.cs b/UnitTests/CoordinateSystem/CanonicalRailOffsetResolverTests.cs index ca843f2..d3091af 100644 --- a/UnitTests/CoordinateSystem/CanonicalRailOffsetResolverTests.cs +++ b/UnitTests/CoordinateSystem/CanonicalRailOffsetResolverTests.cs @@ -91,6 +91,27 @@ namespace NavisworksTransport.UnitTests.CoordinateSystem AssertVector(trackedCenter, 0.0, 0.0, 2.0); } + [TestMethod] + public void RailNormalOffset_ShouldAddExtraDisplacementAlongRailNormal() + { + PathRoute route = new PathRoute + { + PathType = PathType.Rail, + RailMountMode = RailMountMode.OverRail, + RailPathDefinitionMode = RailPathDefinitionMode.RailCenterLine, + RailNormalOffset = 1.5 + }; + + RailLocalFrame frame = new RailLocalFrame(Vector3.UnitX, Vector3.UnitY, Vector3.UnitZ); + Vector3 trackedCenter = CanonicalRailOffsetResolver.ResolveTrackedCenter( + route, + Vector3.Zero, + frame, + 4.0); + + AssertVector(trackedCenter, 0.0, 0.0, 3.5); + } + private static void AssertVector(Vector3 actual, double x, double y, double z) { Assert.AreEqual(x, actual.X, 1e-6); diff --git a/UnitTests/CoordinateSystem/ObjectStartPlacementRequestTests.cs b/UnitTests/CoordinateSystem/ObjectStartPlacementRequestTests.cs new file mode 100644 index 0000000..8c14ad3 --- /dev/null +++ b/UnitTests/CoordinateSystem/ObjectStartPlacementRequestTests.cs @@ -0,0 +1,31 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using NavisworksTransport.Utils.CoordinateSystem; + +namespace NavisworksTransport.UnitTests.CoordinateSystem +{ + [TestClass] + public class ObjectStartPlacementRequestTests + { + [TestMethod] + public void TranslationOnly_ShouldPreserveInitialPoseAndClearRotationCorrection() + { + var request = ObjectStartPlacementRequest.TranslationOnly; + + Assert.AreEqual(ObjectStartPlacementMode.PreserveInitialPose, request.PlacementMode); + Assert.IsTrue(request.PreserveInitialPose); + Assert.AreEqual(LocalEulerRotationCorrection.Zero, request.RotationCorrection); + } + + [TestMethod] + public void RotationCorrectionRequest_ShouldKeepCorrectionAndAlignToPathPose() + { + var correction = new LocalEulerRotationCorrection(15.0, 30.0, 45.0); + + var request = ObjectStartPlacementRequest.CreateRotationCorrection(correction); + + Assert.AreEqual(ObjectStartPlacementMode.AlignToPathPose, request.PlacementMode); + Assert.IsFalse(request.PreserveInitialPose); + Assert.AreEqual(correction, request.RotationCorrection); + } + } +} diff --git a/UnitTests/Core/PathPersistenceTests.cs b/UnitTests/Core/PathPersistenceTests.cs index fa47c56..5830002 100644 --- a/UnitTests/Core/PathPersistenceTests.cs +++ b/UnitTests/Core/PathPersistenceTests.cs @@ -38,6 +38,7 @@ namespace NavisworksTransport.UnitTests.Core ""pathType"": ""Rail"", ""railMountMode"": ""OverRail"", ""railPathDefinitionMode"": ""RailCenterLine"", + ""railNormalOffset"": 0.25, ""railPreferredNormal"": { ""x"": -0.2, ""y"": 0.5, ""z"": 0.8 }, ""totalLength"": 10.0, ""objectLimits"": { ""maxLength"": 0, ""maxWidth"": 0, ""maxHeight"": 0, ""safetyMargin"": 0 }, @@ -57,6 +58,7 @@ namespace NavisworksTransport.UnitTests.Core var importedRoutes = manager.ImportFromJson(filePath); Assert.AreEqual(1, importedRoutes.Count); Assert.IsNotNull(importedRoutes[0].RailPreferredNormal); + Assert.AreEqual(0.25, importedRoutes[0].RailNormalOffset, 1e-6); } finally { @@ -78,7 +80,7 @@ namespace NavisworksTransport.UnitTests.Core - + @@ -91,6 +93,7 @@ namespace NavisworksTransport.UnitTests.Core var importedRoutes = manager.ImportFromXml(filePath); Assert.AreEqual(1, importedRoutes.Count); Assert.IsNotNull(importedRoutes[0].RailPreferredNormal); + Assert.AreEqual(0.25, importedRoutes[0].RailNormalOffset, 1e-6); } finally { diff --git a/doc/design/2026/NavisworksAPI使用方法.md b/doc/design/2026/NavisworksAPI使用方法.md index e70b26d..8f79a8d 100644 --- a/doc/design/2026/NavisworksAPI使用方法.md +++ b/doc/design/2026/NavisworksAPI使用方法.md @@ -1,4 +1,4 @@ -# Navisworks API 使用方法指南 +# Navisworks API 使用方法指南 基于真实官方示例的正确API用法总结 @@ -505,686 +505,364 @@ public void BatchNavisworksOperations(List items) ## 11. Transform 变换操作 -### 11.1 Transform 相关 API 概念 +### 11.1 官方文档结论 -**核心概念**: +以下几条是本项目后续关于变换问题的硬基线,优先级高于历史经验和猜测: -- `ModelItem.Transform` - 返回设计文件中的原始变换,**只读属性**,**不反映override后的状态** -- `OverridePermanentTransform()` - 应用增量变换(相对于原始Transform累积) -- `ResetPermanentTransform()` - 清除所有增量变换,恢复到设计文件原始位置 -- `ModelItem.BoundingBox()` - 返回**当前实际显示**的包围盒(反映override效果) +- `ModelItem.Transform` + - 官方说明:`Returns the Transform attached to this item in the original source design file` + - 结论:它表示原始设计文件变换,不是当前显示姿态 + - 官方页:`doc/navisworks_api/NET/documentation/NetAPIHtml/html/P_Autodesk_Navisworks_Api_ModelItem_Transform.htm` +- `ModelGeometry.ActiveTransform` + - 官方说明:`Returns the currently active transform of the geometry.` + - 结论:它表示当前几何实际生效的变换 + - 官方页:`doc/navisworks_api/NET/documentation/NetAPIHtml/html/P_Autodesk_Navisworks_Api_ModelGeometry_ActiveTransform.htm` +- `DocumentModels.OverridePermanentTransform(...)` + - 官方说明:`Apply an incremental transform to a selection.` + - 结论:这是增量变换,不是把对象直接设成目标绝对姿态 + - 官方页:`doc/navisworks_api/NET/documentation/NetAPIHtml/html/M_Autodesk_Navisworks_Api_DocumentParts_DocumentModels_OverridePermanentTransform_3_131351c5.htm` +- `DocumentModels.ResetPermanentTransform(...)` + - 官方说明:`Reset incremental transforms for all model items contained in the selection.` + - 结论:它清掉的是永久增量层 + - 官方页:`doc/navisworks_api/NET/documentation/NetAPIHtml/html/M_Autodesk_Navisworks_Api_DocumentParts_DocumentModels_ResetPermanentTransform_1_75193b86.htm` +- `ModelGeometry` + - 官方成员表明确提供: + - `OriginalTransform` + - `PermanentOverrideTransform` + - `PermanentTransform` + - `ActiveTransform` + - 官方页:`doc/navisworks_api/NET/documentation/NetAPIHtml/html/AllMembers_T_Autodesk_Navisworks_Api_ModelGeometry.htm` -**⚠️ 关键理解**: +### 11.2 变换层级的正确理解 -1. **`ModelItem.Transform` 永远返回原始值**,即使通过 `OverridePermanentTransform` 改变了物体位置 -2. **Override 信息存储在别处**,不会修改 `ModelItem.Transform` 属性 -3. **要获取实际位置,使用 `BoundingBox().Center`**,它反映override后的实际位置 +以后项目里统一这样理解: -### 11.2 Transform 操作的正确用法 +- `ModelItem.Transform` + - 原始设计文件层变换 + - 只读 + - 不反映 `OverridePermanentTransform` 后的当前状态 +- `ModelItem.Geometry` / `ModelItem.FindFirstGeometry()` + - 进入几何层的正式入口 +- `ModelGeometry.OriginalTransform` + - 几何加载时的原始变换 +- `ModelGeometry.PermanentOverrideTransform` + - 施加在几何上的永久增量覆盖层 +- `ModelGeometry.PermanentTransform` + - 原始变换和永久覆盖组合后的结果 +- `ModelGeometry.ActiveTransform` + - 当前真正生效的几何变换 + - 一般应优先用它判断当前姿态/当前位置 + +### 11.3 项目中的推荐读取入口 + +#### 11.3.1 读“当前实际姿态” + +优先顺序: + +1. `ModelItem.FindFirstGeometry()` 或 `ModelItem.Geometry` +2. `ModelGeometry.ActiveTransform` +3. 必要时再看 `ModelGeometry.PermanentTransform` ```csharp -// ✅ 获取物体的原始Transform(设计文件中的位置) -Transform3D originalTransform = modelItem.Transform; +ModelGeometry geometry = item.FindFirstGeometry(); +if (geometry != null) +{ + Transform3D current = geometry.ActiveTransform; +} +``` -// ✅ 应用增量变换(累积变换) +#### 11.3.2 读“原始姿态” + +优先顺序: + +1. `ModelGeometry.OriginalTransform` +2. `ModelItem.Transform` + +```csharp +ModelGeometry geometry = item.FindFirstGeometry(); +Transform3D original = geometry != null ? geometry.OriginalTransform : item.Transform; +``` + +#### 11.3.3 读“当前实际位置” + +推荐顺序: + +1. 如果语义上需要几何真实中心,优先 `geometry.BoundingBox.Center` +2. 如果业务上有自己定义的 tracked point,就用业务 tracked point +3. 不要把 `ModelItem.Transform.Translation` 直接当成 override 后实际位置 + +### 11.4 `OverridePermanentTransform` 的正确语义 + +`OverridePermanentTransform(items, transform, updateModelTransform)` 的本质是: + +- 对选中的对象施加增量变换 +- 不是设置对象的最终世界姿态 + +另外官方 Remarks 里还有一条很重要: + +- 如果 selection 包含文件,而且 `updateModelTransform = true` +- 则不是对 fragments 施加变换 +- 而是更新 File Units and Transform + +这也是项目里必须区分的两种用法: + +- `updateModelTransform = false` + - 对 item/geometry 的永久增量层操作 +- `updateModelTransform = true` + - 更新 model/file 层的 units and transform + +### 11.5 `ResetPermanentTransform` 的正确语义 + +`ResetPermanentTransform(items)` 只做一件事: + +- 清除选中对象的永久增量变换 + +它不会修改: + +- 原始设计文件几何 +- `ModelItem.Transform` +- `ModelGeometry.OriginalTransform` + +所以“恢复到原始状态”要明确到底指哪一层: + +- 对真实物体: + - 通常是清掉 override,回到原始几何状态 +- 对虚拟物体: + - 如果业务尺寸是后续叠加出来的,单纯 reset 会把业务尺寸也一起清掉 + - 因此虚拟物体常常需要 reset 后再重放业务尺寸 + +### 11.6 常见误区 + +#### 11.6.1 误区:`ModelItem.Transform` 代表当前姿态 + +错误: + +```csharp +Transform3D current = item.Transform; // 这不是当前 override 后姿态 +``` + +正确: + +```csharp +ModelGeometry geometry = item.FindFirstGeometry(); +Transform3D current = geometry != null ? geometry.ActiveTransform : item.Transform; +``` + +#### 11.6.2 误区:`OverridePermanentTransform` 是绝对落位 + +错误理解: + +- “我把目标旋转/平移直接传进去,显示结果就应该等于它” + +正确理解: + +- 这是增量层 +- 最终显示结果要结合 `OriginalTransform / PermanentTransform / ActiveTransform` 一起看 + +#### 11.6.3 误区:只看 `Transform` 不看 `Geometry` + +对于很多真实物体和虚拟物体,真正决定当前显示姿态的是: + +- `ModelGeometry.ActiveTransform` +- 或 fragment 层矩阵 + +而不是: + +- `ModelItem.Transform` + +### 11.7 Fragment 与 Geometry 的关系 + +项目里对 fragment 的定位要统一: + +- fragment 适合做: + - 真实物体参考姿态解释 + - fragment 代表姿态统计 + - COM 层几何分析 +- fragment 不应优先替代 `ModelGeometry.ActiveTransform` 来读取“当前显示姿态” + +当前结论: + +- 读“当前实际姿态”优先用 `ModelGeometry` +- 读“真实物体参考姿态/原始语义姿态”时,fragment 仍然有用 + +### 11.8 COM Fragment 变换的使用规则 + +COM fragment 提供的是: + +- `GetLocalToWorldMatrix()` + - fragment 从本地到世界的完整矩阵 + +项目当前实测结论: + +- fragment 矩阵解释必须和 `ModelGeometry.ActiveTransform` 对齐验证 +- 不要只凭经验猜行列顺序 +- 当前项目排查中已经验证过: + - 解释 fragment 矩阵时,必须以 `.NET ModelGeometry` 的结果为标尺 + +### 11.9 推荐代码模式 + +#### 11.9.1 读取当前几何姿态 + +```csharp +public static bool TryGetCurrentGeometryTransform(ModelItem item, out Transform3D transform) +{ + transform = Transform3D.Identity; + if (item == null) + { + return false; + } + + ModelGeometry geometry = item.FindFirstGeometry(); + if (geometry == null) + { + return false; + } + + transform = geometry.ActiveTransform; + return true; +} +``` + +#### 11.9.2 清掉增量层后恢复到原始状态 + +```csharp var doc = Application.ActiveDocument; -var modelItems = new ModelItemCollection { modelItem }; -doc.Models.OverridePermanentTransform(modelItems, newTransform, false); - -// ✅ 重置到原始位置(清除所有增量变换) -doc.Models.ResetPermanentTransform(modelItems); +var items = new ModelItemCollection { item }; +doc.Models.ResetPermanentTransform(items); ``` -### 11.3 Transform 操作的关键区别 - -| API方法 | 作用 | 使用场景 | 注意事项 | -|---------|------|---------|---------| -| `ModelItem.Transform` | 获取原始变换 | 记录物体初始位置 | 只读属性,返回设计文件位置 | -| `OverridePermanentTransform()` | 应用增量变换 | 动画中移动物体 | 与现有变换累积,不是绝对位置 | -| `ResetPermanentTransform()` | 重置到原始位置 | 清除所有移动,恢复初始状态 | 忽略所有之前的变换 | - -### 11.4 实际应用案例 - -**案例1:动画系统中的Transform管理** +#### 11.9.3 对 item 施加增量变换 ```csharp -// 动画开始时记录原始位置 -private Transform3D _originalTransform; - -public void StartAnimation(ModelItem animatedObject) -{ - // 记录原始Transform - _originalTransform = animatedObject.Transform; - - // 移动到路径起点(增量变换) - var startTransform = Transform3D.CreateTranslation(startPosition); - var modelItems = new ModelItemCollection { animatedObject }; - doc.Models.OverridePermanentTransform(modelItems, startTransform, false); -} - -public void ResetAnimation() -{ - // 动画结束后,使用原始Transform恢复位置 - var modelItems = new ModelItemCollection { _animatedObject }; - doc.Models.OverridePermanentTransform(modelItems, _originalTransform, false); -} +var doc = Application.ActiveDocument; +var items = new ModelItemCollection { item }; +doc.Models.OverridePermanentTransform(items, incrementalTransform, false); ``` -**案例2:用户手动位置恢复** +#### 11.9.4 对 model 层更新 Units and Transform ```csharp -public void RestoreToOriginalPosition(ModelItem selectedObject) -{ - // 不需要记录Transform,直接重置到设计文件原始位置 - var doc = Application.ActiveDocument; - var modelItems = new ModelItemCollection { selectedObject }; - - // 清除所有增量变换,恢复到设计文件原始位置 - doc.Models.ResetPermanentTransform(modelItems); -} +Document doc = Application.ActiveDocument; +Model model = doc.Models[0]; +Transform3D oldTransform = model.Transform; +Transform3DComponents components = oldTransform.Factor(); + +components.Translation = new Vector3D(x, y, z); +components.Rotation = new Rotation3D(new UnitVector3D(0, 0, 1), angleInRadians); +components.Scale = new Vector3D(scaleX, scaleY, scaleZ); + +Transform3D newTransform = components.Combine(); +doc.Models.SetModelUnitsAndTransform(model, Units.Meters, newTransform, true); ``` -### 11.5 常见Transform问题和解决方案 +#### 11.9.5 矩阵语义与行列顺序速查 -**问题1:获取不到实际位置** +这是项目后续排查姿态问题时的硬基线。 + +##### A. `Transform3D.Linear` / `Matrix3` 的项目语义 + +项目当前统一按下面这套语义理解: + +- `Transform3D.Linear` 是 3x3 线性部分 +- `Matrix3.Get(row, column)` 的参数顺序是: + - 先 `row` + - 后 `column` +- 在项目里,`Linear` 的 **列** 表示“局部轴在世界中的方向” + - 第 1 列 = 本地 `X` 轴在世界中的方向 + - 第 2 列 = 本地 `Y` 轴在世界中的方向 + - 第 3 列 = 本地 `Z` 轴在世界中的方向 + +也就是说: ```csharp -// ❌ 错误:以为 Transform 反映当前位置 -var transform = item.Transform; -// 问题:这永远返回原始Transform,即使物体已被移动 +Vector3D worldX = new Vector3D( + linear.Get(0, 0), + linear.Get(1, 0), + linear.Get(2, 0)); -// ✅ 正确:使用 BoundingBox 获取实际位置 -var actualCenter = item.BoundingBox().Center; // 反映override后的实际位置 +Vector3D worldY = new Vector3D( + linear.Get(0, 1), + linear.Get(1, 1), + linear.Get(2, 1)); + +Vector3D worldZ = new Vector3D( + linear.Get(0, 2), + linear.Get(1, 2), + linear.Get(2, 2)); ``` -**问题2:动画结束后位置不准确** +##### B. `Matrix3` 构造时的顺序 + +项目里当前按“逐行传参”构造 `Matrix3`: ```csharp -// ✅ 动画系统应该记录原始Transform并使用增量恢复 -private Transform3D _originalTransform; - -// 动画开始时 -_originalTransform = animatedObject.Transform; - -// 动画结束时恢复 -doc.Models.OverridePermanentTransform(modelItems, _originalTransform, false); +var linear = new Matrix3( + m00, m01, m02, + m10, m11, m12, + m20, m21, m22); ``` -**问题3:记录Transform但不使用** +其中: + +- 第 1 行 = `(m00, m01, m02)` +- 第 2 行 = `(m10, m11, m12)` +- 第 3 行 = `(m20, m21, m22)` + +如果你的业务语义是“列 = 局部轴”,那么组矩阵时应写成: ```csharp -// ❌ 不必要:记录Transform但使用Reset -private Transform3D _originalTransform; -_originalTransform = selectedItem.Transform; // 记录了但不使用 -doc.Models.ResetPermanentTransform(modelItems); // 直接重置 - -// ✅ 简化:直接重置,无需记录 -doc.Models.ResetPermanentTransform(modelItems); +linear = new Matrix3( + worldX.X, worldY.X, worldZ.X, + worldX.Y, worldY.Y, worldZ.Y, + worldX.Z, worldY.Z, worldZ.Z); ``` -### 11.6 Transform 最佳实践 +##### C. `System.Numerics.Matrix4x4` 的项目用法 -1. **选择合适的恢复方式**: - - 动画系统:使用 `OverridePermanentTransform` + 原始Transform - - 用户操作:使用 `ResetPermanentTransform` 直接重置 - -2. **避免不必要的Transform记录**: - - 如果只需要恢复到设计文件原始位置,使用 `ResetPermanentTransform` - - 只有需要恢复到特定中间状态时才记录Transform - -3. **理解增量vs绝对变换**: - - `OverridePermanentTransform` 是增量的,会与现有变换叠加 - - `ResetPermanentTransform` 是绝对的,清除所有变换 - -4. **线程安全**: - - 所有Transform操作都必须在主UI线程中执行 - - 使用 `Dispatcher.Invoke` 确保线程安全 - -### 11.7 旋转操作的关键限制和解决方案 ⚠️ 重要 - -基于实际测试验证的关键发现(2025-12-15)。 - -#### 11.7.1 旋转中心的API限制 - -**⚠️ 核心限制:Navisworks API的旋转总是绕世界原点(0,0,0)进行** +项目里把 `Matrix4x4` 也按同样的“列 = 基向量”语义使用: ```csharp -// ❌ 错误理解:以为旋转绕物体中心 -var rotation = new Transform3D(new Rotation3D(new UnitVector3D(0, 0, 1), angle)); -doc.Models.OverridePermanentTransform(modelItems, rotation, false); -// 实际效果:物体绕世界原点(0,0,0)"公转",不是绕自己"自转" - -// 🔍 实际测试验证: -// 物体在 (-2.499, -1.640, 0.500) 位置 -// 旋转45度后移动到 (-0.608, -2.927, 0.500) -// 验证公式:x' = x*cos(45°) - y*sin(45°) = -0.607 ✓ -// y' = x*sin(45°) + y*cos(45°) = -2.927 ✓ -// 证明:旋转中心是世界原点(0,0,0),不是物体中心 +Matrix4x4 basis = new Matrix4x4( + worldX.X, worldY.X, worldZ.X, 0f, + worldX.Y, worldY.Y, worldZ.Y, 0f, + worldX.Z, worldY.Z, worldZ.Z, 0f, + 0f, 0f, 0f, 1f); ``` -**验证代码**: +在这套写法下: + +- 第 1 列是局部 `X` 轴 +- 第 2 列是局部 `Y` 轴 +- 第 3 列是局部 `Z` 轴 + +然后再用: ```csharp -// ✅ 测试代码:证明旋转绕世界原点 -var initialCenter = item.BoundingBox().Center; // (-2.499, -1.640, 0.500) - -// 应用45度旋转 -var rotation = new Transform3D(new Rotation3D(new UnitVector3D(0, 0, 1), Math.PI/4)); -doc.Models.OverridePermanentTransform(modelItems, rotation, false); - -var afterCenter = item.BoundingBox().Center; // (-0.608, -2.927, 0.500) - -// 计算期望位置(绕原点旋转) -double cos45 = Math.Cos(Math.PI/4); -double sin45 = Math.Sin(Math.PI/4); -double expectedX = initialCenter.X * cos45 - initialCenter.Y * sin45; // -0.607 -double expectedY = initialCenter.X * sin45 + initialCenter.Y * cos45; // -2.927 - -// 验证:实际位置 = 期望位置(绕原点旋转)✓ +Quaternion q = Quaternion.CreateFromRotationMatrix(basis); ``` -#### 11.7.2 Transform3DComponents 的行为 +##### D. 最容易犯错的地方 -**关键理解:`Transform3DComponents.Combine()` 的变换顺序** +1. 把“列是局部轴”误看成“行是局部轴” +2. 读取 `linear.Get(0, 1)` 时,以为拿到的是 `Y.X`,但后续又按行语义消费 +3. 把 `Matrix3` 直接喂给手写 quaternion 公式时,没有先确认公式使用的是“行主序矩阵”还是“列向量基矩阵” +4. 日志里看到 `X=(...), Y=(...), Z=(...)` 时,没有先确认它打印的是“列”还是“行” -```csharp -// Transform3DComponents.Combine() 应用顺序: -// 1. Scale(缩放) -// 2. Rotation(旋转,绕原点) -// 3. Translation(平移) +##### E. 项目中的统一建议 -// ❌ 错误:直接设置rotation和translation -var components = identity.Factor(); -components.Rotation = new Rotation3D(new UnitVector3D(0, 0, 1), deltaYaw); -components.Translation = deltaPos; // 平移在旋转之后应用 -var transform = components.Combine(); +1. 只要是在解释物体三轴,就统一按“列 = 局部轴在世界中的方向” +2. `Matrix3 -> Quaternion` 优先使用 `Matrix4x4 + Quaternion.CreateFromRotationMatrix(...)` +3. 不要在不同文件里混用两套相反的行列语义 +4. 新增日志时,明确写出“这里打印的是列向量/局部轴”,不要只写 `X/Y/Z` -// 问题:物体先绕原点旋转(产生位置偏移),然后平移 -// 结果:物体"公转"到错误位置 -``` - -#### 11.7.2.1 Rotation3D 构造函数的真实语义(2026-03-19 新增,重要) - -这次在“直线装配参考杆”功能中,验证了一个很容易误用的点: - -- `Rotation3D(UnitVector3D, UnitVector3D, UnitVector3D)` **不是**“传入本地 X/Y/Z 三个轴来直接构造三维姿态” -- 之前如果把它当成“三轴姿态构造器”使用,会得到完全错误的旋转结果 - -根据 Navisworks .NET API HTML 文档: - -- `Rotation3D(UnitVector3D vector1, UnitVector3D vector2)` - - 文档摘要:`Creates rotation that rotates vector1 to same direction as vector2` - - 正确语义:**将向量1旋转到与向量2同方向** -- `Rotation3D(UnitVector3D axis, double angle)` - - 文档摘要:`Creates rotation about given axis by angle in radians` - - 正确语义:**绕指定轴按弧度旋转** -- `Rotation3D(UnitVector3D axis, UnitVector3D vector1, UnitVector3D vector2)` - - 文档摘要:`Creates rotation about axis by angle between v1 and v2 projected onto plane normal to axis.` - - 正确语义:**绕 axis 旋转,旋转角由 vector1 和 vector2 在垂直于 axis 的平面上的投影夹角决定** - -**结论:** - -- 3 参数版本是“轴 + 两个参考向量”的构造,不是“三个基向量/三个坐标轴” -- 如果需求是“让物体本地某个轴对齐任意空间方向”,优先使用 **2 参数版本** - -#### 11.7.2.1.1 Rotation3D 四元数分量顺序的实测结论(2026-03-20 新增,重要) - -在 Rail 路径三维姿态调试中,对 `Rotation3D(Double, Double, Double, Double)` 和 `Rotation3D.A/B/C/D` 做了直接实测。 - -**结论:** - -- `Rotation3D(a, b, c, d)` 的 4 个参数顺序是:**`x, y, z, w`** -- 对应属性: - - `A = quaternion.x` - - `B = quaternion.y` - - `C = quaternion.z` - - `D = quaternion.w` - -实测验证方式: - -```csharp -var rx = new Rotation3D(new UnitVector3D(1, 0, 0), Math.PI / 2.0); -var ry = new Rotation3D(new UnitVector3D(0, 1, 0), Math.PI / 2.0); -var rz = new Rotation3D(new UnitVector3D(0, 0, 1), Math.PI / 2.0); -``` - -日志结果: - -- `X90`: `A=0.707107, B=0.000000, C=0.000000, D=0.707107` -- `Y90`: `A=0.000000, B=0.707107, C=0.000000, D=0.707107` -- `Z90`: `A=0.000000, B=0.000000, C=0.707107, D=0.707107` - -并且将 `A/B/C/D` 原样重新传回: - -```csharp -var reconstructed = new Rotation3D(rotation.A, rotation.B, rotation.C, rotation.D); -var linear = new Transform3D(reconstructed).Linear; -``` - -读回的线性矩阵与对应的 `X90 / Y90 / Z90` 旋转完全一致。 - -**因此:** - -- 当你已经算出了目标姿态的 quaternion `(qx, qy, qz, qw)` 时,正确写法是: - -```csharp -var rotation = new Rotation3D(qx, qy, qz, qw); -``` - -- **不要**写成: - -```csharp -var rotation = new Rotation3D(qw, qx, qy, qz); // ❌ 错误 -``` - -这个结论已经在 Rail 三维姿态、参考杆三维定向的实际运行中验证通过。 - -**项目级硬约束:** - -- 后续任何代码和文档中,都不允许再重新解释 `Rotation3D` 的四元数分量顺序 -- 遇到姿态问题时,不要再把问题归因到 `Rotation3D(x,y,z,w)` 是否可能变成了别的顺序 -- 如果真实物体或 fragment 参考姿态出现轴翻转/符号错误,应优先检查: - - fragment 三轴的业务语义解释 - - 宿主坐标系 / 内部坐标系 / 资产坐标系 的边界 - - 是否应该传递显式参考轴,而不是只传 quaternion - -#### 11.7.2.2 三维参考杆/任意向量对齐的正确用法 - -适用场景: - -- 参考杆、箭头、圆柱体等“有明确本地长度轴”的资源 -- 需要将资源的本地某个轴,对齐到一个任意空间向量 - -例如: - -- 资源中心在原点 -- 长轴沿本地 `+X` -- 目标方向是参考线方向 `referenceDirection` - -那么旋转应直接写成: - -```csharp -var rotation = new Rotation3D( - new UnitVector3D(1, 0, 0), - new UnitVector3D(referenceDirection)); -``` - -其含义非常直接: - -- 把资源的本地 `+X` 轴 -- 旋转到 `referenceDirection` - -这正是“直线装配参考杆”最终验证通过的做法。 - -**不要写成下面这样:** - -```csharp -// ❌ 错误:把三个向量误当成 X/Y/Z 三个姿态轴 -var rotation = new Rotation3D( - new UnitVector3D(tangent), - new UnitVector3D(lateral), - new UnitVector3D(normal)); -``` - -这段代码的真实语义并不是“建立一个由 tangent/lateral/normal 组成的姿态”, -而是“绕 tangent 这个轴,根据 lateral 和 normal 的投影夹角去旋转”, -所以结果通常会完全错误。 - -#### 11.7.2.3 三维旋转与平移补偿必须分开理解 - -即使 `Rotation3D(vector1, vector2)` 构造正确,仍然不能忽略 Navisworks 的旋转中心限制: - -- `OverridePermanentTransform()` 仍然是**增量变换** -- 旋转仍然是**绕世界原点** -- 因此三维姿态正确后,位置仍需要像 yaw 场景一样做补偿 - -对于“资源中心在原点、目标中心为 `targetCenter`”的对象,推荐模式是: - -```csharp -// 1. 从资源原始状态出发 -doc.Models.ResetPermanentTransform(modelItems); - -// 2. 计算旋转:本地 X -> 目标方向 -var rotation = new Rotation3D( - new UnitVector3D(1, 0, 0), - new UnitVector3D(referenceDirection)); - -// 3. 计算原始中心绕世界原点旋转后的位置 -var originalCenter = item.BoundingBox().Center; -var rotationTransform = new Transform3D(rotation); -var linear = rotationTransform.Linear; -var rotatedCenter = new Point3D( - linear.Get(0, 0) * originalCenter.X + linear.Get(0, 1) * originalCenter.Y + linear.Get(0, 2) * originalCenter.Z, - linear.Get(1, 0) * originalCenter.X + linear.Get(1, 1) * originalCenter.Y + linear.Get(1, 2) * originalCenter.Z, - linear.Get(2, 0) * originalCenter.X + linear.Get(2, 1) * originalCenter.Y + linear.Get(2, 2) * originalCenter.Z); - -// 4. 用补偿平移把旋转后的中心送到目标中心 -var translation = new Vector3D( - targetCenter.X - rotatedCenter.X, - targetCenter.Y - rotatedCenter.Y, - targetCenter.Z - rotatedCenter.Z); - -// 5. 组合并应用增量变换 -var transform = new Transform3D(rotation, translation); -doc.Models.OverridePermanentTransform(modelItems, transform, false); -``` - -**关键原则:** - -- 先把“旋转方向”算对 -- 再按“绕原点旋转后的实际位置”计算平移补偿 -- 不要把“姿态构造错误”和“旋转中心补偿缺失”混成一个问题 - -#### 11.7.2.4 这类问题的调试建议 - -当三维物体“位置大概对,但方向明显错”时,优先检查: - -1. 是否误用了 `Rotation3D(UnitVector3D, UnitVector3D, UnitVector3D)` -2. 资源文件的本地长度轴是否真的与你的代码约定一致 -3. 是否把 `ModelItem.Transform` 错当成 override 后的真实世界姿态 - -推荐观察项: - -- 参考线/目标向量本身是否正确 -- 资源本地长度轴约定(例如是否沿本地 `+X`) -- 当前实际显示位置,优先看 `BoundingBox()`,不要只看 `ModelItem.Transform` - -#### 11.7.2.5 真实模型做三维旋转时,推荐使用“先移到原点,再旋转,再移到目标点”(2026-03-20 新增,重要) - -在 Rail 路径真实模型调试中,一个非常关键的经验是: - -- **不要**只在公式层面“猜一个补偿平移” -- 对真实模型做完整三维姿态时,更稳的做法是显式拆成三步: - -```csharp -// 1. 把当前参考点移到原点 -doc.Models.OverridePermanentTransform( - modelItems, - Transform3D.CreateTranslation(new Vector3D(-currentPoint.X, -currentPoint.Y, -currentPoint.Z)), - false); - -// 2. 在原点执行三维旋转 -var rotationComponents = Transform3D.CreateTranslation(new Vector3D(0, 0, 0)).Factor(); -rotationComponents.Rotation = deltaRotation; -doc.Models.OverridePermanentTransform(modelItems, rotationComponents.Combine(), false); - -// 3. 再把参考点移到目标位置 -doc.Models.OverridePermanentTransform( - modelItems, - Transform3D.CreateTranslation(new Vector3D(targetPoint.X, targetPoint.Y, targetPoint.Z)), - false); -``` - -**适用场景:** - -- 真实模型不是“原点资源”,本身已经位于世界坐标某个远离原点的位置 -- 需要做完整三维旋转(不只是 yaw) -- 目标是让物体绕某个固定参考点“自转”,而不是带着原始世界位置绕原点“公转” - -**核心理解:** - -- Navisworks 的旋转仍然是绕世界原点 -- 所以如果想绕“某个固定参考点”旋转,就先把这个参考点移到原点 -- 这个固定参考点不一定必须是中心,也可以是底面中心、安装点或其他局部固定点 -- 但在复杂三维姿态下,**几何中心通常是最稳定、最容易验证的参考点** - -**这条原则解决的问题:** - -- 虚拟物体因为本来就接近“原点资源”,旋转通常更容易成功 -- 真实模型自带原始世界位置时,如果直接旋转,很容易出现“方向对了,但位置跑很远” -- 将参考点显式移到原点后再旋转,可以显著降低三维平移补偿出错的概率 - -#### 11.7.3 正确实现"绕物体中心旋转" - -**解决方案:手动计算旋转导致的位置偏移并补偿** - -```csharp -// ✅ 正确方法:计算补偿平移量 -private void UpdateObjectPosition(Point3D newPosition, double newYaw) -{ - var doc = Application.ActiveDocument; - var modelItems = new ModelItemCollection { _animatedObject }; - - // 计算旋转和平移增量 - var deltaPos = new Vector3D( - newPosition.X - _currentPosition.X, - newPosition.Y - _currentPosition.Y, - newPosition.Z - _currentPosition.Z - ); - - Transform3D incrementalTransform; - - if (!double.IsNaN(newYaw)) - { - double deltaYaw = newYaw - _currentYaw; - - // 🎯 关键:计算绕当前位置旋转的等效变换 - // 1. 如果绕原点旋转deltaYaw,当前位置会移到哪里? - double cos = Math.Cos(deltaYaw); - double sin = Math.Sin(deltaYaw); - double rotatedX = _currentPosition.X * cos - _currentPosition.Y * sin; - double rotatedY = _currentPosition.X * sin + _currentPosition.Y * cos; - - // 2. 我们希望物体绕自己旋转,位置移动到newPosition - // 所以需要的平移 = newPosition - (旋转后的位置) - var compensatedTranslation = new Vector3D( - newPosition.X - rotatedX, // 补偿X方向的偏移 - newPosition.Y - rotatedY, // 补偿Y方向的偏移 - newPosition.Z - _currentPosition.Z // Z保持增量 - ); - - // 3. 组合:先旋转(绕原点),再平移(补偿+目标位置) - var identity = Transform3D.CreateTranslation(new Vector3D(0, 0, 0)); - var components = identity.Factor(); - components.Rotation = new Rotation3D(new UnitVector3D(0, 0, 1), deltaYaw); - components.Translation = compensatedTranslation; // 关键:使用补偿后的平移 - - incrementalTransform = components.Combine(); - _currentYaw = newYaw; - } - else - { - // 纯平移:直接使用增量 - incrementalTransform = Transform3D.CreateTranslation(deltaPos); - } - - // 应用增量变换 - doc.Models.OverridePermanentTransform(modelItems, incrementalTransform, false); - _currentPosition = newPosition; -} -``` - -**原理说明**: - -``` -API限制: - 旋转 → 物体绕(0,0,0)旋转 → 位置从P1偏移到P2 - -我们需要的效果: - 旋转 → 物体绕自己旋转 → 位置从P1移动到P_target - -解决方案: - 补偿平移 = P_target - P2 - 最终变换 = Rotation(deltaYaw) + Translation(P_target - P2) - -结果: - 物体先绕原点旋转到P2,然后平移到P_target - 看起来像是绕自己旋转并移动到目标位置 -``` - -#### 11.7.4 初始化问题 - -**⚠️ 重要:初始化yaw必须与第一帧匹配** - -```csharp -// ❌ 错误:初始化为0 -_currentYaw = 0.0; -// 第一帧调用UpdateObjectPosition时: -// deltaYaw = firstFrame.YawRadians - 0.0 // 产生大的旋转增量 -// 导致物体从起点"公转"飞走 - -// ✅ 正确:初始化为第一帧的yaw -if (_animationFrames != null && _animationFrames.Count > 0) -{ - _currentYaw = _animationFrames[0].YawRadians; // 使deltaYaw=0 - - // 第一次调用UpdateObjectPosition - var firstFrame = _animationFrames[0]; - UpdateObjectPosition(firstFrame.Position, firstFrame.YawRadians); - // 此时:deltaYaw = firstFrame.YawRadians - firstFrame.YawRadians = 0 - // 结果:只有平移,没有旋转偏移 -} -``` - -#### 11.7.5 相关API限制说明 - -Autodesk官方论坛已确认的限制(Issue NW-53280): - -- **无法设置旋转中心点**:API不提供指定旋转中心的方法 -- **UI的Override Transform功能**:也是通过计算补偿实现的 -- **建议的解决方案**:手动计算T(center) × R × T(-center)的等效变换 - -#### 11.7.6 旋转操作最佳实践 - -| 场景 | 方法 | 注意事项 | -|------|------|---------| -| 简单旋转(原地) | 使用位置补偿公式 | 必须计算旋转导致的偏移 | -| 旋转+移动 | 组合补偿平移和目标平移 | 理解Combine()的变换顺序 | -| 动画初始化 | `_currentYaw = firstFrame.YawRadians` | 避免第一帧产生旋转增量 | -| 调试验证 | 测试物体远离原点的情况 | 原点附近可能掩盖问题 | - -#### 11.7.7 关键原则:移动物体前必须先重置到CAD位置 ⚠️ 重要 - -**问题场景**: -当物体已经被移动过(如动画结束在终点位置),再次移动时如果直接从当前位置计算增量,会导致错误的结果。 - -**原因**: -`OverridePermanentTransform` 的增量是相对于**CAD原始位置**的,不是相对于当前位置。 - -**❌ 错误做法**: -```csharp -// 物体当前在终点位置,但我们要移动到另一个位置 -var currentPos = item.BoundingBox().Center; // 终点位置 -var deltaPos = new Vector3D( - targetPos.X - currentPos.X, // 从终点计算增量 - 错误! - targetPos.Y - currentPos.Y, - targetPos.Z - currentPos.Z -); -var transform = Transform3D.CreateTranslation(deltaPos); -doc.Models.OverridePermanentTransform(modelItems, transform, false); -// 结果:物体会移动到错误位置(因为增量是相对于CAD位置的) -``` - -**✅ 正确做法**: -```csharp -// 1. 先重置到CAD原始位置 -doc.Models.ResetPermanentTransform(modelItems); - -// 2. 从CAD原始位置计算到目标位置的增量 -var originalBounds = item.BoundingBox(); -var originalPos = new Point3D( - originalBounds.Center.X, - originalBounds.Center.Y, - originalBounds.Min.Z -); -var deltaPos = new Vector3D( - targetPos.X - originalPos.X, // 从CAD位置计算增量 - 正确! - targetPos.Y - originalPos.Y, - targetPos.Z - originalPos.Z -); - -// 3. 应用变换 -var transform = Transform3D.CreateTranslation(deltaPos); -doc.Models.OverridePermanentTransform(modelItems, transform, false); -``` - -**使用场景**: -- 碰撞报告还原物体到碰撞位置 -- 手动指定物体位置 -- 任何需要精确控制物体最终位置的操作 - -**最佳实践**: -```csharp -/// -/// 将物体移动到指定位置和朝向(先回到CAD原始位置) -/// -public static void MoveItemToPositionAndYaw(ModelItem item, Point3D targetPosition, double targetYaw) -{ - var doc = Application.ActiveDocument; - var modelItems = new ModelItemCollection { item }; - - // 🔥 关键:先回到CAD原始位置 - doc.Models.ResetPermanentTransform(modelItems); - - // 获取CAD原始状态 - var originalBounds = item.BoundingBox(); - var originalGroundPos = new Point3D( - originalBounds.Center.X, - originalBounds.Center.Y, - originalBounds.Min.Z - ); - var originalYaw = GetYawFromTransform(item.Transform); - - // 计算从CAD位置到目标位置的增量 - var deltaPos = new Vector3D( - targetPosition.X - originalGroundPos.X, - targetPosition.Y - originalGroundPos.Y, - targetPosition.Z - originalGroundPos.Z - ); - double deltaYaw = targetYaw - originalYaw; - - // 应用增量变换(包含旋转补偿) - Transform3D transform; - if (Math.Abs(deltaYaw) > 0.001) - { - // 计算旋转补偿 - double cos = Math.Cos(deltaYaw); - double sin = Math.Sin(deltaYaw); - double rotatedX = originalGroundPos.X * cos - originalGroundPos.Y * sin; - double rotatedY = originalGroundPos.X * sin + originalGroundPos.Y * cos; - - var compensatedTranslation = new Vector3D( - targetPosition.X - rotatedX, - targetPosition.Y - rotatedY, - deltaPos.Z - ); - - var identity = Transform3D.CreateTranslation(new Vector3D(0, 0, 0)); - var components = identity.Factor(); - components.Rotation = new Rotation3D(new UnitVector3D(0, 0, 1), deltaYaw); - components.Translation = compensatedTranslation; - transform = components.Combine(); - } - else - { - transform = Transform3D.CreateTranslation(deltaPos); - } - - doc.Models.OverridePermanentTransform(modelItems, transform, false); -} -``` - -**调试技巧**: - -```csharp -// ✅ 测试旋转中心的方法 -// 1. 将物体移动到远离原点的位置(如(-5, -5, 0)) -// 2. 应用旋转 -// 3. 检查物体是否"公转"(位置大幅移动)还是"自转"(位置基本不变) -// 4. 如果发现"公转",说明没有正确补偿 - -// ✅ 验证补偿计算的公式 -double expectedX_afterRotation = currentX * cos(angle) - currentY * sin(angle); -double expectedY_afterRotation = currentX * sin(angle) + currentY * cos(angle); -var compensationX = targetX - expectedX_afterRotation; -var compensationY = targetY - expectedY_afterRotation; - -LogManager.Debug($"旋转前: ({currentX}, {currentY})"); -LogManager.Debug($"绕原点旋转后: ({expectedX_afterRotation}, {expectedY_afterRotation})"); -LogManager.Debug($"目标位置: ({targetX}, {targetY})"); -LogManager.Debug($"需要补偿: ({compensationX}, {compensationY})"); -``` +### 11.10 项目级硬约束 +1. 读当前姿态时,不要再默认用 `ModelItem.Transform` +2. 真实物体参考姿态和当前姿态是两回事,不要混 +3. `OverridePermanentTransform` 是增量,不是绝对落位 +4. `ResetPermanentTransform` 清的是增量层,不是原始几何 +5. 涉及当前显示姿态时,优先看 `ModelGeometry.ActiveTransform` +6. 涉及 fragment 矩阵解释时,先与 `ModelGeometry.ActiveTransform` 对齐验证 ## 12. Item属性和自定义属性访问 基于官方示例的正确属性访问方法总结。 diff --git a/doc/working/current-engineering-state.md b/doc/working/current-engineering-state.md index 3e99d9e..44e23ff 100644 --- a/doc/working/current-engineering-state.md +++ b/doc/working/current-engineering-state.md @@ -9,13 +9,27 @@ - `YUp` 模型下,终端安装仿真、`Rail` 姿态、真实物体与虚拟物体通行空间、起点与动画主链路已基本跑通。 - 地面路径在 `YUp` 模型下: - 虚拟物体起点姿态、动画姿态、转弯姿态已恢复正常。 - - 当前已禁止地面/吊装路径偷偷退回旧 `yaw` 链路。 + - 真实物体地面路径当前已恢复到可用状态: + - 起点落位补偿已接入; + - 逐帧补偿会随当前目标姿态一起旋转,不再使用固定世界补偿矢量; + - 动画结束后不再因恢复链或重复落最后一帧而再次跳动。 + - `Ground + 真实物体` 的逐帧姿态约束已经明确: + - 动画播放阶段只允许绕宿主 `up` 轴转动; + - 不再在播放阶段每帧重建完整三维姿态。 + - `Ground/Hoisting + 真实物体` 的前进轴语义当前已固定: + - 对象级前进轴统一按 `PositiveX` 解释; + - 不再因为路径初始方向更偏 `Z` 就把对象前进轴自动切换成 `PositiveZ`。 +- 当前已禁止地面/吊装路径偷偷退回旧 `yaw` 链路。 - 碰撞检测/恢复主链路已稳定: - `ClashDetective` 三维恢复不能再先 `ResetPermanentTransform`。 - 碰撞恢复、自动报告、自动截图已重新对齐到动画主链路。 - 虚拟物体资源问题已确认并修复: - 旧 `unit_cube.nwc` 局部几何中心不在原点,会导致虚拟物体中心偏差。 - 新 `unit_cube.nwc` 已替换为几何中心在原点的版本。 + - 旋转适配入口当前稳定分流: + - 虚拟物体继续走 `HostCoordinateAdapter` 的 `Legacy` 入口; + - 真实物体走 `Direct` 入口; + - 两者不能再强行共用同一条旋转转换链。 ## 2. 当前坐标系架构 @@ -63,6 +77,10 @@ - 当前规则: - 先解释真实物体参考姿态,再做路径对齐 - 不能跳过这一步,直接拿 fragment 世界轴去猜业务姿态 + - 当前补充规则: + - `Ground/Hoisting` 的真实物体“对象前进轴”与 fragment 参考姿态解释是两层语义; + - fragment 负责解释真实参考姿态; + - 对象前进轴当前在 `Ground/Hoisting` 上固定按 `PositiveX` 语义使用,不再做“按路径方向选最近轴”的自动切换。 ### 4.1 Rotation / 矩阵语义 @@ -102,6 +120,15 @@ - 目前稳定可用的是:`Y` 轴转动。 - `X / Z` 轴在起点静态预览或直线段里可能看起来合理,但路径一旦拐弯,逐帧按宿主世界轴重算会让“俯仰/侧倾”语义发生耦合,表现成不符合现场直觉的侧旋。 - 因此当前阶段,对地面路径应按“只稳定支持 `Y` 轴转动”使用;`X / Z` 轴播放链问题留待后续专项修复。 +- 当前新增稳定规则(2026-03-25): + - `Ground + 真实物体` 的播放阶段不能再每帧应用完整三维姿态; + - 必须以起点基姿态为基线,只叠加宿主 `up` 轴上的单轴旋转。 +- 当前新增稳定规则(2026-03-25): + - `Ground + 真实物体` 的起点目标点语义仍然是路径跟踪点,不允许修改路径点本身; + - 如真实物体起点落位存在固定偏差,应把补偿施加到物体位置,而不是回写路径点或路径跟踪点语义。 +- 当前新增稳定规则(2026-03-25): + - `Ground + 真实物体` 的补偿不是固定世界矢量; + - 起点测得的偏差必须随当前目标姿态一起旋转后,再参与逐帧位置应用。 - `YUp` 吊装路径创建主链路已补齐到宿主坐标适配架构: - 提升、水平移动、下降、终点落地都不能再把世界 `Z` 硬编码成“向上”。 - 终点必须使用用户最后一次点击的地面点,不能回填起点地面高程。 @@ -223,10 +250,35 @@ - 保存/恢复姿态 - 关键碰撞恢复 - 虚拟物体应用后中心/偏差 + - 真实物体地面路径: + - `[路径起点诊断]` + - `[路径起点补偿]` + - `[Ground路径补偿]` - 已降级或删除: - 大量重复逐帧宿主姿态轴日志 - 虚拟物体 `Transform` 即时读回日志(容易误导) +## 6. 当前 Navisworks 变换 API 结论 + +- `ModelItem.Transform` + - 只表示原始设计文件变换; + - 不反映 `OverridePermanentTransform` 后的当前显示姿态。 +- `ModelGeometry.ActiveTransform` + - 是当前几何实际生效的变换; + - 当前项目里,凡是要读“当前姿态/当前位置”,优先使用这一层。 +- `ModelGeometry` + - 关键四层语义: + - `OriginalTransform` + - `PermanentOverrideTransform` + - `PermanentTransform` + - `ActiveTransform` +- `OverridePermanentTransform(...)` + - 官方语义是增量变换; + - 不是“直接设成最终世界姿态”。 +- `ResetPermanentTransform(...)` + - 清掉的是永久增量层; + - 不会修改原始设计文件变换。 + ## 6. 当前还值得继续观察的点 - 空轨路径里仍有一个旧 warning: diff --git a/src/Core/Animation/PathAnimationManager.cs b/src/Core/Animation/PathAnimationManager.cs index 2190688..9af4ca4 100644 --- a/src/Core/Animation/PathAnimationManager.cs +++ b/src/Core/Animation/PathAnimationManager.cs @@ -205,6 +205,9 @@ namespace NavisworksTransport.Core.Animation private Rotation3D _savedObjectRotation = Rotation3D.Identity; private bool _savedObjectHasCustomRotation = false; private bool _hasSavedObjectState = false; + private ObjectStartPlacementMode _objectStartPlacementMode = ObjectStartPlacementMode.AlignToPathPose; + private Rotation3D _railPreservedPoseRotation = Rotation3D.Identity; + private bool _hasRailPreservedPoseRotation = false; private bool IsVirtualObjectMode => _animatedObjectMode == AnimatedObjectMode.VirtualObject; private bool IsRealObjectMode => _animatedObjectMode == AnimatedObjectMode.RealObject; @@ -637,6 +640,8 @@ namespace NavisworksTransport.Core.Animation /// 是否成功移动 public bool MoveObjectToPathStart(ModelItem animatedObject = null, List pathPoints = null) { + _objectStartPlacementMode = ObjectStartPlacementMode.AlignToPathPose; + // 前置检查:必须有路径 if (_route == null) { @@ -859,6 +864,97 @@ namespace NavisworksTransport.Core.Animation } } + public bool MoveObjectToPathStartPreservingInitialPose(ModelItem animatedObject = null, List pathPoints = null) + { + _objectStartPlacementMode = ObjectStartPlacementMode.PreserveInitialPose; + + if (_route == null) + { + LogManager.Error("[平移到起点] 路径为空,无法移动物体到路径起点"); + return false; + } + + try + { + if (CurrentControlledObject != null || IsVirtualObjectMode) + { + RestoreObjectToCADPosition(); + LogManager.Info($"[平移到起点] 已恢复物体到原始状态, _currentYaw={_currentYaw * 180 / Math.PI:F2}°"); + } + + if (animatedObject != null) + { + _animatedObject = animatedObject; + bool isVirtualObject = + VirtualObjectManager.Instance.IsVirtualObjectActive && + ReferenceEquals(animatedObject, VirtualObjectManager.Instance.CurrentVirtualObject); + _animatedObjectMode = isVirtualObject + ? AnimatedObjectMode.VirtualObject + : AnimatedObjectMode.RealObject; + ResetRealObjectReferenceRotation(); + _originalTransform = animatedObject.Transform; + _originalCenter = animatedObject.BoundingBox().Center; + _trackedPosition = GetTrackedObjectPosition(animatedObject); + if (isVirtualObject) + { + _currentYaw = ModelItemTransformHelper.GetYawFromTransform(_originalTransform); + _trackedRotation = _originalTransform.Factor().Rotation; + _hasTrackedRotation = true; + } + else + { + SyncTrackedRotationToObjectReference(animatedObject, isVirtualObject: false); + } + } + + if (pathPoints != null) + { + _pathPoints = pathPoints; + } + + if (_pathPoints == null || _pathPoints.Count < 2) + { + LogManager.Warning("[平移到起点] 没有可用的路径点"); + return false; + } + + Point3D pathStartPoint = _pathPoints[0]; + Point3D startPosition = pathStartPoint; + if (_route.PathType == PathType.Hoisting) + { + startPosition = ResolveGroundTrackedCenter(startPosition, GetAnimatedObjectGroundContactHeight()); + } + else if (_route.PathType == PathType.Rail) + { + Point3D previousPoint = _pathPoints[0]; + Point3D nextPoint = _pathPoints.Count > 1 ? _pathPoints[1] : _pathPoints[0]; + double objectHeight = GetAnimatedObjectRailNormalExtent(previousPoint, _pathPoints[0], nextPoint); + startPosition = RailPathPoseHelper.ResolveObjectSpaceCenterPosition(_route, startPosition, previousPoint, nextPoint, objectHeight); + } + else + { + startPosition = ResolveGroundTrackedCenter(startPosition, GetAnimatedObjectGroundContactHeight()); + } + + UpdateObjectPosition(startPosition); + _hasGroundRealObjectBasePose = false; + _groundRealObjectStartCompensation = new Vector3D(0, 0, 0); + _hasGroundRealObjectStartCompensation = false; + + var startAppliedPoint = GetTrackedObjectPosition(CurrentControlledObject ?? _animatedObject); + LogManager.Info( + $"[平移到起点] 已保持初始位姿移动到起点: 路径point0=({pathStartPoint.X:F3},{pathStartPoint.Y:F3},{pathStartPoint.Z:F3}), " + + $"目标trackedPoint=({startPosition.X:F3},{startPosition.Y:F3},{startPosition.Z:F3}), " + + $"实际trackedPoint=({startAppliedPoint.X:F3},{startAppliedPoint.Y:F3},{startAppliedPoint.Z:F3}), 路径类型={_route.PathType.GetDisplayName()}"); + return true; + } + catch (Exception ex) + { + LogManager.Error($"保持初始位姿平移到路径起点失败: {ex.Message}"); + return false; + } + } + /// /// 预计算所有动画帧和碰撞信息 /// @@ -872,7 +968,20 @@ namespace NavisworksTransport.Core.Animation if (_animatedObject != null && _route != null && _route.Points != null && _route.Points.Count > 0) { var pathPoints = _route.Points.Select(p => p.Position).ToList(); - MoveObjectToPathStart(_animatedObject, pathPoints); + MoveObjectToPathStartUsingCurrentPlacementMode(_animatedObject, pathPoints); + if (_route.PathType == PathType.Rail && + _objectStartPlacementMode == ObjectStartPlacementMode.PreserveInitialPose && + _hasTrackedRotation) + { + _railPreservedPoseRotation = _trackedRotation; + _hasRailPreservedPoseRotation = true; + LogManager.Info("[预计算] Rail平移模式已锁定起点姿态为整段动画旋转基线"); + } + else + { + _railPreservedPoseRotation = Rotation3D.Identity; + _hasRailPreservedPoseRotation = false; + } LogManager.Info("[预计算] 物体已移动到路径起点"); } @@ -1169,11 +1278,18 @@ namespace NavisworksTransport.Core.Animation }; if (_route.PathType == PathType.Rail && - TryCreateRailPathRotation( - previousFramePoint, - framePosition, - nextFramePoint, - out var railRotation)) + _objectStartPlacementMode == ObjectStartPlacementMode.PreserveInitialPose && + _hasRailPreservedPoseRotation) + { + frame.Rotation = _railPreservedPoseRotation; + frame.HasCustomRotation = true; + } + else if (_route.PathType == PathType.Rail && + TryCreateRailPathRotation( + previousFramePoint, + framePosition, + nextFramePoint, + out var railRotation)) { frame.Rotation = railRotation; frame.HasCustomRotation = true; @@ -1622,7 +1738,7 @@ namespace NavisworksTransport.Core.Animation if (needsReset) { LogManager.Info($"[动画开始] 物体不在起点,重置到起点: {mismatchSummary}"); - MoveObjectToPathStart(); + MoveObjectToPathStartUsingCurrentPlacementMode(); } } @@ -4623,6 +4739,8 @@ namespace NavisworksTransport.Core.Animation /// public void SetObjectRotationCorrection(LocalEulerRotationCorrection rotationCorrection) { + _objectStartPlacementMode = ObjectStartPlacementMode.AlignToPathPose; + _hasRailPreservedPoseRotation = false; _objectRotationCorrection = rotationCorrection; // 如果动画已创建,更新物体到起点的朝向 @@ -4658,6 +4776,23 @@ namespace NavisworksTransport.Core.Animation LogManager.Debug($"[角度修正] 直接设置角度修正值: {_objectRotationCorrection}(不触发旋转)"); } + public void SetObjectStartPlacementMode(ObjectStartPlacementMode placementMode) + { + _objectStartPlacementMode = placementMode; + if (placementMode != ObjectStartPlacementMode.PreserveInitialPose) + { + _hasRailPreservedPoseRotation = false; + } + LogManager.Debug($"[起点摆放] 当前模式已设置为: {_objectStartPlacementMode}"); + } + + private bool MoveObjectToPathStartUsingCurrentPlacementMode(ModelItem animatedObject = null, List pathPoints = null) + { + return _objectStartPlacementMode == ObjectStartPlacementMode.PreserveInitialPose + ? MoveObjectToPathStartPreservingInitialPose(animatedObject, pathPoints) + : MoveObjectToPathStart(animatedObject, pathPoints); + } + /// /// 保留给旧单轴调用方:按当前宿主 up 轴映射成三轴角度修正。 /// diff --git a/src/Core/PathDataManager.cs b/src/Core/PathDataManager.cs index 98c8e30..c78ae3f 100644 --- a/src/Core/PathDataManager.cs +++ b/src/Core/PathDataManager.cs @@ -50,6 +50,7 @@ namespace NavisworksTransport public string pathType { get; set; } public string railMountMode { get; set; } public string railPathDefinitionMode { get; set; } + public double railNormalOffset { get; set; } public JsonVector3 railPreferredNormal { get; set; } public double totalLength { get; set; } public JsonObjectLimits objectLimits { get; set; } @@ -287,6 +288,7 @@ namespace NavisworksTransport pathType = route.PathType.ToString(), railMountMode = route.RailMountMode.ToString(), railPathDefinitionMode = route.RailPathDefinitionMode.ToString(), + railNormalOffset = Math.Round(route.RailNormalOffset, exportSettings?.Precision ?? 3), railPreferredNormal = route.RailPreferredNormal != null ? new { x = Math.Round(route.RailPreferredNormal.X, exportSettings?.Precision ?? 3), @@ -542,6 +544,8 @@ namespace NavisworksTransport route.RailPathDefinitionMode = railPathDefinitionMode; } + route.RailNormalOffset = jsonRoute.railNormalOffset; + if (jsonRoute.railPreferredNormal != null) { route.RailPreferredNormal = new Point3D( @@ -1409,6 +1413,7 @@ namespace NavisworksTransport routeElement.SetAttribute("pathType", route.PathType.ToString()); routeElement.SetAttribute("railMountMode", route.RailMountMode.ToString()); routeElement.SetAttribute("railPathDefinitionMode", route.RailPathDefinitionMode.ToString()); + routeElement.SetAttribute("railNormalOffset", route.RailNormalOffset.ToString("F3")); if (route.RailPreferredNormal != null) { routeElement.SetAttribute("railPreferredNormalX", route.RailPreferredNormal.X.ToString("F3")); @@ -1557,6 +1562,11 @@ namespace NavisworksTransport route.RailPathDefinitionMode = railPathDefinitionMode; } + if (double.TryParse(routeNode.Attributes?["railNormalOffset"]?.Value, out var railNormalOffset)) + { + route.RailNormalOffset = railNormalOffset; + } + if (double.TryParse(routeNode.Attributes?["railPreferredNormalX"]?.Value, out var railPreferredNormalX) && double.TryParse(routeNode.Attributes?["railPreferredNormalY"]?.Value, out var railPreferredNormalY) && double.TryParse(routeNode.Attributes?["railPreferredNormalZ"]?.Value, out var railPreferredNormalZ)) diff --git a/src/Core/PathDatabase.cs b/src/Core/PathDatabase.cs index 09d4d91..9a0299b 100644 --- a/src/Core/PathDatabase.cs +++ b/src/Core/PathDatabase.cs @@ -101,6 +101,7 @@ namespace NavisworksTransport LiftHeight REAL, RailMountMode INTEGER, RailPathDefinitionMode INTEGER, + RailNormalOffset REAL, RailPreferredNormalX REAL, RailPreferredNormalY REAL, RailPreferredNormalZ REAL, @@ -466,8 +467,8 @@ namespace NavisworksTransport // 路径长度由 PathRoute.TotalLength 计算属性实时从 Edges/Points 计算 var sql = @" INSERT OR REPLACE INTO PathRoutes - (Id, Name, EstimatedTime, TurnRadius, IsCurved, MaxObjectLength, MaxObjectWidth, MaxObjectHeight, SafetyMargin, GridSize, PathType, LiftHeight, RailMountMode, RailPathDefinitionMode, RailPreferredNormalX, RailPreferredNormalY, RailPreferredNormalZ, CreatedTime, LastModified) - VALUES (@id, @name, @time, @turnRadius, @isCurved, @maxLength, @maxWidth, @maxHeight, @safetyMargin, @gridSize, @pathType, @liftHeightMeters, @railMountMode, @railPathDefinitionMode, @railPreferredNormalX, @railPreferredNormalY, @railPreferredNormalZ, @created, @modified) + (Id, Name, EstimatedTime, TurnRadius, IsCurved, MaxObjectLength, MaxObjectWidth, MaxObjectHeight, SafetyMargin, GridSize, PathType, LiftHeight, RailMountMode, RailPathDefinitionMode, RailNormalOffset, RailPreferredNormalX, RailPreferredNormalY, RailPreferredNormalZ, CreatedTime, LastModified) + VALUES (@id, @name, @time, @turnRadius, @isCurved, @maxLength, @maxWidth, @maxHeight, @safetyMargin, @gridSize, @pathType, @liftHeightMeters, @railMountMode, @railPathDefinitionMode, @railNormalOffset, @railPreferredNormalX, @railPreferredNormalY, @railPreferredNormalZ, @created, @modified) "; using (var cmd = new SQLiteCommand(sql, _connection)) @@ -486,6 +487,7 @@ namespace NavisworksTransport cmd.Parameters.AddWithValue("@liftHeightMeters", route.LiftHeight); cmd.Parameters.AddWithValue("@railMountMode", (int)route.RailMountMode); cmd.Parameters.AddWithValue("@railPathDefinitionMode", (int)route.RailPathDefinitionMode); + cmd.Parameters.AddWithValue("@railNormalOffset", route.RailNormalOffset); cmd.Parameters.AddWithValue("@railPreferredNormalX", route.RailPreferredNormal != null ? (object)route.RailPreferredNormal.X : DBNull.Value); cmd.Parameters.AddWithValue("@railPreferredNormalY", route.RailPreferredNormal != null ? (object)route.RailPreferredNormal.Y : DBNull.Value); cmd.Parameters.AddWithValue("@railPreferredNormalZ", route.RailPreferredNormal != null ? (object)route.RailPreferredNormal.Z : DBNull.Value); @@ -1546,6 +1548,7 @@ namespace NavisworksTransport LiftHeight = Convert.ToDouble(reader["LiftHeight"]), RailMountMode = (RailMountMode)Convert.ToInt32(reader["RailMountMode"]), RailPathDefinitionMode = (RailPathDefinitionMode)Convert.ToInt32(reader["RailPathDefinitionMode"]), + RailNormalOffset = reader["RailNormalOffset"] != DBNull.Value ? Convert.ToDouble(reader["RailNormalOffset"]) : 0.0, CreatedTime = Convert.ToDateTime(reader["CreatedTime"]), LastModified = Convert.ToDateTime(reader["LastModified"]) }; @@ -2737,8 +2740,8 @@ namespace NavisworksTransport cmd.CommandText = @" SELECT Id, Name, CreatedTime, LastModified, TurnRadius, IsCurved, MaxObjectLength, MaxObjectWidth, MaxObjectHeight, - SafetyMargin, GridSize, PathType, LiftHeight, RailMountMode, RailPathDefinitionMode, - RailPreferredNormalX, RailPreferredNormalY, RailPreferredNormalZ + SafetyMargin, GridSize, PathType, LiftHeight, RailMountMode, RailPathDefinitionMode, + RailNormalOffset, RailPreferredNormalX, RailPreferredNormalY, RailPreferredNormalZ FROM PathRoutes WHERE Id = @Id"; @@ -2764,7 +2767,8 @@ namespace NavisworksTransport PathType = (PathType)Convert.ToInt32(reader["PathType"]), LiftHeight = Convert.ToDouble(reader["LiftHeight"]), RailMountMode = (RailMountMode)Convert.ToInt32(reader["RailMountMode"]), - RailPathDefinitionMode = (RailPathDefinitionMode)Convert.ToInt32(reader["RailPathDefinitionMode"]) + RailPathDefinitionMode = (RailPathDefinitionMode)Convert.ToInt32(reader["RailPathDefinitionMode"]), + RailNormalOffset = reader["RailNormalOffset"] != DBNull.Value ? Convert.ToDouble(reader["RailNormalOffset"]) : 0.0 }; if (reader["RailPreferredNormalX"] != DBNull.Value && @@ -2812,7 +2816,8 @@ namespace NavisworksTransport { ["railmountmode"] = "ALTER TABLE PathRoutes ADD COLUMN RailMountMode INTEGER DEFAULT 0", ["railpathdefinitionmode"] = "ALTER TABLE PathRoutes ADD COLUMN RailPathDefinitionMode INTEGER DEFAULT 0", - ["railpreferrednormalx"] = "ALTER TABLE PathRoutes ADD COLUMN RailPreferredNormalX REAL", + ["railnormaloffset"] = "ALTER TABLE PathRoutes ADD COLUMN RailNormalOffset REAL DEFAULT 0", + ["railpreferrednormalx"] = "ALTER TABLE PathRoutes ADD COLUMN RailPreferredNormalX REAL", ["railpreferrednormaly"] = "ALTER TABLE PathRoutes ADD COLUMN RailPreferredNormalY REAL", ["railpreferrednormalz"] = "ALTER TABLE PathRoutes ADD COLUMN RailPreferredNormalZ REAL" }; diff --git a/src/Core/PathPlanningManager.cs b/src/Core/PathPlanningManager.cs index f46fe95..94b5455 100644 --- a/src/Core/PathPlanningManager.cs +++ b/src/Core/PathPlanningManager.cs @@ -1271,6 +1271,7 @@ namespace NavisworksTransport route.LiftHeight = loadedRoute.LiftHeight; route.RailMountMode = loadedRoute.RailMountMode; route.RailPathDefinitionMode = loadedRoute.RailPathDefinitionMode; + route.RailNormalOffset = loadedRoute.RailNormalOffset; route.RailPreferredNormal = loadedRoute.RailPreferredNormal != null ? new Point3D( loadedRoute.RailPreferredNormal.X, diff --git a/src/Core/PathPlanningModels.cs b/src/Core/PathPlanningModels.cs index 63341ba..c99356e 100644 --- a/src/Core/PathPlanningModels.cs +++ b/src/Core/PathPlanningModels.cs @@ -743,6 +743,12 @@ namespace NavisworksTransport /// public Point3D RailPreferredNormal { get; set; } + /// + /// Rail 安装法向偏移(模型单位)。 + /// 沿 Rail 最终法向对安装参考点施加额外偏移,用于快速微调路径。 + /// + public double RailNormalOffset { get; set; } + // 数据库分析相关属性 /// /// 碰撞数量(从数据库加载) @@ -784,6 +790,7 @@ namespace NavisworksTransport RailMountMode = RailMountMode.UnderRail; RailPathDefinitionMode = RailPathDefinitionMode.LegacySuspensionPoint; RailPreferredNormal = null; + RailNormalOffset = 0.0; } /// @@ -807,6 +814,7 @@ namespace NavisworksTransport RailMountMode = RailMountMode.UnderRail; RailPathDefinitionMode = RailPathDefinitionMode.LegacySuspensionPoint; RailPreferredNormal = null; + RailNormalOffset = 0.0; } /// @@ -1042,6 +1050,7 @@ namespace NavisworksTransport SafetyMargin = SafetyMargin, RailMountMode = RailMountMode, RailPathDefinitionMode = RailPathDefinitionMode, + RailNormalOffset = RailNormalOffset, RailPreferredNormal = RailPreferredNormal != null ? new Point3D(RailPreferredNormal.X, RailPreferredNormal.Y, RailPreferredNormal.Z) : null diff --git a/src/UI/WPF/ViewModels/AnimationControlViewModel.cs b/src/UI/WPF/ViewModels/AnimationControlViewModel.cs index 6d0fbdd..52dc0ff 100644 --- a/src/UI/WPF/ViewModels/AnimationControlViewModel.cs +++ b/src/UI/WPF/ViewModels/AnimationControlViewModel.cs @@ -2196,7 +2196,15 @@ namespace NavisworksTransport.UI.WPF.ViewModels var dialog = new Views.EditRotationWindow(_objectRotationCorrection); if (dialog.ShowDialog() == true) { - ObjectRotationCorrection = dialog.RotationCorrection; + var placementRequest = dialog.AdjustmentRequest; + if (placementRequest.PreserveInitialPose) + { + ApplyTranslationOnlyObjectPlacement(); + return; + } + + _pathAnimationManager?.SetObjectStartPlacementMode(ObjectStartPlacementMode.AlignToPathPose); + ObjectRotationCorrection = placementRequest.RotationCorrection; LogManager.Info($"物体角度修正已更新: {_objectRotationCorrection}"); UpdateMainStatus($"物体角度修正: {_objectRotationCorrection}"); @@ -2214,6 +2222,40 @@ namespace NavisworksTransport.UI.WPF.ViewModels } } + private void ApplyTranslationOnlyObjectPlacement() + { + try + { + if (_pathAnimationManager == null || !HasSelectedAnimatedObject) + { + return; + } + + _objectRotationCorrection = LocalEulerRotationCorrection.Zero; + _pathAnimationManager.SetObjectRotationCorrectionDirect(LocalEulerRotationCorrection.Zero); + _pathAnimationManager.SetObjectStartPlacementMode(ObjectStartPlacementMode.PreserveInitialPose); + OnPropertyChanged(nameof(ObjectRotationCorrection)); + + if (_pathAnimationManager.MoveObjectToPathStartPreservingInitialPose()) + { + LogManager.Info("[角度修正] 已按平移模式将物体移动到路径起点,保持初始位姿"); + UpdateMainStatus("物体已平移到路径起点并保持初始位姿"); + } + else + { + LogManager.Warning("[角度修正] 平移模式未能将物体移动到路径起点"); + UpdateMainStatus("平移模式执行失败"); + } + + UpdatePassageSpaceVisualization(); + } + catch (Exception ex) + { + LogManager.Error($"平移到路径起点失败: {ex.Message}"); + UpdateMainStatus($"平移到路径起点失败: {ex.Message}"); + } + } + /// /// 更新物体旋转(应用角度修正) /// diff --git a/src/UI/WPF/ViewModels/PathEditingViewModel.cs b/src/UI/WPF/ViewModels/PathEditingViewModel.cs index ef3b6e3..cb660e2 100644 --- a/src/UI/WPF/ViewModels/PathEditingViewModel.cs +++ b/src/UI/WPF/ViewModels/PathEditingViewModel.cs @@ -125,6 +125,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels private const double DefaultAssemblyReferenceRodLengthInMeters = 20.0; private static readonly double DefaultAssemblyReferenceRodDiameterInMeters = CalculateDefaultAssemblyReferenceRodDiameterInMeters(); private const double DefaultAssemblyAnchorVerticalOffsetInMeters = 0.0; + private const double RailNormalOffsetNudgeStepInMeters = 0.1; private const double DefaultAssemblySphereCenterX = 0.0; private const double DefaultAssemblySphereCenterY = 0.0; private const double DefaultAssemblySphereCenterZ = 0.0; @@ -267,6 +268,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels OnPropertyChanged(nameof(CanUsePathLines)); OnPropertyChanged(nameof(IsRailRouteSelected)); OnPropertyChanged(nameof(SelectedRailMountMode)); + OnPropertyChanged(nameof(SelectedRailNormalOffsetInMeters)); if (!CanUsePathLines && ShowPathLines) { // 吊装和空轨路径不能使用路径线,自动关闭 @@ -348,6 +350,38 @@ namespace NavisworksTransport.UI.WPF.ViewModels } } + public double SelectedRailNormalOffsetInMeters + { + get + { + var coreRoute = GetSelectedCoreRoute(); + if (coreRoute == null || coreRoute.PathType != PathType.Rail) + { + return 0.0; + } + + return UnitsConverter.ConvertToMeters(coreRoute.RailNormalOffset); + } + set + { + var coreRoute = GetSelectedCoreRoute(); + if (coreRoute == null || coreRoute.PathType != PathType.Rail) + { + return; + } + + double offsetInModelUnits = UnitsConverter.ConvertFromMeters(value); + if (Math.Abs(coreRoute.RailNormalOffset - offsetInModelUnits) < 1e-6) + { + return; + } + + coreRoute.RailNormalOffset = offsetInModelUnits; + ApplyRailRouteConfigurationChange(coreRoute, $"安装法向偏移已更新为 {value:F3} 米"); + OnPropertyChanged(nameof(SelectedRailNormalOffsetInMeters)); + } + } + public string AssemblyTerminalObjectName { get => _assemblyTerminalObjectName; @@ -992,6 +1026,8 @@ namespace NavisworksTransport.UI.WPF.ViewModels public ICommand SelectAssemblyInstallationPointCommand { get; private set; } public ICommand ClearAssemblyReferenceRodCommand { get; private set; } public ICommand AnalyzeAssemblyTerminalFaceCommand { get; private set; } + public ICommand DecreaseSelectedRailNormalOffsetCommand { get; private set; } + public ICommand IncreaseSelectedRailNormalOffsetCommand { get; private set; } // 多层吊装命令 public ICommand SelectMultiLevelStartPointCommand { get; private set; } @@ -1196,6 +1232,19 @@ namespace NavisworksTransport.UI.WPF.ViewModels LogManager.Info($"[Rail构型] {coreRoute.Name}: {logMessage}"); } + private void AdjustSelectedRailNormalOffset(double deltaInMeters) + { + var coreRoute = GetSelectedCoreRoute(); + if (coreRoute == null || coreRoute.PathType != PathType.Rail) + { + return; + } + + double currentOffsetInMeters = UnitsConverter.ConvertToMeters(coreRoute.RailNormalOffset); + double nextOffsetInMeters = Math.Round(currentOffsetInMeters + deltaInMeters, 3); + SelectedRailNormalOffsetInMeters = nextOffsetInMeters; + } + /// /// 从配置文件加载参数 /// @@ -1336,6 +1385,8 @@ namespace NavisworksTransport.UI.WPF.ViewModels SelectAssemblyInstallationPointCommand = new RelayCommand(async () => await ExecuteSelectAssemblyInstallationPointAsync(), () => CanSelectAssemblyInstallationPoint); ClearAssemblyReferenceRodCommand = new RelayCommand(() => ExecuteClearAssemblyReferenceRod()); AnalyzeAssemblyTerminalFaceCommand = new RelayCommand(async () => await ExecuteAnalyzeAssemblyTerminalFaceAsync(), () => CanAnalyzeAssemblyTerminalFace); + DecreaseSelectedRailNormalOffsetCommand = new RelayCommand(() => AdjustSelectedRailNormalOffset(-RailNormalOffsetNudgeStepInMeters)); + IncreaseSelectedRailNormalOffsetCommand = new RelayCommand(() => AdjustSelectedRailNormalOffset(RailNormalOffsetNudgeStepInMeters)); } #endregion @@ -1704,7 +1755,8 @@ namespace NavisworksTransport.UI.WPF.ViewModels Description = $"直线装配路径 - {AssemblyTerminalObjectName}", PathType = PathType.Rail, RailMountMode = AssemblyMountMode, - RailPathDefinitionMode = RailPathDefinitionMode.RailCenterLine + RailPathDefinitionMode = RailPathDefinitionMode.RailCenterLine, + RailNormalOffset = UnitsConverter.ConvertFromMeters(AssemblyAnchorVerticalOffsetInMeters) }; if (_hasAssemblyInstallationReference) diff --git a/src/UI/WPF/Views/EditRotationWindow.xaml b/src/UI/WPF/Views/EditRotationWindow.xaml index 5eda71f..a1fe247 100644 --- a/src/UI/WPF/Views/EditRotationWindow.xaml +++ b/src/UI/WPF/Views/EditRotationWindow.xaml @@ -1,7 +1,7 @@  + Margin="0,0,10,0"/>