1016 lines
37 KiB
Markdown
1016 lines
37 KiB
Markdown
# 批处理碰撞检测功能实施方案
|
||
|
||
**文档创建日期**: 2026年1月24日
|
||
**文档版本**: 1.0
|
||
**作者**: iFlow CLI
|
||
|
||
---
|
||
|
||
## 一、需求概述
|
||
|
||
基于用户需求,批处理碰撞检测功能需要实现以下核心能力:
|
||
|
||
1. **任务预设**:创建批处理任务,包含多条路径和各自的碰撞检测配置
|
||
2. **参数配置**:
|
||
- 使用系统参数作为默认值(帧率、时长、检测间隙等)
|
||
- 支持对每条路径进行单独配置(覆盖系统参数)
|
||
- 支持指定运动物体/虚拟车辆
|
||
- 支持指定被检测项(可选)
|
||
3. **队列执行**:简单队列,按顺序执行任务
|
||
4. **结果查看**:
|
||
- 汇总报告(所有路径的统计和对比)
|
||
- 单独报告(每条路径的详细报告)
|
||
- 历史记录(数据库持久化)
|
||
|
||
---
|
||
|
||
## 二、核心设计原则
|
||
|
||
### 2.1 复用现有系统
|
||
|
||
- ✅ **碰撞检测结果**:复用 `ClashDetectiveResultRecord` 和 `CollisionResult`
|
||
- ✅ **数据库结构**:复用现有的 `ClashDetectiveResults` 和 `ClashDetectiveCollisionObjects` 表
|
||
- ✅ **单独报告**:复用 `GenerateCollisionReportCommand` 和 `CollisionReportDialog`
|
||
- ✅ **汇总报告**:新开发(汇总多条路径的统计和对比)
|
||
|
||
### 2.2 解耦UI绑定
|
||
|
||
- 提取纯计算接口 `IBatchCollisionProcessor`
|
||
- 批处理不涉及UI操作(鼠标光标、视图重绘、DispatcherTimer)
|
||
- 所有Navisworks COM API调用仍在主线程执行
|
||
|
||
---
|
||
|
||
## 三、数据库设计
|
||
|
||
### 3.1 新增表结构
|
||
|
||
#### BatchTasks 表(批处理任务主表)
|
||
|
||
```sql
|
||
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 表(批处理任务项)
|
||
|
||
```sql
|
||
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格式)
|
||
VehicleObjectId TEXT, -- 运动物体ID(虚拟车辆或真实物体)
|
||
VehicleObjectName 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格式)
|
||
|
||
```json
|
||
{
|
||
"detectionGapMeters": 0.05,
|
||
"frameRate": 30,
|
||
"durationSeconds": 10.0,
|
||
"collisionDetectionEnabled": true,
|
||
"reportGenerationEnabled": true
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 四、核心类设计
|
||
|
||
### 4.1 数据模型类
|
||
|
||
#### BatchCollisionTask.cs
|
||
|
||
```csharp
|
||
|
||
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
|
||
|
||
```csharp
|
||
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 VehicleObjectId { get; set; }
|
||
public string VehicleObjectName { 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
|
||
|
||
```csharp
|
||
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
|
||
|
||
```csharp
|
||
namespace NavisworksTransport.Core.Collision
|
||
{
|
||
/// <summary>
|
||
/// 批处理碰撞检测纯计算接口
|
||
/// 关键特性:完全不涉及UI操作(鼠标光标、视图重绘、DispatcherTimer)
|
||
/// </summary>
|
||
public interface IBatchCollisionProcessor
|
||
{
|
||
/// <summary>
|
||
/// 预计算动画帧和碰撞(纯计算,无UI操作)
|
||
/// </summary>
|
||
List<AnimationFrame> PrecomputeFrames(
|
||
PathRoute route,
|
||
ModelItem animatedObject,
|
||
bool isVirtualVehicle,
|
||
double virtualVehicleLength,
|
||
double virtualVehicleWidth,
|
||
double virtualVehicleHeight,
|
||
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 isVirtualVehicle,
|
||
int frameRate,
|
||
double duration,
|
||
double virtualVehicleLength,
|
||
double virtualVehicleWidth,
|
||
double virtualVehicleHeight);
|
||
}
|
||
}
|
||
```
|
||
|
||
### 4.3 执行器类
|
||
|
||
#### BatchCollisionExecutor.cs
|
||
|
||
```csharp
|
||
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 isVirtualVehicle = false;
|
||
if (!string.IsNullOrEmpty(item.VehicleObjectId))
|
||
{
|
||
// 从Navisworks文档中查找物体
|
||
animatedObject = FindModelItemById(item.VehicleObjectId);
|
||
isVirtualVehicle = false;
|
||
}
|
||
else
|
||
{
|
||
// 使用虚拟车辆
|
||
isVirtualVehicle = true;
|
||
}
|
||
|
||
// 在主线程执行Navisworks API调用(但无UI操作)
|
||
await _uiStateManager.InvokeOnUIThreadAsync(() =>
|
||
{
|
||
// 预计算动画帧和碰撞(纯计算)
|
||
var frames = _processor.PrecomputeFrames(
|
||
pathRoute,
|
||
animatedObject,
|
||
isVirtualVehicle,
|
||
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,
|
||
isVirtualVehicle,
|
||
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
|
||
|
||
```csharp
|
||
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,
|
||
VehicleObjectId = itemConfig.VehicleObjectId,
|
||
VehicleObjectName = itemConfig.VehicleObjectName,
|
||
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
|
||
|
||
```csharp
|
||
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` 表。查看单独报告时,直接复用现有的报告系统:
|
||
|
||
```csharp
|
||
// 复用 GenerateCollisionReportCommand
|
||
var reportCommand = new GenerateCollisionReportCommand();
|
||
reportCommand.SetTestName(item.ClashDetectiveTestName);
|
||
await reportCommand.ExecuteAsync();
|
||
|
||
// 复用 CollisionReportDialog
|
||
CollisionReportDialog.ShowReportModal(reportResult);
|
||
```
|
||
|
||
### 5.2 汇总报告(新开发)
|
||
|
||
汇总报告由 `BatchCollisionSummaryReportGenerator` 生成,包含:
|
||
|
||
- 任务信息(名称、描述、时间、状态)
|
||
- 汇总统计(总碰撞数、有/无碰撞路径数、执行时长)
|
||
- 路径详情列表(每条路径的状态、碰撞数、测试名称、执行时长、查看报告链接)
|
||
|
||
---
|
||
|
||
## 六、实施步骤
|
||
|
||
### Phase 1: 数据库扩展
|
||
|
||
1. 在 `PathDatabase.cs` 中添加批处理表创建逻辑
|
||
2. 实现批处理任务和任务项的CRUD方法
|
||
3. 添加JSON序列化/反序列化辅助方法
|
||
|
||
### Phase 2: 数据模型创建
|
||
|
||
1. 创建 `BatchCollisionTask.cs`
|
||
2. 创建 `BatchTaskItem.cs`
|
||
3. 创建 `CollisionDetectionConfig.cs`
|
||
4. 创建 `BatchCollisionSummaryReport.cs`
|
||
|
||
### Phase 3: 纯计算接口实现(解耦UI)
|
||
|
||
1. 创建 `IBatchCollisionProcessor.cs` 接口
|
||
2. 创建 `BatchCollisionProcessor.cs` 实现类
|
||
3. 从 `PathAnimationManager` 和 `ClashDetectiveIntegration` 提取核心计算逻辑
|
||
4. 确保完全不涉及UI操作(鼠标光标、视图重绘、DispatcherTimer)
|
||
|
||
### Phase 4: 执行器实现
|
||
|
||
1. 创建 `BatchCollisionExecutor.cs`(复用 `IBatchCollisionProcessor`)
|
||
2. 实现任务执行逻辑(主线程COM API调用,但无UI操作)
|
||
3. 从数据库加载汇总统计信息(复用 `ClashDetectiveResults` 表)
|
||
|
||
### Phase 5: 任务管理器实现
|
||
|
||
1. 创建 `BatchTaskManager.cs`(队列管理)
|
||
2. 实现任务创建、查询、删除
|
||
|
||
### Phase 6: 汇总报告生成器实现(新开发)
|
||
|
||
1. 创建 `BatchCollisionSummaryReportGenerator.cs`
|
||
2. 生成汇总报告(HTML格式),包含任务信息、汇总统计、路径详情
|
||
|
||
### Phase 7: Command实现
|
||
|
||
1. 创建 `CreateBatchTaskCommand.cs`
|
||
2. 创建 `ExecuteBatchTaskCommand.cs`
|
||
3. 创建 `DeleteBatchTaskCommand.cs`
|
||
|
||
### Phase 8: UI实现
|
||
|
||
1. 创建 `BatchTaskViewModel.cs`
|
||
2. 创建批处理任务管理页面
|
||
3. 创建创建任务对话框
|
||
4. 创建汇总报告查看对话框(新开发)
|
||
5. 单独报告复用 `GenerateCollisionReportCommand` 和 `CollisionReportDialog`
|
||
|
||
### Phase 9: 测试和优化
|
||
|
||
1. 单元测试和集成测试
|
||
2. 性能优化和错误处理完善
|
||
|
||
---
|
||
|
||
## 七、文件清单
|
||
|
||
### 新增文件
|
||
|
||
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线程安全
|
||
|
||
```csharp
|
||
await _uiStateManager.InvokeOnUIThreadAsync(() =>
|
||
{
|
||
// 所有Navisworks API调用(但无UI操作)
|
||
var frames = _processor.PrecomputeFrames(...);
|
||
var testName = _processor.CreateAndRunClashDetectiveTest(...);
|
||
});
|
||
```
|
||
|
||
### 8.2 复用现有数据库结构
|
||
|
||
- 批处理任务项通过 `ClashDetectiveTestName` 关联到现有的 `ClashDetectiveResults` 表
|
||
- 单独报告直接复用 `GenerateCollisionReportCommand` 和 `CollisionReportDialog`
|
||
|
||
### 8.3 配置合并
|
||
|
||
```csharp
|
||
var config = task.DefaultConfig.MergeWith(item.ConfigOverride);
|
||
```
|
||
|
||
### 8.4 进度更新和事件通知
|
||
|
||
```csharp
|
||
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绑定)
|
||
|
||
1. **DispatcherTimer** - 动画播放定时器(批处理不需要播放)
|
||
2. **鼠标光标** - `Mouse.OverrideCursor`(批处理不需要)
|
||
3. **视图重绘** - `RequestDelayedRedraw()`(批处理不需要)
|
||
4. **状态通知** - UI进度更新(批处理需要,但方式不同)
|
||
|
||
### 9.3 解决方案
|
||
|
||
提取纯计算接口
|
||
|
||
核心思路:将动画检测的核心计算逻辑提取为独立接口,批处理只调用计算部分,不调用UI相关部分。
|
||
|
||
---
|
||
|
||
## 十、总结
|
||
|
||
本方案基于**方案一(提取纯计算接口)**,并**复用现有的碰撞检测结果、数据库结构和报告系统**,实现批处理碰撞检测功能。
|
||
|
||
### 核心优势
|
||
|
||
1. **解耦清晰**:批处理和UI完全分离,互不影响
|
||
2. **易于维护**:UI修改不会影响批处理逻辑
|
||
3. **可测试性**:纯计算接口更容易进行单元测试
|
||
4. **扩展性好**:未来可以支持其他批处理场景
|
||
5. **复用性强**:最大化复用现有系统,减少重复开发
|
||
|
||
### 关键技术
|
||
|
||
- 线程安全:使用 `UIStateManager` 确保COM API在主线程执行
|
||
- 复用数据库:通过 `ClashDetectiveTestName` 关联到现有 `ClashDetectiveResults` 表
|
||
- 复用报告:单独报告直接复用现有报告系统
|
||
- 配置合并:默认配置 + 覆盖配置
|
||
|
||
---
|