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

553 lines
16 KiB
Markdown

# 动画检测集成方案
## 一、现状分析
### 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
**状态**: 待评审