diff --git a/XplorePlane/ViewModels/Cnc/CncInspectionModulePipelineViewModel.cs b/XplorePlane/ViewModels/Cnc/CncInspectionModulePipelineViewModel.cs new file mode 100644 index 0000000..b6dc7d7 --- /dev/null +++ b/XplorePlane/ViewModels/Cnc/CncInspectionModulePipelineViewModel.cs @@ -0,0 +1,401 @@ +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.Windows; +using System.Windows.Input; +using XplorePlane.Models; +using XplorePlane.Services; +using XP.Common.Logging.Interfaces; + +namespace XplorePlane.ViewModels.Cnc +{ + public class CncInspectionModulePipelineViewModel : BindableBase, IPipelineEditorHostViewModel + { + private readonly CncEditorViewModel _editorViewModel; + private readonly IImageProcessingService _imageProcessingService; + private readonly IPipelinePersistenceService _persistenceService; + private readonly ILoggerService _logger; + + private CncNodeViewModel _activeModuleNode; + private PipelineNodeViewModel _selectedNode; + private string _statusMessage = "请选择检测模块以编辑其流水线。"; + private string _currentFilePath; + private bool _isSynchronizing; + + public CncInspectionModulePipelineViewModel( + CncEditorViewModel editorViewModel, + IImageProcessingService imageProcessingService, + IPipelinePersistenceService persistenceService, + ILoggerService logger) + { + _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(); + + PipelineNodes = new ObservableCollection(); + + AddOperatorCommand = new DelegateCommand(AddOperator, _ => HasActiveModule); + RemoveOperatorCommand = new DelegateCommand(RemoveOperator); + MoveNodeUpCommand = new DelegateCommand(MoveNodeUp); + MoveNodeDownCommand = new DelegateCommand(MoveNodeDown); + NewPipelineCommand = new DelegateCommand(NewPipeline); + SavePipelineCommand = new DelegateCommand(SavePipelineToModule); + SaveAsPipelineCommand = new DelegateCommand(async () => await SaveAsPipelineAsync()); + LoadPipelineCommand = new DelegateCommand(async () => await LoadPipelineAsync()); + + _editorViewModel.PropertyChanged += OnEditorPropertyChanged; + RefreshFromSelection(); + } + + public ObservableCollection PipelineNodes { get; } + + public PipelineNodeViewModel SelectedNode + { + get => _selectedNode; + set => SetProperty(ref _selectedNode, value); + } + + public string StatusMessage + { + get => _statusMessage; + private set => SetProperty(ref _statusMessage, value); + } + + 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 MoveNodeUpCommand { get; } + + public ICommand MoveNodeDownCommand { get; } + + public ICommand NewPipelineCommand { get; } + + public ICommand SavePipelineCommand { get; } + + public ICommand SaveAsPipelineCommand { get; } + + public ICommand LoadPipelineCommand { get; } + + 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; + StatusMessage = "请选择检测模块以编辑其流水线。"; + RaiseModuleVisibilityChanged(); + RaiseCommandCanExecuteChanged(); + return; + } + + _activeModuleNode = selected; + 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; + PersistActiveModule($"已添加算子:{displayName}"); + } + + private void RemoveOperator(PipelineNodeViewModel node) + { + if (!HasActiveModule || node == null || !PipelineNodes.Contains(node)) + return; + + PipelineNodes.Remove(node); + RenumberNodes(); + + if (SelectedNode == node) + { + SelectedNode = PipelineNodes.LastOrDefault(); + } + + 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(); + 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(); + PersistActiveModule($"已下移算子:{node.DisplayName}"); + } + + private void NewPipeline() + { + if (!HasActiveModule) + return; + + PipelineNodes.Clear(); + SelectedNode = null; + _currentFilePath = null; + PersistActiveModule("已为当前检测模块新建空流水线。"); + } + + private void SavePipelineToModule() + { + if (!HasActiveModule) + return; + + PersistActiveModule("当前检测模块流水线已同步到 CNC 程序。"); + } + + private async System.Threading.Tasks.Task SaveAsPipelineAsync() + { + if (!HasActiveModule) + return; + + var dialog = new SaveFileDialog + { + Filter = "图像处理流水线 (*.imw)|*.imw", + FileName = GetActivePipelineName() + }; + + if (dialog.ShowDialog() != true) + return; + + var model = BuildPipelineModel(); + await _persistenceService.SaveAsync(model, dialog.FileName); + _currentFilePath = dialog.FileName; + StatusMessage = $"已导出模块流水线:{Path.GetFileName(dialog.FileName)}"; + } + + private async System.Threading.Tasks.Task LoadPipelineAsync() + { + if (!HasActiveModule) + return; + + var dialog = new OpenFileDialog + { + Filter = "图像处理流水线 (*.imw)|*.imw" + }; + + if (dialog.ShowDialog() != true) + return; + + var model = await _persistenceService.LoadAsync(dialog.FileName); + _currentFilePath = dialog.FileName; + LoadPipelineModel(model); + PersistActiveModule($"已加载模块流水线:{model.Name}"); + } + + private void LoadPipelineModel(PipelineModel pipeline) + { + _isSynchronizing = true; + try + { + PipelineNodes.Clear(); + SelectedNode = null; + + var orderedNodes = (pipeline?.Nodes ?? new List()) + .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(); + StatusMessage = HasActiveModule + ? $"正在编辑检测模块:{_activeModuleNode.Name}" + : "请选择检测模块以编辑其流水线。"; + } + finally + { + _isSynchronizing = false; + } + } + + private void LoadNodeParameters(PipelineNodeViewModel node, IDictionary 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; + } + + 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 string GetActivePipelineName() + { + if (!HasActiveModule) + return "InspectionModulePipeline"; + + return string.IsNullOrWhiteSpace(_activeModuleNode.Pipeline?.Name) + ? _activeModuleNode.Name + : _activeModuleNode.Pipeline.Name; + } + + private void RenumberNodes() + { + for (var i = 0; i < PipelineNodes.Count; i++) + { + PipelineNodes[i].Order = i; + } + } + + private void RaiseModuleVisibilityChanged() + { + RaisePropertyChanged(nameof(HasActiveModule)); + RaisePropertyChanged(nameof(EditorVisibility)); + RaisePropertyChanged(nameof(EmptyStateVisibility)); + } + + private void RaiseCommandCanExecuteChanged() + { + (AddOperatorCommand 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(); + } + } + } +} diff --git a/XplorePlane/ViewModels/ImageProcessing/IPipelineEditorHostViewModel.cs b/XplorePlane/ViewModels/ImageProcessing/IPipelineEditorHostViewModel.cs new file mode 100644 index 0000000..6bdcc74 --- /dev/null +++ b/XplorePlane/ViewModels/ImageProcessing/IPipelineEditorHostViewModel.cs @@ -0,0 +1,30 @@ +using System.Collections.ObjectModel; +using System.Windows.Input; + +namespace XplorePlane.ViewModels +{ + public interface IPipelineEditorHostViewModel + { + ObservableCollection PipelineNodes { get; } + + PipelineNodeViewModel SelectedNode { get; set; } + + string StatusMessage { get; } + + ICommand AddOperatorCommand { get; } + + ICommand RemoveOperatorCommand { get; } + + ICommand MoveNodeUpCommand { get; } + + ICommand MoveNodeDownCommand { get; } + + ICommand NewPipelineCommand { get; } + + ICommand SavePipelineCommand { get; } + + ICommand SaveAsPipelineCommand { get; } + + ICommand LoadPipelineCommand { get; } + } +}