From 0b560105363e054f3a299435a9d19697094cc81c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E4=BC=9F?= Date: Thu, 30 Apr 2026 09:22:50 +0800 Subject: [PATCH 01/10] =?UTF-8?q?=E6=9B=B4=E6=94=B9=E6=B0=94=E6=B3=A1?= =?UTF-8?q?=E7=AD=9B=E9=80=89=E9=80=BB=E8=BE=91=EF=BC=9A=E6=8C=89=E7=85=A7?= =?UTF-8?q?=E9=9D=A2=E7=A7=AF=E7=AD=9B=E9=80=89=EF=BC=8C=E5=8E=BB=E9=99=A4?= =?UTF-8?q?=E5=B0=8F=E4=BA=8E=E8=AE=BE=E5=AE=9A=E9=9D=A2=E7=A7=AF=E7=9A=84?= =?UTF-8?q?=E6=B0=94=E6=B3=A1=E9=87=8D=E6=96=B0=E8=AE=A1=E7=AE=97=E7=A9=BA?= =?UTF-8?q?=E9=9A=99=E7=8E=87=E6=9B=B4=E6=96=B0=E7=BB=93=E6=9E=9C=E5=9B=BE?= =?UTF-8?q?=E5=83=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../检测分析/BgaVoidRateProcessor.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/XP.ImageProcessing.Processors/检测分析/BgaVoidRateProcessor.cs b/XP.ImageProcessing.Processors/检测分析/BgaVoidRateProcessor.cs index d78fae9..450d0f1 100644 --- a/XP.ImageProcessing.Processors/检测分析/BgaVoidRateProcessor.cs +++ b/XP.ImageProcessing.Processors/检测分析/BgaVoidRateProcessor.cs @@ -333,14 +333,13 @@ public class BgaVoidRateProcessor : ImageProcessorBase } int voidPixels = CvInvoke.CountNonZero(voidImg); - bga.VoidPixels = voidPixels; - bga.VoidRate = bgaPixels > 0 ? (double)voidPixels / bgaPixels * 100.0 : 0; // 检测每个气泡的轮廓 using var contours = new VectorOfVectorOfPoint(); using var hierarchy = new Mat(); CvInvoke.FindContours(voidImg, contours, hierarchy, RetrType.External, ChainApproxMethod.ChainApproxSimple); + int filteredVoidArea = 0; for (int i = 0; i < contours.Size; i++) { double area = CvInvoke.ContourArea(contours[i]); @@ -349,6 +348,7 @@ public class BgaVoidRateProcessor : ImageProcessorBase var moments = CvInvoke.Moments(contours[i]); if (moments.M00 < 1) continue; + filteredVoidArea += (int)Math.Round(area); bga.Voids.Add(new VoidInfo { Index = bga.Voids.Count + 1, @@ -361,6 +361,10 @@ public class BgaVoidRateProcessor : ImageProcessorBase }); } + // 空隙率基于过滤后的轮廓面积计算 + bga.VoidPixels = filteredVoidArea; + bga.VoidRate = bgaPixels > 0 ? (double)filteredVoidArea / bgaPixels * 100.0 : 0; + // 按面积从大到小排序 bga.Voids.Sort((a, b) => b.Area.CompareTo(a.Area)); for (int i = 0; i < bga.Voids.Count; i++) bga.Voids[i].Index = i + 1; From 64c22fc0883dcf7dbe69279013e215426f153555 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E4=BC=9F?= Date: Thu, 30 Apr 2026 09:25:08 +0800 Subject: [PATCH 02/10] =?UTF-8?q?=E5=8A=A0=E8=BD=BD=E6=96=B0=E5=9B=BE?= =?UTF-8?q?=E5=83=8F=E6=97=B6=E4=BC=9A=E8=87=AA=E5=8A=A8=E6=B8=85=E9=99=A4?= =?UTF-8?q?=E6=97=A7=E7=9A=84=E6=B5=8B=E9=87=8F=E7=BB=93=E6=9E=9C=E3=80=81?= =?UTF-8?q?=E5=8F=A0=E5=8A=A0=E5=B1=82=E5=92=8CROI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Controls/PolygonRoiCanvas.xaml.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/XP.ImageProcessing.RoiControl/Controls/PolygonRoiCanvas.xaml.cs b/XP.ImageProcessing.RoiControl/Controls/PolygonRoiCanvas.xaml.cs index 8c1d4b3..67231f7 100644 --- a/XP.ImageProcessing.RoiControl/Controls/PolygonRoiCanvas.xaml.cs +++ b/XP.ImageProcessing.RoiControl/Controls/PolygonRoiCanvas.xaml.cs @@ -123,7 +123,6 @@ namespace XP.ImageProcessing.RoiControl.Controls var control = (PolygonRoiCanvas)d; if (e.NewValue is BitmapSource bitmap) { - // 使用像素尺寸,避免 DPI 不同导致 DIP 尺寸与实际像素不一致 control.CanvasWidth = bitmap.PixelWidth; control.CanvasHeight = bitmap.PixelHeight; control.ResetView(); @@ -135,6 +134,11 @@ namespace XP.ImageProcessing.RoiControl.Controls control.ResetView(); } + // 图像切换时清除测量、叠加层和ROI + control.ClearMeasurements(); + control.ROIItems?.Clear(); + control.SelectedROI = null; + // 图像尺寸变化后刷新十字线 if (control.ShowCrosshair) control.AddCrosshair(); From 2663bda0ae86421e03dad567fa2edf2d87499e7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E4=BC=9F?= Date: Thu, 30 Apr 2026 13:46:07 +0800 Subject: [PATCH 03/10] =?UTF-8?q?=E5=88=A0=E9=99=A4BGA=E6=89=8B=E5=8A=A8?= =?UTF-8?q?=E6=B5=8B=E9=87=8F=E5=B7=A5=E5=85=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- XplorePlane/ViewModels/Main/MainViewModel.cs | 27 ------ .../ImageProcessing/BgaMeasurePanel.xaml | 96 ------------------- .../ImageProcessing/BgaMeasurePanel.xaml.cs | 74 -------------- XplorePlane/Views/Main/MainWindow.xaml | 9 +- 4 files changed, 1 insertion(+), 205 deletions(-) delete mode 100644 XplorePlane/Views/ImageProcessing/BgaMeasurePanel.xaml delete mode 100644 XplorePlane/Views/ImageProcessing/BgaMeasurePanel.xaml.cs diff --git a/XplorePlane/ViewModels/Main/MainViewModel.cs b/XplorePlane/ViewModels/Main/MainViewModel.cs index 9da4c12..2041f83 100644 --- a/XplorePlane/ViewModels/Main/MainViewModel.cs +++ b/XplorePlane/ViewModels/Main/MainViewModel.cs @@ -92,7 +92,6 @@ namespace XplorePlane.ViewModels public DelegateCommand PointLineDistanceMeasureCommand { get; } public DelegateCommand AngleMeasureCommand { get; } public DelegateCommand ThroughHoleFillRateMeasureCommand { get; } - public DelegateCommand BgaVoidMeasureCommand { get; } public DelegateCommand BgaDetectionCommand { get; } public DelegateCommand VoidDetectionCommand { get; } public DelegateCommand BubbleMeasureCommand { get; } @@ -236,7 +235,6 @@ namespace XplorePlane.ViewModels PointLineDistanceMeasureCommand = new DelegateCommand(ExecutePointLineDistanceMeasure); AngleMeasureCommand = new DelegateCommand(ExecuteAngleMeasure); ThroughHoleFillRateMeasureCommand = new DelegateCommand(ExecuteThroughHoleFillRateMeasure); - BgaVoidMeasureCommand = new DelegateCommand(ExecuteBgaVoidMeasure); BgaDetectionCommand = new DelegateCommand(ExecuteBgaDetection); VoidDetectionCommand = new DelegateCommand(ExecuteVoidDetection); BubbleMeasureCommand = new DelegateCommand(ExecuteBubbleMeasure); @@ -554,31 +552,6 @@ namespace XplorePlane.ViewModels _eventAggregator.GetEvent().Publish(MeasurementToolMode.ThroughHoleFillRate); } - private Window _bgaMeasurePanel; - - private void ExecuteBgaVoidMeasure() - { - if (!CheckImageLoaded()) return; - _logger.Info("BGA空隙测量功能已触发"); - _eventAggregator.GetEvent().Publish(MeasurementToolMode.BgaVoid); - - if (_bgaMeasurePanel != null && _bgaMeasurePanel.IsVisible) - { - _bgaMeasurePanel.Activate(); - return; - } - - _bgaMeasurePanel = new Views.ImageProcessing.BgaMeasurePanel - { - Owner = System.Windows.Application.Current.MainWindow - }; - _bgaMeasurePanel.Closed += (s, e) => - { - _eventAggregator.GetEvent().Publish(MeasurementToolMode.None); - }; - _bgaMeasurePanel.Show(); - } - private Window _bgaDetectionPanel; private void ExecuteBgaDetection() diff --git a/XplorePlane/Views/ImageProcessing/BgaMeasurePanel.xaml b/XplorePlane/Views/ImageProcessing/BgaMeasurePanel.xaml deleted file mode 100644 index 87aad82..0000000 --- a/XplorePlane/Views/ImageProcessing/BgaMeasurePanel.xaml +++ /dev/null @@ -1,96 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/XplorePlane/Views/ImageProcessing/BgaMeasurePanel.xaml.cs b/XplorePlane/Views/ImageProcessing/BgaMeasurePanel.xaml.cs deleted file mode 100644 index ee34e2f..0000000 --- a/XplorePlane/Views/ImageProcessing/BgaMeasurePanel.xaml.cs +++ /dev/null @@ -1,74 +0,0 @@ -using System.Windows; -using XP.ImageProcessing.RoiControl.Controls; - -namespace XplorePlane.Views.ImageProcessing -{ - public partial class BgaMeasurePanel : Window - { - private PolygonRoiCanvas _canvas; - - public BgaMeasurePanel() - { - InitializeComponent(); - Loaded += OnLoaded; - } - - private void OnLoaded(object sender, RoutedEventArgs e) - { - try - { - var mainWin = Owner as MainWindow; - if (mainWin != null) - _canvas = FindChild(mainWin); - } - catch { } - - // 模式切换:通知 canvas 切换气泡/焊球 - RbVoid.Checked += (s, ev) => - { - if (_canvas != null) _canvas.SetBgaDrawBall(false); - }; - RbBall.Checked += (s, ev) => - { - if (_canvas != null) _canvas.SetBgaDrawBall(true); - }; - - // VoidLimit 同步 - SliderVoidLimit.ValueChanged += (s, ev) => - { - TbVoidLimit.Text = SliderVoidLimit.Value.ToString("F1"); - _canvas?.SetBgaVoidLimit(SliderVoidLimit.Value); - }; - - // 监听测量完成事件更新结果 - if (_canvas != null) - { - _canvas.MeasureCompleted += (s, ev) => - { - if (ev is MeasureCompletedEventArgs args && args.MeasureType == "BgaVoid") - { - TbResult.Text = $"空隙率: {args.Distance:F1}%"; - } - }; - } - } - - 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/MainWindow.xaml b/XplorePlane/Views/Main/MainWindow.xaml index a1995f5..a460208 100644 --- a/XplorePlane/Views/Main/MainWindow.xaml +++ b/XplorePlane/Views/Main/MainWindow.xaml @@ -236,15 +236,8 @@ Text="通孔填锡率" /> - + - Date: Thu, 30 Apr 2026 14:01:30 +0800 Subject: [PATCH 04/10] =?UTF-8?q?=E6=89=8B=E5=8A=A8=E6=B5=8B=E9=87=8F?= =?UTF-8?q?=E5=B7=A5=E5=85=B7=EF=BC=9A=E6=8E=A7=E5=88=B6=E7=82=B9=E6=94=B9?= =?UTF-8?q?=E4=B8=BA2*2=E3=80=81=E7=BA=BF=E5=AE=BD=E6=94=B9=E4=B8=BA1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Controls/PolygonRoiCanvas.xaml.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/XP.ImageProcessing.RoiControl/Controls/PolygonRoiCanvas.xaml.cs b/XP.ImageProcessing.RoiControl/Controls/PolygonRoiCanvas.xaml.cs index 67231f7..929433d 100644 --- a/XP.ImageProcessing.RoiControl/Controls/PolygonRoiCanvas.xaml.cs +++ b/XP.ImageProcessing.RoiControl/Controls/PolygonRoiCanvas.xaml.cs @@ -549,7 +549,7 @@ namespace XP.ImageProcessing.RoiControl.Controls private Models.MeasureGroup CreatePPGroup(Point p1, Point p2) { var g = new Models.MeasureGroup { P1 = p1, P2 = p2 }; - g.Line = new Line { Stroke = Brushes.Lime, StrokeThickness = 2, IsHitTestVisible = false }; + g.Line = new Line { Stroke = Brushes.Lime, StrokeThickness = 1, IsHitTestVisible = false }; g.Label = new TextBlock { Foreground = Brushes.Yellow, FontSize = 13, FontWeight = FontWeights.Bold, IsHitTestVisible = false }; g.Dot1 = CreateMDot(Brushes.Red); g.Dot2 = CreateMDot(Brushes.Blue); @@ -580,7 +580,7 @@ namespace XP.ImageProcessing.RoiControl.Controls _ptlTempDot2 = CreateMDot(Brushes.Lime); _measureOverlay.Children.Add(_ptlTempDot2); SetDotPos(_ptlTempDot2, pos); - _ptlTempLine = new Line { Stroke = Brushes.Lime, StrokeThickness = 2, IsHitTestVisible = false, + _ptlTempLine = new Line { Stroke = Brushes.Lime, StrokeThickness = 1, IsHitTestVisible = false, X1 = _ptlTempL1.Value.X, Y1 = _ptlTempL1.Value.Y, X2 = pos.X, Y2 = pos.Y }; _measureOverlay.Children.Add(_ptlTempLine); RaiseMeasureStatusChanged($"点线距 - 直线已定义,请点击测量点"); @@ -606,12 +606,12 @@ namespace XP.ImageProcessing.RoiControl.Controls private Models.PointToLineGroup CreatePTLGroup(Point l1, Point l2, Point p) { var g = new Models.PointToLineGroup { L1 = l1, L2 = l2, P = p }; - g.MainLine = new Line { Stroke = Brushes.Lime, StrokeThickness = 2, IsHitTestVisible = false }; + g.MainLine = new Line { Stroke = Brushes.Lime, StrokeThickness = 1, IsHitTestVisible = false }; g.ExtLine = new Line { Stroke = Brushes.Lime, StrokeThickness = 1, IsHitTestVisible = false, StrokeDashArray = new DoubleCollection { 4, 2 }, Visibility = Visibility.Collapsed }; g.PerpLine = new Line { Stroke = Brushes.Yellow, StrokeThickness = 1, IsHitTestVisible = false, StrokeDashArray = new DoubleCollection { 4, 2 } }; - g.FootDot = new Ellipse { Width = 8, Height = 8, Fill = Brushes.Cyan, Stroke = Brushes.White, StrokeThickness = 1, IsHitTestVisible = false }; + g.FootDot = new Ellipse { Width = 2, Height = 2, Fill = Brushes.Cyan, Stroke = Brushes.White, StrokeThickness = 0.5, IsHitTestVisible = false }; g.Label = new TextBlock { Foreground = Brushes.Yellow, FontSize = 13, FontWeight = FontWeights.Bold, IsHitTestVisible = false }; g.DotL1 = CreateMDot(Brushes.Lime); g.DotL2 = CreateMDot(Brushes.Lime); @@ -713,7 +713,7 @@ namespace XP.ImageProcessing.RoiControl.Controls g.PathE4 = CreateEllipsePath(Brushes.Lime, false); g.FullLine = new Line { Stroke = Brushes.Red, StrokeThickness = 1, IsHitTestVisible = false }; - g.FillLine = new Line { Stroke = Brushes.Lime, StrokeThickness = 2, IsHitTestVisible = false }; + g.FillLine = new Line { Stroke = Brushes.Lime, StrokeThickness = 1, IsHitTestVisible = false }; g.Label = new TextBlock { FontSize = 13, FontWeight = FontWeights.Bold, Cursor = Cursors.Hand }; g.Label.SetValue(ContextMenuService.IsEnabledProperty, false); @@ -741,7 +741,7 @@ namespace XP.ImageProcessing.RoiControl.Controls private Ellipse CreateAxisHandle(Brush fill) { - var h = new Ellipse { Width = 8, Height = 8, Fill = fill, Stroke = Brushes.White, StrokeThickness = 1, Cursor = Cursors.SizeAll }; + var h = new Ellipse { Width = 2, Height = 2, Fill = fill, Stroke = Brushes.White, StrokeThickness = 0.5, Cursor = Cursors.SizeAll }; h.SetValue(ContextMenuService.IsEnabledProperty, false); h.MouseLeftButtonDown += MDot_Down; h.MouseMove += MDot_Move; @@ -842,7 +842,7 @@ namespace XP.ImageProcessing.RoiControl.Controls }; c.CenterDot = CreateMDot(isBall ? Brushes.Lime : Brushes.Orange); c.EdgeDot = CreateMDot(isBall ? Brushes.Lime : Brushes.Orange); - c.EdgeDot.Width = 8; c.EdgeDot.Height = 8; + c.EdgeDot.Width = 2; c.EdgeDot.Height = 2; c.EdgeDot.Cursor = System.Windows.Input.Cursors.SizeAll; _measureOverlay.Children.Add(c.Shape); @@ -906,7 +906,7 @@ namespace XP.ImageProcessing.RoiControl.Controls private Ellipse CreateMDot(Brush fill) { - var dot = new Ellipse { Width = 12, Height = 12, Fill = fill, Stroke = Brushes.White, StrokeThickness = 1.5, Cursor = Cursors.Hand }; + var dot = new Ellipse { Width = 2, Height = 2, Fill = fill, Stroke = Brushes.White, StrokeThickness = 0.5, Cursor = Cursors.Hand }; dot.SetValue(ContextMenuService.IsEnabledProperty, false); dot.MouseLeftButtonDown += MDot_Down; dot.MouseMove += MDot_Move; From 6b35da4cc0072508c5406bd6f66f2ef3a6481c29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E4=BC=9F?= Date: Thu, 30 Apr 2026 15:51:29 +0800 Subject: [PATCH 05/10] =?UTF-8?q?=E7=8A=B6=E6=80=81=E6=A0=8F=E5=8F=B3?= =?UTF-8?q?=E4=B8=8B=E8=A7=92=E6=98=BE=E7=A4=BA=E5=9B=BE=E5=83=8F=E5=83=8F?= =?UTF-8?q?=E7=B4=A0=E5=9D=90=E6=A0=87=E5=92=8C=E7=81=B0=E5=BA=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Controls/PolygonRoiCanvas.xaml | 1 + .../Controls/PolygonRoiCanvas.xaml.cs | 48 +++++++++++++++++++ XplorePlane/ViewModels/Main/MainViewModel.cs | 7 +++ XplorePlane/Views/Main/MainWindow.xaml | 2 +- .../Views/Main/ViewportPanelView.xaml.cs | 19 +++++++- 5 files changed, 74 insertions(+), 3 deletions(-) diff --git a/XP.ImageProcessing.RoiControl/Controls/PolygonRoiCanvas.xaml b/XP.ImageProcessing.RoiControl/Controls/PolygonRoiCanvas.xaml index 0546e65..bc732a8 100644 --- a/XP.ImageProcessing.RoiControl/Controls/PolygonRoiCanvas.xaml +++ b/XP.ImageProcessing.RoiControl/Controls/PolygonRoiCanvas.xaml @@ -41,6 +41,7 @@ MouseLeftButtonDown="Canvas_MouseLeftButtonDown" MouseLeftButtonUp="Canvas_MouseLeftButtonUp" MouseMove="Canvas_MouseMove" + MouseLeave="Canvas_MouseLeave" MouseRightButtonDown="Canvas_MouseRightButtonDown" PreviewMouseRightButtonUp="Canvas_PreviewMouseRightButtonUp"> diff --git a/XP.ImageProcessing.RoiControl/Controls/PolygonRoiCanvas.xaml.cs b/XP.ImageProcessing.RoiControl/Controls/PolygonRoiCanvas.xaml.cs index 929433d..85d81fb 100644 --- a/XP.ImageProcessing.RoiControl/Controls/PolygonRoiCanvas.xaml.cs +++ b/XP.ImageProcessing.RoiControl/Controls/PolygonRoiCanvas.xaml.cs @@ -299,6 +299,19 @@ namespace XP.ImageProcessing.RoiControl.Controls set => SetValue(ShowCrosshairProperty, value); } + // ── 光标信息(像素坐标 + 灰度值)── + + public static readonly DependencyProperty CursorInfoProperty = + DependencyProperty.Register(nameof(CursorInfo), typeof(string), typeof(PolygonRoiCanvas), + new PropertyMetadata("X: -- Y: -- Gray: --")); + + /// 鼠标在图像上的像素坐标和灰度值,格式: "X: 123 Y: 456 Gray: 128" + public string CursorInfo + { + get => (string)GetValue(CursorInfoProperty); + set => SetValue(CursorInfoProperty, value); + } + private Line _crosshairH, _crosshairV; private static void OnShowCrosshairChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) @@ -1739,6 +1752,38 @@ namespace XP.ImageProcessing.RoiControl.Controls #region Mouse Events + private void UpdateCursorInfo(Point pos) + { + if (ImageSource == null) + { + CursorInfo = "X: -- Y: -- Gray: --"; + return; + } + + int px = (int)pos.X, py = (int)pos.Y; + string grayText = "--"; + + if (ImageSource is BitmapSource bmp && px >= 0 && py >= 0 && px < bmp.PixelWidth && py < bmp.PixelHeight) + { + try + { + var pixel = new byte[4]; + var converted = new FormatConvertedBitmap(bmp, PixelFormats.Bgra32, null, 0); + converted.CopyPixels(new System.Windows.Int32Rect(px, py, 1, 1), pixel, 4, 0); + int gray = (int)(pixel[2] * 0.299 + pixel[1] * 0.587 + pixel[0] * 0.114); + grayText = gray.ToString(); + } + catch { } + } + + CursorInfo = $"X: {px} Y: {py} Gray: {grayText}"; + } + + private void Canvas_MouseLeave(object sender, MouseEventArgs e) + { + CursorInfo = "X: -- Y: -- Gray: --"; + } + private void Canvas_MouseWheel(object sender, MouseWheelEventArgs e) { double oldZoom = ZoomScale; @@ -1881,6 +1926,9 @@ namespace XP.ImageProcessing.RoiControl.Controls lastMousePosition = currentPosition; } } + + // 更新光标信息(像素坐标 + 灰度值) + UpdateCursorInfo(e.GetPosition(mainCanvas)); } private void Canvas_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) diff --git a/XplorePlane/ViewModels/Main/MainViewModel.cs b/XplorePlane/ViewModels/Main/MainViewModel.cs index 2041f83..20b36e0 100644 --- a/XplorePlane/ViewModels/Main/MainViewModel.cs +++ b/XplorePlane/ViewModels/Main/MainViewModel.cs @@ -265,6 +265,13 @@ namespace XplorePlane.ViewModels public string CncStatusMessage => _cncEditorViewModel.StatusMessage; public bool CncHasExecutionError => _cncEditorViewModel.HasExecutionError; + private string _cursorInfoText = "X: -- Y: -- Gray: --"; + public string CursorInfoText + { + get => _cursorInfoText; + set => SetProperty(ref _cursorInfoText, value); + } + private void ShowWindow(Window window, string name) { window.Owner = Application.Current.MainWindow; diff --git a/XplorePlane/Views/Main/MainWindow.xaml b/XplorePlane/Views/Main/MainWindow.xaml index a460208..cfee2e7 100644 --- a/XplorePlane/Views/Main/MainWindow.xaml +++ b/XplorePlane/Views/Main/MainWindow.xaml @@ -580,7 +580,7 @@ FontFamily="Consolas" FontSize="11" Foreground="White" - Text="x: 0 y: 0" /> + Text="{Binding CursorInfoText}" /> diff --git a/XplorePlane/Views/Main/ViewportPanelView.xaml.cs b/XplorePlane/Views/Main/ViewportPanelView.xaml.cs index c8ced63..87b3598 100644 --- a/XplorePlane/Views/Main/ViewportPanelView.xaml.cs +++ b/XplorePlane/Views/Main/ViewportPanelView.xaml.cs @@ -17,13 +17,19 @@ namespace XplorePlane.Views { private MainViewModel _mainVm; - private void SetStatus(string msg) + private MainViewModel GetMainVm() { if (_mainVm == null) { try { _mainVm = ContainerLocator.Current?.Resolve(); } catch { } } - if (_mainVm != null) _mainVm.StatusMessage = msg; + return _mainVm; + } + + private void SetStatus(string msg) + { + var vm = GetMainVm(); + if (vm != null) vm.StatusMessage = msg; } public ViewportPanelView() @@ -113,6 +119,15 @@ namespace XplorePlane.Views }, Prism.Events.ThreadOption.UIThread); } catch { } + + // 光标信息:从 RoiCanvas.CursorInfo 同步到 MainViewModel + var cursorInfoDesc = System.ComponentModel.DependencyPropertyDescriptor.FromProperty( + PolygonRoiCanvas.CursorInfoProperty, typeof(PolygonRoiCanvas)); + cursorInfoDesc?.AddValueChanged(RoiCanvas, (s, e) => + { + var vm = GetMainVm(); + if (vm != null) vm.CursorInfoText = RoiCanvas.CursorInfo; + }); } private void OnDataContextChanged(object sender, DependencyPropertyChangedEventArgs e) From 611b2ae1470e7489afd705263752e4ba0c5818b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E4=BC=9F?= Date: Thu, 30 Apr 2026 16:51:07 +0800 Subject: [PATCH 06/10] =?UTF-8?q?=E5=9C=86=E5=BD=A2ROI=E5=9B=BE=E6=A0=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- XplorePlane/Assets/Icons/circle32.png | Bin 0 -> 845 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 XplorePlane/Assets/Icons/circle32.png diff --git a/XplorePlane/Assets/Icons/circle32.png b/XplorePlane/Assets/Icons/circle32.png new file mode 100644 index 0000000000000000000000000000000000000000..88c1158d67a7099e3579ecae35a4cb920489923d GIT binary patch literal 845 zcmV-T1G4;yP)3BTetGrTTs;4r1vyUXTQ3eD&gQVZ@?**R7W}QxFs4}9& zREHR!iJMNRuj;FP-WA{?PKA;}a=l&;szVZ=PN$d6X49d>)E~>gk{#UGC8EKQukxuB zK-4bc8K6Q)w%aYF3lV3#-3~DkB}Sk8^Yi%}GnP3B>GgW)d)as>0CIs4xr!(guo2U! zGbd)@g&^W`xm50rk^q-7BUcnFMMgw*yd^(Ev^0O@NDv{crNdU&?I{UR+Lz8v;~uCWz^xU#Ir?nm_XLR0ax5gmHm6(OFsvYewZbE{Pel$!-Vo94b{F(X zh|e(1CD#HiIW%wZj)`}uKua&Ad*KV!wm=aO= z&J+ab3&=jOEa*B__-H(%L}!00Vm5c?I09Tj0KHnDIcd^_S51666D|i=C^7nETs|S@>dvSA07ezLFmJCRjeA3cCdZ_YNK_7G z{NPA+V%jewN=$QEsz9u9b8gdM$XEGPW*|R<8xmkFi~1qS5Tv?A>FT^e31b+`9Q<|G zNPq_+8fAnZCNU(ccPKH{S?2{~{P-s2hXgbMydP87{s#a6|NpSrpXUGo00v1!K~w_( XVI#g!9^TP&00000NkvXXu0mjfncIH7 literal 0 HcmV?d00001 From 7846445b33b4a1b84da29fb2725f2e6809230175 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E4=BC=9F?= Date: Wed, 6 May 2026 09:10:35 +0800 Subject: [PATCH 07/10] =?UTF-8?q?=E6=B0=94=E6=B3=A1=E6=B5=8B=E9=87=8F?= =?UTF-8?q?=E5=B7=A5=E5=85=B7=EF=BC=9A=E6=89=93=E5=BC=80=E9=9D=A2=E6=9D=BF?= =?UTF-8?q?=E6=97=B6=E5=85=88=E6=B8=85=E9=99=A4ROIItems=E9=87=8C=E7=9A=84?= =?UTF-8?q?=E6=AE=8B=E7=95=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Controls/PolygonRoiCanvas.xaml.cs | 218 +++++++++++++++--- .../ImageProcessing/BubbleMeasurePanel.xaml | 10 +- .../BubbleMeasurePanel.xaml.cs | 169 ++++++++++++-- 3 files changed, 349 insertions(+), 48 deletions(-) diff --git a/XP.ImageProcessing.RoiControl/Controls/PolygonRoiCanvas.xaml.cs b/XP.ImageProcessing.RoiControl/Controls/PolygonRoiCanvas.xaml.cs index 85d81fb..d4e9b53 100644 --- a/XP.ImageProcessing.RoiControl/Controls/PolygonRoiCanvas.xaml.cs +++ b/XP.ImageProcessing.RoiControl/Controls/PolygonRoiCanvas.xaml.cs @@ -388,7 +388,7 @@ namespace XP.ImageProcessing.RoiControl.Controls private Ellipse _bgaPendingDot; // 气泡测量状态 - public enum BubbleSubTool { Roi, Wand, Brush, Eraser } + public enum BubbleSubTool { Roi, RoiCircle, RoiPolygon, Wand, Brush, Eraser } private BubbleSubTool _bubbleTool = BubbleSubTool.Roi; private Rectangle _bubbleRoiRect; private Ellipse _bubbleRoiHandle; // 右下角调整手柄 @@ -398,6 +398,14 @@ namespace XP.ImageProcessing.RoiControl.Controls private bool _bubbleRoiDragging; private bool _bubbleRoiMoving; // 拖动整个 ROI private bool _bubbleRoiResizing; // 右下角调整大小 + // 圆形 ROI + private Ellipse _bubbleCircleShape; + private Point? _bubbleCircleCenter; + private double _bubbleCircleRadius; + private bool _bubbleCircleDragging; + // 多边形 ROI + private Polygon _bubblePolyShape; + private System.Collections.ObjectModel.ObservableCollection _bubblePolyPoints = new(); private Point _bubbleRoiDragOffset; private Image _bubbleMaskImage; private System.Windows.Media.Imaging.WriteableBitmap _bubbleMask; @@ -411,6 +419,12 @@ namespace XP.ImageProcessing.RoiControl.Controls public void SetBubbleThreshold(int val) => _bubbleThreshold = val; public void SetBubbleBrushSize(int val) => _bubbleBrushSize = val; public void SetBubbleVoidLimit(double val) { _bubbleVoidLimit = val; UpdateBubbleResult(); } + public void SetBubblePolyPoints(System.Collections.Generic.IList points) + { + _bubblePolyPoints.Clear(); + foreach (var p in points) _bubblePolyPoints.Add(p); + if (_bubblePolyPoints.Count >= 3) InitBubbleMask(); + } public Rect? BubbleRoi => _bubbleRoi; /// 设置 BGA 测量的气泡/焊球绘制模式 @@ -1171,6 +1185,64 @@ namespace XP.ImageProcessing.RoiControl.Controls // ── 气泡测量辅助 ── + /// 是否已有任何形状的气泡 ROI + private bool HasBubbleRoi => + _bubbleRoi.HasValue || _bubbleCircleCenter.HasValue || _bubblePolyPoints.Count >= 3; + + /// 判断点是否在当前气泡 ROI 内 + private bool IsInBubbleRoi(Point pos) + { + if (_bubbleRoi.HasValue) + return _bubbleRoi.Value.Contains(pos); + if (_bubbleCircleCenter.HasValue) + { + double dx = pos.X - _bubbleCircleCenter.Value.X; + double dy = pos.Y - _bubbleCircleCenter.Value.Y; + return dx * dx + dy * dy <= _bubbleCircleRadius * _bubbleCircleRadius; + } + if (_bubblePolyPoints.Count >= 3) + return IsPointInPolygon(pos, _bubblePolyPoints); + return false; + } + + private static bool IsPointInPolygon(Point p, System.Collections.Generic.IList polygon) + { + bool inside = false; + int n = polygon.Count; + for (int i = 0, j = n - 1; i < n; j = i++) + { + if ((polygon[i].Y > p.Y) != (polygon[j].Y > p.Y) && + p.X < (polygon[j].X - polygon[i].X) * (p.Y - polygon[i].Y) / (polygon[j].Y - polygon[i].Y) + polygon[i].X) + inside = !inside; + } + return inside; + } + + /// 获取当前 ROI 的外接矩形(用于掩码计算范围) + private Rect GetBubbleRoiBounds() + { + if (_bubbleRoi.HasValue) return _bubbleRoi.Value; + if (_bubbleCircleCenter.HasValue) + { + var c = _bubbleCircleCenter.Value; + var r = _bubbleCircleRadius; + return new Rect(c.X - r, c.Y - r, r * 2, r * 2); + } + if (_bubblePolyPoints.Count >= 3) + { + double minX = double.MaxValue, minY = double.MaxValue, maxX = double.MinValue, maxY = double.MinValue; + foreach (var pt in _bubblePolyPoints) + { + if (pt.X < minX) minX = pt.X; + if (pt.Y < minY) minY = pt.Y; + if (pt.X > maxX) maxX = pt.X; + if (pt.Y > maxY) maxY = pt.Y; + } + return new Rect(minX, minY, maxX - minX, maxY - minY); + } + return Rect.Empty; + } + private void EnsureBubbleRoiVisuals() { EnsureMeasureOverlay(); @@ -1228,9 +1300,43 @@ namespace XP.ImageProcessing.RoiControl.Controls } } + private void EnsureBubbleCircleVisuals() + { + EnsureMeasureOverlay(); + if (_bubbleCircleShape == null) + { + _bubbleCircleShape = new Ellipse + { + Stroke = Brushes.Red, + StrokeThickness = 1.5, + Fill = Brushes.Transparent, + Visibility = Visibility.Collapsed, + IsHitTestVisible = false + }; + _measureOverlay.Children.Add(_bubbleCircleShape); + } + } + + private void UpdateBubblePolyVisuals() + { + EnsureMeasureOverlay(); + if (_bubblePolyShape == null) + { + _bubblePolyShape = new Polygon + { + Stroke = Brushes.Red, + StrokeThickness = 1.5, + Fill = Brushes.Transparent, + IsHitTestVisible = false + }; + _measureOverlay.Children.Add(_bubblePolyShape); + } + _bubblePolyShape.Points = new PointCollection(_bubblePolyPoints); + } + private void InitBubbleMask() { - if (!_bubbleRoi.HasValue) return; + if (!HasBubbleRoi) return; int w = (int)CanvasWidth, h = (int)CanvasHeight; if (w <= 0 || h <= 0) return; @@ -1255,9 +1361,9 @@ namespace XP.ImageProcessing.RoiControl.Controls private void ApplyBrushAt(Point pos) { - if (_bubbleMask == null || !_bubbleRoi.HasValue) return; + if (_bubbleMask == null || !HasBubbleRoi) return; - var roi = _bubbleRoi.Value; + var roi = GetBubbleRoiBounds(); int w = _bubbleMask.PixelWidth, h = _bubbleMask.PixelHeight; int cx = (int)pos.X, cy = (int)pos.Y; int r = _bubbleBrushSize; @@ -1266,15 +1372,12 @@ namespace XP.ImageProcessing.RoiControl.Controls int roiX0 = Math.Max(0, (int)roi.X), roiY0 = Math.Max(0, (int)roi.Y); int roiX1 = Math.Min(w, (int)roi.Right), roiY1 = Math.Min(h, (int)roi.Bottom); - // 计算笔刷影响的矩形区域 int x0 = Math.Max(roiX0, cx - r), y0 = Math.Max(roiY0, cy - r); int x1 = Math.Min(roiX1, cx + r + 1), y1 = Math.Min(roiY1, cy + r + 1); if (x0 >= x1 || y0 >= y1) return; int regionW = x1 - x0, regionH = y1 - y0; - int stride = w * 4; - // 读取整行范围的像素 var pixels = new byte[regionW * regionH * 4]; _bubbleMask.CopyPixels(new System.Windows.Int32Rect(x0, y0, regionW, regionH), pixels, regionW * 4, 0); @@ -1285,6 +1388,8 @@ namespace XP.ImageProcessing.RoiControl.Controls { int dx = px2 - cx, dy = py2 - cy; if (dx * dx + dy * dy > r2) continue; + // 检查是否在 ROI 内 + if (!IsInBubbleRoi(new Point(px2, py2))) continue; int idx = ((py2 - y0) * regionW + (px2 - x0)) * 4; if (erase) @@ -1307,8 +1412,8 @@ namespace XP.ImageProcessing.RoiControl.Controls private void UpdateBubbleResult() { - if (_bubbleMask == null || !_bubbleRoi.HasValue) return; - var roi = _bubbleRoi.Value; + if (_bubbleMask == null || !HasBubbleRoi) return; + var roi = GetBubbleRoiBounds(); int w = _bubbleMask.PixelWidth, h = _bubbleMask.PixelHeight; int stride = w * 4; var pixels = new byte[stride * h]; @@ -1321,13 +1426,13 @@ namespace XP.ImageProcessing.RoiControl.Controls for (int y = roiY0; y < roiY1; y++) for (int x = roiX0; x < roiX1; x++) { + if (!IsInBubbleRoi(new Point(x, y))) continue; roiArea++; int idx = (y * w + x) * 4; - if (pixels[idx + 3] > 0) voidArea++; // alpha > 0 表示已标记 + if (pixels[idx + 3] > 0) voidArea++; } double voidRate = roiArea > 0 ? voidArea * 100.0 / roiArea : 0; - // 更新 ROI 上方标签 if (_bubbleResultLabel != null) { string cls = voidRate <= _bubbleVoidLimit ? "PASS" : "FAIL"; @@ -1344,14 +1449,15 @@ namespace XP.ImageProcessing.RoiControl.Controls /// 魔棒:在点击位置做 flood fill public void WandFloodFill(Point pos) { - if (_bubbleMask == null || !_bubbleRoi.HasValue || ImageSource == null) return; + if (_bubbleMask == null || !HasBubbleRoi || ImageSource == null) return; // 保存快照用于撤销 SaveMaskSnapshot(); - var roi = _bubbleRoi.Value; + if (!IsInBubbleRoi(pos)) return; + + var roi = GetBubbleRoiBounds(); int px = (int)pos.X, py = (int)pos.Y; - if (!roi.Contains(pos)) return; // 获取灰度像素 var gray = GetGrayscalePixels(); @@ -1361,9 +1467,8 @@ namespace XP.ImageProcessing.RoiControl.Controls if (px < 0 || px >= w || py < 0 || py >= h) return; int seedVal = gray[py * w + px]; - int lo = _bubbleThreshold, hi = _bubbleThreshold; + int lo = _bubbleThreshold; - // BFS flood fill var visited = new bool[w * h]; var queue = new System.Collections.Generic.Queue<(int x, int y)>(); queue.Enqueue((px, py)); @@ -1379,19 +1484,16 @@ namespace XP.ImageProcessing.RoiControl.Controls var (cx, cy) = queue.Dequeue(); int val = gray[cy * w + cx]; - // 阈值判断:与种子点灰度差在阈值范围内 if (Math.Abs(val - seedVal) > lo) continue; - // 必须在 ROI 内 - if (cx < roiX0 || cx >= roiX1 || cy < roiY0 || cy >= roiY1) continue; + if (!IsInBubbleRoi(new Point(cx, cy))) continue; filled.Add((cx, cy)); - // 四邻域 int[] dx = { -1, 1, 0, 0 }, dy = { 0, 0, -1, 1 }; for (int d = 0; d < 4; d++) { int nx = cx + dx[d], ny = cy + dy[d]; - if (nx >= roiX0 && nx < roiX1 && ny >= roiY0 && ny < roiY1 && !visited[ny * w + nx]) + if (nx >= 0 && nx < w && ny >= 0 && ny < h && !visited[ny * w + nx] && IsInBubbleRoi(new Point(nx, ny))) { visited[ny * w + nx] = true; queue.Enqueue((nx, ny)); @@ -1472,8 +1574,14 @@ namespace XP.ImageProcessing.RoiControl.Controls if (_bubbleRoiHandle != null) { _measureOverlay.Children.Remove(_bubbleRoiHandle); _bubbleRoiHandle = null; } if (_bubbleResultLabel != null) { _measureOverlay.Children.Remove(_bubbleResultLabel); _bubbleResultLabel = null; } if (_bubbleMaskImage != null) { _measureOverlay.Children.Remove(_bubbleMaskImage); _bubbleMaskImage = null; } + if (_bubbleCircleShape != null) { _measureOverlay.Children.Remove(_bubbleCircleShape); _bubbleCircleShape = null; } + if (_bubblePolyShape != null) { _measureOverlay.Children.Remove(_bubblePolyShape); _bubblePolyShape = null; } } _bubbleRoi = null; + _bubbleCircleCenter = null; + _bubbleCircleRadius = 0; + _bubbleCircleDragging = false; + _bubblePolyPoints.Clear(); _bubbleMask = null; _bubbleRoiStart = null; _bubbleRoiDragging = false; @@ -1830,7 +1938,7 @@ namespace XP.ImageProcessing.RoiControl.Controls e.Handled = true; return; } - // 没有 ROI 时才画新的,有 ROI 时点击外部不拦截(让图像正常拖动) + // 没有 ROI 时才画新的 if (!_bubbleRoi.HasValue) { _bubbleRoiStart = pos; @@ -1840,12 +1948,27 @@ namespace XP.ImageProcessing.RoiControl.Controls e.Handled = true; return; } - // 有 ROI 但点击了外部 → 不拦截,走正常拖动逻辑 } - if ((_bubbleTool == BubbleSubTool.Brush || _bubbleTool == BubbleSubTool.Eraser) && _bubbleRoi.HasValue) + if (_bubbleTool == BubbleSubTool.RoiCircle) { - // 只在 ROI 内才启动画笔,ROI 外不拦截,让图像正常拖动 - if (_bubbleRoi.Value.Contains(pos)) + // 没有圆形 ROI 时开始画 + if (!_bubbleCircleCenter.HasValue) + { + _bubbleRoiStart = pos; + _bubbleCircleDragging = true; + EnsureBubbleCircleVisuals(); + mainCanvas.CaptureMouse(); + e.Handled = true; + return; + } + } + if (_bubbleTool == BubbleSubTool.RoiPolygon) + { + // 多边形 ROI 由外部面板通过 CanvasClickedEvent 处理,这里不拦截 + } + if ((_bubbleTool == BubbleSubTool.Brush || _bubbleTool == BubbleSubTool.Eraser) && HasBubbleRoi) + { + if (IsInBubbleRoi(pos)) { SaveMaskSnapshot(); _bubbleBrushDragging = true; @@ -1904,8 +2027,26 @@ namespace XP.ImageProcessing.RoiControl.Controls e.Handled = true; return; } + // 气泡测量:圆形 ROI 拖拽 + if (_bubbleCircleDragging && _bubbleRoiStart.HasValue) + { + var pos = e.GetPosition(mainCanvas); + double dx = pos.X - _bubbleRoiStart.Value.X; + double dy = pos.Y - _bubbleRoiStart.Value.Y; + double r = Math.Sqrt(dx * dx + dy * dy); + if (_bubbleCircleShape != null) + { + _bubbleCircleShape.Width = r * 2; + _bubbleCircleShape.Height = r * 2; + Canvas.SetLeft(_bubbleCircleShape, _bubbleRoiStart.Value.X - r); + Canvas.SetTop(_bubbleCircleShape, _bubbleRoiStart.Value.Y - r); + _bubbleCircleShape.Visibility = Visibility.Visible; + } + e.Handled = true; + return; + } // 气泡测量:画笔/橡皮拖拽 - if (_bubbleBrushDragging && _bubbleRoi.HasValue) + if (_bubbleBrushDragging && HasBubbleRoi) { var pos = e.GetPosition(mainCanvas); ApplyBrushAt(pos); @@ -1966,6 +2107,27 @@ namespace XP.ImageProcessing.RoiControl.Controls e.Handled = true; return; } + // 气泡测量:圆形 ROI 拖拽完成 + if (_bubbleCircleDragging && _bubbleRoiStart.HasValue) + { + var pos = e.GetPosition(mainCanvas); + double dx = pos.X - _bubbleRoiStart.Value.X; + double dy = pos.Y - _bubbleRoiStart.Value.Y; + double r = Math.Sqrt(dx * dx + dy * dy); + _bubbleCircleDragging = false; + mainCanvas.ReleaseMouseCapture(); + + if (r > 5) + { + _bubbleCircleCenter = _bubbleRoiStart.Value; + _bubbleCircleRadius = r; + _bubbleRoiStart = null; + InitBubbleMask(); + RaiseMeasureStatusChanged($"圆形ROI 已设置: 半径={r:F0}px"); + } + e.Handled = true; + return; + } // 气泡测量:画笔/橡皮松开 if (_bubbleBrushDragging) { @@ -1985,7 +2147,7 @@ namespace XP.ImageProcessing.RoiControl.Controls { HandleMeasureClick(clickPosition); // 魔棒点击 - if (CurrentMeasureMode == Models.MeasureMode.BubbleMeasure && _bubbleTool == BubbleSubTool.Wand && _bubbleRoi.HasValue) + if (CurrentMeasureMode == Models.MeasureMode.BubbleMeasure && _bubbleTool == BubbleSubTool.Wand && HasBubbleRoi) { WandFloodFill(clickPosition); } diff --git a/XplorePlane/Views/ImageProcessing/BubbleMeasurePanel.xaml b/XplorePlane/Views/ImageProcessing/BubbleMeasurePanel.xaml index e4fc177..8989ad2 100644 --- a/XplorePlane/Views/ImageProcessing/BubbleMeasurePanel.xaml +++ b/XplorePlane/Views/ImageProcessing/BubbleMeasurePanel.xaml @@ -3,7 +3,7 @@ xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="气泡测量工具" - Width="340" Height="380" + Width="420" Height="380" ResizeMode="NoResize" WindowStartupLocation="CenterOwner" Topmost="True" ShowInTaskbar="False" @@ -60,9 +60,15 @@ - + + + + + + + diff --git a/XplorePlane/Views/ImageProcessing/BubbleMeasurePanel.xaml.cs b/XplorePlane/Views/ImageProcessing/BubbleMeasurePanel.xaml.cs index 99fa7aa..8a5c022 100644 --- a/XplorePlane/Views/ImageProcessing/BubbleMeasurePanel.xaml.cs +++ b/XplorePlane/Views/ImageProcessing/BubbleMeasurePanel.xaml.cs @@ -1,6 +1,9 @@ +using System; +using System.Collections.ObjectModel; using System.Windows; using Prism.Ioc; using XP.ImageProcessing.RoiControl.Controls; +using XP.ImageProcessing.RoiControl.Models; using XplorePlane.ViewModels; namespace XplorePlane.Views.ImageProcessing @@ -8,31 +11,33 @@ namespace XplorePlane.Views.ImageProcessing public partial class BubbleMeasurePanel : Window { private PolygonRoiCanvas _canvas; + private PolygonROI _polyRoiShape; + private bool _polyRoiActive; // 标记多边形 ROI 是否激活 public BubbleMeasurePanel() { InitializeComponent(); Loaded += OnLoaded; + Closed += OnClosed; } 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); + RbRoi.Checked += (s, ev) => { ClearAllBubbleRoi(); _canvas?.SetBubbleTool(PolygonRoiCanvas.BubbleSubTool.Roi); }; + RbRoiCircle.Checked += (s, ev) => { ClearAllBubbleRoi(); _canvas?.SetBubbleTool(PolygonRoiCanvas.BubbleSubTool.RoiCircle); }; + RbRoiPolygon.Checked += (s, ev) => { ClearAllBubbleRoi(); _canvas?.SetBubbleTool(PolygonRoiCanvas.BubbleSubTool.RoiPolygon); EnablePolyRoi(); }; + RbWand.Checked += (s, ev) => { _canvas?.SetBubbleTool(PolygonRoiCanvas.BubbleSubTool.Wand); FinalizePolyRoi(); }; + RbBrush.Checked += (s, ev) => { _canvas?.SetBubbleTool(PolygonRoiCanvas.BubbleSubTool.Brush); FinalizePolyRoi(); }; + RbEraser.Checked += (s, ev) => { _canvas?.SetBubbleTool(PolygonRoiCanvas.BubbleSubTool.Eraser); FinalizePolyRoi(); }; // 阈值同步 SliderThreshold.ValueChanged += (s, ev) => @@ -52,36 +57,136 @@ namespace XplorePlane.Views.ImageProcessing TbVoidLimit.TextChanged += (s, ev) => { if (double.TryParse(TbVoidLimit.Text, out double val)) - _canvas?.SetBubbleVoidLimit(System.Math.Clamp(val, 0, 100)); + _canvas?.SetBubbleVoidLimit(Math.Clamp(val, 0, 100)); }; - // 初始同步:确保面板默认值推送到 canvas + // 初始同步 _canvas?.SetBubbleTool(PolygonRoiCanvas.BubbleSubTool.Roi); _canvas?.SetBubbleThreshold((int)SliderThreshold.Value); _canvas?.SetBubbleBrushSize((int)SliderBrushSize.Value); if (double.TryParse(TbVoidLimit.Text, out double initLimit)) - _canvas?.SetBubbleVoidLimit(System.Math.Clamp(initLimit, 0, 100)); + _canvas?.SetBubbleVoidLimit(Math.Clamp(initLimit, 0, 100)); - // 监听 canvas 的工具切换事件(ROI 画完后自动切换时同步面板) if (_canvas != null) { - _canvas.BubbleToolChanged += (s, ev) => - { - // 同步面板 radio button - }; - + _canvas.BubbleToolChanged += (s, ev) => { }; _canvas.MeasureCompleted += (s, ev) => { if (ev is MeasureCompletedEventArgs args && args.MeasureType == "BubbleVoid") - { TbResult.Text = $"空隙率: {args.Distance:F1}%"; - } }; } } + // ── 多边形 ROI(复用 ROIItems 方式)── + + /// 清除所有类型的气泡 ROI(矩形/圆形/多边形) + private void ClearAllBubbleRoi() + { + CleanupPolyRoi(); + _canvas?.ClearBubbleMeasure(); + // 确保多边形点也被清空 + _canvas?.SetBubblePolyPoints(new System.Collections.Generic.List()); + TbResult.Text = "空隙率: --"; + } + + private void EnablePolyRoi() + { + if (_canvas == null) return; + if (_polyRoiShape != null) return; // 已有 + + if (_canvas.ROIItems == null) + _canvas.ROIItems = new ObservableCollection(); + + _polyRoiShape = new PolygonROI { Color = "Red", IsSelected = true }; + _polyRoiActive = true; + _canvas.ROIItems.Add(_polyRoiShape); + _canvas.SelectedROI = _polyRoiShape; + + _polyRoiShape.Points.CollectionChanged += (s, ev) => + { + if (!_polyRoiActive) return; // 已失效的 handler 不处理 + if (ev.Action == System.Collections.Specialized.NotifyCollectionChangedAction.Add || + ev.Action == System.Collections.Specialized.NotifyCollectionChangedAction.Remove) + { + _canvas.SelectedROI = null; + _canvas.SelectedROI = _polyRoiShape; + SyncPolyToCanvas(); + } + }; + + _canvas.SetValue(System.Windows.Controls.ContextMenuService.IsEnabledProperty, false); + _canvas.AddHandler(PolygonRoiCanvas.CanvasClickedEvent, new RoutedEventHandler(OnCanvasClickedForPoly)); + } + + private void OnCanvasClickedForPoly(object sender, RoutedEventArgs e) + { + if (_polyRoiShape == null) return; + if (e is CanvasClickedEventArgs args) + { + InsertPointToPolygon(args.Position, _polyRoiShape.Points); + _polyRoiShape.IsSelected = true; + _canvas.SelectedROI = _polyRoiShape; + } + } + + /// 将 PolygonROI 的点同步到 canvas 内部的 _bubblePolyPoints + private void SyncPolyToCanvas() + { + if (_canvas == null || _polyRoiShape == null) return; + _canvas.SetBubblePolyPoints(_polyRoiShape.Points); + } + + /// 切换到魔棒/画笔时,锁定多边形 ROI 不可编辑 + private void FinalizePolyRoi() + { + if (_polyRoiShape == null) return; + _polyRoiShape.IsSelected = false; + _polyRoiShape.IsEditable = false; + _canvas?.SetValue(System.Windows.Controls.ContextMenuService.IsEnabledProperty, true); + if (_canvas != null) + _canvas.RemoveHandler(PolygonRoiCanvas.CanvasClickedEvent, new RoutedEventHandler(OnCanvasClickedForPoly)); + _canvas.SelectedROI = null; + } + + private void CleanupPolyRoi() + { + _polyRoiActive = false; + if (_canvas != null) + { + _canvas.RemoveHandler(PolygonRoiCanvas.CanvasClickedEvent, new RoutedEventHandler(OnCanvasClickedForPoly)); + _canvas.SetValue(System.Windows.Controls.ContextMenuService.IsEnabledProperty, true); + } + if (_polyRoiShape != null && _canvas?.ROIItems != null) + { + _canvas.ROIItems.Remove(_polyRoiShape); + _canvas.SelectedROI = null; + _polyRoiShape = null; + } + } + + private void OnClosed(object sender, EventArgs e) + { + // 关闭面板时保留 ROI 但设为不可编辑 + _polyRoiActive = false; + if (_canvas != null) + { + _canvas.RemoveHandler(PolygonRoiCanvas.CanvasClickedEvent, new RoutedEventHandler(OnCanvasClickedForPoly)); + _canvas.SetValue(System.Windows.Controls.ContextMenuService.IsEnabledProperty, true); + } + if (_polyRoiShape != null) + { + _polyRoiShape.IsSelected = false; + _polyRoiShape.IsEditable = false; + _canvas.SelectedROI = null; + } + } + + // ── 通用操作 ── + private void ClearMask_Click(object sender, RoutedEventArgs e) { + CleanupPolyRoi(); _canvas?.ClearBubbleMeasure(); TbResult.Text = "空隙率: --"; } @@ -96,6 +201,34 @@ namespace XplorePlane.Views.ImageProcessing Close(); } + // ── 工具方法 ── + + private static void InsertPointToPolygon(Point newPoint, ObservableCollection points) + { + if (points.Count < 2) { points.Add(newPoint); return; } + + int insertIndex = 0; + double minDistance = double.MaxValue; + double d = PointToSegmentDistance(points[points.Count - 1], points[0], newPoint); + if (d < minDistance) { minDistance = d; insertIndex = 0; } + for (int i = 1; i < points.Count; i++) + { + d = PointToSegmentDistance(points[i - 1], points[i], newPoint); + if (d < minDistance) { minDistance = d; insertIndex = i; } + } + points.Insert(insertIndex, newPoint); + } + + private static double PointToSegmentDistance(Point a, Point b, Point p) + { + double dx = b.X - a.X, dy = b.Y - a.Y; + double lenSq = dx * dx + dy * dy; + if (lenSq < 1e-10) return Math.Sqrt((p.X - a.X) * (p.X - a.X) + (p.Y - a.Y) * (p.Y - a.Y)); + double t = Math.Clamp(((p.X - a.X) * dx + (p.Y - a.Y) * dy) / lenSq, 0, 1); + double projX = a.X + t * dx, projY = a.Y + t * dy; + return Math.Sqrt((p.X - projX) * (p.X - projX) + (p.Y - projY) * (p.Y - projY)); + } + private static T FindChild(DependencyObject parent) where T : DependencyObject { int count = System.Windows.Media.VisualTreeHelper.GetChildrenCount(parent); From 8dbc274e635607acd3efa3d2b30c7847fc31469d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E4=BC=9F?= Date: Wed, 6 May 2026 13:27:41 +0800 Subject: [PATCH 08/10] =?UTF-8?q?=E7=82=B9=E7=82=B9=E8=B7=9D=E6=B5=8B?= =?UTF-8?q?=E9=87=8F=E5=B7=A5=E5=85=B7=EF=BC=9A=E7=AB=AF=E7=82=B9=E7=94=B1?= =?UTF-8?q?=E7=82=B9=E6=94=B9=E4=B8=BA=E7=BA=BF=E6=AE=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Controls/PolygonRoiCanvas.xaml.cs | 78 ++++++++++++++++++- .../Models/MeasureGroup.cs | 49 ++++++++++++ 2 files changed, 126 insertions(+), 1 deletion(-) diff --git a/XP.ImageProcessing.RoiControl/Controls/PolygonRoiCanvas.xaml.cs b/XP.ImageProcessing.RoiControl/Controls/PolygonRoiCanvas.xaml.cs index d4e9b53..e6a3d9f 100644 --- a/XP.ImageProcessing.RoiControl/Controls/PolygonRoiCanvas.xaml.cs +++ b/XP.ImageProcessing.RoiControl/Controls/PolygonRoiCanvas.xaml.cs @@ -578,11 +578,87 @@ namespace XP.ImageProcessing.RoiControl.Controls var g = new Models.MeasureGroup { P1 = p1, P2 = p2 }; g.Line = new Line { Stroke = Brushes.Lime, StrokeThickness = 1, IsHitTestVisible = false }; g.Label = new TextBlock { Foreground = Brushes.Yellow, FontSize = 13, FontWeight = FontWeights.Bold, IsHitTestVisible = false }; + + // 使用垂直线段代替圆点 + g.PerpLine1 = new Line { Stroke = Brushes.Yellow, StrokeThickness = 1, IsHitTestVisible = true, Cursor = Cursors.Hand }; + g.PerpLine2 = new Line { Stroke = Brushes.Yellow, StrokeThickness = 1, IsHitTestVisible = true, Cursor = Cursors.Hand }; + + // 保留圆点以兼容拖拽逻辑(但设为不可见) g.Dot1 = CreateMDot(Brushes.Red); g.Dot2 = CreateMDot(Brushes.Blue); - foreach (UIElement el in new UIElement[] { g.Line, g.Label, g.Dot1, g.Dot2 }) + g.Dot1.Visibility = Visibility.Collapsed; + g.Dot2.Visibility = Visibility.Collapsed; + + foreach (UIElement el in new UIElement[] { g.Line, g.Label, g.PerpLine1, g.PerpLine2, g.Dot1, g.Dot2 }) _measureOverlay.Children.Add(el); + + // 设置圆点位置(用于拖拽计算) SetDotPos(g.Dot1, p1); SetDotPos(g.Dot2, p2); + + // 垂直线段1的拖拽处理 - 直接设置拖拽状态 + g.PerpLine1.MouseLeftButtonDown += (s, e) => + { + _mDraggingOwner = g; + _mDraggingRole = "Dot1"; + _mDraggingDot = g.Dot1; + g.PerpLine1.CaptureMouse(); + e.Handled = true; + }; + g.PerpLine1.MouseMove += (s, e) => + { + if (_mDraggingOwner != g || _mDraggingRole != "Dot1" || _measureOverlay == null) return; + if (e.LeftButton != MouseButtonState.Pressed) return; + var pos = e.GetPosition(_measureOverlay); + SetDotPos(g.Dot1, pos); + g.P1 = pos; + g.UpdateLine(); + g.UpdateLabel(FormatDistance(g.Distance)); + RaiseMeasureCompleted(g.P1, g.P2, g.Distance, MeasureCount, "PointDistance"); + }; + g.PerpLine1.MouseLeftButtonUp += (s, e) => + { + if (_mDraggingOwner == g && _mDraggingRole == "Dot1") + { + _mDraggingOwner = null; + _mDraggingRole = null; + _mDraggingDot = null; + g.PerpLine1.ReleaseMouseCapture(); + } + e.Handled = true; + }; + + // 垂直线段2的拖拽处理 + g.PerpLine2.MouseLeftButtonDown += (s, e) => + { + _mDraggingOwner = g; + _mDraggingRole = "Dot2"; + _mDraggingDot = g.Dot2; + g.PerpLine2.CaptureMouse(); + e.Handled = true; + }; + g.PerpLine2.MouseMove += (s, e) => + { + if (_mDraggingOwner != g || _mDraggingRole != "Dot2" || _measureOverlay == null) return; + if (e.LeftButton != MouseButtonState.Pressed) return; + var pos = e.GetPosition(_measureOverlay); + SetDotPos(g.Dot2, pos); + g.P2 = pos; + g.UpdateLine(); + g.UpdateLabel(FormatDistance(g.Distance)); + RaiseMeasureCompleted(g.P1, g.P2, g.Distance, MeasureCount, "PointDistance"); + }; + g.PerpLine2.MouseLeftButtonUp += (s, e) => + { + if (_mDraggingOwner == g && _mDraggingRole == "Dot2") + { + _mDraggingOwner = null; + _mDraggingRole = null; + _mDraggingDot = null; + g.PerpLine2.ReleaseMouseCapture(); + } + e.Handled = true; + }; + g.UpdateLine(); g.UpdateLabel(FormatDistance(g.Distance)); return g; } diff --git a/XP.ImageProcessing.RoiControl/Models/MeasureGroup.cs b/XP.ImageProcessing.RoiControl/Models/MeasureGroup.cs index 32ba2aa..0c93043 100644 --- a/XP.ImageProcessing.RoiControl/Models/MeasureGroup.cs +++ b/XP.ImageProcessing.RoiControl/Models/MeasureGroup.cs @@ -9,12 +9,21 @@ namespace XP.ImageProcessing.RoiControl.Models /// 一次点点距测量的所有视觉元素 public class MeasureGroup { + // 保留原有圆点属性以兼容其他代码 public Ellipse Dot1 { get; set; } public Ellipse Dot2 { get; set; } + + // 新增:垂直线段(替代圆点的视觉表现) + public Line PerpLine1 { get; set; } + public Line PerpLine2 { get; set; } + public Line Line { get; set; } public TextBlock Label { get; set; } public Point P1 { get; set; } public Point P2 { get; set; } + + /// 垂直线段长度(像素) + public double PerpLineLength { get; set; } = 10.0; public double Distance { @@ -30,6 +39,46 @@ namespace XP.ImageProcessing.RoiControl.Models Line.X1 = P1.X; Line.Y1 = P1.Y; Line.X2 = P2.X; Line.Y2 = P2.Y; Line.Visibility = Visibility.Visible; + + // 更新垂直线段位置 + UpdatePerpLines(); + } + + /// 更新两条垂直线段的位置 + public void UpdatePerpLines() + { + if (PerpLine1 == null || PerpLine2 == null) return; + + // 计算两点连线的方向向量 + double dx = P2.X - P1.X; + double dy = P2.Y - P1.Y; + double len = Math.Sqrt(dx * dx + dy * dy); + + if (len < 0.001) return; // 避免除零 + + // 归一化方向向量 + double ux = dx / len; + double uy = dy / len; + + // 垂直方向向量 (-uy, ux) + double vx = -uy; + double vy = ux; + + // P1处的垂直线段 + double halfLen1 = PerpLineLength / 2; + PerpLine1.X1 = P1.X + vx * halfLen1; + PerpLine1.Y1 = P1.Y + vy * halfLen1; + PerpLine1.X2 = P1.X - vx * halfLen1; + PerpLine1.Y2 = P1.Y - vy * halfLen1; + PerpLine1.Visibility = Visibility.Visible; + + // P2处的垂直线段 + double halfLen2 = PerpLineLength / 2; + PerpLine2.X1 = P2.X + vx * halfLen2; + PerpLine2.Y1 = P2.Y + vy * halfLen2; + PerpLine2.X2 = P2.X - vx * halfLen2; + PerpLine2.Y2 = P2.Y - vy * halfLen2; + PerpLine2.Visibility = Visibility.Visible; } public void UpdateLabel(string distanceText = null) From 6fc53c56c71368e2780e8936b115efa89bb8558f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E4=BC=9F?= Date: Wed, 6 May 2026 14:01:34 +0800 Subject: [PATCH 09/10] =?UTF-8?q?=E6=B0=94=E6=B3=A1=E6=B5=8B=E9=87=8F?= =?UTF-8?q?=E5=B7=A5=E5=85=B7=EF=BC=9A=E5=A2=9E=E5=8A=A0=E5=9C=86=E5=BD=A2?= =?UTF-8?q?ROI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Controls/PolygonRoiCanvas.xaml.cs | 90 ++++++++++++++++++- 1 file changed, 87 insertions(+), 3 deletions(-) diff --git a/XP.ImageProcessing.RoiControl/Controls/PolygonRoiCanvas.xaml.cs b/XP.ImageProcessing.RoiControl/Controls/PolygonRoiCanvas.xaml.cs index e6a3d9f..114bba8 100644 --- a/XP.ImageProcessing.RoiControl/Controls/PolygonRoiCanvas.xaml.cs +++ b/XP.ImageProcessing.RoiControl/Controls/PolygonRoiCanvas.xaml.cs @@ -403,6 +403,9 @@ namespace XP.ImageProcessing.RoiControl.Controls private Point? _bubbleCircleCenter; private double _bubbleCircleRadius; private bool _bubbleCircleDragging; + private bool _bubbleCircleMoving; // 拖动整个圆形 ROI + private bool _bubbleCircleResizing; // 调整圆形 ROI 大小 + private Point _bubbleCircleDragOffset; // 多边形 ROI private Polygon _bubblePolyShape; private System.Collections.ObjectModel.ObservableCollection _bubblePolyPoints = new(); @@ -506,6 +509,7 @@ namespace XP.ImageProcessing.RoiControl.Controls _bubbleRoi = null; _bubbleRoiStart = null; _bubbleRoiDragging = false; _bubbleRoiMoving = false; _bubbleRoiResizing = false; _bubbleBrushDragging = false; + _bubbleCircleDragging = false; _bubbleCircleMoving = false; _bubbleCircleResizing = false; _bubbleTool = BubbleSubTool.Roi; _bubbleUndoStack.Clear(); // 清理外部叠加的结果图层(IsHitTestVisible=false 的 Image,排除背景图) @@ -1327,7 +1331,7 @@ namespace XP.ImageProcessing.RoiControl.Controls _bubbleRoiRect = new Rectangle { Stroke = Brushes.Red, - StrokeThickness = 1.5, + StrokeThickness = 1, Fill = Brushes.Transparent, Visibility = Visibility.Collapsed, IsHitTestVisible = false @@ -1384,7 +1388,7 @@ namespace XP.ImageProcessing.RoiControl.Controls _bubbleCircleShape = new Ellipse { Stroke = Brushes.Red, - StrokeThickness = 1.5, + StrokeThickness = 1, Fill = Brushes.Transparent, Visibility = Visibility.Collapsed, IsHitTestVisible = false @@ -1393,6 +1397,18 @@ namespace XP.ImageProcessing.RoiControl.Controls } } + private void UpdateBubbleCircleVisuals() + { + if (_bubbleCircleShape == null || !_bubbleCircleCenter.HasValue) return; + var c = _bubbleCircleCenter.Value; + var r = _bubbleCircleRadius; + _bubbleCircleShape.Width = r * 2; + _bubbleCircleShape.Height = r * 2; + Canvas.SetLeft(_bubbleCircleShape, c.X - r); + Canvas.SetTop(_bubbleCircleShape, c.Y - r); + _bubbleCircleShape.Visibility = Visibility.Visible; + } + private void UpdateBubblePolyVisuals() { EnsureMeasureOverlay(); @@ -1401,7 +1417,7 @@ namespace XP.ImageProcessing.RoiControl.Controls _bubblePolyShape = new Polygon { Stroke = Brushes.Red, - StrokeThickness = 1.5, + StrokeThickness = 1, Fill = Brushes.Transparent, IsHitTestVisible = false }; @@ -1657,6 +1673,8 @@ namespace XP.ImageProcessing.RoiControl.Controls _bubbleCircleCenter = null; _bubbleCircleRadius = 0; _bubbleCircleDragging = false; + _bubbleCircleMoving = false; + _bubbleCircleResizing = false; _bubblePolyPoints.Clear(); _bubbleMask = null; _bubbleRoiStart = null; @@ -2027,6 +2045,31 @@ namespace XP.ImageProcessing.RoiControl.Controls } if (_bubbleTool == BubbleSubTool.RoiCircle) { + // 检查是否点击了圆形 ROI + if (_bubbleCircleCenter.HasValue) + { + double dx = pos.X - _bubbleCircleCenter.Value.X; + double dy = pos.Y - _bubbleCircleCenter.Value.Y; + double dist = Math.Sqrt(dx * dx + dy * dy); + // 先检查是否点击圆内部(拖动),排除边缘附近区域 + if (dist < _bubbleCircleRadius - 10) + { + _bubbleCircleMoving = true; + _bubbleCircleDragOffset = new Point(pos.X - _bubbleCircleCenter.Value.X, pos.Y - _bubbleCircleCenter.Value.Y); + mainCanvas.CaptureMouse(); + e.Handled = true; + return; + } + // 再检查是否点击边缘附近(调整大小) + if (Math.Abs(dist - _bubbleCircleRadius) < 15) + { + _bubbleCircleResizing = true; + _bubbleRoiStart = pos; + mainCanvas.CaptureMouse(); + e.Handled = true; + return; + } + } // 没有圆形 ROI 时开始画 if (!_bubbleCircleCenter.HasValue) { @@ -2121,6 +2164,27 @@ namespace XP.ImageProcessing.RoiControl.Controls e.Handled = true; return; } + // 气泡测量:圆形 ROI 拖动 + if (_bubbleCircleMoving && _bubbleCircleCenter.HasValue) + { + var pos = e.GetPosition(mainCanvas); + _bubbleCircleCenter = new Point(pos.X - _bubbleCircleDragOffset.X, pos.Y - _bubbleCircleDragOffset.Y); + UpdateBubbleCircleVisuals(); + e.Handled = true; + return; + } + // 气泡测量:圆形 ROI 调整大小 + if (_bubbleCircleResizing && _bubbleCircleCenter.HasValue) + { + var pos = e.GetPosition(mainCanvas); + double dx = pos.X - _bubbleCircleCenter.Value.X; + double dy = pos.Y - _bubbleCircleCenter.Value.Y; + _bubbleCircleRadius = Math.Max(5, Math.Sqrt(dx * dx + dy * dy)); + UpdateBubbleCircleVisuals(); + InitBubbleMask(); + e.Handled = true; + return; + } // 气泡测量:画笔/橡皮拖拽 if (_bubbleBrushDragging && HasBubbleRoi) { @@ -2204,6 +2268,26 @@ namespace XP.ImageProcessing.RoiControl.Controls e.Handled = true; return; } + // 气泡测量:圆形 ROI 拖动完成 + if (_bubbleCircleMoving) + { + _bubbleCircleMoving = false; + mainCanvas.ReleaseMouseCapture(); + InitBubbleMask(); + e.Handled = true; + return; + } + // 气泡测量:圆形 ROI 调整大小完成 + if (_bubbleCircleResizing) + { + _bubbleCircleResizing = false; + _bubbleRoiStart = null; + mainCanvas.ReleaseMouseCapture(); + InitBubbleMask(); + RaiseMeasureStatusChanged($"圆形ROI 已调整: 半径={_bubbleCircleRadius:F0}px"); + e.Handled = true; + return; + } // 气泡测量:画笔/橡皮松开 if (_bubbleBrushDragging) { From 8d7fb4e0e33581fce60249a888a9e6fa1fd30fc8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E4=BC=9F?= Date: Wed, 6 May 2026 16:15:47 +0800 Subject: [PATCH 10/10] =?UTF-8?q?=E6=B0=94=E6=B3=A1=E6=B5=8B=E9=87=8F?= =?UTF-8?q?=E5=B7=A5=E5=85=B7=EF=BC=9A=E4=BF=AE=E6=94=B9=E9=BC=A0=E6=A0=87?= =?UTF-8?q?=E5=85=89=E6=A0=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Controls/PolygonRoiCanvas.xaml.cs | 25 ++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/XP.ImageProcessing.RoiControl/Controls/PolygonRoiCanvas.xaml.cs b/XP.ImageProcessing.RoiControl/Controls/PolygonRoiCanvas.xaml.cs index 114bba8..56e7f05 100644 --- a/XP.ImageProcessing.RoiControl/Controls/PolygonRoiCanvas.xaml.cs +++ b/XP.ImageProcessing.RoiControl/Controls/PolygonRoiCanvas.xaml.cs @@ -418,7 +418,30 @@ namespace XP.ImageProcessing.RoiControl.Controls private bool _bubbleBrushDragging; private readonly System.Collections.Generic.Stack _bubbleUndoStack = new(); - public void SetBubbleTool(BubbleSubTool tool) => _bubbleTool = tool; + public void SetBubbleTool(BubbleSubTool tool) + { + _bubbleTool = tool; + // 根据工具设置光标 + UpdateBubbleToolCursor(); + } + + private void UpdateBubbleToolCursor() + { + if (mainCanvas == null) return; + if (CurrentMeasureMode != Models.MeasureMode.BubbleMeasure) + { + mainCanvas.Cursor = Cursors.Arrow; + return; + } + mainCanvas.Cursor = _bubbleTool switch + { + BubbleSubTool.Wand => Cursors.Cross, + BubbleSubTool.Brush => Cursors.Pen, + BubbleSubTool.Eraser => Cursors.No, + _ => Cursors.Arrow + }; + } + public void SetBubbleThreshold(int val) => _bubbleThreshold = val; public void SetBubbleBrushSize(int val) => _bubbleBrushSize = val; public void SetBubbleVoidLimit(double val) { _bubbleVoidLimit = val; UpdateBubbleResult(); }