3791 lines
157 KiB
C#
3791 lines
157 KiB
C#
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
|
||
}
|
||
} |