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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/XplorePlane/Views/ImageProcessing/BubbleMeasurePanel.xaml.cs b/XplorePlane/Views/ImageProcessing/BubbleMeasurePanel.xaml.cs
new file mode 100644
index 0000000..369fd41
--- /dev/null
+++ b/XplorePlane/Views/ImageProcessing/BubbleMeasurePanel.xaml.cs
@@ -0,0 +1,85 @@
+using System.Windows;
+using Prism.Ioc;
+using XP.ImageProcessing.RoiControl.Controls;
+using XplorePlane.ViewModels;
+
+namespace XplorePlane.Views.ImageProcessing
+{
+ public partial class BubbleMeasurePanel : Window
+ {
+ private PolygonRoiCanvas _canvas;
+
+ public BubbleMeasurePanel()
+ {
+ InitializeComponent();
+ Loaded += OnLoaded;
+ }
+
+ private void OnLoaded(object sender, RoutedEventArgs e)
+ {
+ // 获取主界面的 RoiCanvas
+ try
+ {
+ var mainWin = Owner as MainWindow;
+ if (mainWin != null)
+ {
+ _canvas = FindChild(mainWin);
+ }
+ }
+ catch { }
+
+ // 工具切换
+ RbRoi.Checked += (s, ev) => _canvas?.SetBubbleTool(PolygonRoiCanvas.BubbleSubTool.Roi);
+ RbWand.Checked += (s, ev) => _canvas?.SetBubbleTool(PolygonRoiCanvas.BubbleSubTool.Wand);
+ RbBrush.Checked += (s, ev) => _canvas?.SetBubbleTool(PolygonRoiCanvas.BubbleSubTool.Brush);
+ RbEraser.Checked += (s, ev) => _canvas?.SetBubbleTool(PolygonRoiCanvas.BubbleSubTool.Eraser);
+
+ // 阈值同步
+ SliderThreshold.ValueChanged += (s, ev) =>
+ {
+ TbThreshold.Text = ((int)SliderThreshold.Value).ToString();
+ _canvas?.SetBubbleThreshold((int)SliderThreshold.Value);
+ };
+
+ // 画笔大小同步
+ SliderBrushSize.ValueChanged += (s, ev) =>
+ {
+ TbBrushSize.Text = ((int)SliderBrushSize.Value).ToString();
+ _canvas?.SetBubbleBrushSize((int)SliderBrushSize.Value);
+ };
+
+ // 监听 canvas 的工具切换事件(ROI 画完后自动切换时同步面板)
+ if (_canvas != null)
+ {
+ _canvas.BubbleToolChanged += (s, ev) =>
+ {
+ // 暂不自动切换面板 radio button
+ };
+ }
+ }
+
+ private void ClearMask_Click(object sender, RoutedEventArgs e)
+ {
+ _canvas?.ClearBubbleMeasure();
+ TbResult.Text = "空隙率: --";
+ }
+
+ private void Finish_Click(object sender, RoutedEventArgs e)
+ {
+ Close();
+ }
+
+ private static T FindChild(DependencyObject parent) where T : DependencyObject
+ {
+ int count = System.Windows.Media.VisualTreeHelper.GetChildrenCount(parent);
+ for (int i = 0; i < count; i++)
+ {
+ var child = System.Windows.Media.VisualTreeHelper.GetChild(parent, i);
+ if (child is T t) return t;
+ var result = FindChild(child);
+ if (result != null) return result;
+ }
+ return null;
+ }
+ }
+}
diff --git a/XplorePlane/Views/Main/ViewportPanelView.xaml.cs b/XplorePlane/Views/Main/ViewportPanelView.xaml.cs
index ccda326..9e83eb1 100644
--- a/XplorePlane/Views/Main/ViewportPanelView.xaml.cs
+++ b/XplorePlane/Views/Main/ViewportPanelView.xaml.cs
@@ -79,6 +79,7 @@ namespace XplorePlane.Views
MeasurementToolMode.Angle => XP.ImageProcessing.RoiControl.Models.MeasureMode.Angle,
MeasurementToolMode.ThroughHoleFillRate => XP.ImageProcessing.RoiControl.Models.MeasureMode.FillRate,
MeasurementToolMode.BgaVoid => XP.ImageProcessing.RoiControl.Models.MeasureMode.BgaVoid,
+ MeasurementToolMode.BubbleMeasure => XP.ImageProcessing.RoiControl.Models.MeasureMode.BubbleMeasure,
_ => XP.ImageProcessing.RoiControl.Models.MeasureMode.None
};
}, Prism.Events.ThreadOption.UIThread);