diff --git a/XP.Hardware.Detector/bin/Debug/net8.0-windows7.0/XP.Hardware.Detector.deps.json b/XP.Hardware.Detector/bin/Debug/net8.0-windows7.0/XP.Hardware.Detector.deps.json index 54eff8d..6861e9d 100644 --- a/XP.Hardware.Detector/bin/Debug/net8.0-windows7.0/XP.Hardware.Detector.deps.json +++ b/XP.Hardware.Detector/bin/Debug/net8.0-windows7.0/XP.Hardware.Detector.deps.json @@ -41,178 +41,6 @@ } } }, - "Emgu.CV.runtime.windows/4.10.0.5680": { - "dependencies": { - "Emgu.CV": "4.10.0.5680", - "Emgu.runtime.windows.msvc.rt.arm64": "19.42.34435", - "Emgu.runtime.windows.msvc.rt.x64": "19.42.34435", - "Emgu.runtime.windows.msvc.rt.x86": "19.42.34435" - }, - "runtimeTargets": { - "runtimes/win-arm64/native/cvextern.dll": { - "rid": "win-arm64", - "assetType": "native", - "fileVersion": "4.10.0.5680" - }, - "runtimes/win-x64/native/cvextern.dll": { - "rid": "win-x64", - "assetType": "native", - "fileVersion": "4.10.0.5680" - }, - "runtimes/win-x64/native/libusb-1.0.dll": { - "rid": "win-x64", - "assetType": "native", - "fileVersion": "0.0.0.0" - }, - "runtimes/win-x64/native/opencv_videoio_ffmpeg4100_64.dll": { - "rid": "win-x64", - "assetType": "native", - "fileVersion": "2024.5.0.0" - }, - "runtimes/win-x86/native/cvextern.dll": { - "rid": "win-x86", - "assetType": "native", - "fileVersion": "4.10.0.5680" - }, - "runtimes/win-x86/native/libusb-1.0.dll": { - "rid": "win-x86", - "assetType": "native", - "fileVersion": "0.0.0.0" - }, - "runtimes/win-x86/native/opencv_videoio_ffmpeg4100.dll": { - "rid": "win-x86", - "assetType": "native", - "fileVersion": "2024.5.0.0" - } - } - }, - "Emgu.runtime.windows.msvc.rt.arm64/19.42.34435": { - "runtimeTargets": { - "runtimes/win-arm64/native/concrt140.dll": { - "rid": "win-arm64", - "assetType": "native", - "fileVersion": "14.42.34433.0" - }, - "runtimes/win-arm64/native/msvcp140.dll": { - "rid": "win-arm64", - "assetType": "native", - "fileVersion": "14.42.34433.0" - }, - "runtimes/win-arm64/native/msvcp140_1.dll": { - "rid": "win-arm64", - "assetType": "native", - "fileVersion": "14.42.34433.0" - }, - "runtimes/win-arm64/native/msvcp140_2.dll": { - "rid": "win-arm64", - "assetType": "native", - "fileVersion": "14.42.34433.0" - }, - "runtimes/win-arm64/native/msvcp140_atomic_wait.dll": { - "rid": "win-arm64", - "assetType": "native", - "fileVersion": "14.42.34433.0" - }, - "runtimes/win-arm64/native/msvcp140_codecvt_ids.dll": { - "rid": "win-arm64", - "assetType": "native", - "fileVersion": "14.42.34433.0" - }, - "runtimes/win-arm64/native/vcruntime140.dll": { - "rid": "win-arm64", - "assetType": "native", - "fileVersion": "14.42.34433.0" - }, - "runtimes/win-arm64/native/vcruntime140_1.dll": { - "rid": "win-arm64", - "assetType": "native", - "fileVersion": "14.42.34433.0" - } - } - }, - "Emgu.runtime.windows.msvc.rt.x64/19.42.34435": { - "runtimeTargets": { - "runtimes/win-x64/native/concrt140.dll": { - "rid": "win-x64", - "assetType": "native", - "fileVersion": "14.42.34433.0" - }, - "runtimes/win-x64/native/msvcp140.dll": { - "rid": "win-x64", - "assetType": "native", - "fileVersion": "14.42.34433.0" - }, - "runtimes/win-x64/native/msvcp140_1.dll": { - "rid": "win-x64", - "assetType": "native", - "fileVersion": "14.42.34433.0" - }, - "runtimes/win-x64/native/msvcp140_2.dll": { - "rid": "win-x64", - "assetType": "native", - "fileVersion": "14.42.34433.0" - }, - "runtimes/win-x64/native/msvcp140_atomic_wait.dll": { - "rid": "win-x64", - "assetType": "native", - "fileVersion": "14.42.34433.0" - }, - "runtimes/win-x64/native/msvcp140_codecvt_ids.dll": { - "rid": "win-x64", - "assetType": "native", - "fileVersion": "14.42.34433.0" - }, - "runtimes/win-x64/native/vcruntime140.dll": { - "rid": "win-x64", - "assetType": "native", - "fileVersion": "14.42.34433.0" - }, - "runtimes/win-x64/native/vcruntime140_1.dll": { - "rid": "win-x64", - "assetType": "native", - "fileVersion": "14.42.34433.0" - } - } - }, - "Emgu.runtime.windows.msvc.rt.x86/19.42.34435": { - "runtimeTargets": { - "runtimes/win-x86/native/concrt140.dll": { - "rid": "win-x86", - "assetType": "native", - "fileVersion": "14.42.34433.0" - }, - "runtimes/win-x86/native/msvcp140.dll": { - "rid": "win-x86", - "assetType": "native", - "fileVersion": "14.42.34433.0" - }, - "runtimes/win-x86/native/msvcp140_1.dll": { - "rid": "win-x86", - "assetType": "native", - "fileVersion": "14.42.34433.0" - }, - "runtimes/win-x86/native/msvcp140_2.dll": { - "rid": "win-x86", - "assetType": "native", - "fileVersion": "14.42.34433.0" - }, - "runtimes/win-x86/native/msvcp140_atomic_wait.dll": { - "rid": "win-x86", - "assetType": "native", - "fileVersion": "14.42.34433.0" - }, - "runtimes/win-x86/native/msvcp140_codecvt_ids.dll": { - "rid": "win-x86", - "assetType": "native", - "fileVersion": "14.42.34433.0" - }, - "runtimes/win-x86/native/vcruntime140.dll": { - "rid": "win-x86", - "assetType": "native", - "fileVersion": "14.42.34433.0" - } - } - }, "Microsoft.Bcl.AsyncInterfaces/1.1.1": { "runtime": { "lib/netstandard2.1/Microsoft.Bcl.AsyncInterfaces.dll": { @@ -1868,7 +1696,6 @@ "XP.Common/1.0.0": { "dependencies": { "Emgu.CV": "4.10.0.5680", - "Emgu.CV.runtime.windows": "4.10.0.5680", "Microsoft.Data.Sqlite": "10.0.3", "Prism.Wpf": "9.0.537", "Serilog": "4.3.1", @@ -1910,34 +1737,6 @@ "path": "emgu.cv/4.10.0.5680", "hashPath": "emgu.cv.4.10.0.5680.nupkg.sha512" }, - "Emgu.CV.runtime.windows/4.10.0.5680": { - "type": "package", - "serviceable": true, - "sha512": "sha512-WzBJWWENbF1oQKBo92ODtt9V/Eh4MFo8Y3MxxpWdoKJnZAjStxlE5epy3rB7442ZPAIRAiAaqPPZfrdml5Q9vQ==", - "path": "emgu.cv.runtime.windows/4.10.0.5680", - "hashPath": "emgu.cv.runtime.windows.4.10.0.5680.nupkg.sha512" - }, - "Emgu.runtime.windows.msvc.rt.arm64/19.42.34435": { - "type": "package", - "serviceable": true, - "sha512": "sha512-tb+JdhPLkX0MsMweKhL8zY/XUm+opxl6FLP7fyMf93EqUrUmR3YAyxmzcTyDyndh4XQ4bl+Eain9ugm7y/e26g==", - "path": "emgu.runtime.windows.msvc.rt.arm64/19.42.34435", - "hashPath": "emgu.runtime.windows.msvc.rt.arm64.19.42.34435.nupkg.sha512" - }, - "Emgu.runtime.windows.msvc.rt.x64/19.42.34435": { - "type": "package", - "serviceable": true, - "sha512": "sha512-3aIW16hRIV05a9oRW899LjPhdea/FJamPwZulwNB2P9YItwq+6XMqMeV3lg80giIQalAHQlVRVbwC54N6q3NMw==", - "path": "emgu.runtime.windows.msvc.rt.x64/19.42.34435", - "hashPath": "emgu.runtime.windows.msvc.rt.x64.19.42.34435.nupkg.sha512" - }, - "Emgu.runtime.windows.msvc.rt.x86/19.42.34435": { - "type": "package", - "serviceable": true, - "sha512": "sha512-NrCzZrq+S9jJOLWgh0ybVeHEQgillrASOFhlvV7FRrW1ufxCI3uZgySIpsGYSWQaJYpi5A4jfTP6Ekcjl3shDw==", - "path": "emgu.runtime.windows.msvc.rt.x86/19.42.34435", - "hashPath": "emgu.runtime.windows.msvc.rt.x86.19.42.34435.nupkg.sha512" - }, "Microsoft.Bcl.AsyncInterfaces/1.1.1": { "type": "package", "serviceable": true, 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"> _ppGroups = new(); private readonly System.Collections.Generic.List _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; @@ -355,10 +358,63 @@ namespace XP.ImageProcessing.RoiControl.Controls private Line _ptlTempLine; private Point? _ptlTempL1, _ptlTempL2; + // 角度测量临时状态 + private int _angleClickCount; + private Ellipse _angleTempVDot, _angleTempADot; + private Line _angleTempLineA; + private Point? _angleTempV, _angleTempA; + + // BGA 空隙测量状态 + private Models.BgaVoidGroup _bgaCurrent; // 当前正在编辑的 BGA 组 + private bool _bgaDrawBall; // true=画焊球, false=画气泡 + 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 TextBlock _bubbleResultLabel; // ROI 上方结果标签 + 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; + private readonly System.Collections.Generic.Stack _bubbleUndoStack = new(); + + 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; + + /// 设置 BGA 测量的气泡/焊球绘制模式 + public void SetBgaDrawBall(bool drawBall) + { + _bgaDrawBall = drawBall; + RaiseMeasureStatusChanged(drawBall ? "BGA - 画焊球模式" : "BGA - 画气泡模式"); + } + + /// 设置所有 BGA 组的 VoidLimit 并刷新标签 + public void SetBgaVoidLimit(double limit) + { + foreach (var g in _bgaGroups) + { + g.VoidLimit = limit; + g.UpdateLabel(); + } + } + // 拖拽状态 private Ellipse _mDraggingDot; - private object _mDraggingOwner; // MeasureGroup 或 PointToLineGroup - private string _mDraggingRole; // "Dot1","Dot2","DotL1","DotL2","DotP" + private object _mDraggingOwner; + private string _mDraggingRole; private static void OnMeasureModeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { @@ -383,8 +439,13 @@ namespace XP.ImageProcessing.RoiControl.Controls if (_ptlTempDot1 != null) { _measureOverlay.Children.Remove(_ptlTempDot1); _ptlTempDot1 = null; } if (_ptlTempDot2 != null) { _measureOverlay.Children.Remove(_ptlTempDot2); _ptlTempDot2 = null; } if (_ptlTempLine != null) { _measureOverlay.Children.Remove(_ptlTempLine); _ptlTempLine = null; } - _ptlTempL1 = _ptlTempL2 = null; - _ptlClickCount = 0; + _ptlTempL1 = _ptlTempL2 = null; _ptlClickCount = 0; + if (_angleTempVDot != null) { _measureOverlay.Children.Remove(_angleTempVDot); _angleTempVDot = null; } + 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() @@ -399,16 +460,17 @@ namespace XP.ImageProcessing.RoiControl.Controls private void RemoveMeasureOverlay() { if (_measureOverlay != null) { mainCanvas.Children.Remove(_measureOverlay); _measureOverlay = null; } - _ppGroups.Clear(); - _ptlGroups.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; + _angleTempVDot = _angleTempADot = null; _angleTempLineA = null; + _angleTempV = _angleTempA = null; _angleClickCount = 0; _mDraggingDot = null; _mDraggingOwner = null; } public void ClearMeasurements() => RemoveMeasureOverlay(); - public int MeasureCount => _ppGroups.Count + _ptlGroups.Count; + public int MeasureCount => _ppGroups.Count + _ptlGroups.Count + _angleGroups.Count + _frGroups.Count + _bgaGroups.Count; // ── 点击分发 ── @@ -421,6 +483,13 @@ namespace XP.ImageProcessing.RoiControl.Controls HandlePointDistanceClick(pos); else if (CurrentMeasureMode == Models.MeasureMode.PointToLine) HandlePointToLineClick(pos); + else if (CurrentMeasureMode == Models.MeasureMode.Angle) + HandleAngleClick(pos); + else if (CurrentMeasureMode == Models.MeasureMode.FillRate) + HandleFillRateClick(pos); + else if (CurrentMeasureMode == Models.MeasureMode.BgaVoid) + HandleBgaVoidClick(pos); + // BubbleMeasure 的点击在 MouseDown/Move/Up 中处理(拖拽画 ROI 和画笔) } // ── 点点距 ── @@ -440,6 +509,7 @@ namespace XP.ImageProcessing.RoiControl.Controls var g = CreatePPGroup(_pendingPoint.Value, pos); _ppGroups.Add(g); _measureOverlay.Children.Remove(_pendingDot); + RenumberAll(); _pendingDot = null; _pendingPoint = null; RaiseMeasureCompleted(g.P1, g.P2, g.Distance, MeasureCount, "PointDistance"); CurrentMeasureMode = Models.MeasureMode.None; @@ -490,7 +560,7 @@ namespace XP.ImageProcessing.RoiControl.Controls // 完成:创建正式组,移除临时元素 var g = CreatePTLGroup(_ptlTempL1.Value, _ptlTempL2.Value, pos); _ptlGroups.Add(g); - + RenumberAll(); if (_ptlTempDot1 != null) _measureOverlay.Children.Remove(_ptlTempDot1); if (_ptlTempDot2 != null) _measureOverlay.Children.Remove(_ptlTempDot2); if (_ptlTempLine != null) _measureOverlay.Children.Remove(_ptlTempLine); @@ -523,6 +593,285 @@ namespace XP.ImageProcessing.RoiControl.Controls return g; } + // ── 角度测量 ── + + private void HandleAngleClick(Point pos) + { + _angleClickCount++; + + if (_angleClickCount == 1) + { + _angleTempV = pos; + _angleTempVDot = CreateMDot(Brushes.Red); + _measureOverlay.Children.Add(_angleTempVDot); + SetDotPos(_angleTempVDot, pos); + RaiseMeasureStatusChanged($"角度测量 - 顶点: ({pos.X:F0}, {pos.Y:F0}),请点击射线端点A"); + } + else if (_angleClickCount == 2) + { + _angleTempA = pos; + _angleTempADot = CreateMDot(Brushes.Orange); + _measureOverlay.Children.Add(_angleTempADot); + SetDotPos(_angleTempADot, pos); + _angleTempLineA = new Line { Stroke = Brushes.Lime, StrokeThickness = 1, IsHitTestVisible = false, + X1 = _angleTempV.Value.X, Y1 = _angleTempV.Value.Y, X2 = pos.X, Y2 = pos.Y }; + _measureOverlay.Children.Add(_angleTempLineA); + RaiseMeasureStatusChanged($"角度测量 - 射线A已定义,请点击射线端点B"); + } + else if (_angleClickCount == 3) + { + var g = CreateAngleGroup(_angleTempV.Value, _angleTempA.Value, pos); + _angleGroups.Add(g); + RenumberAll(); + + // 移除临时元素 + if (_angleTempVDot != null) _measureOverlay.Children.Remove(_angleTempVDot); + if (_angleTempADot != null) _measureOverlay.Children.Remove(_angleTempADot); + if (_angleTempLineA != null) _measureOverlay.Children.Remove(_angleTempLineA); + _angleTempVDot = _angleTempADot = null; _angleTempLineA = null; + _angleTempV = _angleTempA = null; _angleClickCount = 0; + + RaiseMeasureCompleted(g.V, g.B, g.AngleDeg, MeasureCount, "Angle"); + CurrentMeasureMode = Models.MeasureMode.None; + } + } + + private Models.AngleGroup CreateAngleGroup(Point v, Point a, Point b) + { + var g = new Models.AngleGroup { V = v, A = a, B = b }; + g.LineA = new Line { Stroke = Brushes.Lime, StrokeThickness = 1, IsHitTestVisible = false }; + g.LineB = new Line { Stroke = Brushes.Lime, StrokeThickness = 1, IsHitTestVisible = false }; + g.Arc = new Path { Stroke = Brushes.Yellow, StrokeThickness = 1.5, IsHitTestVisible = false }; + g.Label = new TextBlock { Foreground = Brushes.Yellow, FontSize = 13, FontWeight = FontWeights.Bold, IsHitTestVisible = false }; + g.DotV = CreateMDot(Brushes.Red); + g.DotA = CreateMDot(Brushes.Orange); + g.DotB = CreateMDot(Brushes.Cyan); + foreach (UIElement el in new UIElement[] { g.LineA, g.LineB, g.Arc, g.Label, g.DotV, g.DotA, g.DotB }) + _measureOverlay.Children.Add(el); + SetDotPos(g.DotV, v); SetDotPos(g.DotA, a); SetDotPos(g.DotB, b); + g.UpdateVisuals(); + return g; + } + + // ── 通孔填锡率 ── + + private void HandleFillRateClick(Point pos) + { + // 单击放置:自动生成4个椭圆的初始位置 + double offset = 70; + var e1 = new Point(pos.X, pos.Y + offset / 2); + var e2 = new Point(pos.X + 20, pos.Y - offset / 2); + var e3 = e1; // 起点=底部 + var e4 = new Point(pos.X + 10, pos.Y - offset / 4); + + var g = CreateFillRateGroup(e1, e2, e3, e4); + _frGroups.Add(g); + RenumberAll(); + + double rate = g.FillRate; + RaiseMeasureCompleted(g.E3, g.E4, rate, MeasureCount, "FillRate"); + CurrentMeasureMode = Models.MeasureMode.None; + } + + private Models.FillRateGroup CreateFillRateGroup(Point e1, Point e2, Point e3, Point e4) + { + var g = new Models.FillRateGroup { E1 = e1, E2 = e2, E3 = e3, E4 = e4 }; + + g.PathE1 = CreateEllipsePath(Brushes.DodgerBlue, false); + g.PathE2 = CreateEllipsePath(Brushes.Cyan, false); + g.PathE3 = CreateEllipsePath(Brushes.Yellow, true); + 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.Label = new TextBlock { FontSize = 13, FontWeight = FontWeights.Bold, Cursor = Cursors.Hand }; + g.Label.SetValue(ContextMenuService.IsEnabledProperty, false); + g.Label.PreviewMouseRightButtonUp += (s, ev) => { ShowTHTLimitEditor(g); ev.Handled = true; }; + + g.DotE1 = CreateMDot(Brushes.DodgerBlue); + g.DotE2 = CreateMDot(Brushes.Cyan); + g.DotE3 = CreateMDot(Brushes.Yellow); + g.DotE4 = CreateMDot(Brushes.Lime); + + // 轴手柄(小方块样式区分) + g.E1AH = CreateAxisHandle(Brushes.DodgerBlue); g.E1BH = CreateAxisHandle(Brushes.DodgerBlue); + g.E2AH = CreateAxisHandle(Brushes.Cyan); g.E2BH = CreateAxisHandle(Brushes.Cyan); + g.E3AH = CreateAxisHandle(Brushes.Yellow); g.E3BH = CreateAxisHandle(Brushes.Yellow); + g.E4AH = CreateAxisHandle(Brushes.Lime); g.E4BH = CreateAxisHandle(Brushes.Lime); + + foreach (var el in g.AllElements) + _measureOverlay.Children.Add(el); + + SetDotPos(g.DotE1, e1); SetDotPos(g.DotE2, e2); + SetDotPos(g.DotE3, e3); SetDotPos(g.DotE4, e4); + g.UpdateVisuals(); + return g; + } + + private Ellipse CreateAxisHandle(Brush fill) + { + var h = new Ellipse { Width = 8, Height = 8, Fill = fill, Stroke = Brushes.White, StrokeThickness = 1, Cursor = Cursors.SizeAll }; + h.SetValue(ContextMenuService.IsEnabledProperty, false); + h.MouseLeftButtonDown += MDot_Down; + h.MouseMove += MDot_Move; + h.MouseLeftButtonUp += MDot_Up; + h.PreviewMouseRightButtonUp += MDot_RightClick; + return h; + } + + private static Path CreateEllipsePath(Brush stroke, bool dashed) + { + var p = new Path { Stroke = stroke, StrokeThickness = 1.5, IsHitTestVisible = false }; + if (dashed) p.StrokeDashArray = new DoubleCollection { 4, 2 }; + 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, IsHitTestVisible = false, Visibility = Visibility.Collapsed }; + _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) @@ -561,6 +910,53 @@ namespace XP.ImageProcessing.RoiControl.Controls if (g.DotP == dot) { _mDraggingOwner = g; _mDraggingRole = "DotP"; break; } } } + // 查找角度组 + if (_mDraggingOwner == null) + { + foreach (var g in _angleGroups) + { + if (g.DotV == dot) { _mDraggingOwner = g; _mDraggingRole = "DotV"; break; } + if (g.DotA == dot) { _mDraggingOwner = g; _mDraggingRole = "DotA"; break; } + if (g.DotB == dot) { _mDraggingOwner = g; _mDraggingRole = "DotB"; break; } + } + } + // 查找填锡率组 + if (_mDraggingOwner == null) + { + foreach (var g in _frGroups) + { + if (g.DotE1 == dot) { _mDraggingOwner = g; _mDraggingRole = "E1"; break; } + if (g.DotE2 == dot) { _mDraggingOwner = g; _mDraggingRole = "E2"; break; } + if (g.DotE3 == dot) { _mDraggingOwner = g; _mDraggingRole = "E3"; break; } + if (g.DotE4 == dot) { _mDraggingOwner = g; _mDraggingRole = "E4"; break; } + if (g.E1AH == dot) { _mDraggingOwner = g; _mDraggingRole = "E1A"; break; } + if (g.E1BH == dot) { _mDraggingOwner = g; _mDraggingRole = "E1B"; break; } + if (g.E2AH == dot) { _mDraggingOwner = g; _mDraggingRole = "E2A"; break; } + if (g.E2BH == dot) { _mDraggingOwner = g; _mDraggingRole = "E2B"; break; } + if (g.E3AH == dot) { _mDraggingOwner = g; _mDraggingRole = "E3A"; break; } + if (g.E3BH == dot) { _mDraggingOwner = g; _mDraggingRole = "E3B"; break; } + if (g.E4AH == dot) { _mDraggingOwner = g; _mDraggingRole = "E4A"; break; } + if (g.E4BH == dot) { _mDraggingOwner = g; _mDraggingRole = "E4B"; break; } + } + } + // 查找 BGA 组(已完成的 + 正在编辑的) + if (_mDraggingOwner == null) + { + var allBga = new System.Collections.Generic.List(_bgaGroups); + if (_bgaCurrent != null) allBga.Add(_bgaCurrent); + foreach (var g in allBga) + { + 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; } } @@ -586,6 +982,60 @@ namespace XP.ImageProcessing.RoiControl.Controls var foot = ptlg.FootPoint; RaiseMeasureCompleted(ptlg.P, foot, ptlg.Distance, MeasureCount, "PointToLine"); } + else if (_mDraggingOwner is Models.AngleGroup ag) + { + if (_mDraggingRole == "DotV") ag.V = pos; + else if (_mDraggingRole == "DotA") ag.A = pos; + else if (_mDraggingRole == "DotB") ag.B = pos; + ag.UpdateVisuals(); + RaiseMeasureCompleted(ag.V, ag.B, ag.AngleDeg, MeasureCount, "Angle"); + } + else if (_mDraggingOwner is Models.FillRateGroup frg) + { + if (_mDraggingRole == "E1") frg.E1 = pos; + else if (_mDraggingRole == "E2") frg.E2 = pos; + else if (_mDraggingRole == "E3") frg.E3 = pos; + else if (_mDraggingRole == "E4") frg.E4 = pos; + else if (_mDraggingRole == "E1A") frg.E1A = Math.Max(10, Models.FillRateGroup.Dist(pos, frg.E1)); + else if (_mDraggingRole == "E1B") frg.E1B = Math.Max(10, Models.FillRateGroup.Dist(pos, frg.E1)); + else if (_mDraggingRole == "E2A") frg.E2A = Math.Max(10, Models.FillRateGroup.Dist(pos, frg.E2)); + else if (_mDraggingRole == "E2B") frg.E2B = Math.Max(10, Models.FillRateGroup.Dist(pos, frg.E2)); + else if (_mDraggingRole == "E3A") frg.E3A = Math.Max(10, Models.FillRateGroup.Dist(pos, frg.E3)); + else if (_mDraggingRole == "E3B") frg.E3B = Math.Max(10, Models.FillRateGroup.Dist(pos, frg.E3)); + else if (_mDraggingRole == "E4A") frg.E4A = Math.Max(10, Models.FillRateGroup.Dist(pos, frg.E4)); + else if (_mDraggingRole == "E4B") frg.E4B = Math.Max(10, Models.FillRateGroup.Dist(pos, frg.E4)); + 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; } @@ -606,6 +1056,7 @@ namespace XP.ImageProcessing.RoiControl.Controls foreach (var el in new UIElement[] { g.Dot1, g.Dot2, g.Line, g.Label }) _measureOverlay.Children.Remove(el); _ppGroups.Remove(g); + RenumberAll(); RaiseMeasureStatusChanged($"已删除测量 | 剩余 {MeasureCount} 条"); e.Handled = true; return; } @@ -618,10 +1069,512 @@ namespace XP.ImageProcessing.RoiControl.Controls foreach (var el in new UIElement[] { g.DotL1, g.DotL2, g.DotP, g.MainLine, g.ExtLine, g.PerpLine, g.FootDot, g.Label }) _measureOverlay.Children.Remove(el); _ptlGroups.Remove(g); + RenumberAll(); RaiseMeasureStatusChanged($"已删除测量 | 剩余 {MeasureCount} 条"); e.Handled = true; return; } } + // 角度删除 + foreach (var g in _angleGroups) + { + if (g.DotV == dot || g.DotA == dot || g.DotB == dot) + { + foreach (var el in new UIElement[] { g.DotV, g.DotA, g.DotB, g.LineA, g.LineB, g.Arc, g.Label }) + _measureOverlay.Children.Remove(el); + _angleGroups.Remove(g); + RenumberAll(); + RaiseMeasureStatusChanged($"已删除测量 | 剩余 {MeasureCount} 条"); + e.Handled = true; return; + } + } + // 填锡率删除 + foreach (var g in _frGroups) + { + bool match = g.DotE1 == dot || g.DotE2 == dot || g.DotE3 == dot || g.DotE4 == dot + || g.E1AH == dot || g.E1BH == dot || g.E2AH == dot || g.E2BH == dot + || g.E3AH == dot || g.E3BH == dot || g.E4AH == dot || g.E4BH == dot; + if (match) + { + foreach (var el in g.AllElements) + _measureOverlay.Children.Remove(el); + _frGroups.Remove(g); + RenumberAll(); + RaiseMeasureStatusChanged($"已删除测量 | 剩余 {MeasureCount} 条"); + 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; + } + } + } + + // ── 气泡测量辅助 ── + + 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); + } + if (_bubbleResultLabel == null) + { + _bubbleResultLabel = new TextBlock + { + FontSize = 13, FontWeight = FontWeights.Bold, + IsHitTestVisible = false, + Visibility = Visibility.Collapsed + }; + _measureOverlay.Children.Add(_bubbleResultLabel); + } + } + + 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) + { + if (_bubbleMask == null || !_bubbleRoi.HasValue) return; + + var roi = _bubbleRoi.Value; + int w = _bubbleMask.PixelWidth, h = _bubbleMask.PixelHeight; + int cx = (int)pos.X, cy = (int)pos.Y; + int r = _bubbleBrushSize; + bool erase = _bubbleTool == BubbleSubTool.Eraser; + + 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); + + int r2 = r * r; + for (int py2 = y0; py2 < y1; py2++) + { + for (int px2 = x0; px2 < x1; px2++) + { + int dx = px2 - cx, dy = py2 - cy; + if (dx * dx + dy * dy > r2) continue; + + int idx = ((py2 - y0) * regionW + (px2 - x0)) * 4; + if (erase) + { + pixels[idx + 0] = 0; pixels[idx + 1] = 0; + pixels[idx + 2] = 0; pixels[idx + 3] = 0; + } + else + { + pixels[idx + 0] = 0; // B + pixels[idx + 1] = 140; // G + pixels[idx + 2] = 255; // R + pixels[idx + 3] = 180; // A + } + } + } + + _bubbleMask.WritePixels(new System.Windows.Int32Rect(x0, y0, regionW, regionH), pixels, regionW * 4, 0); + } + + private void UpdateBubbleResult() + { + if (_bubbleMask == null || !_bubbleRoi.HasValue) return; + var roi = _bubbleRoi.Value; + int w = _bubbleMask.PixelWidth, h = _bubbleMask.PixelHeight; + int stride = w * 4; + var pixels = new byte[stride * h]; + _bubbleMask.CopyPixels(pixels, stride, 0); + + 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 roiArea = 0, voidArea = 0; + for (int y = roiY0; y < roiY1; y++) + for (int x = roiX0; x < roiX1; x++) + { + roiArea++; + int idx = (y * w + x) * 4; + if (pixels[idx + 3] > 0) voidArea++; // alpha > 0 表示已标记 + } + + double voidRate = roiArea > 0 ? voidArea * 100.0 / roiArea : 0; + // 更新 ROI 上方标签 + if (_bubbleResultLabel != null) + { + string cls = voidRate <= 25.0 ? "PASS" : "FAIL"; + _bubbleResultLabel.Text = $"Void: {voidRate:F1}% | {cls}"; + _bubbleResultLabel.Foreground = cls == "PASS" ? Brushes.Lime : Brushes.Red; + Canvas.SetLeft(_bubbleResultLabel, roi.X); + Canvas.SetTop(_bubbleResultLabel, roi.Y - 20); + _bubbleResultLabel.Visibility = Visibility.Visible; + } + + RaiseMeasureCompleted(roi.TopLeft, roi.BottomRight, voidRate, 1, "BubbleVoid"); + } + + /// 魔棒:在点击位置做 flood fill + public void WandFloodFill(Point pos) + { + if (_bubbleMask == null || !_bubbleRoi.HasValue || ImageSource == null) return; + + // 保存快照用于撤销 + SaveMaskSnapshot(); + + var roi = _bubbleRoi.Value; + int px = (int)pos.X, py = (int)pos.Y; + if (!roi.Contains(pos)) return; + + // 获取灰度像素 + var gray = GetGrayscalePixels(); + if (gray == null) return; + + int w = _bubbleMask.PixelWidth, h = _bubbleMask.PixelHeight; + if (px < 0 || px >= w || py < 0 || py >= h) return; + + int seedVal = gray[py * w + px]; + int lo = _bubbleThreshold, hi = _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)); + visited[py * w + px] = true; + + 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); + + var filled = new System.Collections.Generic.List<(int x, int y)>(); + + while (queue.Count > 0) + { + 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; + + 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]) + { + visited[ny * w + nx] = true; + queue.Enqueue((nx, ny)); + } + } + } + + if (filled.Count == 0) return; + + // 写入 mask(橙色半透明) + int stride = w * 4; + var maskPixels = new byte[stride * h]; + _bubbleMask.CopyPixels(maskPixels, stride, 0); + + foreach (var (fx, fy) in filled) + { + int idx = (fy * w + fx) * 4; + maskPixels[idx + 0] = 0; // B + maskPixels[idx + 1] = 140; // G + maskPixels[idx + 2] = 255; // R (橙色) + maskPixels[idx + 3] = 180; // A + } + + _bubbleMask.WritePixels(new System.Windows.Int32Rect(0, 0, w, h), maskPixels, stride, 0); + UpdateBubbleResult(); + } + + /// 从 ImageSource 提取灰度像素数组 + private byte[] GetGrayscalePixels() + { + if (ImageSource is not BitmapSource bmp) return null; + + int w = bmp.PixelWidth, h = bmp.PixelHeight; + if (w != (int)CanvasWidth || h != (int)CanvasHeight) return null; + + // 转为 Bgra32 + var converted = new FormatConvertedBitmap(bmp, PixelFormats.Bgra32, null, 0); + int stride = w * 4; + var pixels = new byte[stride * h]; + converted.CopyPixels(pixels, stride, 0); + + // 提取灰度 + var gray = new byte[w * h]; + for (int i = 0; i < w * h; i++) + { + int idx = i * 4; + gray[i] = (byte)(pixels[idx + 2] * 0.299 + pixels[idx + 1] * 0.587 + pixels[idx] * 0.114); + } + return gray; + } + + private void SaveMaskSnapshot() + { + if (_bubbleMask == null) return; + int w = _bubbleMask.PixelWidth, h = _bubbleMask.PixelHeight; + int stride = w * 4; + var snapshot = new byte[stride * h]; + _bubbleMask.CopyPixels(snapshot, stride, 0); + _bubbleUndoStack.Push(snapshot); + } + + /// 撤销上一次魔棒/画笔操作 + public void UndoBubble() + { + if (_bubbleMask == null || _bubbleUndoStack.Count == 0) return; + var snapshot = _bubbleUndoStack.Pop(); + int w = _bubbleMask.PixelWidth, h = _bubbleMask.PixelHeight; + _bubbleMask.WritePixels(new System.Windows.Int32Rect(0, 0, w, h), snapshot, w * 4, 0); + 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 (_bubbleResultLabel != null) { _measureOverlay.Children.Remove(_bubbleResultLabel); _bubbleResultLabel = 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; + _bubbleUndoStack.Clear(); + } + + // 气泡工具切换事件 + 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() + { + for (int i = 0; i < _ppGroups.Count; i++) + { + _ppGroups[i].Index = i + 1; + _ppGroups[i].UpdateLine(); + _ppGroups[i].UpdateLabel(FormatDistance(_ppGroups[i].Distance)); + } + for (int i = 0; i < _ptlGroups.Count; i++) + { + _ptlGroups[i].Index = i + 1; + _ptlGroups[i].UpdateVisuals(FormatDistance(_ptlGroups[i].Distance)); + } + for (int i = 0; i < _angleGroups.Count; i++) + { + _angleGroups[i].Index = i + 1; + _angleGroups[i].UpdateVisuals(); + } + for (int i = 0; i < _frGroups.Count; i++) + { + _frGroups[i].Index = i + 1; + _frGroups[i].UpdateVisuals(); + } + for (int i = 0; i < _bgaGroups.Count; i++) + { + _bgaGroups[i].Index = i + 1; + _bgaGroups[i].UpdateLabel(); + } + } + + // ── 填锡率阈值编辑 ── + + private TextBox _thtEditBox; + private TextBlock _thtEditLabel; + + private void RemoveTHTEditor() + { + if (_thtEditBox != null && _measureOverlay != null) + _measureOverlay.Children.Remove(_thtEditBox); + if (_thtEditLabel != null && _measureOverlay != null) + _measureOverlay.Children.Remove(_thtEditLabel); + _thtEditBox = null; + _thtEditLabel = null; + } + + private void ShowTHTLimitEditor(Models.FillRateGroup g) + { + if (_measureOverlay == null) return; + + // 移除旧的编辑框 + if (_thtEditBox != null) + { + _measureOverlay.Children.Remove(_thtEditBox); + _thtEditBox = null; + } + if (_thtEditLabel != null) + { + _measureOverlay.Children.Remove(_thtEditLabel); + _thtEditLabel = null; + } + + double left = Canvas.GetLeft(g.Label); + double top = Canvas.GetTop(g.Label) + 22; + + // 参数名称提示 + _thtEditLabel = new TextBlock + { + Text = "THTLimit(%):", + 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.THTLimit.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 + 80); + 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.THTLimit = System.Math.Clamp(val, 0, 100); + g.UpdateVisuals(); + RaiseMeasureCompleted(g.E3, g.E4, g.FillRate, MeasureCount, "FillRate"); + } + RemoveTHTEditor(); + } + else if (ev.Key == System.Windows.Input.Key.Escape) + { + RemoveTHTEditor(); + } + }; + + // 失焦也关闭 + _thtEditBox.LostFocus += (s, ev) => RemoveTHTEditor(); } // ── 事件 ── @@ -774,6 +1727,62 @@ 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) + { + // 只在 ROI 内才启动画笔,ROI 外不拦截,让图像正常拖动 + if (_bubbleRoi.Value.Contains(pos)) + { + SaveMaskSnapshot(); + _bubbleBrushDragging = true; + ApplyBrushAt(pos); + mainCanvas.CaptureMouse(); + e.Handled = true; + return; + } + } + // 魔棒在 MouseUp(CanvasClicked)中处理 + } + lastMousePosition = e.GetPosition(imageDisplayGrid); isDragging = false; mainCanvas.CaptureMouse(); @@ -781,6 +1790,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); @@ -798,13 +1855,63 @@ 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) { Point clickPosition = e.GetPosition(mainCanvas); if (IsMeasuring) + { HandleMeasureClick(clickPosition); + // 魔棒点击 + if (CurrentMeasureMode == Models.MeasureMode.BubbleMeasure && _bubbleTool == BubbleSubTool.Wand && _bubbleRoi.HasValue) + { + WandFloodFill(clickPosition); + } + } OnCanvasClicked(clickPosition); } @@ -813,11 +1920,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/AngleGroup.cs b/XP.ImageProcessing.RoiControl/Models/AngleGroup.cs new file mode 100644 index 0000000..9127261 --- /dev/null +++ b/XP.ImageProcessing.RoiControl/Models/AngleGroup.cs @@ -0,0 +1,87 @@ +using System; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Media; +using System.Windows.Shapes; + +namespace XP.ImageProcessing.RoiControl.Models +{ + /// 一次角度测量的所有视觉元素(顶点V + 射线端点A/B + 两条射线 + 弧线 + 标签) + public class AngleGroup + { + public Ellipse DotV { get; set; } // 顶点 + public Ellipse DotA { get; set; } // 射线端点A + public Ellipse DotB { get; set; } // 射线端点B + public Line LineA { get; set; } // 射线VA + public Line LineB { get; set; } // 射线VB + public Path Arc { get; set; } // 角度弧线 + public TextBlock Label { get; set; } + public Point V { get; set; } + public Point A { get; set; } + public Point B { get; set; } + public int Index { get; set; } + + public double AngleDeg + { + get + { + double vax = A.X - V.X, vay = A.Y - V.Y; + double vbx = B.X - V.X, vby = B.Y - V.Y; + double lenA = Math.Sqrt(vax * vax + vay * vay); + double lenB = Math.Sqrt(vbx * vbx + vby * vby); + if (lenA < 0.001 || lenB < 0.001) return 0; + double dot = vax * vbx + vay * vby; + double cos = Math.Clamp(dot / (lenA * lenB), -1.0, 1.0); + return Math.Acos(cos) * 180.0 / Math.PI; + } + } + + public void UpdateVisuals() + { + // 射线 + LineA.X1 = V.X; LineA.Y1 = V.Y; LineA.X2 = A.X; LineA.Y2 = A.Y; + LineA.Visibility = Visibility.Visible; + LineB.X1 = V.X; LineB.Y1 = V.Y; LineB.X2 = B.X; LineB.Y2 = B.Y; + LineB.Visibility = Visibility.Visible; + + // 弧线 + double vax = A.X - V.X, vay = A.Y - V.Y; + double vbx = B.X - V.X, vby = B.Y - V.Y; + double lenA = Math.Sqrt(vax * vax + vay * vay); + double lenB = Math.Sqrt(vbx * vbx + vby * vby); + + if (lenA < 1 || lenB < 1) { Arc.Visibility = Visibility.Collapsed; return; } + + double arcRadius = Math.Min(30, Math.Min(lenA, lenB) * 0.3); + if (arcRadius < 8) arcRadius = 8; + + double angleARad = Math.Atan2(vay, vax); + double angleBRad = Math.Atan2(vby, vbx); + double cross = vax * vby - vay * vbx; + double angleDeg = AngleDeg; + bool isLargeArc = angleDeg > 180; + var sweepDir = cross >= 0 ? SweepDirection.Clockwise : SweepDirection.Counterclockwise; + + var startPt = new Point(V.X + arcRadius * Math.Cos(angleARad), V.Y + arcRadius * Math.Sin(angleARad)); + var endPt = new Point(V.X + arcRadius * Math.Cos(angleBRad), V.Y + arcRadius * Math.Sin(angleBRad)); + + var arcSeg = new ArcSegment(endPt, new Size(arcRadius, arcRadius), 0, isLargeArc, sweepDir, true); + var fig = new PathFigure(startPt, new[] { arcSeg }, false); + Arc.Data = new PathGeometry(new[] { fig }); + Arc.Visibility = Visibility.Visible; + + // 标签位置 + double midAngle = (angleARad + angleBRad) / 2.0; + double testX = Math.Cos(midAngle), testY = Math.Sin(midAngle); + double testCross = vax * testY - vay * testX; + if ((cross >= 0 && testCross < 0) || (cross < 0 && testCross >= 0)) + midAngle += Math.PI; + + double labelDist = arcRadius + 16; + Label.Text = (Index > 0 ? $"#{Index} " : "") + $"{angleDeg:F1}°"; + Canvas.SetLeft(Label, V.X + labelDist * Math.Cos(midAngle) - 15); + Canvas.SetTop(Label, V.Y + labelDist * Math.Sin(midAngle) - 8); + Label.Visibility = Visibility.Visible; + } + } +} 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/FillRateGroup.cs b/XP.ImageProcessing.RoiControl/Models/FillRateGroup.cs new file mode 100644 index 0000000..7929b15 --- /dev/null +++ b/XP.ImageProcessing.RoiControl/Models/FillRateGroup.cs @@ -0,0 +1,149 @@ +using System; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Media; +using System.Windows.Shapes; + +namespace XP.ImageProcessing.RoiControl.Models +{ + /// 通孔填锡率测量组(4个椭圆中心 + 连线 + 标签) + public class FillRateGroup + { + // 四个椭圆中心的可拖拽圆点 + public Ellipse DotE1 { get; set; } // 底部(蓝) + public Ellipse DotE2 { get; set; } // 顶部(青) + public Ellipse DotE3 { get; set; } // 填锡起点(黄) + public Ellipse DotE4 { get; set; } // 填锡终点(绿) + + // 长轴/短轴手柄(每个椭圆2个) + public Ellipse E1AH { get; set; } + public Ellipse E1BH { get; set; } + public Ellipse E2AH { get; set; } + public Ellipse E2BH { get; set; } + public Ellipse E3AH { get; set; } + public Ellipse E3BH { get; set; } + public Ellipse E4AH { get; set; } + public Ellipse E4BH { get; set; } + + // 椭圆路径 + public Path PathE1 { get; set; } + public Path PathE2 { get; set; } + public Path PathE3 { get; set; } + public Path PathE4 { get; set; } + + // 连线:E1→E2(全高度),E3→E4(填锡高度) + public Line FullLine { get; set; } + public Line FillLine { get; set; } + + public TextBlock Label { get; set; } + + // 四个椭圆中心坐标 + public Point E1 { get; set; } + public Point E2 { get; set; } + public Point E3 { get; set; } + public Point E4 { get; set; } + + // 椭圆轴参数(默认值,后续可拖拽调整) + public double E1A { get; set; } = 60; + public double E1B { get; set; } = 50; + public double E1Angle { get; set; } + public double E2A { get; set; } = 60; + public double E2B { get; set; } = 50; + public double E2Angle { get; set; } + public double E3A { get; set; } = 60; + public double E3B { get; set; } = 50; + public double E3Angle { get; set; } + public double E4A { get; set; } = 55; + public double E4B { get; set; } = 45; + public double E4Angle { get; set; } + + public double THTLimit { get; set; } = 75.0; + public int Index { get; set; } + + public double FillRate + { + get + { + double fullDx = E2.X - E1.X, fullDy = E2.Y - E1.Y; + double fullDist = Math.Sqrt(fullDx * fullDx + fullDy * fullDy); + double fillDx = E4.X - E3.X, fillDy = E4.Y - E3.Y; + double fillDist = Math.Sqrt(fillDx * fillDx + fillDy * fillDy); + return fullDist > 0 ? Math.Clamp(fillDist / fullDist * 100.0, 0, 100) : 0; + } + } + + public string Classification => FillRate >= THTLimit ? "PASS" : "FAIL"; + + public void UpdateVisuals() + { + UpdateEllipsePath(PathE1, E1, E1A, E1B, E1Angle); + UpdateEllipsePath(PathE2, E2, E2A, E2B, E2Angle); + UpdateEllipsePath(PathE3, E3, E3A, E3B, E3Angle); + UpdateEllipsePath(PathE4, E4, E4A, E4B, E4Angle); + + FullLine.X1 = E1.X; FullLine.Y1 = E1.Y; + FullLine.X2 = E2.X; FullLine.Y2 = E2.Y; + FullLine.Visibility = Visibility.Visible; + + FillLine.X1 = E3.X; FillLine.Y1 = E3.Y; + FillLine.X2 = E4.X; FillLine.Y2 = E4.Y; + FillLine.Visibility = Visibility.Visible; + + // 轴手柄定位 + SetHandle(E1AH, EdgePoint(E1, E1A, E1Angle)); + SetHandle(E1BH, EdgePoint(E1, E1B, E1Angle + 90)); + SetHandle(E2AH, EdgePoint(E2, E2A, E2Angle)); + SetHandle(E2BH, EdgePoint(E2, E2B, E2Angle + 90)); + SetHandle(E3AH, EdgePoint(E3, E3A, E3Angle)); + SetHandle(E3BH, EdgePoint(E3, E3B, E3Angle + 90)); + SetHandle(E4AH, EdgePoint(E4, E4A, E4Angle)); + SetHandle(E4BH, EdgePoint(E4, E4B, E4Angle + 90)); + + double rate = FillRate; + string cls = Classification; + Label.Text = (Index > 0 ? $"#{Index} " : "") + $"Fill: {rate:F1}% | THTLimit: {THTLimit:F1}% | {cls}"; + Label.Foreground = cls == "PASS" ? Brushes.Lime : Brushes.Red; + double labelX = Math.Max(Math.Max(E1.X, E2.X), Math.Max(E3.X, E4.X)) + 15; + double labelY = (E1.Y + E2.Y) / 2; + Canvas.SetLeft(Label, labelX); + Canvas.SetTop(Label, labelY - 10); + Label.Visibility = Visibility.Visible; + } + + /// 获取所有 UI 元素(用于添加/移除) + public UIElement[] AllElements => new UIElement[] + { + PathE1, PathE2, PathE3, PathE4, FullLine, FillLine, Label, + DotE1, DotE2, DotE3, DotE4, + E1AH, E1BH, E2AH, E2BH, E3AH, E3BH, E4AH, E4BH + }; + + private static Point EdgePoint(Point center, double radius, double angleDeg) + { + double rad = angleDeg * Math.PI / 180.0; + return new Point(center.X + radius * Math.Cos(rad), center.Y + radius * Math.Sin(rad)); + } + + private static void SetHandle(Ellipse h, Point pos) + { + if (h == null) return; + Canvas.SetLeft(h, pos.X - h.Width / 2); + Canvas.SetTop(h, pos.Y - h.Height / 2); + } + + public static double Dist(Point a, Point b) + { + double dx = b.X - a.X, dy = b.Y - a.Y; + return Math.Sqrt(dx * dx + dy * dy); + } + + private static void UpdateEllipsePath(Path path, Point center, double a, double b, double angleDeg) + { + var eg = new EllipseGeometry(center, a, b); + if (Math.Abs(angleDeg) > 0.01) + eg.Transform = new RotateTransform(angleDeg, center.X, center.Y); + path.Data = eg; + path.Visibility = Visibility.Visible; + } + } +} diff --git a/XP.ImageProcessing.RoiControl/Models/MeasureGroup.cs b/XP.ImageProcessing.RoiControl/Models/MeasureGroup.cs index 2e5670d..32ba2aa 100644 --- a/XP.ImageProcessing.RoiControl/Models/MeasureGroup.cs +++ b/XP.ImageProcessing.RoiControl/Models/MeasureGroup.cs @@ -34,10 +34,12 @@ namespace XP.ImageProcessing.RoiControl.Models public void UpdateLabel(string distanceText = null) { - Label.Text = distanceText ?? $"{Distance:F2} px"; + Label.Text = (Index > 0 ? $"#{Index} " : "") + (distanceText ?? $"{Distance:F2} px"); Canvas.SetLeft(Label, (P1.X + P2.X) / 2 + 8); Canvas.SetTop(Label, (P1.Y + P2.Y) / 2 - 18); Label.Visibility = Visibility.Visible; } + + public int Index { get; set; } } } diff --git a/XP.ImageProcessing.RoiControl/Models/MeasureMode.cs b/XP.ImageProcessing.RoiControl/Models/MeasureMode.cs index c21bce4..039b8d1 100644 --- a/XP.ImageProcessing.RoiControl/Models/MeasureMode.cs +++ b/XP.ImageProcessing.RoiControl/Models/MeasureMode.cs @@ -4,6 +4,10 @@ namespace XP.ImageProcessing.RoiControl.Models { None, PointDistance, - PointToLine + PointToLine, + Angle, + FillRate, + BgaVoid, + BubbleMeasure } } diff --git a/XP.ImageProcessing.RoiControl/Models/PointToLineGroup.cs b/XP.ImageProcessing.RoiControl/Models/PointToLineGroup.cs index 29163e4..6077a26 100644 --- a/XP.ImageProcessing.RoiControl/Models/PointToLineGroup.cs +++ b/XP.ImageProcessing.RoiControl/Models/PointToLineGroup.cs @@ -20,6 +20,7 @@ namespace XP.ImageProcessing.RoiControl.Models public Point L1 { get; set; } public Point L2 { get; set; } public Point P { get; set; } + public int Index { get; set; } public double Distance { @@ -85,7 +86,7 @@ namespace XP.ImageProcessing.RoiControl.Models FootDot.Visibility = Visibility.Visible; // 标签 - Label.Text = distanceText ?? $"{Distance:F2} px"; + Label.Text = (Index > 0 ? $"#{Index} " : "") + (distanceText ?? $"{Distance:F2} px"); Canvas.SetLeft(Label, (P.X + foot.X) / 2 + 8); Canvas.SetTop(Label, (P.Y + foot.Y) / 2 - 18); Label.Visibility = Visibility.Visible; diff --git a/XplorePlane/App.xaml.cs b/XplorePlane/App.xaml.cs index b818096..f1b6bab 100644 --- a/XplorePlane/App.xaml.cs +++ b/XplorePlane/App.xaml.cs @@ -378,7 +378,8 @@ namespace XplorePlane // 注册视图和视图模型 containerRegistry.RegisterForNavigation(); - containerRegistry.Register(); + containerRegistry.RegisterSingleton(); + containerRegistry.RegisterSingleton(); containerRegistry.RegisterSingleton(); // 注册图像处理服务与视图 diff --git a/XplorePlane/Events/MeasurementToolEvent.cs b/XplorePlane/Events/MeasurementToolEvent.cs index 7d8b129..daa45a2 100644 --- a/XplorePlane/Events/MeasurementToolEvent.cs +++ b/XplorePlane/Events/MeasurementToolEvent.cs @@ -11,7 +11,9 @@ namespace XplorePlane.Events PointDistance, PointLineDistance, Angle, - ThroughHoleFillRate + ThroughHoleFillRate, + BgaVoid, + BubbleMeasure } /// diff --git a/XplorePlane/ViewModels/Main/MainViewModel.cs b/XplorePlane/ViewModels/Main/MainViewModel.cs index f3db8d7..7d494ba 100644 --- a/XplorePlane/ViewModels/Main/MainViewModel.cs +++ b/XplorePlane/ViewModels/Main/MainViewModel.cs @@ -15,6 +15,7 @@ using XplorePlane.ViewModels.Cnc; using XplorePlane.Views; using XplorePlane.Views.Cnc; using XP.Common.Logging.Interfaces; +using XP.Common.GeneralForm.Views; using XP.Common.PdfViewer.Interfaces; using XP.Hardware.MotionControl.Abstractions; @@ -31,6 +32,97 @@ namespace XplorePlane.ViewModels private readonly CncEditorViewModel _cncEditorViewModel; private readonly CncPageView _cncPageView; + public string LicenseInfo + { + get => _licenseInfo; + set => SetProperty(ref _licenseInfo, value); + } + + private string _statusMessage = "就绪"; + public string StatusMessage + { + get => _statusMessage; + set => SetProperty(ref _statusMessage, value); + } + + public ObservableCollection NavigationTree { get; set; } + + // 导航命令 + public DelegateCommand NavigateHomeCommand { get; set; } + public DelegateCommand NavigateInspectCommand { get; set; } + public DelegateCommand OpenFileCommand { get; set; } + public DelegateCommand ExportCommand { get; set; } + public DelegateCommand ClearCommand { get; set; } + public DelegateCommand EditPropertiesCommand { get; set; } + + // 窗口打开命令 + public DelegateCommand OpenImageProcessingCommand { get; } + public DelegateCommand LoadImageCommand { get; } + public DelegateCommand OpenPipelineEditorCommand { get; } + public DelegateCommand OpenCncEditorCommand { get; } + public DelegateCommand OpenMatrixEditorCommand { get; } + public DelegateCommand OpenToolboxCommand { get; } + public DelegateCommand OpenLibraryVersionsCommand { get; } + public DelegateCommand OpenUserManualCommand { get; } + public DelegateCommand OpenCameraSettingsCommand { get; } + public DelegateCommand NewCncProgramCommand { get; } + public DelegateCommand SaveCncProgramCommand { get; } + public DelegateCommand LoadCncProgramCommand { get; } + public DelegateCommand InsertReferencePointCommand { get; } + public DelegateCommand InsertSavePositionCommand { get; } + public DelegateCommand InsertCompleteProgramCommand { get; } + public DelegateCommand InsertInspectionMarkerCommand { get; } + public DelegateCommand InsertInspectionModuleCommand { get; } + public DelegateCommand InsertSaveNodeCommand { get; } + public DelegateCommand InsertPauseDialogCommand { get; } + public DelegateCommand InsertWaitDelayCommand { get; } + + // 硬件命令 + public DelegateCommand AxisResetCommand { get; } + public DelegateCommand OpenDetectorConfigCommand { get; } + public DelegateCommand OpenMotionDebugCommand { get; } + public DelegateCommand OpenPlcAddrConfigCommand { get; } + public DelegateCommand OpenRaySourceConfigCommand { get; } + public DelegateCommand WarmUpCommand { get; } + + // 测量命令 + public DelegateCommand PointDistanceMeasureCommand { get; } + public DelegateCommand PointLineDistanceMeasureCommand { get; } + public DelegateCommand AngleMeasureCommand { get; } + public DelegateCommand ThroughHoleFillRateMeasureCommand { get; } + public DelegateCommand BgaVoidMeasureCommand { get; } + public DelegateCommand BubbleMeasureCommand { get; } + + // 辅助线命令 + public DelegateCommand ToggleCrosshairCommand { get; } + + // 设置命令 + public DelegateCommand OpenLanguageSwitcherCommand { get; } + public DelegateCommand OpenRealTimeLogViewerCommand { get; } + + /// 右侧图像区域内容 | Right-side image panel content + public object ImagePanelContent + { + get => _imagePanelContent; + set => SetProperty(ref _imagePanelContent, value); + } + + /// 右侧图像区域宽度 | Right-side image panel width + public GridLength ImagePanelWidth + { + get => _imagePanelWidth; + set => SetProperty(ref _imagePanelWidth, value); + } + + /// 主视图区宽度 | Main viewport width + public GridLength ViewportPanelWidth + { + get => _viewportPanelWidth; + set => SetProperty(ref _viewportPanelWidth, value); + } + + // 窗口引用(单例窗口防止重复打开) + private Window _motionDebugWindow; private Window _detectorConfigWindow; private Window _plcAddrConfigWindow; @@ -92,6 +184,11 @@ namespace XplorePlane.ViewModels PointLineDistanceMeasureCommand = new DelegateCommand(ExecutePointLineDistanceMeasure); AngleMeasureCommand = new DelegateCommand(ExecuteAngleMeasure); ThroughHoleFillRateMeasureCommand = new DelegateCommand(ExecuteThroughHoleFillRateMeasure); + BgaVoidMeasureCommand = new DelegateCommand(ExecuteBgaVoidMeasure); + BubbleMeasureCommand = new DelegateCommand(ExecuteBubbleMeasure); + + // 辅助线命令 + ToggleCrosshairCommand = new DelegateCommand(() => _eventAggregator.GetEvent().Publish()); @@ -113,92 +210,6 @@ namespace XplorePlane.ViewModels _logger.Info("MainViewModel 已初始化"); } - public string LicenseInfo - { - get => _licenseInfo; - set => SetProperty(ref _licenseInfo, value); - } - - public ObservableCollection NavigationTree { get; set; } - - public DelegateCommand NavigateHomeCommand { get; set; } - public DelegateCommand NavigateInspectCommand { get; set; } - public DelegateCommand OpenFileCommand { get; set; } - public DelegateCommand ExportCommand { get; set; } - public DelegateCommand ClearCommand { get; set; } - public DelegateCommand EditPropertiesCommand { get; set; } - - public DelegateCommand OpenImageProcessingCommand { get; } - public DelegateCommand LoadImageCommand { get; } - public DelegateCommand OpenPipelineEditorCommand { get; } - public DelegateCommand OpenCncEditorCommand { get; } - public DelegateCommand OpenMatrixEditorCommand { get; } - public DelegateCommand OpenToolboxCommand { get; } - public DelegateCommand OpenLibraryVersionsCommand { get; } - public DelegateCommand OpenUserManualCommand { get; } - public DelegateCommand OpenCameraSettingsCommand { get; } - public DelegateCommand NewCncProgramCommand { get; } - public DelegateCommand SaveCncProgramCommand { get; } - public DelegateCommand LoadCncProgramCommand { get; } - public DelegateCommand InsertReferencePointCommand { get; } - public DelegateCommand InsertSavePositionCommand { get; } - public DelegateCommand InsertCompleteProgramCommand { get; } - public DelegateCommand InsertInspectionMarkerCommand { get; } - public DelegateCommand InsertInspectionModuleCommand { get; } - public DelegateCommand InsertSaveNodeCommand { get; } - public DelegateCommand InsertPauseDialogCommand { get; } - public DelegateCommand InsertWaitDelayCommand { get; } - - public DelegateCommand AxisResetCommand { get; } - public DelegateCommand OpenDetectorConfigCommand { get; } - public DelegateCommand OpenMotionDebugCommand { get; } - public DelegateCommand OpenPlcAddrConfigCommand { get; } - public DelegateCommand OpenRaySourceConfigCommand { get; } - public DelegateCommand WarmUpCommand { get; } - - public DelegateCommand PointDistanceMeasureCommand { get; } - public DelegateCommand PointLineDistanceMeasureCommand { get; } - public DelegateCommand AngleMeasureCommand { get; } - public DelegateCommand ThroughHoleFillRateMeasureCommand { get; } - public DelegateCommand ToggleCrosshairCommand { get; } - - public DelegateCommand OpenLanguageSwitcherCommand { get; } - public DelegateCommand OpenRealTimeLogViewerCommand { get; } - public DelegateCommand UseLiveDetectorSourceCommand { get; } - - public bool IsMainViewportRealtimeEnabled - { - get => _mainViewportService.IsRealtimeDisplayEnabled; - set - { - if (_mainViewportService.IsRealtimeDisplayEnabled == value) - return; - - _mainViewportService.SetRealtimeDisplayEnabled(value); - RaisePropertyChanged(); - } - } - - public bool IsUsingLiveDetectorSource => _mainViewportService.CurrentSourceMode == MainViewportSourceMode.LiveDetector; - - public object ImagePanelContent - { - get => _imagePanelContent; - set => SetProperty(ref _imagePanelContent, value); - } - - public GridLength ImagePanelWidth - { - get => _imagePanelWidth; - set => SetProperty(ref _imagePanelWidth, value); - } - - public GridLength ViewportPanelWidth - { - get => _viewportPanelWidth; - set => SetProperty(ref _viewportPanelWidth, value); - } - private void ShowWindow(Window window, string name) { window.Owner = Application.Current.MainWindow; @@ -436,30 +447,106 @@ namespace XplorePlane.ViewModels } } + #endregion + + #region 测量命令实现 + + private bool CheckImageLoaded() + { + try + { + var viewportVm = _containerProvider.Resolve(); + if (viewportVm?.ImageSource != null) return true; + } + catch { } + HexMessageBox.Show("请先加载图像", MessageBoxButton.OK, MessageBoxImage.Information); + return false; + } + + private void ExecutePointDistanceMeasure() { + if (!CheckImageLoaded()) return; _logger.Info("点点距测量功能已触发"); _eventAggregator.GetEvent().Publish(MeasurementToolMode.PointDistance); } private void ExecutePointLineDistanceMeasure() { + if (!CheckImageLoaded()) return; _logger.Info("点线距测量功能已触发"); _eventAggregator.GetEvent().Publish(MeasurementToolMode.PointLineDistance); } private void ExecuteAngleMeasure() { + if (!CheckImageLoaded()) return; _logger.Info("角度测量功能已触发"); _eventAggregator.GetEvent().Publish(MeasurementToolMode.Angle); } private void ExecuteThroughHoleFillRateMeasure() { + if (!CheckImageLoaded()) return; _logger.Info("通孔填锡率测量功能已触发"); _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 _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(); + + } + private void ExecuteOpenLanguageSwitcher() { try diff --git a/XplorePlane/Views/ImageProcessing/BgaMeasurePanel.xaml b/XplorePlane/Views/ImageProcessing/BgaMeasurePanel.xaml new file mode 100644 index 0000000..6467a2e --- /dev/null +++ b/XplorePlane/Views/ImageProcessing/BgaMeasurePanel.xaml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +