NavisworksTransport/src/Core/Animation/PathAnimationManager.cs

3791 lines
157 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Threading;
using Autodesk.Navisworks.Api;
using Autodesk.Navisworks.Api.Clash;
using NavisworksTransport.Core.Config;
using NavisworksTransport.Core.Spatial;
using NavisworksTransport.Utils;
using NavisApplication = Autodesk.Navisworks.Api.Application;
namespace NavisworksTransport.Core.Animation
{
/// <summary>
/// 定义动画播放的状态
/// </summary>
public enum AnimationState
{
Idle, // 空闲,未生成动画
Ready, // 已就绪,动画已生成但未播放
Playing, // 播放中
Paused, // 暂停
Stopped, // 已停止
Finished // 已完成
}
/// <summary>
/// 碰撞对象对,用于去重
/// </summary>
public class CollisionPair : IEquatable<CollisionPair>
{
public ModelItem AnimatedObject { get; set; }
public ModelItem CollisionObject { get; set; }
public bool Equals(CollisionPair other)
{
if (other == null) return false;
return ReferenceEquals(AnimatedObject, other.AnimatedObject) &&
ReferenceEquals(CollisionObject, other.CollisionObject);
}
public override bool Equals(object obj)
{
return Equals(obj as CollisionPair);
}
public override int GetHashCode()
{
unchecked
{
int hash = 17;
hash = hash * 31 + (AnimatedObject?.GetHashCode() ?? 0);
hash = hash * 31 + (CollisionObject?.GetHashCode() ?? 0);
return hash;
}
}
}
/// <summary>
/// 路径动画管理器 - 基于TimeLiner和动态变换实现沿路径的动画效果
/// 注意由于Navisworks API限制无法直接使用Animator API因此使用OverridePermanentTransform实现动画
/// 已集成 TimeLiner 功能,支持在 TimeLiner 中显示和管理动画任务
/// </summary>
/// <summary>
/// 动画帧数据(包含位置、朝向和碰撞信息)
/// </summary>
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<CollisionResult> Collisions { get; set; } // 该帧的碰撞结果
public bool HasCollision => Collisions?.Count > 0;
public AnimationFrame()
{
Collisions = new List<CollisionResult>();
YawRadians = 0.0;
}
}
public class PathAnimationManager
{
private static PathAnimationManager _instance;
private static readonly HashSet<string> _completedCollisionTests = new HashSet<string>(); // 记录已完成碰撞检测的动画配置
private static readonly Dictionary<string, List<AnimationFrame>> _animationFrameCache = new Dictionary<string, List<AnimationFrame>>(); // 动画帧缓存
private static readonly Dictionary<string, List<CollisionResult>> _collisionResultCache = new Dictionary<string, List<CollisionResult>>(); // 碰撞结果缓存
private ModelItem _animatedObject;
private bool _isVirtualVehicle = false; // 是否使用虚拟车辆
private double _virtualVehicleLength = 0; // 虚拟车辆长度(米)
private double _virtualVehicleWidth = 0; // 虚拟车辆宽度(米)
private double _virtualVehicleHeight = 0; // 虚拟车辆高度(米)
private List<Point3D> _pathPoints;
private List<ModelItem> _manualCollisionTargets = new List<ModelItem>();
private bool _manualCollisionOverrideEnabled = false;
// === 角度修正 ===
private double _objectRotationCorrection = 0.0; // 物体角度修正值(度,顺时针)
// === 碰撞排除列表缓存管理从LogisticsAnimationManager迁移===
private ModelItem _currentCachedAnimationObject;
private List<ModelItem> _cachedExclusionList;
private DateTime _cacheCreatedTime;
private readonly TimeSpan _cacheValidDuration = TimeSpan.FromMinutes(30); // 缓存有效期30分钟
// === 预计算动画系统 ===
private List<AnimationFrame> _animationFrames; // 所有帧数据
private int _currentFrameIndex = 0; // 当前帧索引
private List<CollisionResult> _allCollisionResults; // 所有碰撞结果(不去重)
// === Human-in-the-Loop 排除列表 ===
// 注意:排除列表是实时管理的,不与动画缓存绑定。用户选择排除哪些物体后,
// 这些物体会一直生效直到被清除或手动移除,不受动画缓存影响。
private HashSet<ModelItem> _excludedObjects = new HashSet<ModelItem>(); // 用户排除的物体列表
private bool _lastHighlightState = false; // 上一帧的高亮状态
private HashSet<ModelItem> _lastCollisionObjects = new HashSet<ModelItem>(); // 上一帧碰撞对象的集合
// === 路径相关 ===
private PathRoute _route = null; // 路径引用
// === 碰撞测试优化 ===
private string _currentAnimationHash; // 当前动画配置的哈希值
// === 动画播放机制 ===
private double _frameInterval; // 帧间隔(毫秒)
private DateTime _lastFrameTime = DateTime.MinValue; // 上一帧时间
private DispatcherTimer _animationTimer; // 备用DispatcherTimer定时器
// === 动画参数 ===
private double _animationDuration; // 动画总时长(秒)
private DateTime _animationStartTime;
private int _animationFrameRate; // 动画帧率默认30FPS
private double _collisionDetectionAccuracy; // 检测精度(内部存储:模型单位/帧)
private double _movementSpeed; // 运动速度(仅用于显示,单位:米/秒)
private double _detectionTolerance; // 检测容差从UI设置
private double _safetyMargin; // 安全间隙模型单位从CreateAnimation传入
private string _pathName; // 路径名称
private string _currentRouteId; // 当前路由ID
// === 双向播放和步进控制 ===
private int _playbackDirection = 1; // 播放方向1=正向,-1=反向
private double _playbackSpeed = 1.0; // 播放速度倍率
// === 性能监控 ===
private int _fpsFrameCount = 0;
private DateTime _fpsCounterStart = DateTime.Now;
private double _actualFPS = 0;
private Transform3D _originalTransform;
private Point3D _originalCenter; // 存储部件的原始中心位置
private Point3D _currentPosition; // 存储部件的当前位置
private AnimationState _currentState = AnimationState.Idle;
private double _pausedProgress = 0.0; // 暂停时的进度0-1之间
private double _currentYaw = 0.0; // 当前偏航角(弧度)
// TimeLiner 集成
private TimeLinerIntegrationManager _timeLinerManager;
private string _currentTaskId;
// === 调试辅助 ===
private static HashSet<int> _arcInfoPrinted = new HashSet<int>(); // 记录已打印信息的圆弧段索引
// === 物体状态保存和恢复 ===
private Point3D _savedObjectPosition;
private double _savedObjectYaw;
private bool _hasSavedObjectState = false;
/// <summary>
/// 当前正在进行动画的对象
/// </summary>
public ModelItem AnimatedObject => _animatedObject;
// --- 新增事件 ---
/// <summary>
/// 当动画状态发生改变时触发
/// </summary>
public event EventHandler<AnimationState> StateChanged;
/// <summary>
/// 当动画进度更新时触发 (0.0-100.0)
/// </summary>
public event EventHandler<double> ProgressChanged;
public PathAnimationManager()
{
_pathPoints = new List<Point3D>();
_animationFrames = new List<AnimationFrame>();
_allCollisionResults = new List<CollisionResult>();
// 🔥 从配置初始化动画参数
var config = ConfigManager.Instance.Current;
_animationFrameRate = config.Animation.FrameRate;
_animationDuration = config.Animation.DurationSeconds;
// 初始化动画模式
_frameInterval = 1000.0 / _animationFrameRate; // 计算帧间隔
_lastFrameTime = DateTime.MinValue;
_fpsCounterStart = DateTime.Now;
// 初始化备用DispatcherTimer
_animationTimer = new DispatcherTimer(DispatcherPriority.Render)
{
Interval = TimeSpan.FromMilliseconds(_frameInterval)
};
_animationTimer.Tick += OnTimerTick;
// 初始化 TimeLiner 集成
try
{
_timeLinerManager = new TimeLinerIntegrationManager();
LogManager.Info($"PathAnimationManager 初始化完成 - DispatcherTimer模式, 目标FPS: {_animationFrameRate}");
}
catch (Exception ex)
{
LogManager.Warning($"TimeLiner 集成初始化失败,将使用基础动画功能: {ex.Message}");
_timeLinerManager = null;
}
// 订阅文档状态事件
DocumentStateManager.Instance.DocumentInvalidated += OnDocumentInvalidated;
// 订阅配置变更事件
SubscribeToConfigChanges();
}
#region
/// <summary>
/// 订阅配置变更事件
/// </summary>
private void SubscribeToConfigChanges()
{
try
{
ConfigManager.Instance.CategoryConfigurationChanged += OnCategoryConfigurationChanged;
LogManager.Info("PathAnimationManager 已订阅配置变更事件");
}
catch (Exception ex)
{
LogManager.Error($"订阅配置变更事件失败: {ex.Message}", ex);
}
}
/// <summary>
/// 取消订阅配置变更事件
/// </summary>
private void UnsubscribeFromConfigChanges()
{
try
{
ConfigManager.Instance.CategoryConfigurationChanged -= OnCategoryConfigurationChanged;
LogManager.Info("PathAnimationManager 已取消订阅配置变更事件");
}
catch (Exception ex)
{
LogManager.Warning($"取消订阅配置变更事件时发生异常: {ex.Message}");
}
}
/// <summary>
/// 配置变更事件处理
/// </summary>
private void OnCategoryConfigurationChanged(object sender, Tuple<ConfigCategory, ConfigurationChangedEventArgs> args)
{
try
{
var category = args.Item1;
var eventArgs = args.Item2;
// 处理 Animation 类别变更
if (category == ConfigCategory.Animation || category == ConfigCategory.All)
{
LogManager.Info("收到 Animation 配置变更通知,正在更新动画参数...");
var config = ConfigManager.Instance.Current;
bool needReset = false;
// 检查帧率是否变更
if (_animationFrameRate != config.Animation.FrameRate)
{
_animationFrameRate = config.Animation.FrameRate;
_frameInterval = 1000.0 / _animationFrameRate;
// 更新定时器间隔
if (_animationTimer != null)
{
_animationTimer.Interval = TimeSpan.FromMilliseconds(_frameInterval);
}
needReset = true;
LogManager.Info($"动画帧率已更新: {_animationFrameRate} fps, 帧间隔: {_frameInterval:F1} ms");
}
// 检查持续时间是否变更
if (_animationDuration != config.Animation.DurationSeconds)
{
_animationDuration = config.Animation.DurationSeconds;
needReset = true;
LogManager.Info($"动画持续时间已更新: {_animationDuration} 秒");
}
// 如果动画正在运行,需要重置
if (needReset && (_currentState == AnimationState.Playing || _currentState == AnimationState.Paused))
{
// 停止当前动画,让用户重新生成
ShutdownAnimation();
LogManager.Info("动画参数已变更,当前动画已停止,请重新生成动画");
}
}
}
catch (Exception ex)
{
LogManager.Error($"处理配置变更事件失败: {ex.Message}", ex);
}
}
#endregion
/// <summary>
/// 获取单例实例
/// </summary>
public static PathAnimationManager GetInstance()
{
if (_instance == null)
{
_instance = new PathAnimationManager();
}
return _instance;
}
/// <summary>
/// 清理当前所有的动画生成结果(帧列表、碰撞结果、状态)
/// </summary>
public void ClearAnimationResults()
{
_animationFrames = null;
_allCollisionResults = null;
_arcInfoPrinted.Clear(); // 清除圆弧段信息打印记录
SetState(AnimationState.Idle);
LogManager.Info("[PathAnimationManager] 动画数据和状态已重置为 Idle");
}
/// <summary>
/// 仅仅将物理模型同步到路径起点(不清理数据,不重置状态)
/// 用于加载碰撞检测结果时
/// </summary>
public void SyncToPathStart(ModelItem item, List<Point3D> points)
{
MoveVehicleToPathStart(item, points);
}
/// <summary>
/// 将动画对象彻底恢复到 CAD 原始位置(清除所有覆盖变换)
/// 常用于模式切换或彻底清除动画干扰
/// </summary>
public void RestoreObjectToCADPosition()
{
try
{
var doc = NavisApplication.ActiveDocument;
if (doc == null) return;
// 🔥 支持虚拟车辆的恢复
ModelItem objectToRestore = null;
bool isVirtual = _isVirtualVehicle;
if (isVirtual)
{
objectToRestore = VirtualVehicleManager.Instance.CurrentVirtualVehicle;
if (objectToRestore == null)
{
LogManager.Warning("[归位] 虚拟车辆未激活,跳过恢复");
return;
}
}
else
{
objectToRestore = _animatedObject;
if (objectToRestore == null)
{
LogManager.Warning("[归位] 没有动画对象,跳过恢复");
return;
}
}
var modelItems = new ModelItemCollection { objectToRestore };
// 1. 彻底清除 Navisworks 中的所有覆盖变换,强制恢复到设计文件定义的原始状态
doc.Models.ResetPermanentTransform(modelItems);
// 2. 重置内部跟踪变量(同步到原始状态)
// 注意ResetPermanentTransform 后物体的 Transform 属性会自动变回原始值
_currentYaw = ModelItemTransformHelper.GetYawFromTransform(objectToRestore.Transform);
var originalBoundingBox = objectToRestore.BoundingBox();
_currentPosition = new Point3D(
originalBoundingBox.Center.X,
originalBoundingBox.Center.Y,
originalBoundingBox.Min.Z
);
string objectName = isVirtual ? "虚拟车辆" : objectToRestore.DisplayName;
LogManager.Info($"[归位] {objectName} 已彻底恢复到原始位置, yaw={_currentYaw:F3}");
}
catch (Exception ex)
{
LogManager.Error($"[归位] 恢复物体到原始位置失败: {ex.Message}");
}
}
/// <summary>
/// 生成动画帧
/// </summary>
/// <param name="animatedObject">要动画化的模型对象</param>
/// <param name="pathPoints">路径点列表</param>
/// <param name="durationSeconds">动画持续时间(秒)</param>
public void SetRoute(PathRoute route)
{
_route = route;
}
/// <summary>
/// 设置动画对象(批处理专用)
/// </summary>
public void SetAnimatedObject(ModelItem animatedObject)
{
_animatedObject = animatedObject;
_isVirtualVehicle = (animatedObject == null);
}
/// <summary>
/// 设置虚拟车辆参数(批处理专用)
/// </summary>
public void SetVirtualVehicleParameters(bool isVirtualVehicle, double length, double width, double height)
{
_isVirtualVehicle = isVirtualVehicle;
_virtualVehicleLength = length;
_virtualVehicleWidth = width;
_virtualVehicleHeight = height;
}
/// <summary>
/// 设置动画参数(批处理专用)
/// </summary>
public void SetAnimationParameters(int frameRate, double duration, double detectionTolerance)
{
_animationFrameRate = frameRate;
_animationDuration = duration;
// 注意:预计算使用安全间隙,不使用检测容差
}
/// <summary>
/// 检测容差(米)- 用于ClashDetective精确检测
/// </summary>
public double DetectionTolerance
{
get
{
var config = ConfigManager.Instance.Current;
return config.Animation.DetectionToleranceMeters;
}
}
/// <summary>
/// 设置手工碰撞目标(批处理专用)
/// </summary>
public void SetManualCollisionTargets(List<ModelItem> targets)
{
if (targets != null && targets.Count > 0)
{
_manualCollisionTargets = targets;
_manualCollisionOverrideEnabled = true;
}
else
{
_manualCollisionTargets = new List<ModelItem>();
_manualCollisionOverrideEnabled = false;
}
}
/// <summary>
/// 预计算动画帧(批处理专用内部方法)
/// </summary>
public List<AnimationFrame> PrecomputeAnimationFramesInternal()
{
PrecomputeAnimationFrames();
return _animationFrames;
}
public void SetupAnimation(
ModelItem animatedObject,
double durationSeconds,
PathRoute route)
{
try
{
if (route == null)
throw new ArgumentNullException(nameof(route), "必须提供路径");
_animatedObject = animatedObject;
_animationDuration = durationSeconds;
_route = route;
LogManager.Info($"使用路径,边数:{route.Edges.Count},总长度:{route.TotalLength:F2}米,动画时长:{durationSeconds:F1}秒");
// 计算动画配置哈希(基于路径边,可以区分不同的曲线化路径)
_currentAnimationHash = ComputeAnimationConfigHashFromRoute(animatedObject, route);
LogManager.Info($"动画配置哈希: {_currentAnimationHash}");
// 检查动画帧缓存
if (_animationFrameCache.TryGetValue(_currentAnimationHash, out var cachedFrames))
{
_animationFrames = cachedFrames;
// 同时加载缓存的碰撞结果
if (_collisionResultCache.TryGetValue(_currentAnimationHash, out var cachedCollisions))
{
_allCollisionResults = cachedCollisions;
LogManager.Info($"使用缓存的动画帧和碰撞结果,帧数: {_animationFrames.Count}, 碰撞数: {_allCollisionResults.Count}");
}
else
{
_allCollisionResults = new List<CollisionResult>();
LogManager.Info($"使用缓存的动画帧,但碰撞结果缓存缺失,帧数: {_animationFrames.Count}");
}
}
else
{
// 缓存未命中,重新计算动画帧
LogManager.Info("缓存未命中,重新计算动画帧和碰撞结果");
PrecomputeAnimationFrames();
// 缓存动画帧和碰撞结果
if (_animationFrames != null && _animationFrames.Count > 0)
{
_animationFrameCache[_currentAnimationHash] = _animationFrames;
if (_allCollisionResults != null)
{
_collisionResultCache[_currentAnimationHash] = _allCollisionResults;
}
LogManager.Info($"动画帧和碰撞结果已缓存,帧数: {_animationFrames.Count}, 碰撞数: {_allCollisionResults?.Count ?? 0}");
}
}
// 验证预计算结果
if (_animationFrames == null || _animationFrames.Count == 0)
{
throw new InvalidOperationException("动画帧预计算失败,无法创建动画");
}
LogManager.Info($"动画设置完成:对象={_animatedObject.DisplayName}, 时长={_animationDuration}秒");
}
catch (Exception ex)
{
LogManager.Error($"设置动画失败: {ex.Message}");
throw;
}
}
/// <summary>
/// 将车辆移动到路径起点
/// </summary>
/// <param name="animatedObject">动画对象可选如果不提供则使用当前的_animatedObject</param>
/// <param name="pathPoints">路径点可选如果不提供则使用当前的_pathPoints</param>
public void MoveVehicleToPathStart(ModelItem animatedObject = null, List<Point3D> pathPoints = null)
{
try
{
// 🔥 重要先恢复物体到原始状态CAD位置和原始朝向确保每次计算都从相同的状态开始
// 这样可以避免之前旋转导致的包围盒尺寸计算不准确的问题
// 注意也要处理虚拟车辆_isVirtualVehicle为true时_animatedObject可能为null
if (_animatedObject != null || _isVirtualVehicle)
{
RestoreObjectToCADPosition();
LogManager.Info($"[移动到起点] 已恢复物体到原始状态, _currentYaw={_currentYaw * 180 / Math.PI:F2}°");
}
// 如果提供了参数,更新内部状态
if (animatedObject != null)
{
_animatedObject = animatedObject;
_originalTransform = animatedObject.Transform;
_originalCenter = animatedObject.BoundingBox().Center;
_currentPosition = new Point3D(_originalCenter.X, _originalCenter.Y, animatedObject.BoundingBox().Min.Z);
// 统一逻辑:从当前 Transform 中提取实际朝向(无论虚拟车辆还是普通物体)
_currentYaw = ModelItemTransformHelper.GetYawFromTransform(_originalTransform);
}
if (pathPoints != null)
{
_pathPoints = pathPoints;
}
// 检查路径点
if (_pathPoints == null || _pathPoints.Count < 2)
{
LogManager.Warning("[移动到起点] 没有可用的路径点");
return;
}
// 计算朝向(使用前两个路径点的方向)
double pathDirectionYaw = Math.Atan2(_pathPoints[1].Y - _pathPoints[0].Y, _pathPoints[1].X - _pathPoints[0].X);
double yaw;
LogManager.Info($"[移动到起点] 路径方向yaw: {pathDirectionYaw * 180 / Math.PI:F2}度, 角度修正: {_objectRotationCorrection:F1}度");
// 根据路径类型调整朝向
if (_route?.PathType == PathType.Hoisting)
{
// 吊装路径:使用水平吊运方向(与通行空间方向一致)
// 从第2个路径点提升点到第3个路径点平移终点的水平方向
yaw = Math.Atan2(_pathPoints[2].Y - _pathPoints[1].Y, _pathPoints[2].X - _pathPoints[1].X);
LogManager.Debug($"[移动到起点] 吊装路径使用水平吊运方向: {yaw * 180 / Math.PI:F2}度");
}
else
{
// 地面路径和空轨路径:直接使用路径方向
yaw = pathDirectionYaw;
LogManager.Debug($"[移动到起点] 地面/空轨路径使用路径方向: {yaw * 180 / Math.PI:F2}度");
}
// 应用角度修正值(顺时针,转换为弧度)
if (_objectRotationCorrection != 0.0)
{
double correctionRad = _objectRotationCorrection * Math.PI / 180.0;
yaw += correctionRad;
LogManager.Debug($"[移动到起点] 应用角度修正: {_objectRotationCorrection:F1}°, 修正后yaw: {yaw * 180 / Math.PI:F2}度");
}
// 根据路径类型调整起点位置
Point3D startPosition = _pathPoints[0];
if (_route?.PathType == PathType.Hoisting)
{
// 吊装路径:第一个路径点(起吊点)是地面位置,物体底面应该在这里
// 不需要向下移动车辆高度
LogManager.Debug($"[移动到起点] 吊装路径起吊点是地面位置物体底面Z={startPosition.Z:F2}");
}
else if (_route?.PathType == PathType.Rail)
{
// 空轨路径:路径点是悬挂点,物体悬挂在下方
double vehicleHeight = _animatedObject.BoundingBox().Max.Z - _animatedObject.BoundingBox().Min.Z;
startPosition = new Point3D(startPosition.X, startPosition.Y, startPosition.Z - vehicleHeight);
LogManager.Debug($"[移动到起点] 空轨路径调整: 悬挂点Z={_pathPoints[0].Z:F2}, 物体底面Z={startPosition.Z:F2}, 物体高度={vehicleHeight:F2}");
}
else
{
// 地面路径points[0] 是地面位置,车辆底面应该在这里
LogManager.Debug($"[移动到起点] 地面路径: pos=({startPosition.X:F2},{startPosition.Y:F2},{startPosition.Z:F2})");
}
// 使用 UpdateObjectPosition 统一处理移动和旋转
UpdateObjectPosition(startPosition, yaw);
string pathTypeName;
if (_route?.PathType == PathType.Rail)
{
pathTypeName = "空轨";
}
else if (_route?.PathType == PathType.Hoisting)
{
pathTypeName = "吊装";
}
else
{
pathTypeName = "地面";
}
LogManager.Info($"物体已初始化到路径起点并对齐朝向: pos=({startPosition.X:F2},{startPosition.Y:F2},{startPosition.Z:F2}), yaw={yaw:F3}rad, 路径类型={pathTypeName}");
// 打印实际物体的位置和方向
if (_animatedObject != null)
{
var bbox = _animatedObject.BoundingBox();
var actualCenter = bbox.Center;
var actualYaw = ModelItemTransformHelper.GetYawFromTransform(_animatedObject.Transform);
LogManager.Info($"实际物体位置: X={actualCenter.X:F2}, Y={actualCenter.Y:F2}, Z={actualCenter.Z:F2}");
LogManager.Info($"实际物体朝向: {actualYaw * 180 / Math.PI:F2}度");
}
else if (_isVirtualVehicle)
{
var virtualVehicle = VirtualVehicleManager.Instance.CurrentVirtualVehicle;
if (virtualVehicle != null)
{
var bbox = virtualVehicle.BoundingBox();
var actualCenter = bbox.Center;
var actualYaw = ModelItemTransformHelper.GetYawFromTransform(virtualVehicle.Transform);
LogManager.Info($"虚拟车辆位置: X={actualCenter.X:F2}, Y={actualCenter.Y:F2}, Z={actualCenter.Z:F2}");
LogManager.Info($"虚拟车辆朝向: {actualYaw * 180 / Math.PI:F2}度");
}
}
}
catch (Exception ex)
{
LogManager.Error($"初始化物体到路径起点失败: {ex.Message}");
}
}
/// <summary>
/// 预计算所有动画帧和碰撞信息
/// </summary>
private void PrecomputeAnimationFrames()
{
try
{
LogManager.Info("=== 使用路径预计算动画帧 ===");
// 🔥 重要:预计算前先将物体移动到起点位置
if (_animatedObject != null && _route != null && _route.Points != null && _route.Points.Count > 0)
{
var pathPoints = _route.Points.Select(p => p.Position).ToList();
MoveVehicleToPathStart(_animatedObject, pathPoints);
LogManager.Info("[预计算] 物体已移动到路径起点");
}
int totalFrames = (int)(_animationDuration * _animationFrameRate);
// 🔥 判断路径类型空中路径使用Points地面路径使用Edges
bool isAerialPath = _route.PathType == PathType.Rail || _route.PathType == PathType.Hoisting;
List<Point3D> allSampledPoints = new List<Point3D>();
List<double> segmentLengths = new List<double>();
if (isAerialPath)
{
// === 空中路径:直接使用路径点进行线性插值 ===
string aerialSubTypeName = _route.PathType == PathType.Rail ? "空轨" : "吊装";
LogManager.Info($"[{aerialSubTypeName}路径] 使用路径点进行线性插值,路径点数: {_route.Points.Count}");
// 将路径点转换为Point3D列表
foreach (var point in _route.Points)
{
allSampledPoints.Add(point.Position);
}
// 计算每段长度(内部计算统一使用模型单位)
for (int i = 0; i < allSampledPoints.Count - 1; i++)
{
double lengthInModelUnits = GeometryHelper.CalculatePointDistance(allSampledPoints[i], allSampledPoints[i + 1]);
segmentLengths.Add(lengthInModelUnits);
}
}
else
{
// === 地面路径使用Edges进行曲线化插值 ===
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);
segmentLengths.Add(edge.PhysicalLength);
}
}
// 🔥 内部计算统一使用模型单位
double metersToModelUnits = UnitsConverter.GetMetersToUnitsConversionFactor();
double totalLengthInModelUnits = _route.TotalLength * metersToModelUnits;
string pathTypeName;
if (_route.PathType == PathType.Rail)
{
pathTypeName = "空轨";
}
else if (_route.PathType == PathType.Hoisting)
{
pathTypeName = "吊装";
}
else
{
pathTypeName = "地面";
}
LogManager.Info($"路径总长度: {totalLengthInModelUnits / metersToModelUnits:F2}米, 采样点数: {allSampledPoints.Count}, 路径类型: {pathTypeName}");
// 2. 按帧数采样生成动画帧
_animationFrames = new List<AnimationFrame>();
_allCollisionResults = new List<CollisionResult>();
_currentFrameIndex = 0;
// 获取动画对象的包围盒信息
var originalBoundingBox = _animatedObject.BoundingBox();
var boundingBoxSize = new Vector3D(
originalBoundingBox.Max.X - originalBoundingBox.Min.X,
originalBoundingBox.Max.Y - originalBoundingBox.Min.Y,
originalBoundingBox.Max.Z - originalBoundingBox.Min.Z
);
// 空间索引设置
SpatialIndexManager spatialIndexManager = null;
var manualTargetsSnapshot = new List<ModelItem>();
bool manualOverrideActive = _manualCollisionOverrideEnabled && _manualCollisionTargets != null && _manualCollisionTargets.Count > 0;
// 获取配置
var config = ConfigManager.Instance.Current;
if (manualOverrideActive)
{
manualTargetsSnapshot = _manualCollisionTargets
.Where(item => item != null && ModelItemAnalysisHelper.IsModelItemValid(item) && !ReferenceEquals(item, _animatedObject))
.Distinct()
.ToList();
if (manualTargetsSnapshot.Count == 0)
{
manualOverrideActive = false;
LogManager.Warning("[手工碰撞] 手工碰撞对象列表为空或无效,自动回退到空间索引模式");
}
else
{
LogManager.Info($"[手工碰撞] 启用手工碰撞对象模式,数量: {manualTargetsSnapshot.Count},跳过空间索引构建");
}
}
if (!manualOverrideActive)
{
LogManager.Info("=== 构建全局空间索引 ===");
spatialIndexManager = SpatialIndexManager.Instance;
// 使用动画专用的空间索引格子大小配置 - 使用模型单位接口
double cellSizeInModelUnits = config.Animation.SpatialIndexCellSize;
LogManager.Info($"[空间索引] 格子大小: {cellSizeInModelUnits:F2}模型单位 (动画配置)");
// 检查空间索引是否需要重新构建(格子大小变化或未初始化)
bool needRebuild = !spatialIndexManager.IsInitialized ||
Math.Abs(spatialIndexManager.CellSize - cellSizeInModelUnits) > 0.0001;
if (needRebuild)
{
LogManager.Info("[空间索引] 空间索引未初始化或格子大小已改变,开始重新构建...");
spatialIndexManager.BuildGlobalIndex(cellSizeInModelUnits);
}
else
{
LogManager.Info("[空间索引] 使用已缓存的空间索引");
}
LogManager.Info(spatialIndexManager.GetStatistics());
// 🔥 优化使用AABB查询代替球形查询查询范围更精确
// 不再预计算球形半径改为每帧计算车辆AABB + 安全间隙的搜索AABB
LogManager.Info($"空间查询使用AABB模式安全间隙: {_safetyMargin:F4}模型单位");
}
// 3. 生成每一帧
double distancePerFrameInModelUnits = totalLengthInModelUnits / totalFrames;
LogManager.Info($"总帧数: {totalFrames}, 总长度: {totalLengthInModelUnits / metersToModelUnits:F2}米, 每帧移动距离: {distancePerFrameInModelUnits / metersToModelUnits:F4}米");
for (int i = 0; i < totalFrames; i++)
{
double targetDistance = i * distancePerFrameInModelUnits; // 按距离采样(模型单位),不是按进度
Point3D framePosition;
double yawRadians;
if (isAerialPath)
{
// === 空中路径:在路径点之间进行线性插值 ===
int segmentIndex = FindSegmentForDistance(targetDistance, segmentLengths);
if (segmentIndex < 0 || segmentIndex >= segmentLengths.Count)
{
string subTypeName = _route.PathType == PathType.Rail ? "空轨" : "吊装";
LogManager.Warning($"[{subTypeName}路径] 无法找到线段,目标距离:{targetDistance / metersToModelUnits:F2}米");
continue;
}
double accumulatedLength = segmentLengths.Take(segmentIndex).Sum();
double segmentProgress = (targetDistance - accumulatedLength) / segmentLengths[segmentIndex];
// 线性插值
Point3D p1 = allSampledPoints[segmentIndex];
Point3D p2 = allSampledPoints[segmentIndex + 1];
framePosition = new Point3D(
p1.X + segmentProgress * (p2.X - p1.X),
p1.Y + segmentProgress * (p2.Y - p1.Y),
p1.Z + segmentProgress * (p2.Z - p1.Z)
);
// 计算朝向(使用当前线段的方向)
yawRadians = Math.Atan2(p2.Y - p1.Y, p2.X - p1.X);
// 吊装路径所有线段都使用水平吊运方向与MoveVehicleToPathStart保持一致
if (_route.PathType == PathType.Hoisting)
{
yawRadians = Math.Atan2(_pathPoints[2].Y - _pathPoints[1].Y, _pathPoints[2].X - _pathPoints[1].X);
}
// 🔥 空中路径:根据路径类型和线段索引调整物体位置
double vehicleHeight = _animatedObject.BoundingBox().Max.Z - _animatedObject.BoundingBox().Min.Z;
if (_route.PathType == PathType.Hoisting)
{
// 吊装路径:支持动态数量的路径点
// 路径点结构:
// 点0起吊点地面位置
// 点1提升点悬挂点
// 点2...点N-2中间悬挂点用户添加的转向点
// 点N-1下降点悬挂点
// 点N落地点地面位置
//
// 线段结构:
// 线段0起吊段地面→悬挂点- 垂直
// 线段1...线段N-2平移段悬挂点→悬挂点- 水平
// 线段N-1下降段悬挂点→地面- 垂直
int totalSegments = _pathPoints.Count - 1;
int firstSegment = 0; // 起吊段
int lastSegment = totalSegments - 1; // 下降段
if (segmentIndex == firstSegment)
{
// 起吊段:从地面逐渐上升到悬挂点-车辆高度
// 进度0时地面物体底面在地面不向下移动
// 进度1时悬挂点物体顶面在悬挂点物体底面=悬挂点-车辆高度
double heightOffset = segmentProgress * vehicleHeight;
framePosition = new Point3D(framePosition.X, framePosition.Y, framePosition.Z - heightOffset);
LogManager.Debug($"[吊装路径-起吊段] 进度={segmentProgress:F2}, 高度偏移={heightOffset:F2}, 物体底面Z={framePosition.Z:F2}");
}
else if (segmentIndex > firstSegment && segmentIndex < lastSegment)
{
// 平移段(中间的所有线段):全程都是悬挂点,物体顶面在悬挂点
framePosition = new Point3D(framePosition.X, framePosition.Y, framePosition.Z - vehicleHeight);
LogManager.Debug($"[吊装路径-平移段] 线段{segmentIndex}, 进度={segmentProgress:F2}, 物体底面Z={framePosition.Z:F2}");
}
else if (segmentIndex == lastSegment)
{
// 下降段:从悬挂点-车辆高度逐渐下降到地面
// 进度0时悬挂点物体顶面在悬挂点物体底面=悬挂点-车辆高度
// 进度1时地面物体底面在地面不向下移动
double heightOffset = (1 - segmentProgress) * vehicleHeight;
framePosition = new Point3D(framePosition.X, framePosition.Y, framePosition.Z - heightOffset);
LogManager.Debug($"[吊装路径-下降段] 进度={segmentProgress:F2}, 高度偏移={heightOffset:F2}, 物体底面Z={framePosition.Z:F2}");
}
}
else if (_route.PathType == PathType.Rail)
{
// 空轨路径:路径点是悬挂点,物体悬挂在下方
// 物体顶面应该在路径点,所以物体底面 = 路径点 - 物体高度
framePosition = new Point3D(framePosition.X, framePosition.Y, framePosition.Z - vehicleHeight);
LogManager.Debug($"[空轨路径] 调整物体位置: 悬挂点Z={framePosition.Z + vehicleHeight:F2}, 物体底面Z={framePosition.Z:F2}");
}
}
else
{
// === 地面路径:在边内插值 ===
int edgeIndex = FindEdgeForDistance(targetDistance, segmentLengths);
if (edgeIndex < 0 || edgeIndex >= _route.Edges.Count)
{
LogManager.Warning($"无法找到边,目标距离:{targetDistance / metersToModelUnits:F2}米");
continue;
}
var edge = _route.Edges[edgeIndex];
double accumulatedLength = segmentLengths.Take(edgeIndex).Sum();
double edgeProgress = (targetDistance - accumulatedLength) / edge.PhysicalLength;
// 在边内插值位置
if (edge.SegmentType == PathSegmentType.Straight)
{
framePosition = InterpolateOnStraightEdge(edge, edgeProgress);
}
else // Arc
{
framePosition = InterpolateOnArcEdge(edge, edgeProgress);
}
yawRadians = ComputeYawOnPath(i, allSampledPoints, edgeIndex, edgeProgress);
}
// 创建帧并检测碰撞
var frame = new AnimationFrame
{
Index = i,
Progress = (double)i / (totalFrames - 1),
Position = framePosition,
YawRadians = yawRadians,
Collisions = new List<CollisionResult>()
};
// 记录第一帧的角度(用于调试)
if (i == 0)
{
LogManager.Debug($"[预计算] 第0帧: pos=({framePosition.X:F2},{framePosition.Y:F2}), yaw={yawRadians * 180 / Math.PI:F2}度");
}
// 虚拟碰撞检测
// 计算车辆包围盒相对于framePosition的偏移
var currentVehicleBoundingBox = _animatedObject.BoundingBox();
var originalCenter = new Point3D(
(currentVehicleBoundingBox.Min.X + currentVehicleBoundingBox.Max.X) / 2,
(currentVehicleBoundingBox.Min.Y + currentVehicleBoundingBox.Max.Y) / 2,
(currentVehicleBoundingBox.Min.Z + currentVehicleBoundingBox.Max.Z) / 2
);
// 计算偏移量当前包围盒中心到framePosition的偏移
var offsetX = framePosition.X - originalCenter.X;
var offsetY = framePosition.Y - originalCenter.Y;
var offsetZ = framePosition.Z - currentVehicleBoundingBox.Min.Z; // Z方向framePosition是底面originalCenter是中心
// 创建新的包围盒将当前包围盒移动到framePosition
var virtualBoundingBox = new BoundingBox3D(
new Point3D(
currentVehicleBoundingBox.Min.X + offsetX,
currentVehicleBoundingBox.Min.Y + offsetY,
framePosition.Z // 底面Z坐标
),
new Point3D(
currentVehicleBoundingBox.Max.X + offsetX,
currentVehicleBoundingBox.Max.Y + offsetY,
framePosition.Z + (currentVehicleBoundingBox.Max.Z - currentVehicleBoundingBox.Min.Z) // 保持高度
)
);
IEnumerable<ModelItem> nearbyObjects;
if (manualOverrideActive)
{
// 🔥 Human-in-the-Loop: 手工模式下也要应用排除列表
nearbyObjects = manualTargetsSnapshot;
if (_excludedObjects.Count > 0)
{
nearbyObjects = nearbyObjects.Where(obj => !_excludedObjects.Contains(obj));
}
}
else
{
// 🔥 优化使用AABB查询代替球形查询避免球体扩大的无效范围
// 查询AABB = 车辆AABB + 安全间隙扩展
var searchBounds = new BoundingBox3D(
new Point3D(
virtualBoundingBox.Min.X - _safetyMargin,
virtualBoundingBox.Min.Y - _safetyMargin,
virtualBoundingBox.Min.Z - _safetyMargin
),
new Point3D(
virtualBoundingBox.Max.X + _safetyMargin,
virtualBoundingBox.Max.Y + _safetyMargin,
virtualBoundingBox.Max.Z + _safetyMargin
)
);
nearbyObjects = spatialIndexManager.FindInAABB(
searchBounds,
excludeObject: _animatedObject
);
// 🔥 Human-in-the-Loop: 应用用户排除列表过滤
if (_excludedObjects.Count > 0)
{
nearbyObjects = nearbyObjects.Where(obj => !_excludedObjects.Contains(obj));
}
}
foreach (var collider in nearbyObjects)
{
var colliderBox = collider.BoundingBox();
// 使用包围盒距离检测方法
double distance = BoundingBoxGeometryUtils.CalculateDistance(virtualBoundingBox, colliderBox);
// _safetyMargin 已经是模型单位在CreateAnimation中已转换直接使用
bool intersects = distance <= _safetyMargin;
if (intersects)
{
LogManager.Debug($"帧 {i} 检测到碰撞: {_animatedObject.DisplayName} <-> {collider.DisplayName}, 距离: {distance:F4},阈值: {_safetyMargin:F4}");
LogManager.Debug($"移动物体位置: {framePosition.X:F2},{framePosition.Y:F2},{framePosition.Z:F2}");
LogManager.Debug($"被撞物体位置:{GetObjectPosition(collider).X:F2},{GetObjectPosition(collider).Y:F2},{GetObjectPosition(collider).Z:F2}");
var collisionResult = new CollisionResult
{
ClashGuid = Guid.NewGuid(),
DisplayName = $"预计算碰撞: {_animatedObject.DisplayName} <-> {collider.DisplayName}",
Status = ClashResultStatus.New,
Item1 = _animatedObject,
Item2 = collider,
CreatedTime = DateTime.Now,
Distance = BoundingBoxGeometryUtils.CalculateCenterDistance(virtualBoundingBox, colliderBox),
Center = new Point3D(
(virtualBoundingBox.Min.X + virtualBoundingBox.Max.X + colliderBox.Min.X + colliderBox.Max.X) / 4,
(virtualBoundingBox.Min.Y + virtualBoundingBox.Max.Y + colliderBox.Min.Y + colliderBox.Max.Y) / 4,
(virtualBoundingBox.Min.Z + virtualBoundingBox.Max.Z + colliderBox.Min.Z + colliderBox.Max.Z) / 4
),
// 🔥 重要:记录虚拟车辆的包围盒中心,而不是底面中心
// 原因:
// 1. framePosition是路径点代表车辆在地面上的位置底面中心
// 2. ClashDetective移动逻辑使用包围盒中心进行定位
// 3. 如果记录底面中心ClashDetective会把包围盒中心移动到底面位置导致车辆下沉半个高度
// 4. 记录包围盒中心可以确保ClashDetective移动后的位置与预计算时的虚拟车辆位置一致
Item1Position = new Point3D(
framePosition.X,
framePosition.Y,
framePosition.Z + (virtualBoundingBox.Max.Z - virtualBoundingBox.Min.Z) / 2
),
Item2Position = GetObjectPosition(collider),
Item1YawRadians = yawRadians, // 记录运动物体朝向
HasPositionInfo = true
};
frame.Collisions.Add(collisionResult);
_allCollisionResults.Add(collisionResult);
}
}
_animationFrames.Add(frame);
}
// 统计碰撞信息
var framesWithCollision = _animationFrames.Count(f => f.HasCollision);
var totalCollisions = _animationFrames.Sum(f => f.Collisions.Count);
// 检查碰撞结果是否包含位置信息
var collisionsWithPosition = _allCollisionResults.Count(c => c.HasPositionInfo && c.Item1Position != null);
LogManager.Info($"=== 预计算完成 ===");
LogManager.Info($"总帧数: {_animationFrames.Count}");
LogManager.Info($"包含碰撞的帧: {framesWithCollision}");
LogManager.Info($"总碰撞次数: {totalCollisions}");
LogManager.Info($"记录的碰撞结果总数: {_allCollisionResults.Count} 个");
LogManager.Info($"包含位置信息的碰撞: {collisionsWithPosition}/{_allCollisionResults.Count}");
// 🔥 Human-in-the-Loop: 记录排除统计
if (_excludedObjects.Count > 0)
{
LogManager.Info($"[排除列表] 本次预计算排除了 {_excludedObjects.Count} 个物体");
}
// 🔥 清除移动物体集合
ClashDetectiveIntegration.ClearAnimatedObject();
}
catch (Exception ex)
{
LogManager.Error($"预计算动画帧失败: {ex.Message}");
_animationFrames = new List<AnimationFrame>();
}
}
/// <summary>
/// 根据路径计算指定帧的yaw角度
/// </summary>
/// <summary>
/// 计算路径上的朝向(改进版)
/// </summary>
private double ComputeYawOnPath(
int frameIndex,
List<Point3D> allSampledPoints,
int currentEdgeIndex,
double edgeProgress)
{
var edge = _route.Edges[currentEdgeIndex];
if (edge.SegmentType == PathSegmentType.Arc && edge.Trajectory != null)
{
// 圆弧段: 使用切线法计算精确朝向
Vector3D startVec = (edge.Trajectory.Ts - edge.Trajectory.ArcCenter).Normalize();
double signedAngle = edge.Trajectory.DeflectionAngle;
double currentAngle = edgeProgress * signedAngle;
// 计算切点Ts的朝向进入切点的切线方向
Vector3D tsRadiusVec = (edge.Trajectory.Ts - edge.Trajectory.ArcCenter).Normalize();
Vector3D rotationAxis = GetArcRotationAxis(edge.Trajectory);
Point3D tsTangent;
if (signedAngle >= 0)
{
tsTangent = GeometryHelper.CrossProduct(
new Point3D(rotationAxis.X, rotationAxis.Y, rotationAxis.Z),
new Point3D(tsRadiusVec.X, tsRadiusVec.Y, tsRadiusVec.Z)
);
}
else
{
tsTangent = GeometryHelper.CrossProduct(
new Point3D(tsRadiusVec.X, tsRadiusVec.Y, tsRadiusVec.Z),
new Point3D(rotationAxis.X, rotationAxis.Y, rotationAxis.Z)
);
}
double tsYaw = Math.Atan2(tsTangent.Y, tsTangent.X);
// 计算切点Te的朝向退出切点的切线方向
Vector3D teRadiusVec = (edge.Trajectory.Te - edge.Trajectory.ArcCenter).Normalize();
Point3D teTangent;
if (signedAngle >= 0)
{
teTangent = GeometryHelper.CrossProduct(
new Point3D(rotationAxis.X, rotationAxis.Y, rotationAxis.Z),
new Point3D(teRadiusVec.X, teRadiusVec.Y, teRadiusVec.Z)
);
}
else
{
teTangent = GeometryHelper.CrossProduct(
new Point3D(teRadiusVec.X, teRadiusVec.Y, teRadiusVec.Z),
new Point3D(rotationAxis.X, rotationAxis.Y, rotationAxis.Z)
);
}
double teYaw = Math.Atan2(teTangent.Y, teTangent.X);
// 计算当前帧的朝向
Point3D rotatedPoint = GeometryHelper.RotatePointAroundAxis(
edge.Trajectory.Ts,
edge.Trajectory.ArcCenter,
rotationAxis,
currentAngle
);
Vector3D radiusVec = (rotatedPoint - edge.Trajectory.ArcCenter).Normalize();
// 计算切线方向:根据旋转方向确定切线顺序
Point3D tangentPoint;
if (signedAngle >= 0)
{
tangentPoint = GeometryHelper.CrossProduct(
new Point3D(rotationAxis.X, rotationAxis.Y, rotationAxis.Z),
new Point3D(radiusVec.X, radiusVec.Y, radiusVec.Z)
);
}
else
{
tangentPoint = GeometryHelper.CrossProduct(
new Point3D(radiusVec.X, radiusVec.Y, radiusVec.Z),
new Point3D(rotationAxis.X, rotationAxis.Y, rotationAxis.Z)
);
}
double currentYaw = Math.Atan2(tangentPoint.Y, tangentPoint.X);
double yawChange = teYaw - tsYaw;
// 计算圆弧段前后直线段的朝向
double prevStraightYaw = 0;
double nextStraightYaw = 0;
// 圆弧段前的直线段朝向
if (currentEdgeIndex > 0 && _route.Edges[currentEdgeIndex - 1].SegmentType == PathSegmentType.Straight)
{
var prevEdge = _route.Edges[currentEdgeIndex - 1];
if (prevEdge.SampledPoints != null && prevEdge.SampledPoints.Count >= 2)
{
Point3D p1 = prevEdge.SampledPoints[0];
Point3D p2 = prevEdge.SampledPoints[prevEdge.SampledPoints.Count - 1];
prevStraightYaw = Math.Atan2(p2.Y - p1.Y, p2.X - p1.X);
}
}
// 圆弧段后的直线段朝向
if (currentEdgeIndex < _route.Edges.Count - 1 && _route.Edges[currentEdgeIndex + 1].SegmentType == PathSegmentType.Straight)
{
var nextEdge = _route.Edges[currentEdgeIndex + 1];
if (nextEdge.SampledPoints != null && nextEdge.SampledPoints.Count >= 2)
{
Point3D p1 = nextEdge.SampledPoints[0];
Point3D p2 = nextEdge.SampledPoints[nextEdge.SampledPoints.Count - 1];
nextStraightYaw = Math.Atan2(p2.Y - p1.Y, p2.X - p1.X);
}
}
// 在第一帧打印完整信息(使用静态变量确保每个圆弧段只打印一次)
if (!_arcInfoPrinted.Contains(currentEdgeIndex))
{
LogManager.Debug($"圆弧段[{currentEdgeIndex}]朝向信息:");
LogManager.Debug($" 前直线段朝向: {prevStraightYaw * 180.0 / Math.PI:F2}度");
LogManager.Debug($" 进入切点(Ts)朝向: {tsYaw * 180.0 / Math.PI:F2}度");
LogManager.Debug($" 圆弧偏转角度: {signedAngle * 180.0 / Math.PI:F2}度");
LogManager.Debug($" 退出切点(Te)朝向: {teYaw * 180.0 / Math.PI:F2}度");
LogManager.Debug($" 后直线段朝向: {nextStraightYaw * 180.0 / Math.PI:F2}度");
LogManager.Debug($" 前后直线段朝向差: {(nextStraightYaw - prevStraightYaw) * 180.0 / Math.PI:F2}度");
LogManager.Debug($" 圆弧段朝向变化: {yawChange * 180.0 / Math.PI:F2}度");
_arcInfoPrinted.Add(currentEdgeIndex);
}
// 每一帧都打印当前朝向
// LogManager.Debug($"帧[{frameIndex}] 圆弧段[{currentEdgeIndex}]: 进度={edgeProgress:F4}, 当前朝向={currentYaw * 180.0 / Math.PI:F2}度");
return currentYaw;
}
else
{
// 直线段: 使用直线方向向量,避免边界处的前后帧差分问题
Point3D startPos = edge.SampledPoints[0];
Point3D endPos = edge.SampledPoints[edge.SampledPoints.Count - 1];
double dx = endPos.X - startPos.X;
double dy = endPos.Y - startPos.Y;
double length = Math.Sqrt(dx * dx + dy * dy);
if (length < 1e-6)
{
// 直线段长度太短,尝试使用前后帧差分
Point3D currentPos = allSampledPoints.Count > 0 ? allSampledPoints[Math.Min(frameIndex, allSampledPoints.Count - 1)] : new Point3D();
int nextIndex = Math.Min(frameIndex + 1, allSampledPoints.Count - 1);
Point3D nextPos = allSampledPoints[nextIndex];
dx = nextPos.X - currentPos.X;
dy = nextPos.Y - currentPos.Y;
length = Math.Sqrt(dx * dx + dy * dy);
if (length < 1e-6)
{
return 0.0;
}
}
return Math.Atan2(dy, dx);
}
}
/// <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();
}
/// <summary>
/// 开始播放动画
/// </summary>
public void StartAnimation()
{
try
{
if (_animatedObject == null || _pathPoints.Count < 2)
{
throw new InvalidOperationException("请先调用SetupAnimation设置动画参数");
}
// 🔥 关键:动画开始前确保物体在起点位置和朝向
// 动画结束时物体在终点,重新播放前必须重置
if (_animationFrames != null && _animationFrames.Count > 0)
{
var firstFrame = _animationFrames[0];
var currentPos = _animatedObject.BoundingBox().Center;
var distanceToStart = Math.Sqrt(
Math.Pow(currentPos.X - firstFrame.Position.X, 2) +
Math.Pow(currentPos.Y - firstFrame.Position.Y, 2)
);
// 如果距离起点超过0.1米,或者朝向不一致,需要重置
var yawDiff = Math.Abs(_currentYaw - firstFrame.YawRadians);
if (yawDiff > Math.PI) yawDiff = 2 * Math.PI - yawDiff; // 处理角度环绕
if (distanceToStart > 0.1 || yawDiff > 0.01)
{
LogManager.Info($"[动画开始] 物体不在起点,重置到起点: 距离={distanceToStart:F2}m, 角度差={yawDiff * 180 / Math.PI:F2}°");
MoveVehicleToPathStart();
}
}
// 创建 TimeLiner 任务(如果可用且未创建过)
if (_timeLinerManager != null && _timeLinerManager.IsTimeLinerAvailable)
{
string objectName = "UnknownObject";
try
{
objectName = _animatedObject?.DisplayName ?? "UnknownObject";
}
catch (Exception)
{
objectName = "DisposedObject";
}
// 使用动画配置hash作为任务标识
var hashPrefix = _currentAnimationHash.Substring(0, Math.Min(8, _currentAnimationHash.Length));
var taskName = $"{objectName}_运输_{hashPrefix}";
// 检查是否已存在相同配置的任务
var existingTaskId = _timeLinerManager.FindTaskByName(taskName);
if (!string.IsNullOrEmpty(existingTaskId))
{
_currentTaskId = existingTaskId;
LogManager.Info($"复用现有 TimeLiner 任务: {taskName} (ID: {_currentTaskId})");
}
else
{
// 创建新任务
var duration = TimeSpan.FromSeconds(_animationDuration);
LogManager.Info($"创建 TimeLiner 任务: {taskName}");
_currentTaskId = _timeLinerManager.CreateTransportTask(
taskName,
_pathPoints,
duration,
_animatedObject);
if (!string.IsNullOrEmpty(_currentTaskId))
{
LogManager.Info($"✓ TimeLiner 任务创建成功: {taskName} (ID: {_currentTaskId})");
}
else
{
LogManager.Warning("TimeLiner 任务创建失败,继续使用基础动画功能");
}
}
}
else
{
LogManager.Info("TimeLiner 不可用,使用基础动画功能");
}
// 初始化动画状态
_animationStartTime = DateTime.Now;
_currentFrameIndex = 0; // 重置当前帧索引,确保从第一帧开始
_pausedProgress = 0.0; // 重置暂停进度
// 初始化动画状态时不再强行覆盖 _currentYaw 为第一帧的 yaw
// 这样在 UpdateObjectPosition(firstFrame.Position, firstFrame.YawRadians) 时
// deltaYaw = firstFrame.YawRadians - 物体初始Yaw从而产生旋转增量让物体转向
if (_animationFrames != null && _animationFrames.Count > 0)
{
var firstFrame = _animationFrames[0];
LogManager.Debug($"[动画开始] _currentYaw之前={_currentYaw * 180 / Math.PI:F2}度, _isVirtualVehicle={_isVirtualVehicle}");
// 🔥 确保物体在起点位置和朝向
// 注意MoveVehicleToPathStart 已在 StartAnimation 中调用,这里只是日志记录
LogManager.Debug($"[动画开始] 物体应在起点: pos=({firstFrame.Position.X:F2},{firstFrame.Position.Y:F2}), yaw={firstFrame.YawRadians:F3}rad");
}
else
{
_currentYaw = 0.0;
}
// 重置动画状态
_lastFrameTime = DateTime.MinValue;
_fpsFrameCount = 0;
_fpsCounterStart = DateTime.Now;
_frameInterval = 1000.0 / _animationFrameRate;
_lastHighlightState = false; // 重置高亮状态
_lastCollisionObjects.Clear(); // 重置碰撞对象集合
// 清除所有碰撞高亮
ModelHighlightHelper.ClearCollisionHighlights();
LogManager.Debug("[动画开始] 已清除所有碰撞高亮");
// 不再高亮车辆对象,保持原始外观(客户要求)
// if (_animatedObject != null)
// {
// var vehicleItems = new List<ModelItem> { _animatedObject };
// ModelHighlightHelper.HighlightItems(ModelHighlightHelper.AnimatedObjectCategory, vehicleItems);
// LogManager.Debug("[动画开始] 已高亮车辆对象为绿色");
// }
// 启动动画播放
StartAnimationPlayback();
SetState(AnimationState.Playing);
LogManager.Info($"动画开始播放 - DispatcherTimer模式");
}
catch (Exception ex)
{
LogManager.Error($"启动动画失败: {ex.Message}");
throw;
}
}
/// <summary>
/// 启动动画播放DispatcherTimer
/// </summary>
private void StartAnimationPlayback()
{
try
{
// 停止之前的播放
StopAnimationPlayback();
// 使用DispatcherTimer模式
_animationTimer.Interval = TimeSpan.FromMilliseconds(_frameInterval);
_animationTimer.Start();
LogManager.Debug("[播放模式] 启动DispatcherTimer模式");
}
catch (Exception ex)
{
LogManager.Error($"启动动画播放失败: {ex.Message}");
}
}
/// <summary>
/// 停止动画播放停止DispatcherTimer
/// </summary>
private void StopAnimationPlayback()
{
try
{
// 停止DispatcherTimer
if (_animationTimer != null)
{
_animationTimer.Stop();
}
LogManager.Debug("[播放模式] 已停止DispatcherTimer");
}
catch (Exception ex)
{
LogManager.Warning($"停止动画播放时出现警告: {ex.Message}");
}
}
/// <summary>
/// DispatcherTimer事件处理器 - 主要动画机制
/// </summary>
private void OnTimerTick(object sender, EventArgs e)
{
try
{
// 处理动画帧
ProcessAnimationFrame();
// 关键改进:显式请求重绘,解决卡顿
NavisApplication.ActiveDocument.ActiveView.RequestDelayedRedraw(ViewRedrawRequests.All);
}
catch (Exception ex)
{
LogManager.Error($"[DispatcherTimer模式] 动画更新异常: {ex.Message}");
// 发生异常时停止动画
ShutdownAnimation();
}
}
/// <summary>
/// 停止动画
/// </summary>
public void CancelAnimation()
{
try
{
// 检查动画状态,避免重复停止
if (_currentState == AnimationState.Stopped || _currentState == AnimationState.Idle)
{
LogManager.Debug($"动画已处于{_currentState}状态,跳过重复停止操作");
return;
}
// 停止播放机制
StopAnimationPlayback();
// 收集整个路径上的所有碰撞对象
var allCollisionResults = new List<CollisionResult>();
var uniqueCollidedItems = new HashSet<ModelItem>();
foreach (var frame in _animationFrames)
{
if (frame.HasCollision)
{
foreach (var collision in frame.Collisions)
{
// 收集唯一的碰撞对象
if (!uniqueCollidedItems.Contains(collision.Item2))
{
uniqueCollidedItems.Add(collision.Item2);
allCollisionResults.Add(collision);
}
}
}
}
SetState(AnimationState.Stopped);
_pausedProgress = 0.0; // 重置暂停进度
_currentFrameIndex = 0; // 重置帧索引
// 清除所有高亮(包括车辆和碰撞)
ModelHighlightHelper.ClearAllHighlights();
LogManager.Info("动画停止:已清除所有高亮");
// 动画停止时保持物体在当前位置(不移回起点)
LogManager.Info("动画已停止,物体保持在当前位置");
// 动画停止时不创建碰撞测试汇总,由动画完成事件统一处理
LogManager.Info("动画停止,等待动画完成事件统一处理碰撞测试...");
// 更新 TimeLiner 任务状态
if (_timeLinerManager != null && !string.IsNullOrEmpty(_currentTaskId))
{
_timeLinerManager.UpdateTaskProgress(_currentTaskId, 0.0, AnimationState.Stopped);
}
}
catch (Exception ex)
{
LogManager.Error($"停止动画失败: {ex.Message}");
}
}
/// <summary>
/// 程序关闭时的动画清理不执行UI操作
/// </summary>
public void ShutdownAnimation()
{
try
{
LogManager.Info("[PathAnimationManager] 程序关闭,执行动画清理");
// 检查动画状态,避免重复停止
if (_currentState == AnimationState.Stopped || _currentState == AnimationState.Idle)
{
LogManager.Debug($"动画已处于{_currentState}状态,跳过关闭清理");
return;
}
// 停止播放机制(关闭时不记录详细日志)
try
{
if (_animationTimer != null) _animationTimer.Stop();
}
catch (Exception stopEx)
{
LogManager.Debug($"关闭清理时停止播放机制异常: {stopEx.Message}");
}
// 直接设置状态为停止不执行任何UI操作
SetState(AnimationState.Stopped);
_pausedProgress = 0.0; // 重置暂停进度
_currentFrameIndex = 0; // 重置帧索引
LogManager.Info("[PathAnimationManager] 动画关闭清理完成");
}
catch (Exception ex)
{
LogManager.Error($"[PathAnimationManager] 关闭动画清理失败: {ex.Message}");
}
}
/// <summary>
/// 完成动画(当动画自然播放结束时调用)- 优化版本,使用预计算碰撞结果
/// </summary>
private void FinishAnimation()
{
try
{
// 停止播放机制
StopAnimationPlayback();
// 重置暂停进度
_pausedProgress = 0.0;
// 使用预计算的所有碰撞结果进行高亮显示(需要去重以避免重复高亮)
var allCollisionResults = _allCollisionResults
.GroupBy(c => new { c.Item1, c.Item2 })
.Select(g => g.First())
.ToList();
// 动画自然结束时保持物体在最终位置(不移回起点)
LogManager.Info("动画播放自然结束,物体保持在最终位置");
// 直接设置为完成状态,避免中间状态切换
SetState(AnimationState.Finished);
// 清除所有高亮(包括车辆和碰撞)
ModelHighlightHelper.ClearAllHighlights();
LogManager.Info("动画完成:已清除所有高亮");
// 修复:显示正确的总帧数,而不是最后一帧的索引
var actualTotalFrames = _animationFrames?.Count ?? 0;
LogManager.Info($"动画播放完成,总帧数: {actualTotalFrames}, 平均FPS: {_actualFPS:F1}");
// 更新 TimeLiner 任务状态
if (_timeLinerManager != null && !string.IsNullOrEmpty(_currentTaskId))
{
_timeLinerManager.UpdateTaskProgress(_currentTaskId, 1.0, AnimationState.Finished);
}
// 检查此动画配置是否已经进行过碰撞检测
if (!_completedCollisionTests.Contains(_currentAnimationHash))
{
LogManager.Info($"此动画配置首次完成,开始创建碰撞测试汇总(基于 {_allCollisionResults.Count} 个预计算碰撞记录)...");
// 🔥 设置等待光标,避免进度条关闭后的无响应
var oldCursor = System.Windows.Input.Mouse.OverrideCursor;
System.Windows.Input.Mouse.OverrideCursor = System.Windows.Input.Cursors.Wait;
bool testCompleted = false;
try
{
// 🔥 使用用户设置的检测容差(米)
testCompleted = ClashDetectiveIntegration.Instance.CreateAllAnimationCollisionTests(
_allCollisionResults,
_detectionTolerance,
_currentRouteId,
_animatedObject,
_isVirtualVehicle,
_animationFrameRate,
_animationDuration,
_virtualVehicleLength,
_virtualVehicleWidth,
_virtualVehicleHeight,
_pathPoints
);
}
finally
{
// 恢复光标
System.Windows.Input.Mouse.OverrideCursor = oldCursor;
}
// 只有在检测成功完成(未被取消)的情况下才记录为已完成
if (testCompleted)
{
_completedCollisionTests.Add(_currentAnimationHash); // 记录此配置已完成碰撞检测
LogManager.Info($"碰撞测试汇总已创建并记录,此配置后续播放将跳过碰撞检测");
}
else
{
LogManager.Info("碰撞检测被取消,不记录为已完成,允许下次重新检测");
}
}
else if (_completedCollisionTests.Contains(_currentAnimationHash))
{
LogManager.Info("此动画配置的碰撞测试已存在,跳过重复创建");
}
}
catch (Exception ex)
{
LogManager.Error($"完成动画失败: {ex.Message}");
}
}
/// <summary>
/// 暂停动画
/// </summary>
public void PauseAnimation()
{
try
{
if (_currentState == AnimationState.Playing)
{
// 计算并保存当前进度
double elapsedSeconds = (DateTime.Now - _animationStartTime).TotalSeconds;
_pausedProgress = Math.Min(elapsedSeconds / _animationDuration, 1.0);
// 停止播放机制
StopAnimationPlayback();
SetState(AnimationState.Paused);
LogManager.Info($"动画已暂停,当前进度: {_pausedProgress:F3}");
// 更新 TimeLiner 任务状态
if (_timeLinerManager != null && !string.IsNullOrEmpty(_currentTaskId))
{
_timeLinerManager.UpdateTaskProgress(_currentTaskId, _pausedProgress, AnimationState.Paused);
}
}
}
catch (Exception ex)
{
LogManager.Error($"暂停动画失败: {ex.Message}");
}
}
/// <summary>
/// 恢复动画
/// </summary>
public void ResumeAnimation()
{
try
{
if (_currentState == AnimationState.Paused)
{
// 使用保存的暂停进度重新设置动画开始时间
// 设置开始时间,使得当前时间对应保存的暂停进度
_animationStartTime = DateTime.Now.AddSeconds(-(_pausedProgress * _animationDuration));
// 重新启动播放机制
_lastFrameTime = DateTime.MinValue; // 确保下一帧立即执行
StartAnimationPlayback();
SetState(AnimationState.Playing);
LogManager.Info($"动画已恢复,从进度 {_pausedProgress:F3} 继续播放");
// 更新 TimeLiner 任务状态
if (_timeLinerManager != null && !string.IsNullOrEmpty(_currentTaskId))
{
_timeLinerManager.UpdateTaskProgress(_currentTaskId, _pausedProgress, AnimationState.Playing);
}
}
}
catch (Exception ex)
{
LogManager.Error($"恢复动画失败: {ex.Message}");
}
}
/// <summary>
/// 重置动画对象到原始位置
/// </summary>
public void ResetAnimation()
{
try
{
// 只在需要时停止动画,避免重复调用
if (_currentState == AnimationState.Playing || _currentState == AnimationState.Paused)
{
CancelAnimation(); // 停止当前动画
}
// 恢复对象的原始变换安全检查Navisworks对象可用性
if (_animatedObject != null)
{
try
{
// 检查Navisworks应用程序和文档是否仍然可用
var activeDoc = NavisApplication.ActiveDocument;
if (activeDoc != null && activeDoc.Models != null)
{
// 第一步:仅仅彻底归位到 CAD 原始位置(不涉及路径起点),这是“清除”按钮的逻辑
RestoreObjectToCADPosition();
// 第二步:如果已经生成了动画路径,则重新对齐到起点(带旋转)
if (_animationFrames != null && _animationFrames.Count > 0)
{
MoveVehicleToPathStart();
LogManager.Info("已重新对齐到路径起点");
}
}
else
{
LogManager.Info("Navisworks文档已不可用跳回空闲状态");
}
}
catch (Exception resetEx)
{
LogManager.Warning($"重置对象位置时出现警告: {resetEx.Message}");
// 不再抛出异常,因为在资源清理阶段这是正常的
}
}
// 重置UI状态这些操作不依赖Navisworks对象
try
{
ProgressChanged?.Invoke(this, 0); // 重置进度条
SetState(AnimationState.Ready); // 重置后回到就绪状态
}
catch (Exception uiEx)
{
LogManager.Warning($"重置UI状态时出现警告: {uiEx.Message}");
}
}
catch (Exception ex)
{
LogManager.Error($"重置动画过程中发生异常: {ex.Message}");
// 在资源清理阶段不再抛出异常,避免影响程序正常关闭
}
}
/// <summary>
/// 安全获取ModelItem的DisplayName避免访问已释放对象的警告
/// </summary>
/// <param name="item">ModelItem对象</param>
/// <returns>安全的显示名称</returns>
private string GetSafeObjectName(ModelItem item)
{
try
{
if (item == null)
{
return "NULL";
}
// 尝试访问DisplayName如果对象已被释放会抛出异常
return item.DisplayName ?? "未命名对象";
}
catch (System.ObjectDisposedException)
{
return "已释放对象";
}
catch (System.Runtime.InteropServices.COMException)
{
return "COM对象已释放";
}
catch (Exception ex)
{
return $"访问失败({ex.GetType().Name})";
}
}
/// <summary>
/// 根据进度插值计算当前位置
/// </summary>
private Point3D InterpolatePosition(double progress)
{
if (_pathPoints.Count < 2)
return _pathPoints[0];
// 确保进度在0-1范围内
progress = Math.Max(0.0, Math.Min(1.0, progress));
// 如果进度达到100%,直接返回终点
if (progress >= 1.0)
{
return _pathPoints[_pathPoints.Count - 1];
}
// 计算总路径长度
var totalDistance = CalculateTotalPathDistance();
// 检查总距离是否有效
if (totalDistance <= 0.0 || double.IsNaN(totalDistance) || double.IsInfinity(totalDistance))
{
LogManager.Error($"路径总长度无效: {totalDistance},返回起点坐标");
return _pathPoints[0];
}
var targetDistance = totalDistance * progress;
// 找到当前应该在哪两个点之间
var accumulatedDistance = 0.0;
for (int i = 0; i < _pathPoints.Count - 1; i++)
{
var segmentDistance = CalculateDistance(_pathPoints[i], _pathPoints[i + 1]);
// 检查段距离是否有效
if (double.IsNaN(segmentDistance) || double.IsInfinity(segmentDistance))
{
LogManager.Error($"路径段[{i}-{i + 1}]距离无效: {segmentDistance},跳过此段");
continue;
}
if (accumulatedDistance + segmentDistance > targetDistance || i == _pathPoints.Count - 2)
{
// 在这个线段内,或者是最后一个线段
var segmentProgress = segmentDistance > 0 ? (targetDistance - accumulatedDistance) / segmentDistance : 0.0;
// 确保段内进度在0-1范围内
segmentProgress = Math.Max(0.0, Math.Min(1.0, segmentProgress));
var interpolatedPoint = InterpolatePoints(_pathPoints[i], _pathPoints[i + 1], segmentProgress);
// 验证插值结果
if (double.IsNaN(interpolatedPoint.X) || double.IsNaN(interpolatedPoint.Y) || double.IsNaN(interpolatedPoint.Z))
{
LogManager.Error($"插值计算结果无效: ({interpolatedPoint.X},{interpolatedPoint.Y},{interpolatedPoint.Z}),返回起点");
return _pathPoints[0];
}
return interpolatedPoint;
}
accumulatedDistance += segmentDistance;
}
// 如果到达这里,返回最后一个点
return _pathPoints[_pathPoints.Count - 1];
}
/// <summary>
/// 在两点间插值
/// </summary>
private Point3D InterpolatePoints(Point3D point1, Point3D point2, double t)
{
return new Point3D(
point1.X + (point2.X - point1.X) * t,
point1.Y + (point2.Y - point1.Y) * t,
point1.Z + (point2.Z - point1.Z) * t
);
}
/// <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;
}
/// <summary>
/// 找到目标距离所在的线段索引(用于空轨路径的线性插值)
/// </summary>
/// <param name="targetDistance">目标距离</param>
/// <param name="segmentLengths">线段长度列表</param>
/// <returns>线段索引</returns>
private int FindSegmentForDistance(double targetDistance, List<double> segmentLengths)
{
double accumulatedLength = 0.0;
for (int i = 0; i < segmentLengths.Count; i++)
{
if (accumulatedLength + segmentLengths[i] >= targetDistance)
{
return i;
}
accumulatedLength += segmentLengths[i];
}
return segmentLengths.Count - 1;
}
/// <summary>
/// 计算路径总长度
/// </summary>
private double CalculateTotalPathDistance()
{
var totalDistance = 0.0;
for (int i = 0; i < _pathPoints.Count - 1; i++)
{
totalDistance += CalculateDistance(_pathPoints[i], _pathPoints[i + 1]);
}
return totalDistance;
}
/// <summary>
/// 获取对象当前位置
/// </summary>
private Point3D GetObjectPosition(ModelItem item)
{
try
{
if (item == null) return new Point3D(0, 0, 0);
var bounds = item.BoundingBox();
if (bounds != null)
{
return new Point3D(
(bounds.Min.X + bounds.Max.X) / 2,
(bounds.Min.Y + bounds.Max.Y) / 2,
(bounds.Min.Z + bounds.Max.Z) / 2
);
}
return new Point3D(0, 0, 0);
}
catch (Exception ex)
{
LogManager.Error($"获取对象位置失败: {ex.Message}");
return new Point3D(0, 0, 0);
}
}
/// <summary>
/// 计算两点间距离
/// </summary>
private double CalculateDistance(Point3D point1, Point3D point2)
{
var dx = point2.X - point1.X;
var dy = point2.Y - point1.Y;
var dz = point2.Z - point1.Z;
return Math.Sqrt(dx * dx + dy * dy + dz * dz);
}
/// <summary>
/// 更新对象位置和朝向(支持绕物体中心旋转)
/// </summary>
private void UpdateObjectPosition(Point3D newPosition, double newYaw = double.NaN)
{
try
{
var doc = NavisApplication.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))
{
// 有旋转:需要实现"绕物体当前位置自转"
// 由于Transform3DComponents.Rotation总是绕世界原点旋转
// 我们需要手动计算旋转导致的位置偏移,并补偿
double deltaYaw = newYaw - _currentYaw;
//LogManager.Debug($"[UpdateObjectPosition] 当前yaw={_currentYaw * 180 / Math.PI:F2}度, 目标yaw={newYaw * 180 / Math.PI:F2}度, 旋转增量deltaYaw={deltaYaw * 180 / Math.PI:F2}度");
// 计算绕当前位置旋转的等效变换:
// 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);
LogManager.Debug($"[UpdateObjectPosition] 纯平移: ({deltaPos.X:F2},{deltaPos.Y:F2},{deltaPos.Z:F2})");
}
// 应用增量变换false = 增量模式)
doc.Models.OverridePermanentTransform(modelItems, incrementalTransform, false);
// 更新当前位置
_currentPosition = newPosition;
}
catch (Exception ex)
{
LogManager.Error($"更新部件位置失败: {ex.Message}");
}
}
/// <summary>
/// 获取文档单位信息(用于日志记录)
/// </summary>
private string GetDocumentUnitsInfo()
{
try
{
var doc = NavisApplication.ActiveDocument;
var units = doc.Units;
LogManager.Debug($"[单位检测] 文档单位: {units}");
return units.ToString();
}
catch (Exception ex)
{
LogManager.Error($"获取文档单位信息失败: {ex.Message}");
return "Unknown";
}
}
/// <summary>
/// 获取模型当前变换
/// </summary>
private Transform3D GetCurrentTransform(ModelItem item)
{
// 获取包围盒中心作为参考点
var boundingBox = item.BoundingBox();
var center = boundingBox.Center;
// 创建基于中心点的单位变换
return Transform3D.CreateTranslation(new Vector3D(center.X, center.Y, center.Z));
}
/// <summary>
/// 设置动画持续时间
/// </summary>
public void SetAnimationDuration(double durationSeconds)
{
_animationDuration = Math.Max(1.0, durationSeconds);
}
/// <summary>
/// 获取动画状态
/// </summary>
public bool IsAnimating => _currentState == AnimationState.Playing;
/// <summary>
/// 获取 TimeLiner 是否可用
/// </summary>
public bool IsTimeLinerAvailable => _timeLinerManager?.IsTimeLinerAvailable ?? false;
/// <summary>
/// 获取当前动画状态
/// </summary>
public AnimationState CurrentState => _currentState;
/// <summary>
/// 获取当前 TimeLiner 任务ID
/// </summary>
public string CurrentTaskId => _currentTaskId;
/// <summary>
/// 获取 TimeLiner 集成管理器
/// </summary>
public TimeLinerIntegrationManager TimeLinerManager => _timeLinerManager;
#region
/// <summary>
/// 获取当前帧索引
/// </summary>
public int CurrentFrame => _currentFrameIndex;
/// <summary>
/// 获取总帧数
/// </summary>
public int TotalFrames => _animationFrames?.Count ?? 0;
/// <summary>
/// 获取物体当前位置和朝向
/// 🔥 关键返回实际当前朝向_currentYaw而不是 Transform 的 CAD 原始朝向
/// </summary>
public (Point3D Position, double Yaw) GetObjectCurrentPosition(ModelItem obj)
{
if (obj == null)
{
LogManager.Warning("GetObjectCurrentPosition: 对象为空");
return (null, 0);
}
var bbox = obj.BoundingBox();
var position = new Point3D(
bbox.Center.X,
bbox.Center.Y,
bbox.Min.Z
);
// 🔥 关键:使用 _currentYaw实际当前朝向而不是 Transform 的 CAD 原始朝向
// Transform 返回的是 CAD 设计时的原始朝向,不是动画后的实际朝向
return (position, _currentYaw);
}
/// <summary>
/// 保存物体当前位置和朝向
/// </summary>
public void SaveObjectState(ModelItem obj)
{
if (obj == null)
{
LogManager.Warning("SaveObjectState: 对象为空");
return;
}
try
{
var (position, yaw) = GetObjectCurrentPosition(obj);
_savedObjectPosition = position;
_savedObjectYaw = yaw;
_hasSavedObjectState = true;
LogManager.Info($"已保存物体状态: pos=({position.X:F2},{position.Y:F2},{position.Z:F2}), yaw={yaw * 180 / Math.PI:F2}度");
}
catch (Exception ex)
{
LogManager.Error($"SaveObjectState: 保存物体状态失败: {ex.Message}");
}
}
/// <summary>
/// 恢复动画物体到保存的位置和朝向
/// 此方法会更新 PathAnimationManager 的内部状态_currentYaw等
/// </summary>
public void RestoreAnimatedObjectState(ModelItem obj)
{
if (obj == null)
{
LogManager.Warning("RestoreAnimatedObjectState: 对象为空");
return;
}
if (!_hasSavedObjectState)
{
LogManager.Warning("RestoreAnimatedObjectState: 没有保存的物体状态");
return;
}
try
{
var originalAnimatedObject = _animatedObject;
_animatedObject = obj;
var bbox = obj.BoundingBox();
_currentPosition = new Point3D(bbox.Center.X, bbox.Center.Y, bbox.Min.Z);
// 使用保存的朝向作为当前朝向避免UpdateObjectPosition计算不必要的大角度差
// 这样能确保物体直接从当前动画位置平滑恢复到保存位置,不会突然转动
_currentYaw = _savedObjectYaw;
UpdateObjectPosition(_savedObjectPosition, _savedObjectYaw);
_animatedObject = originalAnimatedObject;
LogManager.Info($"已恢复物体状态: pos=({_savedObjectPosition.X:F2},{_savedObjectPosition.Y:F2},{_savedObjectPosition.Z:F2}), yaw={_savedObjectYaw * 180 / Math.PI:F2}度");
}
catch (Exception ex)
{
LogManager.Error($"RestoreAnimatedObjectState: 恢复动画物体状态失败: {ex.Message}");
}
}
/// <summary>
/// 获取播放方向1=正向,-1=反向)
/// </summary>
public int PlaybackDirection => _playbackDirection;
/// <summary>
/// 获取播放速度倍率
/// </summary>
public double PlaybackSpeed => _playbackSpeed;
/// <summary>
/// 获取是否正在反向播放
/// </summary>
public bool IsPlayingReverse => _playbackDirection == -1;
/// <summary>
/// 设置播放方向
/// </summary>
/// <param name="reverse">true=反向播放false=正向播放</param>
public void SetPlaybackDirection(bool reverse)
{
_playbackDirection = reverse ? -1 : 1;
LogManager.Info($"播放方向设置为: {(reverse ? "" : "")}");
}
/// <summary>
/// 设置播放速度
/// </summary>
/// <param name="speed">播放速度倍率,负值表示反向</param>
public void SetPlaybackSpeed(double speed)
{
_playbackSpeed = Math.Abs(speed);
_playbackDirection = speed >= 0 ? 1 : -1;
// 更新帧间隔
_frameInterval = 1000.0 / (_animationFrameRate * _playbackSpeed);
LogManager.Info($"播放速度设置为: {speed}x方向: {(speed >= 0 ? "" : "")}");
}
/// <summary>
/// 跳转到指定帧
/// </summary>
/// <param name="frameIndex">目标帧索引</param>
public void SeekToFrame(int frameIndex)
{
try
{
if (_animationFrames == null || _animationFrames.Count == 0)
{
LogManager.Warning("无动画帧数据,无法跳转");
return;
}
// 限制帧索引范围
frameIndex = Math.Max(0, Math.Min(frameIndex, _animationFrames.Count - 1));
if (frameIndex == _currentFrameIndex)
{
LogManager.Debug($"已处于目标帧 {frameIndex},无需跳转");
return;
}
_currentFrameIndex = frameIndex;
// 更新对象位置和朝向
var frameData = _animationFrames[_currentFrameIndex];
UpdateObjectPosition(frameData.Position, frameData.YawRadians);
// 更新碰撞高亮
UpdateCollisionHighlightFromFrame();
// 计算并触发进度事件
double progress = TotalFrames > 1 ? (double)_currentFrameIndex / (TotalFrames - 1) * 100 : 0;
ProgressChanged?.Invoke(this, progress);
}
catch (Exception ex)
{
LogManager.Error($"跳转到帧失败: {ex.Message}");
}
}
/// <summary>
/// 跳转到指定进度
/// </summary>
/// <param name="progress">进度值0.0-1.0</param>
public void SeekToProgress(double progress)
{
try
{
progress = Math.Max(0.0, Math.Min(1.0, progress));
int targetFrame = TotalFrames > 1 ? (int)(progress * (TotalFrames - 1)) : 0;
SeekToFrame(targetFrame);
}
catch (Exception ex)
{
LogManager.Error($"跳转到进度失败: {ex.Message}");
}
}
/// <summary>
/// 单帧前进
/// </summary>
public void StepForward()
{
StepForward(1);
}
/// <summary>
/// 前进指定帧数
/// </summary>
/// <param name="frames">帧数</param>
public void StepForward(int frames)
{
try
{
if (frames <= 0) return;
int targetFrame = _currentFrameIndex + frames;
targetFrame = Math.Min(targetFrame, TotalFrames - 1);
SeekToFrame(targetFrame);
}
catch (Exception ex)
{
LogManager.Error($"前进步进失败: {ex.Message}");
}
}
/// <summary>
/// 单帧后退
/// </summary>
public void StepBackward()
{
StepBackward(1);
}
/// <summary>
/// 后退指定帧数
/// </summary>
/// <param name="frames">帧数</param>
public void StepBackward(int frames)
{
try
{
if (frames <= 0) return;
int targetFrame = _currentFrameIndex - frames;
targetFrame = Math.Max(targetFrame, 0);
SeekToFrame(targetFrame);
}
catch (Exception ex)
{
LogManager.Error($"后退步进失败: {ex.Message}");
}
}
/// <summary>
/// 快进10帧
/// </summary>
public void FastForward()
{
StepForward(10);
}
/// <summary>
/// 快退10帧
/// </summary>
public void FastBackward()
{
StepBackward(10);
}
/// <summary>
/// 开始正向播放
/// </summary>
public void PlayForward()
{
try
{
SetPlaybackDirection(false);
if (_currentState == AnimationState.Paused)
{
ResumeAnimation(); // 从暂停位置继续
}
else if (_currentState != AnimationState.Playing)
{
StartAnimation(); // 只有非播放和非暂停状态才从头开始
}
LogManager.Info("开始正向播放");
}
catch (Exception ex)
{
LogManager.Error($"正向播放失败: {ex.Message}");
}
}
/// <summary>
/// 开始反向播放
/// </summary>
public void PlayReverse()
{
try
{
SetPlaybackDirection(true);
if (_currentState == AnimationState.Paused)
{
ResumeAnimation(); // 从暂停位置继续
}
else if (_currentState != AnimationState.Playing)
{
StartAnimation(); // 只有非播放和非暂停状态才从头开始
}
LogManager.Info("开始反向播放");
}
catch (Exception ex)
{
LogManager.Error($"反向播放失败: {ex.Message}");
}
}
#endregion
/// <summary>
/// 清理对象引用
/// </summary>
public void ClearReferences()
{
try
{
LogManager.Info("[PathAnimationManager] 清理对象引用");
// 停止动画
if (_currentState == AnimationState.Playing || _currentState == AnimationState.Paused)
{
ShutdownAnimation();
}
// 清空对象引用
_animatedObject = null;
_pathPoints?.Clear();
_currentPosition = new Point3D(0, 0, 0);
_originalCenter = new Point3D(0, 0, 0);
LogManager.Info("[PathAnimationManager] 对象引用已清理");
}
catch (Exception ex)
{
LogManager.Error($"[PathAnimationManager] 清理对象引用失败: {ex.Message}");
}
}
/// <summary>
/// 文档失效事件处理
/// </summary>
private void OnDocumentInvalidated(object sender, EventArgs e)
{
try
{
LogManager.Info("[PathAnimationManager] 文档失效,停止动画并清理");
ClearReferences();
}
catch (Exception ex)
{
LogManager.Error($"[PathAnimationManager] 处理文档失效事件失败: {ex.Message}");
}
}
/// <summary>
/// 资源清理
/// </summary>
public void Dispose()
{
try
{
LogManager.Info("开始清理PathAnimationManager资源");
// 0. 取消配置变更事件订阅
UnsubscribeFromConfigChanges();
// 1. 优先停止动画(最小化的操作,不触发复杂的重置逻辑)
// 只有在动画运行时才停止,避免重复调用
try
{
if (_currentState == AnimationState.Playing || _currentState == AnimationState.Paused)
{
ShutdownAnimation();
}
}
catch (Exception stopEx)
{
LogManager.Warning($"停止动画时出现警告: {stopEx.Message},继续清理其他资源");
}
// 2. 在Dispose中不进行重置操作避免访问已释放的对象
// 程序关闭时,对象位置重置是不必要的
LogManager.Info("跳过动画重置操作(程序关闭时不需要恢复对象位置)");
// 3. 清理 TimeLiner 资源
if (_timeLinerManager != null)
{
try
{
_timeLinerManager.Dispose();
_timeLinerManager = null;
LogManager.Info("TimeLiner 集成资源已清理");
}
catch (Exception ex)
{
LogManager.Warning($"清理 TimeLiner 资源时出现警告: {ex.Message}");
}
}
// 4. 清理DispatcherTimer资源
if (_animationTimer != null)
{
try
{
_animationTimer.Stop();
// DispatcherTimer不需要Dispose
_animationTimer = null;
LogManager.Info("DispatcherTimer资源已清理");
}
catch (Exception ex)
{
LogManager.Warning($"清理DispatcherTimer资源时出现警告: {ex.Message}");
}
}
// 5. 取消订阅文档事件
DocumentStateManager.Instance.DocumentInvalidated -= OnDocumentInvalidated;
LogManager.Info("PathAnimationManager资源清理完成");
}
catch (Exception ex)
{
LogManager.Error($"PathAnimationManager资源清理过程中发生异常: {ex.Message}");
// 不抛出异常,避免影响应用程序正常关闭
}
}
/// <summary>
/// 设置动画帧率
/// </summary>
public void SetAnimationFrameRate(int frameRate)
{
_animationFrameRate = Math.Max(10, Math.Min(60, frameRate)); // 限制在10-60FPS之间
_frameInterval = 1000.0 / _animationFrameRate; // 重新计算帧间隔
// 动态更新DispatcherTimer间隔
if (_animationTimer != null)
{
_animationTimer.Interval = TimeSpan.FromMilliseconds(_frameInterval);
LogManager.Info($"DispatcherTimer间隔已更新: {_frameInterval:F1}ms");
}
LogManager.Info($"动画帧率设置为: {_animationFrameRate}FPS (间隔: {_frameInterval:F1}ms)");
}
/// <summary>
/// 设置碰撞检测精度(参数单位:米)
/// </summary>
public void SetCollisionDetectionAccuracy(double accuracyInMeters)
{
// 转换为模型单位存储
var accuracyInModelUnits = UnitsConverter.ConvertFromMeters(Math.Max(0.01, accuracyInMeters));
_collisionDetectionAccuracy = accuracyInModelUnits;
LogManager.Info($"碰撞检测精度设置为: {accuracyInMeters:F3}米");
}
/// <summary>
/// 设置运动速度
/// </summary>
public void SetMovementSpeed(double speed)
{
_movementSpeed = Math.Max(0.1, speed); // 最小0.1米/秒
LogManager.Info($"运动速度设置为: {_movementSpeed}m/s");
}
/// <summary>
/// 设置检测容差(参数单位:米)
/// </summary>
public void SetDetectionTolerance(double toleranceInMeters)
{
_detectionTolerance = Math.Max(0.0, toleranceInMeters);
LogManager.Info($"检测容差设置为: {_detectionTolerance:F2}米仅用于ClashDetective");
}
/// <summary>
/// 设置手工碰撞对象覆写列表
/// </summary>
public void SetManualCollisionTargets(IEnumerable<ModelItem> targets, bool enabled)
{
if (!enabled)
{
_manualCollisionOverrideEnabled = false;
_manualCollisionTargets = new List<ModelItem>();
LogManager.Info("[PathAnimationManager] 手工碰撞对象覆写已关闭");
return;
}
if (targets == null)
{
_manualCollisionOverrideEnabled = false;
_manualCollisionTargets = new List<ModelItem>();
LogManager.Warning("[PathAnimationManager] 手工碰撞对象覆写启用但目标为空,已回退到默认模式");
return;
}
var validTargets = targets
.Where(item => item != null && ModelItemAnalysisHelper.IsModelItemValid(item))
.Where(item => !ReferenceEquals(item, _animatedObject))
.Distinct()
.ToList();
if (validTargets.Count == 0)
{
_manualCollisionOverrideEnabled = false;
_manualCollisionTargets = new List<ModelItem>();
LogManager.Warning("[PathAnimationManager] 手工碰撞对象覆写启用但均无效,已回退到默认模式");
return;
}
_manualCollisionOverrideEnabled = true;
_manualCollisionTargets = validTargets;
LogManager.Info($"[PathAnimationManager] 启用手工碰撞对象覆写,共 {validTargets.Count} 个对象");
}
/// <summary>
/// 获取当前动画帧率
/// </summary>
public int AnimationFrameRate => _animationFrameRate;
/// <summary>
/// 获取所有碰撞结果(不去重)
/// </summary>
public List<CollisionResult> AllCollisionResults => _allCollisionResults;
/// <summary>
/// 获取当前物体的朝向(弧度)
/// 用于保存物体当前状态因为Transform返回的是CAD原始值
/// </summary>
public double CurrentYaw => _currentYaw;
/// <summary>
/// 获取当前碰撞检测精度
/// </summary>
public double CollisionDetectionAccuracy => _collisionDetectionAccuracy;
/// <summary>
/// 获取当前运动速度
/// </summary>
public double MovementSpeed => _movementSpeed;
/// <summary>
/// 获取当前检测间隙(单位:米)
/// </summary>
public double DetectionGap
{
get
{
return _detectionTolerance;
}
}
/// <summary>
/// 获取路径名称
/// </summary>
public string PathName => _pathName;
/// <summary>
/// 获取动画时长
/// </summary>
public double AnimationDuration => _animationDuration;
/// <summary>
/// 设置并触发状态变更事件
/// </summary>
/// <param name="newState">新的动画状态</param>
private void SetState(AnimationState newState)
{
if (_currentState != newState)
{
_currentState = newState;
StateChanged?.Invoke(this, _currentState);
}
}
/// <summary>
/// 创建动画
/// </summary>
/// <param name="animatedObject">要动画化的模型对象</param>
/// <param name="pathPoints">路径点列表</param>
/// <param name="durationSeconds">动画持续时间(秒)</param>
/// <param name="pathName">路径名称</param>
/// <param name="routeId">路由ID</param>
public void CreateAnimation(ModelItem animatedObject, List<Point3D> pathPoints, double durationSeconds = 10.0, string pathName = "未知路径", string routeId = null, bool isVirtualVehicle = false,
double virtualVehicleLength = 0, double virtualVehicleWidth = 0, double virtualVehicleHeight = 0, double safetyMargin = 0)
{
_pathName = pathName;
_currentRouteId = routeId;
_isVirtualVehicle = isVirtualVehicle; // 设置是否使用虚拟车辆
_virtualVehicleLength = virtualVehicleLength; // 模型单位
_virtualVehicleWidth = virtualVehicleWidth; // 模型单位
_virtualVehicleHeight = virtualVehicleHeight; // 模型单位
_safetyMargin = safetyMargin; // 模型单位
_pathPoints = pathPoints;
_animatedObject = animatedObject;
// 初始化物体位置和朝向
if (animatedObject != null)
{
_originalTransform = animatedObject.Transform;
_originalCenter = animatedObject.BoundingBox().Center;
_currentPosition = new Point3D(_originalCenter.X, _originalCenter.Y, animatedObject.BoundingBox().Min.Z);
// 保持当前的 _currentYaw因为物体可能已经被 MoveVehicleToPathStart 旋转)
// 不要从 Transform 中提取,因为 Transform 返回的是原始值,不是当前值
LogManager.Debug($"[CreateAnimation] 保持_currentYaw={_currentYaw * 180 / Math.PI:F2}度不变, _isVirtualVehicle={_isVirtualVehicle}");
}
// 设置动画参数并预计算动画帧
// 注意物体应该在调用CreateAnimation之前已经通过MoveVehicleToPathStart旋转到起点
LogManager.Debug($"[CreateAnimation开始] _currentYaw之前={_currentYaw * 180 / Math.PI:F2}度, _isVirtualVehicle={_isVirtualVehicle}");
SetupAnimation(animatedObject, durationSeconds, _route);
LogManager.Debug($"[CreateAnimation结束] _currentYaw之后={_currentYaw * 180 / Math.PI:F2}度");
// 设置动画状态为Ready动画已生成可以播放
SetState(AnimationState.Ready);
LogManager.Info($"[CreateAnimation] 动画已创建状态设置为Ready");
}
/// <summary>
/// 设置物体角度修正值(度,顺时针)
/// </summary>
/// <param name="rotationCorrection">角度修正值(度)</param>
public void SetObjectRotationCorrection(double rotationCorrection)
{
_objectRotationCorrection = rotationCorrection;
// 如果动画已创建,更新物体到起点的朝向
if (_animatedObject != null && _pathPoints != null && _pathPoints.Count > 0)
{
try
{
// 重新计算并应用朝向
MoveVehicleToPathStart();
LogManager.Info($"[角度修正] 物体角度已更新: {_objectRotationCorrection:F1}°");
}
catch (Exception ex)
{
LogManager.Error($"[角度修正] 更新物体角度失败: {ex.Message}");
}
}
}
/// <summary>
/// 直接设置物体角度修正值(不触发物体旋转)
/// </summary>
/// <param name="rotationCorrection">角度修正值(度)</param>
public void SetObjectRotationCorrectionDirect(double rotationCorrection)
{
_objectRotationCorrection = rotationCorrection;
LogManager.Debug($"[角度修正] 直接设置角度修正值: {_objectRotationCorrection:F1}°(不触发旋转)");
}
#region
/// <summary>
/// DispatcherTimer动画处理核心
/// </summary>
/// <summary>
/// 根据当前帧索引更新碰撞高亮(状态跟踪优化版本)
/// </summary>
private void UpdateCollisionHighlightFromFrame()
{
try
{
// 确保帧索引有效
if (_currentFrameIndex < 0 || _currentFrameIndex >= _animationFrames.Count)
{
return;
}
// 获取当前帧数据
var currentFrame = _animationFrames[_currentFrameIndex];
bool currentHasCollision = currentFrame.HasCollision;
// 收集当前帧碰撞对象的集合
var currentCollisionObjects = new HashSet<ModelItem>();
if (currentHasCollision && currentFrame.Collisions != null)
{
foreach (var collision in currentFrame.Collisions)
{
if (collision.Item1 != null)
currentCollisionObjects.Add(collision.Item1);
if (collision.Item2 != null)
currentCollisionObjects.Add(collision.Item2);
}
}
// 检查是否需要更新高亮:
// 1. 碰撞状态改变(有碰撞 vs 无碰撞)
// 2. 碰撞对象集合改变(碰撞的物体变了)
bool stateChanged = currentHasCollision != _lastHighlightState;
bool objectsChanged = !currentCollisionObjects.SetEquals(_lastCollisionObjects);
if (stateChanged || objectsChanged)
{
if (currentHasCollision)
{
// 有碰撞高亮当前帧的碰撞对象使用CollisionResultsCategory的默认颜色
ModelHighlightHelper.ManageCollisionHighlightsByCategory(ModelHighlightHelper.PrecomputeCollisionResultsCategory, currentFrame.Collisions, clearOtherCategories: true);
LogManager.Debug($"[高亮状态] 帧{_currentFrameIndex}: 高亮碰撞 ({currentFrame.Collisions.Count}个, 对象{currentCollisionObjects.Count}个)");
}
else
{
// 无碰撞:清除碰撞高亮(保留车辆高亮)
ModelHighlightHelper.ClearCategory(ModelHighlightHelper.PrecomputeCollisionResultsCategory);
LogManager.Debug($"[高亮状态] 帧{_currentFrameIndex}: 清除碰撞高亮");
}
// 更新状态记录
_lastHighlightState = currentHasCollision;
_lastCollisionObjects = currentCollisionObjects;
}
}
catch (Exception ex)
{
LogManager.Error($"更新碰撞高亮失败: {ex.Message}");
}
}
/// <summary>
/// 统一的动画帧处理逻辑DispatcherTimer专用
/// </summary>
private void ProcessAnimationFrame()
{
// 检查动画状态
if (_currentState != AnimationState.Playing)
{
return; // 非播放状态,跳过更新
}
// DispatcherTimer已经控制了调用间隔无需手动帧率控制
var now = DateTime.Now;
// 计算下一帧索引(考虑播放方向和速度)
int nextFrameIndex = _currentFrameIndex;
if (_playbackDirection == 1) // 正向播放
{
nextFrameIndex = _currentFrameIndex + 1;
if (nextFrameIndex >= _animationFrames.Count)
{
// 正向播放到达终点
nextFrameIndex = _animationFrames.Count - 1;
StopAnimationPlayback();
FinishAnimation();
return;
}
}
else // 反向播放
{
nextFrameIndex = _currentFrameIndex - 1;
if (nextFrameIndex < 0)
{
// 反向播放到达起点
nextFrameIndex = 0;
StopAnimationPlayback();
FinishAnimation();
return;
}
}
// 更新帧索引和位置
if (nextFrameIndex != _currentFrameIndex)
{
_currentFrameIndex = nextFrameIndex;
// 使用预计算的帧位置和朝向
if (_currentFrameIndex < _animationFrames.Count)
{
var frameData = _animationFrames[_currentFrameIndex];
// 计算实际朝向 = 路径方向 + 角度修正
double actualYaw = frameData.YawRadians;
if (_objectRotationCorrection != 0.0)
{
double correctionRad = _objectRotationCorrection * Math.PI / 180.0;
actualYaw += correctionRad;
}
UpdateObjectPosition(frameData.Position, actualYaw);
// 更新碰撞高亮(基于预计算结果)
UpdateCollisionHighlightFromFrame();
// 计算当前进度(基于帧索引)
double progress = _animationFrames.Count > 1 ? (double)_currentFrameIndex / (_animationFrames.Count - 1) : 0;
// 触发进度事件
var progressPercent = progress * 100;
ProgressChanged?.Invoke(this, progressPercent);
// 同步进度到 TimeLiner如果可用
if (_timeLinerManager != null && !string.IsNullOrEmpty(_currentTaskId))
{
_timeLinerManager.UpdateTaskProgress(_currentTaskId, progress, _currentState);
}
}
}
// 记录帧时间和统计
_lastFrameTime = now;
// 性能监控和自动切换
UpdateFPSCounterAndCheckSwitch();
}
/// <summary>
/// 更新FPS计数器DispatcherTimer模式
/// </summary>
private void UpdateFPSCounterAndCheckSwitch()
{
_fpsFrameCount++;
var elapsed = (DateTime.Now - _fpsCounterStart).TotalSeconds;
// 每秒更新一次FPS统计
if (elapsed >= 1.0)
{
_actualFPS = _fpsFrameCount / elapsed;
LogManager.Debug($"[性能监控] 实际FPS: {_actualFPS:F1} (目标: {_animationFrameRate}) - DispatcherTimer模式");
// 重置计数器
_fpsFrameCount = 0;
_fpsCounterStart = DateTime.Now;
}
}
#region
/// <summary>
/// 计算动画配置的哈希值(基于路径点和对象)
/// </summary>
private string ComputeAnimationConfigHash(ModelItem animatedObject, List<Point3D> pathPoints)
{
try
{
var sb = new StringBuilder();
// 包含对象名称
sb.Append(animatedObject?.DisplayName ?? "null");
sb.Append("|");
// 包含路径点信息
if (pathPoints != null)
{
foreach (var point in pathPoints)
{
sb.Append($"{point.X:F2},{point.Y:F2},{point.Z:F2};");
}
}
// 计算哈希
using (var md5 = System.Security.Cryptography.MD5.Create())
{
var inputBytes = System.Text.Encoding.UTF8.GetBytes(sb.ToString());
var hashBytes = md5.ComputeHash(inputBytes);
return BitConverter.ToString(hashBytes).Replace("-", "");
}
}
catch (Exception ex)
{
LogManager.Warning($"计算动画配置哈希失败: {ex.Message}");
return DateTime.Now.Ticks.ToString(); // 回退到时间戳
}
}
/// <summary>
/// 计算动画配置的哈希值基于PathRoute的Edges可以区分不同的曲线化路径
/// </summary>
private string ComputeAnimationConfigHashFromRoute(ModelItem animatedObject, PathRoute route)
{
try
{
var sb = new StringBuilder();
// 包含对象的唯一标识使用PathId而不是DisplayName
if (animatedObject != null)
{
try
{
var pathId = Autodesk.Navisworks.Api.Application.ActiveDocument.Models.CreatePathId(animatedObject);
sb.Append($"{pathId.ModelIndex}:{pathId.PathId}");
}
catch
{
sb.Append(animatedObject.DisplayName ?? "null");
}
}
else
{
sb.Append("null");
}
sb.Append("|");
// 包含路径ID区分不同的路径
sb.Append(route?.Id ?? "null");
sb.Append("|");
// 包含动画参数(确保参数改变时重新检测)
sb.Append($"Duration:{_animationDuration:F2}s");
sb.Append($"|FrameRate:{_animationFrameRate}");
sb.Append($"|DetectionTolerance:{_detectionTolerance:F4}");
sb.Append("|");
// 包含虚拟车辆尺寸(确保车辆尺寸改变时重新检测)
// 使用传入的车辆尺寸参数而不是从ConfigManager读取
var vehicleSizeString = $"Vehicle:{_virtualVehicleLength:F2}x{_virtualVehicleWidth:F2}x{_virtualVehicleHeight:F2}";
sb.Append(vehicleSizeString);
sb.Append("|");
// 包含角度修正(确保角度修正改变时重新检测)
sb.Append($"RotationCorrection:{_objectRotationCorrection:F2}deg");
sb.Append("|");
// 包含手工检测对象列表(确保手工指定模式的目标变化时重新检测)
// 使用PathId作为唯一标识而不是DisplayName
if (_manualCollisionOverrideEnabled && _manualCollisionTargets != null && _manualCollisionTargets.Count > 0)
{
sb.Append("ManualTargets:");
foreach (var target in _manualCollisionTargets.OrderBy(t => t.DisplayName))
{
try
{
var targetPathId = Autodesk.Navisworks.Api.Application.ActiveDocument.Models.CreatePathId(target);
sb.Append($"{targetPathId.ModelIndex}:{targetPathId.PathId}");
sb.Append(",");
}
catch (Exception)
{
// 忽略无效对象
}
}
sb.Append("|");
}
// 包含路径边的摘要信息(区分不同的曲线化结果)
if (route?.Edges != null && route.Edges.Count > 0)
{
foreach (var edge in route.Edges)
{
sb.Append(edge.SegmentType.ToString());
sb.Append($"({edge.PhysicalLength:F2})");
if (edge.SegmentType == PathSegmentType.Arc && edge.Trajectory != null)
{
sb.Append($"_R{edge.Trajectory.ActualRadius:F2}");
sb.Append($"_A{edge.Trajectory.DeflectionAngle:F2}");
}
sb.Append(";");
}
}
// 包含路径最后修改时间(确保路径修改后重新生成动画)
sb.Append($"LastModified:{route?.LastModified:yyyyMMddHHmmss}");
// 计算哈希
using (var md5 = System.Security.Cryptography.MD5.Create())
{
var inputBytes = System.Text.Encoding.UTF8.GetBytes(sb.ToString());
var hashBytes = md5.ComputeHash(inputBytes);
return BitConverter.ToString(hashBytes).Replace("-", "");
}
}
catch (Exception ex)
{
LogManager.Warning($"计算动画配置哈希失败: {ex.Message}");
return DateTime.Now.Ticks.ToString(); // 回退到时间戳
}
}
/// <summary>
/// 强制重新创建碰撞测试(清除指定配置的碰撞检测记录)
/// </summary>
public void ForceRecreateCollisionTest()
{
if (!string.IsNullOrEmpty(_currentAnimationHash))
{
_completedCollisionTests.Remove(_currentAnimationHash);
LogManager.Info($"已清除动画配置 {_currentAnimationHash} 的碰撞检测记录,下次播放时将重新创建");
}
}
/// <summary>
/// 清除所有碰撞测试记录
/// </summary>
public static void ClearAllCollisionTestRecords()
{
_completedCollisionTests.Clear();
LogManager.Info("已清除所有碰撞测试记录");
}
#endregion
#region LogisticsAnimationManager迁移
/// <summary>
/// 为动画对象预计算并缓存排除列表
/// </summary>
/// <param name="animationObject">动画对象</param>
/// <returns>是否成功缓存</returns>
public bool PrecomputeCollisionExclusions(ModelItem animationObject)
{
try
{
LogManager.Info($"[缓存管理] 开始为动画对象预计算排除列表: {animationObject?.DisplayName ?? "NULL"}");
if (animationObject == null)
{
LogManager.Warning("[缓存管理] 动画对象为空,清除缓存");
ClearExclusionCache();
return false;
}
// 检查是否需要重新计算
if (IsCacheValid(animationObject))
{
LogManager.Info($"[缓存管理] 缓存仍然有效,跳过重新计算");
return true;
}
var startTime = DateTime.Now;
// 只构建特定于动画对象的逻辑排除列表
LogManager.Info("[缓存管理] 构建逻辑排除列表...");
var exclusionList = ModelItemAnalysisHelper.CollectRelatedNodes(animationObject);
// 更新缓存
_currentCachedAnimationObject = animationObject;
_cachedExclusionList = exclusionList;
_cacheCreatedTime = DateTime.Now;
var elapsedMs = (DateTime.Now - startTime).TotalMilliseconds;
LogManager.Info($"[缓存管理] 预计算完成,耗时 {elapsedMs:F1}ms缓存 {exclusionList.Count} 个排除对象");
return true;
}
catch (Exception ex)
{
LogManager.Error($"[缓存管理] 预计算排除列表失败: {ex.Message}", ex);
ClearExclusionCache();
return false;
}
}
/// <summary>
/// 获取缓存的排除列表
/// </summary>
/// <param name="animationObject">动画对象</param>
/// <returns>排除列表如果缓存无效则返回null</returns>
public List<ModelItem> GetCachedExclusionList(ModelItem animationObject)
{
try
{
if (!IsCacheValid(animationObject))
{
LogManager.Debug($"[缓存管理] 缓存无效,建议重新预计算");
return null;
}
LogManager.Debug($"[缓存管理] 返回缓存的排除列表: {_cachedExclusionList?.Count ?? 0} 个对象");
return _cachedExclusionList;
}
catch (Exception ex)
{
LogManager.Warning($"[缓存管理] 获取缓存失败: {ex.Message}");
return null;
}
}
/// <summary>
/// 检查缓存是否有效
/// </summary>
/// <param name="animationObject">当前动画对象</param>
/// <returns>缓存是否有效</returns>
private bool IsCacheValid(ModelItem animationObject)
{
try
{
// 检查是否有缓存
if (_currentCachedAnimationObject == null || _cachedExclusionList == null)
{
return false;
}
// 检查对象是否相同
if (!_currentCachedAnimationObject.Equals(animationObject))
{
string cachedObjectName = ModelItemAnalysisHelper.GetSafeDisplayName(_currentCachedAnimationObject);
string currentObjectName = ModelItemAnalysisHelper.GetSafeDisplayName(animationObject);
LogManager.Debug($"[缓存验证] 动画对象已变更: '{cachedObjectName}' -> '{currentObjectName}'");
return false;
}
// 检查缓存时间是否过期
if (DateTime.Now - _cacheCreatedTime > _cacheValidDuration)
{
LogManager.Debug($"[缓存验证] 缓存已过期: {(DateTime.Now - _cacheCreatedTime).TotalMinutes:F1} 分钟");
return false;
}
return true;
}
catch (Exception ex)
{
LogManager.Warning($"[缓存验证] 验证过程异常: {ex.Message}");
return false;
}
}
/// <summary>
/// 清除排除列表缓存
/// </summary>
public void ClearExclusionCache()
{
try
{
if (_currentCachedAnimationObject != null)
{
try
{
// 安全访问DisplayName防止WeakRef已释放的警告
string objectName = ModelItemAnalysisHelper.GetSafeDisplayName(_currentCachedAnimationObject);
LogManager.Debug($"[缓存管理] 清除缓存: {objectName}");
}
catch (Exception ex)
{
LogManager.Debug($"[缓存管理] 清除缓存时访问对象名称失败: {ex.Message}");
}
}
_currentCachedAnimationObject = null;
_cachedExclusionList = null;
_cacheCreatedTime = DateTime.MinValue;
}
catch (Exception ex)
{
LogManager.Warning($"[缓存管理] 清除缓存失败: {ex.Message}");
}
}
/// <summary>
/// 获取缓存统计信息
/// </summary>
/// <returns>缓存统计信息</returns>
public string GetCacheStats()
{
try
{
if (_currentCachedAnimationObject == null)
{
return "缓存状态: 无缓存";
}
var age = DateTime.Now - _cacheCreatedTime;
string objectName = ModelItemAnalysisHelper.GetSafeDisplayName(_currentCachedAnimationObject);
var count = _cachedExclusionList?.Count ?? 0;
return $"缓存状态: 对象='{objectName}', 排除数={count}, 年龄={age.TotalSeconds:F1}秒";
}
catch (Exception ex)
{
return $"缓存状态: 获取失败 - {ex.Message}";
}
}
#endregion
#region Human-in-the-Loop
/// <summary>
/// 添加排除物体
/// </summary>
/// <param name="obj">要排除的物体</param>
public void AddExcludedObject(ModelItem obj)
{
if (obj != null)
{
_excludedObjects.Add(obj);
LogManager.Debug($"[排除列表] 添加排除物体: {ModelItemAnalysisHelper.GetSafeDisplayName(obj)}, 当前共 {_excludedObjects.Count} 个");
}
}
/// <summary>
/// 批量添加排除物体
/// </summary>
/// <param name="objects">要排除的物体列表</param>
public void AddExcludedObjects(IEnumerable<ModelItem> objects)
{
if (objects != null)
{
foreach (var obj in objects)
{
if (obj != null) _excludedObjects.Add(obj);
}
LogManager.Info($"[排除列表] 批量添加 {objects.Count()} 个排除物体, 当前共 {_excludedObjects.Count} 个");
}
}
/// <summary>
/// 移除排除物体
/// </summary>
/// <param name="obj">要移除的物体</param>
public void RemoveExcludedObject(ModelItem obj)
{
if (obj != null && _excludedObjects.Remove(obj))
{
LogManager.Debug($"[排除列表] 移除排除物体: {ModelItemAnalysisHelper.GetSafeDisplayName(obj)}, 当前共 {_excludedObjects.Count} 个");
}
}
/// <summary>
/// 清除所有排除物体
/// </summary>
public void ClearExcludedObjects()
{
var count = _excludedObjects.Count;
_excludedObjects.Clear();
LogManager.Info($"[排除列表] 清除所有 {count} 个排除物体");
}
/// <summary>
/// 获取当前排除物体列表
/// </summary>
/// <returns>排除物体列表</returns>
public IReadOnlyCollection<ModelItem> GetExcludedObjects()
{
return _excludedObjects.ToList().AsReadOnly();
}
/// <summary>
/// 检查物体是否在排除列表中
/// </summary>
/// <param name="obj">要检查的物体</param>
/// <returns>是否被排除</returns>
public bool IsObjectExcluded(ModelItem obj)
{
return obj != null && _excludedObjects.Contains(obj);
}
/// <summary>
/// 获取排除物体数量
/// </summary>
public int ExcludedObjectCount => _excludedObjects.Count;
/// <summary>
/// 【已废弃】排除列表现在是实时管理的,不绑定到动画缓存
/// </summary>
[Obsolete("排除列表现在是实时管理的,此方法不再执行任何操作", false)]
public void SaveExclusionsToCache()
{
// 排除列表是实时管理的,不保存到动画缓存
LogManager.Debug($"[排除列表] 实时管理模式,当前共 {_excludedObjects.Count} 个排除物体");
}
/// <summary>
/// 【已废弃】排除列表现在是实时管理的,不绑定到动画缓存
/// </summary>
[Obsolete("排除列表现在是实时管理的此方法始终返回false", false)]
public bool LoadExclusionsFromCache()
{
// 排除列表是实时管理的,不从缓存加载
LogManager.Debug("[排除列表] 实时管理模式,不从缓存加载");
return false;
}
/// <summary>
/// 【已废弃】排除列表现在是实时管理的,不绑定到动画缓存
/// </summary>
[Obsolete("排除列表现在是实时管理的,此方法不再执行任何操作", false)]
public static void ClearAllExclusionCaches()
{
// 排除列表是实时管理的,没有缓存需要清除
LogManager.Debug("[排除列表] 实时管理模式,无缓存需要清除");
}
/// <summary>
/// 设置排除物体并清除当前动画缓存(用于重新生成)
/// </summary>
/// <param name="objects">新的排除物体列表</param>
public void SetExcludedObjectsAndClearCache(IEnumerable<ModelItem> objects)
{
// 1. 更新排除列表
_excludedObjects.Clear();
if (objects != null)
{
foreach (var obj in objects)
{
if (obj != null) _excludedObjects.Add(obj);
}
}
// 2. 清除当前动画缓存(强制重新预计算)
if (!string.IsNullOrEmpty(_currentAnimationHash))
{
_animationFrameCache.Remove(_currentAnimationHash);
_collisionResultCache.Remove(_currentAnimationHash);
LogManager.Info($"[排除列表] 已更新排除列表({_excludedObjects.Count}个)并清除当前动画缓存(排除列表实时生效)");
}
else
{
LogManager.Info($"[排除列表] 已更新排除列表({_excludedObjects.Count}个)(排除列表实时生效)");
}
}
/// <summary>
/// 从数据库加载排除对象
/// </summary>
public void LoadExcludedObjectsFromDatabase(PathDatabase database, string routeId = null)
{
try
{
if (database == null)
{
LogManager.Warning("[排除列表] 数据库为空,无法加载排除对象");
return;
}
// 从数据库获取排除对象记录
var records = database.GetExcludedObjects(routeId, includeGlobal: true);
if (records == null || records.Count == 0)
{
LogManager.Info($"[排除列表] 数据库中没有排除对象记录");
return;
}
// 将记录转换为 ModelItem
var doc = Autodesk.Navisworks.Api.Application.ActiveDocument;
if (doc == null || doc.IsClear)
{
LogManager.Warning("[排除列表] 没有活动文档,无法加载排除对象");
return;
}
var loadedObjects = new List<ModelItem>();
int notFoundCount = 0;
foreach (var record in records)
{
try
{
// 尝试通过 PathId 查找对象
var modelItem = FindModelItemByPathId(doc, record.PathId);
if (modelItem != null)
{
loadedObjects.Add(modelItem);
}
else
{
notFoundCount++;
LogManager.Debug($"[排除列表] 未找到排除对象: PathId={record.PathId}, Name={record.DisplayName}");
}
}
catch (Exception ex)
{
LogManager.Error($"[排除列表] 加载排除对象失败: {ex.Message}");
}
}
// 添加到排除列表
foreach (var obj in loadedObjects)
{
_excludedObjects.Add(obj);
}
LogManager.Info($"[排除列表] 从数据库加载 {loadedObjects.Count} 个排除对象,{notFoundCount} 个未找到");
}
catch (Exception ex)
{
LogManager.Error($"[排除列表] 从数据库加载排除对象失败: {ex.Message}", ex);
}
}
/// <summary>
/// 保存排除对象到数据库
/// </summary>
public void SaveExcludedObjectsToDatabase(PathDatabase database, string routeId = null)
{
try
{
if (database == null)
{
LogManager.Warning("[排除列表] 数据库为空,无法保存排除对象");
return;
}
// 清除该路径的旧排除对象记录
database.ClearExcludedObjects(routeId);
// 保存当前排除对象
var records = new List<ExcludedObjectRecord>();
foreach (var obj in _excludedObjects)
{
try
{
var record = new ExcludedObjectRecord
{
RouteId = routeId,
ModelIndex = GetModelIndex(obj),
PathId = GetModelItemPathId(obj),
DisplayName = obj.DisplayName,
ObjectName = GetModelItemObjectName(obj),
ExcludedTime = DateTime.Now,
Reason = "用户排除",
IsGlobal = string.IsNullOrEmpty(routeId)
};
records.Add(record);
}
catch (Exception ex)
{
LogManager.Error($"[排除列表] 转换排除对象记录失败: {ex.Message}");
}
}
// 批量保存到数据库
database.SaveExcludedObjects(records);
LogManager.Info($"[排除列表] 已保存 {records.Count} 个排除对象到数据库");
}
catch (Exception ex)
{
LogManager.Error($"[排除列表] 保存排除对象到数据库失败: {ex.Message}", ex);
}
}
/// <summary>
/// 根据 PathId 查找 ModelItem
/// </summary>
private ModelItem FindModelItemByPathId(Document doc, string pathId)
{
try
{
// 解析 PathId (格式: "模型索引:路径")
var parts = pathId.Split(':');
if (parts.Length < 2) return null;
if (!int.TryParse(parts[0], out int modelIndex)) return null;
// 获取模型
var model = doc.Models[modelIndex];
if (model == null) return null;
// 解析路径索引
var pathIndices = parts[1].Split(',')
.Select(p => int.TryParse(p, out int idx) ? idx : -1)
.Where(idx => idx >= 0)
.ToArray();
if (pathIndices.Length == 0) return null;
// 从根节点开始遍历
ModelItem current = model.RootItem;
foreach (var index in pathIndices)
{
if (current == null) return null;
// 将 Children 转换为列表以便索引访问
var childrenList = current.Children.ToList();
if (index >= childrenList.Count) return null;
current = childrenList[index];
}
return current;
}
catch (Exception ex)
{
LogManager.Error($"[排除列表] 查找 ModelItem 失败: {ex.Message}");
return null;
}
}
/// <summary>
/// 获取 ModelItem 的 PathId
/// </summary>
private string GetModelItemPathId(ModelItem item)
{
try
{
if (item == null) return string.Empty;
// 构建路径索引数组
var indices = new List<int>();
var current = item;
while (current != null && current.Parent != null)
{
var parent = current.Parent;
// 在父节点的子节点中查找当前节点的索引
var childrenList = parent.Children.ToList();
int index = childrenList.FindIndex(c => c == current);
if (index < 0) break;
indices.Insert(0, index);
current = parent;
}
// 获取模型索引
int modelIndex = GetModelIndex(item);
return $"{modelIndex}:{string.Join(",", indices)}";
}
catch (Exception ex)
{
LogManager.Error($"[排除列表] 获取 PathId 失败: {ex.Message}");
return string.Empty;
}
}
/// <summary>
/// 获取 ModelItem 所在的模型索引
/// </summary>
private int GetModelIndex(ModelItem item)
{
try
{
var doc = Autodesk.Navisworks.Api.Application.ActiveDocument;
if (doc == null) return -1;
// 找到根节点
var root = item;
while (root.Parent != null) root = root.Parent;
// 在模型列表中查找
for (int i = 0; i < doc.Models.Count; i++)
{
if (doc.Models[i].RootItem == root)
return i;
}
return -1;
}
catch
{
return -1;
}
}
/// <summary>
/// 获取 ModelItem 的对象名称
/// </summary>
private string GetModelItemObjectName(ModelItem item)
{
try
{
return item.ClassName ?? item.DisplayName ?? "Unknown";
}
catch
{
return "Unknown";
}
}
#endregion
#endregion
}
}