简化批处理,变成队列

This commit is contained in:
tian 2026-01-25 17:58:08 +08:00
parent 6dc266e526
commit b4a73e2081
23 changed files with 2353 additions and 723 deletions

View File

@ -112,14 +112,15 @@
<Compile Include="src\Core\PathDatabase.cs" />
<Compile Include="src\Core\PathAnalysisService.cs" />
<Compile Include="src\Core\PathPlanningModels.cs" />
<Compile Include="src\Core\Models\BatchQueueItem.cs" />
<Compile Include="src\Core\Models\BatchCollisionTask.cs" />
<Compile Include="src\Core\Models\BatchTaskItem.cs" />
<Compile Include="src\Core\Models\BatchTaskUIModels.cs" />
<Compile Include="src\Core\Models\CollisionDetectionConfig.cs" />
<Compile Include="src\Core\Models\BatchCollisionSummaryReport.cs" />
<Compile Include="src\Core\PathCurveEngine.cs" />
<Compile Include="src\Core\GridVisualization.cs" />
<Compile Include="src\Core\BatchTaskManager.cs" />
<Compile Include="src\Core\BatchQueueManager.cs" />
<!-- Core - Events and Interfaces -->
<Compile Include="src\Core\IPathPlanningManagerEvents.cs" />
<Compile Include="src\Core\PathPlanningManagerEventArgs.cs" />
@ -279,6 +280,8 @@
<Compile Include="src\UI\WPF\Converters\IndexConverter.cs" />
<Compile Include="src\UI\WPF\Converters\CountToVisibilityConverter.cs" />
<Compile Include="src\UI\WPF\Converters\PathTypeConverter.cs" />
<Compile Include="src\UI\WPF\Converters\BatchQueueStatusConverter.cs" />
<Compile Include="src\UI\WPF\Converters\BatchQueueStatusHelper.cs" />
<Compile Include="src\UI\WPF\Models\LogisticsModel.cs" />
<Compile Include="src\UI\WPF\Models\PathRouteViewModel.cs" />
<Compile Include="src\UI\WPF\Models\SplitPreviewItem.cs" />
@ -392,6 +395,22 @@
<DependentUpon>CreateBatchTaskDialog.xaml</DependentUpon>
<SubType>Code</SubType>
</Compile>
<Page Include="src\UI\WPF\Views\PathSelectionDialog.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Compile Include="src\UI\WPF\Views\PathSelectionDialog.xaml.cs">
<DependentUpon>PathSelectionDialog.xaml</DependentUpon>
<SubType>Code</SubType>
</Compile>
<Page Include="src\UI\WPF\Views\PathConfigDialog.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Compile Include="src\UI\WPF\Views\PathConfigDialog.xaml.cs">
<DependentUpon>PathConfigDialog.xaml</DependentUpon>
<SubType>Code</SubType>
</Compile>
<!-- Shared Resource Dictionary -->
<Page Include="src\UI\WPF\Resources\NavisworksStyles.xaml">
<SubType>Designer</SubType>

View File

@ -196,8 +196,8 @@ namespace NavisworksTransport.Commands
_commandQueue.Enqueue(request);
OnQueueStatusChanged();
// 启动处理队列
_ = Task.Run(ProcessQueueAsync);
// 启动处理队列直接在主线程执行避免后台线程导致的Navisworks Native对象生命周期问题
ProcessQueueAsync();
LogManager.Info($"命令已加入队列: {command.DisplayName} (优先级: {priority})");

View File

@ -0,0 +1,392 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Autodesk.Navisworks.Api;
using NavisworksTransport.Core.Animation;
using NavisworksTransport.Core.Collision;
using NavisworksTransport.Core.Models;
namespace NavisworksTransport.Core
{
/// <summary>
/// 批处理队列管理器 - 简化版只维护FIFO队列
/// </summary>
public class BatchQueueManager
{
private static BatchQueueManager _instance;
private static readonly object _lock = new object();
private PathDatabase _database;
private BatchCollisionExecutor _executor;
private readonly Queue<int> _queue;
private readonly object _queueLock = new object();
private BatchQueueItem _currentItem;
public event EventHandler<BatchQueueItemEventArgs> ItemAdded;
public event EventHandler<BatchQueueItemEventArgs> ItemStarted;
public event EventHandler<BatchQueueItemEventArgs> ItemCompleted;
public event EventHandler<BatchQueueProgressEventArgs> ItemProgress;
public bool IsExecuting => _currentItem != null;
public int QueueCount => _queue.Count;
public static BatchQueueManager Instance
{
get
{
if (_instance == null)
{
lock (_lock)
{
if (_instance == null)
{
_instance = new BatchQueueManager();
}
}
}
return _instance;
}
}
private BatchQueueManager()
{
// 延迟初始化,等待 PathDatabase 就绪
_database = null;
_executor = null;
_queue = new Queue<int>();
}
/// <summary>
/// 设置PathPlanningManager用于获取数据库
/// </summary>
public void SetPathPlanningManager(PathPlanningManager pathPlanningManager)
{
_database = pathPlanningManager?.GetPathDatabase();
LogManager.Info("BatchQueueManager已设置PathPlanningManager");
}
/// <summary>
/// 确保 Executor 已初始化
/// </summary>
private void EnsureExecutorInitialized()
{
if (_executor == null)
{
var pathManager = PathPlanningManager.Instance;
_database = pathManager?.GetPathDatabase();
if (_database == null)
{
throw new InvalidOperationException("PathDatabase未初始化请确保文档已加载");
}
_executor = new BatchCollisionExecutor(
_database,
new BatchCollisionProcessor(),
UIStateManager.Instance
);
// 转换事件参数类型
_executor.TaskProgress += (s, e) =>
{
ItemProgress?.Invoke(s, new BatchQueueProgressEventArgs
{
ItemId = e.TaskId,
CompletedItems = e.CompletedItems,
TotalItems = e.TotalItems,
Percentage = e.Percentage
});
};
}
}
/// <summary>
/// 添加队列项
/// </summary>
public async Task<int> AddQueueItemAsync(BatchQueueItem item)
{
if (_database == null)
{
throw new InvalidOperationException("PathDatabase未初始化");
}
var itemId = await _database.CreateBatchQueueItemAsync(item);
item.Id = itemId;
lock (_queueLock)
{
_queue.Enqueue(itemId);
}
ItemAdded?.Invoke(this, new BatchQueueItemEventArgs { Item = item });
LogManager.Info($"[批处理队列] 已添加队列项: {item.PathRouteName}, ID: {itemId}");
return itemId;
}
/// <summary>
/// 处理队列(公共方法,供外部调用)
/// </summary>
public async Task ProcessQueueAsync()
{
while (true)
{
int itemId;
lock (_queueLock)
{
if (_queue.Count == 0)
{
break;
}
itemId = _queue.Dequeue();
}
try
{
EnsureExecutorInitialized();
var item = await _database.GetBatchQueueItemAsync(itemId);
_currentItem = item;
ItemStarted?.Invoke(this, new BatchQueueItemEventArgs { Item = item });
await ExecuteQueueItemAsync(item);
ItemCompleted?.Invoke(this, new BatchQueueItemEventArgs { Item = item });
}
catch (Exception ex)
{
LogManager.Error($"队列项执行失败 (Id: {itemId}): {ex.Message}");
// 更新状态为失败
if (_database != null)
{
var item = await _database.GetBatchQueueItemAsync(itemId);
if (item != null)
{
item.Status = BatchQueueStatus.Failed;
item.ErrorMessage = ex.Message;
item.EndTime = DateTime.Now;
await _database.UpdateBatchQueueItemAsync(item);
}
}
}
finally
{
_currentItem = null;
}
}
}
/// <summary>
/// 执行单个队列项
/// </summary>
private async Task ExecuteQueueItemAsync(BatchQueueItem item)
{
item.Status = BatchQueueStatus.Running;
item.StartTime = DateTime.Now;
await _database.UpdateBatchQueueItemAsync(item);
try
{
var pathRoute = await _database.GetPathRouteAsync(item.PathRouteId);
if (pathRoute == null)
{
throw new InvalidOperationException($"路径不存在 (Id: {item.PathRouteId})");
}
// 获取运动物体
ModelItem animatedObject = null;
bool isVirtualVehicle = item.IsVirtualVehicle;
if (!string.IsNullOrEmpty(item.VehicleObjectId))
{
animatedObject = FindModelItemById(item.VehicleObjectId);
isVirtualVehicle = false;
}
else if (isVirtualVehicle)
{
// 🔥 使用现有的虚拟车辆,不创建和清理
// ShowVirtualVehicle 会显示虚拟车辆并更新尺寸(如果已存在)
VirtualVehicleManager.Instance.ShowVirtualVehicle(
item.VirtualVehicleLength,
item.VirtualVehicleWidth,
item.VirtualVehicleHeight
);
animatedObject = VirtualVehicleManager.Instance.CurrentVirtualVehicle;
if (animatedObject == null)
{
throw new InvalidOperationException("虚拟车辆显示失败");
}
LogManager.Info($"[批处理] 使用虚拟车辆: {animatedObject.DisplayName}");
}
// 初始化碰撞检测缓存
LogManager.Info("[批处理] 初始化碰撞检测缓存...");
ClashDetectiveIntegration.InitializeCollisionDetectionCache(animatedObject);
// 在主线程执行Navisworks API调用
var frames = await UIStateManager.Instance.ExecuteUIUpdateAsync(() =>
{
// 🔥 重要:预计算前必须将车辆移动到起点位置,确保变换状态与实际动画播放时一致
var animationManager = PathAnimationManager.GetInstance();
animationManager.MoveVehicleToPathStart(animatedObject, pathRoute.Points.Select(p => p.Position).ToList());
// 预计算动画帧和碰撞
var config = new CollisionDetectionConfig
{
FrameRate = item.FrameRate,
DurationSeconds = item.DurationSeconds,
DetectionGapMeters = item.DetectionGapMeters,
VirtualVehicleLength = item.VirtualVehicleLength,
VirtualVehicleWidth = item.VirtualVehicleWidth,
VirtualVehicleHeight = item.VirtualVehicleHeight,
CollisionDetectionEnabled = true,
ReportGenerationEnabled = true
};
return _executor._processor.PrecomputeFrames(
pathRoute,
animatedObject,
isVirtualVehicle,
config.VirtualVehicleLength,
config.VirtualVehicleWidth,
config.VirtualVehicleHeight,
config.FrameRate,
config.DurationSeconds,
config.DetectionGapMeters,
null
);
});
// 检查预计算是否成功
if (frames == null || frames.Count == 0)
{
throw new InvalidOperationException("预计算动画帧失败:未生成任何帧");
}
// 提取碰撞结果
var collisions = frames.SelectMany(f => f.Collisions).ToList();
// 创建并运行ClashDetective测试直接在主线程执行避免异步切换导致的Navisworks Native对象生命周期问题
var testName = _executor._processor.CreateAndRunClashDetectiveTest(
collisions,
item.DetectionGapMeters,
pathRoute.Name,
pathRoute.Id,
animatedObject,
isVirtualVehicle,
item.FrameRate,
item.DurationSeconds,
item.VirtualVehicleLength,
item.VirtualVehicleWidth,
item.VirtualVehicleHeight
);
// 🔥 使用 ClashDetective 确认的碰撞数,而不是预计算的碰撞数
item.CollisionCount = ClashDetectiveIntegration.Instance.ClashDetectiveCollisionCount;
// 保存测试名称
item.ClashDetectiveTestName = testName;
// 标记为完成
item.Status = BatchQueueStatus.Completed;
item.EndTime = DateTime.Now;
await _database.UpdateBatchQueueItemAsync(item);
LogManager.Info($"[批处理队列] 队列项执行完成: {item.PathRouteName}, 碰撞数: {item.CollisionCount}");
}
catch (Exception ex)
{
// 标记为失败
item.Status = BatchQueueStatus.Failed;
item.ErrorMessage = ex.Message;
item.EndTime = DateTime.Now;
await _database.UpdateBatchQueueItemAsync(item);
LogManager.Error($"[批处理队列] 队列项执行失败: {item.PathRouteName}, 错误: {ex.Message}");
throw; // 重新抛出异常,让上层处理
}
}
/// <summary>
/// 取消当前任务
/// </summary>
public void CancelCurrentItem()
{
EnsureExecutorInitialized();
_executor.Cancel();
}
/// <summary>
/// 获取队列列表
/// </summary>
public async Task<List<BatchQueueItem>> GetQueueItemsAsync(
BatchQueueStatus? statusFilter = null,
int limit = 50,
int offset = 0)
{
return await _database.GetBatchQueueItemsAsync(statusFilter, limit, offset);
}
/// <summary>
/// 获取单个队列项
/// </summary>
public async Task<BatchQueueItem> GetQueueItemAsync(int itemId)
{
return await _database.GetBatchQueueItemAsync(itemId);
}
/// <summary>
/// 删除队列项
/// </summary>
public async Task DeleteQueueItemAsync(int itemId)
{
await _database.DeleteBatchQueueItemAsync(itemId);
}
/// <summary>
/// 查找模型项
/// </summary>
private ModelItem FindModelItemById(string id)
{
try
{
var doc = Autodesk.Navisworks.Api.Application.ActiveDocument;
if (doc == null) return null;
var model = doc.Models.RootItemDescendants.FirstOrDefault(
item => item.ToString() == id
);
return model;
}
catch (Exception ex)
{
LogManager.Error($"查找模型项失败 (Id: {id}): {ex.Message}");
return null;
}
}
}
/// <summary>
/// 批处理队列项事件参数
/// </summary>
public class BatchQueueItemEventArgs : EventArgs
{
public BatchQueueItem Item { get; set; }
}
/// <summary>
/// 批处理队列进度事件参数
/// </summary>
public class BatchQueueProgressEventArgs : EventArgs
{
public int ItemId { get; set; }
public int CompletedItems { get; set; }
public int TotalItems { get; set; }
public double Percentage { get; set; }
}
}

View File

