5465 lines
237 KiB
C#
5465 lines
237 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using System.Collections.ObjectModel;
|
||
using System.Collections.Specialized;
|
||
using System.ComponentModel;
|
||
using System.Linq;
|
||
using System.Numerics;
|
||
using System.Runtime.CompilerServices;
|
||
using System.Threading.Tasks;
|
||
using System.Windows;
|
||
using System.Windows.Input;
|
||
using System.Windows.Threading;
|
||
using Autodesk.Navisworks.Api;
|
||
using Autodesk.Navisworks.Api.Clash;
|
||
using NavisworksTransport.Core;
|
||
using NavisworksTransport.Core.Models;
|
||
using NavisworksTransport.Core.Config;
|
||
using NavisworksTransport.Commands;
|
||
using NavisworksTransport.UI.WPF.Collections;
|
||
using NavisworksTransport.UI.WPF.Views;
|
||
using NavisworksTransport.Utils;
|
||
using NavisworksTransport.Utils.CoordinateSystem;
|
||
|
||
namespace NavisworksTransport.UI.WPF.ViewModels
|
||
{
|
||
/// <summary>
|
||
/// 碰撞热点信息 - 用于分析高频碰撞物体
|
||
/// </summary>
|
||
public class CollisionHotspotInfo
|
||
{
|
||
public ModelItem Object { get; set; }
|
||
public string ObjectName { get; set; }
|
||
public int CollisionCount { get; set; }
|
||
public double Percentage { get; set; }
|
||
public string Reason { get; set; }
|
||
public string RecommendedAction { get; set; }
|
||
}
|
||
|
||
/// <summary>
|
||
/// 碰撞报告数据
|
||
/// </summary>
|
||
public class CollisionReportData
|
||
{
|
||
public bool IsValid { get; set; }
|
||
public string ErrorMessage { get; set; } = string.Empty;
|
||
public DateTime GeneratedTime { get; set; }
|
||
public string AnimatedObjectName { get; set; } = string.Empty;
|
||
public string PathName { get; set; } = string.Empty;
|
||
public int TotalTests { get; set; }
|
||
public int TotalCollisions { get; set; }
|
||
public List<CollisionTestInfo> Tests { get; set; } = new List<CollisionTestInfo>();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 碰撞测试信息
|
||
/// </summary>
|
||
public class CollisionTestInfo
|
||
{
|
||
public string TestName { get; set; } = string.Empty;
|
||
public string TestType { get; set; } = string.Empty;
|
||
public double Tolerance { get; set; }
|
||
public int CollisionCount { get; set; }
|
||
public string Status { get; set; } = string.Empty;
|
||
public List<CollisionDetailInfo> Collisions { get; set; } = new List<CollisionDetailInfo>();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 碰撞详细信息
|
||
/// </summary>
|
||
public class CollisionDetailInfo
|
||
{
|
||
public string CollisionId { get; set; } = string.Empty;
|
||
public string Object1Name { get; set; } = string.Empty;
|
||
public string Object1Position { get; set; } = string.Empty;
|
||
public string Object2Name { get; set; } = string.Empty;
|
||
public string Object2Position { get; set; } = string.Empty;
|
||
public string CollisionCenter { get; set; } = string.Empty;
|
||
public double Distance { get; set; }
|
||
public string Status { get; set; } = string.Empty;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 手工指定的碰撞检测对象
|
||
/// </summary>
|
||
public class ManualCollisionTargetViewModel : INotifyPropertyChanged
|
||
{
|
||
private int _index;
|
||
|
||
public ManualCollisionTargetViewModel(ModelItem modelItem, string displayName, string modelPath, int index = 0)
|
||
{
|
||
ModelItem = modelItem ?? throw new ArgumentNullException(nameof(modelItem));
|
||
DisplayName = displayName;
|
||
ModelPath = modelPath;
|
||
InstanceGuid = modelItem.InstanceGuid;
|
||
_index = index;
|
||
}
|
||
|
||
public ModelItem ModelItem { get; }
|
||
public string DisplayName { get; }
|
||
public string ModelPath { get; }
|
||
public Guid InstanceGuid { get; }
|
||
|
||
public int Index
|
||
{
|
||
get => _index;
|
||
set
|
||
{
|
||
if (_index != value)
|
||
{
|
||
_index = value;
|
||
OnPropertyChanged();
|
||
}
|
||
}
|
||
}
|
||
|
||
public event PropertyChangedEventHandler PropertyChanged;
|
||
|
||
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
|
||
{
|
||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 排除对象ViewModel - 用于检测排除列表
|
||
/// </summary>
|
||
public class ExcludedObjectViewModel : INotifyPropertyChanged
|
||
{
|
||
private int _index;
|
||
|
||
public ExcludedObjectViewModel(ModelItem modelItem, string displayName, string modelPath, int index = 0)
|
||
{
|
||
ModelItem = modelItem ?? throw new ArgumentNullException(nameof(modelItem));
|
||
DisplayName = displayName;
|
||
ModelPath = modelPath;
|
||
InstanceGuid = modelItem.InstanceGuid;
|
||
_index = index;
|
||
}
|
||
|
||
public ModelItem ModelItem { get; }
|
||
public string DisplayName { get; }
|
||
public string ModelPath { get; }
|
||
public Guid InstanceGuid { get; }
|
||
|
||
public int Index
|
||
{
|
||
get => _index;
|
||
set
|
||
{
|
||
if (_index != value)
|
||
{
|
||
_index = value;
|
||
OnPropertyChanged();
|
||
}
|
||
}
|
||
}
|
||
|
||
public event PropertyChangedEventHandler PropertyChanged;
|
||
|
||
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
|
||
{
|
||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 碰撞构件ViewModel
|
||
/// </summary>
|
||
public class CollisionObjectViewModel
|
||
{
|
||
public int Index { get; set; }
|
||
public string DisplayName { get; set; }
|
||
public string ObjectName { get; set; }
|
||
public string ModelPath { get; set; }
|
||
public int ResultId { get; set; }
|
||
public string TestName { get; set; }
|
||
public int? ModelIndex { get; set; }
|
||
public string PathId { get; set; }
|
||
public ModelItem ModelItem { get; set; }
|
||
public ICommand HighlightCommand { get; set; }
|
||
public ICommand ClearHighlightCommand { get; set; }
|
||
|
||
// 动画跟踪点位置信息(用于还原碰撞场景)
|
||
public Point3D AnimatedObjectTrackedPosition { get; set; }
|
||
public double AnimatedObjectTrackedYawRadians { get; set; }
|
||
public Rotation3D AnimatedObjectTrackedRotation { get; set; }
|
||
public bool AnimatedObjectHasTrackedRotation { get; set; }
|
||
public bool HasPositionInfo { get; set; }
|
||
}
|
||
|
||
/// <summary>
|
||
/// ClashDetective碰撞检测结果视图模型
|
||
/// </summary>
|
||
public class ClashDetectiveResultViewModel
|
||
{
|
||
private readonly Action _refreshCallback;
|
||
|
||
public ClashDetectiveResultViewModel(CollisionDetectionRecord record, Action refreshCallback)
|
||
{
|
||
Record = record ?? throw new ArgumentNullException(nameof(record));
|
||
_refreshCallback = refreshCallback ?? throw new ArgumentNullException(nameof(refreshCallback));
|
||
ViewCommand = new RelayCommand(() => ExecuteView());
|
||
ReportCommand = new RelayCommand(() => ExecuteReport());
|
||
DeleteCommand = new RelayCommand(() => ExecuteDelete());
|
||
CollisionObjects = new ObservableCollection<CollisionObjectViewModel>();
|
||
}
|
||
|
||
public CollisionDetectionRecord Record { get; }
|
||
|
||
// 运动物体(从碰撞历史数据库中加载)
|
||
public ModelItem AnimatedObject { get; set; }
|
||
|
||
// 虚拟物体尺寸(当IsVirtualObject=true时使用)
|
||
public double VirtualObjectLengthInMeters { get; set; }
|
||
public double VirtualObjectWidthInMeters { get; set; }
|
||
public double VirtualObjectHeightInMeters { get; set; }
|
||
|
||
public string TestTimeDisplay => Record.TestTime?.ToString("MM-dd HH:mm:ss") ?? Record.CreatedTime.ToString("MM-dd HH:mm:ss");
|
||
public string CollisionCountDisplay => $"{Record.CollisionCount}个";
|
||
public ICommand ViewCommand { get; }
|
||
public ICommand ReportCommand { get; }
|
||
public ICommand DeleteCommand { get; }
|
||
public ObservableCollection<CollisionObjectViewModel> CollisionObjects { get; }
|
||
|
||
public void ExecuteHighlightCollisionObject(CollisionObjectViewModel collisionObject)
|
||
{
|
||
if (collisionObject == null) return;
|
||
|
||
try
|
||
{
|
||
LogManager.Info($"[碰撞构件] 高亮显示构件: {collisionObject.DisplayName}");
|
||
|
||
// 清除之前的高亮
|
||
ModelHighlightHelper.ClearAllHighlights();
|
||
|
||
// 直接使用ModelItem高亮
|
||
if (collisionObject.ModelItem != null && ModelItemAnalysisHelper.IsModelItemValid(collisionObject.ModelItem))
|
||
{
|
||
ModelHighlightHelper.HighlightItems(
|
||
ModelHighlightHelper.ClashDetectiveResultsCategory,
|
||
new[] { collisionObject.ModelItem });
|
||
LogManager.Info($"[碰撞构件] 已高亮构件: {collisionObject.DisplayName}");
|
||
}
|
||
else
|
||
{
|
||
LogManager.Warning($"[碰撞构件] ModelItem无效或为空: {collisionObject.DisplayName}");
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogManager.Error($"[碰撞构件] 高亮显示失败: {ex.Message}", ex);
|
||
}
|
||
}
|
||
|
||
private void ExecuteView()
|
||
{
|
||
// 高亮显示该次检测的碰撞对象
|
||
var clashIntegration = ClashDetectiveIntegration.Instance;
|
||
ModelHighlightHelper.HighlightClashDetectiveResults(Record.TestName, clashIntegration.GetCurrentPathClashResults);
|
||
}
|
||
|
||
private void ExecuteReport()
|
||
{
|
||
// 打开该次检测的详细报告
|
||
try
|
||
{
|
||
var testName = Record.TestName;
|
||
LogManager.Info($"[历史报告] 开始生成历史碰撞报告: {testName}");
|
||
|
||
// 直接使用现有的报告生成命令,但指定测试名称
|
||
// 常规检测已经高亮了,所以 hasHighlighted=true
|
||
var command = GenerateCollisionReportCommand.CreateComprehensive(hasHighlighted: true);
|
||
command.SetTestName(testName);
|
||
_ = command.ExecuteAsync();
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogManager.Error($"[历史报告] 生成历史报告失败: {ex.Message}", ex);
|
||
System.Windows.MessageBox.Show($"生成历史报告失败: {ex.Message}", "错误",
|
||
System.Windows.MessageBoxButton.OK, System.Windows.MessageBoxImage.Error);
|
||
}
|
||
}
|
||
|
||
private void ExecuteDelete()
|
||
{
|
||
// 删除该次检测记录
|
||
var pathDatabase = PathPlanningManager.Instance?.GetPathDatabase();
|
||
if (pathDatabase != null)
|
||
{
|
||
pathDatabase.DeleteDetectionResult(Record.Id);
|
||
// 通过回调刷新列表
|
||
_refreshCallback?.Invoke();
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 动画控制视图模型
|
||
/// 专门处理路径动画播放和碰撞检测功能
|
||
/// </summary>
|
||
public class AnimationControlViewModel : ViewModelBase, IDisposable
|
||
{
|
||
#region 私有字段
|
||
|
||
private readonly Core.Animation.PathAnimationManager _pathAnimationManager;
|
||
private readonly ClashDetectiveIntegration _clashIntegration;
|
||
private readonly UIStateManager _uiStateManager;
|
||
private PathPlanningManager _pathPlanningManager;
|
||
|
||
// 记录最近复用的历史检测记录ID(用于判断是否需要跳过动画生成)
|
||
private int? _lastReusedRecordId;
|
||
|
||
// 碰撞构件查看时保存的动画物体状态(用于恢复)
|
||
private ModelItem _savedAnimatedObjectForCollision;
|
||
|
||
#endregion
|
||
|
||
#region 事件
|
||
|
||
/// <summary>
|
||
/// 请求滚动到指定碰撞历史记录的事件(用于代码选中后UI自动滚动到该项)
|
||
/// </summary>
|
||
public event EventHandler<ClashDetectiveResultViewModel> RequestScrollToClashDetectiveResult;
|
||
private ModelItemTransformHelper.ObjectStateSnapshot _savedAnimatedObjectStateForCollision;
|
||
|
||
// 动画参数相关字段(从配置初始化)
|
||
private ObservableCollection<int> _availableFrameRates;
|
||
private int _selectedFrameRate;
|
||
private double _animationDuration;
|
||
private bool _canStartAnimation = true;
|
||
private bool _canPauseAnimation = false;
|
||
private bool _canStopAnimation = false;
|
||
|
||
// 碰撞检测相关字段
|
||
private CollisionReportResult _lastGeneratedReport; // 缓存最后生成的报告
|
||
|
||
// 碰撞检测参数字段(从配置初始化)
|
||
private double _detectionTolerance; // 检测容差(米)
|
||
private int _animationFrameRate; // 动画帧率(FPS)
|
||
|
||
|
||
// 当前选中路径
|
||
private PathRouteViewModel _currentPathRoute;
|
||
|
||
// 移动物体相关字段
|
||
private ModelItem _selectedAnimatedObject;
|
||
private string _selectedAnimatedObjectName;
|
||
private bool _hasSelectedAnimatedObject = false;
|
||
private bool _canGenerateAnimation = false;
|
||
|
||
// 虚拟物体相关字段
|
||
private bool _useVirtualObject = false; // 使用虚拟物体
|
||
private double _virtualObjectLengthInMeters; // 虚拟物体长度(米)
|
||
private double _virtualObjectWidthInMeters; // 虚拟物体宽度(米)
|
||
private double _virtualObjectHeightInMeters; // 虚拟物体高度(米)
|
||
private double _safetyMarginInMeters; // 检测间隙(米),从路径编辑同步
|
||
|
||
// 角度修正相关字段
|
||
private LocalEulerRotationCorrection _objectRotationCorrection = LocalEulerRotationCorrection.Zero; // 物体绕宿主 X/Y/Z 轴的角度修正
|
||
|
||
// 移动物体原始尺寸(米,在选择物体时保存)
|
||
private double _objectOriginalLength; // 物体原始长度(X方向)
|
||
private double _objectOriginalWidth; // 物体原始宽度(Y方向)
|
||
private double _objectOriginalHeight; // 物体原始高度(Z方向)
|
||
|
||
/// <summary>
|
||
/// 物体原始长度(X方向,米)- 选择物体时保存,不受旋转影响
|
||
/// </summary>
|
||
public double ObjectOriginalLength => _objectOriginalLength;
|
||
|
||
/// <summary>
|
||
/// 物体原始宽度(Y方向,米)- 选择物体时保存,不受旋转影响
|
||
/// </summary>
|
||
public double ObjectOriginalWidth => _objectOriginalWidth;
|
||
|
||
/// <summary>
|
||
/// 物体原始高度(Z方向,米)- 选择物体时保存,不受旋转影响
|
||
/// </summary>
|
||
public double ObjectOriginalHeight => _objectOriginalHeight;
|
||
|
||
// 手工碰撞对象相关字段
|
||
private bool _isManualCollisionTargetEnabled = true;
|
||
private ObservableCollection<ManualCollisionTargetViewModel> _manualCollisionTargets;
|
||
private string _manualCollisionTargetSummary = "未指定碰撞检测对象";
|
||
private DateTime? _manualTargetsLastSyncTime;
|
||
private const int ManualCollisionTargetLimit = 60;
|
||
private const string ManualTargetsHighlightCategory = ModelHighlightHelper.ManualTargetsCategory;
|
||
private const string CollisionResultsHighlightCategory = ModelHighlightHelper.PrecomputeCollisionResultsCategory;
|
||
|
||
// 检测排除对象相关字段
|
||
private ObservableCollection<ExcludedObjectViewModel> _excludedObjects;
|
||
private string _excludedObjectSummary = "未指定排除对象";
|
||
private const string ExcludedObjectsHighlightCategory = "excludedObjects"; // 与ModelHighlightHelper中定义的绿色类别一致
|
||
|
||
#endregion
|
||
|
||
#region 公共属性
|
||
|
||
|
||
/// <summary>
|
||
/// 可用帧率集合
|
||
/// </summary>
|
||
public ObservableCollection<int> AvailableFrameRates
|
||
{
|
||
get => _availableFrameRates;
|
||
set => SetProperty(ref _availableFrameRates, value);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 选中的帧率
|
||
/// </summary>
|
||
public int SelectedFrameRate
|
||
{
|
||
get => _selectedFrameRate;
|
||
set
|
||
{
|
||
if (SetProperty(ref _selectedFrameRate, value))
|
||
{
|
||
// 同步更新AnimationFrameRate字段
|
||
_animationFrameRate = value;
|
||
|
||
// 帧率变化时,步长需要重新计算(步长=路径长度/(帧率*时长))
|
||
OnPropertyChanged(nameof(CollisionDetectionAccuracy));
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 动画持续时间(秒)
|
||
/// </summary>
|
||
public double AnimationDuration
|
||
{
|
||
get => _animationDuration;
|
||
set
|
||
{
|
||
if (SetProperty(ref _animationDuration, value))
|
||
{
|
||
// 动画时长变化时,步长和速度需要重新计算
|
||
OnPropertyChanged(nameof(CollisionDetectionAccuracy));
|
||
OnPropertyChanged(nameof(MovementSpeed));
|
||
}
|
||
}
|
||
}
|
||
|
||
|
||
/// <summary>
|
||
/// 是否可以开始动画
|
||
/// </summary>
|
||
public bool CanStartAnimation
|
||
{
|
||
get => _canStartAnimation;
|
||
set => SetProperty(ref _canStartAnimation, value);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 是否可以暂停动画
|
||
/// </summary>
|
||
public bool CanPauseAnimation
|
||
{
|
||
get => _canPauseAnimation;
|
||
set => SetProperty(ref _canPauseAnimation, value);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 是否可以停止动画
|
||
/// </summary>
|
||
public bool CanStopAnimation
|
||
{
|
||
get => _canStopAnimation;
|
||
set => SetProperty(ref _canStopAnimation, value);
|
||
}
|
||
|
||
|
||
/// <summary>
|
||
/// 是否有碰撞检测结果
|
||
/// </summary>
|
||
private bool _hasClashDetectiveResults;
|
||
public bool HasClashDetectiveResults
|
||
{
|
||
get => _hasClashDetectiveResults;
|
||
set
|
||
{
|
||
if (SetProperty(ref _hasClashDetectiveResults, value))
|
||
{
|
||
System.Windows.Input.CommandManager.InvalidateRequerySuggested();
|
||
}
|
||
}
|
||
}
|
||
|
||
|
||
|
||
/// <summary>
|
||
/// 碰撞检测精度(米) - 步长,根据路径长度、帧率和时长计算得出
|
||
/// 如果无法计算则返回0,表示数据不完整
|
||
/// </summary>
|
||
public double CollisionDetectionAccuracy
|
||
{
|
||
get
|
||
{
|
||
var pathLength = GetPathLength();
|
||
if (pathLength > 0 && _animationFrameRate > 0 && _animationDuration > 0)
|
||
{
|
||
// 步长 = 路径长度 / (帧率 * 时长)
|
||
return Math.Round(pathLength / (_animationFrameRate * _animationDuration), 3);
|
||
}
|
||
|
||
// 无法计算时返回0
|
||
// 注意:如果路径为空(pathLength==0),这是正常的初始状态,不输出警告
|
||
// 只有在配置参数异常时才输出警告
|
||
if (pathLength > 0 && (_animationFrameRate <= 0 || _animationDuration <= 0))
|
||
{
|
||
LogManager.Warning($"[AnimationControlViewModel] 无法计算碰撞检测精度 - 路径长度:{pathLength}m, 帧率:{_animationFrameRate}fps, 时长:{_animationDuration}s");
|
||
}
|
||
return 0;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 运动速度(米/秒) - 根据路径长度和时长计算得出
|
||
/// 如果无法计算则返回0,表示数据不完整
|
||
/// </summary>
|
||
public double MovementSpeed
|
||
{
|
||
get
|
||
{
|
||
var pathLength = GetPathLength();
|
||
if (pathLength > 0 && _animationDuration > 0)
|
||
{
|
||
// 速度 = 路径长度 / 时长
|
||
return Math.Round(pathLength / _animationDuration, 2);
|
||
}
|
||
|
||
// 无法计算时返回0
|
||
// 注意:如果路径为空(pathLength==0),这是正常的初始状态,不输出警告
|
||
// 只有在配置参数异常时才输出警告
|
||
if (pathLength > 0 && _animationDuration <= 0)
|
||
{
|
||
LogManager.Warning($"[AnimationControlViewModel] 无法计算运动速度 - 路径长度:{pathLength}m, 时长:{_animationDuration}s");
|
||
}
|
||
return 0;
|
||
}
|
||
}
|
||
/// <summary>
|
||
/// 路径长度(米) - 计算得出
|
||
/// </summary>
|
||
public double PathLength
|
||
{
|
||
get
|
||
{
|
||
return GetPathLength();
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 动画帧率(FPS)
|
||
/// </summary>
|
||
public int AnimationFrameRate
|
||
{
|
||
get => _animationFrameRate;
|
||
set
|
||
{
|
||
if (SetProperty(ref _animationFrameRate, value))
|
||
{
|
||
// 帧率变化时,步长需要重新计算
|
||
OnPropertyChanged(nameof(CollisionDetectionAccuracy));
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 检测容差(米)
|
||
/// </summary>
|
||
public double DetectionTolerance
|
||
{
|
||
get => _detectionTolerance;
|
||
set
|
||
{
|
||
if (SetProperty(ref _detectionTolerance, value))
|
||
{
|
||
// 容差变化时,碰撞检测精度需要重新计算
|
||
OnPropertyChanged(nameof(CollisionDetectionAccuracy));
|
||
}
|
||
}
|
||
}
|
||
|
||
|
||
/// <summary>
|
||
/// 当前选中的路径
|
||
/// </summary>
|
||
public PathRouteViewModel CurrentPathRoute
|
||
{
|
||
get => _currentPathRoute;
|
||
set
|
||
{
|
||
var oldRoute = _currentPathRoute;
|
||
if (SetProperty(ref _currentPathRoute, value))
|
||
{
|
||
// 路径改变时,步长、速度和路径长度需要重新计算
|
||
OnPropertyChanged(nameof(CollisionDetectionAccuracy));
|
||
OnPropertyChanged(nameof(MovementSpeed));
|
||
OnPropertyChanged(nameof(PathLength));
|
||
UpdateCanGenerateAnimation();
|
||
|
||
// 路径改变时,刷新ClashDetective结果列表
|
||
RefreshClashDetectiveResultsList();
|
||
|
||
// 🔥 路径改变时,如果已选择物体或虚拟物体,立即移动到新路径起点
|
||
// 注意:只有路径真正改变时才移动(避免初始设置null时的不必要移动)
|
||
if (value != null && oldRoute != value)
|
||
{
|
||
MoveAnimatedObjectToPathStart();
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 选中的移动物体
|
||
/// </summary>
|
||
public ModelItem SelectedAnimatedObject
|
||
{
|
||
get => _selectedAnimatedObject;
|
||
set
|
||
{
|
||
if (SetProperty(ref _selectedAnimatedObject, value))
|
||
{
|
||
UpdateAnimatedObjectInfo();
|
||
UpdateCanGenerateAnimation();
|
||
|
||
// 预计算排除列表(异步)
|
||
_ = PrecomputeCollisionExclusionsAsync(value);
|
||
|
||
// 当选择了物体时,根据路径类型打开通行空间可视化
|
||
if (value != null)
|
||
{
|
||
UpdatePassageSpaceVisualization();
|
||
|
||
// 🔥 立即将物体移动到路径起点(如果路径已选择)
|
||
MoveAnimatedObjectToPathStart();
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 选中的移动物体名称
|
||
/// </summary>
|
||
public string SelectedAnimatedObjectName
|
||
{
|
||
get => _selectedAnimatedObjectName;
|
||
set => SetProperty(ref _selectedAnimatedObjectName, value);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 是否已选择移动物体
|
||
/// </summary>
|
||
public bool HasSelectedAnimatedObject
|
||
{
|
||
get => _hasSelectedAnimatedObject;
|
||
set => SetProperty(ref _hasSelectedAnimatedObject, value);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 是否可以生成动画
|
||
/// </summary>
|
||
public bool CanGenerateAnimation
|
||
{
|
||
get => _canGenerateAnimation;
|
||
set => SetProperty(ref _canGenerateAnimation, value);
|
||
}
|
||
|
||
#region 虚拟物体属性
|
||
|
||
/// <summary>
|
||
/// 是否使用选择的模型物体
|
||
/// </summary>
|
||
/// <summary>
|
||
/// 是否使用虚拟物体
|
||
/// </summary>
|
||
public bool UseVirtualObject
|
||
{
|
||
get => _useVirtualObject;
|
||
set
|
||
{
|
||
if (SetProperty(ref _useVirtualObject, value))
|
||
{
|
||
// ✨ 模式切换清理:切换到虚拟物体模式时,移除选择物体的动画数据
|
||
if (value)
|
||
{
|
||
try
|
||
{
|
||
// 🔥 将之前选择的实体物体归位(如果存在)
|
||
// 注意:使用 SelectedAnimatedObject 而不是 _pathAnimationManager.AnimatedObject
|
||
// 因为用户可能只选择了物体但还没有生成动画
|
||
if (_pathAnimationManager != null &&
|
||
SelectedAnimatedObject != null &&
|
||
!VirtualObjectManager.Instance.IsVirtualObjectActive)
|
||
{
|
||
// 先将实体物体设置到动画管理器,然后归位
|
||
_pathAnimationManager.SetAnimatedObject(SelectedAnimatedObject);
|
||
_pathAnimationManager.RestoreObjectToCADPosition();
|
||
_pathAnimationManager.ClearAnimationResults(); // 清理手动选择物体留下的动画数据
|
||
LogManager.Info("已切换到虚拟物体模式,将之前的手动选择物体归位并清理动画数据");
|
||
}
|
||
|
||
// 显示虚拟物体
|
||
LogManager.Info("正在显示虚拟物体...");
|
||
VirtualObjectManager.Instance.ShowVirtualObject(
|
||
VirtualObjectLengthInMeters, VirtualObjectWidthInMeters, VirtualObjectHeightInMeters);
|
||
|
||
// 重置角度修正值(虚拟物体重新选择时重置)
|
||
ObjectRotationCorrection = LocalEulerRotationCorrection.Zero;
|
||
LogManager.Debug("[切换虚拟物体] 已重置角度修正值为0");
|
||
|
||
// 当切换到虚拟物体模式时,根据路径类型打开通行空间可视化
|
||
UpdatePassageSpaceVisualization();
|
||
|
||
// 设置已选择物体状态(虚拟物体也算已选择)
|
||
HasSelectedAnimatedObject = true;
|
||
|
||
// 🔥 立即将虚拟物体移动到路径起点(如果路径已选择)
|
||
MoveAnimatedObjectToPathStart();
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogManager.Warning($"切换到虚拟物体模式失败: {ex.Message}");
|
||
}
|
||
}
|
||
else
|
||
{
|
||
// ✨ 模式切换清理:切换到手动选择模式时,隐藏虚拟物体并清理数据
|
||
try
|
||
{
|
||
// 🔥 先将虚拟物体归位(如果之前被移动过)
|
||
if (_pathAnimationManager != null &&
|
||
VirtualObjectManager.Instance.IsVirtualObjectActive)
|
||
{
|
||
var metersToUnits = UnitsConverter.GetMetersToUnitsConversionFactor();
|
||
_pathAnimationManager.SetVirtualObjectParameters(true,
|
||
VirtualObjectLengthInMeters * metersToUnits,
|
||
VirtualObjectWidthInMeters * metersToUnits,
|
||
VirtualObjectHeightInMeters * metersToUnits);
|
||
_pathAnimationManager.RestoreObjectToCADPosition();
|
||
LogManager.Info("已切换到手动选择模式,虚拟物体已归位");
|
||
}
|
||
|
||
// 隐藏虚拟物体
|
||
VirtualObjectManager.Instance.HideVirtualObject();
|
||
_pathAnimationManager?.ClearAnimationResults(); // 清理虚拟物体留下的动画数据
|
||
LogManager.Info("已切换到手动选择模式,自动隐藏虚拟物体并清理动画数据");
|
||
|
||
// 🔥 如果已选择实体物体且路径已选择,将实体物体移动到路径起点
|
||
if (SelectedAnimatedObject != null && CurrentPathRoute != null)
|
||
{
|
||
MoveAnimatedObjectToPathStart();
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogManager.Warning($"隐藏虚拟物体失败: {ex.Message}");
|
||
}
|
||
}
|
||
UpdateCanGenerateAnimation();
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 虚拟物体长度(米)- 只读,从路径编辑同步
|
||
/// </summary>
|
||
public double VirtualObjectLengthInMeters
|
||
{
|
||
get => _virtualObjectLengthInMeters;
|
||
private set => SetProperty(ref _virtualObjectLengthInMeters, value);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 虚拟物体宽度(米)- 只读,从路径编辑同步
|
||
/// </summary>
|
||
public double VirtualObjectWidthInMeters
|
||
{
|
||
get => _virtualObjectWidthInMeters;
|
||
private set => SetProperty(ref _virtualObjectWidthInMeters, value);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 虚拟物体高度(米)- 只读,从路径编辑同步
|
||
/// </summary>
|
||
public double VirtualObjectHeightInMeters
|
||
{
|
||
get => _virtualObjectHeightInMeters;
|
||
private set => SetProperty(ref _virtualObjectHeightInMeters, value);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 物体绕宿主 X/Y/Z 轴的角度修正。
|
||
/// </summary>
|
||
public LocalEulerRotationCorrection ObjectRotationCorrection
|
||
{
|
||
get => _objectRotationCorrection;
|
||
set
|
||
{
|
||
if (SetProperty(ref _objectRotationCorrection, value))
|
||
{
|
||
// 角度修正值改变时,更新物体朝向和通行空间
|
||
UpdateObjectRotation();
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 设置虚拟物体参数(由主ViewModel调用)
|
||
/// 参数单位:米
|
||
/// </summary>
|
||
public void SetVirtualObjectParameters(double length, double width, double height, double safetyMarginInMeters)
|
||
{
|
||
LogManager.Info($"[AnimationControlViewModel] SetVirtualObjectParameters被调用: length={length:F4}, width={width:F4}, height={height:F4}, safetyMarginInMeters={safetyMarginInMeters:F4}");
|
||
VirtualObjectLengthInMeters = length;
|
||
VirtualObjectWidthInMeters = width;
|
||
VirtualObjectHeightInMeters = height;
|
||
_safetyMarginInMeters = safetyMarginInMeters;
|
||
|
||
LogManager.Info($"[AnimationControlViewModel] 虚拟物体参数已更新: {length:F1}米 × {width:F1}米 × {height:F1}米, 检测间隙: {safetyMarginInMeters:F2}米, _safetyMarginInMeters={_safetyMarginInMeters:F4}");
|
||
|
||
// 如果当前使用虚拟物体模式,更新生成动画的可用状态
|
||
if (UseVirtualObject)
|
||
{
|
||
UpdateCanGenerateAnimation();
|
||
// 更新通行空间可视化
|
||
UpdatePassageSpaceVisualization();
|
||
}
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region 手工碰撞对象属性
|
||
|
||
public bool IsManualCollisionTargetEnabled
|
||
{
|
||
get => _isManualCollisionTargetEnabled;
|
||
set
|
||
{
|
||
if (SetProperty(ref _isManualCollisionTargetEnabled, value))
|
||
{
|
||
HandleManualCollisionModeChanged();
|
||
}
|
||
}
|
||
}
|
||
|
||
public ObservableCollection<ManualCollisionTargetViewModel> ManualCollisionTargets => _manualCollisionTargets;
|
||
|
||
public string ManualCollisionTargetSummary
|
||
{
|
||
get => _manualCollisionTargetSummary;
|
||
set => SetProperty(ref _manualCollisionTargetSummary, value);
|
||
}
|
||
|
||
public bool HasManualCollisionTargets => _manualCollisionTargets?.Count > 0;
|
||
|
||
public bool IsManualTargetModeActive => IsManualCollisionTargetEnabled && HasManualCollisionTargets;
|
||
|
||
// 检测排除对象公共属性
|
||
public ObservableCollection<ExcludedObjectViewModel> ExcludedObjects => _excludedObjects;
|
||
|
||
public string ExcludedObjectSummary
|
||
{
|
||
get => _excludedObjectSummary;
|
||
set => SetProperty(ref _excludedObjectSummary, value);
|
||
}
|
||
|
||
public bool HasExcludedObjects => _excludedObjects?.Count > 0;
|
||
|
||
#endregion
|
||
|
||
#region ClashDetective结果管理
|
||
|
||
private ObservableCollection<ClashDetectiveResultViewModel> _clashDetectiveResults = new ObservableCollection<ClashDetectiveResultViewModel>();
|
||
private ClashDetectiveResultViewModel _selectedClashDetectiveResult;
|
||
|
||
public ObservableCollection<ClashDetectiveResultViewModel> ClashDetectiveResults => _clashDetectiveResults;
|
||
|
||
public ClashDetectiveResultViewModel SelectedClashDetectiveResult
|
||
{
|
||
get => _selectedClashDetectiveResult;
|
||
set
|
||
{
|
||
if (_selectedClashDetectiveResult != value)
|
||
{
|
||
// 切换前恢复动画物体状态
|
||
if (_selectedClashDetectiveResult != null)
|
||
{
|
||
RestoreAnimatedObjectFromCollisionView();
|
||
}
|
||
|
||
_selectedClashDetectiveResult = value;
|
||
OnPropertyChanged();
|
||
|
||
// 触发滚动事件,让View滚动到选中的项
|
||
if (_selectedClashDetectiveResult != null)
|
||
{
|
||
RequestScrollToClashDetectiveResult?.Invoke(this, _selectedClashDetectiveResult);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
public ICommand RefreshClashDetectiveResultsCommand { get; private set; }
|
||
|
||
#endregion
|
||
|
||
|
||
|
||
#region 媒体控制属性
|
||
|
||
/// <summary>
|
||
/// 时间轴位置(0-总帧数)
|
||
/// </summary>
|
||
public double TimelinePosition
|
||
{
|
||
get => _pathAnimationManager?.CurrentFrame ?? 0;
|
||
set
|
||
{
|
||
if (_pathAnimationManager != null)
|
||
{
|
||
_pathAnimationManager.SeekToFrame((int)Math.Round(value));
|
||
}
|
||
OnPropertyChanged();
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 时间轴持续时间(总帧数)
|
||
/// </summary>
|
||
public double TimelineDuration => _pathAnimationManager?.TotalFrames - 1 ?? 0;
|
||
|
||
/// <summary>
|
||
/// 当前时间显示(格式化)
|
||
/// </summary>
|
||
public string CurrentTimeDisplay
|
||
{
|
||
get
|
||
{
|
||
if (_pathAnimationManager == null) return "00:00";
|
||
|
||
var currentSeconds = (_pathAnimationManager.CurrentFrame / (double)(_pathAnimationManager.TotalFrames - 1)) * _animationDuration;
|
||
return FormatTime(currentSeconds);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 总时长显示(格式化)
|
||
/// </summary>
|
||
public string TotalTimeDisplay => FormatTime(_animationDuration);
|
||
|
||
/// <summary>
|
||
/// 帧信息显示
|
||
/// </summary>
|
||
public string FrameInfoDisplay
|
||
{
|
||
get
|
||
{
|
||
if (_pathAnimationManager == null) return "0/0";
|
||
return $"{_pathAnimationManager.CurrentFrame + 1}/{_pathAnimationManager.TotalFrames}";
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 是否正在正向播放
|
||
/// </summary>
|
||
public bool IsPlayingForward => _pathAnimationManager?.PlaybackDirection == 1 &&
|
||
_pathAnimationManager?.CurrentState == NavisworksTransport.Core.Animation.AnimationState.Playing;
|
||
|
||
/// <summary>
|
||
/// 是否正在反向播放
|
||
/// </summary>
|
||
public bool IsPlayingReverse => _pathAnimationManager?.PlaybackDirection == -1 &&
|
||
_pathAnimationManager?.CurrentState == NavisworksTransport.Core.Animation.AnimationState.Playing;
|
||
|
||
#endregion
|
||
|
||
|
||
#endregion
|
||
|
||
#region 命令
|
||
|
||
// 原有命令
|
||
public ICommand StartAnimationCommand { get; private set; }
|
||
public ICommand PauseAnimationCommand { get; private set; }
|
||
public ICommand StopAnimationCommand { get; private set; }
|
||
public ICommand ViewCollisionReportCommand { get; private set; }
|
||
public ICommand SelectAnimatedObjectCommand { get; private set; }
|
||
public ICommand ClearAnimatedObjectCommand { get; private set; }
|
||
public ICommand EditObjectRotationCommand { get; private set; }
|
||
public ICommand GenerateAnimationCommand { get; private set; }
|
||
public ICommand AddToBatchQueueCommand { get; private set; }
|
||
public ICommand ApplyManualTargetsFromSelectionCommand { get; private set; }
|
||
public ICommand ClearManualTargetsCommand { get; private set; }
|
||
public ICommand RemoveManualTargetCommand { get; private set; }
|
||
public ICommand HighlightManualTargetsCommand { get; private set; }
|
||
public ICommand ClearManualHighlightsCommand { get; private set; }
|
||
|
||
// 检测排除对象命令
|
||
public ICommand AddExcludedObjectsFromSelectionCommand { get; private set; }
|
||
public ICommand ClearExcludedObjectsCommand { get; private set; }
|
||
public ICommand RemoveExcludedObjectCommand { get; private set; }
|
||
public ICommand HighlightAllExcludedObjectsCommand { get; private set; }
|
||
public ICommand ClearExcludedHighlightCommand { get; private set; }
|
||
public ICommand HighlightPrecomputedCollisionResultsCommand { get; private set; }
|
||
public ICommand ClearPrecomputedCollisionHighlightsCommand { get; private set; }
|
||
public ICommand ToggleWireframeModeCommand { get; private set; }
|
||
public ICommand HighlightClashDetectiveResultsCommand { get; private set; }
|
||
public ICommand ClearClashDetectiveHighlightsCommand { get; private set; }
|
||
|
||
// 媒体控制命令
|
||
public ICommand PlayForwardCommand { get; private set; }
|
||
public ICommand PlayReverseCommand { get; private set; }
|
||
public ICommand StepForwardCommand { get; private set; }
|
||
public ICommand StepBackwardCommand { get; private set; }
|
||
public ICommand FastForwardCommand { get; private set; }
|
||
public ICommand FastBackwardCommand { get; private set; }
|
||
public ICommand SeekToStartCommand { get; private set; }
|
||
public ICommand SeekToEndCommand { get; private set; }
|
||
|
||
#endregion
|
||
|
||
#region 构造函数
|
||
|
||
public AnimationControlViewModel() : this(null)
|
||
{
|
||
// 调用带参数的构造函数,传入null
|
||
}
|
||
|
||
/// <summary>
|
||
/// 带主ViewModel参数的构造函数,支持统一状态栏
|
||
/// </summary>
|
||
/// <param name="mainViewModel">主ViewModel,用于统一状态栏</param>
|
||
public AnimationControlViewModel(LogisticsControlViewModel mainViewModel) : base()
|
||
{
|
||
try
|
||
{
|
||
// 设置主ViewModel引用到基类
|
||
SetMainViewModel(mainViewModel);
|
||
|
||
// 初始化管理器 - 使用单例模式确保与GenerateCollisionReportCommand使用同一实例
|
||
_pathAnimationManager = NavisworksTransport.Core.Animation.PathAnimationManager.GetInstance();
|
||
_clashIntegration = ClashDetectiveIntegration.Instance;
|
||
_uiStateManager = UIStateManager.Instance;
|
||
|
||
// 订阅PathAnimationManager事件
|
||
_pathAnimationManager.ProgressChanged += OnAnimationProgressChanged;
|
||
_pathAnimationManager.StateChanged += OnAnimationStateChanged;
|
||
|
||
// 初始化集合
|
||
AvailableFrameRates = new ThreadSafeObservableCollection<int>();
|
||
_manualCollisionTargets = new ObservableCollection<ManualCollisionTargetViewModel>();
|
||
_manualCollisionTargets.CollectionChanged += OnManualCollisionTargetsChanged;
|
||
UpdateManualCollisionTargetSummary();
|
||
|
||
// 初始化检测排除对象集合
|
||
_excludedObjects = new ObservableCollection<ExcludedObjectViewModel>();
|
||
_excludedObjects.CollectionChanged += OnExcludedObjectsChanged;
|
||
UpdateExcludedObjectSummary();
|
||
|
||
// 初始化设置
|
||
InitializeAnimationSettings();
|
||
|
||
// 初始化生成动画按钮状态(启用手工指定模式时检查列表)
|
||
UpdateCanGenerateAnimation();
|
||
|
||
// 初始化命令
|
||
InitializeCommands();
|
||
|
||
// 订阅碰撞检测事件
|
||
_clashIntegration.CollisionDetected += OnCollisionDetected;
|
||
_clashIntegration.ClashDetectiveResultSaved += OnClashDetectiveResultSaved;
|
||
|
||
// 订阅文档状态事件
|
||
DocumentStateManager.Instance.DocumentInvalidated += OnDocumentInvalidated;
|
||
DocumentStateManager.Instance.DocumentReady += OnDocumentReady;
|
||
|
||
// 🔥 如果文档已经有效,立即刷新碰撞检测历史列表
|
||
if (DocumentStateManager.Instance.IsDocumentValid)
|
||
{
|
||
RefreshClashDetectiveResultsList();
|
||
LogManager.Info("[AnimationControlViewModel] 文档已有效,初始化时加载碰撞检测历史列表");
|
||
}
|
||
|
||
// 设置静态实例
|
||
_instance = this;
|
||
|
||
LogManager.Info("AnimationControlViewModel初始化完成 - 支持统一状态栏");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogManager.Error($"AnimationControlViewModel初始化失败: {ex.Message}", ex);
|
||
UpdateMainStatus("初始化失败,请检查日志");
|
||
throw;
|
||
}
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region 公共方法
|
||
|
||
/// <summary>
|
||
/// 设置PathPlanningManager(用于批处理队列功能)
|
||
/// </summary>
|
||
public void SetPathPlanningManager(PathPlanningManager pathPlanningManager)
|
||
{
|
||
_pathPlanningManager = pathPlanningManager;
|
||
LogManager.Info("AnimationControlViewModel已设置PathPlanningManager");
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region 初始化方法
|
||
|
||
/// <summary>
|
||
/// 初始化动画设置
|
||
/// </summary>
|
||
private void InitializeAnimationSettings()
|
||
{
|
||
// 从配置加载动画参数
|
||
var config = NavisworksTransport.Core.Config.ConfigManager.Instance.Current;
|
||
|
||
// 初始化可用帧率
|
||
AvailableFrameRates.Clear();
|
||
var frameRates = new[] { 15, 24, 30, 60 };
|
||
foreach (var rate in frameRates)
|
||
{
|
||
AvailableFrameRates.Add(rate);
|
||
}
|
||
|
||
// 从配置读取帧率
|
||
SelectedFrameRate = config.Animation.FrameRate;
|
||
|
||
// 从配置读取动画持续时间
|
||
AnimationDuration = config.Animation.DurationSeconds;
|
||
|
||
// 设置初始状态 - 默认状态未激活,等待有效动画
|
||
CanPauseAnimation = false;
|
||
CanStopAnimation = false;
|
||
|
||
// 初始化碰撞检测状态
|
||
HasClashDetectiveResults = false;
|
||
UpdateMainStatus("碰撞检测就绪");
|
||
|
||
// 从配置读取安全间隙(用于预计算)- 使用米单位接口
|
||
_safetyMarginInMeters = config.PathEditing.SafetyMarginMeters;
|
||
AnimationFrameRate = config.Animation.FrameRate;
|
||
|
||
// 从配置读取检测容差(用于ClashDetective)- 使用米单位接口用于显示
|
||
_detectionTolerance = config.Animation.DetectionToleranceMeters;
|
||
|
||
// 🔥 从配置加载虚拟物体尺寸(与路径编辑保持一致,使用米单位接口)
|
||
VirtualObjectLengthInMeters = config.PathEditing.ObjectLengthMeters;
|
||
VirtualObjectWidthInMeters = config.PathEditing.ObjectWidthMeters;
|
||
VirtualObjectHeightInMeters = config.PathEditing.ObjectHeightMeters;
|
||
|
||
// 设置动画按钮的初始状态
|
||
UpdateAnimationButtonStates();
|
||
|
||
LogManager.Info($"动画设置初始化完成 - 帧率:{SelectedFrameRate}fps, 持续时间:{AnimationDuration}秒, 安全间隙:{_safetyMarginInMeters}米");
|
||
|
||
// 订阅配置变更事件
|
||
SubscribeToConfigChanges();
|
||
}
|
||
|
||
#region 配置变更处理
|
||
|
||
/// <summary>
|
||
/// 订阅配置变更事件
|
||
/// </summary>
|
||
private void SubscribeToConfigChanges()
|
||
{
|
||
try
|
||
{
|
||
ConfigManager.Instance.CategoryConfigurationChanged += OnCategoryConfigurationChanged;
|
||
LogManager.Info("AnimationControlViewModel 已订阅配置变更事件");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogManager.Error($"订阅配置变更事件失败: {ex.Message}", ex);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 取消订阅配置变更事件
|
||
/// </summary>
|
||
private void UnsubscribeFromConfigChanges()
|
||
{
|
||
try
|
||
{
|
||
ConfigManager.Instance.CategoryConfigurationChanged -= OnCategoryConfigurationChanged;
|
||
LogManager.Info("AnimationControlViewModel 已取消订阅配置变更事件");
|
||
}
|
||
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;
|
||
|
||
// 更新动画参数
|
||
SelectedFrameRate = config.Animation.FrameRate;
|
||
AnimationDuration = config.Animation.DurationSeconds;
|
||
AnimationFrameRate = config.Animation.FrameRate;
|
||
_detectionTolerance = config.Animation.DetectionToleranceMeters;
|
||
|
||
LogManager.Info($"Animation 配置已更新 - 帧率:{SelectedFrameRate}fps, 持续时间:{AnimationDuration}秒");
|
||
}
|
||
|
||
// 处理 PathEditing 类别变更(物体参数和安全间隙)
|
||
if (category == ConfigCategory.PathEditing || category == ConfigCategory.All)
|
||
{
|
||
LogManager.Info("收到 PathEditing 配置变更通知,正在更新物体参数...");
|
||
|
||
var config = ConfigManager.Instance.Current;
|
||
|
||
// 更新物体参数
|
||
VirtualObjectLengthInMeters = config.PathEditing.ObjectLengthMeters;
|
||
VirtualObjectWidthInMeters = config.PathEditing.ObjectWidthMeters;
|
||
VirtualObjectHeightInMeters = config.PathEditing.ObjectHeightMeters;
|
||
_safetyMarginInMeters = config.PathEditing.SafetyMarginMeters;
|
||
|
||
LogManager.Info($"物体参数已更新 - 尺寸:{VirtualObjectLengthInMeters:F1}x{VirtualObjectWidthInMeters:F1}x{VirtualObjectHeightInMeters:F1}米, 安全间隙:{_safetyMarginInMeters:F2}米");
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogManager.Error($"处理配置变更事件失败: {ex.Message}", ex);
|
||
}
|
||
}
|
||
|
||
#endregion
|
||
|
||
/// <summary>
|
||
/// 初始化命令
|
||
/// </summary>
|
||
private void InitializeCommands()
|
||
{
|
||
StartAnimationCommand = new RelayCommand(async () => await ExecuteStartAnimationAsync(), () => CanStartAnimation);
|
||
PauseAnimationCommand = new RelayCommand(async () => await ExecutePauseAnimationAsync(), () => CanPauseAnimation);
|
||
StopAnimationCommand = new RelayCommand(async () => await ExecuteStopAnimationAsync(), () => CanStopAnimation);
|
||
ViewCollisionReportCommand = new RelayCommand(async () => await ExecuteViewCollisionReport(), () => HasClashDetectiveResults);
|
||
SelectAnimatedObjectCommand = new RelayCommand(() =>
|
||
{
|
||
// 重置角度修正值(每个物体的旋转独立)
|
||
ObjectRotationCorrection = LocalEulerRotationCorrection.Zero;
|
||
LogManager.Debug("[选择物体] 已重置角度修正值为0");
|
||
ExecuteSelectAnimatedObject();
|
||
});
|
||
ClearAnimatedObjectCommand = new RelayCommand(ExecuteClearAnimatedObject, () => HasSelectedAnimatedObject);
|
||
EditObjectRotationCommand = new RelayCommand(ExecuteEditObjectRotation, () => HasSelectedAnimatedObject);
|
||
GenerateAnimationCommand = new RelayCommand(ExecuteGenerateAnimation, () => CanGenerateAnimation);
|
||
AddToBatchQueueCommand = new RelayCommand(ExecuteAddToBatchQueue, () => CanGenerateAnimation);
|
||
ApplyManualTargetsFromSelectionCommand = new RelayCommand(ExecuteApplyManualTargetsFromSelection);
|
||
ClearManualTargetsCommand = new RelayCommand(ExecuteClearManualTargets, () => HasManualCollisionTargets);
|
||
RemoveManualTargetCommand = new RelayCommand<ManualCollisionTargetViewModel>(ExecuteRemoveManualTarget, target => target != null);
|
||
HighlightManualTargetsCommand = new RelayCommand(ExecuteHighlightManualTargets, () => HasManualCollisionTargets);
|
||
ClearManualHighlightsCommand = new RelayCommand(ExecuteClearManualHighlights, () => HasManualCollisionTargets);
|
||
|
||
// 检测排除对象命令
|
||
AddExcludedObjectsFromSelectionCommand = new RelayCommand(ExecuteAddExcludedObjectsFromSelection);
|
||
ClearExcludedObjectsCommand = new RelayCommand(ExecuteClearExcludedObjects, () => HasExcludedObjects);
|
||
RemoveExcludedObjectCommand = new RelayCommand<ExcludedObjectViewModel>(ExecuteRemoveExcludedObject, obj => obj != null);
|
||
HighlightAllExcludedObjectsCommand = new RelayCommand(ExecuteHighlightAllExcludedObjects, () => HasExcludedObjects);
|
||
ClearExcludedHighlightCommand = new RelayCommand(ExecuteClearExcludedHighlight, () => HasExcludedObjects);
|
||
HighlightPrecomputedCollisionResultsCommand = new RelayCommand(ExecuteHighlightPrecomputedCollisionResults, () => HasClashDetectiveResults);
|
||
ClearPrecomputedCollisionHighlightsCommand = new RelayCommand(ExecuteClearPrecomputedCollisionHighlights, () => HasClashDetectiveResults);
|
||
ToggleWireframeModeCommand = new RelayCommand(ExecuteToggleWireframeMode);
|
||
HighlightClashDetectiveResultsCommand = new RelayCommand(ExecuteHighlightClashDetectiveResults, () => HasClashDetectiveResults);
|
||
ClearClashDetectiveHighlightsCommand = new RelayCommand(ExecuteClearClashDetectiveHighlights, () => HasClashDetectiveResults);
|
||
RefreshClashDetectiveResultsCommand = new RelayCommand(ExecuteRefreshClashDetectiveResults);
|
||
PlayForwardCommand = new RelayCommand(ExecutePlayForward, CanExecuteMediaCommands);
|
||
PlayReverseCommand = new RelayCommand(ExecutePlayReverse, CanExecuteMediaCommands);
|
||
StepForwardCommand = new RelayCommand(ExecuteStepForward, CanExecuteMediaCommands);
|
||
StepBackwardCommand = new RelayCommand(ExecuteStepBackward, CanExecuteMediaCommands);
|
||
FastForwardCommand = new RelayCommand(ExecuteFastForward, CanExecuteMediaCommands);
|
||
FastBackwardCommand = new RelayCommand(ExecuteFastBackward, CanExecuteMediaCommands);
|
||
SeekToStartCommand = new RelayCommand(ExecuteSeekToStart, CanExecuteMediaCommands);
|
||
SeekToEndCommand = new RelayCommand(ExecuteSeekToEnd, CanExecuteMediaCommands);
|
||
|
||
LogManager.Info("动画控制命令初始化完成");
|
||
}
|
||
|
||
|
||
#endregion
|
||
|
||
#region 命令实现
|
||
|
||
/// <summary>
|
||
/// 开始动画命令
|
||
/// </summary>
|
||
private async Task ExecuteStartAnimationAsync()
|
||
{
|
||
if (!CanStartAnimation || CurrentPathRoute == null || CurrentPathRoute.Points.Count < 2)
|
||
{
|
||
await _uiStateManager.ExecuteUIUpdateAsync(() =>
|
||
{
|
||
UpdateMainStatus("请先选择包含至少2个点的路径");
|
||
});
|
||
return;
|
||
}
|
||
|
||
try
|
||
{
|
||
// 根据PathAnimationManager的状态决定操作
|
||
if (_pathAnimationManager.CurrentState == NavisworksTransport.Core.Animation.AnimationState.Paused)
|
||
{
|
||
// 从暂停状态恢复
|
||
_pathAnimationManager.ResumeAnimation();
|
||
LogManager.Info("从暂停状态恢复动画播放");
|
||
return; // 恢复动画后直接返回,不再执行StartAnimation
|
||
}
|
||
else if (_pathAnimationManager.IsAnimating)
|
||
{
|
||
LogManager.Warning("动画已在播放中");
|
||
await _uiStateManager.ExecuteUIUpdateAsync(() =>
|
||
{
|
||
UpdateMainStatus("动画已在播放中");
|
||
CanStartAnimation = true;
|
||
CanPauseAnimation = false;
|
||
CanStopAnimation = false;
|
||
});
|
||
return;
|
||
}
|
||
|
||
// 开始播放物体移动动画
|
||
// 清理之前的碰撞缓存
|
||
_pathAnimationManager?.ClearExclusionCache();
|
||
LogManager.Info("已清理UI层碰撞检测缓存");
|
||
|
||
// 清理之前的碰撞高亮
|
||
ModelHighlightHelper.ClearCollisionHighlights();
|
||
LogManager.Info("已清理之前的碰撞高亮");
|
||
|
||
// 首先重置进度(开始全新动画时)
|
||
_pathAnimationManager.StartAnimation();
|
||
|
||
LogManager.Info($"开始物体移动动画播放: {CurrentPathRoute.Name}, 帧率: {SelectedFrameRate}fps");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
await _uiStateManager.ExecuteUIUpdateAsync(() =>
|
||
{
|
||
UpdateMainStatus($"开始动画出错: {ex.Message}");
|
||
LogManager.Error($"开始动画异常: {ex.Message}", ex);
|
||
|
||
// 恢复按钮状态
|
||
CanStartAnimation = true;
|
||
CanPauseAnimation = false;
|
||
CanStopAnimation = false;
|
||
});
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 暂停动画命令
|
||
/// </summary>
|
||
private async Task ExecutePauseAnimationAsync()
|
||
{
|
||
try
|
||
{
|
||
// 调用PathAnimationManager暂停动画
|
||
// UI状态会通过OnAnimationStateChanged事件自动更新
|
||
_pathAnimationManager.PauseAnimation();
|
||
|
||
LogManager.Info("暂停动画播放");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogManager.Error($"暂停动画异常: {ex.Message}", ex);
|
||
|
||
// 错误时手动恢复UI状态
|
||
await _uiStateManager.ExecuteUIUpdateAsync(() =>
|
||
{
|
||
UpdateMainStatus("暂停动画失败");
|
||
});
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 停止动画命令
|
||
/// </summary>
|
||
private async Task ExecuteStopAnimationAsync()
|
||
{
|
||
try
|
||
{
|
||
await _uiStateManager.ExecuteUIUpdateAsync(() =>
|
||
{
|
||
UpdateMainStatus("动画已停止");
|
||
|
||
CanStartAnimation = true;
|
||
CanPauseAnimation = false;
|
||
CanStopAnimation = false;
|
||
|
||
LogManager.Info("停止动画播放");
|
||
});
|
||
|
||
// 停止当前动画
|
||
_pathAnimationManager.CancelAnimation();
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogManager.Error($"停止动画异常: {ex.Message}", ex);
|
||
}
|
||
}
|
||
|
||
|
||
/// <summary>
|
||
/// 查看碰撞报告命令(显示已缓存的报告)
|
||
/// </summary>
|
||
private async Task ExecuteViewCollisionReport()
|
||
{
|
||
if (!HasClashDetectiveResults)
|
||
{
|
||
UpdateMainStatus("尚无碰撞检测结果可查看");
|
||
return;
|
||
}
|
||
|
||
try
|
||
{
|
||
// 检查是否有已缓存的报告
|
||
if (_lastGeneratedReport == null)
|
||
{
|
||
UpdateMainStatus("报告尚未生成,请等待碰撞检测完成");
|
||
LogManager.Warning("尝试查看报告但缓存为空");
|
||
return;
|
||
}
|
||
|
||
UpdateMainStatus("正在显示碰撞报告...", -1, true);
|
||
LogManager.Info("开始显示已缓存的碰撞报告");
|
||
|
||
// 在UI线程上显示缓存的报告
|
||
await _uiStateManager.ExecuteUIUpdateAsync(() =>
|
||
{
|
||
try
|
||
{
|
||
// 使用WPF碰撞报告对话框直接显示缓存的报告
|
||
var reportDialog = NavisworksTransport.UI.WPF.Views.CollisionReportDialog.ShowReport(_lastGeneratedReport);
|
||
|
||
if (reportDialog != null)
|
||
{
|
||
UpdateMainStatus("碰撞报告已显示");
|
||
LogManager.Info("成功显示缓存的碰撞报告");
|
||
}
|
||
else
|
||
{
|
||
UpdateMainStatus("显示报告对话框失败");
|
||
LogManager.Error("CollisionReportDialog.ShowReport返回null");
|
||
}
|
||
}
|
||
catch (Exception dialogEx)
|
||
{
|
||
UpdateMainStatus($"显示报告失败: {dialogEx.Message}");
|
||
LogManager.Error($"显示WPF报告对话框失败: {dialogEx.Message}");
|
||
}
|
||
});
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
UpdateMainStatus("查看碰撞报告出错");
|
||
LogManager.Error($"查看碰撞报告异常: {ex.Message}", ex);
|
||
}
|
||
}
|
||
|
||
#region 媒体控制命令实现
|
||
|
||
/// <summary>
|
||
/// 检查媒体控制命令是否可执行
|
||
/// </summary>
|
||
private bool CanExecuteMediaCommands()
|
||
{
|
||
return _pathAnimationManager != null &&
|
||
_pathAnimationManager.CurrentState != NavisworksTransport.Core.Animation.AnimationState.Idle &&
|
||
_pathAnimationManager.TotalFrames > 0;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 执行正向播放命令
|
||
/// </summary>
|
||
private void ExecutePlayForward()
|
||
{
|
||
try
|
||
{
|
||
// 自动调整视角到路径中心
|
||
try
|
||
{
|
||
var coreRoute = CurrentPathRoute?.Route;
|
||
if (coreRoute != null)
|
||
{
|
||
ViewpointHelper.AdjustViewpointToPathCenter(coreRoute);
|
||
LogManager.Info($"正向播放前:已自动调整视角到路径中心: {CurrentPathRoute.Name}");
|
||
}
|
||
else
|
||
{
|
||
LogManager.Warning($"正向播放前:CurrentPathRoute.Route 为 null");
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogManager.Error($"正向播放前:调整视角失败: {ex.Message}");
|
||
}
|
||
|
||
_pathAnimationManager?.PlayForward();
|
||
UpdateMediaControlProperties();
|
||
LogManager.Info("执行正向播放命令");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogManager.Error($"正向播放命令执行失败: {ex.Message}");
|
||
UpdateMainStatus("正向播放失败");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 执行反向播放命令
|
||
/// </summary>
|
||
private void ExecutePlayReverse()
|
||
{
|
||
try
|
||
{
|
||
// 自动调整视角到路径中心
|
||
try
|
||
{
|
||
var coreRoute = CurrentPathRoute?.Route;
|
||
if (coreRoute != null)
|
||
{
|
||
ViewpointHelper.AdjustViewpointToPathCenter(coreRoute);
|
||
LogManager.Info($"反向播放前:已自动调整视角到路径中心: {CurrentPathRoute.Name}");
|
||
}
|
||
else
|
||
{
|
||
LogManager.Warning($"反向播放前:CurrentPathRoute.Route 为 null");
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogManager.Error($"反向播放前:调整视角失败: {ex.Message}");
|
||
}
|
||
|
||
_pathAnimationManager?.PlayReverse();
|
||
UpdateMediaControlProperties();
|
||
LogManager.Info("执行反向播放命令");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogManager.Error($"反向播放命令执行失败: {ex.Message}");
|
||
UpdateMainStatus("反向播放失败");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 执行单帧前进命令
|
||
/// </summary>
|
||
private void ExecuteStepForward()
|
||
{
|
||
try
|
||
{
|
||
_pathAnimationManager?.StepForward();
|
||
UpdateMediaControlProperties();
|
||
LogManager.Debug("执行单帧前进命令");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogManager.Error($"单帧前进命令执行失败: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 执行单帧后退命令
|
||
/// </summary>
|
||
private void ExecuteStepBackward()
|
||
{
|
||
try
|
||
{
|
||
_pathAnimationManager?.StepBackward();
|
||
UpdateMediaControlProperties();
|
||
LogManager.Debug("执行单帧后退命令");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogManager.Error($"单帧后退命令执行失败: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 执行快进10帧命令
|
||
/// </summary>
|
||
private void ExecuteFastForward()
|
||
{
|
||
try
|
||
{
|
||
_pathAnimationManager?.FastForward();
|
||
UpdateMediaControlProperties();
|
||
LogManager.Debug("执行快进10帧命令");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogManager.Error($"快进命令执行失败: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 执行快退10帧命令
|
||
/// </summary>
|
||
private void ExecuteFastBackward()
|
||
{
|
||
try
|
||
{
|
||
_pathAnimationManager?.FastBackward();
|
||
UpdateMediaControlProperties();
|
||
LogManager.Debug("执行快退10帧命令");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogManager.Error($"快退命令执行失败: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 执行跳转到开头命令
|
||
/// </summary>
|
||
private void ExecuteSeekToStart()
|
||
{
|
||
try
|
||
{
|
||
_pathAnimationManager?.SeekToFrame(0);
|
||
UpdateMediaControlProperties();
|
||
LogManager.Info("跳转到动画开头");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogManager.Error($"跳转到开头失败: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 执行跳转到结尾命令
|
||
/// </summary>
|
||
private void ExecuteSeekToEnd()
|
||
{
|
||
try
|
||
{
|
||
int lastFrame = (_pathAnimationManager?.TotalFrames ?? 1) - 1;
|
||
_pathAnimationManager?.SeekToFrame(lastFrame);
|
||
UpdateMediaControlProperties();
|
||
LogManager.Info("跳转到动画结尾");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogManager.Error($"跳转到结尾失败: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 执行选择移动物体命令
|
||
/// </summary>
|
||
private void ExecuteSelectAnimatedObject()
|
||
{
|
||
try
|
||
{
|
||
var doc = Autodesk.Navisworks.Api.Application.ActiveDocument;
|
||
if (doc == null || doc.CurrentSelection.IsEmpty)
|
||
{
|
||
LogManager.Warning("请先在场景中选择一个物体");
|
||
return;
|
||
}
|
||
|
||
var newObject = doc.CurrentSelection.SelectedItems.First;
|
||
if (newObject == null) return;
|
||
|
||
// 1. 保存旧物体引用,在设置新物体之前
|
||
var oldObject = _selectedAnimatedObject;
|
||
bool isSameObject = oldObject != null && ReferenceEquals(oldObject, newObject);
|
||
|
||
// 2. 如果有旧物体且与新物体不同,先归位
|
||
if (!isSameObject && oldObject != null)
|
||
{
|
||
_pathAnimationManager?.RestoreObjectToCADPosition();
|
||
LogManager.Info($"[选择物体] 已归位旧物体: {oldObject.DisplayName}");
|
||
}
|
||
_pathAnimationManager?.ClearAnimationResults();
|
||
|
||
// 2. 先保存物体原始尺寸(在设置SelectedAnimatedObject之前)
|
||
var bbox = newObject.BoundingBox();
|
||
double metersToModelUnits = UnitsConverter.GetMetersToUnitsConversionFactor();
|
||
var adapter = CoordinateSystemManager.Instance.CreateHostAdapter();
|
||
var axisConvention = ModelAxisConvention.CreateDefaultForHost(adapter.HostType);
|
||
double objectLength = axisConvention.GetForwardSize(bbox);
|
||
double objectWidth = axisConvention.GetSideSize(bbox);
|
||
double objectHeight = axisConvention.GetUpSize(bbox);
|
||
_objectOriginalLength = objectLength / metersToModelUnits;
|
||
_objectOriginalWidth = objectWidth / metersToModelUnits;
|
||
_objectOriginalHeight = objectHeight / metersToModelUnits;
|
||
_pathAnimationManager?.SetRealObjectDimensions(objectLength, objectWidth, objectHeight);
|
||
LogManager.Debug($"[选择物体] 保存原始尺寸: 长度={_objectOriginalLength:F2}m, 宽度={_objectOriginalWidth:F2}m, 高度={_objectOriginalHeight:F2}m");
|
||
|
||
// 3. 先注册到动画管理器,再设置 SelectedAnimatedObject。
|
||
// SelectedAnimatedObject 的 setter 会立即触发 MoveAnimatedObjectToPathStart();
|
||
// 如果先移动、后 SetAnimatedObject,会把“已摆到起点后的当前姿态”再次当成参考姿态缓存。
|
||
_pathAnimationManager?.SetAnimatedObject(newObject);
|
||
SelectedAnimatedObject = newObject;
|
||
LogManager.Info($"已选择移动物体: {SelectedAnimatedObject.DisplayName}");
|
||
|
||
// 选择实体物体意味着切换到实体模式,避免后续起点同步仍走虚拟物体分支。
|
||
if (UseVirtualObject)
|
||
{
|
||
UseVirtualObject = false;
|
||
LogManager.Debug("[选择物体] 已切换到实体物体模式");
|
||
}
|
||
|
||
// 只有选择不同的物体时,才重置角度修正值
|
||
if (!isSameObject)
|
||
{
|
||
// 重置 ViewModel 中的角度修正值(会自动同步到 PathAnimationManager)
|
||
ObjectRotationCorrection = LocalEulerRotationCorrection.Zero;
|
||
LogManager.Debug("[选择物体] 已重置角度修正值为0(新物体)");
|
||
}
|
||
else
|
||
{
|
||
LogManager.Debug($"[选择物体] 保持当前角度修正值(同一物体)");
|
||
}
|
||
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogManager.Error($"选择物体失败: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 更新媒体控制相关属性
|
||
/// </summary>
|
||
private void UpdateMediaControlProperties()
|
||
{
|
||
OnPropertyChanged(nameof(TimelinePosition));
|
||
OnPropertyChanged(nameof(TimelineDuration));
|
||
OnPropertyChanged(nameof(CurrentTimeDisplay));
|
||
OnPropertyChanged(nameof(FrameInfoDisplay));
|
||
OnPropertyChanged(nameof(IsPlayingForward));
|
||
OnPropertyChanged(nameof(IsPlayingReverse));
|
||
}
|
||
|
||
/// <summary>
|
||
/// 格式化时间显示
|
||
/// </summary>
|
||
private string FormatTime(double seconds)
|
||
{
|
||
var timeSpan = TimeSpan.FromSeconds(Math.Max(0, seconds));
|
||
return $"{timeSpan.Minutes:D2}:{timeSpan.Seconds:D2}";
|
||
}
|
||
|
||
#endregion
|
||
|
||
#endregion
|
||
|
||
#region 辅助方法
|
||
|
||
/// <summary>
|
||
/// 设置当前路径
|
||
/// </summary>
|
||
public void SetCurrentPath(PathRouteViewModel pathRoute)
|
||
{
|
||
// 🔥 先同步路径到 PathAnimationManager(在设置 CurrentPathRoute 之前)
|
||
// 避免 CurrentPathRoute setter 中调用 MoveAnimatedObjectToPathStart 时 _route 还未设置
|
||
if (pathRoute != null && _pathAnimationManager != null)
|
||
{
|
||
var coreRoute = pathRoute.Route;
|
||
if (coreRoute != null)
|
||
{
|
||
_pathAnimationManager.SetRoute(coreRoute);
|
||
LogManager.Info($"[AnimationControlViewModel] 已同步路径到 PathAnimationManager: {coreRoute.Name}");
|
||
}
|
||
}
|
||
|
||
CurrentPathRoute = pathRoute;
|
||
|
||
// 添加调试日志
|
||
if (pathRoute != null)
|
||
{
|
||
LogManager.Info($"[AnimationControlViewModel] 设置当前路径: 名称='{pathRoute.Name}', ID='{pathRoute.Id}', 点数={pathRoute.Points?.Count ?? 0}");
|
||
}
|
||
else
|
||
{
|
||
LogManager.Info("[AnimationControlViewModel] 清空当前路径");
|
||
}
|
||
|
||
// 路径改变时清除已生成的动画(如果有的话)
|
||
if (_pathAnimationManager != null &&
|
||
(_pathAnimationManager.CurrentState == NavisworksTransport.Core.Animation.AnimationState.Ready ||
|
||
_pathAnimationManager.CurrentState == NavisworksTransport.Core.Animation.AnimationState.Finished))
|
||
{
|
||
try
|
||
{
|
||
_pathAnimationManager.CancelAnimation(); // 这会将状态设置为Stopped,需要重新生成动画
|
||
LogManager.Info("路径更改,已清除之前生成的动画");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogManager.Warning($"清除之前动画时出现警告: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
// 路径改变时,步长、速度和路径长度需要重新计算
|
||
OnPropertyChanged(nameof(CollisionDetectionAccuracy));
|
||
OnPropertyChanged(nameof(MovementSpeed));
|
||
OnPropertyChanged(nameof(PathLength));
|
||
|
||
// 按钮状态更新
|
||
UpdateAnimationButtonStates();
|
||
|
||
// 更新碰撞状态
|
||
if (pathRoute == null)
|
||
{
|
||
UpdateMainStatus("请选择路径");
|
||
}
|
||
else if (pathRoute.Points.Count < 2)
|
||
{
|
||
UpdateMainStatus("路径点数不足");
|
||
}
|
||
else
|
||
{
|
||
UpdateMainStatus("碰撞检测就绪");
|
||
}
|
||
|
||
LogManager.Info($"AnimationControlViewModel当前路径更新: {pathRoute?.Name ?? "无"}, 点数: {pathRoute?.Points.Count ?? 0}");
|
||
}
|
||
|
||
/// <summary>
|
||
/// 处理动画进度变化事件
|
||
/// 使用Dispatcher更新UI
|
||
/// </summary>
|
||
private void OnAnimationProgressChanged(object sender, double progressPercent)
|
||
{
|
||
System.Windows.Application.Current.Dispatcher.BeginInvoke(new Action(() =>
|
||
{
|
||
// 更新时间轴相关属性
|
||
OnPropertyChanged(nameof(TimelinePosition));
|
||
OnPropertyChanged(nameof(CurrentTimeDisplay));
|
||
OnPropertyChanged(nameof(FrameInfoDisplay));
|
||
}));
|
||
}
|
||
|
||
/// <summary>
|
||
/// 处理动画状态变化事件
|
||
/// </summary>
|
||
private void OnAnimationStateChanged(object sender, Core.Animation.AnimationState state)
|
||
{
|
||
System.Windows.Application.Current.Dispatcher.BeginInvoke(new Func<Task>(async () =>
|
||
{
|
||
switch (state)
|
||
{
|
||
case Core.Animation.AnimationState.Playing:
|
||
CanStartAnimation = false;
|
||
CanPauseAnimation = true;
|
||
CanStopAnimation = true;
|
||
UpdateMainStatus("动画播放中");
|
||
UpdateMediaControlProperties(); // 更新媒体控制属性
|
||
break;
|
||
case Core.Animation.AnimationState.Paused:
|
||
CanStartAnimation = true; // 暂停状态下可以继续
|
||
CanPauseAnimation = false;
|
||
CanStopAnimation = true;
|
||
UpdateMainStatus("动画已暂停");
|
||
UpdateMediaControlProperties(); // 更新媒体控制属性
|
||
break;
|
||
case Core.Animation.AnimationState.Finished:
|
||
CanStartAnimation = true;
|
||
CanPauseAnimation = false;
|
||
CanStopAnimation = false;
|
||
UpdateMainStatus("动画已完成");
|
||
UpdateMediaControlProperties(); // 更新媒体控制属性
|
||
|
||
// 检查检测是否被取消
|
||
if (_clashIntegration?.WasLastTestCanceled == true)
|
||
{
|
||
LogManager.Info("[动画完成] 检测已被取消,跳过报告生成和高亮");
|
||
// 重置取消标志
|
||
_clashIntegration.ClearWasLastTestCanceled();
|
||
break;
|
||
}
|
||
|
||
ModelHighlightHelper.ClearCollisionHighlights();
|
||
await GenerateAndSaveCollisionReport();
|
||
HighlightClashDetectiveResults();
|
||
break;
|
||
case NavisworksTransport.Core.Animation.AnimationState.Stopped:
|
||
CanStartAnimation = true;
|
||
CanPauseAnimation = false;
|
||
CanStopAnimation = false;
|
||
UpdateMainStatus("动画已停止");
|
||
UpdateMediaControlProperties(); // 更新媒体控制属性
|
||
// 先清除所有碰撞高亮(包括预计算和ClashDetective)
|
||
ModelHighlightHelper.ClearCollisionHighlights();
|
||
// 注意:Stopped状态不进行碰撞检测(例如切换路径时)
|
||
break;
|
||
default:
|
||
UpdateMainStatus($"动画状态: {state}");
|
||
UpdateMediaControlProperties(); // 更新媒体控制属性
|
||
break;
|
||
}
|
||
}));
|
||
}
|
||
|
||
/// <summary>
|
||
/// 生成并保存碰撞报告(无论有无碰撞都生成)
|
||
/// </summary>
|
||
private async Task GenerateAndSaveCollisionReport()
|
||
{
|
||
try
|
||
{
|
||
var animationManager = Core.Animation.PathAnimationManager.GetInstance();
|
||
var hasAnimationCollisions = animationManager?.AllCollisionResults?.Count > 0;
|
||
|
||
// 统一使用GenerateCollisionReportCommand生成报告(无论有无碰撞)
|
||
UpdateMainStatus($"正在生成碰撞报告...", -1, true);
|
||
|
||
var generateReportCommand = NavisworksTransport.Commands.GenerateCollisionReportCommand.CreateComprehensive();
|
||
|
||
// 设置检测记录ID(优先使用ID查询,更可靠)
|
||
var detectionRecordId = _pathAnimationManager?.CurrentDetectionRecordId;
|
||
generateReportCommand.SetDetectionRecordId(detectionRecordId);
|
||
LogManager.Info($"生成碰撞报告,使用检测记录ID: {detectionRecordId?.ToString() ?? "null"}");
|
||
|
||
var result = await generateReportCommand.ExecuteAsync();
|
||
|
||
if (result.IsSuccess && result is PathPlanningResult<CollisionReportResult> reportResult)
|
||
{
|
||
// 缓存报告结果,供后续查看使用
|
||
_lastGeneratedReport = reportResult.Data;
|
||
|
||
// 保存到数据库
|
||
await SaveCollisionReportToDatabase(reportResult.Data);
|
||
|
||
var collisionCount = reportResult.Data.TotalCollisions;
|
||
if (collisionCount > 0)
|
||
{
|
||
UpdateMainStatus($"✅ 碰撞报告已生成并保存:{collisionCount} 个碰撞");
|
||
LogManager.Info("碰撞报告已自动生成并保存到数据库");
|
||
}
|
||
else
|
||
{
|
||
UpdateMainStatus($"✅ 碰撞报告已生成并保存:0 个碰撞");
|
||
LogManager.Info("碰撞报告已自动生成并保存到数据库(无碰撞)");
|
||
}
|
||
}
|
||
else
|
||
{
|
||
UpdateMainStatus($"碰撞报告生成失败: {result.ErrorMessage}");
|
||
LogManager.Error($"自动生成碰撞报告失败: {result.ErrorMessage}");
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogManager.Error($"生成碰撞报告失败: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 高亮ClashDetective结果
|
||
/// </summary>
|
||
private void HighlightClashDetectiveResults()
|
||
{
|
||
try
|
||
{
|
||
// 检查测试名称是否有效,如果为空可能是检测被跳过(配置相同)
|
||
if (string.IsNullOrEmpty(_clashIntegration?.CurrentTestName))
|
||
{
|
||
LogManager.Info("当前没有ClashDetective测试(可能配置相同已跳过检测),从历史记录加载");
|
||
// 使用当前检测记录ID查询数据库获取TestName
|
||
var detectionRecordId = _pathAnimationManager?.CurrentDetectionRecordId;
|
||
if (detectionRecordId.HasValue)
|
||
{
|
||
var pathDatabase = _pathPlanningManager?.GetPathDatabase();
|
||
var record = pathDatabase?.GetDetectionResultById(detectionRecordId.Value);
|
||
if (record != null && !string.IsNullOrEmpty(record.TestName))
|
||
{
|
||
// 设置到ClashDetectiveIntegration以便后续使用
|
||
_clashIntegration.SetCurrentTestName(record.TestName);
|
||
LogManager.Info($"已从数据库加载TestName: {record.TestName}");
|
||
}
|
||
}
|
||
}
|
||
|
||
// 再次检查测试名称
|
||
if (string.IsNullOrEmpty(_clashIntegration?.CurrentTestName))
|
||
{
|
||
LogManager.Info("无法获取ClashDetective测试名称,跳过高亮");
|
||
HasClashDetectiveResults = false;
|
||
return;
|
||
}
|
||
|
||
// 检查是否有当前测试的ClashDetective结果
|
||
var clashResults = _clashIntegration.GetCurrentTestResults();
|
||
|
||
// 统一处理:无论有无碰撞都高亮(有碰撞时高亮碰撞对象,无碰撞时无操作)
|
||
if (clashResults != null && clashResults.Count > 0)
|
||
{
|
||
ModelHighlightHelper.HighlightClashDetectiveResults(_clashIntegration.CurrentTestName, _clashIntegration.GetCurrentPathClashResults);
|
||
HasClashDetectiveResults = true;
|
||
LogManager.Info($"动画结束,已高亮ClashDetective检测结果:{clashResults.Count}个碰撞");
|
||
}
|
||
else
|
||
{
|
||
HasClashDetectiveResults = false;
|
||
}
|
||
|
||
// 插入步骤:如果没有碰撞,显示祝贺窗口
|
||
if (clashResults == null || clashResults.Count == 0)
|
||
{
|
||
LogManager.Info("🎉 恭喜!本次动画未检测到任何碰撞,路径规划非常安全!");
|
||
System.Windows.MessageBox.Show(
|
||
"🎉 恭喜!本次动画仿真过程中未发现任何碰撞!\n\n您的物流路径规划非常成功,物体可以安全通行。",
|
||
"仿真完成 - 无碰撞",
|
||
System.Windows.MessageBoxButton.OK,
|
||
System.Windows.MessageBoxImage.Information);
|
||
}
|
||
|
||
// 统一流程:刷新历史列表并显示报告(无论有无碰撞)
|
||
LogManager.Info($"[HighlightClashDetectiveResults] 刷新历史列表,当前路径: {CurrentPathRoute?.Name}");
|
||
RefreshClashDetectiveResultsList();
|
||
if (_lastGeneratedReport != null)
|
||
{
|
||
NavisworksTransport.UI.WPF.Views.CollisionReportDialog.ShowReport(_lastGeneratedReport);
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogManager.Error($"高亮ClashDetective结果失败: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 处理ClashDetective的碰撞检测事件 - 已废弃,报告生成统一在CheckAndHighlightClashDetectiveResults中处理
|
||
/// </summary>
|
||
private void OnCollisionDetected(object sender, CollisionDetectedEventArgs e)
|
||
{
|
||
// 报告生成已统一在CheckAndHighlightClashDetectiveResults中处理
|
||
// 此事件保留用于日志记录和未来扩展
|
||
try
|
||
{
|
||
var clashResults = _clashIntegration?.GetCurrentTestResults();
|
||
var collisionCount = clashResults?.Count ?? 0;
|
||
LogManager.Info($"[OnCollisionDetected] 碰撞检测事件触发,碰撞数量: {collisionCount}");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogManager.Error($"处理碰撞检测事件失败: {ex.Message}", ex);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// ClashDetective结果保存事件处理
|
||
/// </summary>
|
||
private void OnClashDetectiveResultSaved(object sender, ClashDetectiveResultSavedEventArgs e)
|
||
{
|
||
try
|
||
{
|
||
// 刷新ClashDetective结果列表
|
||
RefreshClashDetectiveResultsList();
|
||
LogManager.Info($"ClashDetective结果已保存,列表已刷新: 路径={e.PathName}, 测试={e.TestName}, 碰撞数={e.CollisionCount}");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogManager.Error($"处理ClashDetective结果保存事件失败: {ex.Message}", ex);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 保存碰撞报告到数据库
|
||
/// </summary>
|
||
private async Task SaveCollisionReportToDatabase(CollisionReportResult reportResult)
|
||
{
|
||
try
|
||
{
|
||
var pathPlanningManager = PathPlanningManager.Instance;
|
||
var pathDatabase = pathPlanningManager?.GetPathDatabase();
|
||
|
||
if (pathDatabase != null && reportResult != null)
|
||
{
|
||
// 获取路由ID
|
||
string routeId = CurrentPathRoute?.Id ?? "";
|
||
|
||
// 注意:检测记录(包括排除列表)已在生成动画时保存到 CollisionDetectionRecords 表
|
||
// 这里只保存碰撞报告,不重复保存排除列表
|
||
|
||
// 从报告中获取所有需要的数据
|
||
await Task.Run(() =>
|
||
{
|
||
// 保存碰撞报告
|
||
pathDatabase.SaveCollisionReport(
|
||
routeId,
|
||
reportResult.PathName,
|
||
SelectedAnimatedObjectName ?? "未知对象",
|
||
reportResult.UniqueCollidedObjectsCount,
|
||
reportResult.FrameRate,
|
||
reportResult.Duration,
|
||
reportResult.DetectionTolerance,
|
||
reportResult.AnimationCollisions,
|
||
reportResult.TotalCollisions
|
||
);
|
||
|
||
// 关联排除对象到检测记录
|
||
if (reportResult.ResultId > 0 && _excludedObjects != null && _excludedObjects.Count > 0)
|
||
{
|
||
// 获取排除对象的记录ID
|
||
var excludedRecords = pathDatabase.GetExcludedObjects(routeId, includeGlobal: true);
|
||
var excludedObjectIds = excludedRecords.Select(r => r.Id).ToList();
|
||
pathDatabase.LinkExcludedObjectsToDetectionRecord(reportResult.ResultId, excludedObjectIds);
|
||
LogManager.Info($"[排除列表] 已关联 {excludedObjectIds.Count} 个排除对象到检测记录 (DetectionRecordId={reportResult.ResultId})");
|
||
}
|
||
});
|
||
|
||
LogManager.Info($"碰撞报告已保存到数据库 - 路径:{reportResult.PathName}, " +
|
||
$"碰撞构件:{reportResult.UniqueCollidedObjectsCount}, " +
|
||
$"动画碰撞:{reportResult.AnimationCollisions}, " +
|
||
$"ClashDetective:{reportResult.TotalCollisions}, " +
|
||
$"排除对象:{_excludedObjects?.Count ?? 0}");
|
||
}
|
||
else
|
||
{
|
||
LogManager.Warning("无法获取PathDatabase实例或报告数据为空,碰撞报告未保存到数据库");
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogManager.Error($"保存碰撞报告到数据库失败: {ex.Message}", ex);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 执行清除移动物体命令
|
||
/// 清除移动物体选择、停止当前动画并更新按钮状态
|
||
/// </summary>
|
||
private void ExecuteClearAnimatedObject()
|
||
{
|
||
try
|
||
{
|
||
if (_pathAnimationManager != null)
|
||
{
|
||
// 1. 归位并清理
|
||
_pathAnimationManager.RestoreObjectToCADPosition();
|
||
_pathAnimationManager.ClearAnimationResults();
|
||
LogManager.Info("已清除PathAnimationManager中的动画数据并归位物体");
|
||
}
|
||
|
||
// 2. 先清空选择,避免角度修正归零时又触发“移动到起点”链路。
|
||
SelectedAnimatedObject = null;
|
||
|
||
// 3. 重置角度修正值为0
|
||
ObjectRotationCorrection = LocalEulerRotationCorrection.Zero;
|
||
LogManager.Debug("[清除物体] 已重置角度修正值为0");
|
||
|
||
// 4. 重置属性
|
||
UpdateAnimatedObjectInfo();
|
||
UpdateCanGenerateAnimation();
|
||
|
||
// 5. 清理高亮
|
||
ModelHighlightHelper.ClearCollisionHighlights();
|
||
|
||
LogManager.Info("移动物体已完全清除并归位");
|
||
UpdateMainStatus("已清除物体选择并恢复位置");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogManager.Error($"清除失败: {ex.Message}");
|
||
UpdateMainStatus($"清除失败: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 执行编辑物体角度修正
|
||
/// </summary>
|
||
private void ExecuteEditObjectRotation()
|
||
{
|
||
try
|
||
{
|
||
var dialog = new Views.EditRotationWindow(_objectRotationCorrection);
|
||
if (dialog.ShowDialog() == true)
|
||
{
|
||
var placementRequest = dialog.AdjustmentRequest;
|
||
if (placementRequest.PreserveInitialPose)
|
||
{
|
||
ApplyTranslationOnlyObjectPlacement();
|
||
return;
|
||
}
|
||
|
||
_pathAnimationManager?.SetObjectStartPlacementMode(ObjectStartPlacementMode.AlignToPathPose);
|
||
ObjectRotationCorrection = placementRequest.RotationCorrection;
|
||
LogManager.Info($"物体角度修正已更新: {_objectRotationCorrection}");
|
||
UpdateMainStatus($"物体角度修正: {_objectRotationCorrection}");
|
||
|
||
// 应用角度修正到物体
|
||
UpdateObjectRotation();
|
||
|
||
// 更新通行空间可视化(考虑旋转后的尺寸)
|
||
UpdatePassageSpaceVisualization();
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogManager.Error($"编辑物体角度失败: {ex.Message}");
|
||
UpdateMainStatus($"编辑物体角度失败: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
private void ApplyTranslationOnlyObjectPlacement()
|
||
{
|
||
try
|
||
{
|
||
if (_pathAnimationManager == null || !HasSelectedAnimatedObject)
|
||
{
|
||
return;
|
||
}
|
||
|
||
_objectRotationCorrection = LocalEulerRotationCorrection.Zero;
|
||
_pathAnimationManager.SetObjectRotationCorrectionDirect(LocalEulerRotationCorrection.Zero);
|
||
_pathAnimationManager.SetObjectStartPlacementMode(ObjectStartPlacementMode.PreserveInitialPose);
|
||
OnPropertyChanged(nameof(ObjectRotationCorrection));
|
||
|
||
if (_pathAnimationManager.MoveObjectToPathStartPreservingInitialPose())
|
||
{
|
||
LogManager.Info("[角度修正] 已按平移模式将物体移动到路径起点,保持初始位姿");
|
||
UpdateMainStatus("物体已平移到路径起点并保持初始位姿");
|
||
}
|
||
else
|
||
{
|
||
LogManager.Warning("[角度修正] 平移模式未能将物体移动到路径起点");
|
||
UpdateMainStatus("平移模式执行失败");
|
||
}
|
||
|
||
UpdatePassageSpaceVisualization();
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogManager.Error($"平移到路径起点失败: {ex.Message}");
|
||
UpdateMainStatus($"平移到路径起点失败: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 更新物体旋转(应用角度修正)
|
||
/// </summary>
|
||
private void UpdateObjectRotation()
|
||
{
|
||
try
|
||
{
|
||
if (_pathAnimationManager != null && HasSelectedAnimatedObject)
|
||
{
|
||
// 将角度修正传递给动画管理器
|
||
_pathAnimationManager.SetObjectRotationCorrection(_objectRotationCorrection);
|
||
LogManager.Debug($"[角度修正] 已更新物体旋转: {_objectRotationCorrection}");
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogManager.Error($"更新物体旋转失败: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 将动画对象(物体或虚拟物体)移动到当前路径的起点
|
||
/// 用于在选择物体/虚拟物体或切换路径时立即定位到起点,方便用户预览和调整角度
|
||
/// </summary>
|
||
private void MoveAnimatedObjectToPathStart()
|
||
{
|
||
try
|
||
{
|
||
// 检查是否有路径
|
||
if (CurrentPathRoute == null || CurrentPathRoute.Points == null || CurrentPathRoute.Points.Count < 2)
|
||
{
|
||
LogManager.Debug("[移动到起点] 路径未选择或路径点不足,跳过移动");
|
||
return;
|
||
}
|
||
|
||
// 检查是否有动画对象(物体或虚拟物体)
|
||
if (!HasSelectedAnimatedObject)
|
||
{
|
||
LogManager.Debug("[移动到起点] 未选择动画对象,跳过移动");
|
||
return;
|
||
}
|
||
|
||
// 获取路径点
|
||
var pathPoints = CurrentPathRoute.Points.Select(p => new Point3D(p.X, p.Y, p.Z)).ToList();
|
||
|
||
// 根据当前模式确定动画对象
|
||
ModelItem animatedObject = null;
|
||
if (UseVirtualObject)
|
||
{
|
||
// 虚拟物体模式:获取当前虚拟物体
|
||
animatedObject = VirtualObjectManager.Instance.CurrentVirtualObject;
|
||
if (animatedObject == null)
|
||
{
|
||
LogManager.Debug("[移动到起点] 虚拟物体未创建,跳过移动");
|
||
return;
|
||
}
|
||
|
||
// 设置虚拟物体参数到动画管理器(转换为模型单位)
|
||
var metersToUnitsMove = UnitsConverter.GetMetersToUnitsConversionFactor();
|
||
_pathAnimationManager?.SetVirtualObjectParameters(true,
|
||
VirtualObjectLengthInMeters * metersToUnitsMove,
|
||
VirtualObjectWidthInMeters * metersToUnitsMove,
|
||
VirtualObjectHeightInMeters * metersToUnitsMove);
|
||
}
|
||
else
|
||
{
|
||
// 实体物体模式:使用选中的物体
|
||
animatedObject = SelectedAnimatedObject;
|
||
if (animatedObject == null)
|
||
{
|
||
LogManager.Debug("[移动到起点] 选中物体为空,跳过移动");
|
||
return;
|
||
}
|
||
}
|
||
|
||
// 执行移动
|
||
if (_pathAnimationManager != null && animatedObject != null)
|
||
{
|
||
bool moved = _pathAnimationManager.MoveObjectToPathStart(animatedObject, pathPoints);
|
||
if (moved)
|
||
{
|
||
LogManager.Info($"[移动到起点] {(UseVirtualObject ? "虚拟物体" : "物体")} 已移动到路径起点");
|
||
}
|
||
else
|
||
{
|
||
LogManager.Warning($"[移动到起点] {(UseVirtualObject ? "虚拟物体" : "物体")} 移动失败(可能路径未设置)");
|
||
}
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogManager.Warning($"[移动到起点] 移动动画对象到路径起点失败: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
#region 手工碰撞对象命令
|
||
|
||
private void ExecuteApplyManualTargetsFromSelection()
|
||
{
|
||
try
|
||
{
|
||
var doc = Autodesk.Navisworks.Api.Application.ActiveDocument;
|
||
var selectedItems = doc?.CurrentSelection?.SelectedItems;
|
||
|
||
if (selectedItems == null || selectedItems.Count == 0)
|
||
{
|
||
UpdateMainStatus("请在Navisworks中选择需要检测的对象");
|
||
LogManager.Warning("[手工碰撞] 未选择任何对象");
|
||
return;
|
||
}
|
||
|
||
var toAdd = new List<ManualCollisionTargetViewModel>();
|
||
foreach (ModelItem item in selectedItems)
|
||
{
|
||
if (item == null)
|
||
continue;
|
||
|
||
if (!ModelItemAnalysisHelper.IsModelItemValid(item) || !HasGeometryRecursive(item))
|
||
continue;
|
||
|
||
if (ManualTargetExists(item))
|
||
continue;
|
||
|
||
var displayName = ModelItemAnalysisHelper.GetSafeDisplayName(item);
|
||
var modelPath = BuildModelPath(item);
|
||
toAdd.Add(new ManualCollisionTargetViewModel(item, displayName, modelPath));
|
||
}
|
||
|
||
if (toAdd.Count == 0)
|
||
{
|
||
if (_manualCollisionTargets.Count >= ManualCollisionTargetLimit)
|
||
{
|
||
UpdateMainStatus($"手工目标数量已达上限({ManualCollisionTargetLimit})");
|
||
}
|
||
else
|
||
{
|
||
UpdateMainStatus("选中的对象已在列表中或不包含几何体");
|
||
}
|
||
return;
|
||
}
|
||
|
||
int availableSlots = ManualCollisionTargetLimit - _manualCollisionTargets.Count;
|
||
if (availableSlots <= 0)
|
||
{
|
||
UpdateMainStatus($"手工目标数量已达上限({ManualCollisionTargetLimit})");
|
||
return;
|
||
}
|
||
|
||
var addedCount = Math.Min(availableSlots, toAdd.Count);
|
||
foreach (var entry in toAdd.Take(addedCount))
|
||
{
|
||
_manualCollisionTargets.Add(entry);
|
||
}
|
||
|
||
// 更新序号
|
||
UpdateManualCollisionTargetIndices();
|
||
|
||
_manualTargetsLastSyncTime = DateTime.Now;
|
||
UpdateManualCollisionTargetSummary();
|
||
RefreshManualTargetHighlights();
|
||
SyncManualCollisionTargetsToAnimationManager();
|
||
|
||
UpdateMainStatus($"已添加 {addedCount} 个碰撞检测对象");
|
||
LogManager.Info($"[手工碰撞] 添加 {addedCount} 个对象,目前共 {_manualCollisionTargets.Count} 个");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogManager.Error($"[手工碰撞] 同步选中对象失败: {ex.Message}");
|
||
UpdateMainStatus("同步手工对象失败");
|
||
}
|
||
}
|
||
|
||
private void ExecuteClearManualTargets()
|
||
{
|
||
if (_manualCollisionTargets.Count == 0)
|
||
return;
|
||
|
||
_manualCollisionTargets.Clear();
|
||
_manualTargetsLastSyncTime = null;
|
||
ModelHighlightHelper.ClearCategory(ManualTargetsHighlightCategory);
|
||
SyncManualCollisionTargetsToAnimationManager();
|
||
UpdateManualCollisionTargetSummary();
|
||
UpdateMainStatus("已清空手工碰撞对象");
|
||
LogManager.Info("[手工碰撞] 手工对象列表已清空");
|
||
}
|
||
|
||
private void ExecuteRemoveManualTarget(ManualCollisionTargetViewModel target)
|
||
{
|
||
if (target == null)
|
||
return;
|
||
|
||
if (_manualCollisionTargets.Remove(target))
|
||
{
|
||
UpdateMainStatus($"已移除 {target.DisplayName}");
|
||
LogManager.Info($"[手工碰撞] 移除对象 {target.DisplayName}");
|
||
|
||
if (_manualCollisionTargets.Count == 0)
|
||
{
|
||
ModelHighlightHelper.ClearCategory(ManualTargetsHighlightCategory);
|
||
}
|
||
else
|
||
{
|
||
RefreshManualTargetHighlights();
|
||
}
|
||
}
|
||
}
|
||
|
||
private void ExecuteHighlightManualTargets()
|
||
{
|
||
if (_manualCollisionTargets.Count == 0)
|
||
return;
|
||
|
||
RefreshManualTargetHighlights(forceHighlight: true);
|
||
UpdateMainStatus("已高亮手工指定对象");
|
||
}
|
||
|
||
private void ExecuteClearManualHighlights()
|
||
{
|
||
ModelHighlightHelper.ClearCategory(ManualTargetsHighlightCategory);
|
||
UpdateMainStatus("已清除手工对象高亮");
|
||
}
|
||
|
||
/// <summary>
|
||
/// 聚焦到手工指定的对象
|
||
/// </summary>
|
||
public void FocusOnManualTarget(ManualCollisionTargetViewModel target)
|
||
{
|
||
if (target?.ModelItem == null) return;
|
||
|
||
try
|
||
{
|
||
// 先高亮该对象
|
||
var items = new List<ModelItem> { target.ModelItem };
|
||
ModelHighlightHelper.HighlightItems(ManualTargetsHighlightCategory, items);
|
||
|
||
// 聚焦到对象(斜上方45度视角)
|
||
ViewpointHelper.FocusOnModelItem(target.ModelItem, viewAngleDegrees: 60.0, targetViewRatio: 0.25);
|
||
|
||
UpdateMainStatus($"已聚焦到: {target.DisplayName}");
|
||
LogManager.Info($"聚焦到手工指定对象: {target.DisplayName}");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogManager.Error($"聚焦到手工指定对象失败: {ex.Message}", ex);
|
||
UpdateMainStatus($"聚焦失败: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 聚焦到排除的对象
|
||
/// </summary>
|
||
public void FocusOnExcludedObject(ExcludedObjectViewModel excludedObject)
|
||
{
|
||
if (excludedObject?.ModelItem == null) return;
|
||
|
||
try
|
||
{
|
||
// 先高亮该对象(使用排除对象专用颜色)
|
||
var items = new List<ModelItem> { excludedObject.ModelItem };
|
||
ModelHighlightHelper.HighlightItems(ExcludedObjectsHighlightCategory, items);
|
||
|
||
// 聚焦到对象(斜上方45度视角)
|
||
ViewpointHelper.FocusOnModelItem(excludedObject.ModelItem, viewAngleDegrees: 60.0, targetViewRatio: 0.25);
|
||
|
||
UpdateMainStatus($"已聚焦到排除对象: {excludedObject.DisplayName}");
|
||
LogManager.Info($"聚焦到排除对象: {excludedObject.DisplayName}");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogManager.Error($"聚焦到排除对象失败: {ex.Message}", ex);
|
||
UpdateMainStatus($"聚焦失败: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 聚焦到碰撞构件,并移动动画物体到碰撞位置
|
||
/// </summary>
|
||
public void FocusOnCollisionObject(CollisionObjectViewModel collisionObject)
|
||
{
|
||
if (collisionObject?.ModelItem == null) return;
|
||
|
||
// 从历史记录中获取运动物体
|
||
var selectedResult = SelectedClashDetectiveResult;
|
||
var animatedObject = selectedResult?.AnimatedObject;
|
||
|
||
// 如果是虚拟物体且尚未创建,现在创建它
|
||
if (selectedResult?.Record?.IsVirtualObject == true && animatedObject == null)
|
||
{
|
||
try
|
||
{
|
||
double unitsToMeters = UnitsConverter.GetUnitsToMetersConversionFactor(Autodesk.Navisworks.Api.Application.ActiveDocument.Units);
|
||
// 创建并显示虚拟物体(尺寸从模型单位转换为米)
|
||
animatedObject = VirtualObjectManager.Instance.CreateVirtualObject(
|
||
selectedResult.VirtualObjectLengthInMeters * unitsToMeters,
|
||
selectedResult.VirtualObjectWidthInMeters * unitsToMeters,
|
||
selectedResult.VirtualObjectHeightInMeters * unitsToMeters);
|
||
selectedResult.AnimatedObject = animatedObject;
|
||
LogManager.Info($"[碰撞构件] 创建虚拟物体并聚焦: {selectedResult.VirtualObjectLengthInMeters:F2}x{selectedResult.VirtualObjectWidthInMeters:F2}x{selectedResult.VirtualObjectHeightInMeters:F2}");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogManager.Warning($"[碰撞构件] 创建虚拟物体失败: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
if (animatedObject == null)
|
||
{
|
||
LogManager.Warning($"[碰撞构件] 未找到运动物体,仅聚焦到被撞对象: {collisionObject.DisplayName}");
|
||
// 仅聚焦,不移动物体
|
||
CollisionSceneHelper.MoveToCollisionAndFocus(
|
||
collisionObject.ModelItem,
|
||
null,
|
||
collisionObject.AnimatedObjectTrackedPosition,
|
||
collisionObject.AnimatedObjectTrackedYawRadians,
|
||
collisionObject.AnimatedObjectTrackedRotation,
|
||
collisionObject.AnimatedObjectHasTrackedRotation,
|
||
false);
|
||
return;
|
||
}
|
||
|
||
// 🔥 关键修复:如果运动物体是虚拟物体,确保它是可见的(可能被之前的切换隐藏了)
|
||
if (selectedResult?.Record?.IsVirtualObject == true)
|
||
{
|
||
try
|
||
{
|
||
double unitsToMeters = UnitsConverter.GetUnitsToMetersConversionFactor(Autodesk.Navisworks.Api.Application.ActiveDocument.Units);
|
||
// 显示虚拟物体(如果已存在且尺寸相同,会显示;如果尺寸不同,会更新尺寸)
|
||
VirtualObjectManager.Instance.ShowVirtualObject(
|
||
selectedResult.VirtualObjectLengthInMeters * unitsToMeters,
|
||
selectedResult.VirtualObjectWidthInMeters * unitsToMeters,
|
||
selectedResult.VirtualObjectHeightInMeters * unitsToMeters);
|
||
LogManager.Debug("[碰撞构件] 虚拟物体已确保可见");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogManager.Warning($"[碰撞构件] 显示虚拟物体失败: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
try
|
||
{
|
||
// 首次查看时保存动画物体状态
|
||
if (_savedAnimatedObjectStateForCollision == null || _savedAnimatedObjectForCollision != animatedObject)
|
||
{
|
||
_savedAnimatedObjectForCollision = animatedObject;
|
||
_savedAnimatedObjectStateForCollision = CollisionSceneHelper.SaveAnimatedObjectState(animatedObject);
|
||
}
|
||
|
||
// 移动动画物体到碰撞位置并聚焦
|
||
CollisionSceneHelper.MoveToCollisionAndFocus(
|
||
collisionObject.ModelItem,
|
||
animatedObject,
|
||
collisionObject.AnimatedObjectTrackedPosition,
|
||
collisionObject.AnimatedObjectTrackedYawRadians,
|
||
collisionObject.AnimatedObjectTrackedRotation,
|
||
collisionObject.AnimatedObjectHasTrackedRotation,
|
||
collisionObject.HasPositionInfo);
|
||
|
||
// 🔥 虚拟物体缩放已通过MoveItemToPositionAndYawWithCurrentScale保留
|
||
|
||
UpdateMainStatus($"已聚焦到碰撞构件: {collisionObject.DisplayName}");
|
||
LogManager.Info($"聚焦到碰撞构件: {collisionObject.DisplayName}");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogManager.Error($"聚焦到碰撞构件失败: {ex.Message}", ex);
|
||
UpdateMainStatus($"聚焦失败: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 恢复动画物体到原始状态(失去焦点或切换历史记录时调用)
|
||
/// </summary>
|
||
public void RestoreAnimatedObjectFromCollisionView()
|
||
{
|
||
if (_savedAnimatedObjectForCollision != null && _savedAnimatedObjectStateForCollision != null)
|
||
{
|
||
CollisionSceneHelper.RestoreAnimatedObjectState(_savedAnimatedObjectForCollision, _savedAnimatedObjectStateForCollision);
|
||
LogManager.Info($"[碰撞构件] 运动物体已恢复到原始状态: {_savedAnimatedObjectForCollision.DisplayName}");
|
||
}
|
||
|
||
// 隐藏虚拟物体(如果之前显示的是虚拟物体)
|
||
if (VirtualObjectManager.Instance.IsVirtualObjectActive)
|
||
{
|
||
VirtualObjectManager.Instance.HideVirtualObject();
|
||
LogManager.Info("[碰撞构件] 虚拟物体已隐藏");
|
||
}
|
||
|
||
_savedAnimatedObjectStateForCollision = null;
|
||
_savedAnimatedObjectForCollision = null;
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region 检测排除对象命令
|
||
|
||
/// <summary>
|
||
/// 从当前选择添加排除对象
|
||
/// </summary>
|
||
private void ExecuteAddExcludedObjectsFromSelection()
|
||
{
|
||
try
|
||
{
|
||
var doc = Autodesk.Navisworks.Api.Application.ActiveDocument;
|
||
var selectedItems = doc?.CurrentSelection?.SelectedItems;
|
||
|
||
if (selectedItems == null || selectedItems.Count == 0)
|
||
{
|
||
UpdateMainStatus("请在Navisworks中选择需要排除的对象");
|
||
LogManager.Warning("[排除对象] 未选择任何对象");
|
||
return;
|
||
}
|
||
|
||
var toAdd = new List<ExcludedObjectViewModel>();
|
||
foreach (ModelItem item in selectedItems)
|
||
{
|
||
if (item == null)
|
||
continue;
|
||
|
||
if (!ModelItemAnalysisHelper.IsModelItemValid(item) || !HasGeometryRecursive(item))
|
||
continue;
|
||
|
||
if (ExcludedObjectExists(item))
|
||
continue;
|
||
|
||
var displayName = ModelItemAnalysisHelper.GetSafeDisplayName(item);
|
||
var modelPath = BuildModelPath(item);
|
||
toAdd.Add(new ExcludedObjectViewModel(item, displayName, modelPath));
|
||
}
|
||
|
||
if (toAdd.Count == 0)
|
||
{
|
||
UpdateMainStatus("选中的对象已在排除列表中或不包含几何体");
|
||
return;
|
||
}
|
||
|
||
int startIndex = _excludedObjects.Count;
|
||
foreach (var vm in toAdd)
|
||
{
|
||
vm.Index = ++startIndex;
|
||
_excludedObjects.Add(vm);
|
||
}
|
||
|
||
UpdateMainStatus($"已添加 {toAdd.Count} 个排除对象");
|
||
LogManager.Info($"[排除对象] 添加 {toAdd.Count} 个对象到排除列表");
|
||
|
||
// 同步到PathAnimationManager
|
||
SyncExcludedObjectsToAnimationManager();
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogManager.Error($"添加排除对象失败: {ex.Message}");
|
||
UpdateMainStatus("添加排除对象失败");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 清除所有排除对象
|
||
/// </summary>
|
||
private void ExecuteClearExcludedObjects()
|
||
{
|
||
try
|
||
{
|
||
int count = _excludedObjects.Count;
|
||
_excludedObjects.Clear();
|
||
ModelHighlightHelper.ClearCategory(ExcludedObjectsHighlightCategory);
|
||
|
||
// 同步到PathAnimationManager
|
||
_pathAnimationManager?.ClearExcludedObjects();
|
||
|
||
UpdateMainStatus($"已清除 {count} 个排除对象");
|
||
LogManager.Info($"[排除对象] 清除所有 {count} 个排除对象");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogManager.Error($"清除排除对象失败: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 移除单个排除对象
|
||
/// </summary>
|
||
private void ExecuteRemoveExcludedObject(ExcludedObjectViewModel obj)
|
||
{
|
||
if (obj == null)
|
||
return;
|
||
|
||
if (_excludedObjects.Remove(obj))
|
||
{
|
||
// 重新编号
|
||
for (int i = 0; i < _excludedObjects.Count; i++)
|
||
{
|
||
_excludedObjects[i].Index = i + 1;
|
||
}
|
||
|
||
UpdateMainStatus($"已移除排除对象 {obj.DisplayName}");
|
||
LogManager.Info($"[排除对象] 移除 {obj.DisplayName}");
|
||
|
||
// 同步到PathAnimationManager(会清除缓存)
|
||
SyncExcludedObjectsToAnimationManager();
|
||
|
||
// 清除该对象的高亮显示
|
||
ModelHighlightHelper.ClearCategory(ExcludedObjectsHighlightCategory);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 高亮显示所有排除对象(全部高亮)
|
||
/// </summary>
|
||
private void ExecuteHighlightAllExcludedObjects()
|
||
{
|
||
try
|
||
{
|
||
if (_excludedObjects.Count == 0)
|
||
return;
|
||
|
||
var itemsToHighlight = _excludedObjects.Select(e => e.ModelItem).Where(m => m != null).ToList();
|
||
ModelHighlightHelper.HighlightItems(ExcludedObjectsHighlightCategory, itemsToHighlight);
|
||
|
||
UpdateMainStatus($"已高亮全部 {itemsToHighlight.Count} 个排除对象");
|
||
LogManager.Info($"[排除对象] 高亮显示全部 {itemsToHighlight.Count} 个对象");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogManager.Error($"高亮排除对象失败: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 清除排除对象高亮
|
||
/// </summary>
|
||
private void ExecuteClearExcludedHighlight()
|
||
{
|
||
try
|
||
{
|
||
ModelHighlightHelper.ClearCategory(ExcludedObjectsHighlightCategory);
|
||
UpdateMainStatus("已清除排除对象高亮");
|
||
LogManager.Info("[排除对象] 清除高亮");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogManager.Error($"清除排除对象高亮失败: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 高亮显示单个排除对象(点选时)
|
||
/// </summary>
|
||
public void HighlightSingleExcludedObject(ExcludedObjectViewModel obj)
|
||
{
|
||
try
|
||
{
|
||
if (obj?.ModelItem == null)
|
||
return;
|
||
|
||
var items = new List<ModelItem> { obj.ModelItem };
|
||
ModelHighlightHelper.HighlightItems(ExcludedObjectsHighlightCategory, items);
|
||
|
||
LogManager.Debug($"[排除对象] 高亮显示单个对象: {obj.DisplayName}");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogManager.Error($"高亮单个排除对象失败: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 检查排除对象是否已存在
|
||
/// </summary>
|
||
private bool ExcludedObjectExists(ModelItem item)
|
||
{
|
||
if (item == null) return false;
|
||
|
||
// 使用ModelItemEquals比较底层原生对象(项目中推荐的比较方式)
|
||
bool exists = _excludedObjects.Any(e => ModelItemAnalysisHelper.ModelItemEquals(e.ModelItem, item));
|
||
|
||
if (exists)
|
||
{
|
||
LogManager.Debug($"[排除对象] 对象已存在: {ModelItemAnalysisHelper.GetSafeDisplayName(item)}");
|
||
}
|
||
|
||
return exists;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 同步排除对象到PathAnimationManager
|
||
/// </summary>
|
||
private void SyncExcludedObjectsToAnimationManager()
|
||
{
|
||
var objectsToExclude = _excludedObjects.Select(e => e.ModelItem).Where(m => m != null).ToList();
|
||
// 使用 SetExcludedObjectsAndClearCache 方法,更新排除列表并清除缓存(确保预计算能实时反映变更)
|
||
_pathAnimationManager?.SetExcludedObjectsAndClearCache(objectsToExclude);
|
||
LogManager.Debug($"[排除对象] 已同步 {objectsToExclude.Count} 个对象到PathAnimationManager,并清除动画缓存");
|
||
}
|
||
|
||
/// <summary>
|
||
/// 从对话框结果添加排除对象到UI列表
|
||
/// </summary>
|
||
private void AddExcludedObjectsToUIList(List<ModelItem> objects)
|
||
{
|
||
if (objects == null || objects.Count == 0) return;
|
||
|
||
int startIndex = _excludedObjects.Count;
|
||
foreach (var item in objects)
|
||
{
|
||
if (item == null || ExcludedObjectExists(item))
|
||
continue;
|
||
|
||
var displayName = ModelItemAnalysisHelper.GetSafeDisplayName(item);
|
||
var modelPath = BuildModelPath(item);
|
||
var vm = new ExcludedObjectViewModel(item, displayName, modelPath, ++startIndex);
|
||
_excludedObjects.Add(vm);
|
||
}
|
||
|
||
UpdateExcludedObjectSummary();
|
||
LogManager.Info($"[排除对象] 从对话框添加 {_excludedObjects.Count} 个排除对象到UI列表");
|
||
}
|
||
|
||
/// <summary>
|
||
/// 排除对象集合变更事件处理
|
||
/// </summary>
|
||
private void OnExcludedObjectsChanged(object sender, NotifyCollectionChangedEventArgs e)
|
||
{
|
||
OnPropertyChanged(nameof(HasExcludedObjects));
|
||
UpdateExcludedObjectSummary();
|
||
|
||
// 刷新命令可用状态
|
||
System.Windows.Input.CommandManager.InvalidateRequerySuggested();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 更新排除对象统计信息
|
||
/// </summary>
|
||
private void UpdateExcludedObjectSummary()
|
||
{
|
||
if (_excludedObjects == null || _excludedObjects.Count == 0)
|
||
{
|
||
ExcludedObjectSummary = "未指定排除对象";
|
||
}
|
||
else
|
||
{
|
||
ExcludedObjectSummary = $"已指定 {_excludedObjects.Count} 个排除对象";
|
||
}
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region 碰撞结果高亮命令
|
||
|
||
private void ExecuteHighlightPrecomputedCollisionResults()
|
||
{
|
||
try
|
||
{
|
||
var deduplicatedResults = _clashIntegration?.GetDeduplicatedCollisionResults();
|
||
LogManager.Debug($"[预计算碰撞结果高亮] 获取到去重后的预计算结果数量: {deduplicatedResults?.Count ?? 0}");
|
||
|
||
if (deduplicatedResults == null || deduplicatedResults.Count == 0)
|
||
{
|
||
UpdateMainStatus("没有可高亮的去重预计算碰撞结果对象");
|
||
LogManager.Warning("[预计算碰撞结果高亮] 去重预计算缓存为空");
|
||
return;
|
||
}
|
||
|
||
// 输出前3个碰撞结果的信息
|
||
for (int i = 0; i < Math.Min(3, deduplicatedResults.Count); i++)
|
||
{
|
||
var collision = deduplicatedResults[i];
|
||
LogManager.Debug($"[预计算碰撞结果高亮] 碰撞{i+1}: {collision.DisplayName}, Item1={collision.Item1?.DisplayName}, Item2={collision.Item2?.DisplayName}");
|
||
}
|
||
|
||
ModelHighlightHelper.ManageCollisionHighlightsByCategory(ModelHighlightHelper.PrecomputeCollisionResultsCategory, deduplicatedResults, clearOtherCategories: false);
|
||
UpdateMainStatus($"已高亮 {deduplicatedResults.Count} 个去重预计算碰撞结果");
|
||
LogManager.Info($"[预计算碰撞结果高亮] 已高亮 {deduplicatedResults.Count} 个结果");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogManager.Error($"高亮预计算碰撞结果失败: {ex.Message}", ex);
|
||
UpdateMainStatus("高亮预计算碰撞结果失败");
|
||
}
|
||
}
|
||
|
||
private void ExecuteClearPrecomputedCollisionHighlights()
|
||
{
|
||
ModelHighlightHelper.ClearCategory(CollisionResultsHighlightCategory);
|
||
UpdateMainStatus("已清除预计算碰撞结果高亮");
|
||
}
|
||
|
||
/// <summary>
|
||
/// 🔥 分析预计算碰撞结果,自动高亮并显示分析对话框
|
||
/// </summary>
|
||
private async Task AnalyzeAndHighlightPrecomputedCollisionsAsync()
|
||
{
|
||
try
|
||
{
|
||
// 1. 获取预计算碰撞结果(从PathAnimationManager,现在缓存帧中已包含碰撞结果)
|
||
var allResults = _pathAnimationManager?.AllCollisionResults;
|
||
if (allResults == null || allResults.Count == 0)
|
||
{
|
||
LogManager.Info("[碰撞分析] 没有预计算碰撞结果,跳过分析");
|
||
// 🔥 没有预计算碰撞,配置已确定,直接保存检测记录
|
||
var recordId = SaveCollisionDetectionRecord();
|
||
if (recordId.HasValue && IsReusedHistoryRecord(recordId.Value))
|
||
{
|
||
LogManager.Info("[碰撞分析] 用户选择使用历史记录,跳过动画生成");
|
||
return;
|
||
}
|
||
return;
|
||
}
|
||
|
||
// 去重:按碰撞对象对去重(取每对的第一次出现)
|
||
var deduplicatedResults = allResults
|
||
.GroupBy(r => new { Item1 = r.Item1, Item2 = r.Item2 })
|
||
.Select(g => g.First())
|
||
.ToList();
|
||
|
||
LogManager.Info($"[碰撞分析] 开始分析 {allResults.Count} 个碰撞结果(去重后 {deduplicatedResults.Count} 个)");
|
||
|
||
// 2. 自动高亮所有预计算碰撞结果
|
||
ModelHighlightHelper.ManageCollisionHighlightsByCategory(
|
||
ModelHighlightHelper.PrecomputeCollisionResultsCategory,
|
||
deduplicatedResults,
|
||
clearOtherCategories: false);
|
||
LogManager.Info($"[碰撞分析] 已高亮 {deduplicatedResults.Count} 个碰撞对象");
|
||
|
||
// 3. 分析碰撞热点(使用原始结果 - 按频率统计)
|
||
var hotspots = AnalyzeCollisionHotspots(allResults);
|
||
|
||
if (hotspots.Count > 0)
|
||
{
|
||
LogManager.Info($"[碰撞分析] 发现 {hotspots.Count} 个高频碰撞物体");
|
||
foreach (var hotspot in hotspots.Take(5))
|
||
{
|
||
LogManager.Info($" - {hotspot.ObjectName}: {hotspot.CollisionCount} 次 ({hotspot.Percentage:F1}%) - {hotspot.Reason}");
|
||
}
|
||
|
||
// 4. 显示分析对话框(在UI线程)- 传递原始总数用于百分比计算
|
||
await _uiStateManager.ExecuteUIUpdateAsync(() =>
|
||
{
|
||
ShowCollisionAnalysisDialog(hotspots, allResults.Count);
|
||
});
|
||
}
|
||
else
|
||
{
|
||
LogManager.Info("[碰撞分析] 没有发现明显的高频碰撞物体");
|
||
// 🔥 有碰撞但无高频热点,配置已确定,保存检测记录
|
||
var recordId = SaveCollisionDetectionRecord();
|
||
if (recordId.HasValue && IsReusedHistoryRecord(recordId.Value))
|
||
{
|
||
// 用户选择使用历史记录,跳过后续流程
|
||
LogManager.Info("[碰撞分析] 用户选择使用历史记录,跳过动画生成");
|
||
return;
|
||
}
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogManager.Error($"[碰撞分析] 分析预计算碰撞结果失败: {ex.Message}", ex);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 分析碰撞热点 - 找出高频碰撞的物体
|
||
/// </summary>
|
||
private List<CollisionHotspotInfo> AnalyzeCollisionHotspots(List<CollisionResult> results)
|
||
{
|
||
// 按被撞物体分组统计
|
||
var objectStats = results
|
||
.Where(r => r.Item2 != null)
|
||
.GroupBy(r => r.Item2)
|
||
.Select(g => new
|
||
{
|
||
Object = g.Key,
|
||
Count = g.Count(),
|
||
Percentage = (double)g.Count() / results.Count * 100
|
||
})
|
||
.Where(x => x.Count >= 10 || x.Percentage > 10) // 只关注高频物体
|
||
.OrderByDescending(x => x.Count)
|
||
.ToList();
|
||
|
||
var hotspots = new List<CollisionHotspotInfo>();
|
||
|
||
foreach (var stat in objectStats)
|
||
{
|
||
var objectName = stat.Object.DisplayName ?? "未知对象";
|
||
var reason = GetHotspotReason(objectName, stat.Object);
|
||
|
||
hotspots.Add(new CollisionHotspotInfo
|
||
{
|
||
Object = stat.Object,
|
||
ObjectName = objectName,
|
||
CollisionCount = stat.Count,
|
||
Percentage = stat.Percentage,
|
||
Reason = reason,
|
||
RecommendedAction = reason.Contains("地面") || reason.Contains("楼板") || reason.Contains("墙体")
|
||
? "建议排除"
|
||
: "需要检查"
|
||
});
|
||
}
|
||
|
||
return hotspots;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取热点原因分析
|
||
/// </summary>
|
||
private string GetHotspotReason(string objectName, ModelItem item)
|
||
{
|
||
var name = objectName.ToLower();
|
||
|
||
// 检查名称特征
|
||
if (name.Contains("地面") || name.Contains("楼板") || name.Contains("floor") || name.Contains("slab"))
|
||
return "地面/楼板 - 大面积平面物体,容易产生假阳性碰撞";
|
||
|
||
if (name.Contains("墙体") || name.Contains("wall") || name.Contains("外墙"))
|
||
return "墙体 - 沿路径延伸,容易产生假阳性碰撞";
|
||
|
||
if (name.Contains("楼梯") || name.Contains("stair"))
|
||
return "楼梯 - 结构复杂,可能产生多次碰撞检测";
|
||
|
||
if (name.Contains("屋顶") || name.Contains("roof") || name.Contains("天花板"))
|
||
return "屋顶/天花板 - 位于上方,可能产生假阳性碰撞";
|
||
|
||
// 检查包围盒大小
|
||
try
|
||
{
|
||
var bbox = item.BoundingBox();
|
||
var volume = (bbox.Max.X - bbox.Min.X) * (bbox.Max.Y - bbox.Min.Y) * (bbox.Max.Z - bbox.Min.Z);
|
||
if (volume > 1000) // 大于1000立方米
|
||
return "大型物体 - 包围盒很大,容易与路径相交";
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogManager.Debug($"[碰撞分析] 获取物体包围盒失败: {ex.Message}");
|
||
}
|
||
|
||
return "未知原因 - 建议手动检查";
|
||
}
|
||
|
||
/// <summary>
|
||
/// 显示碰撞分析对话框
|
||
/// </summary>
|
||
private void ShowCollisionAnalysisDialog(List<CollisionHotspotInfo> hotspots, int totalCollisions)
|
||
{
|
||
try
|
||
{
|
||
var dialog = new CollisionAnalysisDialog(hotspots, totalCollisions);
|
||
DialogHelper.SetOwnerSafely(dialog);
|
||
var result = dialog.ShowDialog();
|
||
|
||
if (result == null)
|
||
{
|
||
// 用户取消
|
||
UpdateMainStatus("预计算碰撞分析已取消");
|
||
return;
|
||
}
|
||
|
||
if (dialog.ExcludedObjects.Count > 0)
|
||
{
|
||
// 用户选择了排除某些物体并重新生成
|
||
LogManager.Info($"[碰撞分析] 用户选择排除 {dialog.ExcludedObjects.Count} 个物体并重新生成");
|
||
|
||
// 🔥 添加排除对象到UI列表(合并,自动去重)
|
||
AddExcludedObjectsToUIList(dialog.ExcludedObjects);
|
||
|
||
// 🔥 从UI列表获取所有排除对象(包括之前手工添加的),设置到Manager并重新生成
|
||
var allExcludedObjects = _excludedObjects.Select(e => e.ModelItem).Where(m => m != null).ToList();
|
||
_pathAnimationManager.SetExcludedObjectsAndClearCache(allExcludedObjects);
|
||
|
||
// 触发重新生成(使用 Dispatcher 避免阻塞 UI)
|
||
System.Windows.Application.Current.Dispatcher.BeginInvoke(new Action(() =>
|
||
{
|
||
ExecuteGenerateAnimation();
|
||
}), DispatcherPriority.Background);
|
||
|
||
UpdateMainStatus($"已排除 {allExcludedObjects.Count} 个物体,正在重新生成...");
|
||
}
|
||
else
|
||
{
|
||
// 用户选择直接继续(无排除对象)
|
||
LogManager.Info("[碰撞分析] 用户选择直接继续生成");
|
||
UpdateMainStatus($"预计算碰撞分析完成,共 {totalCollisions} 个候选碰撞");
|
||
|
||
// 🔥 保存检测记录到数据库(保存测试配置)
|
||
var recordId = SaveCollisionDetectionRecord();
|
||
if (recordId.HasValue && IsReusedHistoryRecord(recordId.Value))
|
||
{
|
||
// 用户选择使用历史记录,跳过后续流程
|
||
LogManager.Info("[碰撞分析] 用户选择使用历史记录,跳过动画生成");
|
||
return;
|
||
}
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogManager.Error($"[碰撞分析] 显示分析对话框失败: {ex.Message}", ex);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 保存检测记录到数据库
|
||
/// 在生成动画完成后调用,创建检测记录并保存当时的完整配置
|
||
/// </summary>
|
||
private int? SaveCollisionDetectionRecord()
|
||
{
|
||
try
|
||
{
|
||
var pathDatabase = _pathPlanningManager?.GetPathDatabase();
|
||
if (pathDatabase == null)
|
||
{
|
||
LogManager.Warning("[检测记录] 数据库未初始化,无法保存检测记录");
|
||
return null;
|
||
}
|
||
|
||
string routeId = CurrentPathRoute?.Id ?? "";
|
||
|
||
// 🔥 检查是否已有相同配置的检测记录
|
||
var existingRecordResult = CheckExistingDetectionRecord();
|
||
if (existingRecordResult.HasValue)
|
||
{
|
||
var existingId = existingRecordResult.Value.id;
|
||
var userChoice = existingRecordResult.Value.userChoice;
|
||
|
||
if (userChoice == UserChoice.UseExisting)
|
||
{
|
||
// 用户选择使用历史记录
|
||
LogManager.Info($"[检测记录] 用户选择使用历史记录 (Id={existingId})");
|
||
_pathAnimationManager.CurrentDetectionRecordId = existingId;
|
||
_pathAnimationManager.IsUsingHistoryRecord = true; // 标记使用历史记录,动画完成后跳过ClashDetective
|
||
|
||
// 在历史列表中选中该记录
|
||
SelectHistoryRecordById(existingId);
|
||
|
||
return existingId;
|
||
}
|
||
// 用户选择重新生成,继续创建新记录
|
||
LogManager.Info($"[检测记录] 用户选择重新生成,创建新记录");
|
||
}
|
||
|
||
int recordId = CreateCollisionDetectionRecordSnapshot("动画生成", bindToAnimationManager: true);
|
||
|
||
// 🔥 注册动画配置哈希与检测记录ID的映射(用于后续检查重复配置)
|
||
try
|
||
{
|
||
var animatedObject = UseVirtualObject ? VirtualObjectManager.Instance.CurrentVirtualObject : SelectedAnimatedObject;
|
||
var route = CurrentPathRoute?.Route;
|
||
if (animatedObject != null && route != null)
|
||
{
|
||
var animationHash = _pathAnimationManager.ComputeAnimationConfigHashFromRoute(animatedObject, route);
|
||
_pathAnimationManager.RegisterAnimationHashToRecordId(animationHash, recordId);
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogManager.Warning($"[检测记录] 注册动画哈希映射失败: {ex.Message}");
|
||
}
|
||
|
||
return recordId;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogManager.Error($"[检测记录] 保存失败: {ex.Message}");
|
||
return null;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 创建碰撞检测记录快照。
|
||
/// 生成动画和批处理都应该创建自己的配置快照,不依赖旧检测结果。
|
||
/// </summary>
|
||
private int CreateCollisionDetectionRecordSnapshot(string scenarioName, bool bindToAnimationManager)
|
||
{
|
||
var pathDatabase = _pathPlanningManager?.GetPathDatabase();
|
||
if (pathDatabase == null)
|
||
{
|
||
throw new InvalidOperationException("数据库未初始化,无法保存检测记录");
|
||
}
|
||
|
||
string routeId = CurrentPathRoute?.Id ?? string.Empty;
|
||
string testName = $"{scenarioName}_{DateTime.Now:MMdd_HHmmssfff}";
|
||
double metersToUnits = UnitsConverter.GetMetersToUnitsConversionFactor();
|
||
|
||
var record = new CollisionDetectionRecord
|
||
{
|
||
RouteId = routeId,
|
||
CreatedTime = DateTime.Now,
|
||
FrameRate = _animationFrameRate,
|
||
DurationSeconds = AnimationDuration,
|
||
DetectionTolerance = _detectionTolerance * metersToUnits,
|
||
IsVirtualObject = UseVirtualObject,
|
||
AnimatedObjectName = UseVirtualObject ? "虚拟物体" : SelectedAnimatedObject?.DisplayName,
|
||
DetectAllObjects = !IsManualCollisionTargetEnabled,
|
||
ObjectRotationCorrection = _objectRotationCorrection.ZDegrees,
|
||
Description = $"{scenarioName}于 {DateTime.Now:yyyy-MM-dd HH:mm:ss}",
|
||
TestName = testName,
|
||
TestTime = DateTime.Now,
|
||
CollisionCount = 0,
|
||
AnimationCollisionCount = 0
|
||
};
|
||
|
||
if (UseVirtualObject)
|
||
{
|
||
record.VirtualObjectLength = VirtualObjectLengthInMeters * metersToUnits;
|
||
record.VirtualObjectWidth = VirtualObjectWidthInMeters * metersToUnits;
|
||
record.VirtualObjectHeight = VirtualObjectHeightInMeters * metersToUnits;
|
||
}
|
||
else if (SelectedAnimatedObject != null)
|
||
{
|
||
try
|
||
{
|
||
var pathId = Autodesk.Navisworks.Api.Application.ActiveDocument.Models.CreatePathId(SelectedAnimatedObject);
|
||
record.ObjectModelIndex = pathId.ModelIndex;
|
||
record.ObjectPathId = pathId.PathId;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogManager.Warning($"[检测记录] 获取运动物体PathId失败: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
int recordId = pathDatabase.SaveCollisionDetectionRecord(record);
|
||
LogManager.Info($"[检测记录] 已创建{scenarioName}记录 (Id={recordId})");
|
||
|
||
if (bindToAnimationManager)
|
||
{
|
||
_pathAnimationManager.CurrentDetectionRecordId = recordId;
|
||
_pathAnimationManager.IsUsingHistoryRecord = false;
|
||
}
|
||
|
||
var excludedObjects = _pathAnimationManager?.GetExcludedObjects();
|
||
if (excludedObjects != null && excludedObjects.Count > 0)
|
||
{
|
||
var excludedRecords = new List<CollisionDetectionExcludedObjectRecord>();
|
||
foreach (var obj in excludedObjects)
|
||
{
|
||
if (obj == null) continue;
|
||
try
|
||
{
|
||
var pathId = Autodesk.Navisworks.Api.Application.ActiveDocument.Models.CreatePathId(obj);
|
||
excludedRecords.Add(new CollisionDetectionExcludedObjectRecord
|
||
{
|
||
DetectionRecordId = recordId,
|
||
ModelIndex = pathId.ModelIndex,
|
||
PathId = pathId.PathId,
|
||
DisplayName = obj.DisplayName,
|
||
ObjectName = obj.DisplayName,
|
||
Reason = "预计算碰撞分析排除"
|
||
});
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogManager.Warning($"[检测记录] 准备排除对象记录失败: {obj.DisplayName}, {ex.Message}");
|
||
}
|
||
}
|
||
|
||
if (excludedRecords.Count > 0)
|
||
{
|
||
pathDatabase.SaveCollisionDetectionExcludedObjects(recordId, excludedRecords);
|
||
LogManager.Info($"[检测记录] 已保存 {excludedRecords.Count} 个排除对象");
|
||
}
|
||
}
|
||
|
||
if (IsManualCollisionTargetEnabled && _manualCollisionTargets != null && _manualCollisionTargets.Count > 0)
|
||
{
|
||
var manualTargetRecords = new List<CollisionDetectionManualTargetRecord>();
|
||
foreach (var target in _manualCollisionTargets)
|
||
{
|
||
if (target?.ModelItem == null) continue;
|
||
try
|
||
{
|
||
var pathId = Autodesk.Navisworks.Api.Application.ActiveDocument.Models.CreatePathId(target.ModelItem);
|
||
manualTargetRecords.Add(new CollisionDetectionManualTargetRecord
|
||
{
|
||
DetectionRecordId = recordId,
|
||
ModelIndex = pathId.ModelIndex,
|
||
PathId = pathId.PathId,
|
||
DisplayName = target.ModelItem.DisplayName,
|
||
ObjectName = target.ModelItem.DisplayName
|
||
});
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogManager.Warning($"[检测记录] 准备手工目标记录失败: {target.DisplayName}, {ex.Message}");
|
||
}
|
||
}
|
||
|
||
if (manualTargetRecords.Count > 0)
|
||
{
|
||
pathDatabase.SaveCollisionDetectionManualTargets(recordId, manualTargetRecords);
|
||
LogManager.Info($"[检测记录] 已保存 {manualTargetRecords.Count} 个手工目标");
|
||
}
|
||
}
|
||
|
||
return recordId;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 用户选择析析
|
||
/// </summary>
|
||
private enum UserChoice
|
||
{
|
||
CreateNew, // 创建新记录
|
||
UseExisting // 使用历史记录
|
||
}
|
||
|
||
/// <summary>
|
||
/// 检查是否已有相同配置的检测记录
|
||
/// 策略:1.先检查内存缓存 2.SQL过滤基础参数 3.比较排除列表和手工目标
|
||
/// </summary>
|
||
/// <returns>(记录ID, 用户选择),不存在或用户取消则返回null</returns>
|
||
private (int id, UserChoice userChoice)? CheckExistingDetectionRecord()
|
||
{
|
||
try
|
||
{
|
||
var routeId = CurrentPathRoute?.Id;
|
||
if (string.IsNullOrEmpty(routeId))
|
||
{
|
||
return null;
|
||
}
|
||
|
||
var pathDatabase = _pathPlanningManager?.GetPathDatabase();
|
||
if (pathDatabase == null)
|
||
{
|
||
return null;
|
||
}
|
||
|
||
var animatedObject = UseVirtualObject ? VirtualObjectManager.Instance.CurrentVirtualObject : SelectedAnimatedObject;
|
||
if (animatedObject == null)
|
||
{
|
||
return null;
|
||
}
|
||
|
||
// 1. 先检查内存缓存(程序运行期间已处理过的)
|
||
var currentHash = _pathAnimationManager.ComputeAnimationConfigHashFromRoute(animatedObject, CurrentPathRoute.Route);
|
||
var cachedRecordId = _pathAnimationManager.GetDetectionRecordIdByHash(currentHash);
|
||
if (cachedRecordId.HasValue)
|
||
{
|
||
var cachedRecord = pathDatabase.GetDetectionResultById(cachedRecordId.Value);
|
||
if (cachedRecord != null)
|
||
{
|
||
LogManager.Info($"[CheckExistingDetectionRecord] 内存缓存命中: RecordId={cachedRecordId}");
|
||
return ShowDuplicateConfigDialog(cachedRecord);
|
||
}
|
||
}
|
||
|
||
// 2. SQL过滤基础参数匹配的记录
|
||
// 注意:数据库存储的是模型单位(不带Meters后缀),查询时传模型单位
|
||
var factor = UnitsConverter.GetMetersToUnitsConversionFactor();
|
||
var matchingRecords = pathDatabase.FindDetectionRecordsByBaseParams(
|
||
routeId,
|
||
_animationFrameRate,
|
||
AnimationDuration,
|
||
_detectionTolerance * factor, // 米 → 模型单位
|
||
UseVirtualObject,
|
||
UseVirtualObject ? VirtualObjectLengthInMeters * factor : (double?)null, // 米 → 模型单位
|
||
UseVirtualObject ? VirtualObjectWidthInMeters * factor : (double?)null,
|
||
UseVirtualObject ? VirtualObjectHeightInMeters * factor : (double?)null,
|
||
_objectRotationCorrection.ZDegrees,
|
||
IsManualCollisionTargetEnabled
|
||
);
|
||
|
||
if (matchingRecords == null || matchingRecords.Count == 0)
|
||
{
|
||
LogManager.Info("[CheckExistingDetectionRecord] SQL基础参数过滤无匹配");
|
||
return null;
|
||
}
|
||
|
||
LogManager.Info($"[CheckExistingDetectionRecord] SQL找到 {matchingRecords.Count} 条记录,开始比较排除列表");
|
||
|
||
// 3. 逐个比较排除列表和手工目标
|
||
var currentExcluded = GetCurrentExcludedObjects();
|
||
var currentTargets = GetCurrentManualTargets();
|
||
|
||
foreach (var record in matchingRecords)
|
||
{
|
||
if (IsExcludedListMatch(record.Id, currentExcluded, pathDatabase) &&
|
||
IsManualTargetListMatch(record.Id, currentTargets, pathDatabase))
|
||
{
|
||
LogManager.Info($"[CheckExistingDetectionRecord] 找到完全匹配: RecordId={record.Id}");
|
||
|
||
// 注册到内存缓存
|
||
_pathAnimationManager.RegisterAnimationHashToRecordId(currentHash, record.Id);
|
||
|
||
return ShowDuplicateConfigDialog(record);
|
||
}
|
||
}
|
||
|
||
LogManager.Info("[CheckExistingDetectionRecord] 未找到完全匹配的记录");
|
||
return null;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogManager.Warning($"[CheckExistingDetectionRecord] 检查失败: {ex.Message}");
|
||
return null;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 显示重复配置对话框
|
||
/// </summary>
|
||
private (int id, UserChoice userChoice)? ShowDuplicateConfigDialog(CollisionDetectionRecord record)
|
||
{
|
||
var message = $"检测配置已存在!\n\n" +
|
||
$"找到一条相同配置的历史检测记录:\n" +
|
||
$"• 测试名称:{record.TestName ?? "未命名"}\n" +
|
||
$"• 检测时间:{record.TestTime:yyyy-MM-dd HH:mm:ss}\n" +
|
||
$"• 碰撞数量:{record.CollisionCount} 个\n\n" +
|
||
$"您可以:\n" +
|
||
$"• 【是】直接使用历史记录(跳过重复检测)\n" +
|
||
$"• 【否】重新生成新记录";
|
||
|
||
var result = DialogHelper.ShowMessageBox(
|
||
message,
|
||
"检测配置已存在",
|
||
System.Windows.MessageBoxButton.YesNo,
|
||
System.Windows.MessageBoxImage.Question);
|
||
|
||
if (result == System.Windows.MessageBoxResult.Yes)
|
||
{
|
||
_lastReusedRecordId = record.Id;
|
||
return (record.Id, UserChoice.UseExisting);
|
||
}
|
||
else
|
||
{
|
||
_lastReusedRecordId = null;
|
||
return (record.Id, UserChoice.CreateNew);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取当前排除对象列表(ModelIndex + PathId)
|
||
/// </summary>
|
||
private List<(int ModelIndex, string PathId)> GetCurrentExcludedObjects()
|
||
{
|
||
var result = new List<(int, string)>();
|
||
var excludedObjects = _pathAnimationManager?.GetExcludedObjects();
|
||
|
||
if (excludedObjects != null)
|
||
{
|
||
foreach (var obj in excludedObjects)
|
||
{
|
||
if (obj == null) continue;
|
||
try
|
||
{
|
||
var pathId = Autodesk.Navisworks.Api.Application.ActiveDocument.Models.CreatePathId(obj);
|
||
result.Add((pathId.ModelIndex, pathId.PathId));
|
||
}
|
||
catch { }
|
||
}
|
||
}
|
||
|
||
return result.OrderBy(x => x.Item1).ThenBy(x => x.Item2).ToList();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取当前手工碰撞目标列表(ModelIndex + PathId)
|
||
/// </summary>
|
||
private List<(int ModelIndex, string PathId)> GetCurrentManualTargets()
|
||
{
|
||
var result = new List<(int, string)>();
|
||
|
||
if (IsManualCollisionTargetEnabled && _manualCollisionTargets != null)
|
||
{
|
||
foreach (var target in _manualCollisionTargets)
|
||
{
|
||
if (target?.ModelItem == null) continue;
|
||
try
|
||
{
|
||
var pathId = Autodesk.Navisworks.Api.Application.ActiveDocument.Models.CreatePathId(target.ModelItem);
|
||
result.Add((pathId.ModelIndex, pathId.PathId));
|
||
}
|
||
catch { }
|
||
}
|
||
}
|
||
|
||
return result.OrderBy(x => x.Item1).ThenBy(x => x.Item2).ToList();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 比较排除列表是否匹配
|
||
/// </summary>
|
||
private bool IsExcludedListMatch(int recordId, List<(int ModelIndex, string PathId)> currentList, PathDatabase db)
|
||
{
|
||
var dbList = db.GetDetectionExcludedObjects(recordId)
|
||
.Select(e => (e.ModelIndex, e.PathId))
|
||
.OrderBy(x => x.ModelIndex)
|
||
.ThenBy(x => x.PathId)
|
||
.ToList();
|
||
|
||
return dbList.SequenceEqual(currentList);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 比较手工目标列表是否匹配
|
||
/// </summary>
|
||
private bool IsManualTargetListMatch(int recordId, List<(int ModelIndex, string PathId)> currentList, PathDatabase db)
|
||
{
|
||
var dbList = db.GetDetectionManualTargets(recordId)
|
||
.Select(t => (t.ModelIndex, t.PathId))
|
||
.OrderBy(x => x.ModelIndex)
|
||
.ThenBy(x => x.PathId)
|
||
.ToList();
|
||
|
||
return dbList.SequenceEqual(currentList);
|
||
}
|
||
|
||
private void ExecuteToggleWireframeMode()
|
||
{
|
||
try
|
||
{
|
||
var doc = Autodesk.Navisworks.Api.Application.ActiveDocument;
|
||
if (doc == null || doc.IsClear)
|
||
{
|
||
LogManager.Warning("[线框模式] 没有活动的文档");
|
||
UpdateMainStatus("无法切换线框模式:没有活动文档");
|
||
return;
|
||
}
|
||
|
||
// 创建当前视角的副本
|
||
var newViewpoint = doc.CurrentViewpoint.Value.CreateCopy();
|
||
|
||
// 判断当前是否已经是线框模式
|
||
bool isWireframe = (newViewpoint.RenderStyle == ViewpointRenderStyle.Wireframe);
|
||
|
||
if (!isWireframe)
|
||
{
|
||
// 切换到线框模式
|
||
newViewpoint.RenderStyle = ViewpointRenderStyle.Wireframe;
|
||
|
||
LogManager.Info("[线框模式] 视图模式已切换为Wireframe");
|
||
UpdateMainStatus("已切换到线框模式");
|
||
}
|
||
else
|
||
{
|
||
// 切换回着色模式
|
||
newViewpoint.RenderStyle = ViewpointRenderStyle.Shaded;
|
||
|
||
LogManager.Info("[线框模式] 视图模式已切换回Shaded");
|
||
UpdateMainStatus("已切换回着色模式");
|
||
}
|
||
|
||
// 应用修改后的视角
|
||
doc.CurrentViewpoint.CopyFrom(newViewpoint);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogManager.Error($"切换线框模式失败: {ex.Message}", ex);
|
||
UpdateMainStatus("切换线框模式失败");
|
||
}
|
||
}
|
||
|
||
private void ExecuteHighlightClashDetectiveResults()
|
||
{
|
||
if (string.IsNullOrEmpty(_clashIntegration?.CurrentTestName))
|
||
{
|
||
LogManager.Warning("[ClashDetective高亮] 当前测试名称为空,无法高亮结果");
|
||
return;
|
||
}
|
||
|
||
ModelHighlightHelper.HighlightClashDetectiveResults(_clashIntegration.CurrentTestName, _clashIntegration.GetCurrentPathClashResults);
|
||
UpdateMainStatus("已高亮ClashDetective检测结果");
|
||
}
|
||
|
||
private void ExecuteClearClashDetectiveHighlights()
|
||
{
|
||
ModelHighlightHelper.ClearClashDetectiveHighlights();
|
||
UpdateMainStatus("已清除ClashDetective高亮");
|
||
}
|
||
|
||
/// <summary>
|
||
/// 刷新ClashDetective结果列表
|
||
/// </summary>
|
||
private void RefreshClashDetectiveResultsList()
|
||
{
|
||
try
|
||
{
|
||
var pathName = CurrentPathRoute?.Name;
|
||
LogManager.Debug($"[刷新碰撞历史] CurrentPathRoute={CurrentPathRoute?.Id}, Name={pathName}");
|
||
if (string.IsNullOrEmpty(pathName))
|
||
{
|
||
LogManager.Warning("[刷新碰撞历史] 路径名称为空,清空列表");
|
||
_clashDetectiveResults.Clear();
|
||
HasClashDetectiveResults = false;
|
||
return;
|
||
}
|
||
|
||
var pathDatabase = PathPlanningManager.Instance?.GetPathDatabase();
|
||
if (pathDatabase == null)
|
||
{
|
||
_clashDetectiveResults.Clear();
|
||
HasClashDetectiveResults = false;
|
||
return;
|
||
}
|
||
|
||
var records = pathDatabase.GetDetectionResultsByPath(pathName);
|
||
LogManager.Info($"[刷新碰撞历史] 查询到 {records.Count} 条记录,路径={pathName}");
|
||
_clashDetectiveResults.Clear();
|
||
|
||
foreach (var record in records)
|
||
{
|
||
var resultViewModel = new ClashDetectiveResultViewModel(record, RefreshClashDetectiveResultsList);
|
||
|
||
// 加载运动物体(Item1)
|
||
// 虚拟物体:只记录尺寸(数据库中是模型单位),等用户点击时再创建
|
||
// 真实物体:尝试解析PathId获取引用
|
||
if (record.IsVirtualObject)
|
||
{
|
||
// 虚拟物体必须有尺寸
|
||
if (!record.VirtualObjectLength.HasValue ||
|
||
!record.VirtualObjectWidth.HasValue ||
|
||
!record.VirtualObjectHeight.HasValue)
|
||
{
|
||
LogManager.Error($"[碰撞历史] 虚拟物体记录缺少尺寸: TestName={record.TestName}");
|
||
}
|
||
resultViewModel.VirtualObjectLengthInMeters = record.VirtualObjectLength.Value;
|
||
resultViewModel.VirtualObjectWidthInMeters = record.VirtualObjectWidth.Value;
|
||
resultViewModel.VirtualObjectHeightInMeters = record.VirtualObjectHeight.Value;
|
||
}
|
||
else if (record.ObjectModelIndex.HasValue && !string.IsNullOrEmpty(record.ObjectPathId))
|
||
{
|
||
try
|
||
{
|
||
var animatedObjectPathId = new Autodesk.Navisworks.Api.DocumentParts.ModelItemPathId
|
||
{
|
||
ModelIndex = record.ObjectModelIndex.Value,
|
||
PathId = record.ObjectPathId
|
||
};
|
||
resultViewModel.AnimatedObject = Autodesk.Navisworks.Api.Application.ActiveDocument.Models.ResolvePathId(animatedObjectPathId);
|
||
LogManager.Info($"[碰撞历史] 已加载运动物体: {resultViewModel.AnimatedObject?.DisplayName ?? "未找到"} for TestName={record.TestName}");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogManager.Warning($"[碰撞历史] 无法加载运动物体: ModelIndex={record.ObjectModelIndex}, PathId={record.ObjectPathId}, 错误: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
// 同时加载该结果的碰撞构件清单
|
||
var collisionObjectRecords = pathDatabase.GetClashDetectiveCollisionObjects(record.Id);
|
||
// record.Id 就是 DetectionRecordId
|
||
if (collisionObjectRecords != null)
|
||
{
|
||
int index = 1;
|
||
foreach (var objRecord in collisionObjectRecords)
|
||
{
|
||
CollisionObjectViewModel objViewModel = null;
|
||
ModelItem modelItem = null;
|
||
string modelPath = "路径获取失败";
|
||
|
||
// 通过 ModelIndex 和 PathId 重建 ModelItem
|
||
if (objRecord.ModelIndex.HasValue && !string.IsNullOrEmpty(objRecord.PathId))
|
||
{
|
||
try
|
||
{
|
||
var pathIdObj = new Autodesk.Navisworks.Api.DocumentParts.ModelItemPathId
|
||
{
|
||
ModelIndex = objRecord.ModelIndex.Value,
|
||
PathId = objRecord.PathId
|
||
};
|
||
modelItem = Autodesk.Navisworks.Api.Application.ActiveDocument.Models.ResolvePathId(pathIdObj);
|
||
|
||
// 获取完整的节点树路径
|
||
if (modelItem != null)
|
||
{
|
||
modelPath = BuildModelPath(modelItem);
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogManager.Warning($"[碰撞构件] 无法通过 PathId 找到 ModelItem: ModelIndex={objRecord.ModelIndex}, PathId={objRecord.PathId}, 错误: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
objViewModel = new CollisionObjectViewModel
|
||
{
|
||
Index = index++,
|
||
DisplayName = objRecord.DisplayName,
|
||
ObjectName = objRecord.ObjectName,
|
||
ModelPath = modelPath,
|
||
ResultId = objRecord.DetectionRecordId,
|
||
TestName = record.TestName,
|
||
ModelIndex = objRecord.ModelIndex,
|
||
PathId = objRecord.PathId,
|
||
ModelItem = modelItem,
|
||
HighlightCommand = new RelayCommand(() => resultViewModel.ExecuteHighlightCollisionObject(objViewModel)),
|
||
// 填充碰撞位置信息
|
||
AnimatedObjectTrackedPosition = objRecord.HasPositionInfo && objRecord.AnimatedObjectTrackedPosX.HasValue ?
|
||
new Point3D(objRecord.AnimatedObjectTrackedPosX.Value, objRecord.AnimatedObjectTrackedPosY.Value, objRecord.AnimatedObjectTrackedPosZ.Value) : null,
|
||
AnimatedObjectTrackedYawRadians = objRecord.AnimatedObjectTrackedYawRadians ?? 0,
|
||
AnimatedObjectTrackedRotation = objRecord.AnimatedObjectHasTrackedRotation &&
|
||
objRecord.AnimatedObjectTrackedRotA.HasValue &&
|
||
objRecord.AnimatedObjectTrackedRotB.HasValue &&
|
||
objRecord.AnimatedObjectTrackedRotC.HasValue &&
|
||
objRecord.AnimatedObjectTrackedRotD.HasValue
|
||
? new Rotation3D(
|
||
objRecord.AnimatedObjectTrackedRotA.Value,
|
||
objRecord.AnimatedObjectTrackedRotB.Value,
|
||
objRecord.AnimatedObjectTrackedRotC.Value,
|
||
objRecord.AnimatedObjectTrackedRotD.Value)
|
||
: Rotation3D.Identity,
|
||
AnimatedObjectHasTrackedRotation = objRecord.AnimatedObjectHasTrackedRotation,
|
||
HasPositionInfo = objRecord.HasPositionInfo
|
||
};
|
||
resultViewModel.CollisionObjects.Add(objViewModel);
|
||
}
|
||
}
|
||
|
||
_clashDetectiveResults.Add(resultViewModel);
|
||
}
|
||
|
||
// 更新HasClashDetectiveResults属性
|
||
HasClashDetectiveResults = _clashDetectiveResults.Count > 0;
|
||
|
||
UpdateMainStatus($"已刷新ClashDetective结果列表:{_clashDetectiveResults.Count}条记录");
|
||
LogManager.Info($"ClashDetective结果列表已刷新:{_clashDetectiveResults.Count}条记录");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogManager.Error($"刷新ClashDetective结果列表失败: {ex.Message}");
|
||
_clashDetectiveResults.Clear();
|
||
HasClashDetectiveResults = false;
|
||
}
|
||
}
|
||
|
||
private void ExecuteRefreshClashDetectiveResults()
|
||
{
|
||
RefreshClashDetectiveResultsList();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 在历史列表中选中指定ID的记录,并打开碰撞报告
|
||
/// </summary>
|
||
private void SelectHistoryRecordById(int recordId)
|
||
{
|
||
try
|
||
{
|
||
// 先刷新列表确保记录存在
|
||
RefreshClashDetectiveResultsList();
|
||
|
||
// 查找并选中记录
|
||
var targetRecord = _clashDetectiveResults.FirstOrDefault(r => r.Record?.Id == recordId);
|
||
if (targetRecord != null)
|
||
{
|
||
// 先设置选中项,确保UI更新
|
||
SelectedClashDetectiveResult = targetRecord;
|
||
LogManager.Info($"[SelectHistoryRecordById] 已在历史列表中选中记录 (Id={recordId})");
|
||
|
||
// 更新状态栏显示
|
||
if (targetRecord.Record?.CollisionCount > 0)
|
||
{
|
||
UpdateMainStatus($"已加载历史记录:{targetRecord.Record.TestName},{targetRecord.Record.CollisionCount} 个碰撞");
|
||
}
|
||
else
|
||
{
|
||
UpdateMainStatus($"已加载历史记录:{targetRecord.Record.TestName},无碰撞");
|
||
}
|
||
|
||
// 🔥 打开该历史记录的碰撞报告,让用户明确知道使用了哪条记录
|
||
if (!string.IsNullOrEmpty(targetRecord.Record?.TestName))
|
||
{
|
||
LogManager.Info($"[SelectHistoryRecordById] 正在打开历史记录报告: {targetRecord.Record.TestName}");
|
||
OpenHistoryReportAsync(recordId);
|
||
}
|
||
}
|
||
else
|
||
{
|
||
LogManager.Warning($"[SelectHistoryRecordById] 未在列表中找到记录 (Id={recordId})");
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogManager.Error($"[SelectHistoryRecordById] 选中记录失败: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 检查是否是复用的历史记录
|
||
/// </summary>
|
||
private bool IsReusedHistoryRecord(int recordId)
|
||
{
|
||
return _lastReusedRecordId.HasValue && _lastReusedRecordId.Value == recordId;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 异步打开历史碰撞报告
|
||
/// </summary>
|
||
private async void OpenHistoryReportAsync(int recordId)
|
||
{
|
||
try
|
||
{
|
||
// 使用检测记录ID查询生成报告,更可靠
|
||
var command = GenerateCollisionReportCommand.CreateComprehensive(hasHighlighted: true);
|
||
command.SetDetectionRecordId(recordId);
|
||
var result = await command.ExecuteAsync();
|
||
|
||
if (result.IsSuccess && result is PathPlanningResult<CollisionReportResult> reportResult)
|
||
{
|
||
NavisworksTransport.UI.WPF.Views.CollisionReportDialog.ShowReport(reportResult.Data);
|
||
}
|
||
else
|
||
{
|
||
LogManager.Warning($"[OpenHistoryReportAsync] 生成报告失败: {result.ErrorMessage}");
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogManager.Error($"[OpenHistoryReportAsync] 打开历史报告失败: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
#endregion
|
||
|
||
/// <summary>
|
||
/// 异步预计算碰撞排除列表
|
||
/// </summary>
|
||
/// <param name="animationObject">动画对象</param>
|
||
private async Task PrecomputeCollisionExclusionsAsync(ModelItem animationObject)
|
||
{
|
||
try
|
||
{
|
||
if (animationObject == null)
|
||
{
|
||
return;
|
||
}
|
||
|
||
LogManager.Info($"[缓存预计算] 开始为新选择的动画对象预计算: {animationObject.DisplayName}");
|
||
|
||
// 在UI线程中更新开始状态
|
||
await System.Windows.Application.Current.Dispatcher.BeginInvoke(new Action(() =>
|
||
{
|
||
UpdateMainStatus("正在分析动画对象...", -1, true);
|
||
}));
|
||
|
||
// 在UI线程中执行Navisworks API操作(线程安全)
|
||
bool success;
|
||
try
|
||
{
|
||
success = _pathAnimationManager.PrecomputeCollisionExclusions(animationObject);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogManager.Error($"[缓存预计算] 预计算失败: {ex.Message}", ex);
|
||
success = false;
|
||
}
|
||
|
||
// 异步更新UI状态
|
||
await System.Windows.Application.Current.Dispatcher.BeginInvoke(new Action(() =>
|
||
{
|
||
if (success)
|
||
{
|
||
var cacheStats = _pathAnimationManager.GetCacheStats();
|
||
UpdateMainStatus("动画对象分析完成");
|
||
LogManager.Info($"[缓存预计算] 预计算成功 - {cacheStats}");
|
||
}
|
||
else
|
||
{
|
||
UpdateMainStatus("动画对象分析失败,无法生成动画");
|
||
LogManager.Error("[缓存预计算] 预计算失败: 无法构建碰撞排除列表");
|
||
}
|
||
}));
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogManager.Error($"[缓存预计算] 异步预计算过程异常: {ex.Message}", ex);
|
||
await System.Windows.Application.Current.Dispatcher.BeginInvoke(new Action(() =>
|
||
{
|
||
UpdateMainStatus("动画对象分析异常");
|
||
}));
|
||
}
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region 手工碰撞对象辅助方法
|
||
|
||
private void OnManualCollisionTargetsChanged(object sender, NotifyCollectionChangedEventArgs e)
|
||
{
|
||
// 先更新序号
|
||
UpdateManualCollisionTargetIndices();
|
||
|
||
UpdateManualCollisionTargetSummary();
|
||
OnPropertyChanged(nameof(HasManualCollisionTargets));
|
||
OnPropertyChanged(nameof(IsManualTargetModeActive));
|
||
System.Windows.Input.CommandManager.InvalidateRequerySuggested();
|
||
RefreshManualTargetHighlights();
|
||
SyncManualCollisionTargetsToAnimationManager();
|
||
UpdateCanGenerateAnimation(); // 更新生成动画按钮状态
|
||
}
|
||
|
||
private void UpdateManualCollisionTargetSummary()
|
||
{
|
||
if (_manualCollisionTargets == null || _manualCollisionTargets.Count == 0)
|
||
{
|
||
ManualCollisionTargetSummary = "未指定碰撞检测对象";
|
||
return;
|
||
}
|
||
|
||
var syncPart = _manualTargetsLastSyncTime.HasValue
|
||
? $" • 最近同步 {_manualTargetsLastSyncTime.Value:HH:mm:ss}"
|
||
: string.Empty;
|
||
ManualCollisionTargetSummary = $"{_manualCollisionTargets.Count} 个对象{syncPart}";
|
||
}
|
||
|
||
/// <summary>
|
||
/// 更新手工碰撞对象的序号
|
||
/// </summary>
|
||
private void UpdateManualCollisionTargetIndices()
|
||
{
|
||
if (_manualCollisionTargets == null)
|
||
return;
|
||
|
||
for (int i = 0; i < _manualCollisionTargets.Count; i++)
|
||
{
|
||
_manualCollisionTargets[i].Index = i + 1; // 序号从1开始
|
||
}
|
||
}
|
||
|
||
private string BuildModelPath(ModelItem item)
|
||
{
|
||
try
|
||
{
|
||
var segments = new List<string>();
|
||
var current = item;
|
||
int guard = 0;
|
||
|
||
while (current != null && guard < 15)
|
||
{
|
||
var name = ModelItemAnalysisHelper.GetSafeDisplayName(current);
|
||
if (!string.IsNullOrWhiteSpace(name))
|
||
{
|
||
segments.Add(name.Trim());
|
||
}
|
||
current = current.Parent;
|
||
guard++;
|
||
}
|
||
|
||
segments.Reverse();
|
||
return string.Join(" / ", segments);
|
||
}
|
||
catch
|
||
{
|
||
return "路径获取失败";
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取运动物体的 PathId 信息
|
||
/// </summary>
|
||
private (int ModelIndex, string PathId) GetObjectPathIdInfo()
|
||
{
|
||
try
|
||
{
|
||
if (SelectedAnimatedObject == null)
|
||
{
|
||
return (0, null);
|
||
}
|
||
|
||
var pathId = Autodesk.Navisworks.Api.Application.ActiveDocument.Models.CreatePathId(SelectedAnimatedObject);
|
||
return (pathId.ModelIndex, pathId.PathId);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogManager.Warning($"[批处理] 无法获取运动对象的 PathId: {SelectedAnimatedObject.DisplayName}, 错误: {ex.Message}");
|
||
return (0, null);
|
||
}
|
||
}
|
||
|
||
private List<ModelItem> GetValidManualCollisionTargets(bool pruneInvalidEntries = false)
|
||
{
|
||
var validItems = new List<ModelItem>();
|
||
if (_manualCollisionTargets == null)
|
||
{
|
||
return validItems;
|
||
}
|
||
|
||
var invalidItems = new List<ManualCollisionTargetViewModel>();
|
||
foreach (var entry in _manualCollisionTargets)
|
||
{
|
||
if (entry?.ModelItem != null &&
|
||
ModelItemAnalysisHelper.IsModelItemValid(entry.ModelItem) &&
|
||
HasGeometryRecursive(entry.ModelItem))
|
||
{
|
||
validItems.Add(entry.ModelItem);
|
||
}
|
||
else if (pruneInvalidEntries && entry != null)
|
||
{
|
||
invalidItems.Add(entry);
|
||
}
|
||
}
|
||
|
||
if (pruneInvalidEntries && invalidItems.Count > 0)
|
||
{
|
||
foreach (var invalid in invalidItems)
|
||
{
|
||
_manualCollisionTargets.Remove(invalid);
|
||
}
|
||
|
||
if (invalidItems.Count > 0)
|
||
{
|
||
UpdateMainStatus("部分手工碰撞对象已失效,已自动清理");
|
||
}
|
||
}
|
||
|
||
return validItems;
|
||
}
|
||
|
||
private void SyncManualCollisionTargetsToAnimationManager(bool pruneInvalidEntries = false)
|
||
{
|
||
if (_pathAnimationManager == null)
|
||
return;
|
||
|
||
var targets = GetValidManualCollisionTargets(pruneInvalidEntries);
|
||
if (IsManualCollisionTargetEnabled && targets.Count > 0)
|
||
{
|
||
_pathAnimationManager.SetManualCollisionTargets(targets, true);
|
||
}
|
||
else
|
||
{
|
||
_pathAnimationManager.SetManualCollisionTargets(null, false);
|
||
}
|
||
}
|
||
|
||
private bool ManualTargetExists(ModelItem item)
|
||
{
|
||
if (item == null)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
if (_manualCollisionTargets == null || _manualCollisionTargets.Count == 0)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
foreach (var target in _manualCollisionTargets)
|
||
{
|
||
if (target == null)
|
||
continue;
|
||
|
||
// 使用正确的 ModelItem 比较方法
|
||
if (ModelItemAnalysisHelper.ModelItemEquals(target.ModelItem, item))
|
||
return true;
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
private void RefreshManualTargetHighlights(bool forceHighlight = false)
|
||
{
|
||
try
|
||
{
|
||
if (_clashIntegration == null)
|
||
return;
|
||
|
||
var validItems = GetValidManualCollisionTargets();
|
||
if (validItems.Count == 0)
|
||
{
|
||
ModelHighlightHelper.ClearCategory(ManualTargetsHighlightCategory);
|
||
return;
|
||
}
|
||
|
||
if (IsManualTargetModeActive || forceHighlight)
|
||
{
|
||
ModelHighlightHelper.HighlightItems(ManualTargetsHighlightCategory, validItems);
|
||
}
|
||
else
|
||
{
|
||
ModelHighlightHelper.ClearCategory(ManualTargetsHighlightCategory);
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogManager.Warning($"[手工碰撞] 更新高亮失败: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
private void HandleManualCollisionModeChanged()
|
||
{
|
||
RefreshManualTargetHighlights();
|
||
SyncManualCollisionTargetsToAnimationManager(true);
|
||
UpdateCanGenerateAnimation(); // 更新生成动画按钮状态
|
||
|
||
if (IsManualCollisionTargetEnabled && !HasManualCollisionTargets)
|
||
{
|
||
UpdateMainStatus("手工模式已启用,请先选择碰撞检测对象");
|
||
}
|
||
else if (!IsManualCollisionTargetEnabled)
|
||
{
|
||
UpdateMainStatus("已切换回默认碰撞检测对象范围");
|
||
}
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region 动画辅助方法
|
||
|
||
/// <summary>
|
||
/// 执行生成动画命令(同步执行,因为Navisworks API需要STA线程)
|
||
/// </summary>
|
||
private async void ExecuteGenerateAnimation()
|
||
{
|
||
// 保存当前光标
|
||
var oldCursor = Mouse.OverrideCursor;
|
||
|
||
try
|
||
{
|
||
if (!CanGenerateAnimation)
|
||
{
|
||
UpdateMainStatus("无法生成动画:缺少必要条件");
|
||
LogManager.Warning("尝试生成动画但条件不满足");
|
||
return;
|
||
}
|
||
|
||
// 设置等待光标
|
||
Mouse.OverrideCursor = Cursors.Wait;
|
||
|
||
UpdateMainStatus("正在生成动画...", -1, true);
|
||
LogManager.Info($"开始生成动画 - 模式: {(UseVirtualObject ? "虚拟物体" : "选择物体")}");
|
||
|
||
var cacheStartTime = DateTime.Now;
|
||
|
||
var manualModeEnabled = IsManualCollisionTargetEnabled;
|
||
List<ModelItem> manualTargets = null;
|
||
|
||
if (manualModeEnabled)
|
||
{
|
||
manualTargets = GetValidManualCollisionTargets(pruneInvalidEntries: true);
|
||
if (manualTargets.Count == 0)
|
||
{
|
||
UpdateMainStatus("手工模式已启用,但没有可用的碰撞对象");
|
||
LogManager.Warning("[动画生成] 手工模式启用但没有有效对象");
|
||
return;
|
||
}
|
||
}
|
||
|
||
// 🔥 预计算动画对象的排除列表(仅在非虚拟物体、非手工模式下)
|
||
if (!manualModeEnabled && !UseVirtualObject)
|
||
{
|
||
UpdateMainStatus("正在分析动画对象...", -1, true);
|
||
|
||
// 设置检测容差(安全间隙通过CreateAnimation传入)
|
||
_pathAnimationManager.SetDetectionTolerance(_detectionTolerance);
|
||
|
||
var success = _pathAnimationManager.PrecomputeCollisionExclusions(SelectedAnimatedObject);
|
||
|
||
if (!success)
|
||
{
|
||
var msg = "[动画生成] 动画对象分析失败,无法继续生成动画";
|
||
LogManager.Error(msg);
|
||
UpdateMainStatus("生成失败:对象分析错误");
|
||
return;
|
||
}
|
||
}
|
||
else if (manualModeEnabled)
|
||
{
|
||
UpdateMainStatus("使用手工碰撞对象,跳过预计算", -1, true);
|
||
LogManager.Info("[动画生成] 手工碰撞对象模式,已跳过对象预计算");
|
||
}
|
||
|
||
UpdateMainStatus("正在生成路径动画...", -1, true);
|
||
|
||
// 设置动画参数到PathAnimationManager
|
||
_pathAnimationManager.SetAnimationFrameRate(_animationFrameRate);
|
||
_pathAnimationManager.SetCollisionDetectionAccuracy(CollisionDetectionAccuracy);
|
||
_pathAnimationManager.SetMovementSpeed(MovementSpeed);
|
||
_pathAnimationManager.SetDetectionTolerance(_detectionTolerance);
|
||
|
||
// 将PathRouteViewModel的点转换为Point3D列表
|
||
var pathPoints = CurrentPathRoute.Points.Select(p => new Point3D(p.X, p.Y, p.Z)).ToList();
|
||
|
||
ModelItem animatedObject = null;
|
||
|
||
if (UseVirtualObject)
|
||
{
|
||
// 使用虚拟物体:总是调用 ShowVirtualObject 来更新尺寸
|
||
LogManager.Info("[ExecuteGenerateAnimation] 显示并更新虚拟物体...");
|
||
UpdateMainStatus("正在显示虚拟物体...", -1, true);
|
||
|
||
VirtualObjectManager.Instance.ShowVirtualObject(
|
||
VirtualObjectLengthInMeters,
|
||
VirtualObjectWidthInMeters,
|
||
VirtualObjectHeightInMeters
|
||
);
|
||
animatedObject = VirtualObjectManager.Instance.CurrentVirtualObject;
|
||
|
||
if (animatedObject == null)
|
||
{
|
||
LogManager.Error("[动画生成] 虚拟物体显示失败");
|
||
UpdateMainStatus("生成失败:无法显示虚拟物体");
|
||
return;
|
||
}
|
||
|
||
// 注意:虚拟物体在选择时已经通过 MoveAnimatedObjectToPathStart 移动到路径起点
|
||
// 这里不需要重复移动,因为 CreateAnimation 内部还会再次调用 MoveObjectToPathStart
|
||
LogManager.Info($"使用虚拟物体: {animatedObject.DisplayName}");
|
||
}
|
||
else
|
||
{
|
||
animatedObject = SelectedAnimatedObject;
|
||
}
|
||
|
||
// 统一准备碰撞检测(根据模式自动决定是否构建全局缓存)
|
||
ClashDetectiveIntegration.PrepareCollisionDetection(animatedObject, manualModeEnabled, manualTargets);
|
||
|
||
// 设置碰撞检测目标
|
||
if (manualModeEnabled)
|
||
{
|
||
_pathAnimationManager.SetManualCollisionTargets(manualTargets, true);
|
||
}
|
||
else
|
||
{
|
||
_pathAnimationManager.SetManualCollisionTargets(null, false);
|
||
}
|
||
|
||
// 【统一使用CreateAnimation,完全复用现有逻辑】
|
||
LogManager.Info($"[ExecuteGenerateAnimation] 准备创建动画: 路径名称='{CurrentPathRoute.Name}', ID='{CurrentPathRoute.Id}', " +
|
||
$"动画对象='{animatedObject.DisplayName}'");
|
||
|
||
// 直接使用 CurrentPathRoute.Route
|
||
var pathRoute = CurrentPathRoute.Route;
|
||
|
||
if (pathRoute == null)
|
||
{
|
||
LogManager.Error($"无法找到路径对象,ID: {CurrentPathRoute.Id}");
|
||
UpdateMainStatus("生成失败:找不到路径数据");
|
||
return;
|
||
}
|
||
|
||
// 先设置路径到动画管理器
|
||
_pathAnimationManager.SetRoute(pathRoute);
|
||
|
||
// 准备物体尺寸参数(从米转换为模型单位)
|
||
var metersToModelUnits = Utils.UnitsConverter.GetMetersToUnitsConversionFactor();
|
||
double vLength = UseVirtualObject ? VirtualObjectLengthInMeters * metersToModelUnits : 0;
|
||
double vWidth = UseVirtualObject ? VirtualObjectWidthInMeters * metersToModelUnits : 0;
|
||
double vHeight = UseVirtualObject ? VirtualObjectHeightInMeters * metersToModelUnits : 0;
|
||
double safetyMarginModelUnits = _safetyMarginInMeters * metersToModelUnits;
|
||
|
||
LogManager.Info($"[ExecuteGenerateAnimation] 准备调用CreateAnimation: UseVirtualObject={UseVirtualObject}, 物体尺寸: {vLength:F2}×{vWidth:F2}×{vHeight:F2}模型单位, 安全间隙: {safetyMarginModelUnits:F4}模型单位 (_safetyMarginInMeters={_safetyMarginInMeters:F4}米)");
|
||
|
||
_pathAnimationManager.CreateAnimation(animatedObject, pathPoints, AnimationDuration, CurrentPathRoute.Name, CurrentPathRoute.Id, UseVirtualObject,
|
||
vLength, vWidth, vHeight, safetyMarginModelUnits);
|
||
|
||
var totalElapsed = (DateTime.Now - cacheStartTime).TotalMilliseconds;
|
||
LogManager.Info($"[动画生成] 动画生成完成,总耗时: {totalElapsed:F1}ms");
|
||
LogManager.Info($"动画生成成功: 物体={animatedObject.DisplayName}, 路径={CurrentPathRoute.Name}");
|
||
|
||
if (UseVirtualObject)
|
||
{
|
||
LogManager.Info($"虚拟物体尺寸: {VirtualObjectLengthInMeters:F1}×{VirtualObjectWidthInMeters:F1}×{VirtualObjectHeightInMeters:F1}m");
|
||
}
|
||
|
||
if (IsManualCollisionTargetEnabled)
|
||
{
|
||
RefreshManualTargetHighlights(forceHighlight: true);
|
||
}
|
||
|
||
// 动画生成完成,先恢复光标再显示分析对话框
|
||
Mouse.OverrideCursor = oldCursor;
|
||
oldCursor = null; // 标记已恢复,避免 finally 再次恢复
|
||
|
||
// 🔥 自动生成动画后,自动高亮预计算碰撞结果并显示分析对话框
|
||
await AnalyzeAndHighlightPrecomputedCollisionsAsync();
|
||
|
||
// 更新状态
|
||
UpdateAnimationButtonStates();
|
||
UpdateMainStatus("动画生成成功");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
UpdateMainStatus("动画生成失败");
|
||
LogManager.Error($"生成动画时发生错误: {ex.Message}");
|
||
|
||
LogManager.Error($"动画生成失败,详细错误信息:{ex}");
|
||
}
|
||
finally
|
||
{
|
||
// 恢复光标(如果还没有恢复)
|
||
if (oldCursor != null)
|
||
{
|
||
Mouse.OverrideCursor = oldCursor;
|
||
}
|
||
}
|
||
}
|
||
|
||
|
||
/// <summary>
|
||
/// 更新移动物体信息
|
||
/// </summary>
|
||
private void UpdateAnimatedObjectInfo()
|
||
{
|
||
if (SelectedAnimatedObject != null)
|
||
{
|
||
SelectedAnimatedObjectName = SelectedAnimatedObject.DisplayName ?? "未命名物体";
|
||
HasSelectedAnimatedObject = true;
|
||
}
|
||
else
|
||
{
|
||
SelectedAnimatedObjectName = "未选择移动物体";
|
||
HasSelectedAnimatedObject = false;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 计算本地三轴预旋转后的有效尺寸
|
||
/// 返回值为模型单位(与Navisworks API一致)
|
||
/// </summary>
|
||
/// <returns>(沿路径, 垂直路径, 法线) - 模型单位</returns>
|
||
private (double effectiveAlongPath, double effectiveAcrossPath, double effectiveNormalToPath) CalculateRotatedDimensions()
|
||
{
|
||
double forwardSize;
|
||
double sideSize;
|
||
double upSize;
|
||
ModelAxisConvention axisConvention;
|
||
|
||
if (UseVirtualObject)
|
||
{
|
||
double metersToUnits = UnitsConverter.GetMetersToUnitsConversionFactor();
|
||
forwardSize = VirtualObjectLengthInMeters * metersToUnits;
|
||
sideSize = VirtualObjectWidthInMeters * metersToUnits;
|
||
upSize = VirtualObjectHeightInMeters * metersToUnits;
|
||
axisConvention = ModelAxisConvention.CreateVirtualObjectAssetConvention();
|
||
}
|
||
else if (SelectedAnimatedObject != null)
|
||
{
|
||
double metersToUnits = UnitsConverter.GetMetersToUnitsConversionFactor();
|
||
forwardSize = _objectOriginalLength * metersToUnits;
|
||
sideSize = _objectOriginalWidth * metersToUnits;
|
||
upSize = _objectOriginalHeight * metersToUnits;
|
||
|
||
var adapter = CoordinateSystemManager.Instance.CreateHostAdapter();
|
||
if ((CurrentPathRoute?.PathType == NavisworksTransport.PathType.Ground ||
|
||
CurrentPathRoute?.PathType == NavisworksTransport.PathType.Hoisting) &&
|
||
_pathAnimationManager != null &&
|
||
_pathAnimationManager.TryGetCurrentRouteRealObjectPlanarProjectedExtents(
|
||
out double resolvedPlanarForward,
|
||
out double resolvedPlanarSide,
|
||
out double resolvedPlanarUp))
|
||
{
|
||
double unitsToMetersForPlanar = UnitsConverter.GetUnitsToMetersConversionFactor();
|
||
LogManager.Debug(
|
||
$"[角度修正] 平面真实物体复用最终姿态尺寸: Forward={resolvedPlanarForward * unitsToMetersForPlanar:F2}m, " +
|
||
$"Side={resolvedPlanarSide * unitsToMetersForPlanar:F2}m, Up={resolvedPlanarUp * unitsToMetersForPlanar:F2}m, 角度={_objectRotationCorrection}");
|
||
return (resolvedPlanarForward, resolvedPlanarSide, resolvedPlanarUp);
|
||
}
|
||
if (CurrentPathRoute?.PathType == NavisworksTransport.PathType.Rail &&
|
||
_pathAnimationManager != null &&
|
||
_pathAnimationManager.TryGetCurrentRouteRealObjectRailProjectedExtents(
|
||
out double resolvedForward,
|
||
out double resolvedSide,
|
||
out double resolvedUp))
|
||
{
|
||
double unitsToMetersForRail = UnitsConverter.GetUnitsToMetersConversionFactor();
|
||
LogManager.Debug(
|
||
$"[角度修正] Rail真实物体复用路径姿态尺寸: Forward={resolvedForward * unitsToMetersForRail:F2}m, " +
|
||
$"Side={resolvedSide * unitsToMetersForRail:F2}m, Up={resolvedUp * unitsToMetersForRail:F2}m, 角度={_objectRotationCorrection}");
|
||
return (resolvedForward, resolvedSide, resolvedUp);
|
||
}
|
||
|
||
axisConvention = CurrentPathRoute?.PathType == NavisworksTransport.PathType.Rail
|
||
? ModelAxisConvention.CreateRailAssetConvention()
|
||
: ModelAxisConvention.CreateDefaultForHost(adapter.HostType);
|
||
}
|
||
else
|
||
{
|
||
return (0, 0, 0);
|
||
}
|
||
|
||
var hostAdapter = CoordinateSystemManager.Instance.CreateHostAdapter();
|
||
Quaternion correctionQuaternion = UseVirtualObject
|
||
? hostAdapter.CreateCanonicalRotationCorrection(_objectRotationCorrection)
|
||
: hostAdapter.CreateHostRotationCorrection(_objectRotationCorrection);
|
||
var result = RotatedObjectExtentHelper.CalculateProjectedSemanticExtents(
|
||
axisConvention,
|
||
forwardSize,
|
||
sideSize,
|
||
upSize,
|
||
correctionQuaternion);
|
||
double unitsToMeters = UnitsConverter.GetUnitsToMetersConversionFactor();
|
||
LogManager.Debug(
|
||
$"[角度修正] 旋转后尺寸: 原始({forwardSize * unitsToMeters:F2}m×{sideSize * unitsToMeters:F2}m×{upSize * unitsToMeters:F2}m) -> " +
|
||
$"有效({result.forwardExtent * unitsToMeters:F2}m×{result.sideExtent * unitsToMeters:F2}m×{result.upExtent * unitsToMeters:F2}m), 角度={_objectRotationCorrection}");
|
||
|
||
return result;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 根据路径类型更新通行空间可视化
|
||
/// 空轨和吊装路径默认打开通行空间(物体通行空间模式),地面路径不打开
|
||
/// 根据选择的物体或虚拟物体尺寸更新通行空间渲染
|
||
/// </summary>
|
||
public void UpdatePassageSpaceVisualization()
|
||
{
|
||
try
|
||
{
|
||
if (CurrentPathRoute == null)
|
||
{
|
||
LogManager.Debug("[通行空间可视化] 没有选择路径,跳过更新");
|
||
return;
|
||
}
|
||
|
||
// 计算旋转后的有效尺寸(考虑角度修正)
|
||
(double effectiveLength, double effectiveWidth, double effectiveHeight) = CalculateRotatedDimensions();
|
||
|
||
var renderPlugin = PathPointRenderPlugin.Instance;
|
||
if (renderPlugin == null)
|
||
{
|
||
throw new InvalidOperationException("[通行空间可视化] 无法获取PathPointRenderPlugin实例");
|
||
}
|
||
|
||
// 计算通行空间尺寸(模型单位)
|
||
double passageAcrossPath, passageNormalToPath, passageAlongPath;
|
||
double passageNormalToPathVertical, passageNormalToPathHorizontal;
|
||
|
||
// 将安全间隙从米转换为模型单位
|
||
var metersToUnitsPassage = UnitsConverter.GetMetersToUnitsConversionFactor();
|
||
double safetyMargin = _safetyMarginInMeters * metersToUnitsPassage;
|
||
if (UseVirtualObject)
|
||
{
|
||
// 虚拟物体的xyz定义:X方向 = 长度(前进方向),Y方向 = 宽度(侧面),Z方向 = 高度
|
||
if (CurrentPathRoute.PathType == NavisworksTransport.PathType.Rail || CurrentPathRoute.PathType == NavisworksTransport.PathType.Hoisting)
|
||
{
|
||
// 空中路径(空轨或吊装):高度上下都加间隙
|
||
passageAcrossPath = effectiveWidth + 2 * safetyMargin; // 旋转后宽度 + 2*间隙(垂直于路径方向)
|
||
passageNormalToPath = effectiveHeight + 2 * safetyMargin; // 法线方向高度 + 2*间隙
|
||
passageAlongPath = effectiveLength; // 旋转后长度(沿路径方向)
|
||
// 吊装路径分段参数
|
||
passageNormalToPathVertical = effectiveLength + 2 * safetyMargin; // 垂直段:物体长度 + 2*间隙
|
||
passageNormalToPathHorizontal = effectiveHeight + 2 * safetyMargin; // 水平段:物体高度 + 2*间隙
|
||
LogManager.Debug($"[通行空间可视化] 空中路径使用虚拟物体尺寸: 有效长度={effectiveLength / metersToUnitsPassage:F2}m, 有效宽度={effectiveWidth / metersToUnitsPassage:F2}m, 通行空间沿路径={passageAlongPath / metersToUnitsPassage:F2}m, 垂直路径={passageAcrossPath / metersToUnitsPassage:F2}m, 法线={passageNormalToPath / metersToUnitsPassage:F2}m");
|
||
}
|
||
else if (CurrentPathRoute.PathType == NavisworksTransport.PathType.Ground)
|
||
{
|
||
// 地面路径:物体的长度方向(X轴,前进方向)朝向路径方向
|
||
// 通行空间沿路径方向 = X方向尺寸(长度),垂直于路径方向 = Y方向尺寸(宽度),高度上方加间隙(下方不需要,因为有地面)
|
||
passageAcrossPath = effectiveWidth + 2 * safetyMargin; // 旋转后宽度 + 2*间隙(垂直于路径方向)
|
||
passageNormalToPath = effectiveHeight + safetyMargin; // 法线方向高度 + 间隙(法线方向,只有上方)
|
||
passageAlongPath = effectiveLength; // 旋转后长度(沿路径方向)
|
||
// 地面路径不需要分段参数
|
||
passageNormalToPathVertical = passageNormalToPath;
|
||
passageNormalToPathHorizontal = passageNormalToPath;
|
||
LogManager.Debug($"[通行空间可视化] 地面路径使用虚拟物体尺寸: 有效长度={effectiveLength / metersToUnitsPassage:F2}m, 有效宽度={effectiveWidth / metersToUnitsPassage:F2}m, 通行空间沿路径={passageAlongPath / metersToUnitsPassage:F2}m, 垂直路径={passageAcrossPath / metersToUnitsPassage:F2}m, 法线={passageNormalToPath / metersToUnitsPassage:F2}m");
|
||
}
|
||
else // Rail
|
||
{
|
||
// 空轨路径:物体的长度方向(X轴,前进方向)朝向路径方向
|
||
// 通行空间沿路径方向 = X方向尺寸(长度),垂直于路径方向 = Y方向尺寸(宽度)
|
||
// 法线方向只在远离轨道的一侧留间隙:
|
||
// - 轨下安装:顶面贴路径,底面留间隙
|
||
// - 轨上安装:底面贴路径,顶面留间隙
|
||
passageAcrossPath = effectiveWidth + 2 * safetyMargin; // 旋转后宽度 + 2*间隙(垂直于路径方向)
|
||
passageNormalToPath = effectiveHeight + safetyMargin; // 法线方向单侧留间隙
|
||
passageAlongPath = effectiveLength; // 旋转后长度(沿路径方向)
|
||
// 空轨路径不需要分段参数
|
||
passageNormalToPathVertical = passageNormalToPath;
|
||
passageNormalToPathHorizontal = passageNormalToPath;
|
||
LogManager.Debug($"[通行空间可视化] 空轨路径使用虚拟物体尺寸: 有效长度={effectiveLength / metersToUnitsPassage:F2}m, 有效宽度={effectiveWidth / metersToUnitsPassage:F2}m, 通行空间沿路径={passageAlongPath / metersToUnitsPassage:F2}m, 垂直路径={passageAcrossPath / metersToUnitsPassage:F2}m, 法线={passageNormalToPath / metersToUnitsPassage:F2}m");
|
||
}
|
||
}
|
||
else if (SelectedAnimatedObject != null)
|
||
{
|
||
// 根据路径类型确定通行空间的尺寸
|
||
if (CurrentPathRoute.PathType == NavisworksTransport.PathType.Hoisting)
|
||
{
|
||
// 吊装路径:
|
||
// 高度上下都加间隙
|
||
passageAcrossPath = effectiveWidth + 2 * safetyMargin; // 旋转后宽度 + 2*间隙(垂直于路径方向)
|
||
passageNormalToPath = effectiveHeight + 2 * safetyMargin; // 局部up方向高度 + 2*间隙(法线方向)
|
||
passageAlongPath = effectiveLength; // 旋转后长度(沿路径方向)
|
||
// 吊装路径分段参数
|
||
passageNormalToPathVertical = effectiveLength + 2 * safetyMargin; // 垂直段:物体长度 + 2*间隙
|
||
passageNormalToPathHorizontal = effectiveHeight + 2 * safetyMargin; // 水平段:物体高度 + 2*间隙
|
||
LogManager.Debug($"[通行空间可视化] 空中路径使用物体尺寸 ({SelectedAnimatedObject.DisplayName}): 有效长度={effectiveLength / metersToUnitsPassage:F2}m, 有效宽度={effectiveWidth / metersToUnitsPassage:F2}m, 有效高度={effectiveHeight / metersToUnitsPassage:F2}m, 通行空间沿路径={passageAlongPath / metersToUnitsPassage:F2}m, 垂直路径={passageAcrossPath / metersToUnitsPassage:F2}m, 法线={passageNormalToPath / metersToUnitsPassage:F2}m");
|
||
}
|
||
else if (CurrentPathRoute.PathType == NavisworksTransport.PathType.Ground)
|
||
{
|
||
// 地面路径(可能有坡度):
|
||
// 物体的长度方向(X轴,前进方向)朝向路径方向
|
||
// 通行空间沿路径方向 = X方向尺寸(长度),垂直于路径方向 = Y方向尺寸(宽度),高度上方加间隙(下方不需要,因为有地面)
|
||
passageAcrossPath = effectiveWidth + 2 * safetyMargin; // 旋转后宽度 + 2*间隙(垂直于路径方向)
|
||
passageNormalToPath = effectiveHeight + safetyMargin; // 局部up方向高度 + 间隙(法线方向,只有上方)
|
||
passageAlongPath = effectiveLength; // 旋转后长度(沿路径方向)
|
||
// 地面路径不需要分段参数
|
||
passageNormalToPathVertical = passageNormalToPath;
|
||
passageNormalToPathHorizontal = passageNormalToPath;
|
||
LogManager.Debug($"[通行空间可视化] 地面路径使用物体尺寸 ({SelectedAnimatedObject.DisplayName}): 有效长度={effectiveLength / metersToUnitsPassage:F2}m, 有效宽度={effectiveWidth / metersToUnitsPassage:F2}m, 有效高度={effectiveHeight / metersToUnitsPassage:F2}m, 通行空间沿路径={passageAlongPath / metersToUnitsPassage:F2}m, 垂直路径={passageAcrossPath / metersToUnitsPassage:F2}m, 法线={passageNormalToPath / metersToUnitsPassage:F2}m");
|
||
}
|
||
else if (CurrentPathRoute.PathType == NavisworksTransport.PathType.Rail)
|
||
{
|
||
// 空轨路径(可能有坡度):
|
||
// 物体的长度方向(X轴,前进方向)朝向路径方向
|
||
// 通行空间沿路径方向 = X方向尺寸(长度),垂直于路径方向 = Y方向尺寸(宽度)
|
||
// 法线方向只在远离轨道的一侧留间隙:
|
||
// - 轨下安装:顶面贴路径,底面留间隙
|
||
// - 轨上安装:底面贴路径,顶面留间隙
|
||
passageAcrossPath = effectiveWidth + 2 * safetyMargin; // 旋转后宽度 + 2*间隙(垂直于路径方向)
|
||
passageNormalToPath = effectiveHeight + safetyMargin; // 局部up方向高度 + 单侧间隙(法线方向)
|
||
passageAlongPath = effectiveLength; // 旋转后长度(沿路径方向)
|
||
passageNormalToPathVertical = passageNormalToPath;
|
||
passageNormalToPathHorizontal = passageNormalToPath;
|
||
LogManager.Debug($"[通行空间可视化] 空轨路径使用物体尺寸 ({SelectedAnimatedObject.DisplayName}): 有效长度={effectiveLength / metersToUnitsPassage:F2}m, 有效宽度={effectiveWidth / metersToUnitsPassage:F2}m, 有效高度={effectiveHeight / metersToUnitsPassage:F2}m, 通行空间沿路径={passageAlongPath / metersToUnitsPassage:F2}m, 垂直路径={passageAcrossPath / metersToUnitsPassage:F2}m, 法线={passageNormalToPath / metersToUnitsPassage:F2}m");
|
||
}
|
||
else
|
||
{
|
||
passageAcrossPath = effectiveWidth + 2 * safetyMargin;
|
||
passageNormalToPath = effectiveHeight + safetyMargin;
|
||
passageAlongPath = effectiveLength;
|
||
passageNormalToPathVertical = passageNormalToPath;
|
||
passageNormalToPathHorizontal = passageNormalToPath;
|
||
}
|
||
}
|
||
else
|
||
{
|
||
// 默认就是使用虚拟物体,如果既不使用虚拟物体也没选择物体,说明配置错误
|
||
throw new InvalidOperationException("[通行空间可视化] 配置错误:既不使用虚拟物体也没有选择物体");
|
||
}
|
||
|
||
// 设置通行空间参数到渲染插件(传递模型单位)
|
||
renderPlugin.SetPassageSpaceParameters(
|
||
passageAcrossPath,
|
||
passageNormalToPath,
|
||
passageAlongPath,
|
||
passageNormalToPathVertical,
|
||
passageNormalToPathHorizontal);
|
||
LogManager.Debug($"[通行空间可视化] 已设置通行空间参数: 沿路径={passageAlongPath / metersToUnitsPassage:F2}m, 垂直路径={passageAcrossPath / metersToUnitsPassage:F2}m, 法线={passageNormalToPath / metersToUnitsPassage:F2}m, 安全间隙={_safetyMarginInMeters:F2}m");
|
||
|
||
// 根据路径类型决定是否切换到物体通行空间模式
|
||
bool enableObjectSpace = false;
|
||
switch (CurrentPathRoute.PathType)
|
||
{
|
||
case NavisworksTransport.PathType.Rail:
|
||
case NavisworksTransport.PathType.Hoisting:
|
||
enableObjectSpace = true;
|
||
LogManager.Debug($"[通行空间可视化] 路径类型={CurrentPathRoute.PathType}(空中路径),切换到物体通行空间模式");
|
||
break;
|
||
case NavisworksTransport.PathType.Ground:
|
||
enableObjectSpace = false;
|
||
LogManager.Debug($"[通行空间可视化] 路径类型={CurrentPathRoute.PathType},不切换到物体通行空间模式");
|
||
break;
|
||
default:
|
||
LogManager.Warning($"[通行空间可视化] 未知的路径类型: {CurrentPathRoute.PathType}");
|
||
return;
|
||
}
|
||
|
||
// 切换到物体通行空间模式(如果需要)
|
||
if (enableObjectSpace)
|
||
{
|
||
renderPlugin.VisualizationMode = PathVisualizationMode.ObjectSpace;
|
||
LogManager.Debug("[通行空间可视化] 已切换到物体通行空间模式");
|
||
}
|
||
|
||
// 刷新路径可视化以应用新的尺寸
|
||
var coreRoute = CurrentPathRoute.Route;
|
||
if (coreRoute != null)
|
||
{
|
||
PathPlanningManager.Instance.DrawRouteVisualization(coreRoute, isAutoPath: false);
|
||
LogManager.Debug("[通行空间可视化] 已刷新路径可视化");
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogManager.Error($"[通行空间可视化] 更新失败: {ex.Message}", ex);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 递归检查ModelItem是否包含几何体
|
||
/// </summary>
|
||
private bool HasGeometryRecursive(ModelItem item)
|
||
{
|
||
if (item == null) return false;
|
||
|
||
// 如果当前项有几何体,直接返回true
|
||
if (item.HasGeometry) return true;
|
||
|
||
// 递归检查子项
|
||
foreach (ModelItem child in item.Children)
|
||
{
|
||
if (HasGeometryRecursive(child))
|
||
return true;
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 更新是否可以生成动画的状态
|
||
/// </summary>
|
||
private void UpdateCanGenerateAnimation()
|
||
{
|
||
var hasPath = CurrentPathRoute != null && CurrentPathRoute.Points.Count >= 2;
|
||
|
||
bool hasValidObject;
|
||
if (UseVirtualObject)
|
||
{
|
||
// 使用虚拟物体时,只需要有效的物体尺寸
|
||
hasValidObject = VirtualObjectLengthInMeters > 0 &&
|
||
VirtualObjectWidthInMeters > 0 &&
|
||
VirtualObjectHeightInMeters > 0;
|
||
}
|
||
else
|
||
{
|
||
// 使用选择物体时,需要已选择物体
|
||
hasValidObject = SelectedAnimatedObject != null;
|
||
}
|
||
|
||
// 启用手工指定模式时,必须指定碰撞检测对象
|
||
bool hasManualTargetsIfRequired = !IsManualCollisionTargetEnabled || HasManualCollisionTargets;
|
||
|
||
CanGenerateAnimation = hasValidObject && hasPath && hasManualTargetsIfRequired;
|
||
|
||
// 更新状态提示
|
||
if (IsManualCollisionTargetEnabled && !HasManualCollisionTargets)
|
||
{
|
||
UpdateMainStatus("手工模式已启用,请先选择碰撞检测对象");
|
||
}
|
||
else if (!hasPath && !hasValidObject)
|
||
{
|
||
UpdateMainStatus(UseVirtualObject ? "请选择动画路径" : "请选择移动物体和动画路径");
|
||
}
|
||
else if (!hasValidObject)
|
||
{
|
||
UpdateMainStatus(UseVirtualObject ? "虚拟物体参数无效" : "请选择移动物体");
|
||
}
|
||
else if (!hasPath)
|
||
{
|
||
UpdateMainStatus("请选择有效的动画路径");
|
||
}
|
||
else
|
||
{
|
||
UpdateMainStatus("可以生成动画");
|
||
}
|
||
|
||
// 同时更新动画按钮状态,因为对象或路径的变化会影响"开始动画"按钮的可用性
|
||
UpdateAnimationButtonStates();
|
||
}
|
||
|
||
|
||
/// <summary>
|
||
/// 计算当前路径的总长度
|
||
/// </summary>
|
||
private double GetPathLength()
|
||
{
|
||
if (CurrentPathRoute == null || CurrentPathRoute.Points.Count < 2)
|
||
{
|
||
return 0.0;
|
||
}
|
||
|
||
double totalLength = 0.0;
|
||
var points = CurrentPathRoute.Points;
|
||
|
||
for (int i = 0; i < points.Count - 1; i++)
|
||
{
|
||
var p1 = points[i];
|
||
var p2 = points[i + 1];
|
||
|
||
// 计算两点之间的欧几里得距离(模型单位)
|
||
double dx = p2.X - p1.X;
|
||
double dy = p2.Y - p1.Y;
|
||
double dz = p2.Z - p1.Z;
|
||
|
||
double segmentLengthInModelUnits = Math.Sqrt(dx * dx + dy * dy + dz * dz);
|
||
|
||
// 转换为米单位
|
||
double segmentLengthInMeters = NavisworksTransport.Utils.UnitsConverter.ConvertToMeters(segmentLengthInModelUnits);
|
||
totalLength += segmentLengthInMeters;
|
||
}
|
||
|
||
return totalLength;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 统一更新动画按钮状态逻辑
|
||
/// 根据当前的路径、动画对象和动画管理器状态来决定按钮的可用性
|
||
/// </summary>
|
||
private void UpdateAnimationButtonStates()
|
||
{
|
||
try
|
||
{
|
||
// 检查是否有有效的动画可以播放
|
||
var hasValidPath = CurrentPathRoute != null && CurrentPathRoute.Points.Count >= 2;
|
||
var hasValidAnimatedObject = SelectedAnimatedObject != null;
|
||
var hasValidAnimation = hasValidPath && hasValidAnimatedObject;
|
||
|
||
// 检查动画管理器当前状态
|
||
var animationState = _pathAnimationManager?.CurrentState ?? NavisworksTransport.Core.Animation.AnimationState.Stopped;
|
||
var isAnimating = _pathAnimationManager?.IsAnimating ?? false;
|
||
|
||
// 检查动画是否已经生成(Ready状态表示动画已生成但未播放)
|
||
var isAnimationReady = animationState == NavisworksTransport.Core.Animation.AnimationState.Ready ||
|
||
animationState == NavisworksTransport.Core.Animation.AnimationState.Finished;
|
||
|
||
// 根据动画状态和条件更新按钮状态
|
||
switch (animationState)
|
||
{
|
||
case NavisworksTransport.Core.Animation.AnimationState.Playing:
|
||
// 播放中:开始按钮禁用,暂停和停止按钮可用
|
||
CanStartAnimation = false;
|
||
CanPauseAnimation = true;
|
||
CanStopAnimation = true;
|
||
UpdateMainStatus("动画播放中");
|
||
break;
|
||
|
||
case NavisworksTransport.Core.Animation.AnimationState.Paused:
|
||
// 暂停中:开始按钮可用(显示为继续),暂停按钮禁用,停止按钮可用
|
||
CanStartAnimation = hasValidAnimation && isAnimationReady;
|
||
CanPauseAnimation = false;
|
||
CanStopAnimation = true;
|
||
UpdateMainStatus("动画已暂停");
|
||
break;
|
||
|
||
default:
|
||
// 停止或其他状态:需要同时满足有效动画条件和动画已生成条件
|
||
CanStartAnimation = hasValidAnimation && isAnimationReady;
|
||
CanPauseAnimation = false;
|
||
CanStopAnimation = false;
|
||
|
||
// 更新状态文本
|
||
if (IsManualCollisionTargetEnabled && !HasManualCollisionTargets)
|
||
{
|
||
UpdateMainStatus("手工模式已启用,请先选择碰撞检测对象");
|
||
}
|
||
else if (!hasValidPath && !hasValidAnimatedObject)
|
||
{
|
||
UpdateMainStatus("请选择路径和移动物体");
|
||
}
|
||
else if (!hasValidPath)
|
||
{
|
||
UpdateMainStatus("请选择有效路径(至少2个点)");
|
||
}
|
||
else if (!hasValidAnimatedObject)
|
||
{
|
||
UpdateMainStatus("请选择移动物体");
|
||
}
|
||
else if (!isAnimationReady)
|
||
{
|
||
UpdateMainStatus("请点击'生成动画'");
|
||
}
|
||
else
|
||
{
|
||
UpdateMainStatus("动画就绪");
|
||
}
|
||
break;
|
||
}
|
||
|
||
LogManager.Debug($"按钮状态已更新: CanStart={CanStartAnimation}, CanPause={CanPauseAnimation}, CanStop={CanStopAnimation}");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogManager.Error($"更新动画按钮状态时发生错误: {ex.Message}", ex);
|
||
}
|
||
}
|
||
|
||
|
||
/// <summary>
|
||
/// 生成碰撞检测报告数据
|
||
/// </summary>
|
||
private CollisionReportData GenerateCollisionReport()
|
||
{
|
||
try
|
||
{
|
||
LogManager.Info("开始从Clash Detective获取碰撞检测结果");
|
||
|
||
var doc = Autodesk.Navisworks.Api.Application.ActiveDocument;
|
||
var documentClash = doc.GetClash();
|
||
|
||
if (documentClash == null)
|
||
{
|
||
LogManager.Warning("无法获取Clash Detective文档");
|
||
return new CollisionReportData { IsValid = false, ErrorMessage = "Clash Detective不可用" };
|
||
}
|
||
|
||
var reportData = new CollisionReportData
|
||
{
|
||
IsValid = true,
|
||
GeneratedTime = DateTime.Now,
|
||
AnimatedObjectName = SelectedAnimatedObject?.DisplayName ?? "未知对象",
|
||
PathName = CurrentPathRoute?.Name ?? "未知路径"
|
||
};
|
||
|
||
// 获取所有动画相关的碰撞测试(包括新的分组测试)
|
||
var animationTests = documentClash.TestsData.Tests.Cast<ClashTest>()
|
||
.Where(t => t.DisplayName.Contains("碰撞检测") )
|
||
.ToList();
|
||
|
||
LogManager.Info($"找到 {animationTests.Count} 个碰撞检测");
|
||
|
||
foreach (var test in animationTests)
|
||
{
|
||
// 递归统计测试中的碰撞总数(包括分组内的)
|
||
var totalCollisionCount = GetTotalClashResultCountForReport(test);
|
||
|
||
var testInfo = new CollisionTestInfo
|
||
{
|
||
TestName = test.DisplayName,
|
||
TestType = test.TestType.ToString(),
|
||
Tolerance = test.Tolerance,
|
||
CollisionCount = totalCollisionCount,
|
||
Status = test.Status.ToString()
|
||
};
|
||
|
||
// 递归获取每个碰撞的详细信息(包括分组内的)
|
||
var allClashResults = GetAllClashResultsFromTest(test);
|
||
foreach (var clashResult in allClashResults)
|
||
{
|
||
var collision = new CollisionDetailInfo
|
||
{
|
||
CollisionId = clashResult.Guid.ToString(),
|
||
Distance = clashResult.Distance,
|
||
Status = clashResult.Status.ToString()
|
||
};
|
||
|
||
// 获取碰撞对象信息
|
||
if (clashResult.CompositeItem1 != null)
|
||
{
|
||
collision.Object1Name = clashResult.CompositeItem1.DisplayName;
|
||
var point = clashResult.CompositeItem1.BoundingBox();
|
||
collision.Object1Position = $"({point.Center.X:F2}, {point.Center.Y:F2}, {point.Center.Z:F2})";
|
||
}
|
||
|
||
if (clashResult.CompositeItem2 != null)
|
||
{
|
||
collision.Object2Name = clashResult.CompositeItem2.DisplayName;
|
||
var point = clashResult.CompositeItem2.BoundingBox();
|
||
collision.Object2Position = $"({point.Center.X:F2}, {point.Center.Y:F2}, {point.Center.Z:F2})";
|
||
}
|
||
|
||
// 获取碰撞中心点
|
||
if (clashResult.Center != null)
|
||
{
|
||
collision.CollisionCenter = $"({clashResult.Center.X:F2}, {clashResult.Center.Y:F2}, {clashResult.Center.Z:F2})";
|
||
}
|
||
|
||
testInfo.Collisions.Add(collision);
|
||
}
|
||
|
||
reportData.Tests.Add(testInfo);
|
||
}
|
||
|
||
// 统计总体信息
|
||
reportData.TotalTests = reportData.Tests.Count;
|
||
reportData.TotalCollisions = reportData.Tests.Sum(t => t.CollisionCount);
|
||
|
||
LogManager.Info($"碰撞报告生成完成:{reportData.TotalTests}个测试,{reportData.TotalCollisions}个碰撞");
|
||
return reportData;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogManager.Error($"生成碰撞报告异常: {ex.Message}");
|
||
return new CollisionReportData { IsValid = false, ErrorMessage = ex.Message };
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 显示碰撞报告
|
||
/// </summary>
|
||
private async Task ShowCollisionReport(CollisionReportData reportData)
|
||
{
|
||
try
|
||
{
|
||
// 更新UI摘要信息
|
||
await _uiStateManager.ExecuteUIUpdateAsync(() =>
|
||
{
|
||
if (reportData.IsValid)
|
||
{
|
||
// 碰撞报告已生成,显示 Clash Detective 权威结果
|
||
UpdateMainStatus("碰撞报告已生成");
|
||
}
|
||
else
|
||
{
|
||
UpdateMainStatus($"报告生成失败:{reportData.ErrorMessage}");
|
||
}
|
||
});
|
||
|
||
// 如果报告有效且有碰撞数据,用户可通过"查看碰撞报告"按钮查看详细信息
|
||
if (reportData.IsValid && reportData.TotalCollisions > 0)
|
||
{
|
||
LogManager.Info($"碰撞检测完成,发现 {reportData.TotalCollisions} 个碰撞问题。用户可通过报告按钮查看详细信息");
|
||
}
|
||
else if (reportData.IsValid && reportData.TotalCollisions == 0)
|
||
{
|
||
// 没有碰撞时显示简单提示
|
||
LogManager.Info("未发现碰撞问题,路径规划良好");
|
||
System.Windows.MessageBox.Show(
|
||
"未发现碰撞问题,路径规划良好!",
|
||
"碰撞检测结果",
|
||
System.Windows.MessageBoxButton.OK,
|
||
System.Windows.MessageBoxImage.Information);
|
||
}
|
||
else
|
||
{
|
||
// 报告无效时的错误处理
|
||
LogManager.Error($"碰撞报告数据无效: {reportData.ErrorMessage}");
|
||
System.Windows.MessageBox.Show(
|
||
$"碰撞报告生成失败:{reportData.ErrorMessage}",
|
||
"错误",
|
||
System.Windows.MessageBoxButton.OK,
|
||
System.Windows.MessageBoxImage.Error);
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogManager.Error($"显示碰撞报告异常: {ex.Message}");
|
||
System.Windows.MessageBox.Show(
|
||
$"显示碰撞报告时发生异常:{ex.Message}",
|
||
"错误",
|
||
System.Windows.MessageBoxButton.OK,
|
||
System.Windows.MessageBoxImage.Error);
|
||
}
|
||
}
|
||
|
||
#endregion
|
||
|
||
|
||
#region 文档管理
|
||
|
||
/// <summary>
|
||
/// 获取单例实例
|
||
/// </summary>
|
||
private static AnimationControlViewModel _instance;
|
||
public static AnimationControlViewModel Instance
|
||
{
|
||
get { return _instance; }
|
||
set { _instance = value; }
|
||
}
|
||
|
||
/// <summary>
|
||
/// 文档失效时的处理
|
||
/// </summary>
|
||
private void OnDocumentInvalidated(object sender, EventArgs e)
|
||
{
|
||
try
|
||
{
|
||
LogManager.Info("[AnimationControlViewModel] 文档失效,重置状态");
|
||
Reset();
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogManager.Error($"[AnimationControlViewModel] 处理文档失效失败: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 文档就绪时的处理
|
||
/// </summary>
|
||
private void OnDocumentReady(object sender, EventArgs e)
|
||
{
|
||
try
|
||
{
|
||
LogManager.Info("[AnimationControlViewModel] 文档就绪,启用功能");
|
||
|
||
// 重新启用UI
|
||
CanStartAnimation = true;
|
||
CanGenerateAnimation = true;
|
||
|
||
// 🔥 文档就绪时刷新碰撞检测历史列表
|
||
RefreshClashDetectiveResultsList();
|
||
|
||
UpdateMainStatus("文档已加载,就绪");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogManager.Error($"[AnimationControlViewModel] 处理文档就绪失败: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 重置视图模型状态
|
||
/// </summary>
|
||
public void Reset()
|
||
{
|
||
try
|
||
{
|
||
LogManager.Info("[AnimationControlViewModel] 重置状态");
|
||
|
||
// 停止动画
|
||
if (_pathAnimationManager != null && _pathAnimationManager.IsAnimating)
|
||
{
|
||
_pathAnimationManager.CancelAnimation();
|
||
}
|
||
|
||
// 重置角度修正值为0
|
||
ObjectRotationCorrection = LocalEulerRotationCorrection.Zero;
|
||
LogManager.Debug("[重置状态] 已重置角度修正值为0");
|
||
|
||
// 清空选中对象
|
||
SelectedAnimatedObject = null;
|
||
SelectedAnimatedObjectName = string.Empty;
|
||
CurrentPathRoute = null;
|
||
|
||
// 禁用按钮
|
||
CanStartAnimation = false;
|
||
CanPauseAnimation = false;
|
||
CanStopAnimation = false;
|
||
CanGenerateAnimation = false;
|
||
HasSelectedAnimatedObject = false;
|
||
|
||
// 清空碰撞结果
|
||
HasClashDetectiveResults = false;
|
||
ModelHighlightHelper.ClearCollisionHighlights();
|
||
|
||
UpdateMainStatus("已重置");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogManager.Error($"[AnimationControlViewModel] 重置失败: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region 资源清理
|
||
|
||
private bool _disposed = false;
|
||
|
||
/// <summary>
|
||
/// 清理资源
|
||
/// </summary>
|
||
public void Dispose()
|
||
{
|
||
Dispose(true);
|
||
GC.SuppressFinalize(this);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 清理资源
|
||
/// </summary>
|
||
protected virtual void Dispose(bool disposing)
|
||
{
|
||
if (!_disposed)
|
||
{
|
||
if (disposing)
|
||
{
|
||
try
|
||
{
|
||
LogManager.Info("开始清理AnimationControlViewModel资源");
|
||
|
||
// 1. 取消事件订阅(在停止动画之前,避免事件处理中访问已释放的对象)
|
||
|
||
// 取消配置变更事件订阅
|
||
UnsubscribeFromConfigChanges();
|
||
|
||
// 取消DocumentStateManager事件订阅
|
||
try
|
||
{
|
||
DocumentStateManager.Instance.DocumentInvalidated -= OnDocumentInvalidated;
|
||
DocumentStateManager.Instance.DocumentReady -= OnDocumentReady;
|
||
LogManager.Debug("DocumentStateManager事件订阅取消完成");
|
||
}
|
||
catch (Exception docEx)
|
||
{
|
||
LogManager.Warning($"取消DocumentStateManager事件订阅时出现警告: {docEx.Message}");
|
||
}
|
||
|
||
if (_pathAnimationManager != null)
|
||
{
|
||
try
|
||
{
|
||
_pathAnimationManager.ProgressChanged -= OnAnimationProgressChanged;
|
||
_pathAnimationManager.StateChanged -= OnAnimationStateChanged;
|
||
LogManager.Debug("PathAnimationManager事件订阅取消完成");
|
||
}
|
||
catch (Exception eventEx)
|
||
{
|
||
LogManager.Warning($"取消PathAnimationManager事件订阅时出现警告: {eventEx.Message}");
|
||
}
|
||
}
|
||
|
||
if (_clashIntegration != null)
|
||
{
|
||
try
|
||
{
|
||
_clashIntegration.CollisionDetected -= OnCollisionDetected;
|
||
_clashIntegration.ClashDetectiveResultSaved -= OnClashDetectiveResultSaved;
|
||
LogManager.Debug("碰撞检测事件订阅取消完成");
|
||
}
|
||
catch (Exception eventEx)
|
||
{
|
||
LogManager.Warning($"取消碰撞检测事件订阅时出现警告: {eventEx.Message}");
|
||
}
|
||
}
|
||
|
||
if (_manualCollisionTargets != null)
|
||
{
|
||
_manualCollisionTargets.CollectionChanged -= OnManualCollisionTargetsChanged;
|
||
_manualCollisionTargets.Clear();
|
||
}
|
||
|
||
|
||
// 2. 不再清理PathAnimationManager - 现在使用单例模式,由应用程序生命周期管理
|
||
// 注意:PathAnimationManager.GetInstance()返回的是单例实例,
|
||
// 不应该在这里Dispose,否则会影响其他使用该单例的地方
|
||
if (_pathAnimationManager != null)
|
||
{
|
||
try
|
||
{
|
||
// 只清理事件订阅和停止动画,不调用Dispose,也不重置位置(避免访问已释放对象)
|
||
_pathAnimationManager.ShutdownAnimation();
|
||
LogManager.Debug("PathAnimationManager已执行关闭清理");
|
||
}
|
||
catch (Exception resetEx)
|
||
{
|
||
LogManager.Warning($"执行PathAnimationManager关闭清理时出现警告: {resetEx.Message}");
|
||
}
|
||
}
|
||
|
||
// 3. 清理PathAnimationManager缓存(缓存功能已迁移到PathAnimationManager)
|
||
// PathAnimationManager的Dispose已经包含了ClearExclusionCache的调用
|
||
|
||
LogManager.Info("AnimationControlViewModel资源清理完成");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogManager.Error($"AnimationControlViewModel资源清理过程中发生异常: {ex.Message}");
|
||
// 即使清理过程中发生异常,也不应该抛出,因为这会干扰应用程序的正常关闭
|
||
}
|
||
}
|
||
|
||
_disposed = true;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 递归统计测试中的碰撞结果总数(包括分组内的结果)- 用于报告
|
||
/// </summary>
|
||
private int GetTotalClashResultCountForReport(ClashTest test)
|
||
{
|
||
if (test == null) return 0;
|
||
|
||
int totalCount = 0;
|
||
foreach (var child in test.Children)
|
||
{
|
||
if (child is ClashResult)
|
||
{
|
||
totalCount++;
|
||
}
|
||
else if (child is ClashResultGroup group)
|
||
{
|
||
// 递归统计分组内的结果
|
||
totalCount += CountClashResultsInGroupForReport(group);
|
||
}
|
||
}
|
||
|
||
return totalCount;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 递归统计分组内的碰撞结果数量 - 用于报告
|
||
/// </summary>
|
||
private int CountClashResultsInGroupForReport(ClashResultGroup group)
|
||
{
|
||
if (group == null) return 0;
|
||
|
||
int count = 0;
|
||
foreach (var child in group.Children)
|
||
{
|
||
if (child is ClashResult)
|
||
{
|
||
count++;
|
||
}
|
||
else if (child is ClashResultGroup nestedGroup)
|
||
{
|
||
// 递归处理嵌套分组
|
||
count += CountClashResultsInGroupForReport(nestedGroup);
|
||
}
|
||
}
|
||
|
||
return count;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 递归获取测试中的所有碰撞结果(包括分组内的)- 用于报告
|
||
/// </summary>
|
||
private List<ClashResult> GetAllClashResultsFromTest(ClashTest test)
|
||
{
|
||
var results = new List<ClashResult>();
|
||
if (test == null) return results;
|
||
|
||
foreach (var child in test.Children)
|
||
{
|
||
if (child is ClashResult result)
|
||
{
|
||
results.Add(result);
|
||
}
|
||
else if (child is ClashResultGroup group)
|
||
{
|
||
// 递归获取分组内的结果
|
||
results.AddRange(GetAllClashResultsFromGroup(group));
|
||
}
|
||
}
|
||
|
||
return results;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 递归获取分组内的所有碰撞结果 - 用于报告
|
||
/// </summary>
|
||
private List<ClashResult> GetAllClashResultsFromGroup(ClashResultGroup group)
|
||
{
|
||
var results = new List<ClashResult>();
|
||
if (group == null) return results;
|
||
|
||
foreach (var child in group.Children)
|
||
{
|
||
if (child is ClashResult result)
|
||
{
|
||
results.Add(result);
|
||
}
|
||
else if (child is ClashResultGroup nestedGroup)
|
||
{
|
||
// 递归处理嵌套分组
|
||
results.AddRange(GetAllClashResultsFromGroup(nestedGroup));
|
||
}
|
||
}
|
||
|
||
return results;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 添加到批处理队列
|
||
/// </summary>
|
||
private async void ExecuteAddToBatchQueue()
|
||
{
|
||
try
|
||
{
|
||
if (!CanGenerateAnimation)
|
||
{
|
||
UpdateMainStatus("无法添加到批处理:缺少必要条件");
|
||
LogManager.Warning("尝试添加到批处理但条件不满足");
|
||
System.Windows.MessageBox.Show(
|
||
"无法添加到批处理:缺少必要条件(路径未选择或路径点不足)",
|
||
"添加失败",
|
||
System.Windows.MessageBoxButton.OK,
|
||
System.Windows.MessageBoxImage.Warning);
|
||
return;
|
||
}
|
||
|
||
// 🔥 检查排除列表是否已设置
|
||
var excludedObjects = _pathAnimationManager?.GetExcludedObjects();
|
||
if (excludedObjects == null || excludedObjects.Count == 0)
|
||
{
|
||
LogManager.Warning("[批处理] 排除列表为空,提示用户");
|
||
var result = System.Windows.MessageBox.Show(
|
||
"排除列表为空。对于复杂路径,建议先运行\"生成动画\"进行预计算碰撞分析,设置排除列表后再添加到批处理。\n\n" +
|
||
"• 是:继续添加(不推荐,可能检测时间过长)\n" +
|
||
"• 否:取消添加,先去设置排除列表\n\n" +
|
||
"提示:排除列表可以显著减少复杂路径的碰撞检测时间。",
|
||
"添加到批处理确认",
|
||
System.Windows.MessageBoxButton.YesNo,
|
||
System.Windows.MessageBoxImage.Warning
|
||
);
|
||
|
||
if (result == System.Windows.MessageBoxResult.No)
|
||
{
|
||
LogManager.Info("[批处理] 用户选择取消添加,去设置排除列表");
|
||
return;
|
||
}
|
||
|
||
LogManager.Info("[批处理] 用户选择继续添加(不使用排除列表)");
|
||
}
|
||
|
||
LogManager.Info($"[批处理] 添加路径到批处理队列: {CurrentPathRoute.Name}");
|
||
|
||
// 为批处理创建独立的检测记录快照,保存当前配置、排除列表和手工目标
|
||
int detectionRecordId = CreateCollisionDetectionRecordSnapshot("批处理任务创建", bindToAnimationManager: false);
|
||
LogManager.Info($"[批处理] 已创建批处理检测记录 (Id={detectionRecordId})");
|
||
|
||
// 创建批处理队列项
|
||
// 注意:虚拟物体尺寸需要从米转换为模型单位存入数据库
|
||
double metersToUnitsForQueue = UnitsConverter.GetMetersToUnitsConversionFactor();
|
||
var queueItem = new BatchQueueItem
|
||
{
|
||
RouteId = CurrentPathRoute.Id,
|
||
Status = BatchQueueStatus.Pending,
|
||
CreatedTime = DateTime.Now,
|
||
|
||
// 动画配置
|
||
FrameRate = _animationFrameRate,
|
||
DurationSeconds = AnimationDuration,
|
||
|
||
// 碰撞检测配置(米 → 模型单位)
|
||
DetectionTolerance = _detectionTolerance * metersToUnitsForQueue,
|
||
|
||
// 运动物体配置(米 → 模型单位,数据库存储模型单位)
|
||
IsVirtualObject = UseVirtualObject,
|
||
VirtualObjectLength = VirtualObjectLengthInMeters * metersToUnitsForQueue,
|
||
VirtualObjectWidth = VirtualObjectWidthInMeters * metersToUnitsForQueue,
|
||
VirtualObjectHeight = VirtualObjectHeightInMeters * metersToUnitsForQueue,
|
||
|
||
// 被检测项配置
|
||
DetectAllObjects = !IsManualCollisionTargetEnabled,
|
||
|
||
// 角度修正配置
|
||
ObjectRotationCorrection = _objectRotationCorrection.ZDegrees,
|
||
|
||
// 🔥 关联的检测记录ID
|
||
DetectionRecordId = detectionRecordId
|
||
};
|
||
|
||
// 调用批处理管理器添加到队列
|
||
var batchQueueManager = Core.BatchQueueManager.Instance;
|
||
|
||
// 确保BatchQueueManager有PathPlanningManager(用于获取数据库)
|
||
if (_pathPlanningManager != null)
|
||
{
|
||
batchQueueManager.SetPathPlanningManager(_pathPlanningManager);
|
||
}
|
||
|
||
var itemId = await batchQueueManager.AddQueueItemAsync(queueItem);
|
||
|
||
// 存储运动物体到 ModelItemReferences 表
|
||
if (!UseVirtualObject && SelectedAnimatedObject != null)
|
||
{
|
||
LogManager.Info($"[批处理] 准备存储运动物体: {SelectedAnimatedObject.DisplayName}");
|
||
var pathIdInfo = GetObjectPathIdInfo();
|
||
LogManager.Info($"[批处理] 运动物体 PathId: ModelIndex={pathIdInfo.ModelIndex}, PathId={pathIdInfo.PathId}");
|
||
|
||
// 只要 PathId 不为空就认为是有效的(ModelIndex=0 表示主模型,也是有效的)
|
||
if (!string.IsNullOrEmpty(pathIdInfo.PathId))
|
||
{
|
||
var db = _pathPlanningManager?.GetPathDatabase();
|
||
LogManager.Info($"[批处理] PathDatabase 是否为 null: {db == null}");
|
||
|
||
if (db != null)
|
||
{
|
||
await db.AddModelItemReferenceAsync(
|
||
itemId,
|
||
"BatchQueueItem",
|
||
pathIdInfo.ModelIndex,
|
||
pathIdInfo.PathId,
|
||
SelectedAnimatedObject.DisplayName,
|
||
SelectedAnimatedObject.DisplayName,
|
||
"MovingObject"
|
||
);
|
||
LogManager.Info($"[批处理] 已存储运动物体: {SelectedAnimatedObject.DisplayName}");
|
||
}
|
||
}
|
||
else
|
||
{
|
||
LogManager.Warning($"[批处理] 运动物体 PathId 无效,跳过存储");
|
||
}
|
||
}
|
||
else
|
||
{
|
||
LogManager.Info($"[批处理] 跳过存储运动物体: UseVirtualObject={UseVirtualObject}, SelectedAnimatedObject 是否为 null={SelectedAnimatedObject == null}");
|
||
}
|
||
|
||
// 存储手工指定的碰撞检测目标到 ModelItemReferences 表
|
||
if (IsManualCollisionTargetEnabled)
|
||
{
|
||
var db = _pathPlanningManager?.GetPathDatabase();
|
||
if (db != null)
|
||
{
|
||
var validItems = GetValidManualCollisionTargets(pruneInvalidEntries: true);
|
||
foreach (var item in validItems)
|
||
{
|
||
var pathId = Autodesk.Navisworks.Api.Application.ActiveDocument.Models.CreatePathId(item);
|
||
await db.AddModelItemReferenceAsync(
|
||
itemId,
|
||
"BatchQueueItem",
|
||
pathId.ModelIndex,
|
||
pathId.PathId,
|
||
item.DisplayName,
|
||
item.DisplayName,
|
||
"CollisionTarget"
|
||
);
|
||
}
|
||
LogManager.Info($"[批处理] 已存储 {validItems.Count} 个手工指定碰撞目标");
|
||
}
|
||
}
|
||
|
||
// 🔥 注意:排除列表和手工目标已通过 DetectionRecordId 关联到 CollisionDetectionRecords 表
|
||
// 不需要再保存到 ModelItemReferences 表
|
||
|
||
LogManager.Info($"[批处理] 已创建队列项: {CurrentPathRoute.Name}, " +
|
||
$"虚拟物体: {queueItem.IsVirtualObject}, " +
|
||
$"帧率: {queueItem.FrameRate}, " +
|
||
$"时长: {queueItem.DurationSeconds:F2}秒, " +
|
||
$"角度修正: {_objectRotationCorrection}, " +
|
||
$"ID: {itemId}");
|
||
|
||
UpdateMainStatus($"已添加到批处理队列: {CurrentPathRoute.Name}");
|
||
|
||
// 显示成功提示
|
||
System.Windows.MessageBox.Show(
|
||
$"已成功添加到批处理队列:{CurrentPathRoute.Name}\n" +
|
||
$"队列项ID: {itemId}",
|
||
"添加成功",
|
||
System.Windows.MessageBoxButton.OK,
|
||
System.Windows.MessageBoxImage.Information);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogManager.Error($"添加到批处理失败: {ex.Message}", ex);
|
||
UpdateMainStatus($"添加失败: {ex.Message}");
|
||
|
||
// 显示失败提示
|
||
System.Windows.MessageBox.Show(
|
||
$"添加到批处理队列失败:{ex.Message}",
|
||
"添加失败",
|
||
System.Windows.MessageBoxButton.OK,
|
||
System.Windows.MessageBoxImage.Error);
|
||
}
|
||
}
|
||
|
||
#endregion
|
||
}
|
||
}
|