1143 lines
50 KiB
C#
1143 lines
50 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using System.Linq;
|
||
using System.Text;
|
||
using System.Threading;
|
||
using System.Threading.Tasks;
|
||
using Autodesk.Navisworks.Api;
|
||
using Autodesk.Navisworks.Api.Clash;
|
||
using NavisworksTransport.Core;
|
||
using NavisworksTransport.Core.Animation;
|
||
using NavisworksTransport.Utils;
|
||
using NavisworksTransport.Utils.CoordinateSystem;
|
||
using NavisworksTransport.UI.WPF.Views;
|
||
|
||
namespace NavisworksTransport.Commands
|
||
{
|
||
/// <summary>
|
||
/// 碰撞报告参数类
|
||
/// </summary>
|
||
public class CollisionReportParameters
|
||
{
|
||
/// <summary>
|
||
/// 报告类型
|
||
/// </summary>
|
||
public enum ReportType
|
||
{
|
||
/// <summary>动画碰撞报告</summary>
|
||
Animation,
|
||
/// <summary>独立碰撞检测报告</summary>
|
||
Independent,
|
||
/// <summary>综合报告(包含所有类型)</summary>
|
||
Comprehensive
|
||
}
|
||
|
||
/// <summary>
|
||
/// 要生成的报告类型
|
||
/// </summary>
|
||
public ReportType Type { get; set; } = ReportType.Comprehensive;
|
||
|
||
/// <summary>
|
||
/// 是否包含详细信息
|
||
/// </summary>
|
||
public bool IncludeDetails { get; set; } = true;
|
||
|
||
/// <summary>
|
||
/// 路径ID(用于视角调整)
|
||
/// </summary>
|
||
public string RouteId { get; set; } = string.Empty;
|
||
|
||
/// <summary>
|
||
/// 碰撞结果是否已经被高亮过了(用于避免重复高亮)
|
||
/// </summary>
|
||
public bool HasHighlighted { get; set; } = false;
|
||
|
||
/// <summary>
|
||
/// 是否显示报告窗口(批处理模式设为false,只生成报告不弹窗)
|
||
/// </summary>
|
||
public bool ShowDialog { get; set; } = true;
|
||
|
||
public PathPlanningResult ValidateParameters()
|
||
{
|
||
return PathPlanningResult.Success("参数验证通过");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 碰撞数据收集结果
|
||
/// </summary>
|
||
public class CollisionDataResult
|
||
{
|
||
public List<CollisionResult> AllCollisions { get; set; } = new List<CollisionResult>();
|
||
public int BoundingBoxTestCount { get; set; } // 包围盒测试数量
|
||
public int ClashDetectiveCollisionCount { get; set; } // Clash Detective结果数量
|
||
public int AnimationCollisions { get; set; } // 动画过程碰撞数量
|
||
public string MovingObjectInfo { get; set; } = string.Empty;
|
||
|
||
// 新增:被撞物体去重统计
|
||
public HashSet<string> UniqueCollidedObjects { get; set; } = new HashSet<string>();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 碰撞报告截图项
|
||
/// </summary>
|
||
public class CollisionReportScreenshot
|
||
{
|
||
/// <summary>截图唯一标识(UI用)</summary>
|
||
public string Id { get; set; } = Guid.NewGuid().ToString("N");
|
||
/// <summary>数据库记录Id(用于删除)</summary>
|
||
public int DatabaseId { get; set; }
|
||
/// <summary>截图文件路径</summary>
|
||
public string FilePath { get; set; }
|
||
/// <summary>图片格式 (PNG/JPG)</summary>
|
||
public string Format { get; set; }
|
||
/// <summary>图片宽度</summary>
|
||
public int Width { get; set; }
|
||
/// <summary>图片高度</summary>
|
||
public int Height { get; set; }
|
||
/// <summary>截图时间</summary>
|
||
public DateTime CaptureTime { get; set; } = DateTime.Now;
|
||
/// <summary>截图描述/备注</summary>
|
||
public string Description { get; set; }
|
||
/// <summary>排序顺序</summary>
|
||
public int SortOrder { get; set; }
|
||
}
|
||
|
||
/// <summary>
|
||
/// 碰撞报告结果类
|
||
/// </summary>
|
||
public class CollisionReportResult
|
||
{
|
||
public string ReportContent { get; set; }
|
||
public int TotalCollisions { get; set; }
|
||
public int AnimationCollisions { get; set; } // 检测点数量
|
||
public List<CollisionResult> AllCollisions { get; set; } = new List<CollisionResult>();
|
||
public long GenerationTimeMs { get; set; }
|
||
public string MovingObjectInfo { get; set; } = string.Empty;
|
||
|
||
// 新增:被撞物体清单
|
||
public List<string> CollidedObjectsList { get; set; } = new List<string>();
|
||
public int UniqueCollidedObjectsCount { get; set; }
|
||
|
||
// 新增:动画参数
|
||
public string RouteId { get; set; } // 路径ID,用于数据库关联
|
||
public int ResultId { get; set; } // 数据库记录Id,用于关联截图
|
||
public string PathName { get; set; }
|
||
public int FrameRate { get; set; }
|
||
public double Duration { get; set; }
|
||
public double DetectionTolerance { get; set; }
|
||
|
||
// 截图列表 - 支持多张截图
|
||
public List<CollisionReportScreenshot> Screenshots { get; set; } = new List<CollisionReportScreenshot>();
|
||
|
||
// 排除对象列表
|
||
public List<ExcludedObjectRecord> ExcludedObjects { get; set; } = new List<ExcludedObjectRecord>();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 生成碰撞报告Command
|
||
/// </summary>
|
||
public class GenerateCollisionReportCommand : CommandBase
|
||
{
|
||
private readonly CollisionReportParameters _parameters;
|
||
private string _specificTestName = null; // 指定的测试名称(用于历史报告)
|
||
private int? _detectionRecordId = null; // 检测记录ID(优先使用)
|
||
|
||
public GenerateCollisionReportCommand(CollisionReportParameters parameters = null)
|
||
: base("GenerateCollisionReport", "生成碰撞报告", "生成并显示详细的碰撞检测报告")
|
||
{
|
||
_parameters = parameters ?? new CollisionReportParameters();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 设置要生成报告的测试名称
|
||
/// </summary>
|
||
public void SetTestName(string testName)
|
||
{
|
||
_specificTestName = testName;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 设置检测记录ID(优先使用ID查询)
|
||
/// </summary>
|
||
public void SetDetectionRecordId(int? detectionRecordId)
|
||
{
|
||
_detectionRecordId = detectionRecordId;
|
||
}
|
||
|
||
protected override PathPlanningResult ValidateParameters()
|
||
{
|
||
return _parameters.ValidateParameters();
|
||
}
|
||
|
||
protected override async Task<PathPlanningResult> ExecuteInternalAsync(CancellationToken cancellationToken)
|
||
{
|
||
var startTime = DateTime.UtcNow;
|
||
|
||
UpdateProgress(10, "开始生成碰撞报告...");
|
||
|
||
// 获取ClashDetectiveIntegration实例
|
||
var clashIntegration = ClashDetectiveIntegration.Instance;
|
||
|
||
// 优先使用检测记录ID,其次使用测试名称
|
||
var targetTestName = _specificTestName ?? clashIntegration?.CurrentTestName;
|
||
|
||
// 获取检测记录信息(优先使用ID查询)
|
||
var testInfo = GetTestInfoFromDatabase();
|
||
if (testInfo == null && !string.IsNullOrEmpty(targetTestName))
|
||
{
|
||
// 如果ID查询失败且提供了测试名称,尝试用名称查询
|
||
testInfo = GetTestInfoFromDatabaseByName(targetTestName);
|
||
}
|
||
|
||
if (testInfo == null)
|
||
{
|
||
return PathPlanningResult.Failure("无法找到检测记录,请先运行碰撞检测或选择历史记录");
|
||
}
|
||
|
||
// 使用实际的数据库记录中的TestName
|
||
targetTestName = testInfo.TestName ?? targetTestName;
|
||
|
||
var result = new CollisionReportResult();
|
||
|
||
try
|
||
{
|
||
UpdateProgress(30, "收集碰撞数据...");
|
||
|
||
// 收集所有碰撞数据及统计信息(同步执行,避免 COM 线程问题)
|
||
var collisionData = CollectAllCollisionData(targetTestName);
|
||
result.AllCollisions = collisionData.AllCollisions;
|
||
|
||
// 修改统计数据
|
||
result.AnimationCollisions = collisionData.AnimationCollisions;
|
||
result.TotalCollisions = collisionData.ClashDetectiveCollisionCount;
|
||
result.MovingObjectInfo = collisionData.MovingObjectInfo;
|
||
|
||
// 新增:被撞物体清单
|
||
result.CollidedObjectsList = collisionData.UniqueCollidedObjects.OrderBy(x => x).ToList();
|
||
result.UniqueCollidedObjectsCount = collisionData.UniqueCollidedObjects.Count;
|
||
|
||
// 新增:从动画管理器获取动画参数
|
||
var animationManager = Core.Animation.PathAnimationManager.GetInstance();
|
||
if (animationManager != null)
|
||
{
|
||
result.FrameRate = animationManager.AnimationFrameRate;
|
||
result.Duration = animationManager.AnimationDuration;
|
||
result.DetectionTolerance = animationManager.DetectionTolerance;
|
||
}
|
||
|
||
// 从数据库记录填充路径信息
|
||
var db = PathPlanningManager.Instance?.GetPathDatabase();
|
||
var route = db?.GetPathRouteSync(testInfo.RouteId);
|
||
result.PathName = route?.Name ?? "未知路径";
|
||
result.RouteId = testInfo.RouteId;
|
||
result.ResultId = testInfo.Id;
|
||
|
||
LogManager.Info($"碰撞报告数据 - TestName={targetTestName}, ResultId={result.ResultId}, PathName={result.PathName}");
|
||
|
||
UpdateProgress(70, "生成报告内容...");
|
||
|
||
// 生成报告内容
|
||
result.ReportContent = GenerateReportContent(collisionData, _parameters.Type, _parameters.IncludeDetails);
|
||
|
||
UpdateProgress(90, "处理报告显示...");
|
||
|
||
// 如果碰撞结果还没有高亮,则执行高亮(不清除之前的高亮,避免影响其他场景)
|
||
if (!_parameters.HasHighlighted && collisionData.AllCollisions.Count > 0)
|
||
{
|
||
try
|
||
{
|
||
var highlightIntegration = ClashDetectiveIntegration.Instance;
|
||
if (highlightIntegration != null)
|
||
{
|
||
// 使用 ClashDetectiveResultsCategory(深红色),不是预计算高亮
|
||
ModelHighlightHelper.ManageCollisionHighlightsByCategory(
|
||
ModelHighlightHelper.ClashDetectiveResultsCategory,
|
||
collisionData.AllCollisions,
|
||
clearOtherCategories: false);
|
||
LogManager.Info($"碰撞报告生成前高亮 {collisionData.AllCollisions.Count} 个碰撞对象");
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogManager.Error($"高亮碰撞对象失败: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
// 保存原始视角
|
||
Viewpoint originalViewpoint = null;
|
||
try
|
||
{
|
||
originalViewpoint = ViewpointHelper.SaveCurrentViewpoint();
|
||
LogManager.Info("原始视角已保存");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogManager.Error($"保存原始视角失败: {ex.Message}");
|
||
}
|
||
|
||
// 智能调整视角(确保所有碰撞都在视野内)
|
||
try
|
||
{
|
||
// 从所有路径中查找当前路径
|
||
var allRoutes = PathPlanningManager.Instance.GetAllRoutes();
|
||
var pathRoute = allRoutes.FirstOrDefault(r => r.Id == _parameters.RouteId);
|
||
|
||
ViewpointHelper.AdjustViewpointSmart(pathRoute, collisionData.AllCollisions);
|
||
|
||
LogManager.Info("视角调整完成");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogManager.Error($"自动调整视角失败: {ex.Message}");
|
||
// 视角调整失败不影响截图生成
|
||
}
|
||
|
||
UpdateProgress(95, "加载碰撞报告截图...");
|
||
|
||
// 加载多截图数据(从新的截图表)
|
||
bool loadedScreenshotsFromDatabase = false;
|
||
var pathDatabase = PathPlanningManager.Instance?.GetPathDatabase();
|
||
if (pathDatabase != null)
|
||
{
|
||
try
|
||
{
|
||
// 先获取结果记录以获取 DetectionRecordId
|
||
var testRecord = pathDatabase.GetDetectionResultByTestName(targetTestName);
|
||
if (testRecord != null)
|
||
{
|
||
LogManager.Debug($"加载截图 - 找到测试记录: TestName={targetTestName}, DetectionRecordId={testRecord.Id}");
|
||
|
||
// 从截图表加载所有截图
|
||
var screenshotRecords = pathDatabase.GetCollisionReportScreenshots(testRecord.Id);
|
||
LogManager.Debug($"加载截图 - 查询到 {screenshotRecords?.Count ?? 0} 张截图 (ResultId={testRecord.Id})");
|
||
|
||
if (screenshotRecords != null && screenshotRecords.Count > 0)
|
||
{
|
||
result.Screenshots = screenshotRecords.Select(r => new CollisionReportScreenshot
|
||
{
|
||
DatabaseId = r.Id, // 数据库主键,用于删除
|
||
FilePath = r.FilePath,
|
||
Format = r.Format,
|
||
Width = r.Width,
|
||
Height = r.Height,
|
||
CaptureTime = r.CaptureTime,
|
||
Description = r.Description,
|
||
SortOrder = r.SortOrder
|
||
}).ToList();
|
||
loadedScreenshotsFromDatabase = true;
|
||
LogManager.Info($"从数据库加载了 {result.Screenshots.Count} 张截图 (ResultId={testRecord.Id})");
|
||
}
|
||
|
||
// 加载排除对象列表
|
||
try
|
||
{
|
||
var excludedObjects = pathDatabase.GetExcludedObjectsForDetectionRecord(testRecord.Id);
|
||
if (excludedObjects != null && excludedObjects.Count > 0)
|
||
{
|
||
result.ExcludedObjects = excludedObjects;
|
||
LogManager.Info($"从数据库加载了 {excludedObjects.Count} 个排除对象 (ResultId={testRecord.Id})");
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogManager.Error($"加载排除对象列表失败: {ex.Message}");
|
||
}
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogManager.Error($"加载截图数据失败: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
// 检查并创建路径子目录(用于存放该路径的所有截图)
|
||
string pathDir = System.IO.Path.Combine(PathHelper.GetScreenshotDirectory(),
|
||
string.Format("{0}_{1}", result.RouteId.Substring(0, 8), result.PathName ?? "collision"));
|
||
PathHelper.EnsureDirectoryExists(pathDir);
|
||
|
||
// 先生成默认截图(整条路径的概览)
|
||
if (result.Screenshots == null || result.Screenshots.Count == 0)
|
||
{
|
||
try
|
||
{
|
||
UpdateProgress(92, "生成默认路径截图...");
|
||
|
||
// 记录截图前虚拟物体位置和动画管理器状态。
|
||
// 不读取 ModelItem.Transform 的“实际朝向”,因为 override 后该值不可靠,容易误导排查。
|
||
var virtualObject = VirtualObjectManager.Instance.CurrentVirtualObject;
|
||
if (virtualObject != null)
|
||
{
|
||
var pam = PathAnimationManager.GetInstance();
|
||
var position = GetAnimatedObjectReportedPosition(virtualObject, pam);
|
||
LogManager.Info(string.Format("[默认截图前] 虚拟物体位置: ({0:F2},{1:F2},{2:F2}), PAM记录朝向: {3:F2}°, customRotation={4}",
|
||
position.X, position.Y, position.Z,
|
||
pam.CurrentYaw * 180 / Math.PI, pam.HasTrackedRotation));
|
||
}
|
||
else
|
||
{
|
||
LogManager.Warning("[默认截图前] 无法获取虚拟物体");
|
||
}
|
||
|
||
// 获取当前视窗尺寸
|
||
var generator = new NavigationMapGenerator();
|
||
var viewportSize = generator.GetCurrentViewportSize();
|
||
int screenshotWidth = viewportSize.Width;
|
||
int screenshotHeight = viewportSize.Height;
|
||
|
||
string screenshotPath = PathHelper.GenerateSceneScreenshotToDirectory(
|
||
pathDir,
|
||
"path",
|
||
screenshotWidth,
|
||
screenshotHeight,
|
||
System.Drawing.Imaging.ImageFormat.Jpeg,
|
||
"overview"
|
||
);
|
||
|
||
// 记录截图后虚拟物体位置和动画管理器状态。
|
||
virtualObject = VirtualObjectManager.Instance.CurrentVirtualObject;
|
||
if (virtualObject != null)
|
||
{
|
||
var pam = PathAnimationManager.GetInstance();
|
||
var position = GetAnimatedObjectReportedPosition(virtualObject, pam);
|
||
LogManager.Info(string.Format("[默认截图后] 虚拟物体位置: ({0:F2},{1:F2},{2:F2}), PAM记录朝向: {3:F2}°, customRotation={4}",
|
||
position.X, position.Y, position.Z,
|
||
pam.CurrentYaw * 180 / Math.PI, pam.HasTrackedRotation));
|
||
}
|
||
|
||
if (screenshotPath != null)
|
||
{
|
||
// 保存截图到数据库
|
||
int screenshotId = 0;
|
||
if (pathDatabase != null && result.ResultId > 0)
|
||
{
|
||
try
|
||
{
|
||
screenshotId = pathDatabase.SaveCollisionReportScreenshot(
|
||
result.ResultId,
|
||
screenshotPath,
|
||
"JPG",
|
||
screenshotWidth,
|
||
screenshotHeight,
|
||
0,
|
||
"路径概览"
|
||
);
|
||
LogManager.Info(string.Format("默认截图已保存到数据库: ScreenshotId={0}", screenshotId));
|
||
}
|
||
catch (Exception dbEx)
|
||
{
|
||
LogManager.Error(string.Format("保存默认截图到数据库失败: {0}", dbEx.Message));
|
||
}
|
||
}
|
||
|
||
result.Screenshots = new List<CollisionReportScreenshot>
|
||
{
|
||
new CollisionReportScreenshot
|
||
{
|
||
DatabaseId = screenshotId, // 设置数据库Id
|
||
FilePath = screenshotPath,
|
||
Format = "JPG",
|
||
Width = screenshotWidth,
|
||
Height = screenshotHeight,
|
||
CaptureTime = DateTime.Now,
|
||
Description = "路径概览",
|
||
SortOrder = 0
|
||
}
|
||
};
|
||
LogManager.Info(string.Format("生成默认截图: {0}", screenshotPath));
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogManager.Error(string.Format("生成默认截图失败: {0}", ex.Message));
|
||
}
|
||
}
|
||
|
||
// 再生成每个碰撞对的单独截图(需要移动物体到碰撞位置)
|
||
// 只有未从数据库加载截图时才生成
|
||
if (!loadedScreenshotsFromDatabase && collisionData.AllCollisions?.Count > 0)
|
||
{
|
||
try
|
||
{
|
||
UpdateProgress(94, string.Format("正在为 {0} 个碰撞对生成视角截图...", collisionData.AllCollisions.Count));
|
||
|
||
// 获取当前视窗尺寸
|
||
var generator2 = new NavigationMapGenerator();
|
||
var viewportSize2 = generator2.GetCurrentViewportSize();
|
||
int collisionScreenshotWidth = viewportSize2.Width;
|
||
int collisionScreenshotHeight = viewportSize2.Height;
|
||
|
||
int sortOrder = result.Screenshots?.Count ?? 0;
|
||
int successCount = 0;
|
||
|
||
// 保存动画物体原始状态(用于截图后恢复)
|
||
// 对受 PathAnimationManager 控制的对象,直接复用与 ClashDetective 相同的保存/恢复主链路,
|
||
// 避免报告生成额外引入第二套状态语义。
|
||
ModelItem animatedObject = null;
|
||
ModelItemTransformHelper.ObjectStateSnapshot savedObjectState = null;
|
||
bool restoreViaAnimationManager = false;
|
||
|
||
// 找到第一个有位置信息的碰撞来获取动画物体
|
||
foreach (var c in collisionData.AllCollisions)
|
||
{
|
||
if (c.Item1 != null && c.HasPositionInfo)
|
||
{
|
||
animatedObject = c.Item1;
|
||
var pam = PathAnimationManager.GetInstance();
|
||
restoreViaAnimationManager = pam != null && pam.ControlsAnimatedObject(animatedObject);
|
||
|
||
if (restoreViaAnimationManager)
|
||
{
|
||
pam.SaveObjectState(animatedObject);
|
||
LogManager.Info(string.Format("已通过动画主链路保存物体状态: {0}", animatedObject.DisplayName));
|
||
}
|
||
else
|
||
{
|
||
savedObjectState = CollisionSceneHelper.SaveAnimatedObjectState(animatedObject);
|
||
}
|
||
break;
|
||
}
|
||
}
|
||
|
||
for (int i = 0; i < collisionData.AllCollisions.Count; i++)
|
||
{
|
||
var collision = collisionData.AllCollisions[i];
|
||
try
|
||
{
|
||
// 使用通用方法移动物体到碰撞位置并聚焦
|
||
CollisionSceneHelper.MoveToCollisionAndFocus(collision, animatedObject);
|
||
|
||
// 生成截图
|
||
string screenshotPath = PathHelper.GenerateSceneScreenshotToDirectory(
|
||
pathDir, "collision", collisionScreenshotWidth, collisionScreenshotHeight,
|
||
System.Drawing.Imaging.ImageFormat.Jpeg, string.Format("collision_{0:D3}", i + 1));
|
||
|
||
if (screenshotPath != null)
|
||
{
|
||
string item1Name = collision.Item1 != null ? collision.Item1.DisplayName : "物体1";
|
||
string item2Name = collision.Item2 != null ? collision.Item2.DisplayName : "物体2";
|
||
string description = string.Format("碰撞对 {0}/{1}: {2} vs {3}", i + 1, collisionData.AllCollisions.Count, item1Name, item2Name);
|
||
|
||
// 先保存到数据库获取Id
|
||
int screenshotId = 0;
|
||
if (pathDatabase != null && result.ResultId > 0)
|
||
{
|
||
try
|
||
{
|
||
screenshotId = pathDatabase.SaveCollisionReportScreenshot(result.ResultId, screenshotPath, "JPG",
|
||
collisionScreenshotWidth, collisionScreenshotHeight, sortOrder, description);
|
||
}
|
||
catch (Exception dbEx)
|
||
{
|
||
LogManager.Error(string.Format("保存碰撞对截图到数据库失败: {0}", dbEx.Message));
|
||
}
|
||
}
|
||
|
||
var screenshot = new CollisionReportScreenshot
|
||
{
|
||
DatabaseId = screenshotId, // 设置数据库Id
|
||
FilePath = screenshotPath,
|
||
Format = "JPG",
|
||
Width = collisionScreenshotWidth,
|
||
Height = collisionScreenshotHeight,
|
||
CaptureTime = DateTime.Now,
|
||
Description = description,
|
||
SortOrder = sortOrder++
|
||
};
|
||
result.Screenshots.Add(screenshot);
|
||
|
||
successCount++;
|
||
}
|
||
|
||
// 截图后立即处理下一张,无需额外延迟
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogManager.Error(string.Format("生成碰撞对 {0} 截图失败: {1}", i + 1, ex.Message));
|
||
}
|
||
}
|
||
|
||
// 恢复动画物体到原始状态
|
||
if (animatedObject != null)
|
||
{
|
||
var pam = PathAnimationManager.GetInstance();
|
||
if (restoreViaAnimationManager && pam != null && pam.ControlsAnimatedObject(animatedObject))
|
||
{
|
||
pam.RestoreAnimatedObjectState(animatedObject);
|
||
LogManager.Info(string.Format("已通过动画主链路恢复物体状态: {0}", animatedObject.DisplayName));
|
||
}
|
||
else
|
||
{
|
||
CollisionSceneHelper.RestoreAnimatedObjectState(animatedObject, savedObjectState);
|
||
}
|
||
}
|
||
|
||
LogManager.Info(string.Format("已生成 {0}/{1} 个碰撞对视角截图", successCount, collisionData.AllCollisions.Count));
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogManager.Error(string.Format("生成碰撞对视角截图失败: {0}", ex.Message));
|
||
}
|
||
}
|
||
|
||
// 恢复原始视角
|
||
if (originalViewpoint != null)
|
||
{
|
||
try
|
||
{
|
||
ViewpointHelper.RestoreViewpoint(originalViewpoint);
|
||
LogManager.Info("原始视角已恢复");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogManager.Error($"恢复原始视角失败: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
UpdateProgress(100, "报告生成完成");
|
||
|
||
// 显示报告(UI线程),根据参数决定是否弹窗
|
||
if (_parameters.ShowDialog)
|
||
{
|
||
ShowReport(result);
|
||
}
|
||
|
||
// 🔥 自动导出HTML报告
|
||
try
|
||
{
|
||
string htmlPath = Utils.CollisionReportHtmlGenerator.AutoSaveHtmlReport(result);
|
||
if (!string.IsNullOrEmpty(htmlPath))
|
||
{
|
||
LogManager.Info($"HTML报告已自动导出: {htmlPath}");
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogManager.Error($"自动导出HTML报告失败: {ex.Message}");
|
||
// HTML导出失败不影响报告生成
|
||
}
|
||
}
|
||
catch (OperationCanceledException)
|
||
{
|
||
return PathPlanningResult.Failure("报告生成被取消");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogManager.Error($"生成碰撞报告失败: {ex.Message}");
|
||
return PathPlanningResult.Failure($"报告生成失败: {ex.Message}");
|
||
}
|
||
|
||
result.GenerationTimeMs = (long)(DateTime.UtcNow - startTime).TotalMilliseconds;
|
||
|
||
var message = result.TotalCollisions > 0 ?
|
||
$"碰撞报告生成完成:发现 {result.TotalCollisions} 个碰撞" :
|
||
"碰撞报告生成完成:未发现碰撞";
|
||
|
||
return PathPlanningResult<CollisionReportResult>.Success(result, message);
|
||
}
|
||
|
||
private static Point3D GetAnimatedObjectReportedPosition(ModelItem animatedObject, PathAnimationManager pam)
|
||
{
|
||
if (animatedObject == null)
|
||
{
|
||
return new Point3D(0, 0, 0);
|
||
}
|
||
|
||
if (pam != null && pam.ControlsAnimatedObject(animatedObject))
|
||
{
|
||
return pam.GetObjectCurrentPosition(animatedObject).Position;
|
||
}
|
||
|
||
var bounds = animatedObject.BoundingBox();
|
||
return bounds?.Center ?? new Point3D(0, 0, 0);
|
||
}
|
||
|
||
|
||
|
||
/// <summary>
|
||
/// 收集所有碰撞数据及统计信息
|
||
/// </summary>
|
||
private CollisionDataResult CollectAllCollisionData(string targetTestName = null)
|
||
{
|
||
var result = new CollisionDataResult();
|
||
var allCollisions = new List<CollisionResult>();
|
||
|
||
try
|
||
{
|
||
LogManager.Info("开始收集所有碰撞数据及统计信息...");
|
||
|
||
var clashIntegration = ClashDetectiveIntegration.Instance;
|
||
if (clashIntegration == null)
|
||
{
|
||
throw new InvalidOperationException("无法获取ClashDetectiveIntegration实例");
|
||
}
|
||
|
||
// 从动画管理器获取当前路径名称
|
||
var animationManager = Core.Animation.PathAnimationManager.GetInstance();
|
||
var currentPathName = animationManager?.PathName;
|
||
|
||
// 如果未提供targetTestName,尝试从字段获取
|
||
if (string.IsNullOrEmpty(targetTestName))
|
||
{
|
||
targetTestName = _specificTestName ?? clashIntegration?.CurrentTestName;
|
||
}
|
||
|
||
if (string.IsNullOrEmpty(targetTestName))
|
||
{
|
||
throw new InvalidOperationException("无法确定测试名称,请先运行碰撞检测或选择历史记录");
|
||
}
|
||
|
||
// 获取碰撞结果(先检查缓存,没有则从数据库加载)
|
||
var testCollisionsRaw = clashIntegration.GetCurrentPathClashResults(targetTestName);
|
||
|
||
if (testCollisionsRaw == null || testCollisionsRaw.Count == 0)
|
||
{
|
||
LogManager.Info($"测试 '{targetTestName}' 没有碰撞结果,返回空数据");
|
||
result.AllCollisions = allCollisions;
|
||
result.ClashDetectiveCollisionCount = 0;
|
||
result.AnimationCollisions = 0;
|
||
result.UniqueCollidedObjects = new HashSet<string>();
|
||
result.BoundingBoxTestCount = 0;
|
||
result.MovingObjectInfo = "无碰撞";
|
||
return result;
|
||
}
|
||
|
||
LogManager.Info($"从缓存获取ClashDetective结果: {testCollisionsRaw.Count}个碰撞,测试名称:{targetTestName}");
|
||
|
||
// 直接使用缓存中的CollisionResult对象(已经过复合对象处理和去重)
|
||
allCollisions.AddRange(testCollisionsRaw);
|
||
|
||
// 统计碰撞总数
|
||
result.ClashDetectiveCollisionCount = testCollisionsRaw.Count;
|
||
LogManager.Debug($"测试 '{targetTestName}' 包含 {testCollisionsRaw.Count} 个碰撞");
|
||
|
||
// 统计被撞物体(缓存中已经是复合对象,不需要再去重)
|
||
var nameCountDict = new Dictionary<string, int>();
|
||
var uniqueCollidedObjectsList = new List<string>();
|
||
|
||
foreach (var collision in testCollisionsRaw)
|
||
{
|
||
if (collision.Item2 != null)
|
||
{
|
||
// Item2已经是容器对象(在创建CollisionResult时已用FindNamedParentContainer处理)
|
||
var item2Name = ModelItemAnalysisHelper.GetSafeDisplayName(collision.Item2) ?? "未知对象";
|
||
|
||
// 为相同名称的对象添加编号区分
|
||
if (nameCountDict.ContainsKey(item2Name))
|
||
{
|
||
nameCountDict[item2Name]++;
|
||
uniqueCollidedObjectsList.Add($"{item2Name} #{nameCountDict[item2Name]}");
|
||
}
|
||
else
|
||
{
|
||
nameCountDict[item2Name] = 1;
|
||
uniqueCollidedObjectsList.Add(item2Name);
|
||
}
|
||
}
|
||
}
|
||
|
||
result.AllCollisions = allCollisions;
|
||
result.UniqueCollidedObjects = new HashSet<string>(uniqueCollidedObjectsList);
|
||
result.BoundingBoxTestCount = 0;
|
||
|
||
// 动画碰撞数量已弃用,始终设为0(预计算碰撞数现在作为参数传递)
|
||
result.AnimationCollisions = 0;
|
||
|
||
// 从碰撞结果的Item1中获取运动构件信息
|
||
if (allCollisions.Count > 0 && allCollisions[0].Item1 != null)
|
||
{
|
||
// Item1已经是容器对象(在创建CollisionResult时已用FindNamedParentContainer处理)
|
||
var movingItemName = ModelItemAnalysisHelper.GetSafeDisplayName(allCollisions[0].Item1) ?? "未知运动构件";
|
||
result.MovingObjectInfo = $"运动构件: {movingItemName}";
|
||
}
|
||
else
|
||
{
|
||
result.MovingObjectInfo = "运动构件: 未知";
|
||
}
|
||
|
||
LogManager.Info($"收集完成 - Clash Detective权威结果: {result.ClashDetectiveCollisionCount}个, 碰撞对象: {allCollisions.Count}个, 被撞构件: {uniqueCollidedObjectsList.Count}个");
|
||
|
||
return result;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogManager.Error($"收集碰撞数据失败: {ex.Message}");
|
||
result.AllCollisions = allCollisions;
|
||
return result;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 从数据库获取路径名称(同步方法,避免在主线程中使用await造成死锁)
|
||
/// </summary>
|
||
private string GetPathNameFromDatabase(string testName)
|
||
{
|
||
var pathDatabase = PathPlanningManager.Instance?.GetPathDatabase();
|
||
if (pathDatabase == null)
|
||
{
|
||
LogManager.Warning("无法获取PathDatabase实例");
|
||
return null;
|
||
}
|
||
|
||
try
|
||
{
|
||
var testInfo = pathDatabase.GetDetectionResultByTestName(testName);
|
||
if (testInfo != null)
|
||
{
|
||
// 通过 RouteId 获取路径名称
|
||
var route = pathDatabase.GetPathRouteSync(testInfo.RouteId);
|
||
return route?.Name;
|
||
}
|
||
return null;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogManager.Error($"从数据库获取路径名称失败: {ex.Message}");
|
||
return null;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 从数据库获取测试信息(优先使用检测记录ID)
|
||
/// </summary>
|
||
private CollisionDetectionRecord GetTestInfoFromDatabase()
|
||
{
|
||
var pathDatabase = PathPlanningManager.Instance?.GetPathDatabase();
|
||
if (pathDatabase == null)
|
||
{
|
||
LogManager.Warning("无法获取PathDatabase实例");
|
||
return null;
|
||
}
|
||
|
||
// 优先使用检测记录ID查询
|
||
if (_detectionRecordId.HasValue)
|
||
{
|
||
try
|
||
{
|
||
var record = pathDatabase.GetDetectionResultById(_detectionRecordId.Value);
|
||
if (record != null)
|
||
{
|
||
LogManager.Info($"使用检测记录ID查询成功: Id={_detectionRecordId}");
|
||
return record;
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogManager.Error($"使用ID查询检测记录失败: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 从数据库获取测试信息(按测试名称查询,作为后备)
|
||
/// </summary>
|
||
private CollisionDetectionRecord GetTestInfoFromDatabaseByName(string testName)
|
||
{
|
||
var pathDatabase = PathPlanningManager.Instance?.GetPathDatabase();
|
||
if (pathDatabase == null)
|
||
{
|
||
LogManager.Warning("无法获取PathDatabase实例");
|
||
return null;
|
||
}
|
||
|
||
if (string.IsNullOrEmpty(testName))
|
||
{
|
||
return null;
|
||
}
|
||
|
||
try
|
||
{
|
||
return pathDatabase.GetDetectionResultByTestName(testName);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogManager.Error($"从数据库获取测试信息失败: {ex.Message}");
|
||
return null;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 生成报告内容
|
||
/// </summary>
|
||
private string GenerateReportContent(CollisionDataResult collisionData,
|
||
CollisionReportParameters.ReportType reportType, bool includeDetails)
|
||
{
|
||
var report = new StringBuilder();
|
||
var now = DateTime.Now;
|
||
var collisions = collisionData.AllCollisions;
|
||
|
||
// 报告头部
|
||
report.AppendLine("========================================");
|
||
report.AppendLine(" NavisworksTransport 碰撞检测报告");
|
||
report.AppendLine("========================================");
|
||
report.AppendLine($"生成时间: {now:yyyy年MM月dd日 HH:mm:ss}");
|
||
report.AppendLine($"报告类型: {GetReportTypeName(reportType)}");
|
||
|
||
// 运动构件信息(原运动物体信息)
|
||
if (!string.IsNullOrEmpty(collisionData.MovingObjectInfo))
|
||
{
|
||
var movingInfo = collisionData.MovingObjectInfo.Replace("运动对象", "运动构件");
|
||
report.AppendLine($"{movingInfo}");
|
||
}
|
||
report.AppendLine();
|
||
|
||
// 总体统计
|
||
report.AppendLine("=== 总体统计 ===");
|
||
|
||
report.AppendLine($"总碰撞数: {collisionData.ClashDetectiveCollisionCount}个");
|
||
report.AppendLine($"碰撞构件: {collisionData.UniqueCollidedObjects.Count}个");
|
||
report.AppendLine();
|
||
|
||
// 碰撞构件清单(新增)
|
||
if (collisionData.UniqueCollidedObjects?.Count > 0)
|
||
{
|
||
report.AppendLine("=== 碰撞构件清单 ===");
|
||
int index = 1;
|
||
foreach (var objName in collisionData.UniqueCollidedObjects.OrderBy(x => x).Take(20)) // 限制显示前20个
|
||
{
|
||
report.AppendLine($" {index}. {objName}");
|
||
index++;
|
||
}
|
||
if (collisionData.UniqueCollidedObjects.Count > 20)
|
||
{
|
||
report.AppendLine($" ... 还有 {collisionData.UniqueCollidedObjects.Count - 20} 个构件");
|
||
}
|
||
report.AppendLine();
|
||
}
|
||
|
||
// 详细信息
|
||
if (includeDetails && collisions.Count > 0)
|
||
{
|
||
report.AppendLine("=== 详细碰撞信息 ===");
|
||
|
||
report.AppendLine("--- 碰撞详情 ---");
|
||
foreach (var collision in collisions.Take(10)) // 限制显示数量
|
||
{
|
||
AppendCollisionDetail(report, collision);
|
||
}
|
||
if (collisions.Count > 10)
|
||
{
|
||
report.AppendLine($"... 还有 {collisions.Count - 10} 个碰撞未显示");
|
||
}
|
||
report.AppendLine();
|
||
}
|
||
|
||
// 建议和结论
|
||
report.AppendLine("=== 建议与结论 ===");
|
||
if (collisions.Count == 0)
|
||
{
|
||
report.AppendLine("✅ 未发现碰撞问题,路径规划良好。");
|
||
}
|
||
else
|
||
{
|
||
report.AppendLine($"⚠️ 发现 {collisions.Count} 个碰撞问题,建议采取以下措施:");
|
||
report.AppendLine(" 1. 调整路径规划,避开碰撞区域");
|
||
report.AppendLine(" 2. 检查物体尺寸和安全边距设置");
|
||
report.AppendLine(" 3. 考虑增加临时障碍物标记");
|
||
report.AppendLine(" 4. 重新评估物流流程设计");
|
||
|
||
report.AppendLine(" 5. 建议进行更详细的碰撞检测分析");
|
||
}
|
||
|
||
report.AppendLine();
|
||
report.AppendLine("========================================");
|
||
report.AppendLine($"报告生成完成 - {now:HH:mm:ss}");
|
||
report.AppendLine("========================================");
|
||
|
||
return report.ToString();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 添加碰撞详细信息到报告中
|
||
/// </summary>
|
||
private void AppendCollisionDetail(StringBuilder report, CollisionResult collision)
|
||
{
|
||
var item1Name = collision.Item1?.DisplayName ?? "未知对象";
|
||
var item2Name = collision.Item2?.DisplayName ?? "未知对象";
|
||
|
||
// 转换距离单位为米
|
||
var distanceInMeters = ConvertDistanceToMeters(collision.Distance);
|
||
|
||
report.AppendLine($"• {item1Name} ↔ {item2Name}");
|
||
report.AppendLine($" 状态: {GetStatusName(collision.Status)}, 距离: {distanceInMeters:F3}m");
|
||
|
||
// 转换位置坐标单位为米
|
||
var centerInMeters = ConvertPointToMeters(collision.Center);
|
||
report.AppendLine($" 位置: ({centerInMeters.X:F2}, {centerInMeters.Y:F2}, {centerInMeters.Z:F2})m");
|
||
report.AppendLine($" GUID: {collision.ClashGuid}");
|
||
report.AppendLine();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取报告类型名称
|
||
/// </summary>
|
||
private string GetReportTypeName(CollisionReportParameters.ReportType reportType)
|
||
{
|
||
switch (reportType)
|
||
{
|
||
case CollisionReportParameters.ReportType.Animation:
|
||
return "动画碰撞报告";
|
||
case CollisionReportParameters.ReportType.Independent:
|
||
return "独立检测报告";
|
||
case CollisionReportParameters.ReportType.Comprehensive:
|
||
return "综合碰撞报告";
|
||
default:
|
||
return "未知类型";
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取碰撞状态名称
|
||
/// </summary>
|
||
private string GetStatusName(ClashResultStatus status)
|
||
{
|
||
switch (status)
|
||
{
|
||
case ClashResultStatus.New:
|
||
return "新发现";
|
||
case ClashResultStatus.Active:
|
||
return "活跃";
|
||
case ClashResultStatus.Reviewed:
|
||
return "已审阅";
|
||
case ClashResultStatus.Approved:
|
||
return "已批准";
|
||
case ClashResultStatus.Resolved:
|
||
return "已解决";
|
||
default:
|
||
return "未知";
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 显示报告
|
||
/// </summary>
|
||
private void ShowReport(CollisionReportResult reportResult)
|
||
{
|
||
try
|
||
{
|
||
// 确保在UI线程中执行
|
||
if (System.Windows.Application.Current != null)
|
||
{
|
||
if (System.Windows.Application.Current.Dispatcher.CheckAccess())
|
||
{
|
||
// 已在UI线程上
|
||
ShowReportInternal(reportResult);
|
||
}
|
||
else
|
||
{
|
||
// 切换到UI线程
|
||
System.Windows.Application.Current.Dispatcher.Invoke(() => ShowReportInternal(reportResult));
|
||
}
|
||
}
|
||
else
|
||
{
|
||
// 如果WPF Application不存在,回退到原来的方式
|
||
ShowReportInternal(reportResult);
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogManager.Error($"显示报告失败: {ex.Message}");
|
||
System.Windows.MessageBox.Show($"显示报告失败: {ex.Message}", "错误",
|
||
System.Windows.MessageBoxButton.OK, System.Windows.MessageBoxImage.Error);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 内部报告显示方法
|
||
/// </summary>
|
||
private void ShowReportInternal(CollisionReportResult reportResult)
|
||
{
|
||
try
|
||
{
|
||
// 使用新的WPF碰撞报告对话框
|
||
var reportDialog = CollisionReportDialog.ShowReport(reportResult);
|
||
|
||
if (reportDialog != null)
|
||
{
|
||
LogManager.Info("WPF碰撞报告对话框显示成功");
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogManager.Error($"显示WPF报告对话框失败: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 创建综合报告命令
|
||
/// </summary>
|
||
/// <param name="routeId">路径ID(用于视角调整),如果为空则使用全局CurrentRoute</param>
|
||
/// <param name="hasHighlighted">碰撞结果是否已经被高亮</param>
|
||
/// <param name="showDialog">是否显示报告窗口(批处理模式设为false)</param>
|
||
public static GenerateCollisionReportCommand CreateComprehensive(string routeId = null, bool hasHighlighted = false, bool showDialog = true)
|
||
{
|
||
// 如果没有传入路径ID,则从全局CurrentRoute获取
|
||
if (string.IsNullOrEmpty(routeId))
|
||
{
|
||
try
|
||
{
|
||
var currentRoute = PathPlanningManager.Instance?.CurrentRoute;
|
||
if (currentRoute != null)
|
||
{
|
||
routeId = currentRoute.Id;
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogManager.Error($"获取当前路径ID失败: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
return new GenerateCollisionReportCommand(new CollisionReportParameters
|
||
{
|
||
Type = CollisionReportParameters.ReportType.Comprehensive,
|
||
IncludeDetails = true,
|
||
HasHighlighted = hasHighlighted,
|
||
RouteId = routeId,
|
||
ShowDialog = showDialog
|
||
});
|
||
}
|
||
|
||
/// <summary>
|
||
/// 将距离转换为米
|
||
/// </summary>
|
||
private double ConvertDistanceToMeters(double distance)
|
||
{
|
||
try
|
||
{
|
||
var conversionFactor = UnitsConverter.GetUnitsToMetersConversionFactor();
|
||
return distance * conversionFactor;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogManager.Warning($"距离单位转换失败: {ex.Message},使用原始值");
|
||
return distance;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 将3D点坐标转换为米
|
||
/// </summary>
|
||
private Point3D ConvertPointToMeters(Point3D point)
|
||
{
|
||
try
|
||
{
|
||
var conversionFactor = UnitsConverter.GetUnitsToMetersConversionFactor();
|
||
return new Point3D(
|
||
point.X * conversionFactor,
|
||
point.Y * conversionFactor,
|
||
point.Z * conversionFactor
|
||
);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogManager.Warning($"坐标单位转换失败: {ex.Message},使用原始值");
|
||
return point;
|
||
}
|
||
}
|
||
}
|
||
}
|