BGA手动空隙测量:画气泡圆+焊球圆交互、拖拽调整、空隙率计算、VoidLimit编辑、右键删除

This commit is contained in:
李伟
2026-04-27 10:38:56 +08:00
parent e2f1b13e0e
commit e7ae7085df
6 changed files with 350 additions and 6 deletions
@@ -41,7 +41,8 @@
MouseLeftButtonDown="Canvas_MouseLeftButtonDown"
MouseLeftButtonUp="Canvas_MouseLeftButtonUp"
MouseMove="Canvas_MouseMove"
MouseRightButtonDown="Canvas_MouseRightButtonDown">
MouseRightButtonDown="Canvas_MouseRightButtonDown"
PreviewMouseRightButtonUp="Canvas_PreviewMouseRightButtonUp">
<!-- 背景图像 -->
<Image x:Name="backgroundImage"
@@ -346,6 +346,7 @@ namespace XP.ImageProcessing.RoiControl.Controls
private readonly System.Collections.Generic.List<Models.PointToLineGroup> _ptlGroups = new();
private readonly System.Collections.Generic.List<Models.AngleGroup> _angleGroups = new();
private readonly System.Collections.Generic.List<Models.FillRateGroup> _frGroups = new();
private readonly System.Collections.Generic.List<Models.BgaVoidGroup> _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;
}
}
/// <summary>右键切换气泡/焊球模式(在 BGA 测量模式下)</summary>
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