@ -16,7 +16,7 @@ namespace NavisworksTransport.Core.Collision
public class BatchCollisionExecutor
{
private readonly PathDatabase _database;
private readonly IBatchCollisionProcessor _processor;
public readonly IBatchCollisionProcessor _processor;
private readonly UIStateManager _uiStateManager;
private CancellationTokenSource _cancellationTokenSource;

View File

@ -471,6 +471,276 @@ namespace NavisworksTransport
}
}
/// <summary>
/// 运行ClashDetective测试并保存到数据库公共方法供批处理和非批处理调用
/// </summary>
/// <returns>碰撞分组、主测试、确认的碰撞数量</returns>
public (ClashResultGroup collisionGroup, ClashTest addedMainTest, int confirmedCount) RunClashDetectiveTestsAndSaveToDatabase(
List<CollisionResult> precomputedCollisions,
double detectionGap,
string pathName,
string routeId,
ModelItem animatedObject,
bool isVirtualVehicle,
int frameRate,
double duration,
double virtualVehicleLength,
double virtualVehicleWidth,
double virtualVehicleHeight,
Progress progress = null)
{
LogManager.Info($"[ClashDetective] 开始运行碰撞检测并保存到数据库(容差: {detectionGap}米)");
// 过滤有效的碰撞
var validCollisions = precomputedCollisions.Where(collision =>
collision.HasPositionInfo &&
IsModelItemValid(collision.Item1) &&
IsModelItemValid(collision.Item2)
).ToList();
if (validCollisions.Count == 0)
{
LogManager.Warning("[ClashDetective] 没有有效的碰撞可以创建测试");
return (null, null, 0);
}
LogManager.Info($"[ClashDetective] 有效碰撞数量: {validCollisions.Count}");
// 第一步:创建主测试(不包含选择集,纯容器)
var mainTestName = $"碰撞检测_{DateTime.Now:MMdd_HHmmss}";
_currentTestName = mainTestName;
LogManager.Info($"[分组测试] 创建主测试: {mainTestName}");
// 创建主测试
var mainTest = new ClashTest
{
DisplayName = mainTestName,
TestType = ClashTestType.Hard,
Tolerance = detectionGap,
Guid = Guid.Empty,
MergeComposites = true
};
// 添加主测试到文档
_documentClash.TestsData.TestsAddCopy(mainTest);
LogManager.Info($"[分组测试] 主测试已添加到文档");
// 获取添加后的测试对象引用
var addedMainTest = _documentClash.TestsData.Tests.FirstOrDefault(t => t.DisplayName == mainTestName) as ClashTest;
if (addedMainTest == null)
{
LogManager.Error("[分组测试] 无法获取添加后的主测试对象");
return (null, null, 0);
}
// 第二步:创建分组并添加碰撞结果(应用智能去重)
// 1. 分组:按碰撞对象对分组
var groupedCollisions = validCollisions
.GroupBy(c => new { Item1 = c.Item1, Item2 = c.Item2 })
.ToList();
LogManager.Info($"[分组测试] 智能去重: {validCollisions.Count} 个检测点 -> {groupedCollisions.Count} 个唯一碰撞对");
// 缓存去重后的碰撞结果(每组取第一个)
lock (_resultsLock)
{
_deduplicatedCollisionResults.Clear();
foreach (var group in groupedCollisions)
{
_deduplicatedCollisionResults.Add(group.First());
}
LogManager.Debug($"[去重缓存] 已缓存 {_deduplicatedCollisionResults.Count} 个去重后的碰撞结果");
}
var collisionGroup = new ClashResultGroup
{
DisplayName = $"碰撞检测组 ({groupedCollisions.Count} 个唯一碰撞对)"
};
LogManager.Info($"[分组测试] 创建碰撞分组: {collisionGroup.DisplayName}");
int confirmedCount = 0;
int skippedCount = 0;
var doc = Application.ActiveDocument;
// 2. 遍历每一组
for (int groupIndex = 0; groupIndex < groupedCollisions.Count; groupIndex++)
{
// 检查用户是否取消
if (progress != null && progress.IsCanceled)
{
LogManager.Info($"[分组测试] 用户取消操作,已处理 {groupIndex}/{groupedCollisions.Count} 组");
break;
}
var group = groupedCollisions[groupIndex];
// 3. 排序:按距离/重叠深度排序,优先检测最严重的碰撞
// 注意Distance通常越小或负值越大表示碰撞越深具体取决于计算方式
// 这里假设Distance越小越严重
var sortedCandidates = group.OrderBy(c => c.Distance).ToList();
bool pairConfirmed = false;
// 4. 验证即止:逐个检测候选帧
for (int i = 0; i < sortedCandidates.Count; i++)
{
var candidate = sortedCandidates[i];
try
{
var testAnimatedObject = candidate.Item1;
var modelItems = new ModelItemCollection { testAnimatedObject };
var targetPosition = candidate.Item1Position;
var currentBounds = testAnimatedObject.BoundingBox();
var currentPos = new Point3D(
(currentBounds.Min.X + currentBounds.Max.X) / 2,
(currentBounds.Min.Y + currentBounds.Max.Y) / 2,
(currentBounds.Min.Z + currentBounds.Max.Z) / 2
);
var offset = new Vector3D(
targetPosition.X - currentPos.X,
targetPosition.Y - currentPos.Y,
targetPosition.Z - currentPos.Z
);
var transform = Transform3D.CreateTranslation(offset);
doc.Models.OverridePermanentTransform(modelItems, transform, false);
var tempTestName = $"临时验证_{confirmedCount + 1}_{i}_{DateTime.Now:HHmmss_fff}";
var tempTest = new ClashTest
{
DisplayName = tempTestName,
TestType = ClashTestType.Hard,
Tolerance = detectionGap,
Guid = Guid.Empty,
MergeComposites = true
};
var selectionA = new ModelItemCollection { candidate.Item1 };
var selectionB = new ModelItemCollection { candidate.Item2 };
tempTest.SelectionA.Selection.CopyFrom(selectionA);
tempTest.SelectionB.Selection.CopyFrom(selectionB);
_documentClash.TestsData.TestsAddCopy(tempTest);
var addedTempTest = _documentClash.TestsData.Tests.FirstOrDefault(t => t.DisplayName == tempTestName) as ClashTest;
if (addedTempTest != null)
{
var copyTest = addedTempTest.CreateCopy() as ClashTest;
copyTest.SelectionA.PrimitiveTypes = PrimitiveTypes.Triangles | PrimitiveTypes.Lines | PrimitiveTypes.Points;
copyTest.SelectionB.PrimitiveTypes = PrimitiveTypes.Triangles | PrimitiveTypes.Lines | PrimitiveTypes.Points;
_documentClash.TestsData.TestsEditTestFromCopy(addedTempTest, copyTest);
_documentClash.TestsData.TestsRunTest(addedTempTest);
var refreshedTempTest = _documentClash.TestsData.Tests.FirstOrDefault(t => t.DisplayName == tempTestName) as ClashTest;
if (refreshedTempTest != null && refreshedTempTest.Children.Count > 0)
{
// !!!发现真实碰撞!!!
pairConfirmed = true;
confirmedCount++;
skippedCount += (sortedCandidates.Count - 1 - i); // 记录跳过的数量
int subResultIndex = 1;
foreach (var child in refreshedTempTest.Children)
{
if (child is ClashResult result)
{
var copiedResult = result.CreateCopy() as ClashResult;
copiedResult.Guid = Guid.NewGuid(); // 生成新的GUID
// 设置唯一且有意义的碰撞名称
// 先找到有意义的容器对象,再获取其名称
var container1 = ModelItemAnalysisHelper.FindNamedParentContainer(result.Item1);
var container2 = ModelItemAnalysisHelper.FindNamedParentContainer(result.Item2);
var object1Name = ModelItemAnalysisHelper.GetSafeDisplayName(container1);
var object2Name = ModelItemAnalysisHelper.GetSafeDisplayName(container2);
var timeStamp = DateTime.Now.ToString("HHmmss");
copiedResult.DisplayName = $"物流碰撞#{confirmedCount:00}-{subResultIndex:00}_{timeStamp}: {object1Name} ↔ {object2Name}";
collisionGroup.Children.Add(copiedResult);
subResultIndex++;
}
}
}
_documentClash.TestsData.TestsRemove(refreshedTempTest ?? addedTempTest);
if (pairConfirmed)
{
break;
}
}
}
catch (Exception itemEx)
{
LogManager.Error($"[ClashDetective] 候选检测失败: {itemEx.Message}");
}
}
}
LogManager.Info($"[分组测试] ClashDetective检测完成: 确认碰撞 {confirmedCount} 组, 跳过 {skippedCount} 个冗余检测点");
// 第三步:处理碰撞结果
// 🔥 重要ClashDetective 返回的是几何体级别的碰撞结果
// Item1 是移动物体的组件名(如"车轮1"、"车轮2"),需要统一为移动物体本身
// Item2 是被撞物体,需要向上查找有意义的父级容器
var clashResults = new List<CollisionResult>();
foreach (var child in collisionGroup.Children)
{
if (child is ClashResult clashResult)
{
var compositeItem1 = animatedObject;
var compositeItem2 = ModelItemAnalysisHelper.FindNamedParentContainer(clashResult.Item2);
var collisionResult = new CollisionResult
{
ClashGuid = clashResult.Guid,
DisplayName = clashResult.DisplayName,
Status = clashResult.Status,
Item1 = compositeItem1,
Item2 = compositeItem2,
Center = clashResult.Center,
Distance = clashResult.Distance,
CreatedTime = DateTime.Now
};
clashResults.Add(collisionResult);
}
}
// 🔥 新增:按碰撞对象对去重
// 原因:同一个移动物体与同一个被撞物体的多个组件碰撞,需要合并为一个记录
var finalClashResults = clashResults
.GroupBy(c => new { Item1 = c.Item1, Item2 = c.Item2 })
.Select(g => g.First())
.ToList();
LogManager.Info($"[最终去重] ClashDetective结果去重: {clashResults.Count} 个碰撞 -> {finalClashResults.Count} 个唯一碰撞对");
// 缓存最终结果
lock (_clashResultsCacheLock)
{
_clashDetectiveResultsCache[_currentTestName] = finalClashResults;
}
LogManager.Info($"[ClashDetective] 已缓存结果:{finalClashResults.Count}个碰撞,测试名称:{_currentTestName}");
// 更新碰撞计数器
_clashDetectiveCollisionCount = clashResults.Count;
// 保存到数据库(使用去重后的结果)
SaveClashDetectiveResultToDatabase(pathName, routeId, finalClashResults, frameRate, duration, detectionGap, animatedObject, isVirtualVehicle,
virtualVehicleLength, virtualVehicleWidth, virtualVehicleHeight);
LogManager.Info($"[ClashDetective] 结果已保存到数据库");
// 第四步:将分组添加到主测试(由调用方完成)
return (collisionGroup, addedMainTest, confirmedCount);
}
/// <summary>
/// 动画结束后统一创建和运行ClashDetective碰撞测试
/// </summary>
@ -547,316 +817,45 @@ namespace NavisworksTransport
var doc = Application.ActiveDocument;
// 使用分组方案:创建一个主测试和一个包含所有碰撞结果的组
LogManager.Info("=== 开始分组方案:创建主测试和碰撞分组 ===");
// 过滤有效的碰撞
var validCollisions = collisionResults.Where(collision =>
collision.HasPositionInfo &&
IsModelItemValid(collision.Item1) &&
IsModelItemValid(collision.Item2)
).ToList();
if (validCollisions.Count == 0)
{
LogManager.Warning("没有有效的碰撞可以创建测试");
return;
}
LogManager.Info($"[分组测试] 有效碰撞数量: {validCollisions.Count}");
// 创建主测试名称
var mainTestName = $"碰撞检测_{DateTime.Now:MMdd_HHmmss}";
_currentTestName = mainTestName;
LogManager.Info($"[分组测试] 创建主测试: {mainTestName}");
// 移除事务以优化性能事务开销约30-65%
// 对于碰撞检测场景,事务的必要性很低,即使失败影响也很小
// 调用公共方法运行ClashDetective测试并保存到数据库
Progress progress = Application.BeginProgress("碰撞检测数据分析中,请稍候...");
ClashTest addedMainTest = null;
Progress progress = null;
ClashResultGroup collisionGroup = null;
int confirmedCount = 0;
try
{
// 开始进度条ClashDetective本身会显示进度这里只提供提示信息
progress = Application.BeginProgress(
"碰撞检测数据分析中,请稍候..."
var result = RunClashDetectiveTestsAndSaveToDatabase(
precomputedCollisions,
detectionGap,
pathName,
routeId,
animatedObject,
isVirtualVehicle,
frameRate,
duration,
virtualVehicleLength,
virtualVehicleWidth,
virtualVehicleHeight,
progress
);
// 第一步:创建主测试(不包含选择集,纯容器)
var mainTest = new ClashTest
{
DisplayName = mainTestName,
// HardConservative 模式的缺点是性能低。如果要提高性能可以用Hard模式
TestType = ClashTestType.Hard,
Tolerance = detectionGap,
Guid = Guid.Empty,
MergeComposites = true
};
// 添加主测试到文档
_documentClash.TestsData.TestsAddCopy(mainTest);
LogManager.Info($"[分组测试] 主测试已添加到文档");
// 获取添加后的测试对象引用
addedMainTest = _documentClash.TestsData.Tests.FirstOrDefault(t => t.DisplayName == mainTestName) as ClashTest;
if (addedMainTest == null)
{
LogManager.Error("无法获取添加后的主测试对象");
return;
}
// 第二步:创建分组并添加碰撞结果(应用智能去重)
// 1. 分组:按碰撞对象对分组
var groupedCollisions = validCollisions
.GroupBy(c => new { Item1 = c.Item1, Item2 = c.Item2 })
.ToList();
LogManager.Info($"[分组测试] 智能去重: {validCollisions.Count} 个检测点 -> {groupedCollisions.Count} 个唯一碰撞对");
// 缓存去重后的碰撞结果(每组取第一个)
lock (_resultsLock)
{
_deduplicatedCollisionResults.Clear();
foreach (var group in groupedCollisions)
{
_deduplicatedCollisionResults.Add(group.First());
}
LogManager.Debug($"[去重缓存] 已缓存 {_deduplicatedCollisionResults.Count} 个去重后的碰撞结果");
}
var collisionGroup = new ClashResultGroup
{
DisplayName = $"碰撞检测组 ({groupedCollisions.Count} 个唯一碰撞对)"
};
LogManager.Info($"[分组测试] 创建碰撞分组: {collisionGroup.DisplayName}");
int confirmedCount = 0;
int skippedCount = 0;
// 2. 遍历每一组
for (int groupIndex = 0; groupIndex < groupedCollisions.Count; groupIndex++)
{
// 检查用户是否取消
if (progress.IsCanceled)
{
LogManager.Info($"[分组测试] 用户取消操作,已处理 {groupIndex}/{groupedCollisions.Count} 组");
break;
}
var group = groupedCollisions[groupIndex];
// 3. 排序:按距离/重叠深度排序,优先检测最严重的碰撞
// 注意Distance通常越小或负值越大表示碰撞越深具体取决于计算方式
// 这里假设Distance越小越严重
var sortedCandidates = group.OrderBy(c => c.Distance).ToList();
bool pairConfirmed = false;
// 4. 验证即止:逐个检测候选帧
for (int i = 0; i < sortedCandidates.Count; i++)
{
var candidate = sortedCandidates[i];
try
{
// 临时移动动画对象到碰撞位置以执行测试
var testAnimatedObject = candidate.Item1;
var modelItems = new ModelItemCollection { testAnimatedObject };
var targetPosition = candidate.Item1Position;
// 计算移动偏移
var currentBounds = testAnimatedObject.BoundingBox();
var currentPos = new Point3D(
(currentBounds.Min.X + currentBounds.Max.X) / 2,
(currentBounds.Min.Y + currentBounds.Max.Y) / 2,
(currentBounds.Min.Z + currentBounds.Max.Z) / 2
);
var offset = new Vector3D(
targetPosition.X - currentPos.X,
targetPosition.Y - currentPos.Y,
targetPosition.Z - currentPos.Z
);
var transform = Transform3D.CreateTranslation(offset);
doc.Models.OverridePermanentTransform(modelItems, transform, false);
// 创建临时测试以获取真实碰撞结果
var tempTestName = $"临时验证_{confirmedCount + 1}_{i}_{DateTime.Now:HHmmss_fff}";
var tempTest = new ClashTest
{
DisplayName = tempTestName,
TestType = ClashTestType.HardConservative,
Tolerance = detectionGap,
Guid = Guid.Empty,
MergeComposites = true
};
// 设置选择集
var selectionA = new ModelItemCollection { candidate.Item1 };
var selectionB = new ModelItemCollection { candidate.Item2 };
tempTest.SelectionA.Selection.CopyFrom(selectionA);
tempTest.SelectionB.Selection.CopyFrom(selectionB);
// 添加并运行临时测试
_documentClash.TestsData.TestsAddCopy(tempTest);
var addedTempTest = _documentClash.TestsData.Tests.FirstOrDefault(t => t.DisplayName == tempTestName) as ClashTest;
if (addedTempTest != null)
{
// 设置几何类型(非关键操作,失败不影响继续执行)
var copyTest = addedTempTest.CreateCopy() as ClashTest;
copyTest.SelectionA.PrimitiveTypes = PrimitiveTypes.Triangles | PrimitiveTypes.Lines | PrimitiveTypes.Points;
copyTest.SelectionB.PrimitiveTypes = PrimitiveTypes.Triangles | PrimitiveTypes.Lines | PrimitiveTypes.Points;
_documentClash.TestsData.TestsEditTestFromCopy(addedTempTest, copyTest);
// 运行测试
_documentClash.TestsData.TestsRunTest(addedTempTest);
// 获取刷新后的测试结果
var refreshedTempTest = _documentClash.TestsData.Tests.FirstOrDefault(t => t.DisplayName == tempTestName) as ClashTest;
if (refreshedTempTest != null && refreshedTempTest.Children.Count > 0)
{
// !!!发现真实碰撞!!!
pairConfirmed = true;
confirmedCount++;
skippedCount += (sortedCandidates.Count - 1 - i); // 记录跳过的数量
// 将碰撞结果复制到分组中
int subResultIndex = 1;
foreach (var child in refreshedTempTest.Children)
{
if (child is ClashResult result)
{
var copiedResult = result.CreateCopy() as ClashResult;
copiedResult.Guid = Guid.NewGuid(); // 生成新的GUID
// 设置唯一且有意义的碰撞名称
// 先找到有意义的容器对象,再获取其名称
var container1 = ModelItemAnalysisHelper.FindNamedParentContainer(result.Item1);
var container2 = ModelItemAnalysisHelper.FindNamedParentContainer(result.Item2);
var object1Name = ModelItemAnalysisHelper.GetSafeDisplayName(container1);
var object2Name = ModelItemAnalysisHelper.GetSafeDisplayName(container2);
var timeStamp = DateTime.Now.ToString("HHmmss");
copiedResult.DisplayName = $"物流碰撞#{confirmedCount:00}-{subResultIndex:00}_{timeStamp}: {object1Name} ↔ {object2Name}";
collisionGroup.Children.Add(copiedResult);
subResultIndex++;
}
}
}
// 清理临时测试
_documentClash.TestsData.TestsRemove(refreshedTempTest ?? addedTempTest);
// 如果已确认碰撞,跳出当前候选循环,不再检测该组剩余帧
if (pairConfirmed)
{
break;
}
}
}
catch (Exception itemEx)
{
LogManager.Error($"候选检测失败: {itemEx.Message}");
}
}
}
LogManager.Info($"[分组测试] ClashDetective检测完成: 确认碰撞 {confirmedCount} 组, 跳过 {skippedCount} 个冗余检测点");
// 第三步:处理碰撞结果
// 🔥 重要ClashDetective 返回的是几何体级别的碰撞结果
// Item1 是移动物体的组件名(如"车轮1"、"车轮2"),需要统一为移动物体本身
// Item2 是被撞物体,需要向上查找有意义的父级容器
var clashResults = new List<CollisionResult>();
foreach (var child in collisionGroup.Children)
{
if (child is ClashResult clashResult)
{
// 🔥 修改Item1 直接使用移动物体本身animatedObject
// 原因ClashDetective 返回的是组件名,需要统一为移动物体本身,这样才有重复的记录可以合并
var compositeItem1 = animatedObject; // 直接使用移动物体本身
// Item2 保持现有逻辑:向上查找有意义的父级容器
var compositeItem2 = ModelItemAnalysisHelper.FindNamedParentContainer(clashResult.Item2);
var collisionResult = new CollisionResult
{
ClashGuid = clashResult.Guid,
DisplayName = clashResult.DisplayName,
Status = clashResult.Status,
Item1 = compositeItem1, // 移动物体本身
Item2 = compositeItem2, // 被撞物体的父级容器
Center = clashResult.Center,
Distance = clashResult.Distance,
CreatedTime = DateTime.Now
};
clashResults.Add(collisionResult);
}
}
// 🔥 新增:按碰撞对象对去重
// 原因:同一个移动物体与同一个被撞物体的多个组件碰撞,需要合并为一个记录
var finalClashResults = clashResults
.GroupBy(c => new { Item1 = c.Item1, Item2 = c.Item2 })
.Select(g => g.First())
.ToList();
LogManager.Info($"[最终去重] ClashDetective结果去重: {clashResults.Count} 个碰撞 -> {finalClashResults.Count} 个唯一碰撞对");
// 缓存最终结果
lock (_clashResultsCacheLock)
{
_clashDetectiveResultsCache[_currentTestName] = finalClashResults;
}
LogManager.Info($"已缓存ClashDetective结果{finalClashResults.Count}个碰撞,测试名称:{_currentTestName}");
// 更新碰撞计数器
_clashDetectiveCollisionCount = clashResults.Count;
// 保存到数据库(使用去重后的结果)
SaveClashDetectiveResultToDatabase(pathName, routeId, finalClashResults, frameRate, duration, detectionGap, animatedObject, isVirtualVehicle,
virtualVehicleLength, virtualVehicleWidth, virtualVehicleHeight);
// 第四步:将分组添加到主测试
if (collisionGroup.Children.Count > 0)
{
_documentClash.TestsData.TestsAddCopy(addedMainTest, collisionGroup);
}
else
{
LogManager.Warning("[分组测试] 分组为空(未检测到真实几何碰撞),未添加到主测试");
}
}
catch (Exception ex)
{
LogManager.Error($"[分组测试] 创建分组测试失败: {ex.Message}");
// 异常时清理已创建的主测试
if (addedMainTest != null)
{
try
{
_documentClash.TestsData.TestsRemove(addedMainTest);
LogManager.Info("[分组测试] 已清理主测试");
}
catch (Exception cleanupEx)
{
LogManager.Warning($"[分组测试] 清理主测试失败: {cleanupEx.Message}");
}
}
collisionGroup = result.collisionGroup;
addedMainTest = result.addedMainTest;
confirmedCount = result.confirmedCount;
}
finally
{
// 确保关闭进度条
if (progress != null)
{
Application.EndProgress();
}
Application.EndProgress();
}
// UI操作将分组添加到主测试
if (collisionGroup != null && addedMainTest != null && collisionGroup.Children.Count > 0)
{
_documentClash.TestsData.TestsAddCopy(addedMainTest, collisionGroup);
}
else
{
LogManager.Warning("[分组测试] 分组为空(未检测到真实几何碰撞),未添加到主测试");
}
// 碰撞测试完成后,将物体恢复到动画终点位置
@ -910,7 +909,7 @@ namespace NavisworksTransport
// 🔥 无论是否有主测试,都触发碰撞检测完成事件,通知生成报告
// 使用ClashDetective权威结果而不是预计算结果
// 如果权威结果为0个碰撞传入空列表以触发祝贺对话框
var finalCollisions = _clashDetectiveCollisionCount > 0 ? validCollisions : new List<CollisionResult>();
var finalCollisions = _clashDetectiveCollisionCount > 0 ? collisionResults : new List<CollisionResult>();
LogManager.Info($"触发CollisionDetected事件通知生成报告{_clashDetectiveCollisionCount}个碰撞,权威数据)");
var eventArgs = new CollisionDetectedEventArgs(finalCollisions);
OnCollisionDetected(eventArgs);
@ -1279,6 +1278,29 @@ namespace NavisworksTransport
}
}
/// <summary>
/// 初始化碰撞检测缓存(用于预计算前的准备工作)
/// </summary>
/// <param name="animatedObject">移动物体</param>
public static void InitializeCollisionDetectionCache(ModelItem animatedObject)
{
LogManager.Info("[碰撞检测] 开始初始化碰撞检测缓存...");
// 1. 清除所有缓存
ClearAllCaches();
// 2. 构建几何对象缓存(包含移动物体)
BuildNonHidddenGeometryItemsCache();
// 3. 构建通道对象缓存
Instance.BuildChannelObjectsCache();
// 4. 设置移动物体并重新构建几何对象缓存(排除移动物体)
SetAnimatedObject(animatedObject);
LogManager.Info("[碰撞检测] 碰撞检测缓存初始化完成");
}
/// <summary>
/// 获取通道对象缓存(供外部使用)
/// </summary>
@ -1549,7 +1571,7 @@ namespace NavisworksTransport
// 过滤有效的碰撞
var validCollisions = precomputedCollisions
.Where(result => IsModelItemValid(result.Item1) && IsModelItemValid(result.Item2))
.Where(collision => IsModelItemValid(collision.Item1) && IsModelItemValid(collision.Item2))
.ToList();
if (validCollisions.Count == 0)
@ -1562,124 +1584,38 @@ namespace NavisworksTransport
_animationCollisionCount = validCollisions.Count;
LogManager.Info($"[批处理] 原始记录: {precomputedCollisions.Count},有效碰撞: {_animationCollisionCount}");
// 创建主测试名称
var mainTestName = $"碰撞检测_{DateTime.Now:MMdd_HHmmss}";
_currentTestName = mainTestName;
LogManager.Info($"[批处理] 创建主测试: {mainTestName}");
// 调用公共方法运行ClashDetective测试并保存到数据库
var result = RunClashDetectiveTestsAndSaveToDatabase(
precomputedCollisions,
detectionGap,
pathName,
routeId,
animatedObject,
isVirtualVehicle,
frameRate,
duration,
virtualVehicleLength,
virtualVehicleWidth,
virtualVehicleHeight,
null
);
// 创建主测试
var mainTest = new ClashTest
var collisionGroup = result.collisionGroup;
var addedMainTest = result.addedMainTest;
var confirmedCount = result.confirmedCount;
// 添加分组到主测试(使用 TestsAddCopy 方法)
if (collisionGroup != null && addedMainTest != null && collisionGroup.Children.Count > 0)
{
DisplayName = mainTestName,
TestType = ClashTestType.Hard,
Tolerance = detectionGap,
Guid = Guid.Empty,
MergeComposites = true
};
// 添加主测试到文档
_documentClash.TestsData.TestsAddCopy(mainTest);
LogManager.Info($"[批处理] 主测试已添加到文档");
// 获取添加后的测试对象引用
ClashTest addedMainTest = _documentClash.TestsData.Tests.FirstOrDefault(t => t.DisplayName == mainTestName) as ClashTest;
if (addedMainTest == null)
_documentClash.TestsData.TestsAddCopy(addedMainTest, collisionGroup);
LogManager.Info($"[批处理] 碰撞分组已添加到主测试,共 {confirmedCount} 个碰撞");
}
else
{
LogManager.Error("[批处理] 无法获取添加后的主测试对象");
return null;
LogManager.Warning("[批处理] 分组为空(未检测到真实几何碰撞),未添加到主测试");
}
// 智能去重:按碰撞对象对分组
var groupedCollisions = validCollisions
.GroupBy(c => new { Item1 = c.Item1, Item2 = c.Item2 })
.ToList();
LogManager.Info($"[批处理] 智能去重: {validCollisions.Count} 个检测点 -> {groupedCollisions.Count} 个唯一碰撞对");
// 缓存去重后的碰撞结果
lock (_resultsLock)
{
_deduplicatedCollisionResults.Clear();
foreach (var group in groupedCollisions)
{
_deduplicatedCollisionResults.Add(group.First());
}
LogManager.Debug($"[批处理] 已缓存 {_deduplicatedCollisionResults.Count} 个去重后的碰撞结果");
}
// 创建碰撞分组
var collisionGroup = new ClashResultGroup
{
DisplayName = $"碰撞检测组 ({groupedCollisions.Count} 个唯一碰撞对)"
};
LogManager.Info($"[批处理] 创建碰撞分组: {collisionGroup.DisplayName}");
// 为每个唯一碰撞对创建碰撞结果
int confirmedCount = 0;
for (int groupIndex = 0; groupIndex < groupedCollisions.Count; groupIndex++)
{
var group = groupedCollisions[groupIndex];
var collision = group.First();
// 创建碰撞结果(不执行实际测试,直接创建结果)
var clashResult = new ClashResult();
// 使用反射设置只读属性因为ClashResult的Item1和Item2是只读的
var item1Property = typeof(ClashResult).GetProperty("Item1");
var item2Property = typeof(ClashResult).GetProperty("Item2");
if (item1Property != null)
{
item1Property.SetValue(clashResult, collision.Item1);
}
if (item2Property != null)
{
item2Property.SetValue(clashResult, collision.Item2);
}
clashResult.DisplayName = $"碰撞_{groupIndex + 1}";
clashResult.Status = ClashResultStatus.New;
clashResult.Center = collision.Center;
clashResult.Distance = collision.Distance;
collisionGroup.Children.Add(clashResult);
confirmedCount++;
}
// 添加分组到主测试
addedMainTest.Children.Add(collisionGroup);
LogManager.Info($"[批处理] 碰撞分组已添加到主测试,共 {confirmedCount} 个碰撞");
// 设置权威碰撞计数
_clashDetectiveCollisionCount = confirmedCount;
LogManager.Info($"[批处理] 权威碰撞计数: {_clashDetectiveCollisionCount}");
// 保存结果到数据库
try
{
// 保存到数据库(使用去重后的结果)
SaveClashDetectiveResultToDatabase(
pathName,
routeId,
_deduplicatedCollisionResults,
frameRate,
duration,
detectionGap,
animatedObject,
isVirtualVehicle,
virtualVehicleLength,
virtualVehicleWidth,
virtualVehicleHeight
);
LogManager.Info($"[批处理] 结果已保存到数据库");
}
catch (Exception ex)
{
LogManager.Error($"[批处理] 保存结果到数据库失败: {ex.Message}");
}
return mainTestName;
return addedMainTest?.DisplayName;
}
catch (Exception ex)
{

View File

@ -4,8 +4,9 @@ using System.Collections.Generic;
namespace NavisworksTransport.Core.Models
{
/// <summary>
/// 批处理碰撞检测任务
/// 批处理任务(已过时,请使用 BatchQueueItem
/// </summary>
[Obsolete("请使用 BatchQueueItem 代替")]
public class BatchCollisionTask
{
public int Id { get; set; }
@ -20,29 +21,45 @@ namespace NavisworksTransport.Core.Models
public int FailedItems { get; set; }
public DateTime LastModified { get; set; }
public List<BatchTaskItem> Items { get; set; } = new List<BatchTaskItem>();
/// <summary>
/// 进度显示文本用于UI绑定
/// </summary>
public string ProgressDisplay
{
get
{
if (TotalItems == 0) return "0/0";
return $"{CompletedItems}/{TotalItems}";
}
}
}
/// <summary>
/// 批处理任务状态
/// 批处理任务状态(已过时,请使用 BatchQueueStatus
/// </summary>
[Obsolete("请使用 BatchQueueStatus 代替")]
public enum BatchTaskStatus
{
Pending, // 等待执行
Running, // 正在执行
Completed, // 已完成
Failed, // 执行失败
Cancelled // 已取消
Pending, Running, Completed, Failed, Cancelled
}
/// <summary>
/// 批处理任务项(已过时)
/// </summary>
[Obsolete("请使用 BatchQueueItem 代替")]
public class BatchTaskItem
{
public int Id { get; set; }
public int BatchTaskId { get; set; }
public string PathRouteId { get; set; }
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; }
}
/// <summary>
/// 批处理任务项状态(已过时)
/// </summary>
[Obsolete("请使用 BatchQueueStatus 代替")]
public enum BatchTaskItemStatus
{
Pending, Running, Completed, Failed, Skipped
}
}

