#0023 整理项目结构
This commit is contained in:
@@ -0,0 +1,403 @@
|
||||
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());
|
||||
}
|
||||
|
||||
// ── 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; }
|
||||
|
||||
// ── Command Implementations ───────────────────────────────────
|
||||
|
||||
private bool CanAddOperator(string operatorKey) =>
|
||||
!string.IsNullOrWhiteSpace(operatorKey) && PipelineNodes.Count < MaxPipelineLength;
|
||||
|
||||
private void AddOperator(string operatorKey)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(operatorKey))
|
||||
{
|
||||
StatusMessage = "算子键不能为空";
|
||||
return;
|
||||
}
|
||||
|
||||
var available = _imageProcessingService.GetAvailableProcessors();
|
||||
if (!available.Contains(operatorKey))
|
||||
{
|
||||
StatusMessage = $"算子 '{operatorKey}' 未注册";
|
||||
return;
|
||||
}
|
||||
|
||||
if (PipelineNodes.Count >= MaxPipelineLength)
|
||||
{
|
||||
StatusMessage = $"流水线节点数已达上限({MaxPipelineLength})";
|
||||
return;
|
||||
}
|
||||
|
||||
var displayName = _imageProcessingService.GetProcessorDisplayName(operatorKey) ?? operatorKey;
|
||||
var node = new PipelineNodeViewModel(operatorKey, displayName)
|
||||
{
|
||||
Order = PipelineNodes.Count
|
||||
};
|
||||
LoadNodeParameters(node);
|
||||
PipelineNodes.Add(node);
|
||||
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 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 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user