Files
XplorePlane/XplorePlane/ViewModels/Cnc/CncInspectionModulePipelineViewModel.cs
T

673 lines
25 KiB
C#

using Microsoft.Win32;
using Prism.Commands;
using Prism.Mvvm;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Input;
using System.Windows.Media.Imaging;
using XplorePlane.Events;
using XplorePlane.Models;
using XplorePlane.Services;
using XplorePlane.Services.MainViewport;
using XplorePlane.Views;
using XP.Common.Logging.Interfaces;
using Prism.Events;
namespace XplorePlane.ViewModels.Cnc
{
public class CncInspectionModulePipelineViewModel : BindableBase, IPipelineEditorHostViewModel
{
private readonly CncEditorViewModel _editorViewModel;
private readonly IImageProcessingService _imageProcessingService;
private readonly IPipelinePersistenceService _persistenceService;
private readonly IPipelineExecutionService _executionService;
private readonly IMainViewportService _mainViewportService;
private readonly IEventAggregator _eventAggregator;
private readonly ILoggerService _logger;
private CncNodeViewModel _activeModuleNode;
private PipelineNodeViewModel _selectedNode;
private string _statusMessage = "请选择检测模块以编辑其流水线。";
private string _pipelineFileDisplayName = "未命名模块.xpm";
private string _currentFilePath;
private PipelineNodeViewModel _executionEndNode;
private bool _isSynchronizing;
private CancellationTokenSource _debounceCts;
private const int DebounceDelayMs = 300;
public CncInspectionModulePipelineViewModel(
CncEditorViewModel editorViewModel,
IImageProcessingService imageProcessingService,
IPipelinePersistenceService persistenceService,
ILoggerService logger,
IPipelineExecutionService executionService = null,
IMainViewportService mainViewportService = null,
IEventAggregator eventAggregator = null)
{
_editorViewModel = editorViewModel ?? throw new ArgumentNullException(nameof(editorViewModel));
_imageProcessingService = imageProcessingService ?? throw new ArgumentNullException(nameof(imageProcessingService));
_persistenceService = persistenceService ?? throw new ArgumentNullException(nameof(persistenceService));
_logger = (logger ?? throw new ArgumentNullException(nameof(logger))).ForModule<CncInspectionModulePipelineViewModel>();
_executionService = executionService;
_mainViewportService = mainViewportService;
_eventAggregator = eventAggregator;
PipelineNodes = new ObservableCollection<PipelineNodeViewModel>();
AddOperatorCommand = new DelegateCommand<string>(AddOperator, _ => HasActiveModule);
RemoveOperatorCommand = new DelegateCommand<PipelineNodeViewModel>(RemoveOperator);
ReorderOperatorCommand = new DelegateCommand<PipelineReorderArgs>(ReorderOperator);
ToggleOperatorEnabledCommand = new DelegateCommand<PipelineNodeViewModel>(ToggleOperatorEnabled);
ExecuteToNodeCommand = new DelegateCommand<PipelineNodeViewModel>(ExecuteToNode);
ClearExecutionRangeCommand = new DelegateCommand(ClearExecutionRange);
MoveNodeUpCommand = new DelegateCommand<PipelineNodeViewModel>(MoveNodeUp);
MoveNodeDownCommand = new DelegateCommand<PipelineNodeViewModel>(MoveNodeDown);
NewPipelineCommand = new DelegateCommand(NewPipeline);
SavePipelineCommand = new DelegateCommand(SavePipelineToModule);
SaveAsPipelineCommand = new DelegateCommand(async () => await SaveAsPipelineAsync());
LoadPipelineCommand = new DelegateCommand(async () => await LoadPipelineAsync());
OpenTemplateMatchingToolCommand = new DelegateCommand(OpenTemplateMatchingTool, CanOpenTemplateMatchingTool);
_editorViewModel.PropertyChanged += OnEditorPropertyChanged;
RefreshFromSelection();
}
public ObservableCollection<PipelineNodeViewModel> PipelineNodes { get; }
public PipelineNodeViewModel SelectedNode
{
get => _selectedNode;
set
{
if (!SetProperty(ref _selectedNode, value))
return;
RaisePropertyChanged(nameof(IsTemplateMatchingNodeSelected));
(OpenTemplateMatchingToolCommand as DelegateCommand)?.RaiseCanExecuteChanged();
}
}
public bool IsTemplateMatchingNodeSelected =>
SelectedNode != null &&
string.Equals(SelectedNode.OperatorKey, "TemplateMatching", StringComparison.Ordinal);
public ICommand OpenTemplateMatchingToolCommand { get; }
public string StatusMessage
{
get => _statusMessage;
private set => SetProperty(ref _statusMessage, value);
}
public bool IsStatusError => false;
public string PipelineFileDisplayName
{
get => _pipelineFileDisplayName;
private set => SetProperty(ref _pipelineFileDisplayName, value);
}
public PipelineNodeViewModel ExecutionEndNode
{
get => _executionEndNode;
private set
{
if (SetProperty(ref _executionEndNode, value))
UpdateExecutionRangeState();
}
}
public bool HasActiveModule => _activeModuleNode?.IsInspectionModule == true;
public Visibility EditorVisibility => HasActiveModule ? Visibility.Visible : Visibility.Collapsed;
public Visibility EmptyStateVisibility => HasActiveModule ? Visibility.Collapsed : Visibility.Visible;
public ICommand AddOperatorCommand { get; }
public ICommand RemoveOperatorCommand { get; }
public ICommand ReorderOperatorCommand { get; }
public ICommand ToggleOperatorEnabledCommand { get; }
public ICommand ExecuteToNodeCommand { get; }
public ICommand ClearExecutionRangeCommand { get; }
public ICommand MoveNodeUpCommand { get; }
public ICommand MoveNodeDownCommand { get; }
public ICommand NewPipelineCommand { get; }
public ICommand SavePipelineCommand { get; }
public ICommand SaveAsPipelineCommand { get; }
public ICommand LoadPipelineCommand { get; }
private static string? GetPipelineRecipeDirectory(string? currentFilePath)
{
if (string.IsNullOrWhiteSpace(currentFilePath))
return null;
try
{
var full = Path.GetFullPath(currentFilePath);
return Path.GetDirectoryName(full);
}
catch
{
return null;
}
}
private bool CanOpenTemplateMatchingTool() => IsTemplateMatchingNodeSelected;
private void OpenTemplateMatchingTool()
{
if (!CanOpenTemplateMatchingTool() || SelectedNode == null)
return;
try
{
var recipeDir = GetPipelineRecipeDirectory(_currentFilePath);
var source = _mainViewportService?.CurrentDisplayImage as BitmapSource
?? _mainViewportService?.LatestManualImage as BitmapSource;
var win = new TemplateMatchingToolWindow(SelectedNode, source, _imageProcessingService, _logger, recipeDir);
win.Owner = Application.Current?.MainWindow;
win.ShowDialog();
}
catch (Exception ex)
{
_logger.Warn("打开模板匹配工具窗失败: {Message}", ex.Message);
StatusMessage = "无法打开模板匹配工具:" + ex.Message;
}
}
private void OnEditorPropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(CncEditorViewModel.SelectedNode))
{
RefreshFromSelection();
}
}
private void RefreshFromSelection()
{
var selected = _editorViewModel.SelectedNode;
if (selected == null || !selected.IsInspectionModule)
{
_activeModuleNode = null;
PipelineNodes.Clear();
SelectedNode = null;
PipelineFileDisplayName = "未命名模块.xpm";
StatusMessage = "请选择检测模块以编辑其流水线。";
RaiseModuleVisibilityChanged();
RaiseCommandCanExecuteChanged();
return;
}
_activeModuleNode = selected;
_currentFilePath = null;
LoadPipelineModel(_activeModuleNode.Pipeline ?? new PipelineModel
{
Name = _activeModuleNode.Name
});
RaiseModuleVisibilityChanged();
RaiseCommandCanExecuteChanged();
}
private void AddOperator(string operatorKey)
{
if (!HasActiveModule || string.IsNullOrWhiteSpace(operatorKey))
return;
var available = _imageProcessingService.GetAvailableProcessors();
if (!available.Contains(operatorKey))
{
StatusMessage = $"算子 '{operatorKey}' 未注册。";
return;
}
var displayName = _imageProcessingService.GetProcessorDisplayName(operatorKey) ?? operatorKey;
var icon = ProcessorUiMetadata.GetOperatorIcon(operatorKey);
var node = new PipelineNodeViewModel(operatorKey, displayName, icon)
{
Order = PipelineNodes.Count
};
LoadNodeParameters(node, null);
PipelineNodes.Add(node);
SelectedNode = node;
UpdateExecutionRangeState();
PersistActiveModule($"已添加算子:{displayName}");
}
private void RemoveOperator(PipelineNodeViewModel node)
{
if (!HasActiveModule || 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);
PersistActiveModule($"已移除算子:{node.DisplayName}");
}
private void MoveNodeUp(PipelineNodeViewModel node)
{
if (!HasActiveModule || node == null)
return;
var index = PipelineNodes.IndexOf(node);
if (index <= 0)
return;
PipelineNodes.Move(index, index - 1);
RenumberNodes();
UpdateExecutionRangeState();
PersistActiveModule($"已上移算子:{node.DisplayName}");
}
private void ReorderOperator(PipelineReorderArgs args)
{
if (!HasActiveModule || args == null)
return;
var oldIndex = args.OldIndex;
var newIndex = args.NewIndex;
if (oldIndex < 0 || oldIndex >= PipelineNodes.Count)
return;
if (newIndex < 0 || newIndex >= PipelineNodes.Count || oldIndex == newIndex)
return;
var node = PipelineNodes[oldIndex];
PipelineNodes.Move(oldIndex, newIndex);
RenumberNodes();
UpdateExecutionRangeState();
SelectedNode = node;
PersistActiveModule($"已调整算子顺序:{node.DisplayName}");
}
private void MoveNodeDown(PipelineNodeViewModel node)
{
if (!HasActiveModule || node == null)
return;
var index = PipelineNodes.IndexOf(node);
if (index < 0 || index >= PipelineNodes.Count - 1)
return;
PipelineNodes.Move(index, index + 1);
RenumberNodes();
UpdateExecutionRangeState();
PersistActiveModule($"已下移算子:{node.DisplayName}");
}
private void ToggleOperatorEnabled(PipelineNodeViewModel node)
{
if (!HasActiveModule || node == null || !PipelineNodes.Contains(node))
return;
node.IsEnabled = !node.IsEnabled;
SelectedNode = node;
PersistActiveModule(node.IsEnabled
? $"已启用算子:{node.DisplayName}"
: $"已停用算子:{node.DisplayName}");
}
private void ExecuteToNode(PipelineNodeViewModel node)
{
if (!HasActiveModule || node == null || !PipelineNodes.Contains(node))
return;
SelectedNode = node;
ExecutionEndNode = node;
PersistActiveModule($"已设置执行截止点:{node.DisplayName}");
}
private void ClearExecutionRange()
{
if (!HasActiveModule || ExecutionEndNode == null)
return;
ExecutionEndNode = null;
PersistActiveModule("已切换为执行全部节点");
}
private void NewPipeline()
{
if (!HasActiveModule)
return;
PipelineNodes.Clear();
SelectedNode = null;
ExecutionEndNode = null;
_currentFilePath = null;
PipelineFileDisplayName = GetActivePipelineFileDisplayName();
PersistActiveModule("已为当前检测模块新建空流水线。");
}
private void SavePipelineToModule()
{
if (!HasActiveModule)
return;
PersistActiveModule("当前检测模块流水线已同步到 CNC 程序。");
}
private async System.Threading.Tasks.Task SaveAsPipelineAsync()
{
if (!HasActiveModule)
return;
var dialog = new SaveFileDialog
{
Filter = "XP 模块流水线 (*.xpm)|*.xpm",
DefaultExt = ".xpm",
AddExtension = true,
FileName = GetActivePipelineName()
};
if (dialog.ShowDialog() != true)
return;
var model = BuildPipelineModel();
await _persistenceService.SaveAsync(model, dialog.FileName);
_currentFilePath = dialog.FileName;
PipelineFileDisplayName = FormatPipelinePath(dialog.FileName);
StatusMessage = $"已导出模块流水线:{Path.GetFileName(dialog.FileName)}";
}
private async System.Threading.Tasks.Task LoadPipelineAsync()
{
if (!HasActiveModule)
return;
var dialog = new OpenFileDialog
{
Filter = "XP 模块流水线 (*.xpm)|*.xpm",
DefaultExt = ".xpm"
};
if (dialog.ShowDialog() != true)
return;
var model = await _persistenceService.LoadAsync(dialog.FileName);
_currentFilePath = dialog.FileName;
PipelineFileDisplayName = FormatPipelinePath(dialog.FileName);
LoadPipelineModel(model);
PersistActiveModule($"已加载模块流水线:{model.Name}");
}
private void LoadPipelineModel(PipelineModel pipeline)
{
_isSynchronizing = true;
try
{
PipelineNodes.Clear();
SelectedNode = null;
ExecutionEndNode = null;
var orderedNodes = (pipeline?.Nodes ?? new List<PipelineNodeModel>())
.OrderBy(node => node.Order)
.ToList();
foreach (var nodeModel in orderedNodes)
{
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, nodeModel.Parameters);
PipelineNodes.Add(node);
}
SelectedNode = PipelineNodes.FirstOrDefault();
UpdateExecutionRangeState();
if (string.IsNullOrEmpty(_currentFilePath))
PipelineFileDisplayName = GetActivePipelineFileDisplayName();
StatusMessage = HasActiveModule
? $"正在编辑检测模块:{_activeModuleNode.Name}"
: "请选择检测模块以编辑其流水线。";
}
finally
{
_isSynchronizing = false;
}
}
private void LoadNodeParameters(PipelineNodeViewModel node, IDictionary<string, object> savedValues)
{
var parameterDefinitions = _imageProcessingService.GetProcessorParameters(node.OperatorKey);
if (parameterDefinitions == null)
return;
node.Parameters.Clear();
foreach (var definition in parameterDefinitions)
{
var parameterVm = new ProcessorParameterVM(definition);
if (savedValues != null && savedValues.TryGetValue(definition.Name, out var savedValue))
{
parameterVm.Value = ConvertSavedValue(savedValue, definition.ValueType);
}
parameterVm.PropertyChanged += (_, e) =>
{
if (e.PropertyName == nameof(ProcessorParameterVM.Value) && !_isSynchronizing)
{
PersistActiveModule($"已更新参数:{parameterVm.DisplayName}");
}
};
node.Parameters.Add(parameterVm);
}
}
private void PersistActiveModule(string statusMessage)
{
if (!HasActiveModule || _isSynchronizing)
return;
_activeModuleNode.Pipeline = BuildPipelineModel();
StatusMessage = statusMessage;
TriggerDebouncedPreview();
}
private void TriggerDebouncedPreview()
{
if (_executionService == null || _mainViewportService == null)
return;
var sourceImage = _mainViewportService.CurrentDisplayImage as BitmapSource
?? _mainViewportService.LatestManualImage as BitmapSource;
if (sourceImage == null)
{
_logger.Debug("[图像链路][CNC] TriggerDebouncedPreview:无可用源图像,跳过");
return;
}
_debounceCts?.Cancel();
_debounceCts = new CancellationTokenSource();
var token = _debounceCts.Token;
Task.Delay(DebounceDelayMs, token).ContinueWith(t =>
{
if (!t.IsCanceled)
_ = ExecutePreviewAsync(sourceImage, token);
}, TaskScheduler.FromCurrentSynchronizationContext());
}
private async Task ExecutePreviewAsync(BitmapSource sourceImage, CancellationToken token)
{
try
{
_logger.Info("[图像链路][CNC] ExecutePreviewAsync:开始执行,节点数={Count}", PipelineNodes.Count);
var result = await _executionService.ExecutePipelineAsync(GetNodesInExecutionScope(), sourceImage, null, token);
_logger.Info("[图像链路][CNC] ExecutePreviewAsync:执行完成,推送结果图像");
_mainViewportService.SetManualImage(result, string.Empty);
_eventAggregator?.GetEvent<PipelinePreviewUpdatedEvent>()
.Publish(new PipelinePreviewUpdatedPayload(result, StatusMessage));
}
catch (OperationCanceledException)
{
_logger.Debug("[图像链路][CNC] ExecutePreviewAsync:已取消");
}
catch (Exception ex)
{
_logger.Error(ex, "[图像链路][CNC] ExecutePreviewAsync:执行失败");
}
}
private PipelineModel BuildPipelineModel()
{
return new PipelineModel
{
Id = _activeModuleNode?.Pipeline?.Id ?? Guid.NewGuid(),
Name = GetActivePipelineName(),
DeviceId = _activeModuleNode?.Pipeline?.DeviceId ?? string.Empty,
CreatedAt = _activeModuleNode?.Pipeline?.CreatedAt ?? DateTime.UtcNow,
UpdatedAt = DateTime.UtcNow,
Nodes = PipelineNodes.Select((node, index) => new PipelineNodeModel
{
Id = node.Id,
OperatorKey = node.OperatorKey,
Order = index,
IsEnabled = node.IsEnabled,
Parameters = node.Parameters.ToDictionary(parameter => parameter.Name, parameter => parameter.Value)
}).ToList()
};
}
private IEnumerable<PipelineNodeViewModel> GetNodesInExecutionScope()
{
var orderedNodes = PipelineNodes.OrderBy(node => node.Order);
if (ExecutionEndNode == null)
return orderedNodes;
return orderedNodes.Where(node => node.Order <= ExecutionEndNode.Order);
}
private string GetActivePipelineName()
{
if (!HasActiveModule)
return "InspectionModulePipeline";
return string.IsNullOrWhiteSpace(_activeModuleNode.Pipeline?.Name)
? _activeModuleNode.Name
: _activeModuleNode.Pipeline.Name;
}
private string GetActivePipelineFileDisplayName()
{
var pipelineName = GetActivePipelineName();
return pipelineName.EndsWith(".xpm", StringComparison.OrdinalIgnoreCase)
? pipelineName
: $"{pipelineName}.xpm";
}
private static string FormatPipelinePath(string filePath)
{
return string.IsNullOrWhiteSpace(filePath)
? "未命名模块.xpm"
: Path.GetFullPath(filePath).Replace('\\', '/');
}
private void RenumberNodes()
{
for (var i = 0; i < PipelineNodes.Count; i++)
{
PipelineNodes[i].Order = i;
}
}
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 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 RaiseModuleVisibilityChanged()
{
RaisePropertyChanged(nameof(HasActiveModule));
RaisePropertyChanged(nameof(EditorVisibility));
RaisePropertyChanged(nameof(EmptyStateVisibility));
}
private void RaiseCommandCanExecuteChanged()
{
(AddOperatorCommand as DelegateCommand<string>)?.RaiseCanExecuteChanged();
(OpenTemplateMatchingToolCommand as DelegateCommand)?.RaiseCanExecuteChanged();
}
private static object ConvertSavedValue(object savedValue, Type targetType)
{
if (savedValue is not JsonElement jsonElement)
return savedValue;
try
{
if (targetType == typeof(int))
return jsonElement.GetInt32();
if (targetType == typeof(double))
return jsonElement.GetDouble();
if (targetType == typeof(bool))
return jsonElement.GetBoolean();
if (targetType == typeof(string))
return jsonElement.GetString() ?? string.Empty;
return jsonElement.ToString();
}
catch
{
return jsonElement.ToString();
}
}
}
}