View File

@ -0,0 +1,55 @@
using System;
using System.Collections.Generic;
namespace NavisworksTransport.Core.Models
{
/// <summary>
/// 批处理队列项 - 简化版,只包含一条路径的碰撞检测任务
/// </summary>
public class BatchQueueItem
{
public int Id { get; set; }
public string PathRouteId { get; set; }
public string PathRouteName { get; set; }
public BatchQueueStatus Status { get; set; }
public DateTime CreatedTime { get; set; }
public DateTime? StartTime { get; set; }
public DateTime? EndTime { get; set; }
public string ErrorMessage { get; set; }
// 动画配置
public int FrameRate { get; set; }
public double DurationSeconds { get; set; }
// 碰撞检测配置
public double DetectionGapMeters { get; set; }
// 运动物体配置
public bool IsVirtualVehicle { get; set; }
public double VirtualVehicleLength { get; set; }
public double VirtualVehicleWidth { get; set; }
public double VirtualVehicleHeight { get; set; }
public string VehicleObjectId { get; set; }
public string VehicleObjectName { get; set; }
// 被检测项
public List<string> DetectionItems { get; set; } = new List<string>();
public bool DetectAllObjects { get; set; } = true;
// 结果
public string ClashDetectiveTestName { get; set; }
public int? CollisionCount { get; set; }
}
/// <summary>
/// 批处理队列项状态
/// </summary>
public enum BatchQueueStatus
{
Pending, // 等待执行
Running, // 正在执行
Completed, // 已完成
Failed, // 执行失败
Skipped // 已跳过
}
}

View File

@ -1,38 +0,0 @@
using System;
using System.Collections.Generic;
namespace NavisworksTransport.Core.Models
{
/// <summary>
/// 批处理任务项
/// </summary>
public class BatchTaskItem
{
public int Id { get; set; }
public int BatchTaskId { get; set; }
public string PathRouteId { get; set; }
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; }
}
/// <summary>
/// 批处理任务项状态
/// </summary>
public enum BatchTaskItemStatus
{
Pending, // 等待执行
Running, // 正在执行
Completed, // 已完成
Failed, // 执行失败
Skipped // 已跳过
}
}

View File

