根因(经树结构实验确认): - 副实例碰撞对象可能是盒内几何对象的祖先节点(IsComposite 嵌套实例 容器——如 Revit 门/窗实例——不在 ObjectsInBox 但可被碰撞) - 盒内映射原只覆盖 ObjectsInBox(238 条)——碰撞对象链无匹配(近候选=0) 修复:盒内映射构建覆盖三类节点: 1) ObjectsInBox 自身 + DescendantsAndSelf(嵌套子节点) 2) 沿祖先链向上到模型根(嵌套实例容器层) → 映射 238 → 619 条;Factory 实测映射率 0/5 → 4/4, 落库明细全部带主模型 PathId(1/1/21/0/0/7/0 等,真正可定位) 诊断:F2映射诊断日志(等长/末3/名字候选对比,映射失败时定位用) 验证:集成测试 25/25
2253 lines
101 KiB
C#
2253 lines
101 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using System.Linq;
|
||
using System.Runtime.InteropServices;
|
||
using Autodesk.Navisworks.Api;
|
||
using Autodesk.Navisworks.Api.Clash;
|
||
using NavisworksTransport.Core;
|
||
using NavisworksTransport.Core.Animation;
|
||
using NavisworksTransport.Utils;
|
||
using NavisworksTransport.Utils.CoordinateSystem;
|
||
|
||
namespace NavisworksTransport
|
||
{
|
||
/// <summary>
|
||
/// Clash Detective 集成管理器
|
||
/// 实现动态碰撞检测与Clash Detective窗口的联动
|
||
/// </summary>
|
||
public class ClashDetectiveIntegration
|
||
{
|
||
// Windows API: 设置前台窗口
|
||
[DllImport("user32.dll")]
|
||
private static extern bool SetForegroundWindow(IntPtr hWnd);
|
||
|
||
private static ClashDetectiveIntegration _instance;
|
||
|
||
private DocumentClash _documentClash;
|
||
private List<ClashResult> _currentCollisions;
|
||
// 通道对象缓存,用于优化碰撞检测性能
|
||
private static HashSet<ModelItem> _channelObjectsCache = null;
|
||
private static readonly object _cacheLock = new object();
|
||
|
||
// 几何对象列表缓存,用于避免重复获取对象列表
|
||
// 使用 ModelItemCollection 而非 List<ModelItem> 以避免昂贵的 ToList() 转换
|
||
// 🔥 优化:此缓存保持稳定,不包含运动物体排除逻辑
|
||
private static ModelItemCollection _allGeometryItemsCache = null;
|
||
|
||
// 已过滤通道对象的几何对象列表缓存(供空间索引使用)
|
||
// 🔥 优化:查询时动态过滤运动物体,不依赖此缓存的稳定性
|
||
private static List<ModelItem> _nonChannelGeometryItemsCache = null;
|
||
|
||
// 🔥 移动物体引用(用于从空间索引中排除移动物体及其所有子节点)
|
||
private static ModelItem _animatedObject = null;
|
||
|
||
// 🔥 新增:运动物体及其后代的快速查找集合
|
||
// 用于查询阶段动态过滤,避免重建基础缓存
|
||
private static HashSet<ModelItem> _animatedObjectAndDescendants = new HashSet<ModelItem>();
|
||
|
||
// 🔥 简化设计:只保留一个权威碰撞计数器
|
||
// 预计算碰撞数作为参数传递,不维护类级别状态
|
||
private int _clashDetectiveCollisionCount = 0;
|
||
|
||
/// <summary>
|
||
/// Clash Detective检测到的权威碰撞数量(去重后的实际碰撞数)
|
||
/// </summary>
|
||
public int ClashDetectiveCollisionCount
|
||
{
|
||
get { return _clashDetectiveCollisionCount; }
|
||
}
|
||
|
||
/// <summary>
|
||
/// 【已弃用】预计算碰撞检测点数量现在作为参数传递,不再维护类级别状态
|
||
/// 此属性始终返回0,请从数据库查询或使用局部变量
|
||
/// </summary>
|
||
[Obsolete("预计算碰撞数现在作为参数传递,不再维护类级别状态")]
|
||
public int AnimationCollisionCount
|
||
{
|
||
get { return 0; }
|
||
}
|
||
|
||
/// <summary>
|
||
/// 当前ClashDetective测试名称
|
||
/// </summary>
|
||
public string CurrentTestName
|
||
{
|
||
get { return _currentTestName; }
|
||
}
|
||
|
||
private string _currentTestName;
|
||
|
||
/// <summary>
|
||
/// 设置当前测试名称(用于从数据库加载历史记录时)
|
||
/// </summary>
|
||
public void SetCurrentTestName(string testName)
|
||
{
|
||
_currentTestName = testName;
|
||
LogManager.Info($"[ClashDetectiveIntegration] 设置当前测试名称: {testName}");
|
||
}
|
||
|
||
// 🔥 检测取消标志
|
||
private bool _wasLastTestCanceled = false;
|
||
|
||
/// <summary>
|
||
/// 上次检测是否被取消
|
||
/// </summary>
|
||
public bool WasLastTestCanceled
|
||
{
|
||
get { return _wasLastTestCanceled; }
|
||
}
|
||
|
||
/// <summary>
|
||
/// 单例实例
|
||
/// </summary>
|
||
public static ClashDetectiveIntegration Instance
|
||
{
|
||
get
|
||
{
|
||
if (_instance == null)
|
||
{
|
||
_instance = new ClashDetectiveIntegration();
|
||
}
|
||
return _instance;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 当前检测到的碰撞结果
|
||
/// </summary>
|
||
public List<ClashResult> CurrentCollisions
|
||
{
|
||
get { return _currentCollisions ?? new List<ClashResult>(); }
|
||
}
|
||
|
||
/// <summary>
|
||
/// 碰撞检测结果变化事件
|
||
/// </summary>
|
||
public event EventHandler<CollisionDetectedEventArgs> CollisionDetected;
|
||
|
||
/// <summary>
|
||
/// ClashDetective结果保存到数据库事件
|
||
/// </summary>
|
||
public event EventHandler<ClashDetectiveResultSavedEventArgs> ClashDetectiveResultSaved;
|
||
|
||
private ClashDetectiveIntegration()
|
||
{
|
||
_currentCollisions = new List<ClashResult>();
|
||
|
||
// 自动初始化 Clash Detective 集成
|
||
try
|
||
{
|
||
LogManager.Info("初始化Clash Detective集成(.NET API模式)...");
|
||
|
||
// 直接使用.NET API获取Clash文档
|
||
_documentClash = Application.ActiveDocument.GetClash();
|
||
if (_documentClash != null)
|
||
{
|
||
LogManager.Info("成功获取.NET API Clash文档");
|
||
LogManager.Info("Clash Detective集成初始化成功(.NET API模式)");
|
||
}
|
||
else
|
||
{
|
||
LogManager.Warning("无法获取Clash文档,Clash Detective可能未安装");
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogManager.Error($"初始化Clash Detective集成失败: {ex.Message}");
|
||
_documentClash = null;
|
||
}
|
||
}
|
||
|
||
private List<CollisionResult> _deduplicatedCollisionResults = new List<CollisionResult>(); // 去重后的预计算碰撞结果
|
||
private readonly object _resultsLock = new object();
|
||
|
||
// ClashDetective测试结果缓存:pathName -> List<CollisionResult>
|
||
// 使用自定义的CollisionResult类型,支持修改Item1和Item2
|
||
private Dictionary<string, List<CollisionResult>> _clashDetectiveResultsCache = new Dictionary<string, List<CollisionResult>>();
|
||
private readonly object _clashResultsCacheLock = new object();
|
||
|
||
// 🔥 新增:记录ClashDetective确认碰撞时的位置信息
|
||
// key: "Item1Id|Item2Id", value: 确认碰撞时的位置信息
|
||
private Dictionary<string, CollisionPositionInfo> _confirmedCollisionPositions = new Dictionary<string, CollisionPositionInfo>();
|
||
|
||
/// <summary>
|
||
/// 碰撞位置信息(用于记录ClashDetective确认时的位置)
|
||
/// </summary>
|
||
private class CollisionPositionInfo
|
||
{
|
||
public Point3D AnimatedObjectTrackedPosition { get; set; }
|
||
public Point3D Item2Position { get; set; }
|
||
public double AnimatedObjectTrackedYawRadians { get; set; }
|
||
public Rotation3D AnimatedObjectTrackedRotation { get; set; }
|
||
public bool AnimatedObjectHasTrackedRotation { get; set; }
|
||
public bool HasPositionInfo { get; set; }
|
||
}
|
||
|
||
/// <summary>
|
||
/// 生成碰撞对象的唯一key(使用PathId,不使用InstanceGuid因为多数元素为0)
|
||
/// </summary>
|
||
private string GetCollisionObjectKey(ModelItem item1, ModelItem item2)
|
||
{
|
||
try
|
||
{
|
||
var doc = Application.ActiveDocument;
|
||
string key1 = "null";
|
||
string key2 = "null";
|
||
|
||
if (item1 != null && doc != null)
|
||
{
|
||
var pathId = doc.Models.CreatePathId(item1);
|
||
key1 = $"{pathId.ModelIndex}:{pathId.PathId}";
|
||
}
|
||
|
||
if (item2 != null && doc != null)
|
||
{
|
||
var pathId = doc.Models.CreatePathId(item2);
|
||
key2 = $"{pathId.ModelIndex}:{pathId.PathId}";
|
||
}
|
||
|
||
return $"{key1}|{key2}";
|
||
}
|
||
catch
|
||
{
|
||
// 回退到DisplayName(可能不唯一,但总比空key好)
|
||
var key1 = item1?.DisplayName ?? "null";
|
||
var key2 = item2?.DisplayName ?? "null";
|
||
return $"{key1}|{key2}";
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取去重后的预计算碰撞结果(第一次去重,按碰撞对象对去重)
|
||
/// </summary>
|
||
public List<CollisionResult> GetDeduplicatedCollisionResults()
|
||
{
|
||
lock (_resultsLock)
|
||
{
|
||
LogManager.Debug($"[GetDeduplicatedCollisionResults] 当前去重缓存数量: {_deduplicatedCollisionResults.Count}");
|
||
return new List<CollisionResult>(_deduplicatedCollisionResults);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 保存ClashDetective结果到数据库
|
||
/// 注意:动画参数和物体配置通过 detectionRecordId 关联到 CollisionDetectionRecords 表
|
||
/// </summary>
|
||
private void SaveClashDetectiveResultToDatabase(string routeId, List<CollisionResult> clashResults,
|
||
int precomputedCollisionCount = 0, int? detectionRecordId = null)
|
||
{
|
||
try
|
||
{
|
||
var pathDatabase = PathPlanningManager.Instance?.GetPathDatabase();
|
||
if (pathDatabase != null && detectionRecordId.HasValue)
|
||
{
|
||
// 更新碰撞检测结果到检测记录
|
||
pathDatabase.UpdateCollisionDetectionResult(
|
||
detectionRecordId.Value,
|
||
_currentTestName,
|
||
clashResults.Count,
|
||
precomputedCollisionCount
|
||
);
|
||
LogManager.Info($"ClashDetective结果已保存到数据库,DetectionRecordId={detectionRecordId}, TestName={_currentTestName}");
|
||
|
||
// 保存被撞物体信息(只保存Item2,不保存物体Item1)
|
||
// 同时保存碰撞时运动物体的位置和朝向,用于还原碰撞场景
|
||
var collisionObjects = new List<ClashDetectiveCollisionObjectRecord>();
|
||
|
||
foreach (var collision in clashResults)
|
||
{
|
||
// 只保存被撞到的物体(Item2)
|
||
if (collision.Item2 != null)
|
||
{
|
||
// 使用 CreatePathId API 获取 ModelIndex 和 PathId
|
||
var pathId = Application.ActiveDocument.Models.CreatePathId(collision.Item2);
|
||
|
||
var collisionRecord = new ClashDetectiveCollisionObjectRecord
|
||
{
|
||
DetectionRecordId = detectionRecordId.Value,
|
||
ModelIndex = pathId.ModelIndex,
|
||
PathId = pathId.PathId,
|
||
DisplayName = ModelItemAnalysisHelper.GetSafeDisplayName(collision.Item2),
|
||
ObjectName = ModelItemAnalysisHelper.GetSafeDisplayName(collision.Item2)
|
||
};
|
||
|
||
// 保存碰撞时运动物体的位置和朝向(如果可用)
|
||
if (collision.HasPositionInfo && collision.AnimatedObjectTrackedPosition != null)
|
||
{
|
||
collisionRecord.AnimatedObjectTrackedPosX = collision.AnimatedObjectTrackedPosition.X;
|
||
collisionRecord.AnimatedObjectTrackedPosY = collision.AnimatedObjectTrackedPosition.Y;
|
||
collisionRecord.AnimatedObjectTrackedPosZ = collision.AnimatedObjectTrackedPosition.Z;
|
||
collisionRecord.AnimatedObjectTrackedYawRadians = collision.AnimatedObjectTrackedYawRadians;
|
||
collisionRecord.AnimatedObjectTrackedRotA = collision.AnimatedObjectTrackedRotation?.A;
|
||
collisionRecord.AnimatedObjectTrackedRotB = collision.AnimatedObjectTrackedRotation?.B;
|
||
collisionRecord.AnimatedObjectTrackedRotC = collision.AnimatedObjectTrackedRotation?.C;
|
||
collisionRecord.AnimatedObjectTrackedRotD = collision.AnimatedObjectTrackedRotation?.D;
|
||
collisionRecord.AnimatedObjectHasTrackedRotation = collision.AnimatedObjectHasTrackedRotation;
|
||
collisionRecord.HasPositionInfo = true;
|
||
|
||
LogManager.Debug($"[保存碰撞对象] 记录运动物体位置: ({collisionRecord.AnimatedObjectTrackedPosX:F2}, {collisionRecord.AnimatedObjectTrackedPosY:F2}, {collisionRecord.AnimatedObjectTrackedPosZ:F2}), yaw={collisionRecord.AnimatedObjectTrackedYawRadians:F2} rad, customRotation={collisionRecord.AnimatedObjectHasTrackedRotation}");
|
||
}
|
||
|
||
collisionObjects.Add(collisionRecord);
|
||
}
|
||
}
|
||
|
||
if (collisionObjects.Count > 0)
|
||
{
|
||
pathDatabase.SaveClashDetectiveCollisionObjects(detectionRecordId.Value, collisionObjects);
|
||
LogManager.Info($"已保存 {collisionObjects.Count} 个被撞物体到数据库");
|
||
}
|
||
|
||
// 触发结果保存事件,通知UI刷新列表(从RouteId获取PathName)
|
||
var pathName = GetPathNameByRouteId(routeId);
|
||
ClashDetectiveResultSaved?.Invoke(this, new ClashDetectiveResultSavedEventArgs(pathName, _currentTestName, clashResults.Count));
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogManager.Error($"保存ClashDetective结果到数据库失败: {ex.Message}");
|
||
// 不影响主流程,继续执行
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 根据RouteId获取路径名称
|
||
/// </summary>
|
||
private string GetPathNameByRouteId(string routeId)
|
||
{
|
||
if (string.IsNullOrEmpty(routeId))
|
||
return "未知路径";
|
||
|
||
try
|
||
{
|
||
var pathDatabase = PathPlanningManager.Instance?.GetPathDatabase();
|
||
if (pathDatabase != null)
|
||
{
|
||
var pathRoute = pathDatabase.GetPathRouteSync(routeId);
|
||
return pathRoute?.Name ?? "未知路径";
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogManager.Error($"获取路径名称失败: {ex.Message}");
|
||
}
|
||
return "未知路径";
|
||
}
|
||
|
||
/// <summary>
|
||
/// 从数据库加载ClashDetective碰撞结果
|
||
/// </summary>
|
||
/// <param name="testName">测试名称</param>
|
||
/// <returns>碰撞结果列表,如果加载失败返回null</returns>
|
||
public List<CollisionResult> GetClashDetectiveResultsFromDatabase(string testName)
|
||
{
|
||
try
|
||
{
|
||
var pathDatabase = PathPlanningManager.Instance?.GetPathDatabase();
|
||
if (pathDatabase == null)
|
||
{
|
||
LogManager.Warning($"[LoadClashDetectiveResultsFromDatabase] PathDatabase不可用");
|
||
return null;
|
||
}
|
||
|
||
// 1. 从数据库读取测试信息(直接从 CollisionDetectionRecords 表查询)
|
||
var detectionRecord = pathDatabase.GetDetectionResultByTestName(testName);
|
||
|
||
if (detectionRecord == null)
|
||
{
|
||
LogManager.Warning($"[LoadClashDetectiveResultsFromDatabase] 未找到测试记录: {testName}");
|
||
return null;
|
||
}
|
||
|
||
LogManager.Info($"[LoadClashDetectiveResultsFromDatabase] 找到测试记录: Id={detectionRecord.Id}, TestName={detectionRecord.TestName}, CollisionCount={detectionRecord.CollisionCount}");
|
||
|
||
// 2. 重建物体对象
|
||
ModelItem objectObject = null;
|
||
if (detectionRecord.IsVirtualObject)
|
||
{
|
||
// 创建虚拟物体(保留现有变换如果尺寸相同)
|
||
// 检查尺寸数据完整性
|
||
if (!detectionRecord.VirtualObjectLength.HasValue ||
|
||
!detectionRecord.VirtualObjectWidth.HasValue ||
|
||
!detectionRecord.VirtualObjectHeight.HasValue)
|
||
{
|
||
throw new InvalidOperationException($"[LoadClashDetectiveResultsFromDatabase] 虚拟物体记录缺少尺寸数据: TestName={testName}");
|
||
}
|
||
var modelToMeters = UnitsConverter.GetUnitsToMetersConversionFactor();
|
||
objectObject = VirtualObjectManager.Instance.CreateVirtualObject(
|
||
detectionRecord.VirtualObjectLength.Value * modelToMeters,
|
||
detectionRecord.VirtualObjectWidth.Value * modelToMeters,
|
||
detectionRecord.VirtualObjectHeight.Value * modelToMeters
|
||
) ?? throw new InvalidOperationException($"[LoadClashDetectiveResultsFromDatabase] 获取虚拟物体失败");
|
||
}
|
||
else if (detectionRecord.ObjectModelIndex.HasValue && !string.IsNullOrEmpty(detectionRecord.ObjectPathId))
|
||
{
|
||
// 通过 PathId 查找真实物体
|
||
try
|
||
{
|
||
var pathIdObj = new Autodesk.Navisworks.Api.DocumentParts.ModelItemPathId
|
||
{
|
||
ModelIndex = detectionRecord.ObjectModelIndex.Value,
|
||
PathId = detectionRecord.ObjectPathId
|
||
};
|
||
objectObject = Application.ActiveDocument.Models.ResolvePathId(pathIdObj) ?? throw new InvalidOperationException($"[LoadClashDetectiveResultsFromDatabase] 无法通过 PathId 找到物体对象: ModelIndex={detectionRecord.ObjectModelIndex}, PathId={detectionRecord.ObjectPathId}");
|
||
LogManager.Info($"[LoadClashDetectiveResultsFromDatabase] 已找到真实物体: {ModelItemAnalysisHelper.GetSafeDisplayName(objectObject)}");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
throw new InvalidOperationException($"[LoadClashDetectiveResultsFromDatabase] ResolvePathId 失败: ModelIndex={detectionRecord.ObjectModelIndex}, PathId={detectionRecord.ObjectPathId}", ex);
|
||
}
|
||
}
|
||
|
||
if (objectObject == null)
|
||
{
|
||
throw new InvalidOperationException($"[LoadClashDetectiveResultsFromDatabase] 无法重建物体对象: IsVirtualObject={detectionRecord.IsVirtualObject}, ObjectModelIndex={detectionRecord.ObjectModelIndex}, ObjectPathId={detectionRecord.ObjectPathId}");
|
||
}
|
||
|
||
// 如果是虚拟物体且是新创建的(不是已存在的),将其移动到路径起点
|
||
// 注意:如果虚拟物体已存在(动画刚结束),保持当前位置(终点),不要移动到起点
|
||
if (detectionRecord.IsVirtualObject && !string.IsNullOrEmpty(detectionRecord.RouteId) &&
|
||
!VirtualObjectManager.Instance.IsVirtualObjectActive)
|
||
{
|
||
try
|
||
{
|
||
var pathPlanningManager = PathPlanningManager.Instance;
|
||
var route = pathPlanningManager.GetAllRoutes().FirstOrDefault(r => r.Id == detectionRecord.RouteId);
|
||
|
||
if (route != null && route.Points != null && route.Points.Count > 0)
|
||
{
|
||
// 获取第一个点(起点)
|
||
var startPoint = route.Points.FirstOrDefault(p => p.Type == PathPointType.StartPoint) ?? route.Points[0];
|
||
var startPointPosition = startPoint.Position;
|
||
|
||
// 使用 PathAnimationManager 将物体移动到起点
|
||
var pathAnimationManager = PathAnimationManager.GetInstance();
|
||
bool moved = pathAnimationManager.MoveObjectToPathStart(objectObject, new List<Point3D> { startPointPosition, startPointPosition });
|
||
if (moved)
|
||
{
|
||
LogManager.Info($"[LoadClashDetectiveResultsFromDatabase] 虚拟物体已移动到路径起点: ({startPointPosition.X:F2}, {startPointPosition.Y:F2}, {startPointPosition.Z:F2})");
|
||
}
|
||
else
|
||
{
|
||
LogManager.Warning($"[LoadClashDetectiveResultsFromDatabase] 虚拟物体移动到路径起点失败");
|
||
}
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogManager.Warning($"[LoadClashDetectiveResultsFromDatabase] 移动虚拟物体到起点失败: {ex.Message}");
|
||
}
|
||
}
|
||
else if (detectionRecord.IsVirtualObject && VirtualObjectManager.Instance.IsVirtualObjectActive)
|
||
{
|
||
LogManager.Info($"[LoadClashDetectiveResultsFromDatabase] 虚拟物体已存在,保持当前位置(终点),不移动到起点");
|
||
}
|
||
|
||
// 3. 从数据库读取被撞物体信息(包含碰撞时运动物体的位置和朝向)
|
||
var collisionObjects = pathDatabase.GetClashDetectiveCollisionObjects(detectionRecord.Id);
|
||
LogManager.Info($"[LoadClashDetectiveResultsFromDatabase] 从数据库读取碰撞对象: DetectionRecordId={detectionRecord.Id}, 找到 {collisionObjects.Count} 个对象");
|
||
|
||
// 4. 重建碰撞结果
|
||
var results = new List<CollisionResult>();
|
||
|
||
foreach (var obj in collisionObjects)
|
||
{
|
||
ModelItem collidedObject = null;
|
||
|
||
// 优先使用 ResolvePathId API(新方式)
|
||
if (obj.ModelIndex.HasValue && !string.IsNullOrEmpty(obj.PathId))
|
||
{
|
||
try
|
||
{
|
||
var pathIdObj = new Autodesk.Navisworks.Api.DocumentParts.ModelItemPathId();
|
||
pathIdObj.ModelIndex = obj.ModelIndex.Value;
|
||
pathIdObj.PathId = obj.PathId;
|
||
collidedObject = Application.ActiveDocument.Models.ResolvePathId(pathIdObj);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
throw new InvalidOperationException($"[LoadClashDetectiveResultsFromDatabase] ResolvePathId 失败: ModelIndex={obj.ModelIndex}, PathId={obj.PathId}", ex);
|
||
}
|
||
}
|
||
|
||
// 🔥 PathId 缺失/解析失败(F1 副实例映射未命中场景):按 DisplayName 在主模型查找(唯一匹配)
|
||
if (collidedObject == null && !string.IsNullOrWhiteSpace(obj.ObjectName))
|
||
{
|
||
collidedObject = TryFindSingleObjectByName(obj.ObjectName);
|
||
if (collidedObject != null)
|
||
{
|
||
LogManager.Info($"[LoadClashDetectiveResultsFromDatabase] 已按名称匹配碰撞对象: {obj.ObjectName}");
|
||
}
|
||
}
|
||
|
||
if (collidedObject == null)
|
||
{
|
||
// 🔥 容错:单个对象无法定位不阻断整个报告——跳过并警告(其余对象仍可显示)
|
||
LogManager.Warning($"[LoadClashDetectiveResultsFromDatabase] 跳过无法定位的碰撞对象: {obj.ObjectName} (ModelIndex={obj.ModelIndex}, PathId={obj.PathId})");
|
||
continue;
|
||
}
|
||
|
||
if (!ModelItemAnalysisHelper.IsModelItemValid(collidedObject))
|
||
{
|
||
throw new InvalidOperationException($"[LoadClashDetectiveResultsFromDatabase] 找到的 ModelItem 无效: {obj.ModelIndex},{obj.PathId}");
|
||
}
|
||
|
||
// 使用重建对象的 DisplayName,而不是数据库中保存的 DisplayName
|
||
var collidedObjectName = ModelItemAnalysisHelper.GetSafeDisplayName(collidedObject);
|
||
|
||
var collisionResult = new CollisionResult
|
||
{
|
||
ClashGuid = Guid.NewGuid(),
|
||
DisplayName = $"历史碰撞: {collidedObjectName}",
|
||
Status = ClashResultStatus.Active,
|
||
Item1 = objectObject,
|
||
Item2 = collidedObject,
|
||
Center = collidedObject.BoundingBox().Center,
|
||
Distance = 0.0,
|
||
CreatedTime = DateTime.Now
|
||
};
|
||
|
||
// 恢复碰撞时运动物体的位置和朝向(如果可用)
|
||
if (obj.HasPositionInfo && obj.AnimatedObjectTrackedPosX.HasValue && obj.AnimatedObjectTrackedPosY.HasValue && obj.AnimatedObjectTrackedPosZ.HasValue)
|
||
{
|
||
collisionResult.AnimatedObjectTrackedPosition = new Point3D(obj.AnimatedObjectTrackedPosX.Value, obj.AnimatedObjectTrackedPosY.Value, obj.AnimatedObjectTrackedPosZ.Value);
|
||
collisionResult.AnimatedObjectTrackedYawRadians = obj.AnimatedObjectTrackedYawRadians ?? 0.0;
|
||
collisionResult.AnimatedObjectHasTrackedRotation = obj.AnimatedObjectHasTrackedRotation;
|
||
if (obj.AnimatedObjectHasTrackedRotation &&
|
||
obj.AnimatedObjectTrackedRotA.HasValue &&
|
||
obj.AnimatedObjectTrackedRotB.HasValue &&
|
||
obj.AnimatedObjectTrackedRotC.HasValue &&
|
||
obj.AnimatedObjectTrackedRotD.HasValue)
|
||
{
|
||
collisionResult.AnimatedObjectTrackedRotation = new Rotation3D(
|
||
obj.AnimatedObjectTrackedRotA.Value,
|
||
obj.AnimatedObjectTrackedRotB.Value,
|
||
obj.AnimatedObjectTrackedRotC.Value,
|
||
obj.AnimatedObjectTrackedRotD.Value);
|
||
}
|
||
collisionResult.HasPositionInfo = true;
|
||
|
||
LogManager.Debug($"[加载碰撞结果] 恢复运动物体位置: ({obj.AnimatedObjectTrackedPosX:F2}, {obj.AnimatedObjectTrackedPosY:F2}, {obj.AnimatedObjectTrackedPosZ:F2}), yaw={obj.AnimatedObjectTrackedYawRadians:F2} rad, customRotation={obj.AnimatedObjectHasTrackedRotation}");
|
||
}
|
||
|
||
results.Add(collisionResult);
|
||
}
|
||
|
||
LogManager.Info($"[LoadClashDetectiveResultsFromDatabase] 从数据库加载测试 '{testName}' 完成,重建了 {results.Count} 个碰撞结果");
|
||
return results;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogManager.Error($"[LoadClashDetectiveResultsFromDatabase] 加载失败: {ex.Message}", ex);
|
||
return null;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取对象当前位置
|
||
/// </summary>
|
||
private Point3D GetObjectPosition(ModelItem item)
|
||
{
|
||
try
|
||
{
|
||
if (item == null) return new Point3D(0, 0, 0);
|
||
|
||
var bounds = item.BoundingBox();
|
||
if (bounds != null)
|
||
{
|
||
return new Point3D(
|
||
(bounds.Min.X + bounds.Max.X) / 2,
|
||
(bounds.Min.Y + bounds.Max.Y) / 2,
|
||
(bounds.Min.Z + bounds.Max.Z) / 2
|
||
);
|
||
}
|
||
return new Point3D(0, 0, 0);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogManager.Error($"获取对象位置失败: {ex.Message}");
|
||
return new Point3D(0, 0, 0);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 通过PathId向上查找匹配的原始对象
|
||
/// 从ClashDetective返回的几何体组件开始,向上遍历父节点,
|
||
/// 直到找到与预计算记录中任一对象PathId匹配的父节点
|
||
/// </summary>
|
||
private ModelItem FindMatchingObjectByPathId(ModelItem clashItem2, List<CollisionResult> precomputedCollisions)
|
||
{
|
||
try
|
||
{
|
||
if (precomputedCollisions == null || precomputedCollisions.Count == 0 || clashItem2 == null)
|
||
return null;
|
||
|
||
var doc = Application.ActiveDocument;
|
||
if (doc?.Models == null) return null;
|
||
|
||
// 1. 收集所有预计算记录中Item2的PathId(作为HashSet提高查找效率)
|
||
var precomputedPathIds = new HashSet<(int ModelIndex, string PathId)>();
|
||
foreach (var precomputed in precomputedCollisions)
|
||
{
|
||
if (precomputed.Item2 != null)
|
||
{
|
||
try
|
||
{
|
||
var pathId = doc.Models.CreatePathId(precomputed.Item2);
|
||
precomputedPathIds.Add((pathId.ModelIndex, pathId.PathId));
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogManager.Warning($"[PathId匹配] 获取PathId失败: {ex.Message}");
|
||
}
|
||
}
|
||
}
|
||
|
||
if (precomputedPathIds.Count == 0)
|
||
{
|
||
LogManager.Debug($"[PathId匹配] 预计算记录中没有有效的PathId");
|
||
return null;
|
||
}
|
||
|
||
// 2. 从ClashDetective返回的Item2开始向上遍历父节点
|
||
var current = clashItem2;
|
||
int levels = 0;
|
||
while (current != null && levels < 20) // 最多向上查找20层
|
||
{
|
||
try
|
||
{
|
||
var currentPathId = doc.Models.CreatePathId(current);
|
||
|
||
// 检查PathId是否在预计算记录中
|
||
if (precomputedPathIds.Contains((currentPathId.ModelIndex, currentPathId.PathId)))
|
||
{
|
||
LogManager.Debug($"[PathId匹配] 成功找到匹配的父节点: {ModelItemAnalysisHelper.GetSafeDisplayName(current)} (向上{levels}层)");
|
||
return current;
|
||
}
|
||
|
||
// 继续向上查找
|
||
current = current.Parent;
|
||
levels++;
|
||
}
|
||
catch
|
||
{
|
||
// 如果当前节点无法获取PathId,继续向上
|
||
current = current.Parent;
|
||
levels++;
|
||
}
|
||
}
|
||
|
||
LogManager.Debug($"[PathId匹配] 未找到匹配的父节点,向上查找了{levels}层");
|
||
return null;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogManager.Warning($"[PathId匹配] 查找失败: {ex.Message}");
|
||
return null;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 运行ClashDetective测试并保存到数据库(公共方法,供批处理和非批处理调用)
|
||
/// </summary>
|
||
/// <returns>碰撞分组、主测试、确认的碰撞数量、是否被取消</returns>
|
||
public (ClashResultGroup collisionGroup, ClashTest addedMainTest, int confirmedCount, bool wasCanceled) RunClashDetectiveTestsAndSaveToDatabase(
|
||
List<CollisionResult> precomputedCollisions,
|
||
double detectionGap,
|
||
string routeId,
|
||
ModelItem animatedObject,
|
||
bool isVirtualObject,
|
||
int frameRate,
|
||
double duration,
|
||
double virtualObjectLength,
|
||
double virtualObjectWidth,
|
||
double virtualObjectHeight,
|
||
Progress progress = null,
|
||
int precomputedCollisionCount = 0,
|
||
int? detectionRecordId = null)
|
||
{
|
||
LogManager.Info($"[ClashDetective] 开始运行碰撞检测并保存到数据库(容差: {detectionGap}米)");
|
||
|
||
// 保存碰撞检测前的物体状态
|
||
if (animatedObject != null && IsModelItemValid(animatedObject))
|
||
{
|
||
PathAnimationManager.GetInstance().SaveObjectState(animatedObject);
|
||
}
|
||
|
||
// 过滤有效的碰撞
|
||
var validCollisions = precomputedCollisions.Where(collision =>
|
||
collision.HasPositionInfo &&
|
||
IsModelItemValid(collision.Item1) &&
|
||
IsModelItemValid(collision.Item2)
|
||
).ToList();
|
||
|
||
LogManager.Info($"[ClashDetective] 有效碰撞数量: {validCollisions.Count}");
|
||
|
||
// 第一步:创建主测试(不包含选择集,纯容器)
|
||
var mainTestName = $"碰撞检测_{DateTime.Now:MMdd_HHmmssfff}";
|
||
_currentTestName = mainTestName;
|
||
LogManager.Info($"[分组测试] 创建主测试: {mainTestName}");
|
||
|
||
// 创建主测试
|
||
var mainTest = new ClashTest
|
||
{
|
||
DisplayName = mainTestName,
|
||
TestType = ClashTestType.Hard,
|
||
Tolerance = detectionGap,
|
||
Guid = Guid.Empty,
|
||
MergeComposites = true
|
||
};
|
||
|
||
// 添加主测试到文档
|
||
_documentClash.TestsData.TestsAddCopy(mainTest);
|
||
LogManager.Info($"[分组测试] 主测试已添加到文档");
|
||
|
||
// 获取添加后的测试对象引用
|
||
var addedMainTest = _documentClash.TestsData.Tests.FirstOrDefault(t => t.DisplayName == mainTestName) as ClashTest;
|
||
if (addedMainTest == null)
|
||
{
|
||
LogManager.Error("[分组测试] 无法获取添加后的主测试对象");
|
||
return (null, null, 0, false);
|
||
}
|
||
|
||
// 如果没有有效碰撞,直接保存空记录并返回
|
||
if (validCollisions.Count == 0)
|
||
{
|
||
LogManager.Warning("[ClashDetective] 没有有效的碰撞,保存空碰撞记录");
|
||
|
||
// 重置碰撞计数器(重要:避免保留上次测试的值)
|
||
_clashDetectiveCollisionCount = 0;
|
||
|
||
// 保存到数据库(配置参数通过 detectionRecordId 关联获取)
|
||
SaveClashDetectiveResultToDatabase(
|
||
routeId,
|
||
new List<CollisionResult>(),
|
||
precomputedCollisionCount,
|
||
detectionRecordId
|
||
);
|
||
|
||
return (null, addedMainTest, 0, false);
|
||
}
|
||
|
||
// 第二步:创建分组并添加碰撞结果(应用智能去重)
|
||
// 1. 分组:按碰撞对象对分组
|
||
var groupedCollisions = validCollisions
|
||
.GroupBy(c => new { Item1 = c.Item1, Item2 = c.Item2 })
|
||
.ToList();
|
||
|
||
LogManager.Info($"[分组测试] 智能去重: {validCollisions.Count} 个检测点 -> {groupedCollisions.Count} 个唯一碰撞对");
|
||
|
||
// 🔥 初始化确认碰撞位置字典
|
||
_confirmedCollisionPositions.Clear();
|
||
|
||
// 缓存去重后的碰撞结果(每组取第一个)
|
||
lock (_resultsLock)
|
||
{
|
||
_deduplicatedCollisionResults.Clear();
|
||
foreach (var group in groupedCollisions)
|
||
{
|
||
_deduplicatedCollisionResults.Add(group.First());
|
||
}
|
||
LogManager.Debug($"[去重缓存] 已缓存 {_deduplicatedCollisionResults.Count} 个去重后的碰撞结果");
|
||
}
|
||
|
||
var collisionGroup = new ClashResultGroup
|
||
{
|
||
DisplayName = $"碰撞检测组 ({groupedCollisions.Count} 个唯一碰撞对)"
|
||
};
|
||
|
||
LogManager.Info($"[分组测试] 创建碰撞分组: {collisionGroup.DisplayName}");
|
||
|
||
int confirmedCount = 0;
|
||
int skippedCount = 0;
|
||
int testCount = 0; // 检测计数器,用于进度日志
|
||
int totalTests = validCollisions.Count; // 总检测次数(预估)
|
||
int totalGroups = groupedCollisions.Count; // 总组数
|
||
var doc = Application.ActiveDocument;
|
||
|
||
// 初始化进度条
|
||
progress?.Update(0.0);
|
||
|
||
// 收集所有临时测试,用于批量删除
|
||
var tempTestsToRemove = new List<ClashTest>();
|
||
|
||
bool wasCanceled = false;
|
||
|
||
// 2. 遍历每一组
|
||
for (int groupIndex = 0; groupIndex < groupedCollisions.Count; groupIndex++)
|
||
{
|
||
// 检查用户是否取消
|
||
if (progress != null && progress.IsCanceled)
|
||
{
|
||
LogManager.Info($"[分组测试] 用户取消操作,已处理 {groupIndex}/{groupedCollisions.Count} 组");
|
||
wasCanceled = true;
|
||
break;
|
||
}
|
||
|
||
// 更新进度条(每5组更新一次,避免过于频繁)
|
||
if (groupIndex % 5 == 0 || groupIndex == groupedCollisions.Count - 1)
|
||
{
|
||
double progressPercent = (double)groupIndex / totalGroups * 0.7; // 组进度占70%
|
||
progress?.Update(progressPercent);
|
||
}
|
||
|
||
var group = groupedCollisions[groupIndex];
|
||
|
||
// 3. 排序:按距离/重叠深度排序,优先检测最严重的碰撞
|
||
// 注意:Distance通常越小(或负值越大)表示碰撞越深,具体取决于计算方式
|
||
// 这里假设Distance越小越严重
|
||
var sortedCandidates = group.OrderBy(c => c.Distance).ToList();
|
||
|
||
bool pairConfirmed = false;
|
||
|
||
// 4. 验证即止:逐个检测候选帧
|
||
for (int i = 0; i < sortedCandidates.Count; i++)
|
||
{
|
||
var candidate = sortedCandidates[i];
|
||
|
||
try
|
||
{
|
||
var testAnimatedObject = candidate.Item1;
|
||
var modelItems = new ModelItemCollection { testAnimatedObject };
|
||
var targetPosition = candidate.AnimatedObjectTrackedPosition;
|
||
var targetYaw = candidate.AnimatedObjectTrackedYawRadians;
|
||
var targetRotation = candidate.AnimatedObjectTrackedRotation;
|
||
|
||
if (candidate.AnimatedObjectHasTrackedRotation && targetRotation != null)
|
||
{
|
||
LogManager.Info(
|
||
$"[ClashDetective验证] 已跟踪姿态候选恢复: " +
|
||
$"对象={testAnimatedObject.DisplayName}, " +
|
||
$"目标跟踪点=({targetPosition.X:F3},{targetPosition.Y:F3},{targetPosition.Z:F3})");
|
||
|
||
var pam = PathAnimationManager.GetInstance();
|
||
if (pam != null && pam.ControlsAnimatedObject(testAnimatedObject))
|
||
{
|
||
pam.MoveAnimatedObjectToPose(
|
||
testAnimatedObject,
|
||
targetPosition,
|
||
targetYaw,
|
||
targetRotation,
|
||
true);
|
||
}
|
||
else
|
||
{
|
||
throw new InvalidOperationException(
|
||
$"ClashDetective已跟踪姿态候选恢复失败:对象 {testAnimatedObject.DisplayName} 不受当前动画管理器控制。");
|
||
}
|
||
}
|
||
else
|
||
{
|
||
throw new InvalidOperationException(
|
||
$"ClashDetective候选恢复缺少完整姿态:对象 {testAnimatedObject.DisplayName}," +
|
||
$"目标跟踪点=({targetPosition.X:F3},{targetPosition.Y:F3},{targetPosition.Z:F3})," +
|
||
$"TrackedYaw={targetYaw:F6}rad。");
|
||
}
|
||
|
||
var tempTestName = $"临时验证_{confirmedCount + 1}_{i}_{DateTime.Now:HHmmss_fff}";
|
||
|
||
// 🔥 优化3:预配置 ClashTest,避免后续 CreateCopy 和 TestsEditTestFromCopy 调用
|
||
var tempTest = new ClashTest
|
||
{
|
||
DisplayName = tempTestName,
|
||
TestType = ClashTestType.Hard,
|
||
Tolerance = detectionGap,
|
||
Guid = Guid.Empty,
|
||
MergeComposites = true
|
||
};
|
||
|
||
var selectionA = new ModelItemCollection { candidate.Item1 };
|
||
var selectionB = new ModelItemCollection { candidate.Item2 };
|
||
tempTest.SelectionA.Selection.CopyFrom(selectionA);
|
||
tempTest.SelectionB.Selection.CopyFrom(selectionB);
|
||
|
||
// 直接设置 PrimitiveTypes,避免后续的 CreateCopy 和 TestsEditTestFromCopy
|
||
tempTest.SelectionA.PrimitiveTypes = PrimitiveTypes.Triangles;
|
||
tempTest.SelectionB.PrimitiveTypes = PrimitiveTypes.Triangles | PrimitiveTypes.Lines | PrimitiveTypes.Points;
|
||
|
||
_documentClash.TestsData.TestsAddCopy(tempTest);
|
||
var addedTempTest = _documentClash.TestsData.Tests.FirstOrDefault(t => t.DisplayName == tempTestName) as ClashTest;
|
||
|
||
if (addedTempTest != null)
|
||
{
|
||
// 🔥 优化3:跳过了 addedTempTest.CreateCopy() 和 TestsEditTestFromCopy 调用
|
||
// 因为 PrimitiveTypes 已在创建 tempTest 时设置好
|
||
|
||
_documentClash.TestsData.TestsRunTest(addedTempTest);
|
||
|
||
// 增加检测计数并打印进度日志(每50次更新一次进度条,每100次打印日志)
|
||
testCount++;
|
||
if (testCount % 50 == 0)
|
||
{
|
||
// 基于总检测次数计算进度(组进度占70%,检测进度占30%)
|
||
double groupProgress = (double)(groupIndex + 1) / totalGroups * 0.7;
|
||
double testProgress = (double)testCount / totalTests * 0.3;
|
||
double totalProgress = Math.Min(groupProgress + testProgress, 0.99);
|
||
progress?.Update(totalProgress);
|
||
}
|
||
if (testCount % 100 == 0)
|
||
{
|
||
double logProgressPercent = (double)testCount / totalTests * 100;
|
||
LogManager.Info($"[ClashDetective进度] 已执行 {testCount}/{totalTests} 次检测 ({logProgressPercent:F1}%)");
|
||
}
|
||
|
||
var refreshedTempTest = _documentClash.TestsData.Tests.FirstOrDefault(t => t.DisplayName == tempTestName) as ClashTest;
|
||
if (refreshedTempTest != null && refreshedTempTest.Children.Count > 0)
|
||
{
|
||
// !!!发现真实碰撞!!!
|
||
pairConfirmed = true;
|
||
confirmedCount++;
|
||
skippedCount += (sortedCandidates.Count - 1 - i); // 记录跳过的数量
|
||
|
||
// 🔥 关键:记录确认碰撞的候选帧位置信息,供后续使用
|
||
// 使用Item2作为key,因为最终去重也是按Item2合并的
|
||
var confirmedPositionKey = GetCollisionObjectKey(candidate.Item1, candidate.Item2);
|
||
_confirmedCollisionPositions[confirmedPositionKey] = new CollisionPositionInfo
|
||
{
|
||
AnimatedObjectTrackedPosition = candidate.AnimatedObjectTrackedPosition,
|
||
Item2Position = candidate.Item2Position,
|
||
AnimatedObjectTrackedYawRadians = candidate.AnimatedObjectTrackedYawRadians,
|
||
AnimatedObjectTrackedRotation = candidate.AnimatedObjectTrackedRotation,
|
||
AnimatedObjectHasTrackedRotation = candidate.AnimatedObjectHasTrackedRotation,
|
||
HasPositionInfo = candidate.HasPositionInfo
|
||
};
|
||
LogManager.Debug($"[ClashDetective确认] 记录碰撞位置: {confirmedPositionKey}, 位置: ({candidate.AnimatedObjectTrackedPosition.X:F2}, {candidate.AnimatedObjectTrackedPosition.Y:F2}, {candidate.AnimatedObjectTrackedPosition.Z:F2})");
|
||
|
||
int subResultIndex = 1;
|
||
// 🔥 优化:预获取运动物体名称,避免在循环中重复获取
|
||
var animatedObjectName = ModelItemAnalysisHelper.GetSafeDisplayName(animatedObject);
|
||
|
||
foreach (var child in refreshedTempTest.Children)
|
||
{
|
||
if (child is ClashResult result)
|
||
{
|
||
var copiedResult = result.CreateCopy() as ClashResult;
|
||
copiedResult.Guid = Guid.NewGuid(); // 生成新的GUID
|
||
|
||
// 设置碰撞名称:运动物体名称直接使用预获取的,被撞物体查找父容器
|
||
var container2 = ModelItemAnalysisHelper.FindNamedParentContainer(result.Item2);
|
||
var object2Name = ModelItemAnalysisHelper.GetSafeDisplayName(container2);
|
||
|
||
var timeStamp = DateTime.Now.ToString("HHmmss");
|
||
copiedResult.DisplayName = $"物流碰撞#{confirmedCount:00}-{subResultIndex:00}_{timeStamp}: {animatedObjectName} ↔ {object2Name}";
|
||
|
||
collisionGroup.Children.Add(copiedResult);
|
||
subResultIndex++;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 🔥 优化4:收集测试用于批量删除,而不是立即删除
|
||
tempTestsToRemove.Add(refreshedTempTest ?? addedTempTest);
|
||
|
||
if (pairConfirmed)
|
||
{
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
catch (Exception itemEx)
|
||
{
|
||
LogManager.Error($"[ClashDetective] 候选检测失败: {itemEx.Message}");
|
||
}
|
||
}
|
||
}
|
||
|
||
// 如果用户取消,直接返回,不执行后续操作(临时测试将在下方统一清理)
|
||
if (wasCanceled)
|
||
{
|
||
LogManager.Info($"[分组测试] 检测已被用户取消,跳过结果处理和保存");
|
||
}
|
||
|
||
// 🔥 优化4:批量删除所有临时测试
|
||
if (tempTestsToRemove.Count > 0)
|
||
{
|
||
LogManager.Debug($"[分组测试] 批量删除 {tempTestsToRemove.Count} 个临时测试");
|
||
foreach (var test in tempTestsToRemove)
|
||
{
|
||
try
|
||
{
|
||
_documentClash.TestsData.TestsRemove(test);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogManager.Debug($"[分组测试] 删除临时测试失败: {ex.Message}");
|
||
}
|
||
}
|
||
}
|
||
|
||
// 如果用户取消,直接返回,不执行后续操作
|
||
if (wasCanceled)
|
||
{
|
||
return (null, addedMainTest, confirmedCount, true);
|
||
}
|
||
|
||
// 完成进度条
|
||
progress?.Update(1.0);
|
||
|
||
LogManager.Info($"[分组测试] ClashDetective检测完成: 执行 {testCount} 次检测, 确认碰撞 {confirmedCount} 组, 跳过 {skippedCount} 个冗余检测点");
|
||
|
||
// 第三步:处理碰撞结果
|
||
// 🔥 重要:ClashDetective 返回的是几何体级别的碰撞结果
|
||
// Item1 是移动物体的组件名(如"车轮1"、"车轮2"),需要统一为移动物体本身
|
||
// Item2 是被撞物体,通过PathId向上查找匹配预计算中的原始对象
|
||
var clashResults = new List<CollisionResult>();
|
||
foreach (var child in collisionGroup.Children)
|
||
{
|
||
if (child is ClashResult clashResult)
|
||
{
|
||
// 🔥 通过PathId向上查找匹配的原始对象
|
||
// 从ClashDetective返回的几何体组件开始,向上遍历父节点,
|
||
// 直到找到与预计算记录中任一对象PathId匹配的父节点
|
||
ModelItem originalItem2 = FindMatchingObjectByPathId(clashResult.Item2, precomputedCollisions);
|
||
if (originalItem2 == null)
|
||
{
|
||
originalItem2 = clashResult.Item2; // 回退到原始对象
|
||
}
|
||
|
||
// 🔥 从确认碰撞位置字典中获取位置信息(ClashDetective实际验证通过的帧)
|
||
var positionKey = GetCollisionObjectKey(animatedObject, originalItem2);
|
||
_confirmedCollisionPositions.TryGetValue(positionKey, out var confirmedPosition);
|
||
|
||
var collisionResult = new CollisionResult
|
||
{
|
||
ClashGuid = clashResult.Guid,
|
||
DisplayName = clashResult.DisplayName,
|
||
Status = clashResult.Status,
|
||
Item1 = animatedObject,
|
||
Item2 = originalItem2,
|
||
Center = clashResult.Center,
|
||
Distance = clashResult.Distance,
|
||
CreatedTime = DateTime.Now,
|
||
// 🔥 使用ClashDetective确认时的位置信息
|
||
AnimatedObjectTrackedPosition = confirmedPosition?.AnimatedObjectTrackedPosition,
|
||
Item2Position = confirmedPosition?.Item2Position,
|
||
AnimatedObjectTrackedYawRadians = confirmedPosition?.AnimatedObjectTrackedYawRadians ?? 0,
|
||
AnimatedObjectTrackedRotation = confirmedPosition?.AnimatedObjectTrackedRotation,
|
||
AnimatedObjectHasTrackedRotation = confirmedPosition?.AnimatedObjectHasTrackedRotation ?? false,
|
||
HasPositionInfo = confirmedPosition != null
|
||
};
|
||
clashResults.Add(collisionResult);
|
||
}
|
||
}
|
||
|
||
// 🔥 新增:按碰撞对象对去重
|
||
// 原因:同一个移动物体与同一个被撞物体的多个组件碰撞,需要合并为一个记录
|
||
var finalClashResults = clashResults
|
||
.GroupBy(c => new { Item1 = c.Item1, Item2 = c.Item2 })
|
||
.Select(g => g.First())
|
||
.ToList();
|
||
|
||
// 🔥 日志:统计位置信息保留情况
|
||
var withPositionCount = finalClashResults.Count(c => c.HasPositionInfo && c.AnimatedObjectTrackedPosition != null);
|
||
LogManager.Info($"[最终去重] ClashDetective结果去重: {clashResults.Count} 个碰撞 -> {finalClashResults.Count} 个唯一碰撞对,其中 {withPositionCount} 个包含位置信息");
|
||
|
||
// 🔥 优化:在去重后为结果设置详细的 DisplayName
|
||
// 这样只对保留的结果进行名称计算,避免在子碰撞结果上浪费性能
|
||
foreach (var result in finalClashResults)
|
||
{
|
||
if (result.Item1 != null && result.Item2 != null)
|
||
{
|
||
var object1Name = ModelItemAnalysisHelper.GetSafeDisplayName(result.Item1);
|
||
var object2Name = ModelItemAnalysisHelper.GetSafeDisplayName(result.Item2);
|
||
result.DisplayName = $"物流碰撞: {object1Name} ↔ {object2Name}";
|
||
}
|
||
}
|
||
|
||
// 缓存最终结果
|
||
lock (_clashResultsCacheLock)
|
||
{
|
||
_clashDetectiveResultsCache[_currentTestName] = finalClashResults;
|
||
}
|
||
LogManager.Info($"[ClashDetective] 已缓存结果:{finalClashResults.Count}个碰撞,测试名称:{_currentTestName}");
|
||
|
||
// 更新碰撞计数器
|
||
_clashDetectiveCollisionCount = clashResults.Count;
|
||
|
||
// 保存到数据库(使用去重后的结果,配置参数通过 detectionRecordId 关联获取)
|
||
SaveClashDetectiveResultToDatabase(routeId, finalClashResults, precomputedCollisionCount, detectionRecordId);
|
||
|
||
LogManager.Info($"[ClashDetective] 结果已保存到数据库");
|
||
|
||
// 第四步:将分组添加到主测试(由调用方完成)
|
||
return (collisionGroup, addedMainTest, confirmedCount, false);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 动画结束后统一创建和运行ClashDetective碰撞测试
|
||
/// </summary>
|
||
/// <param name="precomputedCollisions">预计算碰撞结果</param>
|
||
/// <param name="detectionGap">检测间隙容差</param>
|
||
/// <param name="routeId">路由ID</param>
|
||
/// <param name="animatedObject">动画对象(如果是真实物体)</param>
|
||
/// <param name="isVirtualObject">是否使用虚拟物体</param>
|
||
/// <param name="frameRate">帧率</param>
|
||
/// <param name="duration">动画时长</param>
|
||
/// <param name="virtualObjectLength">虚拟物体长度(米)</param>
|
||
/// <param name="virtualObjectWidth">虚拟物体宽度(米)</param>
|
||
/// <param name="virtualObjectHeight">虚拟物体高度(米)</param>
|
||
/// <param name="pathPoints">路径点列表(用于测试完成后恢复物体位置)</param>
|
||
/// <summary>
|
||
/// 创建并运行ClashDetective碰撞测试
|
||
/// </summary>
|
||
/// <returns>true = 检测完成,false = 用户取消</returns>
|
||
public bool CreateAllAnimationCollisionTests(
|
||
List<CollisionResult> precomputedCollisions,
|
||
double detectionGap,
|
||
string routeId,
|
||
ModelItem animatedObject,
|
||
bool isVirtualObject,
|
||
int frameRate,
|
||
double duration,
|
||
double virtualObjectLength,
|
||
double virtualObjectWidth,
|
||
double virtualObjectHeight,
|
||
List<Point3D> pathPoints = null,
|
||
int? detectionRecordId = null)
|
||
{
|
||
// 重置取消标志
|
||
_wasLastTestCanceled = false;
|
||
|
||
try
|
||
{
|
||
LogManager.Info($"=== 使用预计算碰撞数据创建ClashDetective测试(容差: {detectionGap}米)===");
|
||
|
||
LogManager.Info($"[预计算数据] 共有 {precomputedCollisions.Count} 个碰撞记录");
|
||
|
||
// 检查预计算碰撞结果是否包含位置信息
|
||
var collisionsWithPosition = precomputedCollisions.Count(c => c.HasPositionInfo && c.AnimatedObjectTrackedPosition != null);
|
||
LogManager.Info($"[预计算数据] 包含位置信息的碰撞: {collisionsWithPosition}/{precomputedCollisions.Count}");
|
||
|
||
// 直接使用所有预计算结果,只过滤有效对象(不去重)
|
||
var collisionResults = precomputedCollisions
|
||
.Where(result => IsModelItemValid(result.Item1) && IsModelItemValid(result.Item2))
|
||
.ToList();
|
||
|
||
// 记录预计算碰撞数量(作为参考值,不作为类状态)
|
||
int precomputedCollisionCount = collisionResults.Count;
|
||
LogManager.Info($"[预计算处理] 原始记录: {precomputedCollisions.Count},有效碰撞: {precomputedCollisionCount}");
|
||
|
||
// 获取动画对象
|
||
if (collisionResults.Count > 0)
|
||
{
|
||
animatedObject = collisionResults[0].Item1;
|
||
}
|
||
|
||
var doc = Application.ActiveDocument;
|
||
|
||
// 调用公共方法运行ClashDetective测试并保存到数据库
|
||
Progress progress = Application.BeginProgress("碰撞检测数据分析中,请稍候...");
|
||
ClashTest addedMainTest = null;
|
||
ClashResultGroup collisionGroup = null;
|
||
int confirmedCount = 0;
|
||
bool wasCanceled = false;
|
||
|
||
try
|
||
{
|
||
var result = RunClashDetectiveTestsAndSaveToDatabase(
|
||
precomputedCollisions,
|
||
detectionGap,
|
||
routeId,
|
||
animatedObject,
|
||
isVirtualObject,
|
||
frameRate,
|
||
duration,
|
||
virtualObjectLength,
|
||
virtualObjectWidth,
|
||
virtualObjectHeight,
|
||
progress,
|
||
precomputedCollisionCount,
|
||
detectionRecordId
|
||
);
|
||
collisionGroup = result.collisionGroup;
|
||
addedMainTest = result.addedMainTest;
|
||
confirmedCount = result.confirmedCount;
|
||
wasCanceled = result.wasCanceled;
|
||
|
||
// 检查是否被取消
|
||
if (wasCanceled)
|
||
{
|
||
LogManager.Info("[CreateAllAnimationCollisionTests] 用户取消了碰撞检测");
|
||
|
||
// 设置取消标志
|
||
_wasLastTestCanceled = true;
|
||
|
||
// 清空当前测试结果,避免取消后显示旧结果
|
||
ClearCurrentTestCache();
|
||
_currentTestName = null;
|
||
_clashDetectiveCollisionCount = 0;
|
||
|
||
// 恢复物体状态
|
||
if (animatedObject != null && IsModelItemValid(animatedObject))
|
||
{
|
||
PathAnimationManager.GetInstance().RestoreAnimatedObjectState(animatedObject);
|
||
}
|
||
|
||
// 重置动画完成标志,允许重新运行动画
|
||
ResetAnimationCompletedFlag(routeId);
|
||
}
|
||
}
|
||
finally
|
||
{
|
||
// 先激活 Navisworks 主窗口(避免关闭进度条后丢失焦点)
|
||
try
|
||
{
|
||
var mainWindow = Autodesk.Navisworks.Api.Application.Gui.MainWindow;
|
||
if (mainWindow != null)
|
||
{
|
||
// 使用 Windows API 激活窗口
|
||
SetForegroundWindow(mainWindow.Handle);
|
||
LogManager.Debug("[ClashDetective] 已激活 Navisworks 主窗口");
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogManager.Debug($"[ClashDetective] 激活主窗口失败: {ex.Message}");
|
||
}
|
||
|
||
// 关闭进度条
|
||
Application.EndProgress();
|
||
}
|
||
|
||
// 如果取消,显示提示消息后返回
|
||
if (wasCanceled)
|
||
{
|
||
// 在 finally 之后显示消息对话框
|
||
ShowCancellationMessage();
|
||
return false; // 返回取消状态
|
||
}
|
||
|
||
// UI操作:将分组添加到主测试
|
||
if (collisionGroup != null && addedMainTest != null && collisionGroup.Children.Count > 0)
|
||
{
|
||
_documentClash.TestsData.TestsAddCopy(addedMainTest, collisionGroup);
|
||
}
|
||
else
|
||
{
|
||
LogManager.Warning("[分组测试] 分组为空(未检测到真实几何碰撞),未添加到主测试");
|
||
}
|
||
|
||
// 恢复物体到碰撞检测前的状态
|
||
if (animatedObject != null && IsModelItemValid(animatedObject))
|
||
{
|
||
PathAnimationManager.GetInstance().RestoreAnimatedObjectState(animatedObject);
|
||
}
|
||
|
||
// 检查是否成功创建了主测试
|
||
var finalMainTest = _documentClash.TestsData.Tests.FirstOrDefault(t => t.DisplayName.Contains("碰撞检测")) as ClashTest;
|
||
if (finalMainTest != null)
|
||
{
|
||
// 刷新Clash Detective窗口
|
||
RefreshClashDetectiveUI();
|
||
|
||
// 自动高亮ClashDetective结果
|
||
ModelHighlightHelper.HighlightClashDetectiveResults(_currentTestName, GetCurrentPathClashResults);
|
||
LogManager.Info("自动高亮ClashDetective检测结果完成");
|
||
|
||
LogManager.Info($"=== 碰撞统计最终结果 ===");
|
||
LogManager.Info($"预计算碰撞检测点: {precomputedCollisionCount}个 (参考值)");
|
||
LogManager.Info($"Clash Detective权威结果: {_clashDetectiveCollisionCount}个碰撞 (权威数据)");
|
||
}
|
||
|
||
// 🔥 无论是否有主测试,都触发碰撞检测完成事件,通知生成报告
|
||
// 使用ClashDetective权威结果,而不是预计算结果
|
||
// 如果权威结果为0个碰撞,传入空列表以触发祝贺对话框
|
||
var finalCollisions = _clashDetectiveCollisionCount > 0 ? collisionResults : new List<CollisionResult>();
|
||
LogManager.Info($"触发CollisionDetected事件,通知生成报告({_clashDetectiveCollisionCount}个碰撞,权威数据)");
|
||
var eventArgs = new CollisionDetectedEventArgs(finalCollisions);
|
||
OnCollisionDetected(eventArgs);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogManager.Error($"动画结束后创建测试失败: {ex.Message}");
|
||
}
|
||
|
||
return !_wasLastTestCanceled; // 返回是否成功完成(未被取消)
|
||
}
|
||
|
||
/// <summary>
|
||
/// 刷新Clash Detective UI - 安全版本
|
||
/// </summary>
|
||
private void RefreshClashDetectiveUI()
|
||
{
|
||
try
|
||
{
|
||
// 检查文档是否有效
|
||
var doc = Application.ActiveDocument;
|
||
if (doc == null || doc.IsClear)
|
||
{
|
||
LogManager.Warning("文档无效,跳过UI刷新");
|
||
return;
|
||
}
|
||
|
||
// 检查视图是否有效
|
||
if (doc.ActiveView != null)
|
||
{
|
||
try
|
||
{
|
||
doc.ActiveView.RequestDelayedRedraw(ViewRedrawRequests.All);
|
||
LogManager.Info("Clash Detective UI已刷新");
|
||
}
|
||
catch (ObjectDisposedException)
|
||
{
|
||
LogManager.Warning("视图对象已释放,跳过重绘");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogManager.Error($"重绘视图失败: {ex.Message}");
|
||
}
|
||
}
|
||
else
|
||
{
|
||
LogManager.Warning("活动视图无效,跳过UI刷新");
|
||
}
|
||
}
|
||
catch (ObjectDisposedException)
|
||
{
|
||
LogManager.Warning("文档或视图对象已释放,跳过UI刷新");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogManager.Error($"刷新Clash Detective UI失败: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 按 DisplayName 在模型树中查找唯一匹配对象(F1 副实例对象映射回主模型用)。
|
||
/// 多匹配(重名)返回 null,避免误配。
|
||
/// </summary>
|
||
private static ModelItem TryFindSingleObjectByName(string displayName)
|
||
{
|
||
try
|
||
{
|
||
var document = Application.ActiveDocument;
|
||
if (document == null || document.IsClear)
|
||
{
|
||
return null;
|
||
}
|
||
|
||
ModelItem found = null;
|
||
foreach (var model in document.Models)
|
||
{
|
||
if (model.RootItem == null)
|
||
{
|
||
continue;
|
||
}
|
||
|
||
foreach (var item in model.RootItem.DescendantsAndSelf)
|
||
{
|
||
if (string.Equals(item.DisplayName, displayName, StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
if (found != null)
|
||
{
|
||
return null; // 重名 → 不匹配
|
||
}
|
||
|
||
found = item;
|
||
}
|
||
}
|
||
}
|
||
|
||
return found;
|
||
}
|
||
catch
|
||
{
|
||
return null;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 显示碰撞检测取消提示消息
|
||
/// </summary>
|
||
private void ShowCancellationMessage()
|
||
{
|
||
// 自动模式下跳过取消提示(日志已记录,避免残留弹窗阻塞自动化)
|
||
if (AutomationModeContext.IsAutomaticMode)
|
||
{
|
||
LogManager.Info("[ClashDetective] 自动模式下跳过检测取消提示框");
|
||
return;
|
||
}
|
||
|
||
try
|
||
{
|
||
// 获取 Navisworks 主窗口句柄作为父窗口
|
||
var mainWindow = System.Windows.Forms.Form.FromHandle(
|
||
Autodesk.Navisworks.Api.Application.Gui.MainWindow.Handle);
|
||
|
||
// 使用 Windows Forms 显示提示框,指定父窗口保持焦点
|
||
System.Windows.Forms.MessageBox.Show(
|
||
mainWindow,
|
||
"碰撞检测已取消。\n\n您可以重新运行动画来重新检测碰撞。",
|
||
"检测取消",
|
||
System.Windows.Forms.MessageBoxButtons.OK,
|
||
System.Windows.Forms.MessageBoxIcon.Information);
|
||
|
||
LogManager.Info("[ClashDetective] 已显示取消提示消息");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
// 如果获取父窗口失败,使用默认方式
|
||
System.Windows.Forms.MessageBox.Show(
|
||
"碰撞检测已取消。\n\n您可以重新运行动画来重新检测碰撞。",
|
||
"检测取消",
|
||
System.Windows.Forms.MessageBoxButtons.OK,
|
||
System.Windows.Forms.MessageBoxIcon.Information);
|
||
|
||
LogManager.Info("[ClashDetective] 已显示取消提示消息(无父窗口)");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 重置动画完成标志,允许重新运行动画进行碰撞检测
|
||
/// </summary>
|
||
/// <param name="routeId">路径ID</param>
|
||
private void ResetAnimationCompletedFlag(string routeId)
|
||
{
|
||
try
|
||
{
|
||
// 清除动画配置哈希到记录ID的映射,允许重新检测
|
||
PathAnimationManager.ClearAllCollisionTestRecords();
|
||
LogManager.Info($"[ClashDetective] 已清除检测记录映射,允许重新检测。路径ID: {routeId}");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogManager.Error($"重置动画完成标志失败: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 检查ModelItem是否仍然有效
|
||
/// </summary>
|
||
private bool IsModelItemValid(ModelItem item)
|
||
{
|
||
try
|
||
{
|
||
if (item == null)
|
||
return false;
|
||
|
||
// 尝试访问对象的属性来检查是否有效
|
||
var displayName = item.DisplayName;
|
||
var hasGeometry = item.HasGeometry;
|
||
|
||
// 额外检查:确保对象没有被释放
|
||
var boundingBox = item.BoundingBox();
|
||
return true;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogManager.Debug($"ModelItem无效: {ex.Message}");
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取所有碰撞检测测试
|
||
/// </summary>
|
||
/// <returns>碰撞检测测试列表</returns>
|
||
public List<ClashTest> GetClashTests()
|
||
{
|
||
if (_documentClash == null)
|
||
{
|
||
throw new InvalidOperationException("ClashDetective文档未初始化");
|
||
}
|
||
|
||
return _documentClash.TestsData.Tests.Cast<ClashTest>()
|
||
.Where(t => t.DisplayName.Contains("碰撞检测"))
|
||
.ToList();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取当前路径的Clash结果
|
||
/// </summary>
|
||
/// <param name="pathName">路径名称</param>
|
||
/// <returns>当前路径的碰撞结果列表(如果没有结果返回空列表)</returns>
|
||
public List<CollisionResult> GetCurrentPathClashResults(string testName)
|
||
{
|
||
lock (_clashResultsCacheLock)
|
||
{
|
||
// 先尝试从缓存获取
|
||
if (_clashDetectiveResultsCache.TryGetValue(testName, out var cachedResults))
|
||
{
|
||
LogManager.Debug($"[ClashDetective结果] 从缓存获取 '{testName}' 的结果:{cachedResults.Count}个碰撞");
|
||
return cachedResults;
|
||
}
|
||
|
||
// 缓存中没有,尝试从数据库加载
|
||
LogManager.Info($"[ClashDetective结果] 缓存中没有 '{testName}',尝试从数据库加载");
|
||
var loadedResults = GetClashDetectiveResultsFromDatabase(testName);
|
||
|
||
if (loadedResults != null && loadedResults.Count > 0)
|
||
{
|
||
// 缓存加载的结果
|
||
_clashDetectiveResultsCache[testName] = loadedResults;
|
||
LogManager.Info($"[ClashDetective结果] 已从数据库加载 '{testName}' 的结果并缓存:{loadedResults.Count}个碰撞");
|
||
return loadedResults;
|
||
}
|
||
|
||
// 没有找到结果,返回空列表
|
||
LogManager.Info($"[ClashDetective结果] 未找到测试 '{testName}' 的碰撞结果,返回空列表");
|
||
return new List<CollisionResult>();
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取当前测试的碰撞结果(便捷方法)
|
||
/// </summary>
|
||
/// <returns>当前测试的碰撞结果列表</returns>
|
||
public List<CollisionResult> GetCurrentTestResults()
|
||
{
|
||
return GetCurrentPathClashResults(_currentTestName);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 清理当前测试的缓存
|
||
/// </summary>
|
||
public void ClearCurrentTestCache()
|
||
{
|
||
if (string.IsNullOrEmpty(_currentTestName))
|
||
{
|
||
return;
|
||
}
|
||
|
||
lock (_clashResultsCacheLock)
|
||
{
|
||
if (_clashDetectiveResultsCache.ContainsKey(_currentTestName))
|
||
{
|
||
_clashDetectiveResultsCache.Remove(_currentTestName);
|
||
LogManager.Info($"[ClashDetective缓存] 已清理当前测试 '{_currentTestName}' 的缓存");
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 清空取消标志
|
||
/// </summary>
|
||
public void ClearWasLastTestCanceled()
|
||
{
|
||
if (_wasLastTestCanceled)
|
||
{
|
||
LogManager.Info("[ClashDetective] 清空取消标志");
|
||
_wasLastTestCanceled = false;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 清空当前测试名称
|
||
/// </summary>
|
||
public void ClearCurrentTestName()
|
||
{
|
||
if (!string.IsNullOrEmpty(_currentTestName))
|
||
{
|
||
LogManager.Info($"[ClashDetective] 清空当前测试名称: {_currentTestName}");
|
||
_currentTestName = null;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 从测试中提取碰撞结果
|
||
/// </summary>
|
||
/// <param name="clashTest">测试</param>
|
||
/// <returns>碰撞结果列表</returns>
|
||
public List<ClashResult> ExtractClashResultsFromTest(ClashTest clashTest)
|
||
{
|
||
var results = new List<ClashResult>();
|
||
ExtractClashResultsFromTestRecursive(clashTest, results);
|
||
return results;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 递归提取测试中的碰撞结果
|
||
/// </summary>
|
||
/// <param name="clashTest">测试</param>
|
||
/// <param name="results">结果列表</param>
|
||
private void ExtractClashResultsFromTestRecursive(ClashTest clashTest, List<ClashResult> results)
|
||
{
|
||
foreach (var child in clashTest.Children)
|
||
{
|
||
if (child is ClashResult clashResult)
|
||
{
|
||
results.Add(clashResult);
|
||
}
|
||
else if (child is ClashResultGroup resultGroup)
|
||
{
|
||
ExtractClashResultsFromGroupRecursive(resultGroup, results);
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 递归提取分组中的碰撞结果
|
||
/// </summary>
|
||
/// <param name="group">分组</param>
|
||
/// <param name="results">结果列表</param>
|
||
private void ExtractClashResultsFromGroupRecursive(ClashResultGroup group, List<ClashResult> results)
|
||
{
|
||
foreach (var child in group.Children)
|
||
{
|
||
if (child is ClashResult result)
|
||
{
|
||
results.Add(result);
|
||
}
|
||
else if (child is ClashResultGroup subGroup)
|
||
{
|
||
ExtractClashResultsFromGroupRecursive(subGroup, results);
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取所有碰撞检测的碰撞结果
|
||
/// </summary>
|
||
/// <returns>碰撞结果列表</returns>
|
||
public List<ClashResult> GetAllClashResults()
|
||
{
|
||
var tests = GetClashTests();
|
||
var allResults = new List<ClashResult>();
|
||
|
||
foreach (var test in tests)
|
||
{
|
||
var results = ExtractClashResultsFromTest(test);
|
||
allResults.AddRange(results);
|
||
}
|
||
|
||
return allResults;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 构建通道对象缓存,一次性扫描所有对象,避免重复的属性查询
|
||
/// </summary>
|
||
public void BuildChannelObjectsCache()
|
||
{
|
||
lock (_cacheLock)
|
||
{
|
||
if (_channelObjectsCache != null) return; // 双重检查锁定
|
||
|
||
var cacheStopwatch = new System.Diagnostics.Stopwatch();
|
||
cacheStopwatch.Start();
|
||
|
||
_channelObjectsCache = new HashSet<ModelItem>();
|
||
|
||
try
|
||
{
|
||
var document = Application.ActiveDocument;
|
||
|
||
// 获取所有可通行的物流模型项
|
||
var allChannelItems = CategoryAttributeManager.GetAllTraversableLogisticsItems();
|
||
|
||
if (allChannelItems.Count == 0)
|
||
{
|
||
LogManager.Warning("[通道缓存] ⚠️ 未找到任何可通行的物流元素,请检查模型中的物流属性设置");
|
||
cacheStopwatch.Stop();
|
||
return;
|
||
}
|
||
|
||
// 🔥 优化:只提取复合对象(IsComposite = true)
|
||
// 原因:通道对象通常是复合对象(如通道、楼梯、电梯)
|
||
// 我们只需要检测复合对象,不需要检测其子节点
|
||
foreach (var channelItem in allChannelItems)
|
||
{
|
||
try
|
||
{
|
||
// 只添加复合对象
|
||
if (channelItem.IsComposite)
|
||
{
|
||
_channelObjectsCache.Add(channelItem);
|
||
}
|
||
else if (channelItem.HasGeometry)
|
||
{
|
||
// 非复合对象但有几何数据(独立实体节点):也添加
|
||
_channelObjectsCache.Add(channelItem);
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogManager.Warning($"[通道收集] 处理通道节点 '{channelItem?.DisplayName ?? "NULL"}' 时出错: {ex.Message}");
|
||
// 出错时至少保证通道本身被添加
|
||
_channelObjectsCache.Add(channelItem);
|
||
}
|
||
}
|
||
|
||
cacheStopwatch.Stop();
|
||
LogManager.Info($"通道对象缓存构建完成,耗时: {cacheStopwatch.ElapsedMilliseconds}ms");
|
||
LogManager.Info($" - 可通行物流根对象: {allChannelItems.Count} 个");
|
||
LogManager.Info($" - 缓存总对象数: {_channelObjectsCache.Count} 个");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogManager.Error($"构建通道对象缓存时发生错误: {ex.Message}", ex);
|
||
_channelObjectsCache = new HashSet<ModelItem>(); // 创建空缓存,避免重复构建
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 清除通道对象缓存,在模型变化时调用
|
||
/// </summary>
|
||
public static void ClearChannelObjectsCache()
|
||
{
|
||
lock (_cacheLock)
|
||
{
|
||
_channelObjectsCache = null;
|
||
LogManager.Debug("通道对象缓存已清除");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 设置移动物体(用于从空间索引中排除移动物体及其所有子节点)
|
||
/// 🔥 优化:只更新运动物体后代集合,不重建基础缓存
|
||
/// </summary>
|
||
/// <param name="animatedObject">移动物体</param>
|
||
public static void SetAnimatedObject(ModelItem animatedObject)
|
||
{
|
||
lock (_cacheLock)
|
||
{
|
||
_animatedObject = animatedObject;
|
||
LogManager.Info($"移动物体已设置: {animatedObject?.DisplayName ?? "null"}");
|
||
|
||
// 🔥 优化:只重建运动物体后代集合,不清除基础缓存
|
||
_animatedObjectAndDescendants.Clear();
|
||
if (_animatedObject != null)
|
||
{
|
||
var sw = System.Diagnostics.Stopwatch.StartNew();
|
||
CollectAllDescendants(_animatedObject, _animatedObjectAndDescendants);
|
||
sw.Stop();
|
||
LogManager.Info($"移动物体后代收集完成: {_animatedObjectAndDescendants.Count} 个对象,耗时 {sw.ElapsedMilliseconds}ms");
|
||
}
|
||
|
||
// 🔥 关键:只清空派生缓存,不重建基础缓存
|
||
// 基础缓存 _allGeometryItemsCache 保持稳定
|
||
_nonChannelGeometryItemsCache = null;
|
||
LogManager.Info("非通道几何对象缓存已标记为失效,将在下次访问时重新过滤");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 递归收集对象及其所有后代
|
||
/// </summary>
|
||
private static void CollectAllDescendants(ModelItem item, HashSet<ModelItem> collection)
|
||
{
|
||
if (item == null) return;
|
||
|
||
collection.Add(item);
|
||
|
||
// 限制递归深度,防止异常模型结构
|
||
try
|
||
{
|
||
foreach (var child in item.Children)
|
||
{
|
||
CollectAllDescendants(child, collection);
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogManager.Warning($"收集对象 '{item.DisplayName}' 的后代时出错: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 清除移动物体引用(不清除基础缓存)
|
||
/// 🔥 优化:用于虚拟物体切换时,只清除引用而不重建80秒的基础缓存
|
||
/// </summary>
|
||
public static void ClearAnimatedObject()
|
||
{
|
||
lock (_cacheLock)
|
||
{
|
||
_animatedObject = null;
|
||
_animatedObjectAndDescendants.Clear();
|
||
LogManager.Debug("移动物体引用已清除(基础缓存保留)");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 初始化碰撞检测缓存(用于预计算前的准备工作)
|
||
/// 🔥 优化:基础缓存保持稳定,运动物体在查询阶段动态过滤
|
||
/// </summary>
|
||
/// <param name="animatedObject">移动物体</param>
|
||
public static void InitializeCollisionDetectionCache(ModelItem animatedObject)
|
||
{
|
||
LogManager.Info("[碰撞检测] 开始初始化碰撞检测缓存...");
|
||
|
||
lock (_cacheLock)
|
||
{
|
||
// 1. 设置运动物体(使用优化后的方法)
|
||
_animatedObject = animatedObject;
|
||
LogManager.Info($"移动物体已设置: {_animatedObject?.DisplayName ?? "null"}");
|
||
|
||
// 2. 清空运动物体后代集合并重新收集
|
||
_animatedObjectAndDescendants.Clear();
|
||
if (_animatedObject != null)
|
||
{
|
||
CollectAllDescendants(_animatedObject, _animatedObjectAndDescendants);
|
||
LogManager.Info($"运动物体后代收集完成: {_animatedObjectAndDescendants.Count} 个对象");
|
||
}
|
||
|
||
// 3. 清空派生缓存(基础缓存保持稳定)
|
||
_nonChannelGeometryItemsCache = null;
|
||
|
||
// 4. 如果通道缓存不存在,清空它以便重建
|
||
if (_channelObjectsCache == null)
|
||
{
|
||
LogManager.Info("通道对象缓存不存在,将重新构建");
|
||
}
|
||
}
|
||
|
||
// 5. 构建基础缓存(如果不存在)- 使用双重检查锁定,不会重复构建
|
||
BuildNonHidddenGeometryItemsCache();
|
||
|
||
// 6. 构建通道对象缓存(如果不存在)
|
||
if (_channelObjectsCache == null)
|
||
{
|
||
Instance.BuildChannelObjectsCache();
|
||
}
|
||
|
||
LogManager.Info("[碰撞检测] 碰撞检测缓存初始化完成");
|
||
}
|
||
|
||
/// <summary>
|
||
/// 统一准备碰撞检测(整合常规处理和批处理的公共逻辑)
|
||
/// 🔥 优化:基础缓存保持稳定,运动物体变化时快速更新
|
||
/// </summary>
|
||
/// <param name="animatedObject">运动物体</param>
|
||
/// <param name="isManualMode">是否为手工指定检测对象模式</param>
|
||
/// <param name="manualTargets">手工指定的检测目标(仅在手工模式下使用)</param>
|
||
/// <returns>是否成功准备</returns>
|
||
public static bool PrepareCollisionDetection(ModelItem animatedObject, bool isManualMode, List<ModelItem> manualTargets = null)
|
||
{
|
||
try
|
||
{
|
||
if (animatedObject == null)
|
||
{
|
||
LogManager.Warning("[碰撞检测] 准备失败:运动物体为空");
|
||
return false;
|
||
}
|
||
|
||
if (isManualMode)
|
||
{
|
||
// 手工模式:只设置移动物体,不构建全局缓存
|
||
LogManager.Info("[碰撞检测] 手工模式 - 跳过全局缓存初始化");
|
||
SetAnimatedObject(animatedObject); // 使用优化后的方法
|
||
return true;
|
||
}
|
||
else
|
||
{
|
||
// 全局模式:使用优化后的初始化
|
||
LogManager.Info("[碰撞检测] 全局模式 - 初始化碰撞检测缓存");
|
||
|
||
// 🔥 优化:如果基础缓存已存在,只更新运动物体
|
||
if (_allGeometryItemsCache != null && _channelObjectsCache != null)
|
||
{
|
||
LogManager.Info("[碰撞检测] 基础缓存已存在,仅更新运动物体");
|
||
SetAnimatedObject(animatedObject);
|
||
return true;
|
||
}
|
||
|
||
// 否则执行完整初始化
|
||
InitializeCollisionDetectionCache(animatedObject);
|
||
return true;
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogManager.Error($"[碰撞检测] 准备碰撞检测失败: {ex.Message}");
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取通道对象缓存(供外部使用)
|
||
/// </summary>
|
||
/// <returns>通道对象集合,如果缓存不存在则返回null</returns>
|
||
public static HashSet<ModelItem> GetChannelObjectsCache()
|
||
{
|
||
lock (_cacheLock)
|
||
{
|
||
return _channelObjectsCache;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 构建几何对象列表缓存,一次性获取非隐藏几何对象
|
||
/// </summary>
|
||
public static void BuildNonHidddenGeometryItemsCache()
|
||
{
|
||
lock (_cacheLock)
|
||
{
|
||
if (_allGeometryItemsCache != null) return; // 双重检查锁定
|
||
|
||
var cacheStopwatch = new System.Diagnostics.Stopwatch();
|
||
cacheStopwatch.Start();
|
||
|
||
try
|
||
{
|
||
// 优化方案:递归遍历 (Top-Down Traversal)
|
||
// 原因:Search API 的 Hidden 属性不检查父级可见性,且 IsHidden 属性检查自身。
|
||
// 解决方案:统一走 ModelItemTreeWalker 深度遍历,如果遭遇 Hidden 节点则停止深入该分支。
|
||
// 这天然地利用了 Navisworks 的层级可见性规则。
|
||
_allGeometryItemsCache = new ModelItemCollection();
|
||
int visibleCount = 0;
|
||
|
||
ModelItemTreeWalker.WalkModels(
|
||
Application.ActiveDocument.Models,
|
||
item =>
|
||
{
|
||
// 🔥 优化:只提取复合对象(IsComposite = true)或独立实体节点(HasGeometry)
|
||
// 原因:复合对象(如窗户)包含多个实体节点(玻璃、框架、把手)
|
||
// 我们只需要检测复合对象,不需要检测其子节点
|
||
// Clash Detective API 会自动处理复合对象的子节点几何体
|
||
if (item.IsComposite)
|
||
{
|
||
// 复合对象:添加到缓存,不继续遍历子节点
|
||
_allGeometryItemsCache.Add(item);
|
||
visibleCount++;
|
||
return ModelItemWalkAction.Prune;
|
||
}
|
||
|
||
if (item.HasGeometry)
|
||
{
|
||
// 非复合对象但有几何数据(独立实体节点):添加到缓存
|
||
_allGeometryItemsCache.Add(item);
|
||
visibleCount++;
|
||
return ModelItemWalkAction.Prune;
|
||
}
|
||
|
||
// 空节点:继续遍历子节点
|
||
return ModelItemWalkAction.Continue;
|
||
});
|
||
|
||
cacheStopwatch.Stop();
|
||
LogManager.Info($"复合对象列表缓存构建完成 (DFS遍历 + 自动剪枝),耗时: {cacheStopwatch.ElapsedMilliseconds}ms");
|
||
LogManager.Info($" - 缓存可见对象: {visibleCount}");
|
||
LogManager.Info($" - 策略: 深度优先搜索 (父节点隐藏则跳过分支)");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogManager.Error($"构建几何对象列表缓存时发生错误: {ex.Message}", ex);
|
||
_allGeometryItemsCache = new ModelItemCollection(); // 创建空缓存,避免重复构建
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取复合对象缓存(供外部使用)
|
||
/// </summary>
|
||
/// <returns>复合对象集合,如果缓存不存在则返回null</returns>
|
||
public static ModelItemCollection GetAllGeometryItemsCache()
|
||
{
|
||
lock (_cacheLock)
|
||
{
|
||
return _allGeometryItemsCache;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取已排除通道对象的几何对象缓存(供空间索引使用)
|
||
/// </summary>
|
||
/// <returns>排除通道后的几何对象列表,如果缓存不存在则返回null</returns>
|
||
public static List<ModelItem> GetNonChannelGeometryItemsCache()
|
||
{
|
||
lock (_cacheLock)
|
||
{
|
||
// 如果已经有过滤后的缓存,直接返回
|
||
if (_nonChannelGeometryItemsCache != null)
|
||
{
|
||
return _nonChannelGeometryItemsCache;
|
||
}
|
||
|
||
// 如果原始缓存不存在,返回 null
|
||
if (_allGeometryItemsCache == null || _channelObjectsCache == null)
|
||
{
|
||
return null;
|
||
}
|
||
|
||
// 第一次调用:构建并缓存过滤后的列表
|
||
LogManager.Info($"[空间索引] 构建非通道几何对象缓存...");
|
||
var sw = System.Diagnostics.Stopwatch.StartNew();
|
||
|
||
// 🔥 优化:动态排除通道对象和运动物体(及其后代)
|
||
_nonChannelGeometryItemsCache = _allGeometryItemsCache
|
||
.Where(item => !_channelObjectsCache.Contains(item))
|
||
.Where(item => !_animatedObjectAndDescendants.Contains(item)) // 🔥 动态排除运动物体
|
||
.ToList();
|
||
|
||
sw.Stop();
|
||
LogManager.Info($"[空间索引] 非通道几何对象缓存构建完成,耗时: {sw.ElapsedMilliseconds}ms,对象数: {_nonChannelGeometryItemsCache.Count}");
|
||
|
||
return _nonChannelGeometryItemsCache;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 清除所有缓存,在模型变化时调用
|
||
/// </summary>
|
||
public static void ClearAllCaches()
|
||
{
|
||
lock (_cacheLock)
|
||
{
|
||
_channelObjectsCache = null;
|
||
_allGeometryItemsCache = null;
|
||
_nonChannelGeometryItemsCache = null;
|
||
_animatedObject = null; // 清除移动物体引用
|
||
_animatedObjectAndDescendants.Clear(); // 清除运动物体后代集合
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 清理碰撞缓存
|
||
/// </summary>
|
||
public void ClearCollisionCache()
|
||
{
|
||
try
|
||
{
|
||
LogManager.Info("[ClashDetectiveIntegration] 清理碰撞缓存");
|
||
|
||
// 清空当前碰撞列表
|
||
_currentCollisions?.Clear();
|
||
|
||
// 清空缓存的结果
|
||
lock (_resultsLock)
|
||
{
|
||
_deduplicatedCollisionResults?.Clear();
|
||
}
|
||
|
||
// 清除对象缓存
|
||
ClearAllCaches();
|
||
|
||
// 重置权威碰撞计数器(预计算计数是局部变量,无需重置)
|
||
_clashDetectiveCollisionCount = 0;
|
||
|
||
LogManager.Info("[ClashDetectiveIntegration] 碰撞缓存已清理");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogManager.Error($"[ClashDetectiveIntegration] 清理碰撞缓存失败: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 清理资源
|
||
/// </summary>
|
||
public void Cleanup()
|
||
{
|
||
try
|
||
{
|
||
LogManager.Info("开始Clash Detective集成资源清理...");
|
||
|
||
// 检查文档是否仍然有效,如果无效则跳过操作
|
||
var document = Application.ActiveDocument;
|
||
if (document == null || document.IsClear)
|
||
{
|
||
LogManager.Info("文档已无效,跳过资源清理操作");
|
||
return;
|
||
}
|
||
|
||
try
|
||
{
|
||
ModelHighlightHelper.ClearAllHighlights();
|
||
LogManager.Info("已清除临时材质高亮");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogManager.Warning($"清除临时材质失败: {ex.Message}");
|
||
}
|
||
|
||
// 清空内存中的结果
|
||
_currentCollisions?.Clear();
|
||
lock (_resultsLock)
|
||
{
|
||
_deduplicatedCollisionResults?.Clear();
|
||
}
|
||
|
||
// 清理.NET API引用
|
||
_documentClash = null;
|
||
|
||
LogManager.Info("Clash Detective集成资源清理完成");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogManager.Error($"清理Clash Detective资源时发生异常: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 批处理版本:创建并运行ClashDetective测试(不触发UI操作)
|
||
/// 此方法专门用于批处理场景,不显示进度条,不触发UI刷新
|
||
/// </summary>
|
||
public string CreateAllAnimationCollisionTestsForBatch(
|
||
List<CollisionResult> precomputedCollisions,
|
||
double detectionGap,
|
||
string routeId,
|
||
ModelItem animatedObject,
|
||
bool isVirtualObject,
|
||
int frameRate,
|
||
double duration,
|
||
double virtualObjectLength,
|
||
double virtualObjectWidth,
|
||
double virtualObjectHeight,
|
||
List<Point3D> pathPoints = null,
|
||
int? detectionRecordId = null)
|
||
{
|
||
try
|
||
{
|
||
LogManager.Info($"[批处理] 使用预计算碰撞数据创建ClashDetective测试(容差: {detectionGap}米)");
|
||
|
||
LogManager.Info($"[批处理] 共有 {precomputedCollisions.Count} 个碰撞记录");
|
||
|
||
// 记录预计算碰撞数量(作为参考值,不作为类状态)
|
||
int precomputedCollisionCount = precomputedCollisions.Count;
|
||
LogManager.Info($"[批处理] 预计算碰撞: {precomputedCollisionCount}");
|
||
|
||
// 调用公共方法运行ClashDetective测试并保存到数据库
|
||
var result = RunClashDetectiveTestsAndSaveToDatabase(
|
||
precomputedCollisions,
|
||
detectionGap,
|
||
routeId,
|
||
animatedObject,
|
||
isVirtualObject,
|
||
frameRate,
|
||
duration,
|
||
virtualObjectLength,
|
||
virtualObjectWidth,
|
||
virtualObjectHeight,
|
||
null,
|
||
precomputedCollisionCount,
|
||
detectionRecordId
|
||
);
|
||
|
||
var collisionGroup = result.collisionGroup;
|
||
var addedMainTest = result.addedMainTest;
|
||
var confirmedCount = result.confirmedCount;
|
||
|
||
// 🔥 重要:更新ClashDetective碰撞计数为确认的碰撞数
|
||
_clashDetectiveCollisionCount = confirmedCount;
|
||
LogManager.Info($"[批处理] 更新ClashDetective碰撞计数: {confirmedCount}");
|
||
|
||
// 添加分组到主测试(使用 TestsAddCopy 方法)
|
||
if (collisionGroup != null && addedMainTest != null && collisionGroup.Children.Count > 0)
|
||
{
|
||
_documentClash.TestsData.TestsAddCopy(addedMainTest, collisionGroup);
|
||
LogManager.Info($"[批处理] 碰撞分组已添加到主测试,共 {confirmedCount} 个碰撞");
|
||
}
|
||
else
|
||
{
|
||
LogManager.Warning("[批处理] 分组为空(未检测到真实几何碰撞),未添加到主测试");
|
||
}
|
||
|
||
// 碰撞测试完成后,不再将物体恢复到路径起点(客户要求保持最终位置)
|
||
// if (animatedObject != null && IsModelItemValid(animatedObject) && pathPoints != null && pathPoints.Count > 0)
|
||
// {
|
||
// try
|
||
// {
|
||
// PathAnimationManager.GetInstance().MoveObjectToPathStart(animatedObject, pathPoints);
|
||
// LogManager.Info($"[批处理] 已将 {animatedObject.DisplayName} 恢复到路径起点位置");
|
||
// }
|
||
// catch (Exception restoreEx)
|
||
// {
|
||
// LogManager.Error($"[批处理] 恢复物体到路径起点失败: {restoreEx.Message}");
|
||
// }
|
||
// }
|
||
|
||
return addedMainTest?.DisplayName;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogManager.Error($"[批处理] 创建ClashDetective测试失败: {ex.Message}");
|
||
return null;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 触发碰撞检测事件
|
||
/// </summary>
|
||
private void OnCollisionDetected(CollisionDetectedEventArgs e)
|
||
{
|
||
CollisionDetected?.Invoke(this, e);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 碰撞结果数据结构
|
||
/// </summary>
|
||
public class CollisionResult : IEquatable<CollisionResult>
|
||
{
|
||
public Guid ClashGuid { get; set; }
|
||
public string DisplayName { get; set; }
|
||
public ClashResultStatus Status { get; set; }
|
||
public string GridLocation { get; set; }
|
||
public ModelItem Item1 { get; set; }
|
||
public ModelItem Item2 { get; set; }
|
||
public Point3D Center { get; set; }
|
||
public double Distance { get; set; }
|
||
public DateTime CreatedTime { get; set; }
|
||
|
||
// 位置和朝向信息用于还原碰撞场景
|
||
public Point3D AnimatedObjectTrackedPosition { get; set; }
|
||
public Point3D Item2Position { get; set; }
|
||
public double AnimatedObjectTrackedYawRadians { get; set; } // 动画跟踪点对应的运动物体朝向(弧度)
|
||
public Rotation3D AnimatedObjectTrackedRotation { get; set; } // 动画跟踪点对应的运动物体完整三维姿态
|
||
public bool AnimatedObjectHasTrackedRotation { get; set; }
|
||
public bool HasPositionInfo { get; set; }
|
||
|
||
// IEquatable<CollisionResult> 实现:基于碰撞对象进行去重
|
||
public bool Equals(CollisionResult other)
|
||
{
|
||
if (other == null) return false;
|
||
|
||
// 使用 ModelItem.Equals 比较底层原生对象
|
||
bool item1Equal = (Item1 == null && other.Item1 == null) ||
|
||
(Item1 != null && Item1.Equals(other.Item1));
|
||
bool item2Equal = (Item2 == null && other.Item2 == null) ||
|
||
(Item2 != null && Item2.Equals(other.Item2));
|
||
|
||
return item1Equal && item2Equal;
|
||
}
|
||
|
||
public override bool Equals(object obj)
|
||
{
|
||
return Equals(obj as CollisionResult);
|
||
}
|
||
|
||
public override int GetHashCode()
|
||
{
|
||
unchecked
|
||
{
|
||
int hash = 17;
|
||
hash = hash * 31 + (Item1?.GetHashCode() ?? 0);
|
||
hash = hash * 31 + (Item2?.GetHashCode() ?? 0);
|
||
return hash;
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 碰撞检测事件参数
|
||
/// </summary>
|
||
public class CollisionDetectedEventArgs : EventArgs
|
||
{
|
||
public List<CollisionResult> Results { get; private set; }
|
||
public int CollisionCount { get; private set; }
|
||
|
||
public CollisionDetectedEventArgs(List<CollisionResult> results)
|
||
{
|
||
Results = results;
|
||
CollisionCount = results.Count;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// ClashDetective结果保存事件参数
|
||
/// </summary>
|
||
public class ClashDetectiveResultSavedEventArgs : EventArgs
|
||
{
|
||
public string PathName { get; private set; }
|
||
public string TestName { get; private set; }
|
||
public int CollisionCount { get; private set; }
|
||
|
||
public ClashDetectiveResultSavedEventArgs(string pathName, string testName, int collisionCount)
|
||
{
|
||
PathName = pathName;
|
||
TestName = testName;
|
||
CollisionCount = collisionCount;
|
||
}
|
||
}
|
||
}
|