From e7ae7085df216e10552b90306e526eb5af1442ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E4=BC=9F?= Date: Mon, 27 Apr 2026 10:38:56 +0800 Subject: [PATCH] =?UTF-8?q?BGA=E6=89=8B=E5=8A=A8=E7=A9=BA=E9=9A=99?= =?UTF-8?q?=E6=B5=8B=E9=87=8F=EF=BC=9A=E7=94=BB=E6=B0=94=E6=B3=A1=E5=9C=86?= =?UTF-8?q?+=E7=84=8A=E7=90=83=E5=9C=86=E4=BA=A4=E4=BA=92=E3=80=81?= =?UTF-8?q?=E6=8B=96=E6=8B=BD=E8=B0=83=E6=95=B4=E3=80=81=E7=A9=BA=E9=9A=99?= =?UTF-8?q?=E7=8E=87=E8=AE=A1=E7=AE=97=E3=80=81VoidLimit=E7=BC=96=E8=BE=91?= =?UTF-8?q?=E3=80=81=E5=8F=B3=E9=94=AE=E5=88=A0=E9=99=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Controls/PolygonRoiCanvas.xaml | 3 +- .../Controls/PolygonRoiCanvas.xaml.cs | 251 +++++++++++++++++- .../Models/BgaVoidGroup.cs | 87 ++++++ .../Models/MeasureMode.cs | 3 +- XplorePlane/Views/Main/MainWindow.xaml | 9 +- .../Views/Main/ViewportPanelView.xaml.cs | 3 + 6 files changed, 350 insertions(+), 6 deletions(-) create mode 100644 XP.ImageProcessing.RoiControl/Models/BgaVoidGroup.cs diff --git a/XP.ImageProcessing.RoiControl/Controls/PolygonRoiCanvas.xaml b/XP.ImageProcessing.RoiControl/Controls/PolygonRoiCanvas.xaml index be035cd..0546e65 100644 --- a/XP.ImageProcessing.RoiControl/Controls/PolygonRoiCanvas.xaml +++ b/XP.ImageProcessing.RoiControl/Controls/PolygonRoiCanvas.xaml @@ -41,7 +41,8 @@ MouseLeftButtonDown="Canvas_MouseLeftButtonDown" MouseLeftButtonUp="Canvas_MouseLeftButtonUp" MouseMove="Canvas_MouseMove" - MouseRightButtonDown="Canvas_MouseRightButtonDown"> + MouseRightButtonDown="Canvas_MouseRightButtonDown" + PreviewMouseRightButtonUp="Canvas_PreviewMouseRightButtonUp"> _ptlGroups = new(); private readonly System.Collections.Generic.List _angleGroups = new(); private readonly System.Collections.Generic.List _frGroups = new(); + private readonly System.Collections.Generic.List _bgaGroups = new(); // 点点距临时状态 private Ellipse _pendingDot; @@ -363,6 +364,12 @@ namespace XP.ImageProcessing.RoiControl.Controls private Line _angleTempLineA; private Point? _angleTempV, _angleTempA; + // BGA 空隙测量状态 + private Models.BgaVoidGroup _bgaCurrent; // 当前正在编辑的 BGA 组 + private bool _bgaDrawBall; // true=画焊球, false=画气泡 + private Point? _bgaPendingCenter; // 等待第二次点击定半径 + private Ellipse _bgaPendingDot; + // 拖拽状态 private Ellipse _mDraggingDot; private object _mDraggingOwner; @@ -396,6 +403,8 @@ namespace XP.ImageProcessing.RoiControl.Controls if (_angleTempADot != null) { _measureOverlay.Children.Remove(_angleTempADot); _angleTempADot = null; } if (_angleTempLineA != null) { _measureOverlay.Children.Remove(_angleTempLineA); _angleTempLineA = null; } _angleTempV = _angleTempA = null; _angleClickCount = 0; + if (_bgaPendingDot != null) { _measureOverlay.Children.Remove(_bgaPendingDot); _bgaPendingDot = null; } + _bgaPendingCenter = null; } private void EnsureMeasureOverlay() @@ -410,7 +419,7 @@ namespace XP.ImageProcessing.RoiControl.Controls private void RemoveMeasureOverlay() { if (_measureOverlay != null) { mainCanvas.Children.Remove(_measureOverlay); _measureOverlay = null; } - _ppGroups.Clear(); _ptlGroups.Clear(); _angleGroups.Clear(); _frGroups.Clear(); + _ppGroups.Clear(); _ptlGroups.Clear(); _angleGroups.Clear(); _frGroups.Clear(); _bgaGroups.Clear(); _pendingDot = null; _pendingPoint = null; _ptlTempDot1 = _ptlTempDot2 = null; _ptlTempLine = null; _ptlTempL1 = _ptlTempL2 = null; _ptlClickCount = 0; @@ -420,7 +429,7 @@ namespace XP.ImageProcessing.RoiControl.Controls } public void ClearMeasurements() => RemoveMeasureOverlay(); - public int MeasureCount => _ppGroups.Count + _ptlGroups.Count + _angleGroups.Count + _frGroups.Count; + public int MeasureCount => _ppGroups.Count + _ptlGroups.Count + _angleGroups.Count + _frGroups.Count + _bgaGroups.Count; // ── 点击分发 ── @@ -437,6 +446,8 @@ namespace XP.ImageProcessing.RoiControl.Controls HandleAngleClick(pos); else if (CurrentMeasureMode == Models.MeasureMode.FillRate) HandleFillRateClick(pos); + else if (CurrentMeasureMode == Models.MeasureMode.BgaVoid) + HandleBgaVoidClick(pos); } // ── 点点距 ── @@ -634,7 +645,7 @@ namespace XP.ImageProcessing.RoiControl.Controls g.Label = new TextBlock { FontSize = 13, FontWeight = FontWeights.Bold, Cursor = Cursors.Hand }; g.Label.SetValue(ContextMenuService.IsEnabledProperty, false); - g.Label.MouseRightButtonUp += (s, ev) => { ShowTHTLimitEditor(g); ev.Handled = true; }; + g.Label.PreviewMouseRightButtonUp += (s, ev) => { ShowTHTLimitEditor(g); ev.Handled = true; }; g.DotE1 = CreateMDot(Brushes.DodgerBlue); g.DotE2 = CreateMDot(Brushes.Cyan); @@ -674,6 +685,153 @@ namespace XP.ImageProcessing.RoiControl.Controls return p; } + // ── BGA 空隙测量 ── + + private void HandleBgaVoidClick(Point pos) + { + if (_measureOverlay == null) EnsureMeasureOverlay(); + if (_measureOverlay == null) return; + + // 第一次进入:创建新的 BGA 组 + if (_bgaCurrent == null) + { + _bgaCurrent = new Models.BgaVoidGroup(); + var currentGroup = _bgaCurrent; // 局部变量供闭包捕获 + _bgaCurrent.Label = new TextBlock { FontSize = 13, FontWeight = FontWeights.Bold, Cursor = System.Windows.Input.Cursors.Hand, Visibility = Visibility.Collapsed }; + _bgaCurrent.Label.SetValue(ContextMenuService.IsEnabledProperty, false); + _bgaCurrent.Label.PreviewMouseRightButtonUp += (s, ev) => { ShowBgaLimitEditor(currentGroup); ev.Handled = true; }; + _measureOverlay.Children.Add(_bgaCurrent.Label); + _bgaDrawBall = false; + RaiseMeasureStatusChanged("BGA空隙 - 点击画气泡圆心(右键切换为画焊球)"); + } + + if (!_bgaPendingCenter.HasValue) + { + // 第一次点击:圆心 + _bgaPendingCenter = pos; + _bgaPendingDot = CreateMDot(_bgaDrawBall ? Brushes.Lime : Brushes.Orange); + _measureOverlay.Children.Add(_bgaPendingDot); + SetDotPos(_bgaPendingDot, pos); + RaiseMeasureStatusChanged(_bgaDrawBall ? "BGA - 焊球圆心已定,点击边缘定半径" : "BGA - 气泡圆心已定,点击边缘定半径"); + } + else + { + // 第二次点击:半径 + double r = Math.Max(5, Models.FillRateGroup.Dist(pos, _bgaPendingCenter.Value)); + var circle = CreateBgaCircle(_bgaPendingCenter.Value, r, _bgaDrawBall); + + if (_bgaDrawBall) + { + // 如果已有焊球,移除旧的 + if (_bgaCurrent.Ball != null) + { + _measureOverlay.Children.Remove(_bgaCurrent.Ball.Shape); + _measureOverlay.Children.Remove(_bgaCurrent.Ball.CenterDot); + _measureOverlay.Children.Remove(_bgaCurrent.Ball.EdgeDot); + } + _bgaCurrent.Ball = circle; + // 画完焊球 → 完成本组,计算结果 + _bgaGroups.Add(_bgaCurrent); + RenumberAll(); + _bgaCurrent.UpdateLabel(); + _bgaCurrent.Label.Cursor = System.Windows.Input.Cursors.Hand; + RaiseMeasureCompleted(_bgaCurrent.Ball.Center, _bgaCurrent.Ball.Center, _bgaCurrent.VoidRate, MeasureCount, "BgaVoid"); + _bgaCurrent = null; + _bgaDrawBall = false; + CurrentMeasureMode = Models.MeasureMode.None; + } + else + { + _bgaCurrent.Voids.Add(circle); + RaiseMeasureStatusChanged($"BGA - 已画 {_bgaCurrent.Voids.Count} 个气泡,继续画气泡或右键切换画焊球"); + } + + // 清除临时点 + if (_bgaPendingDot != null) { _measureOverlay.Children.Remove(_bgaPendingDot); _bgaPendingDot = null; } + _bgaPendingCenter = null; + } + } + + /// 右键切换气泡/焊球模式(在 BGA 测量模式下) + private void HandleBgaRightClick() + { + if (CurrentMeasureMode != Models.MeasureMode.BgaVoid || _bgaCurrent == null) return; + _bgaDrawBall = !_bgaDrawBall; + RaiseMeasureStatusChanged(_bgaDrawBall ? "BGA - 已切换为画焊球模式" : "BGA - 已切换为画气泡模式"); + } + + private Models.BgaCircle CreateBgaCircle(Point center, double radius, bool isBall) + { + var c = new Models.BgaCircle { Center = center, Radius = radius, IsBall = isBall }; + c.Shape = new Ellipse + { + Stroke = isBall ? Brushes.Lime : Brushes.Orange, + StrokeThickness = isBall ? 2 : 1.5, + Fill = Brushes.Transparent, + IsHitTestVisible = false + }; + 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.Cursor = System.Windows.Input.Cursors.SizeAll; + + _measureOverlay.Children.Add(c.Shape); + _measureOverlay.Children.Add(c.CenterDot); + _measureOverlay.Children.Add(c.EdgeDot); + c.UpdateVisuals(); + return c; + } + + // BGA Limit 编辑(和 FillRate 类似) + private void ShowBgaLimitEditor(Models.BgaVoidGroup g) + { + if (_measureOverlay == null || g == null) return; + RemoveTHTEditor(); + + double left = Canvas.GetLeft(g.Label); + double top = Canvas.GetTop(g.Label) + 22; + + _thtEditLabel = new TextBlock + { + Text = "VoidLimit(%):", + FontSize = 11, Foreground = Brushes.White, + Background = new SolidColorBrush(Color.FromArgb(180, 0, 0, 0)), + Padding = new Thickness(3, 1, 3, 1) + }; + Canvas.SetLeft(_thtEditLabel, left); + Canvas.SetTop(_thtEditLabel, top); + _measureOverlay.Children.Add(_thtEditLabel); + + _thtEditBox = new TextBox + { + Text = g.VoidLimit.ToString("F1"), + Width = 60, Height = 22, FontSize = 12, + Background = Brushes.White, BorderBrush = Brushes.Orange, BorderThickness = new Thickness(2), + Padding = new Thickness(2, 0, 2, 0) + }; + Canvas.SetLeft(_thtEditBox, left + 85); + Canvas.SetTop(_thtEditBox, top); + _measureOverlay.Children.Add(_thtEditBox); + _thtEditBox.Focus(); + _thtEditBox.SelectAll(); + + _thtEditBox.KeyDown += (s, ev) => + { + if (ev.Key == System.Windows.Input.Key.Enter) + { + if (double.TryParse(_thtEditBox.Text, out double val)) + { + g.VoidLimit = System.Math.Clamp(val, 0, 100); + g.UpdateLabel(); + RaiseMeasureCompleted(g.Ball?.Center ?? default, g.Ball?.Center ?? default, g.VoidRate, MeasureCount, "BgaVoid"); + } + RemoveTHTEditor(); + } + else if (ev.Key == System.Windows.Input.Key.Escape) RemoveTHTEditor(); + }; + _thtEditBox.LostFocus += (s, ev) => RemoveTHTEditor(); + } + // ── 共用:圆点创建、定位、拖拽、删除 ── private Ellipse CreateMDot(Brush fill) @@ -741,6 +899,22 @@ namespace XP.ImageProcessing.RoiControl.Controls if (g.E4BH == dot) { _mDraggingOwner = g; _mDraggingRole = "E4B"; break; } } } + // 查找 BGA 组 + if (_mDraggingOwner == null) + { + foreach (var g in _bgaGroups) + { + if (g.Ball?.CenterDot == dot) { _mDraggingOwner = g; _mDraggingRole = "BallCenter"; break; } + if (g.Ball?.EdgeDot == dot) { _mDraggingOwner = g; _mDraggingRole = "BallEdge"; break; } + bool found = false; + for (int vi = 0; vi < g.Voids.Count; vi++) + { + if (g.Voids[vi].CenterDot == dot) { _mDraggingOwner = g; _mDraggingRole = $"V{vi}Center"; found = true; break; } + if (g.Voids[vi].EdgeDot == dot) { _mDraggingOwner = g; _mDraggingRole = $"V{vi}Edge"; found = true; break; } + } + if (found) break; + } + } if (_mDraggingOwner != null) { _mDraggingDot = dot; dot.CaptureMouse(); e.Handled = true; } } @@ -791,6 +965,35 @@ namespace XP.ImageProcessing.RoiControl.Controls frg.UpdateVisuals(); RaiseMeasureCompleted(frg.E3, frg.E4, frg.FillRate, MeasureCount, "FillRate"); } + else if (_mDraggingOwner is Models.BgaVoidGroup bgag) + { + if (_mDraggingRole == "BallCenter" && bgag.Ball != null) + { + bgag.Ball.Center = pos; bgag.Ball.UpdateVisuals(); + } + else if (_mDraggingRole == "BallEdge" && bgag.Ball != null) + { + bgag.Ball.Radius = Math.Max(5, Models.FillRateGroup.Dist(pos, bgag.Ball.Center)); + bgag.Ball.UpdateVisuals(); + } + else if (_mDraggingRole.StartsWith("V") && _mDraggingRole.Length > 1) + { + // 解析 V{index}Center 或 V{index}Edge + string rest = _mDraggingRole.Substring(1); + bool isEdge = rest.EndsWith("Edge"); + string idxStr = isEdge ? rest.Replace("Edge", "") : rest.Replace("Center", ""); + if (int.TryParse(idxStr, out int vi) && vi >= 0 && vi < bgag.Voids.Count) + { + if (isEdge) + bgag.Voids[vi].Radius = Math.Max(5, Models.FillRateGroup.Dist(pos, bgag.Voids[vi].Center)); + else + bgag.Voids[vi].Center = pos; + bgag.Voids[vi].UpdateVisuals(); + } + } + bgag.UpdateLabel(); + RaiseMeasureCompleted(bgag.Ball?.Center ?? default, bgag.Ball?.Center ?? default, bgag.VoidRate, MeasureCount, "BgaVoid"); + } e.Handled = true; } @@ -858,6 +1061,27 @@ namespace XP.ImageProcessing.RoiControl.Controls e.Handled = true; return; } } + // BGA 删除 + foreach (var g in _bgaGroups) + { + bool match = g.Ball?.CenterDot == dot || g.Ball?.EdgeDot == dot; + if (!match) + { + foreach (var v in g.Voids) + { + if (v.CenterDot == dot || v.EdgeDot == dot) { match = true; break; } + } + } + if (match) + { + foreach (var el in g.AllElements) + _measureOverlay.Children.Remove(el); + _bgaGroups.Remove(g); + RenumberAll(); + RaiseMeasureStatusChanged($"已删除测量 | 剩余 {MeasureCount} 条"); + e.Handled = true; return; + } + } } // ── 重编号 ── @@ -885,6 +1109,11 @@ namespace XP.ImageProcessing.RoiControl.Controls _frGroups[i].Index = i + 1; _frGroups[i].UpdateVisuals(); } + for (int i = 0; i < _bgaGroups.Count; i++) + { + _bgaGroups[i].Index = i + 1; + _bgaGroups[i].UpdateLabel(); + } } // ── 填锡率阈值编辑 ── @@ -1164,11 +1393,27 @@ namespace XP.ImageProcessing.RoiControl.Controls private void Canvas_MouseRightButtonDown(object sender, MouseButtonEventArgs e) { + // BGA 模式下右键切换气泡/焊球 + if (CurrentMeasureMode == Models.MeasureMode.BgaVoid && _bgaCurrent != null) + { + HandleBgaRightClick(); + e.Handled = true; + return; + } // 右键点击完成多边形 OnRightClick(); // 不设 e.Handled,让 ContextMenu 正常弹出 } + private void Canvas_PreviewMouseRightButtonUp(object sender, MouseButtonEventArgs e) + { + // BGA 模式下阻止 ContextMenu 弹出 + if (CurrentMeasureMode == Models.MeasureMode.BgaVoid && _bgaCurrent != null) + { + e.Handled = true; + } + } + private void ROI_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) { // 选择ROI diff --git a/XP.ImageProcessing.RoiControl/Models/BgaVoidGroup.cs b/XP.ImageProcessing.RoiControl/Models/BgaVoidGroup.cs new file mode 100644 index 0000000..c3c777c --- /dev/null +++ b/XP.ImageProcessing.RoiControl/Models/BgaVoidGroup.cs @@ -0,0 +1,87 @@ +using System; +using System.Collections.Generic; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Media; +using System.Windows.Shapes; + +namespace XP.ImageProcessing.RoiControl.Models +{ + /// BGA 空隙测量中的一个圆(气泡或焊球) + public class BgaCircle + { + public Ellipse Shape { get; set; } // 圆形轮廓 + public Ellipse CenterDot { get; set; } // 中心拖拽点 + public Ellipse EdgeDot { get; set; } // 边缘拖拽点(调半径) + public Point Center { get; set; } + public double Radius { get; set; } + public bool IsBall { get; set; } // true=焊球, false=气泡 + + public double Area => Math.PI * Radius * Radius; + + public void UpdateVisuals() + { + double d = Radius * 2; + Shape.Width = d; Shape.Height = d; + Canvas.SetLeft(Shape, Center.X - Radius); + Canvas.SetTop(Shape, Center.Y - Radius); + + Canvas.SetLeft(CenterDot, Center.X - CenterDot.Width / 2); + Canvas.SetTop(CenterDot, Center.Y - CenterDot.Height / 2); + + var edgePt = new Point(Center.X + Radius, Center.Y); + Canvas.SetLeft(EdgeDot, edgePt.X - EdgeDot.Width / 2); + Canvas.SetTop(EdgeDot, edgePt.Y - EdgeDot.Height / 2); + } + } + + /// 一次 BGA 空隙测量组(1个焊球 + N个气泡 + 标签) + public class BgaVoidGroup + { + public BgaCircle Ball { get; set; } + public List Voids { get; } = new(); + public TextBlock Label { get; set; } + public int Index { get; set; } + public double VoidLimit { get; set; } = 25.0; + + public double VoidRate + { + get + { + if (Ball == null || Ball.Area < 1) return 0; + double totalVoid = 0; + foreach (var v in Voids) totalVoid += v.Area; + return Math.Clamp(totalVoid / Ball.Area * 100.0, 0, 100); + } + } + + public string Classification => VoidRate <= VoidLimit ? "PASS" : "FAIL"; + + public void UpdateLabel() + { + if (Label == null || Ball == null) return; + double rate = VoidRate; + string cls = Classification; + Label.Text = (Index > 0 ? $"#{Index} " : "") + + $"Void: {rate:F1}% | Limit: {VoidLimit:F1}% | {cls}"; + Label.Foreground = cls == "PASS" ? Brushes.Lime : Brushes.Red; + + Canvas.SetLeft(Label, Ball.Center.X + Ball.Radius + 10); + Canvas.SetTop(Label, Ball.Center.Y - 10); + Label.Visibility = Visibility.Visible; + } + + /// 获取所有 UI 元素 + public List AllElements + { + get + { + var list = new List(); + if (Ball != null) { list.Add(Ball.Shape); list.Add(Ball.CenterDot); list.Add(Ball.EdgeDot); } + foreach (var v in Voids) { list.Add(v.Shape); list.Add(v.CenterDot); list.Add(v.EdgeDot); } + if (Label != null) list.Add(Label); + return list; + } + } + } +} diff --git a/XP.ImageProcessing.RoiControl/Models/MeasureMode.cs b/XP.ImageProcessing.RoiControl/Models/MeasureMode.cs index f872803..700cf53 100644 --- a/XP.ImageProcessing.RoiControl/Models/MeasureMode.cs +++ b/XP.ImageProcessing.RoiControl/Models/MeasureMode.cs @@ -6,6 +6,7 @@ namespace XP.ImageProcessing.RoiControl.Models PointDistance, PointToLine, Angle, - FillRate + FillRate, + BgaVoid } } diff --git a/XplorePlane/Views/Main/MainWindow.xaml b/XplorePlane/Views/Main/MainWindow.xaml index f2c3aa2..c3ce257 100644 --- a/XplorePlane/Views/Main/MainWindow.xaml +++ b/XplorePlane/Views/Main/MainWindow.xaml @@ -443,7 +443,7 @@ Text="通孔填锡率" /> - + +