# 动画检测集成方案

## 一、现状分析

### 1.1 当前动画系统架构

#### 核心组件

- **PathAnimationManager**: 动画管理器,负责动画帧预计算和播放
- **AnimationControlViewModel**: 动画控制视图模型,处理UI交互
- **PathCurveEngine**: 路径曲线化引擎,已实现圆弧过渡功能

#### 当前路径插值机制

```csharp
// PathAnimationManager.cs:1253
private Point3D InterpolatePosition(double progress)
```

- 基于原始控制点(_pathPoints)进行线性插值
- 按路径段距离比例计算位置
- **问题**: 未使用PathRoute.Edges,动画沿直线段运动

#### 当前朝向计算机制

```csharp
// PathAnimationManager.cs:600
private double ComputeYawFromPath(int frameIndex, List<Point3D> framePositions)
```

- 基于前后帧位置计算方向向量
- 使用atan2计算偏航角
- **问题**: 在直线段上方向准确,但在圆弧处需要改进

### 1.2 路径数据结构

#### PathRoute模型

```csharp
// PathPlanningModels.cs
public class PathRoute
{
    public List<PathPoint> Points { get; set; }        // 控制点
    public List<PathEdge> Edges { get; set; }          // 物理路径段
    public bool IsCurved { get; set; }                 // 是否已曲线化
    public double TurnRadius { get; set; }             // 转向半径
}
```

#### PathEdge类型

```csharp
public enum PathSegmentType
{
    Straight,  // 直线段
    Arc        // 圆弧段
}

public class PathEdge
{
    public PathSegmentType SegmentType { get; set; }
    public ArcTrajectory Trajectory { get; set; }      // 圆弧轨迹数据
    public List<Point3D> SampledPoints { get; set; }  // 采样点序列
    public double PhysicalLength { get; set; }         // 物理长度
}
```

#### ArcTrajectory数据

```csharp
public class ArcTrajectory
{
    public Point3D Ts { get; set; }           // 进入切点
    public Point3D Te { get; set; }           // 退出切点
    public Point3D ArcCenter { get; set; }    // 圆心
    public double ActualRadius { get; set; }  // 实际半径
    public double DeflectionAngle { get; set; } // 偏转角(弧度)
    public double ArcLength { get; set; }     // 圆弧长度
}
```

### 1.3 PathCurveEngine已实现功能

- **CalculateFillet**: 计算圆弧切点和轨迹参数
- **SampleArc**: 圆弧采样为离散点序列
- **ApplyCurvatureToRoute**: 对路径应用曲线化处理
- **GenerateSampledPoints**: 生成采样点序列

## 二、集成方案设计

### 2.1 核心目标

1. **动画帧预计算使用路径**: 替代原有的直线插值
2. **圆弧处方向计算准确**: 确保物体在圆弧段正确朝向
3. **性能优化**: 避免重复计算采样点

### 2.2 数据流设计

```
PathRoute (控制点)
    ↓
PathCurveEngine.ApplyCurvatureToRoute()
    ↓
PathRoute.Edges (包含直线段和圆弧段)
    ↓
PathAnimationManager.PrecomputeAnimationFrames()
    ↓
使用PathEdge.SampledPoints生成动画帧
    ↓
动画播放
```

### 2.3 关键修改点

#### 修改点1: PathAnimationManager.SetupAnimation()

**当前实现**:

```csharp
// PathAnimationManager.cs:407
public void SetupAnimation(ModelItem animatedObject, List<Point3D> pathPoints, double durationSeconds = 10.0)
{
    // 直接使用pathPoints作为路径
    _pathPoints = new List<Point3D>(pathPoints);
    PrecomputeAnimationFrames();
}
```

**修改方案**:

```csharp
public void SetupAnimation(
    ModelItem animatedObject,
    double durationSeconds,
    PathRoute route)
{
    if (route == null)
        throw new ArgumentNullException(nameof(route), "必须提供路径");

    _animatedObject = animatedObject;
    _animationDuration = durationSeconds;
    _route = route;

    LogManager.Info($"使用路径,边数:{route.Edges.Count},总长度:{route.TotalLength:F2}米,动画时长:{durationSeconds:F1}秒");

    PrecomputeAnimationFrames();
}
```

#### 修改点2: PathAnimationManager.PrecomputeAnimationFrames()

**当前实现**:

