diff --git a/XplorePlane/ViewModels/Cnc/CncInspectionModulePipelineViewModel.cs b/XplorePlane/ViewModels/Cnc/CncInspectionModulePipelineViewModel.cs index 24c4b6f..f16a061 100644 --- a/XplorePlane/ViewModels/Cnc/CncInspectionModulePipelineViewModel.cs +++ b/XplorePlane/ViewModels/Cnc/CncInspectionModulePipelineViewModel.cs @@ -37,6 +37,7 @@ namespace XplorePlane.ViewModels.Cnc private string _statusMessage = "请选择检测模块以编辑其流水线。"; private string _pipelineFileDisplayName = "未命名模块.xpm"; private string _currentFilePath; + private PipelineNodeViewModel _executionEndNode; private bool _isSynchronizing; private CancellationTokenSource _debounceCts; @@ -65,6 +66,8 @@ namespace XplorePlane.ViewModels.Cnc RemoveOperatorCommand = new DelegateCommand(RemoveOperator); ReorderOperatorCommand = new DelegateCommand(ReorderOperator); ToggleOperatorEnabledCommand = new DelegateCommand(ToggleOperatorEnabled); + ExecuteToNodeCommand = new DelegateCommand(ExecuteToNode); + ClearExecutionRangeCommand = new DelegateCommand(ClearExecutionRange); MoveNodeUpCommand = new DelegateCommand(MoveNodeUp); MoveNodeDownCommand = new DelegateCommand(MoveNodeDown); NewPipelineCommand = new DelegateCommand(NewPipeline); @@ -98,6 +101,16 @@ namespace XplorePlane.ViewModels.Cnc 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; @@ -112,6 +125,10 @@ namespace XplorePlane.ViewModels.Cnc public ICommand ToggleOperatorEnabledCommand { get; } + public ICommand ExecuteToNodeCommand { get; } + + public ICommand ClearExecutionRangeCommand { get; } + public ICommand MoveNodeUpCommand { get; } public ICommand MoveNodeDownCommand { get; } @@ -179,6 +196,7 @@ namespace XplorePlane.ViewModels.Cnc LoadNodeParameters(node, null); PipelineNodes.Add(node); SelectedNode = node; + UpdateExecutionRangeState(); PersistActiveModule($"已添加算子:{displayName}"); } @@ -190,6 +208,10 @@ namespace XplorePlane.ViewModels.Cnc var removedIndex = PipelineNodes.IndexOf(node); PipelineNodes.Remove(node); RenumberNodes(); + if (ReferenceEquals(ExecutionEndNode, node)) + ExecutionEndNode = null; + else + UpdateExecutionRangeState(); SelectNeighborAfterRemoval(removedIndex); PersistActiveModule($"已移除算子:{node.DisplayName}"); @@ -206,6 +228,7 @@ namespace XplorePlane.ViewModels.Cnc PipelineNodes.Move(index, index - 1); RenumberNodes(); + UpdateExecutionRangeState(); PersistActiveModule($"已上移算子:{node.DisplayName}"); } @@ -225,6 +248,7 @@ namespace XplorePlane.ViewModels.Cnc var node = PipelineNodes[oldIndex]; PipelineNodes.Move(oldIndex, newIndex); RenumberNodes(); + UpdateExecutionRangeState(); SelectedNode = node; PersistActiveModule($"已调整算子顺序:{node.DisplayName}"); } @@ -240,6 +264,7 @@ namespace XplorePlane.ViewModels.Cnc PipelineNodes.Move(index, index + 1); RenumberNodes(); + UpdateExecutionRangeState(); PersistActiveModule($"已下移算子:{node.DisplayName}"); } @@ -255,6 +280,25 @@ namespace XplorePlane.ViewModels.Cnc : $"已停用算子:{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) @@ -262,6 +306,7 @@ namespace XplorePlane.ViewModels.Cnc PipelineNodes.Clear(); SelectedNode = null; + ExecutionEndNode = null; _currentFilePath = null; PipelineFileDisplayName = GetActivePipelineFileDisplayName(); PersistActiveModule("已为当前检测模块新建空流水线。"); @@ -326,6 +371,7 @@ namespace XplorePlane.ViewModels.Cnc { PipelineNodes.Clear(); SelectedNode = null; + ExecutionEndNode = null; var orderedNodes = (pipeline?.Nodes ?? new List()) .OrderBy(node => node.Order) @@ -346,6 +392,7 @@ namespace XplorePlane.ViewModels.Cnc } SelectedNode = PipelineNodes.FirstOrDefault(); + UpdateExecutionRangeState(); if (string.IsNullOrEmpty(_currentFilePath)) PipelineFileDisplayName = GetActivePipelineFileDisplayName(); StatusMessage = HasActiveModule @@ -423,7 +470,7 @@ namespace XplorePlane.ViewModels.Cnc try { _logger.Info("[图像链路][CNC] ExecutePreviewAsync:开始执行,节点数={Count}", PipelineNodes.Count); - var result = await _executionService.ExecutePipelineAsync(PipelineNodes, sourceImage, null, token); + var result = await _executionService.ExecutePipelineAsync(GetNodesInExecutionScope(), sourceImage, null, token); _logger.Info("[图像链路][CNC] ExecutePreviewAsync:执行完成,推送结果图像"); _mainViewportService.SetManualImage(result, string.Empty); _eventAggregator?.GetEvent() @@ -459,6 +506,15 @@ namespace XplorePlane.ViewModels.Cnc }; } + private IEnumerable 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) @@ -492,6 +548,19 @@ namespace XplorePlane.ViewModels.Cnc } } + 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) diff --git a/XplorePlane/ViewModels/ImageProcessing/IPipelineEditorHostViewModel.cs b/XplorePlane/ViewModels/ImageProcessing/IPipelineEditorHostViewModel.cs index 9883ca8..d29a005 100644 --- a/XplorePlane/ViewModels/ImageProcessing/IPipelineEditorHostViewModel.cs +++ b/XplorePlane/ViewModels/ImageProcessing/IPipelineEditorHostViewModel.cs @@ -23,6 +23,10 @@ namespace XplorePlane.ViewModels ICommand ToggleOperatorEnabledCommand { get; } + ICommand ExecuteToNodeCommand { get; } + + ICommand ClearExecutionRangeCommand { get; } + ICommand MoveNodeUpCommand { get; } ICommand MoveNodeDownCommand { get; } diff --git a/XplorePlane/ViewModels/ImageProcessing/PipelineEditorViewModel.cs b/XplorePlane/ViewModels/ImageProcessing/PipelineEditorViewModel.cs index 20b817d..07242f4 100644 --- a/XplorePlane/ViewModels/ImageProcessing/PipelineEditorViewModel.cs +++ b/XplorePlane/ViewModels/ImageProcessing/PipelineEditorViewModel.cs @@ -39,6 +39,7 @@ namespace XplorePlane.ViewModels private string _statusMessage = string.Empty; private string _pipelineFileDisplayName = DefaultPipelineFileDisplayName; private string _currentFilePath; + private PipelineNodeViewModel _executionEndNode; private CancellationTokenSource _executionCts; private CancellationTokenSource _debounceCts; @@ -64,6 +65,8 @@ namespace XplorePlane.ViewModels ReorderOperatorCommand = new DelegateCommand(ReorderOperator); ToggleOperatorEnabledCommand = new DelegateCommand(ToggleOperatorEnabled); ExecutePipelineCommand = new DelegateCommand(async () => await ExecutePipelineAsync(), () => !IsExecuting && SourceImage != null); + ExecuteToNodeCommand = new DelegateCommand(async node => await ExecuteToNodeAsync(node), CanExecuteToNode); + ClearExecutionRangeCommand = new DelegateCommand(async () => await ClearExecutionRangeAsync(), CanClearExecutionRange); CancelExecutionCommand = new DelegateCommand(CancelExecution, () => IsExecuting); NewPipelineCommand = new DelegateCommand(NewPipeline); SavePipelineCommand = new DelegateCommand(async () => await SavePipelineAsync()); @@ -162,6 +165,20 @@ namespace XplorePlane.ViewModels private set => SetProperty(ref _pipelineFileDisplayName, value); } + public PipelineNodeViewModel ExecutionEndNode + { + get => _executionEndNode; + private set + { + if (SetProperty(ref _executionEndNode, value)) + { + UpdateExecutionRangeState(); + ExecuteToNodeCommand.RaiseCanExecuteChanged(); + ClearExecutionRangeCommand.RaiseCanExecuteChanged(); + } + } + } + // ── Commands ────────────────────────────────────────────────── public DelegateCommand AddOperatorCommand { get; } @@ -169,6 +186,8 @@ namespace XplorePlane.ViewModels public DelegateCommand ReorderOperatorCommand { get; } public DelegateCommand ToggleOperatorEnabledCommand { get; } public DelegateCommand ExecutePipelineCommand { get; } + public DelegateCommand ExecuteToNodeCommand { get; } + public DelegateCommand ClearExecutionRangeCommand { get; } public DelegateCommand CancelExecutionCommand { get; } public DelegateCommand NewPipelineCommand { get; } public DelegateCommand SavePipelineCommand { get; } @@ -184,6 +203,8 @@ namespace XplorePlane.ViewModels ICommand IPipelineEditorHostViewModel.RemoveOperatorCommand => RemoveOperatorCommand; ICommand IPipelineEditorHostViewModel.ReorderOperatorCommand => ReorderOperatorCommand; ICommand IPipelineEditorHostViewModel.ToggleOperatorEnabledCommand => ToggleOperatorEnabledCommand; + ICommand IPipelineEditorHostViewModel.ExecuteToNodeCommand => ExecuteToNodeCommand; + ICommand IPipelineEditorHostViewModel.ClearExecutionRangeCommand => ClearExecutionRangeCommand; ICommand IPipelineEditorHostViewModel.MoveNodeUpCommand => MoveNodeUpCommand; ICommand IPipelineEditorHostViewModel.MoveNodeDownCommand => MoveNodeDownCommand; ICommand IPipelineEditorHostViewModel.NewPipelineCommand => NewPipelineCommand; @@ -234,6 +255,7 @@ namespace XplorePlane.ViewModels LoadNodeParameters(node); PipelineNodes.Add(node); SelectedNode = node; + UpdateExecutionRangeState(); _logger.Info("节点已添加到 PipelineNodes:{Key} ({DisplayName}),当前节点数={Count}", operatorKey, displayName, PipelineNodes.Count); SetInfoStatus($"已添加算子:{displayName}"); @@ -247,6 +269,10 @@ namespace XplorePlane.ViewModels var removedIndex = PipelineNodes.IndexOf(node); PipelineNodes.Remove(node); RenumberNodes(); + if (ReferenceEquals(ExecutionEndNode, node)) + ExecutionEndNode = null; + else + UpdateExecutionRangeState(); SelectNeighborAfterRemoval(removedIndex); SetInfoStatus($"已移除算子:{node.DisplayName}"); @@ -260,6 +286,7 @@ namespace XplorePlane.ViewModels if (index <= 0) return; PipelineNodes.Move(index, index - 1); RenumberNodes(); + UpdateExecutionRangeState(); TriggerDebouncedExecution(); } @@ -270,6 +297,7 @@ namespace XplorePlane.ViewModels if (index < 0 || index >= PipelineNodes.Count - 1) return; PipelineNodes.Move(index, index + 1); RenumberNodes(); + UpdateExecutionRangeState(); TriggerDebouncedExecution(); } @@ -287,6 +315,7 @@ namespace XplorePlane.ViewModels PipelineNodes.RemoveAt(oldIndex); PipelineNodes.Insert(newIndex, node); RenumberNodes(); + UpdateExecutionRangeState(); SelectedNode = node; SetInfoStatus($"已调整算子顺序:{node.DisplayName}"); TriggerDebouncedExecution(); @@ -304,6 +333,34 @@ namespace XplorePlane.ViewModels TriggerDebouncedExecution(); } + private bool CanExecuteToNode(PipelineNodeViewModel node) => + node != null && PipelineNodes.Contains(node) && !IsExecuting && SourceImage != null; + + private async Task ExecuteToNodeAsync(PipelineNodeViewModel node) + { + if (!CanExecuteToNode(node)) + return; + + SelectedNode = node; + ExecutionEndNode = node; + await ExecutePipelineAsync(); + } + + private bool CanClearExecutionRange() => + ExecutionEndNode != null && !IsExecuting; + + private async Task ClearExecutionRangeAsync() + { + if (ExecutionEndNode == null) + return; + + ExecutionEndNode = null; + SetInfoStatus("已切换为执行全部节点"); + + if (SourceImage != null) + await ExecutePipelineAsync(); + } + private void RenumberNodes() { for (int i = 0; i < PipelineNodes.Count; i++) @@ -367,10 +424,15 @@ namespace XplorePlane.ViewModels _executionCts?.Cancel(); _executionCts = new CancellationTokenSource(); var token = _executionCts.Token; + var executionNodes = GetNodesInExecutionScope() + .Where(n => n.IsEnabled) + .OrderBy(n => n.Order) + .ToList(); IsExecuting = true; - SetInfoStatus("正在执行流水线..."); - _logger.Info("[图像链路] ExecutePipelineAsync:开始执行,节点数={Count}", PipelineNodes.Count); + SetInfoStatus(BuildExecutionStartMessage(executionNodes.Count)); + _logger.Info("[图像链路] ExecutePipelineAsync:开始执行,范围节点数={Count},截止节点={Node}", + executionNodes.Count, ExecutionEndNode?.DisplayName ?? ""); try { @@ -378,10 +440,10 @@ namespace XplorePlane.ViewModels SetInfoStatus($"执行中:{p.CurrentOperator} ({p.CurrentStep}/{p.TotalSteps})")); var result = await _executionService.ExecutePipelineAsync( - PipelineNodes, SourceImage, progress, token); + executionNodes, SourceImage, progress, token); PreviewImage = result; - SetInfoStatus("流水线执行完成"); + SetInfoStatus(BuildExecutionCompletedMessage(executionNodes.Count)); _logger.Info("[图像链路] ExecutePipelineAsync:执行完成,准备发布 PipelinePreviewUpdatedEvent"); PublishPipelinePreviewUpdated(result, StatusMessage); } @@ -408,7 +470,7 @@ namespace XplorePlane.ViewModels private bool TryReportInvalidParameters() { - var firstInvalidNode = PipelineNodes + var firstInvalidNode = GetNodesInExecutionScope() .Where(n => n.IsEnabled) .OrderBy(n => n.Order) .FirstOrDefault(n => n.Parameters.Any(p => !p.IsValueValid)); @@ -549,6 +611,7 @@ namespace XplorePlane.ViewModels { PipelineNodes.Clear(); SelectedNode = null; + ExecutionEndNode = null; PipelineName = "新建流水线"; PreviewImage = null; _currentFilePath = null; @@ -648,6 +711,7 @@ namespace XplorePlane.ViewModels PipelineNodes.Clear(); SelectedNode = null; + ExecutionEndNode = null; PipelineName = model.Name; SelectedDevice = model.DeviceId; @@ -676,6 +740,8 @@ namespace XplorePlane.ViewModels PipelineNodes.Add(node); } + UpdateExecutionRangeState(); + _logger.Info("流水线已加载:{Name},节点数={Count}", model.Name, PipelineNodes.Count); SetInfoStatus($"已加载流水线:{model.Name}({PipelineNodes.Count} 个节点)"); } @@ -704,6 +770,44 @@ namespace XplorePlane.ViewModels }; } + private System.Collections.Generic.IEnumerable GetNodesInExecutionScope() + { + var orderedNodes = PipelineNodes.OrderBy(n => n.Order); + if (ExecutionEndNode == null) + return orderedNodes; + + return orderedNodes.Where(n => n.Order <= ExecutionEndNode.Order); + } + + 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 string BuildExecutionStartMessage(int executionCount) + { + if (ExecutionEndNode == null) + return "正在执行流水线..."; + + return $"正在执行到“{ExecutionEndNode.DisplayName}” ({executionCount} 个有效节点)..."; + } + + private string BuildExecutionCompletedMessage(int executionCount) + { + if (ExecutionEndNode == null) + return "流水线执行完成"; + + return $"已执行到“{ExecutionEndNode.DisplayName}” ({executionCount} 个有效节点)"; + } + private static string GetPipelineDirectory() { var dir = Path.Combine( diff --git a/XplorePlane/ViewModels/ImageProcessing/PipelineNodeViewModel.cs b/XplorePlane/ViewModels/ImageProcessing/PipelineNodeViewModel.cs index 3da5690..755ee39 100644 --- a/XplorePlane/ViewModels/ImageProcessing/PipelineNodeViewModel.cs +++ b/XplorePlane/ViewModels/ImageProcessing/PipelineNodeViewModel.cs @@ -11,6 +11,8 @@ namespace XplorePlane.ViewModels private int _order; private bool _isSelected; private bool _isEnabled = true; + private bool _isExecutionEndNode; + private bool _isSkippedByExecutionRange; public PipelineNodeViewModel(string operatorKey, string displayName, string iconPath = null) { @@ -51,9 +53,49 @@ namespace XplorePlane.ViewModels public bool IsEnabled { get => _isEnabled; - set => SetProperty(ref _isEnabled, value); + set + { + if (SetProperty(ref _isEnabled, value)) + RaisePropertyChanged(nameof(NodeStateText)); + } } public ObservableCollection Parameters { get; } + + public bool IsExecutionEndNode + { + get => _isExecutionEndNode; + set + { + if (SetProperty(ref _isExecutionEndNode, value)) + RaisePropertyChanged(nameof(NodeStateText)); + } + } + + public bool IsSkippedByExecutionRange + { + get => _isSkippedByExecutionRange; + set + { + if (SetProperty(ref _isSkippedByExecutionRange, value)) + RaisePropertyChanged(nameof(NodeStateText)); + } + } + + public string NodeStateText + { + get + { + if (IsExecutionEndNode && !IsEnabled) + return "执行到此(停用)"; + if (IsExecutionEndNode) + return "执行到此"; + if (!IsEnabled) + return "已停用"; + if (IsSkippedByExecutionRange) + return "未参与本次执行"; + return "已启用"; + } + } } -} \ No newline at end of file +} diff --git a/XplorePlane/ViewModels/Main/MainViewModel.cs b/XplorePlane/ViewModels/Main/MainViewModel.cs index f4770ae..dff8675 100644 --- a/XplorePlane/ViewModels/Main/MainViewModel.cs +++ b/XplorePlane/ViewModels/Main/MainViewModel.cs @@ -205,7 +205,7 @@ namespace XplorePlane.ViewModels LoadImageCommand = new DelegateCommand(ExecuteLoadImage); - OpenPipelineEditorCommand = new DelegateCommand(() => ShowWindow(new Views.PipelineEditorWindow(), "流水线编辑器")); + OpenCncEditorCommand = new DelegateCommand(ExecuteOpenCncEditor); OpenMatrixEditorCommand = new DelegateCommand(() => ShowWindow(new Views.Cnc.MatrixEditorWindow(), "矩阵编排")); OpenToolboxCommand = new DelegateCommand(ExecuteOpenToolbox); diff --git a/XplorePlane/Views/ImageProcessing/PipelineEditorView.xaml b/XplorePlane/Views/ImageProcessing/PipelineEditorView.xaml index c05249d..184061c 100644 --- a/XplorePlane/Views/ImageProcessing/PipelineEditorView.xaml +++ b/XplorePlane/Views/ImageProcessing/PipelineEditorView.xaml @@ -99,6 +99,19 @@ Content="加载" Style="{StaticResource ToolbarBtn}" ToolTip="加载流水线" /> +