@ -28,19 +28,44 @@ namespace NavisworksTransport.Core.Models
// UI专用属性
public bool IsVirtualVehicle { get; set; }
public string VehicleSizeText { get; set; }
public double VirtualVehicleLength { get; set; } = 1.0;
public double VirtualVehicleWidth { get; set; } = 1.0;
public double VirtualVehicleHeight { get; set; } = 2.0;
// 显示属性
public string VehicleTypeDisplay => IsVirtualVehicle ? "虚拟物体" : "真实物体";
public string VehicleInfoText => IsVirtualVehicle
? $"{VirtualVehicleLength:F2}×{VirtualVehicleWidth:F2}×{VirtualVehicleHeight:F2} 米"
: (VehicleObjectName ?? "未选择");
public string DetectionItemsText => DetectionItems?.Count > 0 ? $"{DetectionItems.Count} 项" : "全部";
public string ConfigStatusText => ConfigOverride != null ? "自定义" : "默认";
public string VehicleSizeText => IsVirtualVehicle
? $"{VirtualVehicleLength:F2}×{VirtualVehicleWidth:F2}×{VirtualVehicleHeight:F2}"
: "-";
/// <summary>
/// 转换为核心配置对象
/// </summary>
public BatchTaskItemConfig ToCoreConfig()
{
var configOverride = ConfigOverride;
if (IsVirtualVehicle && configOverride == null)
{
// 如果使用虚拟物体但没有配置覆盖,创建默认配置
configOverride = new CollisionDetectionConfig
{
VirtualVehicleLength = VirtualVehicleLength,
VirtualVehicleWidth = VirtualVehicleWidth,
VirtualVehicleHeight = VirtualVehicleHeight
};
}
return new BatchTaskItemConfig
{
PathRouteId = PathRouteId,
PathRouteName = PathRouteName,
SequenceNumber = SequenceNumber,
ConfigOverride = ConfigOverride,
ConfigOverride = configOverride,
VehicleObjectId = VehicleObjectId,
VehicleObjectName = VehicleObjectName,
DetectionItems = DetectionItems

View File

@ -212,50 +212,37 @@ namespace NavisworksTransport
ExecuteNonQuery("CREATE INDEX IF NOT EXISTS idx_clash_objects_result ON ClashDetectiveCollisionObjects(ResultId)");
ExecuteNonQuery("CREATE INDEX IF NOT EXISTS idx_clash_objects_path ON ClashDetectiveCollisionObjects(PathId)");
// 8. 批处理任务表
// 8. 批处理队列表简化版只维护FIFO队列
ExecuteNonQuery(@"
CREATE TABLE IF NOT EXISTS BatchTasks (
CREATE TABLE IF NOT EXISTS BatchQueueItems (
Id INTEGER PRIMARY KEY AUTOINCREMENT,
Name TEXT NOT NULL,
Description TEXT,
CreatedTime TEXT NOT NULL,
Status TEXT NOT NULL,
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
)
");
// 9. 批处理任务项表
ExecuteNonQuery(@"
CREATE TABLE IF NOT EXISTS BatchTaskItems (
Id INTEGER PRIMARY KEY AUTOINCREMENT,
BatchTaskId INTEGER NOT NULL,
PathRouteId TEXT NOT NULL,
PathRouteName TEXT NOT NULL,
SequenceNumber INTEGER NOT NULL,
Status TEXT NOT NULL,
CreatedTime TEXT NOT NULL,
StartTime TEXT,
EndTime TEXT,
ErrorMessage TEXT,
ConfigOverride TEXT,
FrameRate INTEGER NOT NULL,
DurationSeconds REAL NOT NULL,
DetectionGapMeters REAL NOT NULL,
IsVirtualVehicle INTEGER NOT NULL,
VirtualVehicleLength REAL,
VirtualVehicleWidth REAL,
VirtualVehicleHeight REAL,
VehicleObjectId TEXT,
VehicleObjectName TEXT,
DetectionItems TEXT,
DetectAllObjects INTEGER NOT NULL DEFAULT 1,
ClashDetectiveTestName TEXT,
FOREIGN KEY (BatchTaskId) REFERENCES BatchTasks(Id) ON DELETE CASCADE
CollisionCount INTEGER
)
");
// 批处理表索引
ExecuteNonQuery("CREATE INDEX IF NOT EXISTS idx_batch_tasks_status ON BatchTasks(Status)");
ExecuteNonQuery("CREATE INDEX IF NOT EXISTS idx_batch_tasks_created ON BatchTasks(CreatedTime)");
ExecuteNonQuery("CREATE INDEX IF NOT EXISTS idx_batch_task_items_batch ON BatchTaskItems(BatchTaskId)");
ExecuteNonQuery("CREATE INDEX IF NOT EXISTS idx_batch_task_items_path ON BatchTaskItems(PathRouteId)");
ExecuteNonQuery("CREATE INDEX IF NOT EXISTS idx_batch_task_items_status ON BatchTaskItems(Status)");
ExecuteNonQuery("CREATE INDEX IF NOT EXISTS idx_batch_queue_status ON BatchQueueItems(Status)");
ExecuteNonQuery("CREATE INDEX IF NOT EXISTS idx_batch_queue_created ON BatchQueueItems(CreatedTime)");
ExecuteNonQuery("CREATE INDEX IF NOT EXISTS idx_batch_queue_path ON BatchQueueItems(PathRouteId)");
}
/// <summary>
@ -1566,11 +1553,11 @@ namespace NavisworksTransport
using (var cmd = new SQLiteCommand(_connection))
{
cmd.CommandText = @"
SELECT Id, RouteId, Name, CreatedTime, LastModified, PathData, TotalDistance, TotalDuration, CurveType, CurveTension, IsCurved
SELECT Id, Name, CreatedTime, LastModified, TotalLength
FROM PathRoutes
WHERE RouteId = @RouteId";
WHERE Id = @Id";
cmd.Parameters.AddWithValue("@RouteId", routeId);
cmd.Parameters.AddWithValue("@Id", routeId);
using (var reader = cmd.ExecuteReader())
{
@ -1578,14 +1565,15 @@ namespace NavisworksTransport
{
var route = new PathRoute(reader["Name"].ToString())
{
Id = reader["RouteId"].ToString(),
Id = reader["Id"].ToString(),
CreatedTime = DateTime.Parse(reader["CreatedTime"].ToString()),
LastModified = DateTime.Parse(reader["LastModified"].ToString()),
TotalLength = Convert.ToDouble(reader["TotalDistance"])
TotalLength = Convert.ToDouble(reader["TotalLength"])
};
// 加载路径点
// 加载路径点和路径边
LoadPathPoints(route);
LoadPathEdges(route);
return route;
}
return null;
@ -1596,6 +1584,242 @@ namespace NavisworksTransport
#endregion
#region
/// <summary>
/// 创建批处理队列项
/// </summary>
public async Task<int> CreateBatchQueueItemAsync(BatchQueueItem item)
{
return await Task.Run(() =>
{
using (var cmd = new SQLiteCommand(_connection))
{
cmd.CommandText = @"
INSERT INTO BatchQueueItems (
PathRouteId, PathRouteName, Status, CreatedTime, StartTime, EndTime, ErrorMessage,
FrameRate, DurationSeconds, DetectionGapMeters,
IsVirtualVehicle, VirtualVehicleLength, VirtualVehicleWidth, VirtualVehicleHeight,
VehicleObjectId, VehicleObjectName, DetectionItems, DetectAllObjects,
ClashDetectiveTestName, CollisionCount
)
VALUES (
@PathRouteId, @PathRouteName, @Status, @CreatedTime, @StartTime, @EndTime, @ErrorMessage,
@FrameRate, @DurationSeconds, @DetectionGapMeters,
@IsVirtualVehicle, @VirtualVehicleLength, @VirtualVehicleWidth, @VirtualVehicleHeight,
@VehicleObjectId, @VehicleObjectName, @DetectionItems, @DetectAllObjects,
@ClashDetectiveTestName, @CollisionCount
);
SELECT last_insert_rowid();";
cmd.Parameters.AddWithValue("@PathRouteId", item.PathRouteId);
cmd.Parameters.AddWithValue("@PathRouteName", item.PathRouteName ?? "");
cmd.Parameters.AddWithValue("@Status", item.Status.ToString());
cmd.Parameters.AddWithValue("@CreatedTime", item.CreatedTime.ToString("o"));
cmd.Parameters.AddWithValue("@StartTime", item.StartTime?.ToString("o") ?? "");
cmd.Parameters.AddWithValue("@EndTime", item.EndTime?.ToString("o") ?? "");
cmd.Parameters.AddWithValue("@ErrorMessage", item.ErrorMessage ?? "");
cmd.Parameters.AddWithValue("@FrameRate", item.FrameRate);
cmd.Parameters.AddWithValue("@DurationSeconds", item.DurationSeconds);
cmd.Parameters.AddWithValue("@DetectionGapMeters", item.DetectionGapMeters);
cmd.Parameters.AddWithValue("@IsVirtualVehicle", item.IsVirtualVehicle ? 1 : 0);
cmd.Parameters.AddWithValue("@VirtualVehicleLength", item.VirtualVehicleLength);
cmd.Parameters.AddWithValue("@VirtualVehicleWidth", item.VirtualVehicleWidth);
cmd.Parameters.AddWithValue("@VirtualVehicleHeight", item.VirtualVehicleHeight);
cmd.Parameters.AddWithValue("@VehicleObjectId", item.VehicleObjectId ?? "");
cmd.Parameters.AddWithValue("@VehicleObjectName", item.VehicleObjectName ?? "");
cmd.Parameters.AddWithValue("@DetectionItems", item.DetectionItems != null ? string.Join(",", item.DetectionItems) : "");
cmd.Parameters.AddWithValue("@DetectAllObjects", item.DetectAllObjects ? 1 : 0);
cmd.Parameters.AddWithValue("@ClashDetectiveTestName", item.ClashDetectiveTestName ?? "");
cmd.Parameters.AddWithValue("@CollisionCount", item.CollisionCount ?? (object)DBNull.Value);
return Convert.ToInt32(cmd.ExecuteScalar());
}
});
}
/// <summary>
/// 获取批处理队列项
/// </summary>
public async Task<BatchQueueItem> GetBatchQueueItemAsync(int itemId)
{
return await Task.Run(() =>
{
using (var cmd = new SQLiteCommand(_connection))
{
cmd.CommandText = @"
SELECT Id, PathRouteId, PathRouteName, Status, CreatedTime, StartTime, EndTime, ErrorMessage,
FrameRate, DurationSeconds, DetectionGapMeters,
IsVirtualVehicle, VirtualVehicleLength, VirtualVehicleWidth, VirtualVehicleHeight,
VehicleObjectId, VehicleObjectName, DetectionItems, DetectAllObjects,
ClashDetectiveTestName, CollisionCount
FROM BatchQueueItems
WHERE Id = @Id";
cmd.Parameters.AddWithValue("@Id", itemId);
using (var reader = cmd.ExecuteReader())
{
if (reader.Read())
{
return new BatchQueueItem
{
Id = Convert.ToInt32(reader["Id"]),
PathRouteId = reader["PathRouteId"].ToString(),
PathRouteName = reader["PathRouteName"].ToString(),
Status = (BatchQueueStatus)Enum.Parse(typeof(BatchQueueStatus), reader["Status"].ToString()),
CreatedTime = DateTime.Parse(reader["CreatedTime"].ToString()),
StartTime = reader["StartTime"].ToString() != "" ? (DateTime?)DateTime.Parse(reader["StartTime"].ToString()) : null,
EndTime = reader["EndTime"].ToString() != "" ? (DateTime?)DateTime.Parse(reader["EndTime"].ToString()) : null,
ErrorMessage = reader["ErrorMessage"].ToString(),
FrameRate = Convert.ToInt32(reader["FrameRate"]),
DurationSeconds = Convert.ToDouble(reader["DurationSeconds"]),
DetectionGapMeters = Convert.ToDouble(reader["DetectionGapMeters"]),
IsVirtualVehicle = Convert.ToBoolean(reader["IsVirtualVehicle"]),
VirtualVehicleLength = Convert.ToDouble(reader["VirtualVehicleLength"]),
VirtualVehicleWidth = Convert.ToDouble(reader["VirtualVehicleWidth"]),
VirtualVehicleHeight = Convert.ToDouble(reader["VirtualVehicleHeight"]),
VehicleObjectId = reader["VehicleObjectId"].ToString(),
VehicleObjectName = reader["VehicleObjectName"].ToString(),
DetectionItems = reader["DetectionItems"].ToString() != "" ? reader["DetectionItems"].ToString().Split(',').ToList() : new List<string>(),
DetectAllObjects = Convert.ToBoolean(reader["DetectAllObjects"]),
ClashDetectiveTestName = reader["ClashDetectiveTestName"].ToString(),
CollisionCount = !Convert.IsDBNull(reader["CollisionCount"]) ? (int?)Convert.ToInt32(reader["CollisionCount"]) : null
};
}
return null;
}
}
});
}
/// <summary>
/// 获取批处理队列项列表
/// </summary>
public async Task<List<BatchQueueItem>> GetBatchQueueItemsAsync(BatchQueueStatus? statusFilter = null, int limit = 50, int offset = 0)
{
return await Task.Run(() =>
{
var items = new List<BatchQueueItem>();
using (var cmd = new SQLiteCommand(_connection))
{
var sql = @"
SELECT Id, PathRouteId, PathRouteName, Status, CreatedTime, StartTime, EndTime, ErrorMessage,
FrameRate, DurationSeconds, DetectionGapMeters,
IsVirtualVehicle, VirtualVehicleLength, VirtualVehicleWidth, VirtualVehicleHeight,
VehicleObjectId, VehicleObjectName, DetectionItems, DetectAllObjects,
ClashDetectiveTestName, CollisionCount
FROM BatchQueueItems";
var conditions = new List<string>();
if (statusFilter.HasValue)
{
conditions.Add("Status = @Status");
cmd.Parameters.AddWithValue("@Status", statusFilter.Value.ToString());
}
if (conditions.Count > 0)
{
sql += " WHERE " + string.Join(" AND ", conditions);
}
sql += " ORDER BY CreatedTime DESC";
if (limit > 0)
{
sql += " LIMIT @Limit";
cmd.Parameters.AddWithValue("@Limit", limit);
}
if (offset > 0)
{
sql += " OFFSET @Offset";
cmd.Parameters.AddWithValue("@Offset", offset);
}
cmd.CommandText = sql;
using (var reader = cmd.ExecuteReader())
{
while (reader.Read())
{
items.Add(new BatchQueueItem
{
Id = Convert.ToInt32(reader["Id"]),
PathRouteId = reader["PathRouteId"].ToString(),
PathRouteName = reader["PathRouteName"].ToString(),
Status = (BatchQueueStatus)Enum.Parse(typeof(BatchQueueStatus), reader["Status"].ToString()),
CreatedTime = DateTime.Parse(reader["CreatedTime"].ToString()),
StartTime = reader["StartTime"].ToString() != "" ? (DateTime?)DateTime.Parse(reader["StartTime"].ToString()) : null,
EndTime = reader["EndTime"].ToString() != "" ? (DateTime?)DateTime.Parse(reader["EndTime"].ToString()) : null,
ErrorMessage = reader["ErrorMessage"].ToString(),
FrameRate = Convert.ToInt32(reader["FrameRate"]),
DurationSeconds = Convert.ToDouble(reader["DurationSeconds"]),
DetectionGapMeters = Convert.ToDouble(reader["DetectionGapMeters"]),
IsVirtualVehicle = Convert.ToBoolean(reader["IsVirtualVehicle"]),
VirtualVehicleLength = Convert.ToDouble(reader["VirtualVehicleLength"]),
VirtualVehicleWidth = Convert.ToDouble(reader["VirtualVehicleWidth"]),
VirtualVehicleHeight = Convert.ToDouble(reader["VirtualVehicleHeight"]),
VehicleObjectId = reader["VehicleObjectId"].ToString(),
VehicleObjectName = reader["VehicleObjectName"].ToString(),
DetectionItems = reader["DetectionItems"].ToString() != "" ? reader["DetectionItems"].ToString().Split(',').ToList() : new List<string>(),
DetectAllObjects = Convert.ToBoolean(reader["DetectAllObjects"]),
ClashDetectiveTestName = reader["ClashDetectiveTestName"].ToString(),
CollisionCount = !Convert.IsDBNull(reader["CollisionCount"]) ? (int?)Convert.ToInt32(reader["CollisionCount"]) : null
});
}
}
}
return items;
});
}
/// <summary>
/// 更新批处理队列项
/// </summary>
public async Task UpdateBatchQueueItemAsync(BatchQueueItem item)
{
await Task.Run(() =>
{
using (var cmd = new SQLiteCommand(_connection))
{
cmd.CommandText = @"
UPDATE BatchQueueItems
SET Status = @Status, StartTime = @StartTime, EndTime = @EndTime, ErrorMessage = @ErrorMessage,
ClashDetectiveTestName = @ClashDetectiveTestName, CollisionCount = @CollisionCount
WHERE Id = @Id";
cmd.Parameters.AddWithValue("@Status", item.Status.ToString());
cmd.Parameters.AddWithValue("@StartTime", item.StartTime?.ToString("o") ?? "");
cmd.Parameters.AddWithValue("@EndTime", item.EndTime?.ToString("o") ?? "");
cmd.Parameters.AddWithValue("@ErrorMessage", item.ErrorMessage ?? "");
cmd.Parameters.AddWithValue("@ClashDetectiveTestName", item.ClashDetectiveTestName ?? "");
cmd.Parameters.AddWithValue("@CollisionCount", item.CollisionCount ?? (object)DBNull.Value);
cmd.Parameters.AddWithValue("@Id", item.Id);
cmd.ExecuteNonQuery();
}
});
}
/// <summary>
/// 删除批处理队列项
/// </summary>
public async Task DeleteBatchQueueItemAsync(int itemId)
{
await Task.Run(() =>
{
using (var cmd = new SQLiteCommand(_connection))
{
cmd.CommandText = "DELETE FROM BatchQueueItems WHERE Id = @Id";
cmd.Parameters.AddWithValue("@Id", itemId);
cmd.ExecuteNonQuery();
}
});
}
#endregion
/// <summary>
/// 释放资源
/// </summary>

View File

@ -0,0 +1,41 @@
using System;
using System.Globalization;
using System.Windows.Data;
using NavisworksTransport.Core.Models;
namespace NavisworksTransport.UI.WPF.Converters
{
/// <summary>
/// 批处理队列状态转换器将BatchQueueStatus枚举转换为中文显示
/// </summary>
public class BatchQueueStatusConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is BatchQueueStatus status)
{
switch (status)
{
case BatchQueueStatus.Pending:
return "待处理";
case BatchQueueStatus.Running:
return "执行中";
case BatchQueueStatus.Completed:
return "已完成";
case BatchQueueStatus.Failed:
return "失败";
case BatchQueueStatus.Skipped:
return "已跳过";
default:
return "未知";
}
}
return "待处理";
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}

View File

@ -0,0 +1,24 @@
using System.Collections.Generic;
using NavisworksTransport.Core.Models;
namespace NavisworksTransport.UI.WPF.Converters
{
/// <summary>
/// 批处理队列状态辅助类,提供所有状态选项
/// </summary>
public static class BatchQueueStatusHelper
{
/// <summary>
/// 获取所有队列状态(包括"全部"选项)
/// </summary>
public static List<BatchQueueStatus?> AllStatuses => new List<BatchQueueStatus?>
{
null, // 表示"全部"
BatchQueueStatus.Pending,
BatchQueueStatus.Running,
BatchQueueStatus.Completed,
BatchQueueStatus.Failed,
BatchQueueStatus.Skipped
};
}
}

View File

@ -1,5 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
@ -9,6 +8,7 @@ using System.Windows.Input;
using Autodesk.Navisworks.Api;
using Autodesk.Navisworks.Api.Clash;
using NavisworksTransport.Core;
using NavisworksTransport.Core.Models;
using NavisworksTransport.Commands;
using NavisworksTransport.UI.WPF.Collections;
using NavisworksTransport.Utils;
@ -201,6 +201,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
private readonly Core.Animation.PathAnimationManager _pathAnimationManager;
private readonly ClashDetectiveIntegration _clashIntegration;
private readonly UIStateManager _uiStateManager;
private PathPlanningManager _pathPlanningManager;
// 动画参数相关字段(从配置初始化)
private ObservableCollection<int> _availableFrameRates;
@ -804,6 +805,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
public ICommand ClearAnimatedObjectCommand { get; private set; }
public ICommand EditObjectRotationCommand { get; private set; }
public ICommand GenerateAnimationCommand { get; private set; }
public ICommand AddToBatchQueueCommand { get; private set; }
public ICommand ApplyManualTargetsFromSelectionCommand { get; private set; }
public ICommand ClearManualTargetsCommand { get; private set; }
public ICommand RemoveManualTargetCommand { get; private set; }
@ -888,6 +890,19 @@ namespace NavisworksTransport.UI.WPF.ViewModels
#endregion
#region
/// <summary>
/// 设置PathPlanningManager用于批处理队列功能
/// </summary>
public void SetPathPlanningManager(PathPlanningManager pathPlanningManager)
{
_pathPlanningManager = pathPlanningManager;
LogManager.Info("AnimationControlViewModel已设置PathPlanningManager");
}
#endregion
#region
/// <summary>
@ -956,6 +971,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
ClearAnimatedObjectCommand = new RelayCommand(ExecuteClearAnimatedObject, () => HasSelectedAnimatedObject);
EditObjectRotationCommand = new RelayCommand(ExecuteEditObjectRotation, () => HasSelectedAnimatedObject);
GenerateAnimationCommand = new RelayCommand(ExecuteGenerateAnimation, () => CanGenerateAnimation);
AddToBatchQueueCommand = new RelayCommand(ExecuteAddToBatchQueue, () => CanGenerateAnimation);
ApplyManualTargetsFromSelectionCommand = new RelayCommand(ExecuteApplyManualTargetsFromSelection);
ClearManualTargetsCommand = new RelayCommand(ExecuteClearManualTargets, () => HasManualCollisionTargets);
RemoveManualTargetCommand = new RelayCommand<ManualCollisionTargetViewModel>(ExecuteRemoveManualTarget, target => target != null);
@ -2509,6 +2525,12 @@ namespace NavisworksTransport.UI.WPF.ViewModels
animatedObject = SelectedAnimatedObject;
}
// 初始化碰撞检测缓存(在 animatedObject 声明之后)
if (!manualModeEnabled)
{
ClashDetectiveIntegration.InitializeCollisionDetectionCache(animatedObject);
}
if (manualModeEnabled)
{
_pathAnimationManager.SetManualCollisionTargets(manualTargets, true);
@ -3456,6 +3478,78 @@ namespace NavisworksTransport.UI.WPF.ViewModels
return results;
}
/// <summary>
/// 添加到批处理队列
/// </summary>
private async void ExecuteAddToBatchQueue()
{
try
{
if (!CanGenerateAnimation)
{
UpdateMainStatus("无法添加到批处理:缺少必要条件");
LogManager.Warning("尝试添加到批处理但条件不满足");
return;
}
LogManager.Info($"[批处理] 添加路径到批处理队列: {CurrentPathRoute.Name}");
// 创建批处理队列项
var queueItem = new BatchQueueItem
{
PathRouteId = CurrentPathRoute.Id,
PathRouteName = CurrentPathRoute.Name,
Status = BatchQueueStatus.Pending,
CreatedTime = DateTime.Now,
// 动画配置
FrameRate = _animationFrameRate,
DurationSeconds = AnimationDuration,
// 碰撞检测配置
DetectionGapMeters = _detectionGap,
// 运动物体配置
IsVirtualVehicle = UseVirtualVehicle,
VirtualVehicleLength = VirtualVehicleLength,
VirtualVehicleWidth = VirtualVehicleWidth,
VirtualVehicleHeight = VirtualVehicleHeight,
VehicleObjectId = UseVirtualVehicle ? null : SelectedAnimatedObject?.ToString(),
VehicleObjectName = UseVirtualVehicle ? null : SelectedAnimatedObject?.DisplayName,
// 被检测项配置
DetectAllObjects = !IsManualCollisionTargetEnabled,
DetectionItems = IsManualCollisionTargetEnabled
? GetValidManualCollisionTargets(pruneInvalidEntries: true).Select(t => t.ToString()).ToList()
: new List<string>()
};
// 调用批处理管理器添加到队列
var batchQueueManager = Core.BatchQueueManager.Instance;
// 确保BatchQueueManager有PathPlanningManager用于获取数据库
if (_pathPlanningManager != null)
{
batchQueueManager.SetPathPlanningManager(_pathPlanningManager);
}
var itemId = await batchQueueManager.AddQueueItemAsync(queueItem);
LogManager.Info($"[批处理] 已创建队列项: {queueItem.PathRouteName}, " +
$"虚拟车辆: {queueItem.IsVirtualVehicle}, " +
$"帧率: {queueItem.FrameRate}, " +
$"时长: {queueItem.DurationSeconds:F2}秒, " +
$"ID: {itemId}");
UpdateMainStatus($"已添加到批处理队列: {queueItem.PathRouteName}");
}
catch (Exception ex)
{
LogManager.Error($"添加到批处理失败: {ex.Message}", ex);
UpdateMainStatus($"添加失败: {ex.Message}");
}
}
#endregion
}
}

View File

@ -6,29 +6,28 @@ using System.Threading.Tasks;
using System.Windows;
using System.Windows.Input;
using NavisworksTransport.Core;
using NavisworksTransport.Core.Collision;
using NavisworksTransport.Core.Models;
using NavisworksTransport.Utils;
using NavisworksTransport.UI.WPF.Commands;
using NavisworksTransport.Commands;
using NavisworksTransport.UI.WPF.Views;
namespace NavisworksTransport.UI.WPF.ViewModels
{
/// <summary>
/// 批处理任务管理ViewModel
/// 批处理队列管理ViewModel - 简化的FIFO队列界面
/// </summary>
public class BatchTaskManagementViewModel : ViewModelBase
{
#region
private PathDatabase _database;
private BatchTaskManager _taskManager;
private BatchCollisionSummaryReportGenerator _reportGenerator;
private PathPlanningManager _pathPlanningManager;
private BatchQueueManager _queueManager;
private readonly UIStateManager _uiStateManager;
private ObservableCollection<BatchCollisionTask> _tasks;
private BatchCollisionTask _selectedTask;
private BatchTaskStatus? _statusFilter;
private ObservableCollection<BatchQueueItem> _queueItems;
private BatchQueueItem _selectedItem;
private BatchQueueStatus? _statusFilter;
private string _progressText;
private bool _isExecuting;
@ -37,34 +36,34 @@ namespace NavisworksTransport.UI.WPF.ViewModels
#region
/// <summary>
/// 任务列表
/// 队列项列表
/// </summary>
public ObservableCollection<BatchCollisionTask> Tasks
public ObservableCollection<BatchQueueItem> QueueItems
{
get => _tasks;
set => SetProperty(ref _tasks, value);
get => _queueItems;
set => SetProperty(ref _queueItems, value);
}
/// <summary>
/// 选中的任务
/// 选中的队列项
/// </summary>
public BatchCollisionTask SelectedTask
public BatchQueueItem SelectedItem
{
get => _selectedTask;
set => SetProperty(ref _selectedTask, value);
get => _selectedItem;
set => SetProperty(ref _selectedItem, value);
}
/// <summary>
/// 状态筛选器
/// </summary>
public BatchTaskStatus? StatusFilter
public BatchQueueStatus? StatusFilter
{
get => _statusFilter;
set
{
if (SetProperty(ref _statusFilter, value))
{
_ = LoadTasksAsync();
_ = LoadQueueItemsAsync();
}
}
}
@ -88,34 +87,40 @@ namespace NavisworksTransport.UI.WPF.ViewModels
}
/// <summary>
/// 是否可以执行任务
/// 是否可以执行队列
/// </summary>
public bool CanExecuteTask => SelectedTask != null && SelectedTask.Status == BatchTaskStatus.Pending && !IsExecuting;
public bool CanExecuteQueue => !IsExecuting && HasPendingItems;
/// <summary>
/// 是否可以取消任务
/// 是否可以取消当前任务
/// </summary>
public bool CanCancelTask => SelectedTask != null && SelectedTask.Status == BatchTaskStatus.Running;
public bool CanCancelCurrent => IsExecuting;
/// <summary>
/// 是否可以删除任务
/// 是否可以删除选中项
/// </summary>
public bool CanDeleteTask => SelectedTask != null &&
(SelectedTask.Status == BatchTaskStatus.Pending || SelectedTask.Status == BatchTaskStatus.Completed || SelectedTask.Status == BatchTaskStatus.Failed);
public bool CanDeleteItem => SelectedItem != null &&
(SelectedItem.Status == BatchQueueStatus.Pending ||
SelectedItem.Status == BatchQueueStatus.Completed ||
SelectedItem.Status == BatchQueueStatus.Failed);
/// <summary>
/// 是否可以查看报告
/// </summary>
public bool CanViewReport => SelectedTask != null && SelectedTask.Status == BatchTaskStatus.Completed;
public bool CanViewReport => SelectedItem != null && SelectedItem.Status == BatchQueueStatus.Completed;
/// <summary>
/// 是否有待处理项
/// </summary>
public bool HasPendingItems => QueueItems?.Any(item => item.Status == BatchQueueStatus.Pending) ?? false;
#endregion
#region
public ICommand CreateTaskCommand { get; }
public ICommand ExecuteTaskCommand { get; }
public ICommand CancelTaskCommand { get; }
public ICommand DeleteTaskCommand { get; }
public ICommand ExecuteQueueCommand { get; }
public ICommand CancelCurrentCommand { get; }
public ICommand DeleteItemCommand { get; }
public ICommand ViewReportCommand { get; }
public ICommand RefreshCommand { get; }
@ -127,27 +132,23 @@ namespace NavisworksTransport.UI.WPF.ViewModels
{
try
{
// 不在构造函数中访问BatchTaskManager因为此时PathDatabase可能还未初始化
_database = null;
_taskManager = null;
_pathPlanningManager = null;
_queueManager = null;
_uiStateManager = UIStateManager.Instance;
_reportGenerator = null;
_tasks = new ObservableCollection<BatchCollisionTask>();
_queueItems = new ObservableCollection<BatchQueueItem>();
// 初始化命令
CreateTaskCommand = new RelayCommand(async () => await CreateTaskAsync(), () => !IsExecuting);
ExecuteTaskCommand = new RelayCommand(async () => await ExecuteTaskAsync(), () => CanExecuteTask);
CancelTaskCommand = new RelayCommand(CancelTask, () => CanCancelTask);
DeleteTaskCommand = new RelayCommand(async () => await DeleteTaskAsync(), () => CanDeleteTask);
ExecuteQueueCommand = new RelayCommand(async () => await ExecuteQueueAsync(), () => CanExecuteQueue);
CancelCurrentCommand = new RelayCommand(CancelCurrent, () => CanCancelCurrent);
DeleteItemCommand = new RelayCommand(async () => await DeleteItemAsync(), () => CanDeleteItem);
ViewReportCommand = new RelayCommand(async () => await ViewReportAsync(), () => CanViewReport);
RefreshCommand = new RelayCommand(async () => await LoadTasksAsync(), () => !IsExecuting);
RefreshCommand = new RelayCommand(async () => await LoadQueueItemsAsync(), () => !IsExecuting);
}
catch (Exception ex)
{
LogManager.Error($"BatchTaskManagementViewModel构造函数异常: {ex.Message}", ex);
// 确保对象处于可用状态,即使初始化失败
_tasks = new ObservableCollection<BatchCollisionTask>();
_queueItems = new ObservableCollection<BatchQueueItem>();
}
}
@ -156,62 +157,83 @@ namespace NavisworksTransport.UI.WPF.ViewModels
#region
/// <summary>
/// 设置BatchTaskManager由LogisticsControlPanel在PathDatabase初始化后调用)
/// 设置PathPlanningManager由LogisticsControlPanel在初始化时调用)
/// </summary>
public void SetBatchTaskManager(PathDatabase database)
public void SetPathPlanningManager(PathPlanningManager pathPlanningManager)
{
try
{
_database = database;
_taskManager = BatchTaskManager.Instance;
_reportGenerator = new BatchCollisionSummaryReportGenerator(_database);
_pathPlanningManager = pathPlanningManager;
_queueManager = BatchQueueManager.Instance;
// 订阅任务管理器事件
if (_taskManager != null)
// 订阅队列管理器事件
if (_queueManager != null)
{
_taskManager.TaskStarted += OnTaskStarted;
_taskManager.TaskCompleted += OnTaskCompleted;
_taskManager.TaskProgress += OnTaskProgress;
_queueManager.ItemStarted += OnItemStarted;
_queueManager.ItemCompleted += OnItemCompleted;
_queueManager.ItemProgress += OnItemProgress;
}
// 加载任务列表
_ = LoadTasksAsync();
// 加载队列列表
_ = LoadQueueItemsAsync();
LogManager.Info("[BatchTaskManagement] BatchTaskManager已设置");
LogManager.Info("[BatchQueueManagement] PathPlanningManager已设置");
}
catch (Exception ex)
{
LogManager.Error($"[BatchTaskManagement] 设置BatchTaskManager失败: {ex.Message}");
LogManager.Error($"[BatchQueueManagement] 设置PathPlanningManager失败: {ex.Message}");
}
}
/// <summary>
/// 加载任务列表
/// 获取数据库(延迟加载)
/// </summary>
public async Task LoadTasksAsync()
private PathDatabase GetDatabase()
{
return _pathPlanningManager?.GetPathDatabase();
}
/// <summary>
/// 加载队列项列表
/// </summary>
public async Task LoadQueueItemsAsync()
{
try
{
if (_taskManager == null)
var database = GetDatabase();
if (database == null)
{
LogManager.Warning("[BatchTaskManagement] BatchTaskManager未初始化");
LogManager.Warning("[BatchQueueManagement] Database未初始化");
return;
}
var tasks = await _taskManager.GetBatchTasksAsync(StatusFilter, limit: 100);
var items = await database.GetBatchQueueItemsAsync(statusFilter: StatusFilter, limit: 1000);
// 应用筛选
if (StatusFilter.HasValue)
{
items = items.Where(item => item.Status == StatusFilter.Value).ToList();
}
// 按创建时间倒序
items = items.OrderByDescending(item => item.CreatedTime).ToList();
await _uiStateManager.ExecuteUIUpdateAsync(() =>
{
Tasks.Clear();
foreach (var task in tasks)
QueueItems.Clear();
foreach (var item in items)
{
Tasks.Add(task);
QueueItems.Add(item);
}
// 通知属性变化以更新按钮状态
OnPropertyChanged(nameof(HasPendingItems));
OnPropertyChanged(nameof(CanExecuteQueue));
});
}
catch (Exception ex)
{
LogManager.Error($"[BatchTaskManagement] 加载任务列表失败: {ex.Message}");
LogManager.Error($"[BatchQueueManagement] 加载队列列表失败: {ex.Message}");
}
}
@ -220,84 +242,56 @@ namespace NavisworksTransport.UI.WPF.ViewModels
#region
/// <summary>
/// 创建任务
/// 执行队列
/// </summary>
private async Task CreateTaskAsync()
private async Task ExecuteQueueAsync()
{
try
{
var dialog = new CreateBatchTaskDialog();
if (dialog.ShowDialog() == true)
{
var taskConfig = dialog.GetTaskConfig();
if (_taskManager != null)
{
var taskId = await _taskManager.CreateBatchTaskAsync(
taskConfig.Name,
taskConfig.Description,
taskConfig.ItemConfigs
);
if (_queueManager == null) return;
LogManager.Info($"[BatchTaskManagement] 已创建批处理任务ID: {taskId}");
await LoadTasksAsync();
}
await _queueManager.ProcessQueueAsync();
LogManager.Info("[BatchQueueManagement] 已开始执行队列");
}
catch (Exception ex)
{
LogManager.Error($"[BatchQueueManagement] 执行队列失败: {ex.Message}");
}
}
/// <summary>
/// 取消当前任务
/// </summary>
private void CancelCurrent()
{
try
{
if (_queueManager != null)
{
_queueManager.CancelCurrentItem();
LogManager.Info("[BatchQueueManagement] 已取消当前任务");
}
}
catch (Exception ex)
{
LogManager.Error($"[BatchTaskManagement] 创建任务失败: {ex.Message}");
LogManager.Error($"[BatchQueueManagement] 取消任务失败: {ex.Message}");
}
}
/// <summary>
/// 执行任务
/// 删除队列项
/// </summary>
private async Task ExecuteTaskAsync()
private async Task DeleteItemAsync()
{
try
{
if (SelectedTask == null || _taskManager == null) return;
if (SelectedItem == null) return;
_taskManager.EnqueueTask(SelectedTask.Id);
LogManager.Info($"[BatchTaskManagement] 已加入执行队列任务ID: {SelectedTask.Id}");
}
catch (Exception ex)
{
LogManager.Error($"[BatchTaskManagement] 执行任务失败: {ex.Message}");
}
}
/// <summary>
/// 取消任务
/// </summary>
private void CancelTask()
{
try
{
if (_taskManager != null)
{
_taskManager.CancelCurrentTask();
LogManager.Info("[BatchTaskManagement] 已取消当前任务");
}
}
catch (Exception ex)
{
LogManager.Error($"[BatchTaskManagement] 取消任务失败: {ex.Message}");
}
}
/// <summary>
/// 删除任务
/// </summary>
private async Task DeleteTaskAsync()
{
try
{
if (SelectedTask == null || _taskManager == null) return;
var database = GetDatabase();
if (database == null) return;
var result = MessageBox.Show(
$"确定要删除任务\"{SelectedTask.Name}\"吗?",
$"确定要删除队列项\"{SelectedItem.PathRouteName}\"吗?",
"确认删除",
MessageBoxButton.YesNo,
MessageBoxImage.Question
@ -305,38 +299,43 @@ namespace NavisworksTransport.UI.WPF.ViewModels
if (result == MessageBoxResult.Yes)
{
await _taskManager.DeleteBatchTaskAsync(SelectedTask.Id);
LogManager.Info($"[BatchTaskManagement] 已删除任务ID: {SelectedTask.Id}");
await LoadTasksAsync();
await database.DeleteBatchQueueItemAsync(SelectedItem.Id);
LogManager.Info($"[BatchQueueManagement] 已删除队列项ID: {SelectedItem.Id}");
await LoadQueueItemsAsync();
}
}
catch (Exception ex)
{
LogManager.Error($"[BatchTaskManagement] 删除任务失败: {ex.Message}");
LogManager.Error($"[BatchQueueManagement] 删除队列项失败: {ex.Message}");
}
}
/// <summary>
/// 查看报告
/// 查看报告 - 复用碰撞历史列表中的报告按钮代码
/// </summary>
private async Task ViewReportAsync()
{
try
{
if (SelectedTask == null || _reportGenerator == null) return;
if (SelectedItem == null || string.IsNullOrEmpty(SelectedItem.ClashDetectiveTestName))
{
MessageBox.Show("未找到碰撞检测结果", "提示", MessageBoxButton.OK, MessageBoxImage.Information);
return;
}
var html = await _reportGenerator.GenerateSummaryReportHtmlAsync(SelectedTask.Id);
// 保存到临时文件并打开
var tempPath = System.IO.Path.Combine(System.IO.Path.GetTempPath(), $"BatchTaskReport_{SelectedTask.Id}.html");
System.IO.File.WriteAllText(tempPath, html);
System.Diagnostics.Process.Start(tempPath);
LogManager.Info($"[BatchTaskManagement] 已打开报告任务ID: {SelectedTask.Id}");
var testName = SelectedItem.ClashDetectiveTestName;
LogManager.Info($"[批处理队列] 开始生成历史碰撞报告: {testName}");
// 复用碰撞历史列表中的报告按钮代码
var command = GenerateCollisionReportCommand.CreateComprehensive(autoHighlight: false);
command.SetTestName(testName);
await command.ExecuteAsync();
}
catch (Exception ex)
{
LogManager.Error($"[BatchTaskManagement] 查看报告失败: {ex.Message}");
LogManager.Error($"[批处理队列] 生成历史报告失败: {ex.Message}", ex);
MessageBox.Show($"生成历史报告失败: {ex.Message}", "错误",
MessageBoxButton.OK, MessageBoxImage.Error);
}
}
@ -344,26 +343,26 @@ namespace NavisworksTransport.UI.WPF.ViewModels
#region
private async void OnTaskStarted(object sender, BatchTaskEventArgs e)
private async void OnItemStarted(object sender, BatchQueueItemEventArgs e)
{
await _uiStateManager.ExecuteUIUpdateAsync(() =>
{
IsExecuting = true;
ProgressText = $"正在执行任务: {e.Task.Name}";
ProgressText = $"正在执行: {e.Item.PathRouteName}";
});
}
private async void OnTaskCompleted(object sender, BatchTaskEventArgs e)
private async void OnItemCompleted(object sender, BatchQueueItemEventArgs e)
{
await _uiStateManager.ExecuteUIUpdateAsync(async () =>
{
IsExecuting = false;
ProgressText = $"任务完成: {e.Task.Name}";
await LoadTasksAsync();
ProgressText = $"完成: {e.Item.PathRouteName} - 碰撞数: {e.Item.CollisionCount ?? 0}";
await LoadQueueItemsAsync();
});
}
private async void OnTaskProgress(object sender, BatchTaskProgressEventArgs e)
private async void OnItemProgress(object sender, BatchQueueProgressEventArgs e)
{
await _uiStateManager.ExecuteUIUpdateAsync(() =>
{

View File

@ -334,7 +334,12 @@ NavisworksTransport 检测动画页签视图 - 采用与类别设置和分层管
<Button Content="生成动画"
Command="{Binding GenerateAnimationCommand}"
IsEnabled="{Binding CanGenerateAnimation}"
Style="{StaticResource SecondaryButtonStyle}"/>
Style="{StaticResource SecondaryButtonStyle}"
Margin="0,0,10,0"/>
<Button Content="添加到批处理"
Command="{Binding AddToBatchQueueCommand}"
IsEnabled="{Binding CanGenerateAnimation}"
Style="{StaticResource ActionButtonStyle}"/>
</StackPanel>
<!-- 生成状态提示 -->

View File

@ -1,11 +1,11 @@
<!--
NavisworksTransport 批处理任务管理页签视图 - 采用与其他页签一致的Navisworks 2026风格
NavisworksTransport 批处理队列管理页签视图 - 采用与其他页签一致的Navisworks 2026风格
功能说明:
1. 任务列表:显示所有批处理任务,支持状态筛选
2. 任务操作:创建、执行、取消、删除任务
3. 任务监控:实时显示任务执行进度
4. 报告查看:查看汇总报告和单独报告
1. 队列列表:显示所有批处理队列项,支持状态筛选
2. 队列操作:执行队列、取消当前、删除项
3. 队列监控:实时显示队列执行进度
4. 报告查看:查看单项碰撞报告
设计原则与Navisworks 2026风格一致480像素宽度现代化UI布局采用Border分组
-->
@ -27,8 +27,9 @@ NavisworksTransport 批处理任务管理页签视图 - 采用与其他页签一
<!-- 转换器资源 -->
<converters:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter"/>
<converters:BatchQueueStatusConverter x:Key="BatchQueueStatusConverter"/>
<!-- 批处理任务管理页面特有的样式 -->
<!-- 批处理队列管理页面特有的样式 -->
<Style x:Key="InfoTextStyle" TargetType="Label">
<Setter Property="FontSize" Value="10"/>
<Setter Property="Foreground" Value="{StaticResource NavisworksTextBrush}"/>
@ -39,29 +40,36 @@ NavisworksTransport 批处理任务管理页签视图 - 采用与其他页签一
<ScrollViewer VerticalScrollBarVisibility="Auto" Padding="10">
<StackPanel>
<!-- 区域1: 任务操作 -->
<!-- 区域1: 队列操作 -->
<Border BorderBrush="#FFD4E7FF" BorderThickness="1" CornerRadius="0" Margin="0,0,0,15" Padding="12">
<StackPanel>
<Label Content="批处理任务管理" Style="{StaticResource SectionHeaderStyle}"/>
<Label Content="批处理队列管理" Style="{StaticResource SectionHeaderStyle}"/>
<!-- 操作按钮 -->
<StackPanel Orientation="Horizontal" Margin="0,10,0,10">
<Button Content="创建任务"
Command="{Binding CreateTaskCommand}"
<Button Content="执行队列"
Command="{Binding ExecuteQueueCommand}"
Style="{StaticResource ActionButtonStyle}"/>
<Button Content="执行任务"
Command="{Binding ExecuteTaskCommand}"
<Button Content="取消当前"
Command="{Binding CancelCurrentCommand}"
Style="{StaticResource ActionButtonStyle}"/>
<Button Content="取消任务"
Command="{Binding CancelTaskCommand}"
<Button Content="删除选中"
Command="{Binding DeleteItemCommand}"
Style="{StaticResource ActionButtonStyle}"/>
<Button Content="删除任务"
Command="{Binding DeleteTaskCommand}"
<Button Content="查看报告"
Command="{Binding ViewReportCommand}"
Style="{StaticResource ActionButtonStyle}"/>
<Button Content="刷新"
Command="{Binding RefreshCommand}"
Style="{StaticResource ActionButtonStyle}"/>
</StackPanel>
<!-- 说明文本 -->
<TextBlock Text="提示:在动画控制页面点击&quot;添加到批处理&quot;按钮可添加队列项"
FontSize="10"
Foreground="{StaticResource NavisworksTextBrush}"
Margin="0,5,0,0"
TextWrapping="Wrap"/>
</StackPanel>
</Border>
@ -77,27 +85,32 @@ NavisworksTransport 批处理任务管理页签视图 - 采用与其他页签一
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Grid.Column="0" Content="任务状态:" Style="{StaticResource ParameterLabelStyle}" Width="80"/>
<Label Grid.Column="0" Content="队列状态:" Style="{StaticResource ParameterLabelStyle}" Width="80"/>
<ComboBox Grid.Column="1"
ItemsSource="{Binding StatusFilterOptions}"
ItemsSource="{x:Static converters:BatchQueueStatusHelper.AllStatuses}"
SelectedItem="{Binding StatusFilter}"
DisplayMemberPath="DisplayName"
Width="200"
HorizontalAlignment="Left"
Margin="5,0,0,0"
ToolTip="选择要显示的任务状态"/>
ToolTip="选择要显示的队列状态">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Converter={StaticResource BatchQueueStatusConverter}}"/>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</Grid>
</StackPanel>
</Border>
<!-- 区域3: 任务列表 -->
<!-- 区域3: 队列列表 -->
<Border BorderBrush="#FFD4E7FF" BorderThickness="1" CornerRadius="0" Margin="0,0,0,15" Padding="12">
<StackPanel>
<Label Content="任务列表" Style="{StaticResource SectionHeaderStyle}"/>
<Label Content="队列列表" Style="{StaticResource SectionHeaderStyle}"/>
<!-- 任务列表表格 -->
<DataGrid ItemsSource="{Binding Tasks}"
SelectedItem="{Binding SelectedTask}"
<!-- 队列列表表格 -->
<DataGrid ItemsSource="{Binding QueueItems}"
SelectedItem="{Binding SelectedItem}"
AutoGenerateColumns="False"
CanUserAddRows="False"
CanUserDeleteRows="False"
@ -107,9 +120,9 @@ NavisworksTransport 批处理任务管理页签视图 - 采用与其他页签一
Margin="0,10,0,0"
Height="300">
<DataGrid.Columns>
<DataGridTextColumn Header="名称" Binding="{Binding Name}" Width="*"/>
<DataGridTextColumn Header="状态" Binding="{Binding Status}" Width="80"/>
<DataGridTextColumn Header="进度" Binding="{Binding ProgressDisplay}" Width="100"/>
<DataGridTextColumn Header="路径名称" Binding="{Binding PathRouteName}" Width="*"/>
<DataGridTextColumn Header="状态" Binding="{Binding Status, Converter={StaticResource BatchQueueStatusConverter}}" Width="80"/>
<DataGridTextColumn Header="碰撞数" Binding="{Binding CollisionCount, StringFormat=\{0\}}" Width="60"/>
<DataGridTextColumn Header="创建时间" Binding="{Binding CreatedTime, StringFormat=yyyy-MM-dd HH:mm}" Width="120"/>
</DataGrid.Columns>
</DataGrid>
@ -127,14 +140,6 @@ NavisworksTransport 批处理任务管理页签视图 - 采用与其他页签一
Foreground="{StaticResource NavisworksTextBrush}"
Margin="0,10,0,10"
TextWrapping="Wrap"/>
<!-- 查看报告按钮 -->
<Button Content="查看汇总报告"
Command="{Binding ViewReportCommand}"
Style="{StaticResource ActionButtonStyle}"
Width="150"
HorizontalAlignment="Left"
Visibility="{Binding CanViewReport, Converter={StaticResource BoolToVisibilityConverter}}"/>
</StackPanel>
</Border>
</StackPanel>

View File

@ -72,6 +72,8 @@
<StackPanel Orientation="Horizontal" Margin="0,5,0,0">
<Button Content="添加路径" Name="btnAddPath" Click="btnAddPath_Click"
Style="{StaticResource ActionButtonStyle}" Width="100" Margin="0,0,5,0"/>
<Button Content="配置" Name="btnConfigPath" Click="btnConfigPath_Click"
Style="{StaticResource ActionButtonStyle}" Width="80" Margin="0,0,5,0"/>
<Button Content="移除路径" Name="btnRemovePath" Click="btnRemovePath_Click"
Style="{StaticResource SecondaryButtonStyle}" Width="100" Margin="0,0,5,0"/>
<Button Content="上移" Name="btnMoveUp" Click="btnMoveUp_Click"
@ -81,7 +83,7 @@
</StackPanel>
</StackPanel>
<DataGrid Grid.Row="1"
<DataGrid Grid.Row="1"
Name="dgPaths"
AutoGenerateColumns="False"
CanUserAddRows="False"
@ -93,8 +95,10 @@
<DataGrid.Columns>
<DataGridTextColumn Header="序号" Binding="{Binding SequenceNumber}" Width="50" IsReadOnly="True"/>
<DataGridTextColumn Header="路径名称" Binding="{Binding PathRouteName}" Width="*"/>
<DataGridCheckBoxColumn Header="使用虚拟车辆" Binding="{Binding IsVirtualVehicle}" Width="100"/>
<DataGridTextColumn Header="车辆尺寸" Binding="{Binding VehicleSizeText}" Width="120"/>
<DataGridTextColumn Header="物流类型" Binding="{Binding VehicleTypeDisplay}" Width="100"/>
<DataGridTextColumn Header="物流信息" Binding="{Binding VehicleInfoText}" Width="200"/>
<DataGridTextColumn Header="被检测项" Binding="{Binding DetectionItemsText}" Width="100"/>
<DataGridTextColumn Header="配置" Binding="{Binding ConfigStatusText}" Width="80"/>
</DataGrid.Columns>
</DataGrid>
</Grid>

View File

@ -4,6 +4,7 @@ using System.Collections.ObjectModel;
using System.Linq;
using System.Windows;
using NavisworksTransport.Core;
using NavisworksTransport.Core.Config;
using NavisworksTransport.Core.Models;
namespace NavisworksTransport.UI.WPF.Views
@ -50,19 +51,111 @@ namespace NavisworksTransport.UI.WPF.Views
return;
}
// TODO: 显示路径选择对话框
// 这里简化处理,直接添加示例路径
var config = new BatchTaskItemConfigUI
try
{
PathRouteId = "sample_path_" + DateTime.Now.Ticks,
PathRouteName = "示例路径 " + (_pathConfigs.Count + 1),
SequenceNumber = _pathConfigs.Count,
IsVirtualVehicle = true,
ConfigOverride = null
};
_pathConfigs.Add(config);
UpdateSequenceNumbers();
// 显示路径选择对话框
var pathDialog = new PathSelectionDialog();
if (pathDialog.ShowDialog() == true)
{
var selectedPaths = pathDialog.GetSelectedPaths();
// 从系统配置读取虚拟车辆默认值
var systemConfig = ConfigManager.Instance.Current;
foreach (var path in selectedPaths)
{
var config = new BatchTaskItemConfigUI
{
PathRouteId = path.Id,
PathRouteName = path.Name,
SequenceNumber = _pathConfigs.Count,
IsVirtualVehicle = true, // 默认使用虚拟车辆
VirtualVehicleLength = systemConfig.PathEditing.VehicleLengthMeters,
VirtualVehicleWidth = systemConfig.PathEditing.VehicleWidthMeters,
VirtualVehicleHeight = systemConfig.PathEditing.VehicleHeightMeters,
ConfigOverride = null
};
_pathConfigs.Add(config);
}
UpdateSequenceNumbers();
}
}
catch (Exception ex)
{
MessageBox.Show($"添加路径失败: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
/// <summary>
/// 配置路径
/// </summary>
private void btnConfigPath_Click(object sender, RoutedEventArgs e)
{
if (dgPaths.SelectedItem is BatchTaskItemConfigUI selected)
{
try
{
LogManager.Info($"[CreateBatchTaskDialog] 开始配置路径: {selected.PathRouteName}");
// 创建配置对话框,传入当前配置
var configDialog = new PathConfigDialog(selected.ConfigOverride);
// 设置当前值
configDialog.rbVirtualVehicle.IsChecked = selected.IsVirtualVehicle;
configDialog.rbRealObject.IsChecked = !selected.IsVirtualVehicle;
configDialog.txtVehicleLength.Text = selected.VirtualVehicleLength.ToString("F2");
configDialog.txtVehicleWidth.Text = selected.VirtualVehicleWidth.ToString("F2");
configDialog.txtVehicleHeight.Text = selected.VirtualVehicleHeight.ToString("F2");
if (!string.IsNullOrEmpty(selected.VehicleObjectName))
{
configDialog.txtSelectedObject.Text = selected.VehicleObjectName;
configDialog.txtObjectId.Text = selected.VehicleObjectId;
}
if (selected.DetectionItems != null && selected.DetectionItems.Count > 0)
{
configDialog.txtDetectionItemCount.Text = selected.DetectionItems.Count.ToString();
configDialog.rbDetectSelected.IsChecked = true;
}
if (selected.ConfigOverride != null)
{
configDialog.cbOverrideConfig.IsChecked = true;
}
if (configDialog.ShowDialog() == true)
{
var result = configDialog.GetResult();
// 更新配置
selected.IsVirtualVehicle = result.IsVirtualVehicle;
selected.VirtualVehicleLength = result.VirtualVehicleLength;
selected.VirtualVehicleWidth = result.VirtualVehicleWidth;
selected.VirtualVehicleHeight = result.VirtualVehicleHeight;
selected.VehicleObjectId = result.VehicleObjectId;
selected.VehicleObjectName = result.VehicleObjectName;
selected.DetectionItems = result.DetectionItems;
selected.ConfigOverride = result.ConfigOverride;
// 刷新显示
dgPaths.Items.Refresh();
LogManager.Info($"[CreateBatchTaskDialog] 路径配置完成: {selected.PathRouteName}");
}
}
catch (Exception ex)
{
LogManager.Error($"[CreateBatchTaskDialog] 配置路径失败: {ex.Message}\n{ex.StackTrace}");
MessageBox.Show($"配置路径失败: {ex.Message}\n\n堆栈跟踪:\n{ex.StackTrace}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
else
{
MessageBox.Show("请先选择一条路径", "提示", MessageBoxButton.OK, MessageBoxImage.Information);
}
}
/// <summary>

View File

@ -162,6 +162,13 @@ namespace NavisworksTransport.UI.WPF
// 更新引用
AnimationControlView = newAnimationControlView;
// 设置PathPlanningManager用于批处理队列功能
if (AnimationControlView?.ViewModel != null && _pathPlanningManager != null)
{
AnimationControlView.ViewModel.SetPathPlanningManager(_pathPlanningManager);
LogManager.Info("AnimationControlView初始化完成 - 已设置PathPlanningManager");
}
// 初始化时同步车辆参数
SyncVehicleParametersToAnimationView();
@ -260,19 +267,11 @@ namespace NavisworksTransport.UI.WPF
_batchTaskManagementView = newBatchTaskManagementView;
// 设置BatchTaskManager类似PathEditingView的设置方式
// 设置PathPlanningManager类似PathEditingView的设置方式
if (_batchTaskManagementView?.ViewModel != null && _pathPlanningManager != null)
{
var database = _pathPlanningManager.GetPathDatabase();
if (database != null)
{
_batchTaskManagementView.ViewModel.SetBatchTaskManager(database);
LogManager.Info("BatchTaskManagementView初始化完成 - 已设置BatchTaskManager");
}
else
{
LogManager.Info("BatchTaskManagementView初始化完成 - PathDatabase未初始化BatchTaskManager将在文档加载后设置");
}
_batchTaskManagementView.ViewModel.SetPathPlanningManager(_pathPlanningManager);
LogManager.Info("BatchTaskManagementView初始化完成 - 已设置PathPlanningManager");
}
LogManager.Info("BatchTaskManagementView初始化完成");

View File

@ -0,0 +1,182 @@
<!--
路径配置对话框
功能说明:
1. 选择车辆类型(虚拟车辆或真实物体)
2. 配置虚拟车辆尺寸
3. 选择真实物体(支持从视图或选择树中选择)
4. 选择被检测项(可选)
5. 配置覆盖参数(帧率、时长、检测间隙)
-->
<Window x:Class="NavisworksTransport.UI.WPF.Views.PathConfigDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
Title="路径配置"
Height="700"
Width="750"
WindowStartupLocation="CenterScreen"
ResizeMode="CanResize">
<Window.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="pack://application:,,,/NavisworksTransportPlugin;component/src/UI/WPF/Resources/NavisworksStyles.xaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Window.Resources>
<Grid Margin="20">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<!-- 物流类型选择 -->
<Border Grid.Row="0" BorderBrush="#FFD4E7FF" BorderThickness="1" CornerRadius="0" Padding="12" Margin="0,0,0,10">
<StackPanel>
<Label Content="物流类型" Style="{StaticResource SectionHeaderStyle}"/>
<StackPanel Orientation="Horizontal" Margin="0,10,0,0">
<RadioButton Name="rbVirtualVehicle" Content="使用虚拟物体" Margin="0,0,20,0" Checked="rbVirtualVehicle_Checked"/>
<RadioButton Name="rbRealObject" Content="使用真实物体" Checked="rbRealObject_Checked"/>
</StackPanel>
</StackPanel>
</Border>
<!-- 虚拟物体参数 -->
<Border Grid.Row="1" BorderBrush="#FFD4E7FF" BorderThickness="1" CornerRadius="0" Padding="12" Margin="0,0,0,10"
Name="borderVirtualVehicle">
<StackPanel>
<Label Content="虚拟物体尺寸(米)" Style="{StaticResource SectionHeaderStyle}"/>
<Grid Margin="0,10,0,0">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="120"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="80"/>
</Grid.ColumnDefinitions>
<Label Grid.Row="0" Grid.Column="0" Content="长度X轴:" VerticalAlignment="Center"/>
<TextBox Grid.Row="0" Grid.Column="1" Name="txtVehicleLength" Text="1.0" Margin="0,0,5,5"/>
<TextBlock Grid.Row="0" Grid.Column="2" Text="米" VerticalAlignment="Center" Margin="0,0,0,5"/>
<Label Grid.Row="1" Grid.Column="0" Content="宽度Y轴:" VerticalAlignment="Center"/>
<TextBox Grid.Row="1" Grid.Column="1" Name="txtVehicleWidth" Text="1.0" Margin="0,0,5,5"/>
<TextBlock Grid.Row="1" Grid.Column="2" Text="米" VerticalAlignment="Center" Margin="0,0,0,5"/>
<Label Grid.Row="2" Grid.Column="0" Content="高度Z轴:" VerticalAlignment="Center"/>
<TextBox Grid.Row="2" Grid.Column="1" Name="txtVehicleHeight" Text="2.0" Margin="0,0,5,5"/>
<TextBlock Grid.Row="2" Grid.Column="2" Text="米" VerticalAlignment="Center" Margin="0,0,0,5"/>
</Grid>
</StackPanel>
</Border>
<!-- 真实物体选择 -->
<Border Grid.Row="2" BorderBrush="#FFD4E7FF" BorderThickness="1" CornerRadius="0" Padding="12" Margin="0,0,0,10"
Name="borderRealObject" Visibility="Collapsed">
<StackPanel>
<Label Content="选择运动物体" Style="{StaticResource SectionHeaderStyle}"/>
<Grid Margin="0,10,0,0">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="120"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="120"/>
</Grid.ColumnDefinitions>
<Label Grid.Row="0" Grid.Column="0" Content="已选择物体:" VerticalAlignment="Center"/>
<TextBox Grid.Row="0" Grid.Column="1" Name="txtSelectedObject" IsReadOnly="True"
Text="(未选择)" Margin="0,0,5,5"/>
<Button Grid.Row="0" Grid.Column="2" Name="btnSelectFromView" Content="从视图选择"
Click="btnSelectFromView_Click" Style="{StaticResource ActionButtonStyle}" Margin="0,0,0,5"/>
<Label Grid.Row="1" Grid.Column="0" Content="物体ID:" VerticalAlignment="Center"/>
<TextBlock Grid.Row="1" Grid.Column="1" Name="txtObjectId" Text="-" VerticalAlignment="Center" Margin="0,0,0,5"/>
</Grid>
</StackPanel>
</Border>
<!-- 被检测项选择 -->
<Border Grid.Row="3" BorderBrush="#FFD4E7FF" BorderThickness="1" CornerRadius="0" Padding="12" Margin="0,0,0,10">
<StackPanel>
<Label Content="被检测项(可选)" Style="{StaticResource SectionHeaderStyle}"/>
<Grid Margin="0,10,0,0">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="120"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="120"/>
</Grid.ColumnDefinitions>
<Label Grid.Row="0" Grid.Column="0" Content="已选择项数:" VerticalAlignment="Center"/>
<TextBlock Grid.Row="0" Grid.Column="1" Name="txtDetectionItemCount" Text="0" VerticalAlignment="Center" Margin="0,0,5,5"/>
<Button Grid.Row="0" Grid.Column="2" Name="btnSelectDetectionItems" Content="选择检测项"
Click="btnSelectDetectionItems_Click" Style="{StaticResource ActionButtonStyle}" Margin="0,0,0,5"/>
<Label Grid.Row="1" Grid.Column="0" Content="检测模式:" VerticalAlignment="Center"/>
<StackPanel Grid.Row="1" Grid.Column="1" Orientation="Horizontal" Margin="0,0,0,5">
<RadioButton Name="rbDetectAll" Content="检测所有物体" IsChecked="True" Margin="0,0,20,0"/>
<RadioButton Name="rbDetectSelected" Content="仅检测选中项"/>
</StackPanel>
</Grid>
</StackPanel>
</Border>
<!-- 配置覆盖 -->
<Border Grid.Row="4" BorderBrush="#FFD4E7FF" BorderThickness="1" CornerRadius="0" Padding="12" Margin="0,0,0,10">
<StackPanel>
<Label Content="配置覆盖(可选)" Style="{StaticResource SectionHeaderStyle}"/>
<CheckBox Name="cbOverrideConfig" Content="使用自定义配置" Margin="0,10,0,0" Checked="cbOverrideConfig_Checked" Unchecked="cbOverrideConfig_Unchecked"/>
<Grid Name="gridOverrideConfig" Margin="0,10,0,0" IsEnabled="False">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="150"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="80"/>
</Grid.ColumnDefinitions>
<Label Grid.Row="0" Grid.Column="0" Content="帧率:" VerticalAlignment="Center"/>
<TextBox Grid.Row="0" Grid.Column="1" Name="txtFrameRate" Text="30" Margin="0,0,5,5"/>
<TextBlock Grid.Row="0" Grid.Column="2" Text="fps" VerticalAlignment="Center" Margin="0,0,0,5"/>
<Label Grid.Row="1" Grid.Column="0" Content="动画时长:" VerticalAlignment="Center"/>
<TextBox Grid.Row="1" Grid.Column="1" Name="txtDuration" Text="10.0" Margin="0,0,5,5"/>
<TextBlock Grid.Row="1" Grid.Column="2" Text="秒" VerticalAlignment="Center" Margin="0,0,0,5"/>
<Label Grid.Row="2" Grid.Column="0" Content="检测间隙:" VerticalAlignment="Center"/>
<TextBox Grid.Row="2" Grid.Column="1" Name="txtDetectionGap" Text="0.05" Margin="0,0,5,5"/>
<TextBlock Grid.Row="2" Grid.Column="2" Text="米" VerticalAlignment="Center" Margin="0,0,0,5"/>
</Grid>
</StackPanel>
</Border>
<!-- 底部按钮 -->
<StackPanel Grid.Row="5" Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,10,0,0">
<Button Content="确定" Name="btnOK" Click="btnOK_Click"
Style="{StaticResource ActionButtonStyle}" Width="100" Margin="0,0,10,0"/>
<Button Content="取消" Name="btnCancel" Click="btnCancel_Click"
Style="{StaticResource SecondaryButtonStyle}" Width="100"/>
</StackPanel>
</Grid>
</Window>

View File

@ -0,0 +1,297 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using Autodesk.Navisworks.Api;
using NavisworksTransport.Core.Config;
using NavisworksTransport.Core.Models;
using NavisworksTransport.Utils;
namespace NavisworksTransport.UI.WPF.Views
{
/// <summary>
/// 路径配置对话框
/// </summary>
public partial class PathConfigDialog : Window
{
private List<ModelItem> _detectionItems;
private ModelItem _selectedObject;
public PathConfigDialog(CollisionDetectionConfig existingConfig = null)
{
InitializeComponent();
_detectionItems = new List<ModelItem>();
_selectedObject = null;
// 从系统配置读取虚拟车辆默认值
var systemConfig = ConfigManager.Instance.Current;
txtVehicleLength.Text = systemConfig.PathEditing.VehicleLengthMeters.ToString("F2");
txtVehicleWidth.Text = systemConfig.PathEditing.VehicleWidthMeters.ToString("F2");
txtVehicleHeight.Text = systemConfig.PathEditing.VehicleHeightMeters.ToString("F2");
// 设置初始状态(在 InitializeComponent 之后,避免触发 Checked 事件)
rbVirtualVehicle.IsChecked = true;
// 如果有现有配置,加载它(会覆盖上面的默认值)
if (existingConfig != null)
{
LoadExistingConfig(existingConfig);
}
}
/// <summary>
/// 获取配置
/// </summary>
public PathConfigResult GetResult()
{
var result = new PathConfigResult
{
IsVirtualVehicle = rbVirtualVehicle.IsChecked == true,
VirtualVehicleLength = double.Parse(txtVehicleLength.Text),
VirtualVehicleWidth = double.Parse(txtVehicleWidth.Text),
VirtualVehicleHeight = double.Parse(txtVehicleHeight.Text),
VehicleObjectId = _selectedObject?.ToString(),
VehicleObjectName = _selectedObject?.DisplayName,
DetectionItems = _detectionItems.Select(i => i.ToString()).ToList(),
DetectAllObjects = rbDetectAll.IsChecked == true,
UseOverrideConfig = cbOverrideConfig.IsChecked == true
};
if (result.UseOverrideConfig)
{
result.ConfigOverride = new CollisionDetectionConfig
{
FrameRate = int.Parse(txtFrameRate.Text),
DurationSeconds = double.Parse(txtDuration.Text),
DetectionGapMeters = double.Parse(txtDetectionGap.Text)
};
}
return result;
}
/// <summary>
/// 加载现有配置
/// </summary>
private void LoadExistingConfig(CollisionDetectionConfig config)
{
if (config == null) return;
txtVehicleLength.Text = config.VirtualVehicleLength.ToString("F2");
txtVehicleWidth.Text = config.VirtualVehicleWidth.ToString("F2");
txtVehicleHeight.Text = config.VirtualVehicleHeight.ToString("F2");
txtFrameRate.Text = config.FrameRate.ToString();
txtDuration.Text = config.DurationSeconds.ToString("F2");
txtDetectionGap.Text = config.DetectionGapMeters.ToString("F2");
}
/// <summary>
/// 虚拟车辆选中
/// </summary>
private void rbVirtualVehicle_Checked(object sender, RoutedEventArgs e)
{
borderVirtualVehicle.Visibility = Visibility.Visible;
borderRealObject.Visibility = Visibility.Collapsed;
}
/// <summary>
/// 真实物物体选中
/// </summary>
private void rbRealObject_Checked(object sender, RoutedEventArgs e)
{
borderVirtualVehicle.Visibility = Visibility.Collapsed;
borderRealObject.Visibility = Visibility.Visible;
}
/// <summary>
/// 从视图选择物体
/// </summary>
private void btnSelectFromView_Click(object sender, RoutedEventArgs e)
{
try
{
// 获取当前选中的物体
var doc = Autodesk.Navisworks.Api.Application.ActiveDocument;
if (doc == null)
{
MessageBox.Show("没有打开的文档", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
return;
}
var selection = doc.CurrentSelection;
if (selection == null || selection.IsEmpty)
{
MessageBox.Show("请先在视图或选择树中选择物体", "提示", MessageBoxButton.OK, MessageBoxImage.Information);
return;
}
var selectedItems = selection.SelectedItems;
if (selectedItems.Count > 1)
{
MessageBox.Show("请只选择一个物体", "提示", MessageBoxButton.OK, MessageBoxImage.Information);
return;
}
_selectedObject = selectedItems.First;
txtSelectedObject.Text = _selectedObject.DisplayName;
txtObjectId.Text = _selectedObject.ToString();
LogManager.Info($"[路径配置] 已选择物体: {_selectedObject.DisplayName}");
}
catch (Exception ex)
{
MessageBox.Show($"选择物体失败: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
LogManager.Error($"[路径配置] 选择物体失败: {ex.Message}");
}
}
/// <summary>
/// 选择检测项
/// </summary>
private void btnSelectDetectionItems_Click(object sender, RoutedEventArgs e)
{
try
{
var doc = Autodesk.Navisworks.Api.Application.ActiveDocument;
if (doc == null)
{
MessageBox.Show("没有打开的文档", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
return;
}
var selection = doc.CurrentSelection;
if (selection == null || selection.IsEmpty)
{
MessageBox.Show("请先在视图或选择树中选择检测项", "提示", MessageBoxButton.OK, MessageBoxImage.Information);
return;
}
_detectionItems.Clear();
var selectedItems = selection.SelectedItems;
foreach (var item in selectedItems)
{
_detectionItems.Add(item);
}
txtDetectionItemCount.Text = _detectionItems.Count.ToString();
LogManager.Info($"[路径配置] 已选择 {_detectionItems.Count} 个检测项");
}
catch (Exception ex)
{
MessageBox.Show($"选择检测项失败: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
LogManager.Error($"[路径配置] 选择检测项失败: {ex.Message}");
}
}
/// <summary>
/// 启用配置覆盖
/// </summary>
private void cbOverrideConfig_Checked(object sender, RoutedEventArgs e)
{
gridOverrideConfig.IsEnabled = true;
}
/// <summary>
/// 禁用配置覆盖
/// </summary>
private void cbOverrideConfig_Unchecked(object sender, RoutedEventArgs e)
{
gridOverrideConfig.IsEnabled = false;
}
/// <summary>
/// 确定
/// </summary>
private void btnOK_Click(object sender, RoutedEventArgs e)
{
try
{
// 验证虚拟车辆参数
if (rbVirtualVehicle.IsChecked == true)
{
if (!double.TryParse(txtVehicleLength.Text, out var length) || length <= 0)
{
MessageBox.Show("请输入有效的车辆长度", "提示", MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}
if (!double.TryParse(txtVehicleWidth.Text, out var width) || width <= 0)
{
MessageBox.Show("请输入有效的车辆宽度", "提示", MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}
if (!double.TryParse(txtVehicleHeight.Text, out var height) || height <= 0)
{
MessageBox.Show("请输入有效的车辆高度", "提示", MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}
}
// 验证真实物体选择
if (rbRealObject.IsChecked == true && _selectedObject == null)
{
MessageBox.Show("请选择一个真实物体", "提示", MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}
// 验证配置覆盖参数
if (cbOverrideConfig.IsChecked == true)
{
if (!int.TryParse(txtFrameRate.Text, out var frameRate) || frameRate <= 0)
{
MessageBox.Show("请输入有效的帧率", "提示", MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}
if (!double.TryParse(txtDuration.Text, out var duration) || duration <= 0)
{
MessageBox.Show("请输入有效的动画时长", "提示", MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}
if (!double.TryParse(txtDetectionGap.Text, out var gap) || gap <= 0)
{
MessageBox.Show("请输入有效的检测间隙", "提示", MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}
}
DialogResult = true;
Close();
}
catch (Exception ex)
{
MessageBox.Show($"验证失败: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
/// <summary>
/// 取消
/// </summary>
private void btnCancel_Click(object sender, RoutedEventArgs e)
{
DialogResult = false;
Close();
}
}
/// <summary>
/// 路径配置结果
/// </summary>
public class PathConfigResult
{
public bool IsVirtualVehicle { get; set; }
public double VirtualVehicleLength { get; set; }
public double VirtualVehicleWidth { get; set; }
public double VirtualVehicleHeight { get; set; }
public string VehicleObjectId { get; set; }
public string VehicleObjectName { get; set; }
public List<string> DetectionItems { get; set; }
public bool DetectAllObjects { get; set; }
public bool UseOverrideConfig { get; set; }
public CollisionDetectionConfig ConfigOverride { get; set; }
}
}

View File

@ -0,0 +1,82 @@
<!--
路径选择对话框
功能说明:
1. 从数据库加载所有路径
2. 支持多选
3. 显示路径基本信息(名称、类型、长度)
-->
<Window x:Class="NavisworksTransport.UI.WPF.Views.PathSelectionDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
Title="选择路径"
Height="500"
Width="800"
WindowStartupLocation="CenterScreen"
ResizeMode="CanResize">
<Window.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="pack://application:,,,/NavisworksTransportPlugin;component/src/UI/WPF/Resources/NavisworksStyles.xaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Window.Resources>
<Grid Margin="20">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<!-- 搜索框 -->
<Border Grid.Row="0" BorderBrush="#FFD4E7FF" BorderThickness="1" CornerRadius="0" Padding="12" Margin="0,0,0,10">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Label Grid.Column="0" Content="搜索:" VerticalAlignment="Center"/>
<TextBox Grid.Column="1" Name="txtSearch" Margin="5,0,5,0" TextChanged="txtSearch_TextChanged"/>
<Button Grid.Column="2" Content="全选" Name="btnSelectAll" Click="btnSelectAll_Click"
Style="{StaticResource SecondaryButtonStyle}" Width="80" Margin="5,0,0,0"/>
</Grid>
</Border>
<!-- 路径列表 -->
<DataGrid Grid.Row="1"
Name="dgPaths"
AutoGenerateColumns="False"
CanUserAddRows="False"
CanUserDeleteRows="False"
SelectionMode="Extended"
HeadersVisibility="Column"
GridLinesVisibility="Horizontal"
Margin="0,0,0,10">
<DataGrid.Columns>
<DataGridCheckBoxColumn Header="选择" Binding="{Binding IsSelected}" Width="50"/>
<DataGridTextColumn Header="路径名称" Binding="{Binding Name}" Width="*"/>
<DataGridTextColumn Header="类型" Binding="{Binding PathTypeDisplay}" Width="100"/>
<DataGridTextColumn Header="长度" Binding="{Binding TotalLengthDisplay}" Width="100"/>
<DataGridTextColumn Header="点数" Binding="{Binding PointCount}" Width="80"/>
<DataGridTextColumn Header="创建时间" Binding="{Binding CreatedTimeDisplay}" Width="150"/>
</DataGrid.Columns>
</DataGrid>
<!-- 底部按钮 -->
<StackPanel Grid.Row="2" Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,10,0,0">
<TextBlock Name="txtSelectedCount" VerticalAlignment="Center" Margin="0,0,20,0"
Text="已选择: 0 条路径"/>
<Button Content="确定" Name="btnOK" Click="btnOK_Click"
Style="{StaticResource ActionButtonStyle}" Width="100" Margin="0,0,10,0"/>
<Button Content="取消" Name="btnCancel" Click="btnCancel_Click"
Style="{StaticResource SecondaryButtonStyle}" Width="100"/>
</StackPanel>
</Grid>
</Window>

View File

@ -0,0 +1,175 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Windows;
using NavisworksTransport.Core;
using NavisworksTransport.Core.Models;
namespace NavisworksTransport.UI.WPF.Views
{
/// <summary>
/// 路径选择对话框
/// </summary>
public partial class PathSelectionDialog : Window
{
private ObservableCollection<PathRouteDisplayItem> _allPaths;
private ObservableCollection<PathRouteDisplayItem> _filteredPaths;
public PathSelectionDialog()
{
InitializeComponent();
_allPaths = new ObservableCollection<PathRouteDisplayItem>();
_filteredPaths = new ObservableCollection<PathRouteDisplayItem>();
dgPaths.ItemsSource = _filteredPaths;
LoadPaths();
}
/// <summary>
/// 加载路径列表
/// </summary>
private void LoadPaths()
{
try
{
var database = PathPlanningManager.Instance?.GetPathDatabase();
if (database == null)
{
MessageBox.Show("数据库未初始化", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
return;
}
var routes = database.GetAllPathRoutes();
_allPaths.Clear();
foreach (var route in routes)
{
_allPaths.Add(new PathRouteDisplayItem(route));
}
_filteredPaths.Clear();
foreach (var item in _allPaths)
{
_filteredPaths.Add(item);
}
UpdateSelectedCount();
}
catch (Exception ex)
{
MessageBox.Show($"加载路径失败: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
/// <summary>
/// 获取选中的路径
/// </summary>
public List<PathRoute> GetSelectedPaths()
{
return _allPaths.Where(p => p.IsSelected).Select(p => p.Route).ToList();
}
/// <summary>
/// 搜索框文本变化
/// </summary>
private void txtSearch_TextChanged(object sender, System.Windows.Controls.TextChangedEventArgs e)
{
var searchText = txtSearch.Text.ToLower();
_filteredPaths.Clear();
foreach (var item in _allPaths)
{
if (string.IsNullOrEmpty(searchText) ||
item.Name.ToLower().Contains(searchText) ||
item.PathTypeDisplay.ToLower().Contains(searchText))
{
_filteredPaths.Add(item);
}
}
}
/// <summary>
/// 全选
/// </summary>
private void btnSelectAll_Click(object sender, RoutedEventArgs e)
{
var allSelected = _filteredPaths.All(p => p.IsSelected);
foreach (var item in _filteredPaths)
{
item.IsSelected = !allSelected;
}
UpdateSelectedCount();
}
/// <summary>
/// 更新选中数量显示
/// </summary>
private void UpdateSelectedCount()
{
txtSelectedCount.Text = $"已选择: {_allPaths.Count(p => p.IsSelected)} 条路径";
}
/// <summary>
/// 确定
/// </summary>
private void btnOK_Click(object sender, RoutedEventArgs e)
{
var selectedPaths = GetSelectedPaths();
if (selectedPaths.Count == 0)
{
MessageBox.Show("请至少选择一条路径", "提示", MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}
DialogResult = true;
Close();
}
/// <summary>
/// 取消
/// </summary>
private void btnCancel_Click(object sender, RoutedEventArgs e)
{
DialogResult = false;
Close();
}
}
/// <summary>
/// 路径显示项
/// </summary>
public class PathRouteDisplayItem
{
public PathRoute Route { get; }
public bool IsSelected { get; set; }
public string Name => Route.Name;
public string PathTypeDisplay => GetPathTypeDisplay(Route.PathType);
public string TotalLengthDisplay => $"{Route.TotalLength:F2} 米";
public int PointCount => Route.Points?.Count ?? 0;
public string CreatedTimeDisplay => Route.CreatedTime.ToString("yyyy-MM-dd HH:mm");
public PathRouteDisplayItem(PathRoute route)
{
Route = route;
IsSelected = false;
}
private string GetPathTypeDisplay(PathType pathType)
{
switch (pathType)
{
case PathType.Ground:
return "地面";
case PathType.Aerial:
return "空中";
default:
return "未知";
}
}
}
}