```csharp
// PathAnimationManager.cs:468
private void PrecomputeAnimationFrames()
{
    // 第一遍:收集所有帧位置(使用直线插值)
    var framePositions = new List<Point3D>();
    for (int i = 0; i < totalFrames; i++)
    {
        double progress = (double)i / (totalFrames - 1);
        var framePosition = InterpolatePosition(progress);  // 直线插值
        framePositions.Add(framePosition);
    }

    // 第二遍:计算朝向并预计算每一帧
    for (int i = 0; i < totalFrames; i++)
    {
        double yawRadians = ComputeYawFromPath(i, framePositions);
        // ...
    }
}
```

**修改方案**:

```csharp
private void PrecomputeAnimationFrames()
{
    LogManager.Info("=== 使用路径预计算动画帧 ===");

    int totalFrames = (int)(_animationDuration * _animationFrameRate);

    // 1. 构建完整的采样点序列(按边顺序拼接)
    var allSampledPoints = new List<Point3D>();
    var edgeLengths = new List<double>();  // 每条边的长度
    var edgeStartIndices = new List<int>(); // 每条边在采样序列中的起始索引

    foreach (var edge in _route.Edges)
    {
        if (edge.SampledPoints == null || edge.SampledPoints.Count == 0)
        {
            // 延迟加载采样点
            var samplingStep = ConfigManager.Instance.Current.PathEditing.ArcSamplingStep;
            edge.SampledPoints = PathCurveEngine.GenerateSampledPoints(edge, samplingStep);
        }

        edgeStartIndices.Add(allSampledPoints.Count);
        allSampledPoints.AddRange(edge.SampledPoints);
        edgeLengths.Add(edge.PhysicalLength);
    }

    double totalLength = _route.TotalLength;
    LogManager.Info($"路径总长度: {totalLength:F2}米, 采样点数: {allSampledPoints.Count}");

    // 2. 按帧数采样生成动画帧
    _animationFrames = new List<AnimationFrame>();

    for (int i = 0; i < totalFrames; i++)
    {
        double progress = (double)i / (totalFrames - 1);
        double targetDistance = totalLength * progress;

        // 3. 找到当前在哪条边上
        int edgeIndex = FindEdgeForDistance(targetDistance, edgeLengths);
        if (edgeIndex < 0 || edgeIndex >= _route.Edges.Count)
        {
            LogManager.Warning($"无法找到边,目标距离:{targetDistance:F2}米");
            continue;
        }

        var edge = _route.Edges[edgeIndex];
        double accumulatedLength = edgeLengths.Take(edgeIndex).Sum();
        double edgeProgress = (targetDistance - accumulatedLength) / edge.PhysicalLength;

        // 4. 在边内插值位置
        Point3D framePosition;
        if (edge.SegmentType == PathSegmentType.Straight)
        {
            framePosition = InterpolateOnStraightEdge(edge, edgeProgress);
        }
        else // Arc
        {
            framePosition = InterpolateOnArcEdge(edge, edgeProgress);
        }

        // 5. 计算朝向(关键改进)
        double yawRadians = ComputeYawOnPath(i, allSampledPoints, edgeIndex, edgeProgress);

        // 6. 创建帧并检测碰撞
        var frame = new AnimationFrame
        {
            Index = i,
            Progress = progress,
            Position = framePosition,
            YawRadians = yawRadians,
            Collisions = new List<CollisionResult>()
        };

        // ... 碰撞检测逻辑保持不变 ...

        _animationFrames.Add(frame);
    }
}
```

#### 修改点3: 曲线边内插值方法

```csharp
/// <summary>
/// 在直线边上插值位置
/// </summary>
private Point3D InterpolateOnStraightEdge(PathEdge edge, double progress)
{
    if (edge.SampledPoints == null || edge.SampledPoints.Count < 2)
        return edge.Trajectory?.Ts ?? new Point3D();

    int pointIndex = (int)(progress * (edge.SampledPoints.Count - 1));
    pointIndex = Math.Max(0, Math.Min(pointIndex, edge.SampledPoints.Count - 1));

    return edge.SampledPoints[pointIndex];
}

/// <summary>
/// 在圆弧边上插值位置
/// </summary>
private Point3D InterpolateOnArcEdge(PathEdge edge, double progress)
{
    if (edge.Trajectory == null || edge.SampledPoints == null || edge.SampledPoints.Count < 2)
        return edge.Trajectory?.Ts ?? new Point3D();

    // 使用采样点插值(更精确)
    int pointIndex = (int)(progress * (edge.SampledPoints.Count - 1));
    pointIndex = Math.Max(0, Math.Min(pointIndex, edge.SampledPoints.Count - 1));

    return edge.SampledPoints[pointIndex];
}

/// <summary>
/// 查找指定距离对应的边索引
/// </summary>
private int FindEdgeForDistance(double targetDistance, List<double> edgeLengths)
{
    double accumulatedLength = 0.0;

    for (int i = 0; i < edgeLengths.Count; i++)
    {
        if (accumulatedLength + edgeLengths[i] >= targetDistance)
        {
            return i;
        }
        accumulatedLength += edgeLengths[i];
    }

    return edgeLengths.Count - 1; // 返回最后一条边
}
```

