diff --git a/XP.ImageProcessing.RoiControl/Controls/PolygonRoiCanvas.xaml.cs b/XP.ImageProcessing.RoiControl/Controls/PolygonRoiCanvas.xaml.cs index d529e63..7c9581b 100644 --- a/XP.ImageProcessing.RoiControl/Controls/PolygonRoiCanvas.xaml.cs +++ b/XP.ImageProcessing.RoiControl/Controls/PolygonRoiCanvas.xaml.cs @@ -370,6 +370,28 @@ namespace XP.ImageProcessing.RoiControl.Controls private Point? _bgaPendingCenter; // 等待第二次点击定半径 private Ellipse _bgaPendingDot; + // 气泡测量状态 + public enum BubbleSubTool { Roi, Wand, Brush, Eraser } + private BubbleSubTool _bubbleTool = BubbleSubTool.Roi; + private Rectangle _bubbleRoiRect; + private Ellipse _bubbleRoiHandle; // 右下角调整手柄 + private Rect? _bubbleRoi; + private Point? _bubbleRoiStart; + private bool _bubbleRoiDragging; + private bool _bubbleRoiMoving; // 拖动整个 ROI + private bool _bubbleRoiResizing; // 右下角调整大小 + private Point _bubbleRoiDragOffset; + private Image _bubbleMaskImage; + private System.Windows.Media.Imaging.WriteableBitmap _bubbleMask; + private int _bubbleThreshold = 128; + private int _bubbleBrushSize = 5; + private bool _bubbleBrushDragging; + + public void SetBubbleTool(BubbleSubTool tool) => _bubbleTool = tool; + public void SetBubbleThreshold(int val) => _bubbleThreshold = val; + public void SetBubbleBrushSize(int val) => _bubbleBrushSize = val; + public Rect? BubbleRoi => _bubbleRoi; + // 拖拽状态 private Ellipse _mDraggingDot; private object _mDraggingOwner; @@ -448,6 +470,7 @@ namespace XP.ImageProcessing.RoiControl.Controls HandleFillRateClick(pos); else if (CurrentMeasureMode == Models.MeasureMode.BgaVoid) HandleBgaVoidClick(pos); + // BubbleMeasure 的点击在 MouseDown/Move/Up 中处理(拖拽画 ROI 和画笔) } // ── 点点距 ── @@ -1084,6 +1107,125 @@ namespace XP.ImageProcessing.RoiControl.Controls } } + // ── 气泡测量辅助 ── + + private void EnsureBubbleRoiVisuals() + { + EnsureMeasureOverlay(); + if (_bubbleRoiRect == null) + { + _bubbleRoiRect = new Rectangle + { + Stroke = Brushes.Red, + StrokeThickness = 1.5, + Fill = Brushes.Transparent, + Visibility = Visibility.Collapsed, + IsHitTestVisible = false + }; + _measureOverlay.Children.Add(_bubbleRoiRect); + } + if (_bubbleRoiHandle == null) + { + _bubbleRoiHandle = new Ellipse + { + Width = 10, Height = 10, + Fill = Brushes.Red, Stroke = Brushes.White, StrokeThickness = 1, + Cursor = Cursors.SizeNWSE, + Visibility = Visibility.Collapsed, + IsHitTestVisible = false // 命中测试由 MouseDown 中的距离判断处理 + }; + _measureOverlay.Children.Add(_bubbleRoiHandle); + } + } + + private void SyncBubbleRoiVisuals() + { + if (_bubbleRoiRect == null || !_bubbleRoi.HasValue) return; + var r = _bubbleRoi.Value; + Canvas.SetLeft(_bubbleRoiRect, r.X); + Canvas.SetTop(_bubbleRoiRect, r.Y); + _bubbleRoiRect.Width = r.Width; + _bubbleRoiRect.Height = r.Height; + _bubbleRoiRect.Visibility = Visibility.Visible; + + if (_bubbleRoiHandle != null) + { + Canvas.SetLeft(_bubbleRoiHandle, r.Right - _bubbleRoiHandle.Width / 2); + Canvas.SetTop(_bubbleRoiHandle, r.Bottom - _bubbleRoiHandle.Height / 2); + _bubbleRoiHandle.Visibility = Visibility.Visible; + } + } + + private void InitBubbleMask() + { + if (!_bubbleRoi.HasValue) return; + int w = (int)CanvasWidth, h = (int)CanvasHeight; + if (w <= 0 || h <= 0) return; + + _bubbleMask = new System.Windows.Media.Imaging.WriteableBitmap(w, h, 96, 96, + PixelFormats.Bgra32, null); + + if (_bubbleMaskImage == null) + { + EnsureMeasureOverlay(); + _bubbleMaskImage = new Image + { + IsHitTestVisible = false, + Opacity = 0.45, + Stretch = Stretch.Fill + }; + _bubbleMaskImage.SetBinding(Image.WidthProperty, new System.Windows.Data.Binding("CanvasWidth") { Source = this }); + _bubbleMaskImage.SetBinding(Image.HeightProperty, new System.Windows.Data.Binding("CanvasHeight") { Source = this }); + _measureOverlay.Children.Add(_bubbleMaskImage); + } + _bubbleMaskImage.Source = _bubbleMask; + } + + private void ApplyBrushAt(Point pos) + { + // 画笔/橡皮:暂时空实现,下一步实现 + } + + private void UpdateBubbleResult() + { + // 计算空隙率:暂时空实现,下一步实现 + } + + /// 清除气泡测量所有状态 + public void ClearBubbleMeasure() + { + if (_measureOverlay != null) + { + if (_bubbleRoiRect != null) { _measureOverlay.Children.Remove(_bubbleRoiRect); _bubbleRoiRect = null; } + if (_bubbleRoiHandle != null) { _measureOverlay.Children.Remove(_bubbleRoiHandle); _bubbleRoiHandle = null; } + if (_bubbleMaskImage != null) { _measureOverlay.Children.Remove(_bubbleMaskImage); _bubbleMaskImage = null; } + } + _bubbleRoi = null; + _bubbleMask = null; + _bubbleRoiStart = null; + _bubbleRoiDragging = false; + _bubbleRoiMoving = false; + _bubbleRoiResizing = false; + _bubbleBrushDragging = false; + _bubbleTool = BubbleSubTool.Roi; + } + + // 气泡工具切换事件 + public static readonly RoutedEvent BubbleToolChangedEvent = + EventManager.RegisterRoutedEvent(nameof(BubbleToolChanged), RoutingStrategy.Bubble, + typeof(RoutedEventHandler), typeof(PolygonRoiCanvas)); + + public event RoutedEventHandler BubbleToolChanged + { + add { AddHandler(BubbleToolChangedEvent, value); } + remove { RemoveHandler(BubbleToolChangedEvent, value); } + } + + private void RaiseBubbleToolChanged() + { + RaiseEvent(new RoutedEventArgs(BubbleToolChangedEvent)); + } + // ── 重编号 ── private void RenumberAll() @@ -1354,6 +1496,57 @@ namespace XP.ImageProcessing.RoiControl.Controls private void Canvas_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) { + // 气泡测量模式:ROI 拖拽 / 画笔涂抹 + if (CurrentMeasureMode == Models.MeasureMode.BubbleMeasure) + { + var pos = e.GetPosition(mainCanvas); + if (_bubbleTool == BubbleSubTool.Roi) + { + // 检查是否点击了右下角手柄 + if (_bubbleRoiHandle != null && _bubbleRoi.HasValue) + { + var hx = Canvas.GetLeft(_bubbleRoiHandle) + _bubbleRoiHandle.Width / 2; + var hy = Canvas.GetTop(_bubbleRoiHandle) + _bubbleRoiHandle.Height / 2; + if (Math.Abs(pos.X - hx) < 10 && Math.Abs(pos.Y - hy) < 10) + { + _bubbleRoiResizing = true; + mainCanvas.CaptureMouse(); + e.Handled = true; + return; + } + } + // 检查是否点击了 ROI 内部(拖动) + if (_bubbleRoi.HasValue && _bubbleRoi.Value.Contains(pos)) + { + _bubbleRoiMoving = true; + _bubbleRoiDragOffset = new Point(pos.X - _bubbleRoi.Value.X, pos.Y - _bubbleRoi.Value.Y); + mainCanvas.CaptureMouse(); + e.Handled = true; + return; + } + // 没有 ROI 时才画新的,有 ROI 时点击外部不拦截(让图像正常拖动) + if (!_bubbleRoi.HasValue) + { + _bubbleRoiStart = pos; + _bubbleRoiDragging = true; + EnsureBubbleRoiVisuals(); + mainCanvas.CaptureMouse(); + e.Handled = true; + return; + } + // 有 ROI 但点击了外部 → 不拦截,走正常拖动逻辑 + } + if ((_bubbleTool == BubbleSubTool.Brush || _bubbleTool == BubbleSubTool.Eraser) && _bubbleRoi.HasValue) + { + _bubbleBrushDragging = true; + ApplyBrushAt(pos); + mainCanvas.CaptureMouse(); + e.Handled = true; + return; + } + // 魔棒在 MouseUp(CanvasClicked)中处理 + } + lastMousePosition = e.GetPosition(imageDisplayGrid); isDragging = false; mainCanvas.CaptureMouse(); @@ -1361,6 +1554,54 @@ namespace XP.ImageProcessing.RoiControl.Controls private void Canvas_MouseMove(object sender, MouseEventArgs e) { + // 气泡测量:ROI 拖拽画新矩形 + if (_bubbleRoiDragging && _bubbleRoiStart.HasValue && _bubbleRoiRect != null) + { + var pos = e.GetPosition(mainCanvas); + double x = Math.Min(_bubbleRoiStart.Value.X, pos.X); + double y = Math.Min(_bubbleRoiStart.Value.Y, pos.Y); + double w = Math.Abs(pos.X - _bubbleRoiStart.Value.X); + double h = Math.Abs(pos.Y - _bubbleRoiStart.Value.Y); + Canvas.SetLeft(_bubbleRoiRect, x); + Canvas.SetTop(_bubbleRoiRect, y); + _bubbleRoiRect.Width = w; + _bubbleRoiRect.Height = h; + _bubbleRoiRect.Visibility = Visibility.Visible; + if (_bubbleRoiHandle != null) _bubbleRoiHandle.Visibility = Visibility.Collapsed; + e.Handled = true; + return; + } + // 气泡测量:拖动 ROI + if (_bubbleRoiMoving && _bubbleRoi.HasValue) + { + var pos = e.GetPosition(mainCanvas); + double nx = pos.X - _bubbleRoiDragOffset.X; + double ny = pos.Y - _bubbleRoiDragOffset.Y; + _bubbleRoi = new Rect(nx, ny, _bubbleRoi.Value.Width, _bubbleRoi.Value.Height); + SyncBubbleRoiVisuals(); + e.Handled = true; + return; + } + // 气泡测量:右下角调整大小 + if (_bubbleRoiResizing && _bubbleRoi.HasValue) + { + var pos = e.GetPosition(mainCanvas); + double w = Math.Max(20, pos.X - _bubbleRoi.Value.X); + double h = Math.Max(20, pos.Y - _bubbleRoi.Value.Y); + _bubbleRoi = new Rect(_bubbleRoi.Value.X, _bubbleRoi.Value.Y, w, h); + SyncBubbleRoiVisuals(); + e.Handled = true; + return; + } + // 气泡测量:画笔/橡皮拖拽 + if (_bubbleBrushDragging && _bubbleRoi.HasValue) + { + var pos = e.GetPosition(mainCanvas); + ApplyBrushAt(pos); + e.Handled = true; + return; + } + if (e.LeftButton == MouseButtonState.Pressed && mainCanvas.IsMouseCaptured) { Point currentPosition = e.GetPosition(imageDisplayGrid); @@ -1378,6 +1619,49 @@ namespace XP.ImageProcessing.RoiControl.Controls private void Canvas_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) { + // 气泡测量:ROI 拖拽完成 + if (_bubbleRoiDragging && _bubbleRoiStart.HasValue) + { + var pos = e.GetPosition(mainCanvas); + double x = Math.Min(_bubbleRoiStart.Value.X, pos.X); + double y = Math.Min(_bubbleRoiStart.Value.Y, pos.Y); + double w = Math.Abs(pos.X - _bubbleRoiStart.Value.X); + double h = Math.Abs(pos.Y - _bubbleRoiStart.Value.Y); + _bubbleRoiDragging = false; + _bubbleRoiStart = null; + mainCanvas.ReleaseMouseCapture(); + + if (w > 5 && h > 5) + { + _bubbleRoi = new Rect(x, y, w, h); + SyncBubbleRoiVisuals(); + InitBubbleMask(); + RaiseMeasureStatusChanged($"ROI 已设置: {w:F0}×{h:F0},可拖动/调整大小,或在面板切换魔棒工具"); + } + e.Handled = true; + return; + } + // 气泡测量:拖动/调整完成 + if (_bubbleRoiMoving || _bubbleRoiResizing) + { + _bubbleRoiMoving = false; + _bubbleRoiResizing = false; + mainCanvas.ReleaseMouseCapture(); + if (_bubbleRoi.HasValue) + RaiseMeasureStatusChanged($"ROI: {_bubbleRoi.Value.Width:F0}×{_bubbleRoi.Value.Height:F0}"); + e.Handled = true; + return; + } + // 气泡测量:画笔/橡皮松开 + if (_bubbleBrushDragging) + { + _bubbleBrushDragging = false; + mainCanvas.ReleaseMouseCapture(); + UpdateBubbleResult(); + e.Handled = true; + return; + } + mainCanvas.ReleaseMouseCapture(); if (!isDragging) diff --git a/XP.ImageProcessing.RoiControl/Models/MeasureMode.cs b/XP.ImageProcessing.RoiControl/Models/MeasureMode.cs index 700cf53..039b8d1 100644 --- a/XP.ImageProcessing.RoiControl/Models/MeasureMode.cs +++ b/XP.ImageProcessing.RoiControl/Models/MeasureMode.cs @@ -7,6 +7,7 @@ namespace XP.ImageProcessing.RoiControl.Models PointToLine, Angle, FillRate, - BgaVoid + BgaVoid, + BubbleMeasure } } diff --git a/XplorePlane/Events/MeasurementToolEvent.cs b/XplorePlane/Events/MeasurementToolEvent.cs index 279af4a..daa45a2 100644 --- a/XplorePlane/Events/MeasurementToolEvent.cs +++ b/XplorePlane/Events/MeasurementToolEvent.cs @@ -12,7 +12,8 @@ namespace XplorePlane.Events PointLineDistance, Angle, ThroughHoleFillRate, - BgaVoid + BgaVoid, + BubbleMeasure } /// diff --git a/XplorePlane/ViewModels/Main/MainViewModel.cs b/XplorePlane/ViewModels/Main/MainViewModel.cs index fe23b8f..d6e784e 100644 --- a/XplorePlane/ViewModels/Main/MainViewModel.cs +++ b/XplorePlane/ViewModels/Main/MainViewModel.cs @@ -89,6 +89,7 @@ namespace XplorePlane.ViewModels public DelegateCommand AngleMeasureCommand { get; } public DelegateCommand ThroughHoleFillRateMeasureCommand { get; } public DelegateCommand BgaVoidMeasureCommand { get; } + public DelegateCommand BubbleMeasureCommand { get; } // 辅助线命令 public DelegateCommand ToggleCrosshairCommand { get; } @@ -176,6 +177,7 @@ namespace XplorePlane.ViewModels AngleMeasureCommand = new DelegateCommand(ExecuteAngleMeasure); ThroughHoleFillRateMeasureCommand = new DelegateCommand(ExecuteThroughHoleFillRateMeasure); BgaVoidMeasureCommand = new DelegateCommand(ExecuteBgaVoidMeasure); + BubbleMeasureCommand = new DelegateCommand(ExecuteBubbleMeasure); // 辅助线命令 ToggleCrosshairCommand = new DelegateCommand(() => @@ -497,6 +499,35 @@ namespace XplorePlane.ViewModels _eventAggregator.GetEvent().Publish(MeasurementToolMode.BgaVoid); } + private Window _bubbleMeasurePanel; + + private void ExecuteBubbleMeasure() + { + if (!CheckImageLoaded()) return; + _logger.Info("气泡测量功能已触发"); + + // 进入气泡测量模式 + _eventAggregator.GetEvent().Publish(MeasurementToolMode.BubbleMeasure); + + // 弹出工具面板 + if (_bubbleMeasurePanel != null && _bubbleMeasurePanel.IsVisible) + { + _bubbleMeasurePanel.Activate(); + return; + } + + _bubbleMeasurePanel = new Views.ImageProcessing.BubbleMeasurePanel + { + Owner = System.Windows.Application.Current.MainWindow + }; + _bubbleMeasurePanel.Closed += (s, e) => + { + // 关闭面板时退出气泡测量模式 + _eventAggregator.GetEvent().Publish(MeasurementToolMode.None); + }; + _bubbleMeasurePanel.Show(); + } + #endregion #region 设置命令实现 diff --git a/XplorePlane/Views/ImageProcessing/BubbleMeasurePanel.xaml b/XplorePlane/Views/ImageProcessing/BubbleMeasurePanel.xaml new file mode 100644 index 0000000..bcd5849 --- /dev/null +++ b/XplorePlane/Views/ImageProcessing/BubbleMeasurePanel.xaml @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +