37 KiB
批处理碰撞检测功能实施方案
文档创建日期: 2026年1月24日
文档版本: 1.0
作者: iFlow CLI
一、需求概述
基于用户需求,批处理碰撞检测功能需要实现以下核心能力:
- 任务预设:创建批处理任务,包含多条路径和各自的碰撞检测配置
- 参数配置:
- 使用系统参数作为默认值(帧率、时长、检测间隙等)
- 支持对每条路径进行单独配置(覆盖系统参数)
- 支持指定运动物体/虚拟物体
- 支持指定被检测项(可选)
- 队列执行:简单队列,按顺序执行任务
- 结果查看:
- 汇总报告(所有路径的统计和对比)
- 单独报告(每条路径的详细报告)
- 历史记录(数据库持久化)
二、核心设计原则
2.1 复用现有系统
- ✅ 碰撞检测结果:复用
ClashDetectiveResultRecord和CollisionResult - ✅ 数据库结构:复用现有的
ClashDetectiveResults和ClashDetectiveCollisionObjects表 - ✅ 单独报告:复用
GenerateCollisionReportCommand和CollisionReportDialog - ✅ 汇总报告:新开发(汇总多条路径的统计和对比)
2.2 解耦UI绑定
- 提取纯计算接口
IBatchCollisionProcessor - 批处理不涉及UI操作(鼠标光标、视图重绘、DispatcherTimer)
- 所有Navisworks COM API调用仍在主线程执行
三、数据库设计
3.1 新增表结构
BatchTasks 表(批处理任务主表)
CREATE TABLE BatchTasks (
Id INTEGER PRIMARY KEY AUTOINCREMENT,
Name TEXT NOT NULL, -- 任务名称
Description TEXT, -- 任务描述
CreatedTime TEXT NOT NULL, -- 创建时间(ISO 8601)
Status TEXT NOT NULL, -- 任务状态:Pending, Running, Completed, Failed, Cancelled
StartTime TEXT, -- 开始时间
EndTime TEXT, -- 结束时间
TotalItems INTEGER NOT NULL DEFAULT 0, -- 总任务项数
CompletedItems INTEGER NOT NULL DEFAULT 0, -- 已完成任务项数
FailedItems INTEGER NOT NULL DEFAULT 0, -- 失败任务项数
LastModified TEXT NOT NULL -- 最后修改时间
);
CREATE INDEX idx_batch_tasks_status ON BatchTasks(Status);
CREATE INDEX idx_batch_tasks_created ON BatchTasks(CreatedTime);
BatchTaskItems 表(批处理任务项)
CREATE TABLE BatchTaskItems (
Id INTEGER PRIMARY KEY AUTOINCREMENT,
BatchTaskId INTEGER NOT NULL, -- 关联的批处理任务ID
RouteId TEXT NOT NULL, -- 关联的路径ID(使用TEXT,与PathRoutes.Id一致)
PathRouteName TEXT NOT NULL, -- 路径名称(冗余,方便查询)
SequenceNumber INTEGER NOT NULL, -- 执行顺序
Status TEXT NOT NULL, -- 任务项状态:Pending, Running, Completed, Failed, Skipped
StartTime TEXT, -- 开始时间
EndTime TEXT, -- 结束时间
ErrorMessage TEXT, -- 错误信息
ConfigOverride TEXT, -- 配置覆盖(JSON格式)
ObjectObjectId TEXT, -- 运动物体ID(虚拟物体或真实物体)
ObjectObjectName TEXT, -- 运动物体名称
DetectionItems TEXT, -- 被检测项列表(JSON格式,可选)
ClashDetectiveTestName TEXT, -- 关联的ClashDetective测试名称(复用现有表)
FOREIGN KEY (BatchTaskId) REFERENCES BatchTasks(Id) ON DELETE CASCADE
);
CREATE INDEX idx_batch_task_items_batch ON BatchTaskItems(BatchTaskId);
CREATE INDEX idx_batch_task_items_path ON BatchTaskItems(RouteId);
CREATE INDEX idx_batch_task_items_status ON BatchTaskItems(Status);
3.2 配置数据结构(JSON格式)
{
"detectionGapMeters": 0.05,
"frameRate": 30,
"durationSeconds": 10.0,
"collisionDetectionEnabled": true,
"reportGenerationEnabled": true
}
四、核心类设计
4.1 数据模型类
BatchCollisionTask.cs
namespace NavisworksTransport.Core.Models
{
public class BatchCollisionTask
{
public int Id { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public DateTime CreatedTime { get; set; }
public BatchTaskStatus Status { get; set; }
public DateTime? StartTime { get; set; }
public DateTime? EndTime { get; set; }
public int TotalItems { get; set; }
public int CompletedItems { get; set; }
public int FailedItems { get; set; }
public DateTime LastModified { get; set; }
public List<BatchTaskItem> Items { get; set; } = new List<BatchTaskItem>();
}
public enum BatchTaskStatus
{
Pending, Running, Completed, Failed, Cancelled
}
}
BatchTaskItem.cs
public class BatchTaskItem
{
public int Id { get; set; }
public int BatchTaskId { get; set; }
public string RouteId { get; set; } // 使用TEXT,与PathRoutes.Id一致
public string PathRouteName { get; set; }
public int SequenceNumber { get; set; }
public BatchTaskItemStatus Status { get; set; }
public DateTime? StartTime { get; set; }
public DateTime? EndTime { get; set; }
public string ErrorMessage { get; set; }
public CollisionDetectionConfig ConfigOverride { get; set; }
public string ObjectObjectId { get; set; }
public string ObjectObjectName { get; set; }
public List<string> DetectionItems { get; set; } = new List<string>();
public string ClashDetectiveTestName { get; set; } // 关联到ClashDetectiveResults.TestName
}
public enum BatchTaskItemStatus
{
Pending, Running, Completed, Failed, Skipped
}
CollisionDetectionConfig.cs
public class CollisionDetectionConfig
{
public double DetectionGapMeters { get; set; } = 0.05;
public int FrameRate { get; set; } = 30;
public double DurationSeconds { get; set; } = 10.0;
public bool CollisionDetectionEnabled { get; set; } = true;
public bool ReportGenerationEnabled { get; set; } = true;
/// <summary>
/// 合并配置(覆盖配置优先)
/// </summary>
public CollisionDetectionConfig MergeWith(CollisionDetectionConfig overrideConfig)
{
if (overrideConfig == null) return this;
return new CollisionDetectionConfig
{
DetectionGapMeters = overrideConfig.DetectionGapMeters > 0
? overrideConfig.DetectionGapMeters : DetectionGapMeters,
FrameRate = overrideConfig.FrameRate > 0
? overrideConfig.FrameRate : FrameRate,
DurationSeconds = overrideConfig.DurationSeconds > 0
? overrideConfig.DurationSeconds : DurationSeconds,
CollisionDetectionEnabled = overrideConfig.CollisionDetectionEnabled,
ReportGenerationEnabled = overrideConfig.ReportGenerationEnabled
};
}
}
4.2 纯计算接口(解耦UI绑定)
IBatchCollisionProcessor.cs
namespace NavisworksTransport.Core.Collision
{
/// <summary>
/// 批处理碰撞检测纯计算接口
/// 关键特性:完全不涉及UI操作(鼠标光标、视图重绘、DispatcherTimer)
/// </summary>
public interface IBatchCollisionProcessor
{
/// <summary>
/// 预计算动画帧和碰撞(纯计算,无UI操作)
/// </summary>
List<AnimationFrame> PrecomputeFrames(
PathRoute route,
ModelItem animatedObject,
bool isVirtualObject,
double virtualObjectLength,
double virtualObjectWidth,
double virtualObjectHeight,
int frameRate,
double duration,
double detectionGap,
List<ModelItem> manualDetectionTargets = null);
/// <summary>
/// 创建并运行ClashDetective测试(纯计算,无UI操作)
/// 返回测试名称,用于关联到ClashDetectiveResults表
/// </summary>
string CreateAndRunClashDetectiveTest(
List<CollisionResult> precomputedCollisions,
double detectionGap,
string pathName,
string routeId,
ModelItem animatedObject,
bool isVirtualObject,
int frameRate,
double duration,
double virtualObjectLength,
double virtualObjectWidth,
double virtualObjectHeight);
}
}
4.3 执行器类
BatchCollisionExecutor.cs
namespace NavisworksTransport.Core.Collision
{
public class BatchCollisionExecutor
{
private readonly PathDatabase _database;
private readonly IBatchCollisionProcessor _processor;
private readonly UIStateManager _uiStateManager;
private CancellationTokenSource _cancellationTokenSource;
public event EventHandler<BatchTaskProgressEventArgs> TaskProgress;
public event EventHandler<BatchTaskItemCompletedEventArgs> ItemCompleted;
public BatchCollisionExecutor(
PathDatabase database,
IBatchCollisionProcessor processor,
UIStateManager uiStateManager)
{
_database = database;
_processor = processor;
_uiStateManager = uiStateManager;
}
/// <summary>
/// 执行批处理任务
/// </summary>
public async Task ExecuteBatchTaskAsync(BatchCollisionTask task, CancellationToken cancellationToken = default)
{
_cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
try
{
await UpdateTaskStatusAsync(task.Id, BatchTaskStatus.Running);
var items = await _database.GetBatchTaskItemsAsync(task.Id);
task.TotalItems = items.Count;
var summaryReport = new BatchCollisionSummaryReport();
var startTime = DateTime.Now;
// 按顺序执行每个任务项
foreach (var item in items.OrderBy(i => i.SequenceNumber))
{
if (_cancellationTokenSource.Token.IsCancellationRequested)
{
await UpdateTaskStatusAsync(task.Id, BatchTaskStatus.Cancelled);
break;
}
try
{
await ExecuteBatchTaskItemAsync(task, item, summaryReport);
task.CompletedItems++;
await UpdateTaskProgressAsync(task);
TaskProgress?.Invoke(this, new BatchTaskProgressEventArgs
{
TaskId = task.Id,
CompletedItems = task.CompletedItems,
TotalItems = task.TotalItems,
Percentage = (double)task.CompletedItems / task.TotalItems * 100
});
}
catch (Exception ex)
{
LogManager.Error($"执行任务项失败 (RouteId: {item.RouteId}): {ex.Message}");
item.Status = BatchTaskItemStatus.Failed;
item.ErrorMessage = ex.Message;
task.FailedItems++;
await _database.UpdateBatchTaskItemAsync(item);
}
}
// 生成汇总报告
summaryReport.TotalDurationSeconds = (DateTime.Now - startTime).TotalSeconds;
summaryReport.TotalPaths = items.Count;
summaryReport.PathsFailed = task.FailedItems;
// 从数据库加载统计信息(复用ClashDetectiveResults表)
await LoadSummaryStatisticsAsync(task, summaryReport);
task.SummaryReport = summaryReport;
await _database.UpdateBatchTaskAsync(task);
var finalStatus = _cancellationTokenSource.Token.IsCancellationRequested
? BatchTaskStatus.Cancelled
: BatchTaskStatus.Completed;
await UpdateTaskStatusAsync(task.Id, finalStatus);
}
catch (Exception ex)
{
LogManager.Error($"批处理任务执行失败 (TaskId: {task.Id}): {ex.Message}");
await UpdateTaskStatusAsync(task.Id, BatchTaskStatus.Failed);
throw;
}
}
/// <summary>
/// 执行单个任务项
/// </summary>
private async Task ExecuteBatchTaskItemAsync(
BatchCollisionTask task,
BatchTaskItem item,
BatchCollisionSummaryReport summaryReport)
{
item.Status = BatchTaskItemStatus.Running;
item.StartTime = DateTime.Now;
await _database.UpdateBatchTaskItemAsync(item);
var config = task.DefaultConfig.MergeWith(item.ConfigOverride);
var pathRoute = await _database.GetPathRouteAsync(item.RouteId);
if (pathRoute == null)
{
throw new InvalidOperationException($"路径不存在 (Id: {item.RouteId})");
}
// 获取运动物体
ModelItem animatedObject = null;
bool isVirtualObject = false;
if (!string.IsNullOrEmpty(item.ObjectObjectId))
{
// 从Navisworks文档中查找物体
animatedObject = FindModelItemById(item.ObjectObjectId);
isVirtualObject = false;
}
else
{
// 使用虚拟物体
isVirtualObject = true;
}
// 在主线程执行Navisworks API调用(但无UI操作)
await _uiStateManager.InvokeOnUIThreadAsync(() =>
{
// 预计算动画帧和碰撞(纯计算)
var frames = _processor.PrecomputeFrames(
pathRoute,
animatedObject,
isVirtualObject,
config.DetectionGapMeters, // 虚拟物体尺寸从配置获取
config.DetectionGapMeters,
config.DetectionGapMeters,
config.FrameRate,
config.DurationSeconds,
config.DetectionGapMeters,
null // 暂不支持手动检测项
);
// 提取碰撞结果
var collisions = frames.SelectMany(f => f.Collisions).ToList();
// 创建并运行ClashDetective测试(纯计算,无UI操作)
var testName = _processor.CreateAndRunClashDetectiveTest(
collisions,
config.DetectionGapMeters,
pathRoute.Name,
pathRoute.Id,
animatedObject,
isVirtualObject,
config.FrameRate,
config.DurationSeconds,
config.DetectionGapMeters,
config.DetectionGapMeters,
config.DetectionGapMeters
);
// 保存测试名称到任务项
item.ClashDetectiveTestName = testName;
});
item.Status = BatchTaskItemStatus.Completed;
item.EndTime = DateTime.Now;
await _database.UpdateBatchTaskItemAsync(item);
ItemCompleted?.Invoke(this, new BatchTaskItemCompletedEventArgs
{
TaskId = task.Id,
TaskItemId = item.Id,
PathRouteName = item.PathRouteName,
Success = true
});
}
/// <summary>
/// 从数据库加载汇总统计信息(复用ClashDetectiveResults表)
/// </summary>
private async Task LoadSummaryStatisticsAsync(BatchCollisionTask task, BatchCollisionSummaryReport summaryReport)
{
var items = await _database.GetBatchTaskItemsAsync(task.Id);
foreach (var item in items.Where(i => i.Status == BatchTaskItemStatus.Completed))
{
if (!string.IsNullOrEmpty(item.ClashDetectiveTestName))
{
// 从ClashDetectiveResults表加载结果
var result = await _database.GetClashDetectiveResultByTestNameAsync(item.ClashDetectiveTestName);
if (result != null)
{
summaryReport.TotalCollisions += result.CollisionCount;
summaryReport.PathSummaries.Add(new PathCollisionSummary
{
RouteId = item.RouteId,
PathRouteName = item.PathRouteName,
CollisionCount = result.CollisionCount,
TestName = result.TestName,
ExecutionDurationSeconds = (item.EndTime.Value - item.StartTime.Value).TotalSeconds
});
}
}
}
summaryReport.PathsCollided = summaryReport.PathSummaries.Count(s => s.CollisionCount > 0);
summaryReport.PathsNoCollision = summaryReport.PathSummaries.Count(s => s.CollisionCount == 0);
}
public void Cancel()
{
_cancellationTokenSource?.Cancel();
}
private async Task UpdateTaskStatusAsync(int taskId, BatchTaskStatus status)
{
var task = await _database.GetBatchTaskAsync(taskId);
if (status == BatchTaskStatus.Running)
{
task.StartTime = DateTime.Now;
}
else if (status == BatchTaskStatus.Completed ||
status == BatchTaskStatus.Failed ||
status == BatchTaskStatus.Cancelled)
{
task.EndTime = DateTime.Now;
}
task.Status = status;
task.LastModified = DateTime.Now;
await _database.UpdateBatchTaskAsync(task);
}
private async Task UpdateTaskProgressAsync(BatchCollisionTask task)
{
task.LastModified = DateTime.Now;
await _database.UpdateBatchTaskAsync(task);
}
}
}
4.4 管理器类
BatchTaskManager.cs
namespace NavisworksTransport.Core
{
public class BatchTaskManager
{
private readonly PathDatabase _database;
private readonly BatchCollisionExecutor _executor;
private readonly CommandManager _commandManager;
private readonly Queue<int> _taskQueue;
private readonly object _queueLock = new object();
private BatchCollisionTask _currentTask;
public event EventHandler<BatchTaskEventArgs> TaskCreated;
public event EventHandler<BatchTaskEventArgs> TaskStarted;
public event EventHandler<BatchTaskEventArgs> TaskCompleted;
public event EventHandler<BatchTaskProgressEventArgs> TaskProgress;
public bool IsExecuting => _currentTask != null;
public int QueueCount => _taskQueue.Count;
public BatchTaskManager(
PathDatabase database,
BatchCollisionExecutor executor,
CommandManager commandManager)
{
_database = database;
_executor = executor;
_commandManager = commandManager;
_taskQueue = new Queue<int>();
_executor.TaskProgress += (s, e) => TaskProgress?.Invoke(s, e);
}
public async Task<int> CreateBatchTaskAsync(
string name,
string description,
List<BatchTaskItemConfig> itemConfigs,
CollisionDetectionConfig defaultConfig = null)
{
var task = new BatchCollisionTask
{
Name = name,
Description = description,
CreatedTime = DateTime.Now,
CreatedBy = Environment.UserName,
Status = BatchTaskStatus.Pending,
DefaultConfig = defaultConfig ?? GetDefaultConfig(),
LastModified = DateTime.Now
};
var taskId = await _database.CreateBatchTaskAsync(task);
task.Id = taskId;
var sequence = 0;
foreach (var itemConfig in itemConfigs)
{
var item = new BatchTaskItem
{
BatchTaskId = taskId,
RouteId = itemConfig.RouteId,
PathRouteName = itemConfig.PathRouteName,
SequenceNumber = sequence++,
Status = BatchTaskItemStatus.Pending,
ConfigOverride = itemConfig.ConfigOverride,
ObjectObjectId = itemConfig.ObjectObjectId,
ObjectObjectName = itemConfig.ObjectObjectName,
DetectionItems = itemConfig.DetectionItems
};
await _database.CreateBatchTaskItemAsync(item);
}
task.TotalItems = itemConfigs.Count;
await _database.UpdateBatchTaskAsync(task);
TaskCreated?.Invoke(this, new BatchTaskEventArgs { Task = task });
return taskId;
}
public void EnqueueTask(int taskId)
{
lock (_queueLock)
{
_taskQueue.Enqueue(taskId);
}
if (!IsExecuting)
{
Task.Run(() => ProcessQueueAsync());
}
}
private async Task ProcessQueueAsync()
{
while (true)
{
int taskId;
lock (_queueLock)
{
if (_taskQueue.Count == 0)
{
break;
}
taskId = _taskQueue.Dequeue();
}
try
{
var task = await _database.GetBatchTaskAsync(taskId);
_currentTask = task;
TaskStarted?.Invoke(this, new BatchTaskEventArgs { Task = task });
await _executor.ExecuteBatchTaskAsync(task);
TaskCompleted?.Invoke(this, new BatchTaskEventArgs { Task = task });
}
catch (Exception ex)
{
LogManager.Error($"队列任务执行失败 (TaskId: {taskId}): {ex.Message}");
}
finally
{
_currentTask = null;
}
}
}
public void CancelCurrentTask()
{
_executor.Cancel();
}
public async Task<List<BatchCollisionTask>> GetBatchTasksAsync(
BatchTaskStatus? statusFilter = null,
int limit = 50,
int offset = 0)
{
return await _database.GetBatchTasksAsync(statusFilter, limit, offset);
}
public async Task<BatchCollisionTask> GetBatchTaskAsync(int taskId)
{
return await _database.GetBatchTaskAsync(taskId);
}
public async Task DeleteBatchTaskAsync(int taskId)
{
await _database.DeleteBatchTaskAsync(taskId);
}
private CollisionDetectionConfig GetDefaultConfig()
{
var systemConfig = ConfigManager.Instance.Current;
return new CollisionDetectionConfig
{
DetectionGapMeters = systemConfig.AnimationConfig.DetectionGapMeters,
FrameRate = systemConfig.AnimationConfig.FrameRate,
DurationSeconds = systemConfig.AnimationConfig.DurationSeconds
};
}
}
}
4.5 汇总报告生成器(新开发)
BatchCollisionSummaryReportGenerator.cs
namespace NavisworksTransport.Core.Collision
{
public class BatchCollisionSummaryReportGenerator
{
private readonly PathDatabase _database;
public BatchCollisionSummaryReportGenerator(PathDatabase database)
{
_database = database;
}
/// <summary>
/// 生成汇总报告(HTML格式)
/// </summary>
public async Task<string> GenerateSummaryReportHtmlAsync(int taskId)
{
var task = await _database.GetBatchTaskAsync(taskId);
if (task == null) throw new ArgumentException($"任务不存在 (Id: {taskId})");
var items = await _database.GetBatchTaskItemsAsync(taskId);
var sb = new StringBuilder();
sb.AppendLine("<!DOCTYPE html>");
sb.AppendLine("<html><head><meta charset='utf-8'>");
sb.AppendLine("<title>批处理碰撞检测汇总报告</title>");
sb.AppendLine("<style>");
sb.AppendLine("body { font-family: Arial, sans-serif; margin: 20px; }");
sb.AppendLine("h1 { color: #333; }");
sb.AppendLine("h2 { color: #666; border-bottom: 2px solid #ddd; padding-bottom: 10px; }");
sb.AppendLine("table { border-collapse: collapse; width: 100%; margin: 20px 0; }");
sb.AppendLine("th, td { border: 1px solid #ddd; padding: 12px; text-align: left; }");
sb.AppendLine("th { background-color: #4CAF50; color: white; }");
sb.AppendLine("tr:nth-child(even) { background-color: #f2f2f2; }");
sb.AppendLine(".status-completed { color: green; font-weight: bold; }");
sb.AppendLine(".status-failed { color: red; font-weight: bold; }");
sb.AppendLine(".status-pending { color: orange; font-weight: bold; }");
sb.AppendLine(".status-running { color: blue; font-weight: bold; }");
sb.AppendLine(".summary-box { background-color: #e7f3fe; border-left: 6px solid #2196F3; padding: 15px; margin: 20px 0; }");
sb.AppendLine("</style></head><body>");
// 任务信息
sb.AppendLine("<h1>批处理碰撞检测汇总报告</h1>");
sb.AppendLine("<h2>任务信息</h2>");
sb.AppendLine("<table>");
sb.AppendLine($"<tr><th>任务名称</th><td>{task.Name}</td></tr>");
sb.AppendLine($"<tr><th>任务描述</th><td>{task.Description ?? "-"}</td></tr>");
sb.AppendLine($"<tr><th>创建时间</th><td>{task.CreatedTime:yyyy-MM-dd HH:mm:ss}</td></tr>");
sb.AppendLine($"<tr><th>开始时间</th><td>{task.StartTime?.ToString("yyyy-MM-dd HH:mm:ss") ?? "-"}</td></tr>");
sb.AppendLine($"<tr><th>结束时间</th><td>{task.EndTime?.ToString("yyyy-MM-dd HH:mm:ss") ?? "-"}</td></tr>");
sb.AppendLine($"<tr><th>状态</th><td class='status-{task.Status.ToString().ToLower()}'>{task.Status}</td></tr>");
sb.AppendLine($"<tr><th>总路径数</th><td>{task.TotalItems}</td></tr>");
sb.AppendLine($"<tr><th>已完成</th><td>{task.CompletedItems}</td></tr>");
sb.AppendLine($"<tr><th>失败</th><td>{task.FailedItems}</td></tr>");
sb.AppendLine("</table>");
// 汇总统计
if (task.SummaryReport != null)
{
sb.AppendLine("<h2>汇总统计</h2>");
sb.AppendLine("<div class='summary-box'>");
sb.AppendLine("<p><strong>总碰撞数:</strong>" + task.SummaryReport.TotalCollisions + "</p>");
sb.AppendLine("<p><strong>有碰撞的路径:</strong>" + task.SummaryReport.PathsCollided + "</p>");
sb.AppendLine("<p><strong>无碰撞的路径:</strong>" + task.SummaryReport.PathsNoCollision + "</p>");
sb.AppendLine("<p><strong>执行总时长:</strong>" + task.SummaryReport.TotalDurationSeconds.ToString("F2") + " 秒</p>");
sb.AppendLine("</div>");
}
// 路径详情
sb.AppendLine("<h2>路径详情</h2>");
sb.AppendLine("<table>");
sb.AppendLine("<tr><th>序号</th><th>路径名称</th><th>状态</th><th>碰撞数</th><th>测试名称</th><th>执行时长</th><th>操作</th></tr>");
foreach (var item in items.OrderBy(i => i.SequenceNumber))
{
var duration = item.StartTime.HasValue && item.EndTime.HasValue
? (item.EndTime.Value - item.StartTime.Value).TotalSeconds
: 0;
var collisionCount = 0;
if (!string.IsNullOrEmpty(item.ClashDetectiveTestName))
{
var result = await _database.GetClashDetectiveResultByTestNameAsync(item.ClashDetectiveTestName);
if (result != null)
{
collisionCount = result.CollisionCount;
}
}
sb.AppendLine("<tr>");
sb.AppendLine($"<td>{item.SequenceNumber + 1}</td>");
sb.AppendLine($"<td>{item.PathRouteName}</td>");
sb.AppendLine($"<td class='status-{item.Status.ToString().ToLower()}'>{item.Status}</td>");
sb.AppendLine($"<td>{collisionCount}</td>");
sb.AppendLine($"<td>{item.ClashDetectiveTestName ?? "-"}</td>");
sb.AppendLine($"<td>{duration:F2} 秒</td>");
sb.AppendLine($"<td>{(!string.IsNullOrEmpty(item.ClashDetectiveTestName) ? $"<a href='#' onclick='viewPathReport(\"{item.ClashDetectiveTestName}\")'>查看报告</a>" : "-")}</td>");
sb.AppendLine("</tr>");
}
sb.AppendLine("</table>");
sb.AppendLine("</body></html>");
return sb.ToString();
}
}
}
五、报告系统
5.1 单独报告(复用)
批处理任务的每个路径完成碰撞检测后,其结果已保存到 ClashDetectiveResults 表。查看单独报告时,直接复用现有的报告系统:
// 复用 GenerateCollisionReportCommand
var reportCommand = new GenerateCollisionReportCommand();
reportCommand.SetTestName(item.ClashDetectiveTestName);
await reportCommand.ExecuteAsync();
// 复用 CollisionReportDialog
CollisionReportDialog.ShowReportModal(reportResult);
5.2 汇总报告(新开发)
汇总报告由 BatchCollisionSummaryReportGenerator 生成,包含:
- 任务信息(名称、描述、时间、状态)
- 汇总统计(总碰撞数、有/无碰撞路径数、执行时长)
- 路径详情列表(每条路径的状态、碰撞数、测试名称、执行时长、查看报告链接)
六、实施步骤
Phase 1: 数据库扩展
- 在
PathDatabase.cs中添加批处理表创建逻辑 - 实现批处理任务和任务项的CRUD方法
- 添加JSON序列化/反序列化辅助方法
Phase 2: 数据模型创建
- 创建
BatchCollisionTask.cs - 创建
BatchTaskItem.cs - 创建
CollisionDetectionConfig.cs - 创建
BatchCollisionSummaryReport.cs
Phase 3: 纯计算接口实现(解耦UI)
- 创建
IBatchCollisionProcessor.cs接口 - 创建
BatchCollisionProcessor.cs实现类 - 从
PathAnimationManager和ClashDetectiveIntegration提取核心计算逻辑 - 确保完全不涉及UI操作(鼠标光标、视图重绘、DispatcherTimer)
Phase 4: 执行器实现
- 创建
BatchCollisionExecutor.cs(复用IBatchCollisionProcessor) - 实现任务执行逻辑(主线程COM API调用,但无UI操作)
- 从数据库加载汇总统计信息(复用
ClashDetectiveResults表)
Phase 5: 任务管理器实现
- 创建
BatchTaskManager.cs(队列管理) - 实现任务创建、查询、删除
Phase 6: 汇总报告生成器实现(新开发)
- 创建
BatchCollisionSummaryReportGenerator.cs - 生成汇总报告(HTML格式),包含任务信息、汇总统计、路径详情
Phase 7: Command实现
- 创建
CreateBatchTaskCommand.cs - 创建
ExecuteBatchTaskCommand.cs - 创建
DeleteBatchTaskCommand.cs
Phase 8: UI实现
- 创建
BatchTaskViewModel.cs - 创建批处理任务管理页面
- 创建创建任务对话框
- 创建汇总报告查看对话框(新开发)
- 单独报告复用
GenerateCollisionReportCommand和CollisionReportDialog
Phase 9: 测试和优化
- 单元测试和集成测试
- 性能优化和错误处理完善
七、文件清单
新增文件
src/Core/Models/BatchCollisionTask.cs src/Core/Models/BatchTaskItem.cs src/Core/Models/CollisionDetectionConfig.cs src/Core/Models/BatchCollisionSummaryReport.cs src/Core/Collision/IBatchCollisionProcessor.cs src/Core/Collision/BatchCollisionProcessor.cs src/Core/Collision/BatchCollisionExecutor.cs src/Core/Collision/BatchCollisionSummaryReportGenerator.cs src/Core/BatchTaskManager.cs src/Commands/CreateBatchTaskCommand.cs src/Commands/ExecuteBatchTaskCommand.cs src/Commands/DeleteBatchTaskCommand.cs src/UI/WPF/ViewModels/BatchTaskViewModel.cs src/UI/WPF/Views/BatchTaskView.xaml src/UI/WPF/Views/BatchTaskView.xaml.cs src/UI/WPF/Views/CreateBatchTaskDialog.xaml src/UI/WPF/Views/CreateBatchTaskDialog.xaml.cs src/UI/WPF/Views/BatchSummaryReportDialog.xaml src/UI/WPF/Views/BatchSummaryReportDialog.xaml.cs
修改文件
src/Core/PathDatabase.cs (新增批处理相关方法) src/Core/MainPlugin.cs (注册BatchTaskManager) src/UI/WPF/ViewModels/LogisticsControlViewModel.cs (集成批处理页面)
八、关键技术要点
8.1 COM API线程安全
await _uiStateManager.InvokeOnUIThreadAsync(() =>
{
// 所有Navisworks API调用(但无UI操作)
var frames = _processor.PrecomputeFrames(...);
var testName = _processor.CreateAndRunClashDetectiveTest(...);
});
8.2 复用现有数据库结构
- 批处理任务项通过
ClashDetectiveTestName关联到现有的ClashDetectiveResults表 - 单独报告直接复用
GenerateCollisionReportCommand和CollisionReportDialog
8.3 配置合并
var config = task.DefaultConfig.MergeWith(item.ConfigOverride);
8.4 进度更新和事件通知
TaskProgress?.Invoke(this, new BatchTaskProgressEventArgs { ... });
九、现有动画检测与UI绑定分析
9.1 UI绑定点分析
现有动画检测流程中,确实存在多处UI绑定:
| 绑定位置 | 代码位置 | UI元素 | 用途 |
|---|---|---|---|
| 鼠标光标 | PathAnimationManager.cs:1396-1418 |
System.Windows.Input.Mouse.OverrideCursor |
显示等待光标 |
| DispatcherTimer | PathAnimationManager.cs:126,199 |
DispatcherTimer |
动画播放定时器 |
| 视图重绘 | PathAnimationManager.cs:1195 |
NavisApplication.ActiveDocument.ActiveView.RequestDelayedRedraw() |
3D视图实时更新 |
| 状态通知 | 多处 | 通过事件通知UI更新 | 进度条、状态显示 |
9.2 能否支持后台批处理?
答案:可以,但需要解耦UI绑定**
✅ 可以复用的部分(无UI绑定)
PrecomputeAnimationFrames()- 纯计算逻辑ClashDetectiveIntegration.CreateAllAnimationCollisionTests()- 核心碰撞检测ClashDetectiveIntegration.SaveClashDetectiveResultToDatabase()- 数据持久化
❌ 需要解耦的部分(有UI绑定)
- DispatcherTimer - 动画播放定时器(批处理不需要播放)
- 鼠标光标 -
Mouse.OverrideCursor(批处理不需要) - 视图重绘 -
RequestDelayedRedraw()(批处理不需要) - 状态通知 - UI进度更新(批处理需要,但方式不同)
9.3 解决方案
提取纯计算接口
核心思路:将动画检测的核心计算逻辑提取为独立接口,批处理只调用计算部分,不调用UI相关部分。
十、总结
本方案基于方案一(提取纯计算接口),并复用现有的碰撞检测结果、数据库结构和报告系统,实现批处理碰撞检测功能。
核心优势
- 解耦清晰:批处理和UI完全分离,互不影响
- 易于维护:UI修改不会影响批处理逻辑
- 可测试性:纯计算接口更容易进行单元测试
- 扩展性好:未来可以支持其他批处理场景
- 复用性强:最大化复用现有系统,减少重复开发
关键技术
- 线程安全:使用
UIStateManager确保COM API在主线程执行 - 复用数据库:通过
ClashDetectiveTestName关联到现有ClashDetectiveResults表 - 复用报告:单独报告直接复用现有报告系统
- 配置合并:默认配置 + 覆盖配置