#### 修改点4: 路径朝向计算(核心)

```csharp
/// <summary>
/// 计算路径上的朝向(改进版)
/// </summary>
private double ComputeYawOnPath(
    int frameIndex,
    List<Point3D> allSampledPoints,
    int currentEdgeIndex,
    double edgeProgress)
{
    Point3D currentPos = allSampledPoints.Count > 0 ? allSampledPoints[Math.Min(frameIndex, allSampledPoints.Count - 1)] : new Point3D();
    Point3D nextPos;

    var edge = _route.Edges[currentEdgeIndex];

    if (edge.SegmentType == PathSegmentType.Arc && edge.Trajectory != null)
    {
        // 圆弧段: 计算切线方向
        // 使用圆弧参数方程计算精确方向

        // 1. 获取当前点在圆弧上的角度
        Vector3D startVec = (edge.Trajectory.Ts - edge.Trajectory.ArcCenter).Normalize();
        double signedAngle = edge.Trajectory.DeflectionAngle; // 总偏转角
        double currentAngle = edgeProgress * signedAngle;

        // 2. 旋转起始向量得到当前方向向量
        Point3D rotatedPoint = RotatePointAroundAxis(
            edge.Trajectory.Ts,
            edge.Trajectory.ArcCenter,
            GetArcRotationAxis(edge.Trajectory),
            currentAngle
        );

        // 3. 计算切线方向(沿圆弧切线)
        Vector3D radiusVec = (rotatedPoint - edge.Trajectory.ArcCenter).Normalize();
        Vector3D tangentVec = Vector3D.CrossProduct(
            GetArcRotationAxis(edge.Trajectory),
            radiusVec
        );

        // 4. 使用atan2计算yaw
        return Math.Atan2(tangentVec.Y, tangentVec.X);
    }
    else
    {
        // 直线段: 查找下一个采样点
        int nextIndex = Math.Min(frameIndex + 1, allSampledPoints.Count - 1);
        nextPos = allSampledPoints[nextIndex];

        double dx = nextPos.X - currentPos.X;
        double dy = nextPos.Y - currentPos.Y;
        double length = Math.Sqrt(dx * dx + dy * dy);

        if (length < 1e-6)
        {
            // 如果距离太小,尝试查找更远的点
            for (int offset = 2; offset < 10 && frameIndex + offset < allSampledPoints.Count; offset++)
            {
                nextPos = allSampledPoints[frameIndex + offset];
                dx = nextPos.X - currentPos.X;
                dy = nextPos.Y - currentPos.Y;
                length = Math.Sqrt(dx * dx + dy * dy);
                if (length > 1e-6) break;
            }
        }

        return length > 1e-6 ? Math.Atan2(dy, dx) : 0.0;
    }
}

/// <summary>
/// 获取圆弧旋转轴
/// </summary>
private Vector3D GetArcRotationAxis(ArcTrajectory trajectory)
{
    Vector3D startVec = (trajectory.Ts - trajectory.ArcCenter).Normalize();
    Vector3D endVec = (trajectory.Te - trajectory.ArcCenter).Normalize();

    Point3D cross = GeometryHelper.CrossProduct(
        new Point3D(startVec.X, startVec.Y, startVec.Z),
        new Point3D(endVec.X, endVec.Y, endVec.Z)
    );

    return new Vector3D(cross.X, cross.Y, cross.Z).Normalize();
}
```

### 2.4 UI集成修改

#### 修改点2: AnimationControlViewModel.ExecuteGenerateAnimation()

**当前实现**:

```csharp
// AnimationControlViewModel.cs:815
private void ExecuteGenerateAnimation()
{
    // 获取当前路径的控制点
    var pathPoints = CurrentPathRoute.Points.Select(p => p.Position).ToList();

    // 调用SetupAnimation
    _pathAnimationManager.SetupAnimation(
        _animatedObject,
        pathPoints,
        _animationDuration
    );
}
```

**修改方案**:

