diff --git a/XP.ImageProcessing.RoiControl/Controls/PolygonRoiCanvas.xaml.cs b/XP.ImageProcessing.RoiControl/Controls/PolygonRoiCanvas.xaml.cs index ee9872b..9e40016 100644 --- a/XP.ImageProcessing.RoiControl/Controls/PolygonRoiCanvas.xaml.cs +++ b/XP.ImageProcessing.RoiControl/Controls/PolygonRoiCanvas.xaml.cs @@ -139,10 +139,8 @@ namespace XP.ImageProcessing.RoiControl.Controls control.ResetView(); } - // 图像切换时清除测量、叠加层和ROI + // 图像切换时清除测量和叠加层,但保留 ROI(ROI 是用户标注,应跟随图像持续显示) control.ClearMeasurements(); - control.ROIItems?.Clear(); - control.SelectedROI = null; // 图像尺寸变化后刷新十字线 if (control.ShowCrosshair) diff --git a/XplorePlane/ViewModels/Cnc/CncEditorViewModel.cs b/XplorePlane/ViewModels/Cnc/CncEditorViewModel.cs index b82bc00..5e4ae4d 100644 --- a/XplorePlane/ViewModels/Cnc/CncEditorViewModel.cs +++ b/XplorePlane/ViewModels/Cnc/CncEditorViewModel.cs @@ -628,6 +628,10 @@ namespace XplorePlane.ViewModels.Cnc if (_currentProgram == null) 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); IsModified = true; ProgramName = _currentProgram.Name; @@ -636,6 +640,9 @@ namespace XplorePlane.ViewModels.Cnc private void RefreshNodes() { + _logger.Debug("[CNC-ROI][RefreshNodes] 触发,调用栈:{Stack}", + new System.Diagnostics.StackTrace(1, false).ToString().Split('\n')[0].Trim()); + NormalizeDefaultNodeNamesInCurrentProgram(); var selectedId = _preferredSelectedNodeId ?? SelectedNode?.Id; diff --git a/XplorePlane/ViewModels/Cnc/CncInspectionModulePipelineViewModel.cs b/XplorePlane/ViewModels/Cnc/CncInspectionModulePipelineViewModel.cs index 01fd133..cdfc014 100644 --- a/XplorePlane/ViewModels/Cnc/CncInspectionModulePipelineViewModel.cs +++ b/XplorePlane/ViewModels/Cnc/CncInspectionModulePipelineViewModel.cs @@ -93,8 +93,21 @@ namespace XplorePlane.ViewModels.Cnc get => _selectedNode; 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)) + { + _logger.Debug("[CNC-ROI][SelectedNode] 值未变化,跳过"); return; + } + + _logger.Info("[CNC-ROI][SelectedNode] 已切换:{Old} → {New},调用栈:{Stack}", + oldKey, newKey, + new System.Diagnostics.StackTrace(1, false).ToString().Split('\n')[0].Trim()); // 切换节点时停止当前 ROI 编辑 CancelRoiEdit(); @@ -171,9 +184,13 @@ namespace XplorePlane.ViewModels.Cnc private void RefreshFromSelection() { + _logger.Info("[CNC-ROI][RefreshFromSelection] 触发,调用栈:{Stack}", + new System.Diagnostics.StackTrace(1, false).ToString().Split('\n')[0].Trim()); + var selected = _editorViewModel.SelectedNode; if (selected == null || !selected.IsInspectionModule) { + _logger.Debug("[CNC-ROI][RefreshFromSelection] 无检测模块,清空"); _activeModuleNode = null; PipelineNodes.Clear(); SelectedNode = null; @@ -184,6 +201,8 @@ namespace XplorePlane.ViewModels.Cnc return; } + _logger.Info("[CNC-ROI][RefreshFromSelection] 加载模块:{Name},Pipeline节点数={Count}", + selected.Name, selected.Pipeline?.Nodes?.Count ?? 0); _activeModuleNode = selected; _currentFilePath = null; LoadPipelineModel(_activeModuleNode.Pipeline ?? new PipelineModel @@ -389,6 +408,9 @@ namespace XplorePlane.ViewModels.Cnc 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; try { @@ -440,7 +462,14 @@ namespace XplorePlane.ViewModels.Cnc var parameterVm = new ProcessorParameterVM(definition); 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) => @@ -457,9 +486,29 @@ namespace XplorePlane.ViewModels.Cnc private void PersistActiveModule(string statusMessage) { if (!HasActiveModule || _isSynchronizing) + { + _logger.Debug("[CNC-ROI][PersistActiveModule] 跳过:HasActiveModule={Has},_isSynchronizing={Sync},msg={Msg}", + HasActiveModule, _isSynchronizing, statusMessage); return; + } + + _logger.Debug("[CNC-ROI][PersistActiveModule] 执行:{Msg},调用栈:{Stack}", + statusMessage, + new System.Diagnostics.StackTrace(1, false).ToString().Split('\n')[0].Trim()); _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; TriggerDebouncedPreview(); } @@ -698,6 +747,11 @@ namespace XplorePlane.ViewModels.Cnc StatusMessage = "ROI 编辑中:在图像上点击添加顶点,完成后点击「完成 ROI」"; 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().Publish(new CncRoiEditRequestedPayload { @@ -767,10 +821,21 @@ namespace XplorePlane.ViewModels.Cnc return Convert.ToInt32(polyCountParam.Value); } - private static IReadOnlyList ReadRoiPoints(PipelineNodeViewModel node) + private IReadOnlyList 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); - if (count < 3) return Array.Empty(); + if (count < 3) + { + _logger.Debug("[CNC-ROI][ReadRoiPoints] count={Count} < 3,返回空列表", count); + return Array.Empty(); + } var points = new List(count); for (int i = 0; i < count; i++) @@ -781,24 +846,34 @@ namespace XplorePlane.ViewModels.Cnc double y = py != null ? Convert.ToDouble(py.Value) : 0; points.Add(new System.Windows.Point(x, y)); } + _logger.Debug("[CNC-ROI][ReadRoiPoints] 读取完成,返回 {Count} 个顶点", points.Count); return points; } - private static void WriteRoiPoints(PipelineNodeViewModel node, IReadOnlyList points) + private void WriteRoiPoints(PipelineNodeViewModel node, IReadOnlyList points) { if (node == null) return; 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); // 更新 RoiMode(BgaVoidRate 专用) var roiModeParam = node.Parameters.FirstOrDefault(p => p.Name == "RoiMode"); 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 var polyCountParam = node.Parameters.FirstOrDefault(p => p.Name == "PolyCount"); if (polyCountParam != null) + { + _logger.Debug("[CNC-ROI][WriteRoiPoints] 设置 PolyCount:{Old} → {New}", polyCountParam.Value, count); polyCountParam.Value = count; + } // 更新坐标(最多 32 个点) for (int i = 0; i < 32; i++) @@ -810,6 +885,12 @@ namespace XplorePlane.ViewModels.Cnc if (px != null) px.Value = (int)x; 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); } } } diff --git a/XplorePlane/Views/Main/ViewportPanelView.xaml.cs b/XplorePlane/Views/Main/ViewportPanelView.xaml.cs index d319cad..8dac837 100644 --- a/XplorePlane/Views/Main/ViewportPanelView.xaml.cs +++ b/XplorePlane/Views/Main/ViewportPanelView.xaml.cs @@ -8,6 +8,7 @@ using System.Windows.Media; using System.Windows.Media.Imaging; using Microsoft.Win32; using Prism.Ioc; +using Serilog; using XP.ImageProcessing.RoiControl.Controls; using XplorePlane.Events; using XplorePlane.Services; @@ -17,6 +18,7 @@ namespace XplorePlane.Views { public partial class ViewportPanelView : UserControl { + private static readonly ILogger _log = Log.ForContext(); private MainViewModel _mainVm; private MainViewModel GetMainVm() @@ -187,43 +189,75 @@ namespace XplorePlane.Views private void OnCncRoiEditRequested(Events.CncRoiEditRequestedPayload payload) { - _cncRoiPayload = payload; + _log.Information("[CNC-ROI] OnCncRoiEditRequested 触发,ExistingPoints={Count}", + payload?.ExistingPoints?.Count ?? -1); - // 清理旧的 ROI shape + // 先清理旧状态(在保存新 payload 之前) CleanupCncRoi(); + _cncRoiPayload = payload; + // 确保 ROIItems 集合存在 if (RoiCanvas.ROIItems == null) + { + _log.Debug("[CNC-ROI] ROIItems 为 null,创建新集合"); RoiCanvas.ROIItems = new System.Collections.ObjectModel.ObservableCollection(); + } - // 创建新的多边形 ROI + _log.Debug("[CNC-ROI] 当前 ROIItems.Count={Count}", RoiCanvas.ROIItems.Count); + + // 创建新的多边形 ROI(先加入 canvas,再添加顶点,确保 UI 绑定已建立) _cncRoiShape = new XP.ImageProcessing.RoiControl.Models.PolygonROI { Color = "Cyan", - IsSelected = true + IsSelected = false }; - // 恢复已保存的顶点 - if (payload.ExistingPoints != null) + RoiCanvas.ROIItems.Add(_cncRoiShape); + _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) _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.AddHandler(XP.ImageProcessing.RoiControl.Controls.PolygonRoiCanvas.CanvasClickedEvent, new RoutedEventHandler(OnCncRoiCanvasClicked)); - // 顶点变化时回调 - _cncRoiShape.Points.CollectionChanged += OnCncRoiPointsCollectionChanged; + // 延迟设置 SelectedROI,确保 ItemsControl 完成布局后 Adorner 才能正确创建 + var shapeRef = _cncRoiShape; + Dispatcher.BeginInvoke(new Action(() => + { + if (shapeRef == _cncRoiShape) // 确保没有被 cleanup 掉 + { + _log.Debug("[CNC-ROI] Dispatcher 延迟:设置 SelectedROI,Points.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() { + _log.Information("[CNC-ROI] OnCncRoiEditCancelled 触发"); CleanupCncRoi(); } @@ -232,6 +266,8 @@ namespace XplorePlane.Views if (_cncRoiShape == null || _cncRoiPayload == null) return; 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); _cncRoiShape.IsSelected = true; RoiCanvas.SelectedROI = _cncRoiShape; @@ -242,11 +278,13 @@ namespace XplorePlane.Views System.Collections.Specialized.NotifyCollectionChangedEventArgs e) { if (_cncRoiPayload?.OnPointsChanged == null || _cncRoiShape == null) return; + _log.Debug("[CNC-ROI] Points 变化,当前顶点数={Count}", _cncRoiShape.Points.Count); _cncRoiPayload.OnPointsChanged(new List(_cncRoiShape.Points)); } private void CleanupCncRoi() { + _log.Debug("[CNC-ROI] CleanupCncRoi,_cncRoiShape={HasShape}", _cncRoiShape != null); if (_cncRoiShape != null) { _cncRoiShape.Points.CollectionChanged -= OnCncRoiPointsCollectionChanged;