830 lines
32 KiB
C#
830 lines
32 KiB
C#
using Microsoft.Win32;
|
||
using Prism.Events;
|
||
using Prism.Commands;
|
||
using Prism.Mvvm;
|
||
using System;
|
||
using System.Collections.ObjectModel;
|
||
using System.IO;
|
||
using System.Linq;
|
||
using System.Threading;
|
||
using System.Threading.Tasks;
|
||
using System.Windows.Input;
|
||
using System.Windows.Media.Imaging;
|
||
using XP.Common.Logging.Interfaces;
|
||
using XplorePlane.Events;
|
||
using XplorePlane.Models;
|
||
using XplorePlane.Services;
|
||
using XplorePlane.Services.Storage;
|
||
|
||
namespace XplorePlane.ViewModels
|
||
{
|
||
public class PipelineEditorViewModel : BindableBase, IPipelineEditorHostViewModel
|
||
{
|
||
private const int MaxPipelineLength = 20;
|
||
private const int DebounceDelayMs = 300;
|
||
private const string DefaultPipelineFileDisplayName = "未命名模块.xpm";
|
||
|
||
private readonly IImageProcessingService _imageProcessingService;
|
||
private readonly IPipelineExecutionService _executionService;
|
||
private readonly IPipelinePersistenceService _persistenceService;
|
||
private readonly IEventAggregator _eventAggregator;
|
||
private readonly ILoggerService _logger;
|
||
private readonly IXpDataPathService _dataPathService;
|
||
|
||
private PipelineNodeViewModel _selectedNode;
|
||
private BitmapSource _sourceImage;
|
||
private BitmapSource _previewImage;
|
||
private string _pipelineName = "新建模块";
|
||
private string _selectedDevice = string.Empty;
|
||
private bool _isExecuting;
|
||
private bool _isStatusError;
|
||
private string _statusMessage = string.Empty;
|
||
private string _pipelineFileDisplayName = DefaultPipelineFileDisplayName;
|
||
private string _currentFilePath;
|
||
private PipelineNodeViewModel _executionEndNode;
|
||
|
||
private CancellationTokenSource _executionCts;
|
||
private CancellationTokenSource _debounceCts;
|
||
|
||
public PipelineEditorViewModel(
|
||
IImageProcessingService imageProcessingService,
|
||
IPipelineExecutionService executionService,
|
||
IPipelinePersistenceService persistenceService,
|
||
IEventAggregator eventAggregator,
|
||
ILoggerService logger,
|
||
IXpDataPathService dataPathService)
|
||
{
|
||
_imageProcessingService = imageProcessingService ?? throw new ArgumentNullException(nameof(imageProcessingService));
|
||
_executionService = executionService ?? throw new ArgumentNullException(nameof(executionService));
|
||
_persistenceService = persistenceService ?? throw new ArgumentNullException(nameof(persistenceService));
|
||
_eventAggregator = eventAggregator ?? throw new ArgumentNullException(nameof(eventAggregator));
|
||
_logger = logger?.ForModule<PipelineEditorViewModel>() ?? throw new ArgumentNullException(nameof(logger));
|
||
_dataPathService = dataPathService ?? throw new ArgumentNullException(nameof(dataPathService));
|
||
|
||
PipelineNodes = new ObservableCollection<PipelineNodeViewModel>();
|
||
AvailableDevices = new ObservableCollection<string>();
|
||
|
||
AddOperatorCommand = new DelegateCommand<string>(AddOperator, CanAddOperator);
|
||
RemoveOperatorCommand = new DelegateCommand<PipelineNodeViewModel>(RemoveOperator);
|
||
ReorderOperatorCommand = new DelegateCommand<PipelineReorderArgs>(ReorderOperator);
|
||
ToggleOperatorEnabledCommand = new DelegateCommand<PipelineNodeViewModel>(ToggleOperatorEnabled);
|
||
ExecutePipelineCommand = new DelegateCommand(async () => await ExecutePipelineAsync(), () => !IsExecuting && SourceImage != null);
|
||
ExecuteToNodeCommand = new DelegateCommand<PipelineNodeViewModel>(async node => await ExecuteToNodeAsync(node), CanExecuteToNode);
|
||
ClearExecutionRangeCommand = new DelegateCommand(async () => await ClearExecutionRangeAsync(), CanClearExecutionRange);
|
||
CancelExecutionCommand = new DelegateCommand(CancelExecution, () => IsExecuting);
|
||
NewPipelineCommand = new DelegateCommand(NewPipeline);
|
||
SavePipelineCommand = new DelegateCommand(async () => await SavePipelineAsync());
|
||
SaveAsPipelineCommand = new DelegateCommand(async () => await SaveAsPipelineAsync());
|
||
DeletePipelineCommand = new DelegateCommand(async () => await DeletePipelineAsync());
|
||
LoadPipelineCommand = new DelegateCommand(async () => await LoadPipelineAsync());
|
||
LoadImageCommand = new DelegateCommand(LoadImage);
|
||
MoveNodeUpCommand = new DelegateCommand<PipelineNodeViewModel>(MoveNodeUp);
|
||
MoveNodeDownCommand = new DelegateCommand<PipelineNodeViewModel>(MoveNodeDown);
|
||
|
||
_eventAggregator.GetEvent<ManualImageLoadedEvent>()
|
||
.Subscribe(OnManualImageLoaded);
|
||
}
|
||
|
||
// ── State Properties ──────────────────────────────────────────
|
||
|
||
public ObservableCollection<PipelineNodeViewModel> PipelineNodes { get; }
|
||
public ObservableCollection<string> AvailableDevices { get; }
|
||
|
||
public PipelineNodeViewModel SelectedNode
|
||
{
|
||
get => _selectedNode;
|
||
set
|
||
{
|
||
if (SetProperty(ref _selectedNode, value) && value != null)
|
||
LoadNodeParameters(value);
|
||
}
|
||
}
|
||
|
||
public BitmapSource SourceImage
|
||
{
|
||
get => _sourceImage;
|
||
set
|
||
{
|
||
if (SetProperty(ref _sourceImage, value))
|
||
{
|
||
ExecutePipelineCommand.RaiseCanExecuteChanged();
|
||
RaisePropertyChanged(nameof(DisplayImage));
|
||
TriggerDebouncedExecution();
|
||
}
|
||
}
|
||
}
|
||
|
||
public BitmapSource PreviewImage
|
||
{
|
||
get => _previewImage;
|
||
set
|
||
{
|
||
if (SetProperty(ref _previewImage, value))
|
||
RaisePropertyChanged(nameof(DisplayImage));
|
||
}
|
||
}
|
||
|
||
public BitmapSource DisplayImage => PreviewImage ?? SourceImage;
|
||
|
||
public string PipelineName
|
||
{
|
||
get => _pipelineName;
|
||
set => SetProperty(ref _pipelineName, value);
|
||
}
|
||
|
||
public string SelectedDevice
|
||
{
|
||
get => _selectedDevice;
|
||
set => SetProperty(ref _selectedDevice, value);
|
||
}
|
||
|
||
public bool IsExecuting
|
||
{
|
||
get => _isExecuting;
|
||
private set
|
||
{
|
||
if (SetProperty(ref _isExecuting, value))
|
||
{
|
||
ExecutePipelineCommand.RaiseCanExecuteChanged();
|
||
CancelExecutionCommand.RaiseCanExecuteChanged();
|
||
}
|
||
}
|
||
}
|
||
|
||
public string StatusMessage
|
||
{
|
||
get => _statusMessage;
|
||
set => SetProperty(ref _statusMessage, value);
|
||
}
|
||
|
||
public bool IsStatusError
|
||
{
|
||
get => _isStatusError;
|
||
private set => SetProperty(ref _isStatusError, value);
|
||
}
|
||
|
||
public string PipelineFileDisplayName
|
||
{
|
||
get => _pipelineFileDisplayName;
|
||
private set => SetProperty(ref _pipelineFileDisplayName, value);
|
||
}
|
||
|
||
public PipelineNodeViewModel ExecutionEndNode
|
||
{
|
||
get => _executionEndNode;
|
||
private set
|
||
{
|
||
if (SetProperty(ref _executionEndNode, value))
|
||
{
|
||
UpdateExecutionRangeState();
|
||
ExecuteToNodeCommand.RaiseCanExecuteChanged();
|
||
ClearExecutionRangeCommand.RaiseCanExecuteChanged();
|
||
}
|
||
}
|
||
}
|
||
|
||
// ── Commands ──────────────────────────────────────────────────
|
||
|
||
public DelegateCommand<string> AddOperatorCommand { get; }
|
||
public DelegateCommand<PipelineNodeViewModel> RemoveOperatorCommand { get; }
|
||
public DelegateCommand<PipelineReorderArgs> ReorderOperatorCommand { get; }
|
||
public DelegateCommand<PipelineNodeViewModel> ToggleOperatorEnabledCommand { get; }
|
||
public DelegateCommand ExecutePipelineCommand { get; }
|
||
public DelegateCommand<PipelineNodeViewModel> ExecuteToNodeCommand { get; }
|
||
public DelegateCommand ClearExecutionRangeCommand { get; }
|
||
public DelegateCommand CancelExecutionCommand { get; }
|
||
public DelegateCommand NewPipelineCommand { get; }
|
||
public DelegateCommand SavePipelineCommand { get; }
|
||
public DelegateCommand SaveAsPipelineCommand { get; }
|
||
public DelegateCommand DeletePipelineCommand { get; }
|
||
public DelegateCommand LoadPipelineCommand { get; }
|
||
public DelegateCommand LoadImageCommand { get; }
|
||
|
||
public DelegateCommand<PipelineNodeViewModel> MoveNodeUpCommand { get; }
|
||
public DelegateCommand<PipelineNodeViewModel> MoveNodeDownCommand { get; }
|
||
|
||
ICommand IPipelineEditorHostViewModel.AddOperatorCommand => AddOperatorCommand;
|
||
ICommand IPipelineEditorHostViewModel.RemoveOperatorCommand => RemoveOperatorCommand;
|
||
ICommand IPipelineEditorHostViewModel.ReorderOperatorCommand => ReorderOperatorCommand;
|
||
ICommand IPipelineEditorHostViewModel.ToggleOperatorEnabledCommand => ToggleOperatorEnabledCommand;
|
||
ICommand IPipelineEditorHostViewModel.ExecuteToNodeCommand => ExecuteToNodeCommand;
|
||
ICommand IPipelineEditorHostViewModel.ClearExecutionRangeCommand => ClearExecutionRangeCommand;
|
||
ICommand IPipelineEditorHostViewModel.MoveNodeUpCommand => MoveNodeUpCommand;
|
||
ICommand IPipelineEditorHostViewModel.MoveNodeDownCommand => MoveNodeDownCommand;
|
||
ICommand IPipelineEditorHostViewModel.NewPipelineCommand => NewPipelineCommand;
|
||
ICommand IPipelineEditorHostViewModel.SavePipelineCommand => SavePipelineCommand;
|
||
ICommand IPipelineEditorHostViewModel.SaveAsPipelineCommand => SaveAsPipelineCommand;
|
||
ICommand IPipelineEditorHostViewModel.LoadPipelineCommand => LoadPipelineCommand;
|
||
|
||
// ── Command Implementations ───────────────────────────────────
|
||
|
||
private bool CanAddOperator(string operatorKey) =>
|
||
!string.IsNullOrWhiteSpace(operatorKey) && PipelineNodes.Count < MaxPipelineLength;
|
||
|
||
private void AddOperator(string operatorKey)
|
||
{
|
||
_logger.Debug("AddOperator 被调用,operatorKey={OperatorKey}", operatorKey);
|
||
|
||
if (string.IsNullOrWhiteSpace(operatorKey))
|
||
{
|
||
SetInfoStatus("算子键不能为空");
|
||
_logger.Warn("AddOperator 失败:operatorKey 为空");
|
||
return;
|
||
}
|
||
|
||
var available = _imageProcessingService.GetAvailableProcessors();
|
||
_logger.Debug("可用算子数量:{Count},包含 {Key}:{Contains}",
|
||
available.Count(), operatorKey, available.Contains(operatorKey));
|
||
|
||
if (!available.Contains(operatorKey))
|
||
{
|
||
SetInfoStatus($"算子 '{operatorKey}' 未注册");
|
||
_logger.Warn("AddOperator 失败:算子 {Key} 未注册", operatorKey);
|
||
return;
|
||
}
|
||
|
||
if (PipelineNodes.Count >= MaxPipelineLength)
|
||
{
|
||
SetInfoStatus($"流水线节点数已达上限({MaxPipelineLength})");
|
||
_logger.Warn("AddOperator 失败:节点数已达上限 {Max}", MaxPipelineLength);
|
||
return;
|
||
}
|
||
|
||
var displayName = _imageProcessingService.GetProcessorDisplayName(operatorKey) ?? operatorKey;
|
||
var icon = ProcessorUiMetadata.GetOperatorIcon(operatorKey);
|
||
var node = new PipelineNodeViewModel(operatorKey, displayName, icon)
|
||
{
|
||
Order = PipelineNodes.Count
|
||
};
|
||
LoadNodeParameters(node);
|
||
PipelineNodes.Add(node);
|
||
SelectedNode = node;
|
||
UpdateExecutionRangeState();
|
||
_logger.Info("节点已添加到 PipelineNodes:{Key} ({DisplayName}),当前节点数={Count}",
|
||
operatorKey, displayName, PipelineNodes.Count);
|
||
SetInfoStatus($"已添加算子:{displayName}");
|
||
TriggerDebouncedExecution();
|
||
}
|
||
|
||
private void RemoveOperator(PipelineNodeViewModel node)
|
||
{
|
||
if (node == null || !PipelineNodes.Contains(node)) return;
|
||
|
||
var removedIndex = PipelineNodes.IndexOf(node);
|
||
PipelineNodes.Remove(node);
|
||
RenumberNodes();
|
||
if (ReferenceEquals(ExecutionEndNode, node))
|
||
ExecutionEndNode = null;
|
||
else
|
||
UpdateExecutionRangeState();
|
||
SelectNeighborAfterRemoval(removedIndex);
|
||
|
||
SetInfoStatus($"已移除算子:{node.DisplayName}");
|
||
TriggerDebouncedExecution();
|
||
}
|
||
|
||
private void MoveNodeUp(PipelineNodeViewModel node)
|
||
{
|
||
if (node == null) return;
|
||
int index = PipelineNodes.IndexOf(node);
|
||
if (index <= 0) return;
|
||
PipelineNodes.Move(index, index - 1);
|
||
RenumberNodes();
|
||
UpdateExecutionRangeState();
|
||
TriggerDebouncedExecution();
|
||
}
|
||
|
||
private void MoveNodeDown(PipelineNodeViewModel node)
|
||
{
|
||
if (node == null) return;
|
||
int index = PipelineNodes.IndexOf(node);
|
||
if (index < 0 || index >= PipelineNodes.Count - 1) return;
|
||
PipelineNodes.Move(index, index + 1);
|
||
RenumberNodes();
|
||
UpdateExecutionRangeState();
|
||
TriggerDebouncedExecution();
|
||
}
|
||
|
||
private void ReorderOperator(PipelineReorderArgs args)
|
||
{
|
||
if (args == null) return;
|
||
int oldIndex = args.OldIndex;
|
||
int newIndex = args.NewIndex;
|
||
|
||
if (oldIndex < 0 || oldIndex >= PipelineNodes.Count) return;
|
||
if (newIndex < 0 || newIndex >= PipelineNodes.Count) return;
|
||
if (oldIndex == newIndex) return;
|
||
|
||
var node = PipelineNodes[oldIndex];
|
||
PipelineNodes.RemoveAt(oldIndex);
|
||
PipelineNodes.Insert(newIndex, node);
|
||
RenumberNodes();
|
||
UpdateExecutionRangeState();
|
||
SelectedNode = node;
|
||
SetInfoStatus($"已调整算子顺序:{node.DisplayName}");
|
||
TriggerDebouncedExecution();
|
||
}
|
||
|
||
private void ToggleOperatorEnabled(PipelineNodeViewModel node)
|
||
{
|
||
if (node == null || !PipelineNodes.Contains(node)) return;
|
||
|
||
node.IsEnabled = !node.IsEnabled;
|
||
SelectedNode = node;
|
||
SetInfoStatus(node.IsEnabled
|
||
? $"已启用算子:{node.DisplayName}"
|
||
: $"已停用算子:{node.DisplayName}");
|
||
TriggerDebouncedExecution();
|
||
}
|
||
|
||
private bool CanExecuteToNode(PipelineNodeViewModel node) =>
|
||
node != null && PipelineNodes.Contains(node) && !IsExecuting && SourceImage != null;
|
||
|
||
private async Task ExecuteToNodeAsync(PipelineNodeViewModel node)
|
||
{
|
||
if (!CanExecuteToNode(node))
|
||
return;
|
||
|
||
SelectedNode = node;
|
||
ExecutionEndNode = node;
|
||
await ExecutePipelineAsync();
|
||
}
|
||
|
||
private bool CanClearExecutionRange() =>
|
||
ExecutionEndNode != null && !IsExecuting;
|
||
|
||
private async Task ClearExecutionRangeAsync()
|
||
{
|
||
if (ExecutionEndNode == null)
|
||
return;
|
||
|
||
ExecutionEndNode = null;
|
||
SetInfoStatus("已切换为执行全部节点");
|
||
|
||
if (SourceImage != null)
|
||
await ExecutePipelineAsync();
|
||
}
|
||
|
||
private void RenumberNodes()
|
||
{
|
||
for (int i = 0; i < PipelineNodes.Count; i++)
|
||
PipelineNodes[i].Order = i;
|
||
}
|
||
|
||
private void SelectNeighborAfterRemoval(int removedIndex)
|
||
{
|
||
if (PipelineNodes.Count == 0)
|
||
{
|
||
SelectedNode = null;
|
||
return;
|
||
}
|
||
|
||
var nextIndex = removedIndex < PipelineNodes.Count
|
||
? removedIndex
|
||
: PipelineNodes.Count - 1;
|
||
SelectedNode = PipelineNodes[nextIndex];
|
||
}
|
||
|
||
private void LoadNodeParameters(PipelineNodeViewModel node)
|
||
{
|
||
var paramDefs = _imageProcessingService.GetProcessorParameters(node.OperatorKey);
|
||
if (paramDefs == null) return;
|
||
|
||
// 保留已有参数快照
|
||
var snapshot = node.Parameters.ToDictionary(p => p.Name, p => p.Value);
|
||
node.Parameters.Clear();
|
||
|
||
foreach (var paramDef in paramDefs)
|
||
{
|
||
var vm = new ProcessorParameterVM(paramDef);
|
||
if (snapshot.TryGetValue(paramDef.Name, out var savedValue))
|
||
vm.Value = savedValue;
|
||
vm.PropertyChanged += (_, e) =>
|
||
{
|
||
if (e.PropertyName == nameof(ProcessorParameterVM.Value))
|
||
{
|
||
if (TryReportInvalidParameters())
|
||
return;
|
||
|
||
TriggerDebouncedExecution();
|
||
}
|
||
};
|
||
node.Parameters.Add(vm);
|
||
}
|
||
}
|
||
|
||
private async Task ExecutePipelineAsync()
|
||
{
|
||
if (SourceImage == null || IsExecuting)
|
||
{
|
||
_logger.Debug("[图像链路] ExecutePipelineAsync:跳过,SourceImage={HasImage},IsExecuting={IsExec}",
|
||
SourceImage != null, IsExecuting);
|
||
return;
|
||
}
|
||
|
||
if (TryReportInvalidParameters())
|
||
return;
|
||
|
||
_executionCts?.Cancel();
|
||
_executionCts = new CancellationTokenSource();
|
||
var token = _executionCts.Token;
|
||
var executionNodes = GetNodesInExecutionScope()
|
||
.Where(n => n.IsEnabled)
|
||
.OrderBy(n => n.Order)
|
||
.ToList();
|
||
|
||
IsExecuting = true;
|
||
SetInfoStatus(BuildExecutionStartMessage(executionNodes.Count));
|
||
_logger.Info("[图像链路] ExecutePipelineAsync:开始执行,范围节点数={Count},截止节点={Node}",
|
||
executionNodes.Count, ExecutionEndNode?.DisplayName ?? "<all>");
|
||
|
||
try
|
||
{
|
||
var progress = new Progress<PipelineProgress>(p =>
|
||
SetInfoStatus($"执行中:{p.CurrentOperator} ({p.CurrentStep}/{p.TotalSteps})"));
|
||
|
||
var result = await _executionService.ExecutePipelineAsync(
|
||
executionNodes, SourceImage, progress, token);
|
||
|
||
PreviewImage = result;
|
||
SetInfoStatus(BuildExecutionCompletedMessage(executionNodes.Count));
|
||
_logger.Info("[图像链路] ExecutePipelineAsync:执行完成,准备发布 PipelinePreviewUpdatedEvent");
|
||
PublishPipelinePreviewUpdated(result, StatusMessage);
|
||
}
|
||
catch (OperationCanceledException)
|
||
{
|
||
SetInfoStatus("流水线执行已取消");
|
||
_logger.Info("[图像链路] ExecutePipelineAsync:执行已取消");
|
||
}
|
||
catch (PipelineExecutionException ex)
|
||
{
|
||
SetErrorStatus($"执行失败:{ex.Message}");
|
||
_logger.Warn("[图像链路] ExecutePipelineAsync:执行失败 {Msg}", ex.Message);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
SetErrorStatus($"执行错误:{ex.Message}");
|
||
_logger.Error(ex, "[图像链路] ExecutePipelineAsync:未预期异常");
|
||
}
|
||
finally
|
||
{
|
||
IsExecuting = false;
|
||
}
|
||
}
|
||
|
||
private bool TryReportInvalidParameters()
|
||
{
|
||
var firstInvalidNode = GetNodesInExecutionScope()
|
||
.Where(n => n.IsEnabled)
|
||
.OrderBy(n => n.Order)
|
||
.FirstOrDefault(n => n.Parameters.Any(p => !p.IsValueValid));
|
||
|
||
if (firstInvalidNode == null)
|
||
return false;
|
||
|
||
var invalidNames = firstInvalidNode.Parameters
|
||
.Where(p => !p.IsValueValid)
|
||
.Select(p => p.DisplayName);
|
||
SetErrorStatus($"参数错误:算子 '{firstInvalidNode.DisplayName}' 的 {string.Join("、", invalidNames)} 输入不合理,请修正后重试。");
|
||
return true;
|
||
}
|
||
|
||
private void SetInfoStatus(string message)
|
||
{
|
||
IsStatusError = false;
|
||
StatusMessage = message;
|
||
}
|
||
|
||
private void SetErrorStatus(string message)
|
||
{
|
||
IsStatusError = true;
|
||
StatusMessage = message;
|
||
PublishPipelinePreviewUpdated(PreviewImage ?? SourceImage, message);
|
||
}
|
||
|
||
private void LoadImage()
|
||
{
|
||
var dialog = new OpenFileDialog
|
||
{
|
||
Title = "加载图像",
|
||
Filter = "图像文件|*.bmp;*.png;*.jpg;*.jpeg;*.tif;*.tiff|所有文件|*.*"
|
||
};
|
||
|
||
if (dialog.ShowDialog() != true)
|
||
return;
|
||
|
||
try
|
||
{
|
||
LoadImageFromFile(dialog.FileName);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
SetErrorStatus($"加载图像失败:{ex.Message}");
|
||
_logger.Error(ex, "加载图像失败:{Path}", dialog.FileName);
|
||
}
|
||
}
|
||
|
||
internal void LoadImageFromFile(string filePath)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(filePath))
|
||
throw new ArgumentException("图像路径不能为空", nameof(filePath));
|
||
|
||
var bitmap = new BitmapImage();
|
||
bitmap.BeginInit();
|
||
bitmap.UriSource = new Uri(filePath, UriKind.Absolute);
|
||
bitmap.CacheOption = BitmapCacheOption.OnLoad;
|
||
bitmap.EndInit();
|
||
bitmap.Freeze();
|
||
|
||
SourceImage = bitmap;
|
||
PreviewImage = bitmap;
|
||
SetInfoStatus($"已加载图像:{Path.GetFileName(filePath)}");
|
||
PublishManualImageLoaded(bitmap, filePath);
|
||
}
|
||
|
||
internal void LoadImageFromBitmap(BitmapSource bitmap, string filePath, bool runPipeline = true)
|
||
{
|
||
if (bitmap == null)
|
||
throw new ArgumentNullException(nameof(bitmap));
|
||
|
||
SourceImage = bitmap;
|
||
PreviewImage = bitmap;
|
||
SetInfoStatus($"已加载图像:{Path.GetFileName(filePath)}");
|
||
PublishManualImageLoaded(bitmap, filePath);
|
||
|
||
if (runPipeline)
|
||
TriggerDebouncedExecution();
|
||
}
|
||
|
||
private void PublishManualImageLoaded(BitmapSource bitmap, string filePath)
|
||
{
|
||
_eventAggregator.GetEvent<ManualImageLoadedEvent>()
|
||
.Publish(new ManualImageLoadedPayload(bitmap, filePath));
|
||
}
|
||
|
||
private void PublishPipelinePreviewUpdated(BitmapSource bitmap, string statusMessage)
|
||
{
|
||
if (bitmap == null)
|
||
{
|
||
_logger.Warn("[图像链路] PublishPipelinePreviewUpdated:bitmap 为 null,跳过发布");
|
||
return;
|
||
}
|
||
|
||
_logger.Info("[图像链路] PublishPipelinePreviewUpdated:发布事件,statusMessage={Msg}", statusMessage);
|
||
_eventAggregator.GetEvent<PipelinePreviewUpdatedEvent>()
|
||
.Publish(new PipelinePreviewUpdatedPayload(bitmap, statusMessage));
|
||
}
|
||
|
||
private void OnManualImageLoaded(ManualImageLoadedPayload payload)
|
||
{
|
||
if (payload?.Image == null) return;
|
||
if (ReferenceEquals(SourceImage, payload.Image)) return;
|
||
|
||
_logger.Info("[图像链路] OnManualImageLoaded:收到图像 {File},设置 SourceImage", payload.FileName);
|
||
SourceImage = payload.Image;
|
||
PreviewImage = payload.Image;
|
||
SetInfoStatus($"已加载图像:{payload.FileName}");
|
||
}
|
||
|
||
private void CancelExecution()
|
||
{
|
||
_executionCts?.Cancel();
|
||
}
|
||
|
||
private void TriggerDebouncedExecution()
|
||
{
|
||
if (SourceImage == null)
|
||
{
|
||
_logger.Debug("[图像链路] TriggerDebouncedExecution:SourceImage 为 null,跳过执行");
|
||
return;
|
||
}
|
||
|
||
_logger.Debug("[图像链路] TriggerDebouncedExecution:触发防抖执行,节点数={Count}", PipelineNodes.Count);
|
||
_debounceCts?.Cancel();
|
||
_debounceCts = new CancellationTokenSource();
|
||
var token = _debounceCts.Token;
|
||
|
||
Task.Delay(DebounceDelayMs, token).ContinueWith(t =>
|
||
{
|
||
if (!t.IsCanceled)
|
||
_ = ExecutePipelineAsync();
|
||
}, TaskScheduler.FromCurrentSynchronizationContext());
|
||
}
|
||
|
||
private void NewPipeline()
|
||
{
|
||
PipelineNodes.Clear();
|
||
SelectedNode = null;
|
||
ExecutionEndNode = null;
|
||
PipelineName = "新建流水线";
|
||
PreviewImage = null;
|
||
_currentFilePath = null;
|
||
PipelineFileDisplayName = DefaultPipelineFileDisplayName;
|
||
SetInfoStatus("已新建流水线");
|
||
}
|
||
|
||
private async Task SavePipelineAsync()
|
||
{
|
||
if (string.IsNullOrWhiteSpace(PipelineName) || PipelineName.Length > 100)
|
||
{
|
||
SetInfoStatus("流水线名称不能为空且长度不超过 100 个字符");
|
||
return;
|
||
}
|
||
|
||
if (string.IsNullOrEmpty(_currentFilePath))
|
||
{
|
||
await SaveAsPipelineAsync();
|
||
return;
|
||
}
|
||
|
||
await SaveToFileAsync(_currentFilePath);
|
||
}
|
||
|
||
private async Task SaveAsPipelineAsync()
|
||
{
|
||
if (string.IsNullOrWhiteSpace(PipelineName) || PipelineName.Length > 100)
|
||
{
|
||
SetInfoStatus("流水线名称不能为空且长度不超过 100 个字符");
|
||
return;
|
||
}
|
||
|
||
var dialog = new SaveFileDialog
|
||
{
|
||
Filter = "XP 模块 (*.xpm)|*.xpm",
|
||
DefaultExt = ".xpm",
|
||
AddExtension = true,
|
||
FileName = PipelineName,
|
||
InitialDirectory = GetPipelineDirectory()
|
||
};
|
||
|
||
if (dialog.ShowDialog() == true)
|
||
{
|
||
_currentFilePath = dialog.FileName;
|
||
await SaveToFileAsync(_currentFilePath);
|
||
}
|
||
}
|
||
|
||
private async Task SaveToFileAsync(string filePath)
|
||
{
|
||
try
|
||
{
|
||
var model = BuildPipelineModel();
|
||
await _persistenceService.SaveAsync(model, filePath);
|
||
PipelineFileDisplayName = FormatPipelinePath(filePath);
|
||
SetInfoStatus($"流水线已保存:{Path.GetFileName(filePath)}");
|
||
}
|
||
catch (IOException ex)
|
||
{
|
||
SetErrorStatus($"保存失败:{ex.Message}");
|
||
}
|
||
}
|
||
|
||
private async Task DeletePipelineAsync()
|
||
{
|
||
if (string.IsNullOrEmpty(_currentFilePath)) return;
|
||
|
||
try
|
||
{
|
||
if (File.Exists(_currentFilePath))
|
||
File.Delete(_currentFilePath);
|
||
|
||
NewPipeline();
|
||
SetInfoStatus("流水线已删除");
|
||
}
|
||
catch (IOException ex)
|
||
{
|
||
SetErrorStatus($"删除失败:{ex.Message}");
|
||
}
|
||
await Task.CompletedTask;
|
||
}
|
||
|
||
private async Task LoadPipelineAsync()
|
||
{
|
||
var dialog = new OpenFileDialog
|
||
{
|
||
Filter = "XP 模块 (*.xpm)|*.xpm",
|
||
DefaultExt = ".xpm",
|
||
InitialDirectory = GetPipelineDirectory()
|
||
};
|
||
|
||
if (dialog.ShowDialog() != true) return;
|
||
|
||
try
|
||
{
|
||
var model = await _persistenceService.LoadAsync(dialog.FileName);
|
||
|
||
PipelineNodes.Clear();
|
||
SelectedNode = null;
|
||
ExecutionEndNode = null;
|
||
|
||
PipelineName = model.Name;
|
||
SelectedDevice = model.DeviceId;
|
||
_currentFilePath = dialog.FileName;
|
||
PipelineFileDisplayName = FormatPipelinePath(dialog.FileName);
|
||
|
||
foreach (var nodeModel in model.Nodes)
|
||
{
|
||
var displayName = _imageProcessingService.GetProcessorDisplayName(nodeModel.OperatorKey)
|
||
?? nodeModel.OperatorKey;
|
||
var icon = ProcessorUiMetadata.GetOperatorIcon(nodeModel.OperatorKey);
|
||
var node = new PipelineNodeViewModel(nodeModel.OperatorKey, displayName, icon)
|
||
{
|
||
Order = nodeModel.Order,
|
||
IsEnabled = nodeModel.IsEnabled
|
||
};
|
||
LoadNodeParameters(node);
|
||
|
||
// 恢复已保存的参数值
|
||
foreach (var param in node.Parameters)
|
||
{
|
||
if (nodeModel.Parameters.TryGetValue(param.Name, out var savedValue))
|
||
param.Value = savedValue;
|
||
}
|
||
|
||
PipelineNodes.Add(node);
|
||
}
|
||
|
||
UpdateExecutionRangeState();
|
||
|
||
_logger.Info("流水线已加载:{Name},节点数={Count}", model.Name, PipelineNodes.Count);
|
||
SetInfoStatus($"已加载流水线:{model.Name}({PipelineNodes.Count} 个节点)");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.Warn("加载流水线失败:{Error}", ex.Message);
|
||
SetErrorStatus($"加载失败:{ex.Message}");
|
||
}
|
||
}
|
||
|
||
private PipelineModel BuildPipelineModel()
|
||
{
|
||
return new PipelineModel
|
||
{
|
||
Name = PipelineName,
|
||
DeviceId = SelectedDevice,
|
||
UpdatedAt = DateTime.UtcNow,
|
||
Nodes = PipelineNodes.Select(n => new PipelineNodeModel
|
||
{
|
||
Id = n.Id,
|
||
OperatorKey = n.OperatorKey,
|
||
Order = n.Order,
|
||
IsEnabled = n.IsEnabled,
|
||
Parameters = n.Parameters.ToDictionary(p => p.Name, p => p.Value)
|
||
}).ToList()
|
||
};
|
||
}
|
||
|
||
private System.Collections.Generic.IEnumerable<PipelineNodeViewModel> GetNodesInExecutionScope()
|
||
{
|
||
var orderedNodes = PipelineNodes.OrderBy(n => n.Order);
|
||
if (ExecutionEndNode == null)
|
||
return orderedNodes;
|
||
|
||
return orderedNodes.Where(n => n.Order <= ExecutionEndNode.Order);
|
||
}
|
||
|
||
private void UpdateExecutionRangeState()
|
||
{
|
||
if (_executionEndNode != null && !PipelineNodes.Contains(_executionEndNode))
|
||
_executionEndNode = null;
|
||
|
||
var endOrder = _executionEndNode?.Order;
|
||
foreach (var node in PipelineNodes)
|
||
{
|
||
node.IsExecutionEndNode = endOrder.HasValue && node.Order == endOrder.Value;
|
||
node.IsSkippedByExecutionRange = endOrder.HasValue && node.Order > endOrder.Value;
|
||
}
|
||
}
|
||
|
||
private string BuildExecutionStartMessage(int executionCount)
|
||
{
|
||
if (ExecutionEndNode == null)
|
||
return "正在执行流水线...";
|
||
|
||
return $"正在执行到“{ExecutionEndNode.DisplayName}” ({executionCount} 个有效节点)...";
|
||
}
|
||
|
||
private string BuildExecutionCompletedMessage(int executionCount)
|
||
{
|
||
if (ExecutionEndNode == null)
|
||
return "流水线执行完成";
|
||
|
||
return $"已执行到“{ExecutionEndNode.DisplayName}” ({executionCount} 个有效节点)";
|
||
}
|
||
|
||
private string GetPipelineDirectory()
|
||
{
|
||
var dir = _dataPathService.ToolsPath;
|
||
Directory.CreateDirectory(dir);
|
||
return dir;
|
||
}
|
||
|
||
private static string FormatPipelinePath(string filePath)
|
||
{
|
||
return string.IsNullOrWhiteSpace(filePath)
|
||
? DefaultPipelineFileDisplayName
|
||
: Path.GetFullPath(filePath).Replace('\\', '/');
|
||
}
|
||
}
|
||
}
|