```csharp
private void ExecuteGenerateAnimation()
{
    if (CurrentPathRoute == null)
        throw new InvalidOperationException("未选择路径");

    LogManager.Info($"使用路径生成动画,边数:{CurrentPathRoute.Edges.Count},总长度:{CurrentPathRoute.TotalLength:F2}米");

    // 调用SetupAnimation,传入路径和用户配置的动画时长
    _pathAnimationManager.SetupAnimation(
        _animatedObject,
        _animationDuration,
        CurrentPathRoute
    );

    SetState(AnimationState.Ready);
    UpdateMainStatus($"动画已生成,时长:{_animationDuration}秒,帧率:{_animationFrameRate}fps");
}
```

### 2.4 新增字段

在PathAnimationManager中添加:

```csharp
// 路径相关
private PathRoute _route = null;      // 路径引用
```

## 三、实现步骤

### 阶段1: 数据结构准备

1. ✅ PathRoute、PathEdge、ArcTrajectory已存在
2. ✅ PathCurveEngine已实现曲线化算法
3. ⏳ 在PathAnimationManager中添加_route字段

### 阶段2: 核心算法实现

1. ⏳ 修改SetupAnimation()方法,必需传入route参数
2. ⏳ 重写PrecomputeAnimationFrames()方法,使用路径
3. ⏳ 实现曲线边插值方法(InterpolateOnStraightEdge, InterpolateOnArcEdge)
4. ⏳ 实现FindEdgeForDistance()方法
5. ⏳ 实现ComputeYawOnPath()方法(核心)
6. ⏳ 实现GetArcRotationAxis()辅助方法

### 阶段3: UI集成

1. ⏳ 修改AnimationControlViewModel.ExecuteGenerateAnimation()
2. ⏳ 添加路径验证和边数据生成
3. ⏳ 更新日志输出

### 阶段4: 测试验证

1. ⏳ 单元测试:曲线边插值准确性
2. ⏳ 单元测试:圆弧朝向计算准确性
3. ⏳ 集成测试:完整动画播放流程
4. ⏳ 性能测试:预计算时间

## 四、关键技术点

### 4.1 圆弧朝向计算原理

#### 方法1: 采样点差分法(简单但精度较低)

```csharp
// 使用前后采样点计算方向
Point3D prevPoint = allSampledPoints[Math.Max(0, frameIndex - 1)];
Point3D nextPoint = allSampledPoints[Math.Min(frameIndex + 1, allSampledPoints.Count - 1)];
Vector3D direction = (nextPoint - prevPoint).Normalize();
double yaw = Math.Atan2(direction.Y, direction.X);
```

#### 方法2: 圆弧切线法(精确)

```csharp
// 计算圆弧切线方向
Vector3D radiusVec = (currentPoint - arcCenter).Normalize();
Vector3D rotationAxis = GetArcRotationAxis(trajectory);
Vector3D tangentVec = Vector3D.CrossProduct(rotationAxis, radiusVec);
double yaw = Math.Atan2(tangentVec.Y, tangentVec.X);
```

**推荐使用方法2**,因为:

1. 数学上精确,不依赖采样密度
2. 在圆弧段上方向连续平滑
3. 性能更好,无需查找相邻采样点

### 4.2 采样点密度控制

```toml
# default_config.toml
[path_editing]
# 圆弧采样步长(米) - 推荐值:0.02-0.1
arc_sampling_step = 0.05
```

- 步长越小:动画越平滑,但内存占用越大
- 步长越大:内存占用小,但可能不够平滑
- **推荐值**:0.05米(平衡精度和性能)

### 4.3 边界情况处理

1. **单边路径**:只有一条直线边
2. **全圆弧路径**:所有边都是圆弧
3. **混合路径**:直线段和圆弧段交替
4. **零长度边**:跳过或合并
5. **采样点不足**:自动补充或调整步长

### 4.4 性能优化

1. **采样点缓存**:PathEdge.SampledPoints只生成一次
2. **边长度缓存**:edgeLengths只计算一次
3. **二分查找优化**:FindEdgeForDistance可使用二分查找
4. **并行计算**:碰撞检测可并行化(考虑线程安全)

## 五、后续优化方向

1. **贝塞尔曲线支持**: 扩展PathCurveEngine支持三次贝塞尔曲线
2. **速度曲线**: 支持在路径上设置变速点
3. **碰撞精度自适应**: 根据曲率动态调整检测密度
4. **GPU加速**: 使用CUDA加速大量碰撞检测计算

---

**文档版本**: 1.0
**创建日期**: 2026-01-05
**作者**: iFlow CLI
**状态**: 待评审
