NavisworksTransport/doc/working/动画检测曲线化集成方案_20260105.md

16 KiB

动画检测集成方案

一、现状分析

1.1 当前动画系统架构

核心组件

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

当前路径插值机制

// PathAnimationManager.cs:1253
private Point3D InterpolatePosition(double progress)
  • 基于原始控制点(_pathPoints)进行线性插值
  • 按路径段距离比例计算位置
  • 问题: 未使用PathRoute.Edges,动画沿直线段运动

当前朝向计算机制

// PathAnimationManager.cs:600
private double ComputeYawFromPath(int frameIndex, List<Point3D> framePositions)
  • 基于前后帧位置计算方向向量
  • 使用atan2计算偏航角
  • 问题: 在直线段上方向准确,但在圆弧处需要改进

1.2 路径数据结构

PathRoute模型

// 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类型

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数据

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()

当前实现:

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

修改方案:

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()

当前实现:

// 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);
        // ...
    }
}

修改方案:

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: 曲线边内插值方法

/// <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: 路径朝向计算(核心)

/// <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()

当前实现:

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

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

修改方案:

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中添加:

// 路径相关
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: 采样点差分法(简单但精度较低)

// 使用前后采样点计算方向
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: 圆弧切线法(精确)

// 计算圆弧切线方向
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 采样点密度控制

# 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 状态: 待评审