diff --git a/XplorePlane/Events/CncRoiEditRequestedEvent.cs b/XplorePlane/Events/CncRoiEditRequestedEvent.cs new file mode 100644 index 0000000..0211191 --- /dev/null +++ b/XplorePlane/Events/CncRoiEditRequestedEvent.cs @@ -0,0 +1,37 @@ +using Prism.Events; +using System; +using System.Collections.Generic; +using System.Windows; + +namespace XplorePlane.Events +{ + /// + /// 请求在主视口画布上激活 ROI 编辑模式的事件。 + /// 由 CNC 流水线编辑器发布,ViewportPanelView 订阅并操作 PolygonRoiCanvas。 + /// + public sealed class CncRoiEditRequestedEvent : PubSubEvent + { + } + + public class CncRoiEditRequestedPayload + { + /// 已保存的 ROI 多边形顶点(图像坐标)。为空表示新建 ROI。 + public IReadOnlyList ExistingPoints { get; set; } + + /// + /// ROI 顶点变化时的回调,参数为最新的顶点列表。 + /// 每次用户添加/移动顶点后调用,用于实时写回参数并触发预览。 + /// + public Action> OnPointsChanged { get; set; } + + /// ROI 编辑结束(用户完成或取消)时的回调。 + public Action OnEditFinished { get; set; } + } + + /// + /// 请求停止 ROI 编辑模式(清理画布状态)。 + /// + public sealed class CncRoiEditCancelledEvent : PubSubEvent + { + } +} diff --git a/XplorePlane/ViewModels/Cnc/CncInspectionModulePipelineViewModel.cs b/XplorePlane/ViewModels/Cnc/CncInspectionModulePipelineViewModel.cs index 426744d..01fd133 100644 --- a/XplorePlane/ViewModels/Cnc/CncInspectionModulePipelineViewModel.cs +++ b/XplorePlane/ViewModels/Cnc/CncInspectionModulePipelineViewModel.cs @@ -75,6 +75,10 @@ namespace XplorePlane.ViewModels.Cnc SaveAsPipelineCommand = new DelegateCommand(async () => await SaveAsPipelineAsync()); LoadPipelineCommand = new DelegateCommand(async () => await LoadPipelineAsync()); + EditRoiCommand = new DelegateCommand(ExecuteEditRoi, () => SelectedNodeIsAdvancedModule && !IsRoiEditing); + ClearRoiCommand = new DelegateCommand(ExecuteClearRoi, () => SelectedNodeIsAdvancedModule && SelectedNodeHasRoi); + FinishRoiCommand = new DelegateCommand(FinishRoiEdit, () => IsRoiEditing); + _editorViewModel.PropertyChanged += OnEditorPropertyChanged; RefreshFromSelection(); @@ -91,6 +95,15 @@ namespace XplorePlane.ViewModels.Cnc { if (!SetProperty(ref _selectedNode, value)) return; + + // 切换节点时停止当前 ROI 编辑 + CancelRoiEdit(); + RaisePropertyChanged(nameof(SelectedNodeIsAdvancedModule)); + RaisePropertyChanged(nameof(SelectedNodeHasRoi)); + RaisePropertyChanged(nameof(SelectedNodeRoiSummary)); + // CanExecute 依赖 SelectedNode,必须手动通知 + (EditRoiCommand as DelegateCommand)?.RaiseCanExecuteChanged(); + (ClearRoiCommand as DelegateCommand)?.RaiseCanExecuteChanged(); } } @@ -623,5 +636,180 @@ namespace XplorePlane.ViewModels.Cnc return jsonElement.ToString(); } } + + private static readonly HashSet AdvancedModuleOperatorKeys = new(StringComparer.OrdinalIgnoreCase) + { + "BgaVoidRate", + "VoidMeasurement" + }; + + // ── ROI 内联编辑 ────────────────────────────────────────────────────── + + private bool _isRoiEditing; + + /// 当前选中节点是否为支持 ROI 的高级模块算子 + public bool SelectedNodeIsAdvancedModule => + SelectedNode != null && AdvancedModuleOperatorKeys.Contains(SelectedNode.OperatorKey); + + /// 当前选中节点是否已有保存的 ROI 多边形 + public bool SelectedNodeHasRoi => GetRoiPointCount(SelectedNode) >= 3; + + /// ROI 摘要文字(如"多边形 ROI:6 个顶点") + public string SelectedNodeRoiSummary + { + get + { + int count = GetRoiPointCount(SelectedNode); + if (count < 3) return "未设置 ROI(全图检测)"; + return $"多边形 ROI:{count} 个顶点"; + } + } + + /// 是否正在编辑 ROI + public bool IsRoiEditing + { + get => _isRoiEditing; + private set + { + if (SetProperty(ref _isRoiEditing, value)) + { + (EditRoiCommand as DelegateCommand)?.RaiseCanExecuteChanged(); + (ClearRoiCommand as DelegateCommand)?.RaiseCanExecuteChanged(); + (FinishRoiCommand as DelegateCommand)?.RaiseCanExecuteChanged(); + } + } + } + + public ICommand EditRoiCommand { get; } + public ICommand ClearRoiCommand { get; } + public ICommand FinishRoiCommand { get; } + + private void InitRoiCommands() + { + // Commands are initialized in constructor via field initializers below + } + + private void ExecuteEditRoi() + { + if (SelectedNode == null || !SelectedNodeIsAdvancedModule || _eventAggregator == null) + return; + + IsRoiEditing = true; + StatusMessage = "ROI 编辑中:在图像上点击添加顶点,完成后点击「完成 ROI」"; + + var existingPoints = ReadRoiPoints(SelectedNode); + + _eventAggregator.GetEvent().Publish(new CncRoiEditRequestedPayload + { + ExistingPoints = existingPoints, + OnPointsChanged = points => OnRoiPointsChanged(SelectedNode, points), + OnEditFinished = FinishRoiEdit + }); + } + + private void ExecuteClearRoi() + { + if (SelectedNode == null || !SelectedNodeIsAdvancedModule) return; + + WriteRoiPoints(SelectedNode, null); + CancelRoiEdit(); + PersistActiveModule("已清除 ROI,将使用全图检测"); + RaiseRoiProperties(); + } + + private void OnRoiPointsChanged(PipelineNodeViewModel node, IReadOnlyList points) + { + if (node == null) return; + WriteRoiPoints(node, points); + PersistActiveModule($"ROI 已更新:{points?.Count ?? 0} 个顶点"); + RaiseRoiProperties(); + TriggerDebouncedPreview(); + } + + private void FinishRoiEdit() + { + IsRoiEditing = false; + StatusMessage = HasActiveModule + ? $"正在编辑检测模块:{_activeModuleNode?.Name}" + : "请选择检测模块以编辑其流水线。"; + RaiseRoiProperties(); + } + + private void CancelRoiEdit() + { + if (!_isRoiEditing) return; + IsRoiEditing = false; + _eventAggregator?.GetEvent().Publish(); + } + + private void RaiseRoiProperties() + { + RaisePropertyChanged(nameof(SelectedNodeHasRoi)); + RaisePropertyChanged(nameof(SelectedNodeRoiSummary)); + } + + // ── ROI 参数读写 ────────────────────────────────────────────────────── + + private static int GetRoiPointCount(PipelineNodeViewModel node) + { + if (node == null) return 0; + + // BgaVoidRate 用 RoiMode + PolyCount + var roiModeParam = node.Parameters.FirstOrDefault(p => p.Name == "RoiMode"); + if (roiModeParam != null) + { + if (!string.Equals(roiModeParam.Value?.ToString(), "Polygon", StringComparison.OrdinalIgnoreCase)) + return 0; + } + + var polyCountParam = node.Parameters.FirstOrDefault(p => p.Name == "PolyCount"); + if (polyCountParam == null) return 0; + return Convert.ToInt32(polyCountParam.Value); + } + + private static IReadOnlyList ReadRoiPoints(PipelineNodeViewModel node) + { + int count = GetRoiPointCount(node); + if (count < 3) return Array.Empty(); + + var points = new List(count); + for (int i = 0; i < count; i++) + { + var px = node.Parameters.FirstOrDefault(p => p.Name == $"PolyX{i}"); + var py = node.Parameters.FirstOrDefault(p => p.Name == $"PolyY{i}"); + double x = px != null ? Convert.ToDouble(px.Value) : 0; + double y = py != null ? Convert.ToDouble(py.Value) : 0; + points.Add(new System.Windows.Point(x, y)); + } + return points; + } + + private static void WriteRoiPoints(PipelineNodeViewModel node, IReadOnlyList points) + { + if (node == null) return; + + int count = points?.Count >= 3 ? points.Count : 0; + + // 更新 RoiMode(BgaVoidRate 专用) + var roiModeParam = node.Parameters.FirstOrDefault(p => p.Name == "RoiMode"); + if (roiModeParam != null) + roiModeParam.Value = count >= 3 ? "Polygon" : "None"; + + // 更新 PolyCount + var polyCountParam = node.Parameters.FirstOrDefault(p => p.Name == "PolyCount"); + if (polyCountParam != null) + polyCountParam.Value = count; + + // 更新坐标(最多 32 个点) + for (int i = 0; i < 32; i++) + { + var px = node.Parameters.FirstOrDefault(p => p.Name == $"PolyX{i}"); + var py = node.Parameters.FirstOrDefault(p => p.Name == $"PolyY{i}"); + double x = (points != null && i < points.Count) ? points[i].X : 0; + double y = (points != null && i < points.Count) ? points[i].Y : 0; + if (px != null) px.Value = (int)x; + if (py != null) py.Value = (int)y; + } + } } } diff --git a/XplorePlane/Views/ImageProcessing/BgaDetectionPanel.xaml.cs b/XplorePlane/Views/ImageProcessing/BgaDetectionPanel.xaml.cs index 86e335e..da3595c 100644 --- a/XplorePlane/Views/ImageProcessing/BgaDetectionPanel.xaml.cs +++ b/XplorePlane/Views/ImageProcessing/BgaDetectionPanel.xaml.cs @@ -17,7 +17,6 @@ namespace XplorePlane.Views.ImageProcessing Loaded += (s, e) => { - // 获取主界面的 RoiCanvas 传给 ViewModel var mainWin = Owner as MainWindow; if (mainWin != null) { @@ -26,32 +25,23 @@ namespace XplorePlane.Views.ImageProcessing vm.SetCanvas(canvas); } - // 从 MainViewModel 获取 CncEditorViewModel 引用 if (DataContext is BgaDetectionViewModel bgaVm && Owner?.DataContext is ViewModels.MainViewModel mainVm) { var cncEditorField = mainVm.GetType().GetField("_cncEditorViewModel", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); if (cncEditorField?.GetValue(mainVm) is CncEditorViewModel cncEditor) - { bgaVm.SetCncEditorViewModel(cncEditor); - } } }; Closed += (s, e) => { if (DataContext is BgaDetectionViewModel vm) - { - // 恢复右键菜单,但保留 ROI vm.RestoreContextMenu(); - } }; } - private void Close_Click(object sender, RoutedEventArgs e) - { - Close(); - } + private void Close_Click(object sender, RoutedEventArgs e) => Close(); private static T FindChild(DependencyObject parent) where T : DependencyObject { diff --git a/XplorePlane/Views/ImageProcessing/PipelineEditorView.xaml b/XplorePlane/Views/ImageProcessing/PipelineEditorView.xaml index 270f3a3..ac8cd0e 100644 --- a/XplorePlane/Views/ImageProcessing/PipelineEditorView.xaml +++ b/XplorePlane/Views/ImageProcessing/PipelineEditorView.xaml @@ -298,6 +298,147 @@ FontWeight="Bold" Foreground="#555" Text="属性" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +