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"/>
+ Margin="0,0,10,0"/>
+ Margin="0,0,10,0"/>