NavisworksTransport/doc/working/批处理碰撞检测功能实施方案_20260124.md

37 KiB
Raw Blame History

批处理碰撞检测功能实施方案

文档创建日期: 2026年1月24日
文档版本: 1.0
作者: iFlow CLI


一、需求概述

基于用户需求,批处理碰撞检测功能需要实现以下核心能力:

  1. 任务预设:创建批处理任务,包含多条路径和各自的碰撞检测配置
  2. 参数配置
    • 使用系统参数作为默认值(帧率、时长、检测间隙等)
    • 支持对每条路径进行单独配置(覆盖系统参数)
    • 支持指定运动物体/虚拟物体
    • 支持指定被检测项(可选)
  3. 队列执行:简单队列,按顺序执行任务
  4. 结果查看
    • 汇总报告(所有路径的统计和对比)
    • 单独报告(每条路径的详细报告)
    • 历史记录(数据库持久化)

二、核心设计原则

2.1 复用现有系统

  • 碰撞检测结果:复用 ClashDetectiveResultRecordCollisionResult
  • 数据库结构:复用现有的 ClashDetectiveResultsClashDetectiveCollisionObjects
  • 单独报告:复用 GenerateCollisionReportCommandCollisionReportDialog
  • 汇总报告:新开发(汇总多条路径的统计和对比)

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: 数据库扩展

  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. PathAnimationManagerClashDetectiveIntegration 提取核心计算逻辑
  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. 单独报告复用 GenerateCollisionReportCommandCollisionReportDialog

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线程安全

await _uiStateManager.InvokeOnUIThreadAsync(() =>
{
    // 所有Navisworks API调用但无UI操作
    var frames = _processor.PrecomputeFrames(...);
    var testName = _processor.CreateAndRunClashDetectiveTest(...);
});

8.2 复用现有数据库结构

  • 批处理任务项通过 ClashDetectiveTestName 关联到现有的 ClashDetectiveResults
  • 单独报告直接复用 GenerateCollisionReportCommandCollisionReportDialog

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绑定

  1. DispatcherTimer - 动画播放定时器(批处理不需要播放)
  2. 鼠标光标 - Mouse.OverrideCursor(批处理不需要)
  3. 视图重绘 - RequestDelayedRedraw()(批处理不需要)
  4. 状态通知 - UI进度更新批处理需要但方式不同

9.3 解决方案

提取纯计算接口

核心思路将动画检测的核心计算逻辑提取为独立接口批处理只调用计算部分不调用UI相关部分。


十、总结

本方案基于方案一(提取纯计算接口),并复用现有的碰撞检测结果、数据库结构和报告系统,实现批处理碰撞检测功能。

核心优势

  1. 解耦清晰批处理和UI完全分离互不影响
  2. 易于维护UI修改不会影响批处理逻辑
  3. 可测试性:纯计算接口更容易进行单元测试
  4. 扩展性好:未来可以支持其他批处理场景
  5. 复用性强:最大化复用现有系统,减少重复开发

关键技术

  • 线程安全:使用 UIStateManager 确保COM API在主线程执行
  • 复用数据库:通过 ClashDetectiveTestName 关联到现有 ClashDetectiveResults
  • 复用报告:单独报告直接复用现有报告系统
  • 配置合并:默认配置 + 覆盖配置