# 动画朝向变换方案

**状态：✅ 已完成（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限制说明和解决方案）
