Files
XplorePlane/XplorePlane/ViewModels/ImageProcessing/PipelineEditorViewModel.cs
T
2026-03-16 17:12:51 +08:00

518 lines
19 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 System;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Media.Imaging;
using Microsoft.Win32;
using Prism.Commands;
using Prism.Mvvm;
using XplorePlane.Models;
using XplorePlane.Services;
namespace XplorePlane.ViewModels
{
public class PipelineEditorViewModel : BindableBase
{
private const int MaxPipelineLength = 20;
private const int DebounceDelayMs = 300;
private readonly IImageProcessingService _imageProcessingService;
private readonly IPipelineExecutionService _executionService;
private readonly IPipelinePersistenceService _persistenceService;
private PipelineNodeViewModel _selectedNode;
private BitmapSource _sourceImage;
private BitmapSource _previewImage;
private string _pipelineName = "新建流水线";
private string _selectedDevice = string.Empty;
private bool _isExecuting;
private string _statusMessage = string.Empty;
private string _currentFilePath;
private CancellationTokenSource _executionCts;
private CancellationTokenSource _debounceCts;
public PipelineEditorViewModel(
IImageProcessingService imageProcessingService,
IPipelineExecutionService executionService,
IPipelinePersistenceService persistenceService)
{
_imageProcessingService = imageProcessingService ?? throw new ArgumentNullException(nameof(imageProcessingService));
_executionService = executionService ?? throw new ArgumentNullException(nameof(executionService));
_persistenceService = persistenceService ?? throw new ArgumentNullException(nameof(persistenceService));
PipelineNodes = new ObservableCollection<PipelineNodeViewModel>();
AvailableDevices = new ObservableCollection<string>();
AddOperatorCommand = new DelegateCommand<string>(AddOperator, CanAddOperator);
RemoveOperatorCommand = new DelegateCommand<PipelineNodeViewModel>(RemoveOperator);
ReorderOperatorCommand = new DelegateCommand<PipelineReorderArgs>(ReorderOperator);
ExecutePipelineCommand = new DelegateCommand(async () => await ExecutePipelineAsync(), () => !IsExecuting && SourceImage != null);
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());
OpenToolboxCommand = new DelegateCommand(OpenToolbox);
MoveNodeUpCommand = new DelegateCommand<PipelineNodeViewModel>(MoveNodeUp);
MoveNodeDownCommand = new DelegateCommand<PipelineNodeViewModel>(MoveNodeDown);
}
// ── 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();
TriggerDebouncedExecution();
}
}
}
public BitmapSource PreviewImage
{
get => _previewImage;
set => SetProperty(ref _previewImage, value);
}
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);
}
// ── Commands ──────────────────────────────────────────────────
public DelegateCommand<string> AddOperatorCommand { get; }
public DelegateCommand<PipelineNodeViewModel> RemoveOperatorCommand { get; }
public DelegateCommand<PipelineReorderArgs> ReorderOperatorCommand { get; }
public DelegateCommand ExecutePipelineCommand { 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 OpenToolboxCommand { get; }
public DelegateCommand<PipelineNodeViewModel> MoveNodeUpCommand { get; }
public DelegateCommand<PipelineNodeViewModel> MoveNodeDownCommand { get; }
// ── Command Implementations ───────────────────────────────────
private bool CanAddOperator(string operatorKey) =>
!string.IsNullOrWhiteSpace(operatorKey) && PipelineNodes.Count < MaxPipelineLength;
private void AddOperator(string operatorKey)
{
Serilog.Log.Debug("AddOperator 被调用,operatorKey={OperatorKey}", operatorKey);
if (string.IsNullOrWhiteSpace(operatorKey))
{
StatusMessage = "算子键不能为空";
Serilog.Log.Warning("AddOperator 失败:operatorKey 为空");
return;
}
var available = _imageProcessingService.GetAvailableProcessors();
Serilog.Log.Debug("可用算子数量:{Count},包含 {Key}{Contains}",
available.Count(), operatorKey, available.Contains(operatorKey));
if (!available.Contains(operatorKey))
{
StatusMessage = $"算子 '{operatorKey}' 未注册";
Serilog.Log.Warning("AddOperator 失败:算子 {Key} 未注册", operatorKey);
return;
}
if (PipelineNodes.Count >= MaxPipelineLength)
{
StatusMessage = $"流水线节点数已达上限({MaxPipelineLength}";
Serilog.Log.Warning("AddOperator 失败:节点数已达上限 {Max}", MaxPipelineLength);
return;
}
var displayName = _imageProcessingService.GetProcessorDisplayName(operatorKey) ?? operatorKey;
var node = new PipelineNodeViewModel(operatorKey, displayName)
{
Order = PipelineNodes.Count
};
LoadNodeParameters(node);
PipelineNodes.Add(node);
Serilog.Log.Information("节点已添加到 PipelineNodes{Key} ({DisplayName}),当前节点数={Count}",
operatorKey, displayName, PipelineNodes.Count);
StatusMessage = $"已添加算子:{displayName}";
TriggerDebouncedExecution();
}
private void RemoveOperator(PipelineNodeViewModel node)
{
if (node == null || !PipelineNodes.Contains(node)) return;
PipelineNodes.Remove(node);
RenumberNodes();
if (SelectedNode == node)
SelectedNode = null;
StatusMessage = $"已移除算子:{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();
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();
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();
TriggerDebouncedExecution();
}
private void RenumberNodes()
{
for (int i = 0; i < PipelineNodes.Count; i++)
PipelineNodes[i].Order = i;
}
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))
TriggerDebouncedExecution();
};
node.Parameters.Add(vm);
}
}
private async Task ExecutePipelineAsync()
{
if (SourceImage == null || IsExecuting) return;
_executionCts?.Cancel();
_executionCts = new CancellationTokenSource();
var token = _executionCts.Token;
IsExecuting = true;
StatusMessage = "正在执行流水线...";
try
{
var progress = new Progress<PipelineProgress>(p =>
StatusMessage = $"执行中:{p.CurrentOperator} ({p.CurrentStep}/{p.TotalSteps})");
var result = await _executionService.ExecutePipelineAsync(
PipelineNodes, SourceImage, progress, token);
PreviewImage = result;
StatusMessage = "流水线执行完成";
}
catch (OperationCanceledException)
{
StatusMessage = "流水线执行已取消";
}
catch (PipelineExecutionException ex)
{
StatusMessage = $"节点 '{ex.FailedOperatorKey}' 执行失败:{ex.Message}";
}
catch (Exception ex)
{
StatusMessage = $"执行错误:{ex.Message}";
}
finally
{
IsExecuting = false;
}
}
private void CancelExecution()
{
_executionCts?.Cancel();
}
private void TriggerDebouncedExecution()
{
if (SourceImage == null) return;
_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;
PipelineName = "新建流水线";
PreviewImage = null;
_currentFilePath = null;
StatusMessage = "已新建流水线";
}
private async Task SavePipelineAsync()
{
if (string.IsNullOrWhiteSpace(PipelineName) || PipelineName.Length > 100)
{
StatusMessage = "流水线名称不能为空且长度不超过 100 个字符";
return;
}
if (string.IsNullOrEmpty(_currentFilePath))
{
await SaveAsPipelineAsync();
return;
}
await SaveToFileAsync(_currentFilePath);
}
private async Task SaveAsPipelineAsync()
{
if (string.IsNullOrWhiteSpace(PipelineName) || PipelineName.Length > 100)
{
StatusMessage = "流水线名称不能为空且长度不超过 100 个字符";
return;
}
var dialog = new SaveFileDialog
{
Filter = "流水线文件 (*.pipeline.json)|*.pipeline.json",
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);
StatusMessage = $"流水线已保存:{Path.GetFileName(filePath)}";
}
catch (IOException ex)
{
StatusMessage = $"保存失败:{ex.Message}";
}
}
private async Task DeletePipelineAsync()
{
if (string.IsNullOrEmpty(_currentFilePath)) return;
try
{
if (File.Exists(_currentFilePath))
File.Delete(_currentFilePath);
NewPipeline();
StatusMessage = "流水线已删除";
}
catch (IOException ex)
{
StatusMessage = $"删除失败:{ex.Message}";
}
await Task.CompletedTask;
}
private async Task LoadPipelineAsync()
{
var dialog = new OpenFileDialog
{
Filter = "流水线文件 (*.pipeline.json)|*.pipeline.json",
InitialDirectory = GetPipelineDirectory()
};
if (dialog.ShowDialog() != true) return;
try
{
var model = await _persistenceService.LoadAsync(dialog.FileName);
PipelineNodes.Clear();
SelectedNode = null;
PipelineName = model.Name;
SelectedDevice = model.DeviceId;
_currentFilePath = dialog.FileName;
foreach (var nodeModel in model.Nodes)
{
var displayName = _imageProcessingService.GetProcessorDisplayName(nodeModel.OperatorKey)
?? nodeModel.OperatorKey;
var node = new PipelineNodeViewModel(nodeModel.OperatorKey, displayName)
{
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);
}
Serilog.Log.Information("流水线已加载:{Name},节点数={Count}", model.Name, PipelineNodes.Count);
StatusMessage = $"已加载流水线:{model.Name}{PipelineNodes.Count} 个节点)";
}
catch (Exception ex)
{
Serilog.Log.Warning("加载流水线失败:{Error}", ex.Message);
StatusMessage = $"加载失败:{ex.Message}";
}
}
private void OpenToolbox() //打开图像工具箱
{
Serilog.Log.Information("OpenToolbox 被调用");
try
{
var window = new Views.OperatorToolboxWindow
{
Owner = System.Windows.Application.Current.MainWindow
};
Serilog.Log.Information("OperatorToolboxWindow 已创建,准备 Show()");
window.Show();
Serilog.Log.Information("OperatorToolboxWindow.Show() 完成");
}
catch (Exception ex)
{
Serilog.Log.Error(ex, "OpenToolbox 打开窗口失败");
}
}
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 static string GetPipelineDirectory()
{
var dir = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"XplorePlane", "Pipelines");
Directory.CreateDirectory(dir);
return dir;
}
}
}