518 lines
19 KiB
C#
518 lines
19 KiB
C#
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;
|
||
}
|
||
}
|
||
}
|