diff --git a/.gitignore b/.gitignore index cfff40d..4373112 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,4 @@ packages/ navisworks_api/ *.exe +*.db diff --git a/clean_database.bat b/clean_database.bat deleted file mode 100644 index 3866d43..0000000 --- a/clean_database.bat +++ /dev/null @@ -1,35 +0,0 @@ -@echo off -echo ======================================== -echo NavisworksTransport 数据库清理工具 -echo ======================================== -echo. -echo 此脚本将删除所有现有的路径数据库文件(.db文件) -echo 下次启动插件时将自动创建新的数据库结构 -echo. -echo 警告:此操作将删除所有历史路径数据! -echo. -pause - -REM 设置数据库文件所在目录 -set DB_DIR=C:\Users\Tellme\Documents\NavisworksTransport\分层输出 - -echo. -echo 正在清理数据库文件... -echo 目录: %DB_DIR% -echo. - -REM 删除所有.db文件 -if exist "%DB_DIR%\*.db" ( - del /f /q "%DB_DIR%\*.db" - echo 已删除所有数据库文件 -) else ( - echo 未找到数据库文件 -) - -echo. -echo ======================================== -echo 数据库清理完成! -echo 下次启动Navisworks插件时将自动创建新的数据库 -echo ======================================== -echo. -pause \ No newline at end of file diff --git a/doc/design/2026/NavisworksAPI使用方法.md b/doc/design/2026/NavisworksAPI使用方法.md index 7cf6efb..8f1237e 100644 --- a/doc/design/2026/NavisworksAPI使用方法.md +++ b/doc/design/2026/NavisworksAPI使用方法.md @@ -509,9 +509,16 @@ public void BatchNavisworksOperations(List items) **核心概念**: -- `ModelItem.Transform` - 返回设计文件中的原始变换,只读属性 -- `OverridePermanentTransform()` - 应用增量变换(与现有变换叠加) +- `ModelItem.Transform` - 返回设计文件中的原始变换,**只读属性**,**不反映override后的状态** +- `OverridePermanentTransform()` - 应用增量变换(相对于原始Transform累积) - `ResetPermanentTransform()` - 清除所有增量变换,恢复到设计文件原始位置 +- `ModelItem.BoundingBox()` - 返回**当前实际显示**的包围盒(反映override效果) + +**⚠️ 关键理解**: + +1. **`ModelItem.Transform` 永远返回原始值**,即使通过 `OverridePermanentTransform` 改变了物体位置 +2. **Override 信息存储在别处**,不会修改 `ModelItem.Transform` 属性 +3. **要获取实际位置,使用 `BoundingBox().Center`**,它反映override后的实际位置 ### 11.2 Transform 操作的正确用法 @@ -579,16 +586,15 @@ public void RestoreToOriginalPosition(ModelItem selectedObject) ### 11.5 常见Transform问题和解决方案 -**问题1:位置恢复有偏移** +**问题1:获取不到实际位置** ```csharp -// ❌ 错误:使用增量变换恢复位置 -doc.Models.OverridePermanentTransform(modelItems, originalTransform, false); -// 问题:如果物体已经被移动过,这会导致累积偏移 +// ❌ 错误:以为 Transform 反映当前位置 +var transform = item.Transform; +// 问题:这永远返回原始Transform,即使物体已被移动 -// ✅ 正确:重置到原始位置 -doc.Models.ResetPermanentTransform(modelItems); -// 结果:直接恢复到设计文件中的原始位置,无偏移 +// ✅ 正确:使用 BoundingBox 获取实际位置 +var actualCenter = item.BoundingBox().Center; // 反映override后的实际位置 ``` **问题2:动画结束后位置不准确** @@ -634,6 +640,209 @@ doc.Models.ResetPermanentTransform(modelItems); - 所有Transform操作都必须在主UI线程中执行 - 使用 `Dispatcher.Invoke` 确保线程安全 +### 11.7 旋转操作的关键限制和解决方案 ⚠️ 重要 + +基于实际测试验证的关键发现(2025-12-15)。 + +#### 11.7.1 旋转中心的API限制 + +**⚠️ 核心限制:Navisworks API的旋转总是绕世界原点(0,0,0)进行** + +```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),不是物体中心 +``` + +**验证代码**: + +```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 + +// 验证:实际位置 = 期望位置(绕原点旋转)✓ +``` + +#### 11.7.2 Transform3DComponents 的行为 + +**关键理解:`Transform3DComponents.Combine()` 的变换顺序** + +```csharp +// Transform3DComponents.Combine() 应用顺序: +// 1. Scale(缩放) +// 2. Rotation(旋转,绕原点) +// 3. Translation(平移) + +// ❌ 错误:直接设置rotation和translation +var components = identity.Factor(); +components.Rotation = new Rotation3D(new UnitVector3D(0, 0, 1), deltaYaw); +components.Translation = deltaPos; // 平移在旋转之后应用 +var transform = components.Combine(); + +// 问题:物体先绕原点旋转(产生位置偏移),然后平移 +// 结果:物体"公转"到错误位置 +``` + +#### 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` | 避免第一帧产生旋转增量 | +| 调试验证 | 测试物体远离原点的情况 | 原点附近可能掩盖问题 | + +**调试技巧**: + +```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})"); +``` + ## 12. Item属性和自定义属性访问 基于官方示例的正确属性访问方法总结。 diff --git a/doc/working/animation_orientation_plan_20251215.md b/doc/working/animation_orientation_plan_20251215.md new file mode 100644 index 0000000..eee0024 --- /dev/null +++ b/doc/working/animation_orientation_plan_20251215.md @@ -0,0 +1,381 @@ +# 动画朝向变换方案 + +**状态:✅ 已完成(2025-12-19)** + +## 背景 + +- 需求来源:`doc/requirement/todo_features.md` 中"动画时,物流模型朝向随路径变化"。 +- 现状:`PathAnimationManager` 仅对 `_animatedObject` 做平移增量,未处理旋转;预计算碰撞也以静态 AABB 为基准,无法反映转弯时的真实占位。 +- 补充约束:移动对象为转运车,只需在 XY 平面上旋转(绕世界 Z 轴的 yaw)。 + +## 关键API限制(实测验证) + +⚠️ **核心发现**:Navisworks API的旋转操作存在关键限制,必须理解才能正确实现: + +### 1. `ModelItem.Transform` 不反映override状态 + +```csharp +// ❌ 错误理解 +var transform = item.Transform; +// 以为这会返回当前实际位置,但实际上永远返回设计文件中的原始Transform + +// ✅ 正确做法 +var actualCenter = item.BoundingBox().Center; // 这才反映override后的实际位置 +``` + +**关键理解**: + +- `item.Transform` 永远返回设计文件中的原始Transform +- 通过 `OverridePermanentTransform` 改变位置后,`item.Transform` 不变 +- Override信息存储在别处,无法通过API直接读取 + +### 2. 旋转总是绕世界原点(0,0,0) + +```csharp +// ❌ 错误理解:以为旋转绕物体中心 +var rotation = new Transform3D(new Rotation3D(new UnitVector3D(0, 0, 1), angle)); +doc.Models.OverridePermanentTransform(modelItems, rotation, false); +// 实际效果:物体绕世界原点(0,0,0)"公转",不是绕自己"自转" +``` + +**实测验证**(2025-12-19): + +``` +物体初始位置:(-2.499, -1.640, 0.500) +应用45度Z轴旋转后:(-0.608, -2.927, 0.500) + +验证计算(绕原点旋转公式): +x' = -2.499 × cos(45°) - (-1.640) × sin(45°) = -0.607 ✓ +y' = -2.499 × sin(45°) + (-1.640) × cos(45°) = -2.927 ✓ + +结论:旋转中心是世界原点(0,0,0),不是物体中心 +``` + +**官方确认**: + +- Autodesk官方论坛Issue NW-53280已确认这是API限制 +- 无法指定自定义旋转中心点 +- 建议通过计算补偿实现绕物体中心旋转 + +### 3. Transform3DComponents的变换顺序 + +```csharp +// Transform3DComponents.Combine() 应用顺序: +// 1. Scale(缩放) +// 2. Rotation(旋转,绕原点) +// 3. Translation(平移) + +// ❌ 错误:直接设置rotation和translation +components.Rotation = new Rotation3D(new UnitVector3D(0, 0, 1), deltaYaw); +components.Translation = deltaPos; +// 问题:物体先绕原点旋转(产生位置偏移),然后平移 +// 结果:物体"公转"到错误位置 +``` + +参考:`doc/design/2026/NavisworksAPI使用方法.md` 第11.7节 + +## 目标 + +1. **实现"绕物体中心旋转"的视觉效果**:通过计算补偿平移量,抵消API的"绕原点旋转"导致的位置偏移。 +2. **路径朝向计算**:预计算阶段为每帧生成朝向角度(yaw),供播放时使用。 +3. **碰撞检测一致性**:预计算碰撞时创建旋转后的虚拟包围盒,避免仅平移带来的漏检/误检。 + +## 实施步骤 + +### 1. 帧数据扩展 + +在 `AnimationFrame` 中新增: + +```csharp +public double YawRadians { get; set; } // 当前帧的朝向角度(弧度) +``` + +在 `PathAnimationManager` 中添加: + +```csharp +private double _currentYaw = 0.0; // 跟踪当前偏航角 +``` + +### 2. 路径朝向计算 + +在 `PrecomputeAnimationFrames()` 中为每帧计算yaw: + +```csharp +private double ComputeYawFromPath(int frameIndex) +{ + // 取前后帧计算切线方向 + int prevIndex = Math.Max(0, frameIndex - 1); + int nextIndex = Math.Min(_animationFrames.Count - 1, frameIndex + 1); + + var prev = _animationFrames[prevIndex].Position; + var next = _animationFrames[nextIndex].Position; + + // XY平面上的方向向量 + double dx = next.X - prev.X; + double dy = next.Y - prev.Y; + + // 计算yaw角(atan2自动处理象限) + double yaw = Math.Atan2(dy, dx); + + // 零长度段使用上一帧方向 + if (Math.Abs(dx) < 1e-6 && Math.Abs(dy) < 1e-6) + { + yaw = frameIndex > 0 ? _animationFrames[frameIndex - 1].YawRadians : 0.0; + } + + return yaw; +} +``` + +### 3. 正确实现"绕物体中心旋转" + +**核心解决方案**:手动计算旋转导致的位置偏移并补偿 + +```csharp +private void UpdateObjectPosition(Point3D newPosition, double newYaw = double.NaN) +{ + 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); + } + + // 应用增量变换(false=增量模式) + 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 + 看起来像是绕自己旋转并移动到目标位置 +``` + +### 4. 初始化问题修复 + +⚠️ **关键**:`_currentYaw` 必须初始化为第一帧的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 + // 结果:只有平移,没有旋转偏移 +} +``` + +### 5. 旋转包围盒预计算 + +为了在碰撞检测中反映旋转后的实际占位,需要创建旋转后的虚拟包围盒: + +```csharp +private BoundingBox3D CreateVirtualBoundingBox(Point3D centerPosition, Vector3D size, double yawRadians) +{ + // 1. 计算包围盒的4个底面角点(相对中心) + double halfX = size.X / 2.0; + double halfY = size.Y / 2.0; + + Point3D[] corners = new Point3D[4] + { + new Point3D(-halfX, -halfY, 0), // 左下 + new Point3D( halfX, -halfY, 0), // 右下 + new Point3D( halfX, halfY, 0), // 右上 + new Point3D(-halfX, halfY, 0) // 左上 + }; + + // 2. 旋转每个角点 + double cos = Math.Cos(yawRadians); + double sin = Math.Sin(yawRadians); + + for (int i = 0; i < 4; i++) + { + double x = corners[i].X; + double y = corners[i].Y; + corners[i] = new Point3D( + x * cos - y * sin, + x * sin + y * cos, + corners[i].Z + ); + } + + // 3. 找出旋转后的新AABB边界 + double minX = corners.Min(c => c.X); + double maxX = corners.Max(c => c.X); + double minY = corners.Min(c => c.Y); + double maxY = corners.Max(c => c.Y); + + // 4. 平移到实际位置 + return new BoundingBox3D( + new Point3D(centerPosition.X + minX, centerPosition.Y + minY, centerPosition.Z), + new Point3D(centerPosition.X + maxX, centerPosition.Y + maxY, centerPosition.Z + size.Z) + ); +} +``` + +在预计算碰撞时使用: + +```csharp +// 使用旋转后的包围盒进行碰撞检测 +var rotatedBBox = CreateVirtualBoundingBox( + frame.Position, + boundingBoxSize, + frame.YawRadians +); + +// 使用rotatedBBox进行碰撞检测... +``` + +## 验证策略 + +### 1. 旋转中心验证 + +**测试方法**: + +```csharp +// 1. 将物体移动到远离原点的位置(如(-5, -5, 0)) +// 2. 应用旋转 +// 3. 检查物体是否"公转"(位置大幅移动)还是"自转"(位置基本不变) +``` + +**预期结果**: + +- 使用补偿公式后,物体应该绕自己旋转,位置基本不变(仅路径平移) +- 不应该出现"公转"现象 + +### 2. 路径覆盖测试 + +- 挑选含直角转弯、斜率变化的路径 +- 播放/Seek/反向播放,观察模型朝向是否随路径切线方向 +- 验证零长度段(重复点)是否正确使用上一帧方向 + +### 3. 碰撞一致性测试 + +- 对比旋转前/后的预计算碰撞结果 +- 在狭窄转弯处确保碰撞被正确捕获 +- 验证非正方形物体(如长方形车辆)的碰撞检测准确性 + +### 4. 初始化测试 + +- 验证动画开始时物体在正确的起点位置 +- 确认第一帧没有产生意外的旋转偏移 + +## 调试技巧 + +### 验证补偿计算 + +```csharp +// 在UpdateObjectPosition中添加调试日志 +double expectedX_afterRotation = _currentPosition.X * cos - _currentPosition.Y * sin; +double expectedY_afterRotation = _currentPosition.X * sin + _currentPosition.Y * cos; +var compensationX = newPosition.X - expectedX_afterRotation; +var compensationY = newPosition.Y - expectedY_afterRotation; + +LogManager.Debug($"旋转前: ({_currentPosition.X}, {_currentPosition.Y})"); +LogManager.Debug($"绕原点旋转后: ({expectedX_afterRotation}, {expectedY_afterRotation})"); +LogManager.Debug($"目标位置: ({newPosition.X}, {newPosition.Y})"); +LogManager.Debug($"需要补偿: ({compensationX}, {compensationY})"); +``` + +### 检测"公转"问题 + +```csharp +// 如果看到以下现象,说明没有正确补偿: +// - 物体起点位置不对 +// - 转弯时物体"飞"到错误位置 +// - 包围盒中心变化量远大于路径移动距离 +``` + +## 实际实现结果 + +**完成时间**:2025-12-19 + +**关键成果**: + +1. ✅ 成功实现"绕物体中心旋转"的视觉效果 +2. ✅ 物体沿路径平滑移动并旋转(自转而非公转) +3. ✅ 初始化问题已修复,起点位置正确 +4. ✅ 碰撞检测支持旋转后的包围盒 + +**关键代码文件**: + +- `src/Core/Animation/PathAnimationManager.cs` + - `AnimationFrame` 添加了 `YawRadians` 字段 + - `ComputeYawFromPath()` 计算每帧朝向 + - `UpdateObjectPosition()` 实现补偿平移算法 + - `CreateVirtualBoundingBox()` 创建旋转后的包围盒 + +**核心洞察**: + +- Navisworks API的旋转是固定绕世界原点的,这是设计限制而非bug +- 通过数学补偿可以实现任意旋转中心的视觉效果 +- `ModelItem.Transform` 不反映override状态,需要用 `BoundingBox().Center` 获取实际位置 + +**参考文档**: + +- `doc/design/2026/NavisworksAPI使用方法.md` 第11.7节(详细的API限制说明和解决方案) diff --git a/src/Commands/ReadTransformTestCommand.cs b/src/Commands/ReadTransformTestCommand.cs new file mode 100644 index 0000000..db0ca8a --- /dev/null +++ b/src/Commands/ReadTransformTestCommand.cs @@ -0,0 +1,66 @@ +using System; +using System.Linq; +using System.Windows; +using Autodesk.Navisworks.Api; +using Autodesk.Navisworks.Api.Plugins; +using NavisworksTransport.Core; + +namespace NavisworksTransport.Commands +{ + [PluginAttribute("ReadTransformTest", "YourDeveloperID", DisplayName = "读取Transform测试")] + [AddInPluginAttribute(AddInLocation.AddIn)] + public class ReadTransformTestCommand : AddInPlugin + { + public override int Execute(params string[] parameters) + { + try + { + var doc = Autodesk.Navisworks.Api.Application.ActiveDocument; + var selection = doc.CurrentSelection.SelectedItems; + + if (selection.Count == 0) + { + MessageBox.Show("请先选择一个对象!", "提示"); + return 0; + } + + var item = selection.First(); + + // 读取Transform + var transform = item.Transform; + var components = transform.Factor(); + var bbox = item.BoundingBox(); + var center = bbox.Center; + + var info = $"=== Transform信息 ===\n\n"; + info += $"对象: {item.DisplayName}\n\n"; + info += $"包围盒中心:\n"; + info += $" X = {center.X:F3}\n"; + info += $" Y = {center.Y:F3}\n"; + info += $" Z = {center.Z:F3}\n\n"; + info += $"Transform.Translation:\n"; + info += $" X = {components.Translation.X:F3}\n"; + info += $" Y = {components.Translation.Y:F3}\n"; + info += $" Z = {components.Translation.Z:F3}\n\n"; + info += $"Transform.Scale:\n"; + info += $" X = {components.Scale.X:F3}\n"; + info += $" Y = {components.Scale.Y:F3}\n"; + info += $" Z = {components.Scale.Z:F3}\n\n"; + info += $"Transform.Rotation:\n"; + info += $" Axis = ({components.Rotation.Axis.X:F3}, {components.Rotation.Axis.Y:F3}, {components.Rotation.Axis.Z:F3})\n"; + info += $" Angle = {components.Rotation.Angle:F6} rad = {components.Rotation.Angle * 180 / Math.PI:F2}°\n"; + + LogManager.Info(info); + MessageBox.Show(info, "Transform信息", MessageBoxButton.OK); + + return 0; + } + catch (Exception ex) + { + MessageBox.Show($"错误: {ex.Message}", "错误"); + LogManager.Error($"读取Transform失败: {ex.Message}\n{ex.StackTrace}"); + return 1; + } + } + } +} diff --git a/src/Core/Animation/PathAnimationManager.cs b/src/Core/Animation/PathAnimationManager.cs index b011f67..8de88ad 100644 --- a/src/Core/Animation/PathAnimationManager.cs +++ b/src/Core/Animation/PathAnimationManager.cs @@ -64,19 +64,21 @@ namespace NavisworksTransport.Core.Animation /// 已集成 TimeLiner 功能,支持在 TimeLiner 中显示和管理动画任务 /// /// - /// 动画帧数据(包含位置和碰撞信息) + /// 动画帧数据(包含位置、朝向和碰撞信息) /// public class AnimationFrame { public int Index { get; set; } // 帧索引 public double Progress { get; set; } // 进度(0-1) public Point3D Position { get; set; } // 该帧的位置 + public double YawRadians { get; set; } // 绕Z轴的偏航角(弧度) public List Collisions { get; set; } // 该帧的碰撞结果 public bool HasCollision => Collisions?.Count > 0; public AnimationFrame() { Collisions = new List(); + YawRadians = 0.0; } } @@ -136,6 +138,7 @@ namespace NavisworksTransport.Core.Animation private Point3D _currentPosition; // 存储部件的当前位置 private AnimationState _currentState = AnimationState.Idle; private double _pausedProgress = 0.0; // 暂停时的进度(0-1之间) + private double _currentYaw = 0.0; // 当前偏航角(弧度) // TimeLiner 集成 private TimeLinerIntegrationManager _timeLinerManager; @@ -259,11 +262,17 @@ namespace NavisworksTransport.Core.Animation _animationDuration = durationSeconds; // 保存原始变换以便重置 - _originalTransform = GetCurrentTransform(_animatedObject); + _originalTransform = _animatedObject.Transform; // 使用真正的Transform,不是自造的 // 保存车辆的原始中心位置 var originalBoundingBox = animatedObject.BoundingBox(); _originalCenter = originalBoundingBox.Center; + + // 调试:查看原始Transform的组成 + var origComponents = _originalTransform.Factor(); + LogManager.Info($"[原始Transform] Translation=({origComponents.Translation.X:F2},{origComponents.Translation.Y:F2},{origComponents.Translation.Z:F2})"); + LogManager.Info($"[原始Transform] Scale=({origComponents.Scale.X:F2},{origComponents.Scale.Y:F2},{origComponents.Scale.Z:F2})"); + LogManager.Info($"[原始Transform] 包围盒Center=({_originalCenter.X:F2},{_originalCenter.Y:F2},{_originalCenter.Z:F2})"); // 预计算动画帧和碰撞 PrecomputeAnimationFrames(); @@ -442,11 +451,23 @@ namespace NavisworksTransport.Core.Animation LogManager.Info($"空间查询半径: {searchRadiusInModelUnits:F2} 模型单位"); } - // 预计算每一帧 + // 第一遍:收集所有帧位置 + var framePositions = new List(); for (int i = 0; i < totalFrames; i++) { double progress = (double)i / totalFrames; var framePosition = InterpolatePosition(progress); + framePositions.Add(framePosition); + } + + // 第二遍:计算朝向并预计算每一帧 + for (int i = 0; i < totalFrames; i++) + { + double progress = (double)i / totalFrames; + var framePosition = framePositions[i]; + + // 计算朝向(基于前后帧) + double yawRadians = ComputeYawFromPath(i, framePositions); // 创建帧数据 var frame = new AnimationFrame @@ -454,11 +475,12 @@ namespace NavisworksTransport.Core.Animation Index = i, Progress = progress, Position = framePosition, + YawRadians = yawRadians, Collisions = new List() }; - // 虚拟碰撞检测(不移动实际物体) - var virtualBoundingBox = CreateVirtualBoundingBox(framePosition, boundingBoxSize); + // 虚拟碰撞检测(不移动实际物体),考虑旋转 + var virtualBoundingBox = CreateVirtualBoundingBox(framePosition, boundingBoxSize, yawRadians); IEnumerable nearbyObjects; if (manualOverrideActive) @@ -530,19 +552,116 @@ namespace NavisworksTransport.Core.Animation } /// - /// 创建虚拟包围盒(用于碰撞检测) + /// 根据路径计算指定帧的yaw角度 /// - private BoundingBox3D CreateVirtualBoundingBox(Point3D position, Vector3D size) + private double ComputeYawFromPath(int frameIndex, List framePositions) { + if (framePositions == null || framePositions.Count < 2) + return 0.0; + + int totalFrames = framePositions.Count; + Point3D currentPos = framePositions[frameIndex]; + Point3D nextPos; + + // 首帧或中间帧:看向下一帧 + if (frameIndex < totalFrames - 1) + { + nextPos = framePositions[frameIndex + 1]; + } + // 尾帧:保持上一帧的方向 + else if (frameIndex > 0) + { + Point3D prevPos = framePositions[frameIndex - 1]; + nextPos = new Point3D( + currentPos.X + (currentPos.X - prevPos.X), + currentPos.Y + (currentPos.Y - prevPos.Y), + currentPos.Z + ); + } + else + { + return 0.0; // 只有一帧 + } + + // 计算XY平面的方向向量 + double dx = nextPos.X - currentPos.X; + double dy = nextPos.Y - currentPos.Y; + double length = Math.Sqrt(dx * dx + dy * dy); + + // 如果距离太小,保持当前yaw(或返回0) + if (length < 1e-6) + return (frameIndex > 0 && frameIndex < totalFrames) ? ComputeYawFromPath(frameIndex - 1, framePositions) : 0.0; + + // 计算yaw角度(atan2返回弧度) + return Math.Atan2(dy, dx); + } + + /// + /// 创建虚拟包围盒(用于碰撞检测),考虑旋转 + /// + private BoundingBox3D CreateVirtualBoundingBox(Point3D position, Vector3D size, double yawRadians = 0.0) + { + // 如果没有旋转,返回简单的轴对齐包围盒 + if (Math.Abs(yawRadians) < 1e-6) + { + return new BoundingBox3D( + new Point3D( + position.X - size.X / 2, + position.Y - size.Y / 2, + position.Z + ), + new Point3D( + position.X + size.X / 2, + position.Y + size.Y / 2, + position.Z + size.Z + ) + ); + } + + // 有旋转:计算旋转后的轴对齐包围盒 + // 1. 定义物体包围盒的4个底面角点(相对于中心) + double halfX = size.X / 2; + double halfY = size.Y / 2; + + var corners = new[] + { + new { x = -halfX, y = -halfY }, + new { x = halfX, y = -halfY }, + new { x = halfX, y = halfY }, + new { x = -halfX, y = halfY } + }; + + // 2. 旋转这些角点 + double cos = Math.Cos(yawRadians); + double sin = Math.Sin(yawRadians); + + double minX = double.MaxValue; + double maxX = double.MinValue; + double minY = double.MaxValue; + double maxY = double.MinValue; + + foreach (var corner in corners) + { + // 旋转公式:x' = x*cos - y*sin, y' = x*sin + y*cos + double rotatedX = corner.x * cos - corner.y * sin; + double rotatedY = corner.x * sin + corner.y * cos; + + minX = Math.Min(minX, rotatedX); + maxX = Math.Max(maxX, rotatedX); + minY = Math.Min(minY, rotatedY); + maxY = Math.Max(maxY, rotatedY); + } + + // 3. 创建包含所有旋转后角点的轴对齐包围盒 return new BoundingBox3D( new Point3D( - position.X - size.X / 2, - position.Y - size.Y / 2, + position.X + minX, + position.Y + minY, position.Z ), new Point3D( - position.X + size.X / 2, - position.Y + size.Y / 2, + position.X + maxX, + position.Y + maxY, position.Z + size.Z ) ); @@ -651,6 +770,21 @@ namespace NavisworksTransport.Core.Animation _animationFrameCount = 0; // 重置帧计数 _currentFrameIndex = 0; // 重置当前帧索引,确保从第一帧开始 _pausedProgress = 0.0; // 重置暂停进度 + + // 初始化yaw为第一帧的yaw,避免第一帧就产生旋转增量 + if (_animationFrames != null && _animationFrames.Count > 0) + { + var firstFrame = _animationFrames[0]; + _currentYaw = firstFrame.YawRadians; // 关键:设置为第一帧的yaw,使deltaYaw=0 + + // 立即设置到第一帧的位置和朝向(此时deltaYaw=0,只有平移) + UpdateObjectPosition(firstFrame.Position, firstFrame.YawRadians); + LogManager.Debug($"[动画开始] 设置初始位置和朝向: pos=({firstFrame.Position.X:F2},{firstFrame.Position.Y:F2}), yaw={firstFrame.YawRadians:F3}rad"); + } + else + { + _currentYaw = 0.0; + } // 重置动画状态 _lastFrameTime = DateTime.MinValue; @@ -1224,26 +1358,64 @@ namespace NavisworksTransport.Core.Animation } /// - /// 更新对象位置 + /// 更新对象位置和朝向(支持绕物体中心旋转) /// - private void UpdateObjectPosition(Point3D newPosition) + private void UpdateObjectPosition(Point3D newPosition, double newYaw = double.NaN) { try { var doc = NavisApplication.ActiveDocument; var modelItems = new ModelItemCollection { _animatedObject }; - // 正确的增量变换:计算从当前位置到新位置的偏移 - var incrementalOffset = new Vector3D( + // 计算平移和旋转的增量 + var deltaPos = new Vector3D( newPosition.X - _currentPosition.X, newPosition.Y - _currentPosition.Y, newPosition.Z - _currentPosition.Z ); - // 创建增量变换 - var incrementalTransform = Transform3D.CreateTranslation(incrementalOffset); + Transform3D incrementalTransform; - // 应用增量变换(不重置之前的变换) + if (!double.IsNaN(newYaw)) + { + // 有旋转:需要实现"绕物体当前位置自转" + // 由于Transform3DComponents.Rotation总是绕世界原点旋转 + // 我们需要手动计算旋转导致的位置偏移,并补偿 + + double deltaYaw = newYaw - _currentYaw; + + // 计算绕当前位置旋转的等效变换: + // 1. 如果绕原点旋转deltaYaw,当前位置_currentPosition会移动到哪里? + 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, + newPosition.Y - rotatedY, + newPosition.Z - _currentPosition.Z // Z保持deltaPos + ); + + // 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); + } + + // 应用增量变换(false = 增量模式) doc.Models.OverridePermanentTransform(modelItems, incrementalTransform, false); // 更新当前位置 @@ -1440,9 +1612,9 @@ namespace NavisworksTransport.Core.Animation _currentFrameIndex = frameIndex; - // 更新对象位置 + // 更新对象位置和朝向 var frameData = _animationFrames[_currentFrameIndex]; - UpdateObjectPosition(frameData.Position); + UpdateObjectPosition(frameData.Position, frameData.YawRadians); // 更新碰撞高亮 UpdateCollisionHighlightFromFrame(); @@ -2004,11 +2176,11 @@ namespace NavisworksTransport.Core.Animation { _currentFrameIndex = nextFrameIndex; - // 使用预计算的帧位置 + // 使用预计算的帧位置和朝向 if (_currentFrameIndex < _animationFrames.Count) { var frameData = _animationFrames[_currentFrameIndex]; - UpdateObjectPosition(frameData.Position); + UpdateObjectPosition(frameData.Position, frameData.YawRadians); // 更新碰撞高亮(基于预计算结果) UpdateCollisionHighlightFromFrame(); diff --git a/src/UI/WPF/ViewModels/SystemManagementViewModel.cs b/src/UI/WPF/ViewModels/SystemManagementViewModel.cs index c26bd43..c8f9c05 100644 --- a/src/UI/WPF/ViewModels/SystemManagementViewModel.cs +++ b/src/UI/WPF/ViewModels/SystemManagementViewModel.cs @@ -274,6 +274,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels // 功能测试命令 public ICommand TestVoxelGridSDFCommand { get; private set; } public ICommand TestVoxelPathFindingCommand { get; private set; } + public ICommand ReadTransformTestCommand { get; private set; } #endregion @@ -377,6 +378,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels // 功能测试命令 TestVoxelGridSDFCommand = new RelayCommand(() => ExecuteTestVoxelGridSDF()); TestVoxelPathFindingCommand = new RelayCommand(() => ExecuteTestVoxelPathFinding()); + ReadTransformTestCommand = new RelayCommand(() => ExecuteReadTransformTest()); LogManager.Info("系统管理命令初始化完成"); } @@ -1025,6 +1027,79 @@ namespace NavisworksTransport.UI.WPF.ViewModels }, "体素路径规划测试"); } + /// + /// 读取选中对象的Transform信息,并测试旋转 + /// + private void ExecuteReadTransformTest() + { + try + { + var doc = Autodesk.Navisworks.Api.Application.ActiveDocument; + var selection = doc.CurrentSelection.SelectedItems; + + if (selection.Count == 0) + { + System.Windows.MessageBox.Show("请先选择一个对象!", "提示", + System.Windows.MessageBoxButton.OK, System.Windows.MessageBoxImage.Information); + return; + } + + var item = selection[0]; + var modelItems = new Autodesk.Navisworks.Api.ModelItemCollection { item }; + + // === 读取初始状态 === + var transform1 = item.Transform; + var components1 = transform1.Factor(); + var bbox1 = item.BoundingBox(); + var center1 = bbox1.Center; + + var info = $"=== 初始状态 ===\n"; + info += $"对象: {item.DisplayName}\n"; + info += $"包围盒中心: ({center1.X:F3}, {center1.Y:F3}, {center1.Z:F3})\n"; + info += $"Transform.Translation: ({components1.Translation.X:F3}, {components1.Translation.Y:F3}, {components1.Translation.Z:F3})\n"; + info += $"Transform.Rotation: {components1.Rotation}\n\n"; + + // === 应用旋转override === + // 绕Z轴旋转45度(π/4弧度) + double angle = Math.PI / 4; // 45度 + var rotationTransform = new Autodesk.Navisworks.Api.Transform3D( + new Autodesk.Navisworks.Api.Rotation3D( + new Autodesk.Navisworks.Api.UnitVector3D(0, 0, 1), + angle + ) + ); + + // 应用override(false=增量模式) + doc.Models.OverridePermanentTransform(modelItems, rotationTransform, false); + + info += $"=== 应用旋转后(绕Z轴45度)===\n"; + + // === 重新读取状态 === + var transform2 = item.Transform; + var components2 = transform2.Factor(); + var bbox2 = item.BoundingBox(); + var center2 = bbox2.Center; + + info += $"包围盒中心: ({center2.X:F3}, {center2.Y:F3}, {center2.Z:F3})\n"; + info += $"Transform.Translation: ({components2.Translation.X:F3}, {components2.Translation.Y:F3}, {components2.Translation.Z:F3})\n"; + info += $"Transform.Rotation: {components2.Rotation}\n\n"; + + info += $"=== 对比 ===\n"; + info += $"包围盒中心变化: ({center2.X - center1.X:F3}, {center2.Y - center1.Y:F3}, {center2.Z - center1.Z:F3})\n"; + info += $"item.Transform是否变化: {!transform1.Equals(transform2)}\n"; + + LogManager.Info(info); + System.Windows.MessageBox.Show(info, "Transform测试", + System.Windows.MessageBoxButton.OK, System.Windows.MessageBoxImage.Information); + } + catch (Exception ex) + { + System.Windows.MessageBox.Show($"错误: {ex.Message}", "错误", + System.Windows.MessageBoxButton.OK, System.Windows.MessageBoxImage.Error); + LogManager.Error($"读取Transform失败: {ex.Message}\n{ex.StackTrace}"); + } + } + #endregion #region 辅助方法 diff --git a/src/UI/WPF/Views/SystemManagementView.xaml b/src/UI/WPF/Views/SystemManagementView.xaml index 34525cd..19be832 100644 --- a/src/UI/WPF/Views/SystemManagementView.xaml +++ b/src/UI/WPF/Views/SystemManagementView.xaml @@ -270,6 +270,12 @@ NavisworksTransport 系统管理页签视图 - 采用与其他页签一致的Nav Command="{Binding TestVoxelPathFindingCommand}" Style="{StaticResource ActionButtonStyle}" ToolTip="使用3D A*算法在体素网格中规划路径,支持楼梯、坡道等垂直移动"/> + + +