Files
XplorePlane/XplorePlane/ViewModels/ImageProcessing/PipelineEditorViewModel.cs
T

830 lines
32 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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("[图像链路] PublishPipelinePreviewUpdatedbitmap 为 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("[图像链路] TriggerDebouncedExecutionSourceImage 为 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('\\', '/');
}
}
}