#0023 整理项目结构

This commit is contained in:
zhengxuan.zhang
2026-03-15 00:24:02 +08:00
parent dc35faabf1
commit 1b1b0872c7
43 changed files with 303 additions and 220 deletions
@@ -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;
}
}
}