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="属性" />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+