修复高级算子的ROI编辑能力

This commit is contained in:
zhengxuan.zhang
2026-05-21 14:35:49 +08:00
parent 2d14954bd3
commit 2ac84ecc85
4 changed files with 142 additions and 18 deletions
@@ -139,10 +139,8 @@ namespace XP.ImageProcessing.RoiControl.Controls
control.ResetView(); control.ResetView();
} }
// 图像切换时清除测量叠加层和ROI // 图像切换时清除测量叠加层,但保留 ROI(ROI 是用户标注,应跟随图像持续显示)
control.ClearMeasurements(); control.ClearMeasurements();
control.ROIItems?.Clear();
control.SelectedROI = null;
// 图像尺寸变化后刷新十字线 // 图像尺寸变化后刷新十字线
if (control.ShowCrosshair) if (control.ShowCrosshair)
@@ -628,6 +628,10 @@ namespace XplorePlane.ViewModels.Cnc
if (_currentProgram == null) if (_currentProgram == null)
return; return;
_logger.Debug("[CNC-ROI][HandleNodeModelChanged] 节点={Name}(idx={Idx})updatedNode类型={Type},调用栈:{Stack}",
nodeVm.Name, nodeVm.Index, updatedNode.GetType().Name,
new System.Diagnostics.StackTrace(1, false).ToString().Split('\n')[0].Trim());
_currentProgram = _cncProgramService.UpdateNode(_currentProgram, nodeVm.Index, updatedNode); _currentProgram = _cncProgramService.UpdateNode(_currentProgram, nodeVm.Index, updatedNode);
IsModified = true; IsModified = true;
ProgramName = _currentProgram.Name; ProgramName = _currentProgram.Name;
@@ -636,6 +640,9 @@ namespace XplorePlane.ViewModels.Cnc
private void RefreshNodes() private void RefreshNodes()
{ {
_logger.Debug("[CNC-ROI][RefreshNodes] 触发,调用栈:{Stack}",
new System.Diagnostics.StackTrace(1, false).ToString().Split('\n')[0].Trim());
NormalizeDefaultNodeNamesInCurrentProgram(); NormalizeDefaultNodeNamesInCurrentProgram();
var selectedId = _preferredSelectedNodeId ?? SelectedNode?.Id; var selectedId = _preferredSelectedNodeId ?? SelectedNode?.Id;
@@ -93,8 +93,21 @@ namespace XplorePlane.ViewModels.Cnc
get => _selectedNode; get => _selectedNode;
set set
{ {
var oldKey = _selectedNode?.OperatorKey ?? "(null)";
var newKey = value?.OperatorKey ?? "(null)";
_logger.Debug("[CNC-ROI][SelectedNode] setter 触发:old={Old}(id={OldId}), new={New}(id={NewId}), caller={Caller}",
oldKey, _selectedNode?.GetHashCode(), newKey, value?.GetHashCode(),
new System.Diagnostics.StackTrace(1, false).GetFrame(0)?.GetMethod()?.Name ?? "?");
if (!SetProperty(ref _selectedNode, value)) if (!SetProperty(ref _selectedNode, value))
{
_logger.Debug("[CNC-ROI][SelectedNode] 值未变化,跳过");
return; return;
}
_logger.Info("[CNC-ROI][SelectedNode] 已切换:{Old} → {New},调用栈:{Stack}",
oldKey, newKey,
new System.Diagnostics.StackTrace(1, false).ToString().Split('\n')[0].Trim());
// 切换节点时停止当前 ROI 编辑 // 切换节点时停止当前 ROI 编辑
CancelRoiEdit(); CancelRoiEdit();
@@ -171,9 +184,13 @@ namespace XplorePlane.ViewModels.Cnc
private void RefreshFromSelection() private void RefreshFromSelection()
{ {
_logger.Info("[CNC-ROI][RefreshFromSelection] 触发,调用栈:{Stack}",
new System.Diagnostics.StackTrace(1, false).ToString().Split('\n')[0].Trim());
var selected = _editorViewModel.SelectedNode; var selected = _editorViewModel.SelectedNode;
if (selected == null || !selected.IsInspectionModule) if (selected == null || !selected.IsInspectionModule)
{ {
_logger.Debug("[CNC-ROI][RefreshFromSelection] 无检测模块,清空");
_activeModuleNode = null; _activeModuleNode = null;
PipelineNodes.Clear(); PipelineNodes.Clear();
SelectedNode = null; SelectedNode = null;
@@ -184,6 +201,8 @@ namespace XplorePlane.ViewModels.Cnc
return; return;
} }
_logger.Info("[CNC-ROI][RefreshFromSelection] 加载模块:{Name}Pipeline节点数={Count}",
selected.Name, selected.Pipeline?.Nodes?.Count ?? 0);
_activeModuleNode = selected; _activeModuleNode = selected;
_currentFilePath = null; _currentFilePath = null;
LoadPipelineModel(_activeModuleNode.Pipeline ?? new PipelineModel LoadPipelineModel(_activeModuleNode.Pipeline ?? new PipelineModel
@@ -389,6 +408,9 @@ namespace XplorePlane.ViewModels.Cnc
private void LoadPipelineModel(PipelineModel pipeline) private void LoadPipelineModel(PipelineModel pipeline)
{ {
_logger.Info("[CNC-ROI][LoadPipelineModel] 开始,节点数={Count},调用栈:{Stack}",
pipeline?.Nodes?.Count ?? 0,
new System.Diagnostics.StackTrace(1, false).ToString().Split('\n')[0].Trim());
_isSynchronizing = true; _isSynchronizing = true;
try try
{ {
@@ -440,7 +462,14 @@ namespace XplorePlane.ViewModels.Cnc
var parameterVm = new ProcessorParameterVM(definition); var parameterVm = new ProcessorParameterVM(definition);
if (savedValues != null && savedValues.TryGetValue(definition.Name, out var savedValue)) if (savedValues != null && savedValues.TryGetValue(definition.Name, out var savedValue))
{ {
parameterVm.Value = ConvertSavedValue(savedValue, definition.ValueType); var converted = ConvertSavedValue(savedValue, definition.ValueType);
parameterVm.Value = converted;
// 只记录 ROI 相关参数
if (definition.Name is "PolyCount" or "RoiMode")
_logger.Debug("[CNC-ROI][LoadNodeParameters] 算子={Key},参数={Param}savedValue={Saved}({SavedType})converted={Conv}({ConvType})",
node.OperatorKey, definition.Name,
savedValue, savedValue?.GetType().Name,
converted, converted?.GetType().Name);
} }
parameterVm.PropertyChanged += (_, e) => parameterVm.PropertyChanged += (_, e) =>
@@ -457,9 +486,29 @@ namespace XplorePlane.ViewModels.Cnc
private void PersistActiveModule(string statusMessage) private void PersistActiveModule(string statusMessage)
{ {
if (!HasActiveModule || _isSynchronizing) if (!HasActiveModule || _isSynchronizing)
{
_logger.Debug("[CNC-ROI][PersistActiveModule] 跳过:HasActiveModule={Has}_isSynchronizing={Sync}msg={Msg}",
HasActiveModule, _isSynchronizing, statusMessage);
return; return;
}
_logger.Debug("[CNC-ROI][PersistActiveModule] 执行:{Msg},调用栈:{Stack}",
statusMessage,
new System.Diagnostics.StackTrace(1, false).ToString().Split('\n')[0].Trim());
_activeModuleNode.Pipeline = BuildPipelineModel(); _activeModuleNode.Pipeline = BuildPipelineModel();
// 记录写入后 Pipeline 中的 ROI 参数
var roiNode = _activeModuleNode.Pipeline?.Nodes?.FirstOrDefault(
n => AdvancedModuleOperatorKeys.Contains(n.OperatorKey));
if (roiNode != null)
{
roiNode.Parameters.TryGetValue("PolyCount", out var pc);
roiNode.Parameters.TryGetValue("RoiMode", out var rm);
_logger.Debug("[CNC-ROI][PersistActiveModule] 写入Pipeline:算子={Key}PolyCount={PC}RoiMode={RM}",
roiNode.OperatorKey, pc, rm);
}
StatusMessage = statusMessage; StatusMessage = statusMessage;
TriggerDebouncedPreview(); TriggerDebouncedPreview();
} }
@@ -698,6 +747,11 @@ namespace XplorePlane.ViewModels.Cnc
StatusMessage = "ROI 编辑中:在图像上点击添加顶点,完成后点击「完成 ROI」"; StatusMessage = "ROI 编辑中:在图像上点击添加顶点,完成后点击「完成 ROI」";
var existingPoints = ReadRoiPoints(SelectedNode); var existingPoints = ReadRoiPoints(SelectedNode);
var polyCountParam = SelectedNode.Parameters.FirstOrDefault(p => p.Name == "PolyCount");
var roiModeParam = SelectedNode.Parameters.FirstOrDefault(p => p.Name == "RoiMode");
_logger.Info("[CNC-ROI] ExecuteEditRoi:算子={Key},已有顶点数={Count}PolyCount参数值={PC}RoiMode={RM}",
SelectedNode.OperatorKey, existingPoints.Count,
polyCountParam?.Value, roiModeParam?.Value);
_eventAggregator.GetEvent<CncRoiEditRequestedEvent>().Publish(new CncRoiEditRequestedPayload _eventAggregator.GetEvent<CncRoiEditRequestedEvent>().Publish(new CncRoiEditRequestedPayload
{ {
@@ -767,10 +821,21 @@ namespace XplorePlane.ViewModels.Cnc
return Convert.ToInt32(polyCountParam.Value); return Convert.ToInt32(polyCountParam.Value);
} }
private static IReadOnlyList<System.Windows.Point> ReadRoiPoints(PipelineNodeViewModel node) private IReadOnlyList<System.Windows.Point> ReadRoiPoints(PipelineNodeViewModel node)
{ {
var roiModeParam = node?.Parameters.FirstOrDefault(p => p.Name == "RoiMode");
var polyCountParam = node?.Parameters.FirstOrDefault(p => p.Name == "PolyCount");
_logger.Debug("[CNC-ROI][ReadRoiPoints] 算子={Key}nodeId={Id}RoiMode={RM}({RMType})PolyCount={PC}({PCType})",
node?.OperatorKey, node?.GetHashCode(),
roiModeParam?.Value, roiModeParam?.Value?.GetType().Name,
polyCountParam?.Value, polyCountParam?.Value?.GetType().Name);
int count = GetRoiPointCount(node); int count = GetRoiPointCount(node);
if (count < 3) return Array.Empty<System.Windows.Point>(); if (count < 3)
{
_logger.Debug("[CNC-ROI][ReadRoiPoints] count={Count} < 3,返回空列表", count);
return Array.Empty<System.Windows.Point>();
}
var points = new List<System.Windows.Point>(count); var points = new List<System.Windows.Point>(count);
for (int i = 0; i < count; i++) for (int i = 0; i < count; i++)
@@ -781,24 +846,34 @@ namespace XplorePlane.ViewModels.Cnc
double y = py != null ? Convert.ToDouble(py.Value) : 0; double y = py != null ? Convert.ToDouble(py.Value) : 0;
points.Add(new System.Windows.Point(x, y)); points.Add(new System.Windows.Point(x, y));
} }
_logger.Debug("[CNC-ROI][ReadRoiPoints] 读取完成,返回 {Count} 个顶点", points.Count);
return points; return points;
} }
private static void WriteRoiPoints(PipelineNodeViewModel node, IReadOnlyList<System.Windows.Point> points) private void WriteRoiPoints(PipelineNodeViewModel node, IReadOnlyList<System.Windows.Point> points)
{ {
if (node == null) return; if (node == null) return;
int count = points?.Count >= 3 ? points.Count : 0; int count = points?.Count >= 3 ? points.Count : 0;
_logger.Debug("[CNC-ROI][WriteRoiPoints] 开始写入:算子={Key}nodeId={Id},输入点数={In},将写count={Count}",
node.OperatorKey, node.GetHashCode(), points?.Count ?? 0, count);
// 更新 RoiModeBgaVoidRate 专用) // 更新 RoiModeBgaVoidRate 专用)
var roiModeParam = node.Parameters.FirstOrDefault(p => p.Name == "RoiMode"); var roiModeParam = node.Parameters.FirstOrDefault(p => p.Name == "RoiMode");
if (roiModeParam != null) if (roiModeParam != null)
roiModeParam.Value = count >= 3 ? "Polygon" : "None"; {
var newRoiMode = count >= 3 ? "Polygon" : "None";
_logger.Debug("[CNC-ROI][WriteRoiPoints] 设置 RoiMode{Old} → {New}", roiModeParam.Value, newRoiMode);
roiModeParam.Value = newRoiMode;
}
// 更新 PolyCount // 更新 PolyCount
var polyCountParam = node.Parameters.FirstOrDefault(p => p.Name == "PolyCount"); var polyCountParam = node.Parameters.FirstOrDefault(p => p.Name == "PolyCount");
if (polyCountParam != null) if (polyCountParam != null)
{
_logger.Debug("[CNC-ROI][WriteRoiPoints] 设置 PolyCount{Old} → {New}", polyCountParam.Value, count);
polyCountParam.Value = count; polyCountParam.Value = count;
}
// 更新坐标(最多 32 个点) // 更新坐标(最多 32 个点)
for (int i = 0; i < 32; i++) for (int i = 0; i < 32; i++)
@@ -810,6 +885,12 @@ namespace XplorePlane.ViewModels.Cnc
if (px != null) px.Value = (int)x; if (px != null) px.Value = (int)x;
if (py != null) py.Value = (int)y; if (py != null) py.Value = (int)y;
} }
// 写入后验证
var verifyRoiMode = node.Parameters.FirstOrDefault(p => p.Name == "RoiMode")?.Value;
var verifyPolyCount = node.Parameters.FirstOrDefault(p => p.Name == "PolyCount")?.Value;
_logger.Debug("[CNC-ROI][WriteRoiPoints] 写入完成验证:RoiMode={RM}PolyCount={PC}",
verifyRoiMode, verifyPolyCount);
} }
} }
} }
@@ -8,6 +8,7 @@ using System.Windows.Media;
using System.Windows.Media.Imaging; using System.Windows.Media.Imaging;
using Microsoft.Win32; using Microsoft.Win32;
using Prism.Ioc; using Prism.Ioc;
using Serilog;
using XP.ImageProcessing.RoiControl.Controls; using XP.ImageProcessing.RoiControl.Controls;
using XplorePlane.Events; using XplorePlane.Events;
using XplorePlane.Services; using XplorePlane.Services;
@@ -17,6 +18,7 @@ namespace XplorePlane.Views
{ {
public partial class ViewportPanelView : UserControl public partial class ViewportPanelView : UserControl
{ {
private static readonly ILogger _log = Log.ForContext<ViewportPanelView>();
private MainViewModel _mainVm; private MainViewModel _mainVm;
private MainViewModel GetMainVm() private MainViewModel GetMainVm()
@@ -187,43 +189,75 @@ namespace XplorePlane.Views
private void OnCncRoiEditRequested(Events.CncRoiEditRequestedPayload payload) private void OnCncRoiEditRequested(Events.CncRoiEditRequestedPayload payload)
{ {
_cncRoiPayload = payload; _log.Information("[CNC-ROI] OnCncRoiEditRequested 触发,ExistingPoints={Count}",
payload?.ExistingPoints?.Count ?? -1);
// 清理旧的 ROI shape // 清理旧状态(在保存新 payload 之前)
CleanupCncRoi(); CleanupCncRoi();
_cncRoiPayload = payload;
// 确保 ROIItems 集合存在 // 确保 ROIItems 集合存在
if (RoiCanvas.ROIItems == null) if (RoiCanvas.ROIItems == null)
{
_log.Debug("[CNC-ROI] ROIItems 为 null,创建新集合");
RoiCanvas.ROIItems = new System.Collections.ObjectModel.ObservableCollection<XP.ImageProcessing.RoiControl.Models.ROIShape>(); RoiCanvas.ROIItems = new System.Collections.ObjectModel.ObservableCollection<XP.ImageProcessing.RoiControl.Models.ROIShape>();
}
// 创建新的多边形 ROI _log.Debug("[CNC-ROI] 当前 ROIItems.Count={Count}", RoiCanvas.ROIItems.Count);
// 创建新的多边形 ROI(先加入 canvas,再添加顶点,确保 UI 绑定已建立)
_cncRoiShape = new XP.ImageProcessing.RoiControl.Models.PolygonROI _cncRoiShape = new XP.ImageProcessing.RoiControl.Models.PolygonROI
{ {
Color = "Cyan", Color = "Cyan",
IsSelected = true IsSelected = false
}; };
// 恢复已保存的顶点 RoiCanvas.ROIItems.Add(_cncRoiShape);
if (payload.ExistingPoints != null) _log.Debug("[CNC-ROI] PolygonROI 已加入 ROIItems,当前 Count={Count}", RoiCanvas.ROIItems.Count);
// 恢复已保存的顶点(canvas 已绑定,每次 Add 都会触发 UI 更新)
if (payload.ExistingPoints != null && payload.ExistingPoints.Count >= 3)
{ {
foreach (var pt in payload.ExistingPoints) foreach (var pt in payload.ExistingPoints)
_cncRoiShape.Points.Add(pt); _cncRoiShape.Points.Add(pt);
_log.Information("[CNC-ROI] 已恢复 {Count} 个顶点", _cncRoiShape.Points.Count);
}
else
{
_log.Information("[CNC-ROI] 无已保存顶点,新建空 ROI");
} }
RoiCanvas.ROIItems.Add(_cncRoiShape); // 顶点变化时回调(在顶点恢复之后订阅,避免恢复时触发不必要的回调)
RoiCanvas.SelectedROI = _cncRoiShape; _cncRoiShape.Points.CollectionChanged += OnCncRoiPointsCollectionChanged;
// 禁用右键菜单,启用画布点击添加顶点 // 禁用右键菜单,启用画布点击添加顶点
RoiCanvas.SetValue(System.Windows.Controls.ContextMenuService.IsEnabledProperty, false); RoiCanvas.SetValue(System.Windows.Controls.ContextMenuService.IsEnabledProperty, false);
RoiCanvas.AddHandler(XP.ImageProcessing.RoiControl.Controls.PolygonRoiCanvas.CanvasClickedEvent, RoiCanvas.AddHandler(XP.ImageProcessing.RoiControl.Controls.PolygonRoiCanvas.CanvasClickedEvent,
new RoutedEventHandler(OnCncRoiCanvasClicked)); new RoutedEventHandler(OnCncRoiCanvasClicked));
// 顶点变化时回调 // 延迟设置 SelectedROI,确保 ItemsControl 完成布局后 Adorner 才能正确创建
_cncRoiShape.Points.CollectionChanged += OnCncRoiPointsCollectionChanged; var shapeRef = _cncRoiShape;
Dispatcher.BeginInvoke(new Action(() =>
{
if (shapeRef == _cncRoiShape) // 确保没有被 cleanup 掉
{
_log.Debug("[CNC-ROI] Dispatcher 延迟:设置 SelectedROIPoints.Count={Count}", shapeRef.Points.Count);
RoiCanvas.SelectedROI = null;
RoiCanvas.SelectedROI = shapeRef;
shapeRef.IsSelected = true;
_log.Information("[CNC-ROI] SelectedROI 已设置,ROI 应显示在画布上");
}
else
{
_log.Warning("[CNC-ROI] Dispatcher 延迟:shapeRef 已被 cleanup,跳过 SelectedROI 设置");
}
}), System.Windows.Threading.DispatcherPriority.Loaded);
} }
private void OnCncRoiEditCancelled() private void OnCncRoiEditCancelled()
{ {
_log.Information("[CNC-ROI] OnCncRoiEditCancelled 触发");
CleanupCncRoi(); CleanupCncRoi();
} }
@@ -232,6 +266,8 @@ namespace XplorePlane.Views
if (_cncRoiShape == null || _cncRoiPayload == null) return; if (_cncRoiShape == null || _cncRoiPayload == null) return;
if (e is XP.ImageProcessing.RoiControl.Controls.CanvasClickedEventArgs args) if (e is XP.ImageProcessing.RoiControl.Controls.CanvasClickedEventArgs args)
{ {
_log.Debug("[CNC-ROI] 画布点击:Position={X},{Y},当前顶点数={Count}",
args.Position.X, args.Position.Y, _cncRoiShape.Points.Count);
InsertPointToPolygon(args.Position, _cncRoiShape.Points); InsertPointToPolygon(args.Position, _cncRoiShape.Points);
_cncRoiShape.IsSelected = true; _cncRoiShape.IsSelected = true;
RoiCanvas.SelectedROI = _cncRoiShape; RoiCanvas.SelectedROI = _cncRoiShape;
@@ -242,11 +278,13 @@ namespace XplorePlane.Views
System.Collections.Specialized.NotifyCollectionChangedEventArgs e) System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{ {
if (_cncRoiPayload?.OnPointsChanged == null || _cncRoiShape == null) return; if (_cncRoiPayload?.OnPointsChanged == null || _cncRoiShape == null) return;
_log.Debug("[CNC-ROI] Points 变化,当前顶点数={Count}", _cncRoiShape.Points.Count);
_cncRoiPayload.OnPointsChanged(new List<System.Windows.Point>(_cncRoiShape.Points)); _cncRoiPayload.OnPointsChanged(new List<System.Windows.Point>(_cncRoiShape.Points));
} }
private void CleanupCncRoi() private void CleanupCncRoi()
{ {
_log.Debug("[CNC-ROI] CleanupCncRoi_cncRoiShape={HasShape}", _cncRoiShape != null);
if (_cncRoiShape != null) if (_cncRoiShape != null)
{ {
_cncRoiShape.Points.CollectionChanged -= OnCncRoiPointsCollectionChanged; _cncRoiShape.Points.CollectionChanged -= OnCncRoiPointsCollectionChanged;