using XP.ImageProcessing.RoiControl.Models; using System; using System.Collections.ObjectModel; using System.Collections.Specialized; using System.Windows; using System.Windows.Controls; using System.Windows.Documents; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Imaging; using System.Windows.Shapes; namespace XP.ImageProcessing.RoiControl.Controls { public partial class PolygonRoiCanvas : UserControl { private bool isDragging = false; private Point lastMousePosition; private const double ZoomStep = 1.2; private Adorner? currentAdorner; public PolygonRoiCanvas() { InitializeComponent(); Loaded += PolygonRoiCanvas_Loaded; } private void PolygonRoiCanvas_Loaded(object sender, RoutedEventArgs e) { // 监听ROI集合变化 if (ROIItems != null) { ROIItems.CollectionChanged += ROIItems_CollectionChanged; foreach (var roi in ROIItems) { roi.PropertyChanged += ROI_PropertyChanged; // 如果是多边形ROI,监听Points集合变化 if (roi is PolygonROI polygonROI) { polygonROI.Points.CollectionChanged += Points_CollectionChanged; } } } } private void ROIItems_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) { if (e.NewItems != null) { foreach (ROIShape roi in e.NewItems) { roi.PropertyChanged += ROI_PropertyChanged; // 如果是多边形ROI,监听Points集合变化 if (roi is PolygonROI polygonROI) { polygonROI.Points.CollectionChanged += Points_CollectionChanged; } } } if (e.OldItems != null) { foreach (ROIShape roi in e.OldItems) { roi.PropertyChanged -= ROI_PropertyChanged; // 取消监听Points集合变化 if (roi is PolygonROI polygonROI) { polygonROI.Points.CollectionChanged -= Points_CollectionChanged; } } } } private void Points_CollectionChanged(object? sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e) { // 只在删除或添加顶点时更新Adorner,拖拽时的Replace操作不触发更新 if (e.Action == System.Collections.Specialized.NotifyCollectionChangedAction.Remove || e.Action == System.Collections.Specialized.NotifyCollectionChangedAction.Add) { // Points集合变化时,如果当前选中的是多边形ROI,更新Adorner if (SelectedROI is PolygonROI polygonROI && sender == polygonROI.Points) { // 使用Dispatcher延迟更新,确保UI已经处理完Points的变化 Dispatcher.BeginInvoke(new Action(() => { UpdateAdorner(); }), System.Windows.Threading.DispatcherPriority.Render); } } // Replace操作(拖拽时)不需要重建Adorner,只需要让现有Adorner重新布局 } private void ROI_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e) { if (e.PropertyName == nameof(ROIShape.IsSelected)) { UpdateAdorner(); } // 监听Points属性变化(整个集合替换的情况) else if (e.PropertyName == "Points" && sender is PolygonROI) { UpdateAdorner(); } } #region Dependency Properties public static readonly DependencyProperty ImageSourceProperty = DependencyProperty.Register(nameof(ImageSource), typeof(ImageSource), typeof(PolygonRoiCanvas), new PropertyMetadata(null, OnImageSourceChanged)); public ImageSource? ImageSource { get => (ImageSource?)GetValue(ImageSourceProperty); set => SetValue(ImageSourceProperty, value); } private static void OnImageSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { var control = (PolygonRoiCanvas)d; if (e.NewValue is BitmapSource bitmap) { // 使用像素尺寸,避免 DPI 不同导致 DIP 尺寸与实际像素不一致 control.CanvasWidth = bitmap.PixelWidth; control.CanvasHeight = bitmap.PixelHeight; control.ResetView(); } else if (e.NewValue is ImageSource imageSource) { control.CanvasWidth = imageSource.Width; control.CanvasHeight = imageSource.Height; control.ResetView(); } // 图像尺寸变化后刷新十字线 if (control.ShowCrosshair) control.AddCrosshair(); } public static readonly DependencyProperty ROIItemsProperty = DependencyProperty.Register(nameof(ROIItems), typeof(ObservableCollection), typeof(PolygonRoiCanvas), new PropertyMetadata(null)); public ObservableCollection? ROIItems { get => (ObservableCollection?)GetValue(ROIItemsProperty); set => SetValue(ROIItemsProperty, value); } public static readonly DependencyProperty ZoomScaleProperty = DependencyProperty.Register(nameof(ZoomScale), typeof(double), typeof(PolygonRoiCanvas), new PropertyMetadata(1.0, OnZoomScaleChanged)); public double ZoomScale { get => (double)GetValue(ZoomScaleProperty); set => SetValue(ZoomScaleProperty, Math.Max(0.1, Math.Min(10.0, value))); } private static void OnZoomScaleChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { var control = (PolygonRoiCanvas)d; // 缩放变化时更新Adorner以调整控制点大小 control.UpdateAdorner(); } public static readonly DependencyProperty PanOffsetXProperty = DependencyProperty.Register(nameof(PanOffsetX), typeof(double), typeof(PolygonRoiCanvas), new PropertyMetadata(0.0, OnPanOffsetChanged)); public double PanOffsetX { get => (double)GetValue(PanOffsetXProperty); set => SetValue(PanOffsetXProperty, value); } public static readonly DependencyProperty PanOffsetYProperty = DependencyProperty.Register(nameof(PanOffsetY), typeof(double), typeof(PolygonRoiCanvas), new PropertyMetadata(0.0, OnPanOffsetChanged)); public double PanOffsetY { get => (double)GetValue(PanOffsetYProperty); set => SetValue(PanOffsetYProperty, value); } private static void OnPanOffsetChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { var control = (PolygonRoiCanvas)d; // 平移时重建Adorner,确保控制点位置正确 if (control.SelectedROI != null && control.SelectedROI.IsSelected) { control.UpdateAdorner(); } } public static readonly DependencyProperty CanvasWidthProperty = DependencyProperty.Register(nameof(CanvasWidth), typeof(double), typeof(PolygonRoiCanvas), new PropertyMetadata(800.0)); public double CanvasWidth { get => (double)GetValue(CanvasWidthProperty); set => SetValue(CanvasWidthProperty, value); } public static readonly DependencyProperty CanvasHeightProperty = DependencyProperty.Register(nameof(CanvasHeight), typeof(double), typeof(PolygonRoiCanvas), new PropertyMetadata(600.0)); public double CanvasHeight { get => (double)GetValue(CanvasHeightProperty); set => SetValue(CanvasHeightProperty, value); } public static readonly DependencyProperty SelectedROIProperty = DependencyProperty.Register(nameof(SelectedROI), typeof(ROIShape), typeof(PolygonRoiCanvas), new PropertyMetadata(null, OnSelectedROIChanged)); public ROIShape? SelectedROI { get => (ROIShape?)GetValue(SelectedROIProperty); set => SetValue(SelectedROIProperty, value); } private static void OnSelectedROIChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { var control = (PolygonRoiCanvas)d; // 更新IsSelected状态 if (e.OldValue is ROIShape oldROI) { oldROI.IsSelected = false; } if (e.NewValue is ROIShape newROI) { newROI.IsSelected = true; } control.UpdateAdorner(); } #endregion Dependency Properties #region Measurement Config public static readonly DependencyProperty PixelSizeProperty = DependencyProperty.Register(nameof(PixelSize), typeof(double), typeof(PolygonRoiCanvas), new PropertyMetadata(1.0)); /// 每像素对应的物理尺寸(= 探测器像素尺寸 / 放大倍率),默认 1.0 public double PixelSize { get => (double)GetValue(PixelSizeProperty); set => SetValue(PixelSizeProperty, value); } public static readonly DependencyProperty MeasureUnitProperty = DependencyProperty.Register(nameof(MeasureUnit), typeof(string), typeof(PolygonRoiCanvas), new PropertyMetadata("px")); /// 测量单位,默认 "px",可设为 "mm"/"μm"/"cm" public string MeasureUnit { get => (string)GetValue(MeasureUnitProperty); set => SetValue(MeasureUnitProperty, value); } /// 将像素距离转换为物理距离文本 internal string FormatDistance(double pixelDistance) { string unit = MeasureUnit ?? "px"; if (unit == "px" || PixelSize <= 0 || PixelSize == 1.0) return $"{pixelDistance:F2} px"; double physical = pixelDistance * PixelSize; return $"{physical:F4} {unit}"; } #endregion Measurement Config #region Crosshair public static readonly DependencyProperty ShowCrosshairProperty = DependencyProperty.Register(nameof(ShowCrosshair), typeof(bool), typeof(PolygonRoiCanvas), new PropertyMetadata(false, OnShowCrosshairChanged)); public bool ShowCrosshair { get => (bool)GetValue(ShowCrosshairProperty); set => SetValue(ShowCrosshairProperty, value); } private Line _crosshairH, _crosshairV; private static void OnShowCrosshairChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { var c = (PolygonRoiCanvas)d; if ((bool)e.NewValue) c.AddCrosshair(); else c.RemoveCrosshair(); } private void AddCrosshair() { RemoveCrosshair(); double w = CanvasWidth, h = CanvasHeight; if (w <= 0 || h <= 0) return; _crosshairH = new Line { X1 = 0, Y1 = h / 2, X2 = w, Y2 = h / 2, Stroke = Brushes.Red, StrokeThickness = 1, Opacity = 0.7, IsHitTestVisible = false }; _crosshairV = new Line { X1 = w / 2, Y1 = 0, X2 = w / 2, Y2 = h, Stroke = Brushes.Red, StrokeThickness = 1, Opacity = 0.7, IsHitTestVisible = false }; mainCanvas.Children.Add(_crosshairH); mainCanvas.Children.Add(_crosshairV); } private void RemoveCrosshair() { if (_crosshairH != null) { mainCanvas.Children.Remove(_crosshairH); _crosshairH = null; } if (_crosshairV != null) { mainCanvas.Children.Remove(_crosshairV); _crosshairV = null; } } #endregion Crosshair #region Measurement public static readonly DependencyProperty CurrentMeasureModeProperty = DependencyProperty.Register(nameof(CurrentMeasureMode), typeof(Models.MeasureMode), typeof(PolygonRoiCanvas), new PropertyMetadata(Models.MeasureMode.None, OnMeasureModeChanged)); public Models.MeasureMode CurrentMeasureMode { get => (Models.MeasureMode)GetValue(CurrentMeasureModeProperty); set => SetValue(CurrentMeasureModeProperty, value); } // 保留 IsMeasuring 作为便捷属性 public bool IsMeasuring => CurrentMeasureMode != Models.MeasureMode.None; private Canvas _measureOverlay; private readonly System.Collections.Generic.List _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; private Point? _pendingPoint; // 点线距临时状态 private int _ptlClickCount; private Ellipse _ptlTempDot1, _ptlTempDot2; 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 Rect? _bubbleRoi; private Point? _bubbleRoiStart; private bool _bubbleRoiDragging; private bool _bubbleRoiMoving; // 拖动整个 ROI private bool _bubbleRoiResizing; // 右下角调整大小 private Point _bubbleRoiDragOffset; private Image _bubbleMaskImage; private System.Windows.Media.Imaging.WriteableBitmap _bubbleMask; private int _bubbleThreshold = 128; private int _bubbleBrushSize = 5; private bool _bubbleBrushDragging; public void SetBubbleTool(BubbleSubTool tool) => _bubbleTool = tool; public void SetBubbleThreshold(int val) => _bubbleThreshold = val; public void SetBubbleBrushSize(int val) => _bubbleBrushSize = val; public Rect? BubbleRoi => _bubbleRoi; // 拖拽状态 private Ellipse _mDraggingDot; private object _mDraggingOwner; private string _mDraggingRole; private static void OnMeasureModeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { var c = (PolygonRoiCanvas)d; var newMode = (Models.MeasureMode)e.NewValue; if (newMode != Models.MeasureMode.None) { c.EnsureMeasureOverlay(); } else { // 退出测量模式:清除未完成的临时元素 c.ClearPendingElements(); } } private void ClearPendingElements() { if (_measureOverlay == null) return; if (_pendingDot != null) { _measureOverlay.Children.Remove(_pendingDot); _pendingDot = null; } _pendingPoint = null; 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; 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() { if (_measureOverlay != null) return; _measureOverlay = new Canvas { IsHitTestVisible = true, Background = Brushes.Transparent }; _measureOverlay.SetBinding(Canvas.WidthProperty, new System.Windows.Data.Binding("CanvasWidth") { Source = this }); _measureOverlay.SetBinding(Canvas.HeightProperty, new System.Windows.Data.Binding("CanvasHeight") { Source = this }); mainCanvas.Children.Add(_measureOverlay); } private void RemoveMeasureOverlay() { if (_measureOverlay != null) { mainCanvas.Children.Remove(_measureOverlay); _measureOverlay = null; } _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 + _angleGroups.Count + _frGroups.Count + _bgaGroups.Count; // ── 点击分发 ── private void HandleMeasureClick(Point pos) { if (_measureOverlay == null) EnsureMeasureOverlay(); if (_measureOverlay == null) return; if (CurrentMeasureMode == Models.MeasureMode.PointDistance) 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 和画笔) } // ── 点点距 ── private void HandlePointDistanceClick(Point pos) { if (!_pendingPoint.HasValue) { _pendingPoint = pos; _pendingDot = CreateMDot(Brushes.Red); _measureOverlay.Children.Add(_pendingDot); SetDotPos(_pendingDot, pos); RaiseMeasureStatusChanged($"点点距 - 第一点: ({pos.X:F0}, {pos.Y:F0}),请点击第二个点"); } else { 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; } } private Models.MeasureGroup CreatePPGroup(Point p1, Point p2) { var g = new Models.MeasureGroup { P1 = p1, P2 = p2 }; g.Line = new Line { Stroke = Brushes.Lime, StrokeThickness = 2, IsHitTestVisible = false }; g.Label = new TextBlock { Foreground = Brushes.Yellow, FontSize = 13, FontWeight = FontWeights.Bold, IsHitTestVisible = false }; g.Dot1 = CreateMDot(Brushes.Red); g.Dot2 = CreateMDot(Brushes.Blue); foreach (UIElement el in new UIElement[] { g.Line, g.Label, g.Dot1, g.Dot2 }) _measureOverlay.Children.Add(el); SetDotPos(g.Dot1, p1); SetDotPos(g.Dot2, p2); g.UpdateLine(); g.UpdateLabel(FormatDistance(g.Distance)); return g; } // ── 点线距 ── private void HandlePointToLineClick(Point pos) { _ptlClickCount++; if (_ptlClickCount == 1) { _ptlTempL1 = pos; _ptlTempDot1 = CreateMDot(Brushes.Lime); _measureOverlay.Children.Add(_ptlTempDot1); SetDotPos(_ptlTempDot1, pos); RaiseMeasureStatusChanged($"点线距 - 直线端点1: ({pos.X:F0}, {pos.Y:F0}),请点击直线端点2"); } else if (_ptlClickCount == 2) { _ptlTempL2 = pos; _ptlTempDot2 = CreateMDot(Brushes.Lime); _measureOverlay.Children.Add(_ptlTempDot2); SetDotPos(_ptlTempDot2, pos); _ptlTempLine = new Line { Stroke = Brushes.Lime, StrokeThickness = 2, IsHitTestVisible = false, X1 = _ptlTempL1.Value.X, Y1 = _ptlTempL1.Value.Y, X2 = pos.X, Y2 = pos.Y }; _measureOverlay.Children.Add(_ptlTempLine); RaiseMeasureStatusChanged($"点线距 - 直线已定义,请点击测量点"); } else if (_ptlClickCount == 3) { // 完成:创建正式组,移除临时元素 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); _ptlTempDot1 = _ptlTempDot2 = null; _ptlTempLine = null; _ptlTempL1 = _ptlTempL2 = null; _ptlClickCount = 0; var foot = g.FootPoint; RaiseMeasureCompleted(g.P, foot, g.Distance, MeasureCount, "PointToLine"); CurrentMeasureMode = Models.MeasureMode.None; } } private Models.PointToLineGroup CreatePTLGroup(Point l1, Point l2, Point p) { var g = new Models.PointToLineGroup { L1 = l1, L2 = l2, P = p }; g.MainLine = new Line { Stroke = Brushes.Lime, StrokeThickness = 2, IsHitTestVisible = false }; g.ExtLine = new Line { Stroke = Brushes.Lime, StrokeThickness = 1, IsHitTestVisible = false, StrokeDashArray = new DoubleCollection { 4, 2 }, Visibility = Visibility.Collapsed }; g.PerpLine = new Line { Stroke = Brushes.Yellow, StrokeThickness = 1, IsHitTestVisible = false, StrokeDashArray = new DoubleCollection { 4, 2 } }; g.FootDot = new Ellipse { Width = 8, Height = 8, Fill = Brushes.Cyan, Stroke = Brushes.White, StrokeThickness = 1, IsHitTestVisible = false }; g.Label = new TextBlock { Foreground = Brushes.Yellow, FontSize = 13, FontWeight = FontWeights.Bold, IsHitTestVisible = false }; g.DotL1 = CreateMDot(Brushes.Lime); g.DotL2 = CreateMDot(Brushes.Lime); g.DotP = CreateMDot(Brushes.Red); foreach (UIElement el in new UIElement[] { g.MainLine, g.ExtLine, g.PerpLine, g.FootDot, g.Label, g.DotL1, g.DotL2, g.DotP }) _measureOverlay.Children.Add(el); SetDotPos(g.DotL1, l1); SetDotPos(g.DotL2, l2); SetDotPos(g.DotP, p); g.UpdateVisuals(FormatDistance(g.Distance)); 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, 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) { var dot = new Ellipse { Width = 12, Height = 12, Fill = fill, Stroke = Brushes.White, StrokeThickness = 1.5, Cursor = Cursors.Hand }; dot.SetValue(ContextMenuService.IsEnabledProperty, false); dot.MouseLeftButtonDown += MDot_Down; dot.MouseMove += MDot_Move; dot.MouseLeftButtonUp += MDot_Up; dot.PreviewMouseRightButtonUp += MDot_RightClick; return dot; } private static void SetDotPos(Ellipse dot, Point pos) { Canvas.SetLeft(dot, pos.X - dot.Width / 2); Canvas.SetTop(dot, pos.Y - dot.Height / 2); } private void MDot_Down(object sender, MouseButtonEventArgs e) { if (sender is not Ellipse dot) return; // 查找点点距组 foreach (var g in _ppGroups) { if (g.Dot1 == dot) { _mDraggingOwner = g; _mDraggingRole = "Dot1"; break; } if (g.Dot2 == dot) { _mDraggingOwner = g; _mDraggingRole = "Dot2"; break; } } // 查找点线距组 if (_mDraggingOwner == null) { foreach (var g in _ptlGroups) { if (g.DotL1 == dot) { _mDraggingOwner = g; _mDraggingRole = "DotL1"; break; } if (g.DotL2 == dot) { _mDraggingOwner = g; _mDraggingRole = "DotL2"; break; } 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) { 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; } } private void MDot_Move(object sender, MouseEventArgs e) { if (_mDraggingDot == null || _mDraggingOwner == null || _measureOverlay == null) return; if (e.LeftButton != MouseButtonState.Pressed) return; var pos = e.GetPosition(_measureOverlay); SetDotPos(_mDraggingDot, pos); if (_mDraggingOwner is Models.MeasureGroup ppg) { if (_mDraggingRole == "Dot1") ppg.P1 = pos; else ppg.P2 = pos; ppg.UpdateLine(); ppg.UpdateLabel(FormatDistance(ppg.Distance)); RaiseMeasureCompleted(ppg.P1, ppg.P2, ppg.Distance, MeasureCount, "PointDistance"); } else if (_mDraggingOwner is Models.PointToLineGroup ptlg) { if (_mDraggingRole == "DotL1") ptlg.L1 = pos; else if (_mDraggingRole == "DotL2") ptlg.L2 = pos; else if (_mDraggingRole == "DotP") ptlg.P = pos; ptlg.UpdateVisuals(FormatDistance(ptlg.Distance)); 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; } private void MDot_Up(object sender, MouseButtonEventArgs e) { if (_mDraggingDot != null) { _mDraggingDot.ReleaseMouseCapture(); _mDraggingDot = null; _mDraggingOwner = null; e.Handled = true; } } private void MDot_RightClick(object sender, MouseButtonEventArgs e) { if (sender is not Ellipse dot || _measureOverlay == null) return; // 点点距删除 foreach (var g in _ppGroups) { if (g.Dot1 == dot || g.Dot2 == dot) { 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; } } // 点线距删除 foreach (var g in _ptlGroups) { if (g.DotL1 == dot || g.DotL2 == dot || g.DotP == dot) { 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); } } private void SyncBubbleRoiVisuals() { if (_bubbleRoiRect == null || !_bubbleRoi.HasValue) return; var r = _bubbleRoi.Value; Canvas.SetLeft(_bubbleRoiRect, r.X); Canvas.SetTop(_bubbleRoiRect, r.Y); _bubbleRoiRect.Width = r.Width; _bubbleRoiRect.Height = r.Height; _bubbleRoiRect.Visibility = Visibility.Visible; if (_bubbleRoiHandle != null) { Canvas.SetLeft(_bubbleRoiHandle, r.Right - _bubbleRoiHandle.Width / 2); Canvas.SetTop(_bubbleRoiHandle, r.Bottom - _bubbleRoiHandle.Height / 2); _bubbleRoiHandle.Visibility = Visibility.Visible; } } private void InitBubbleMask() { if (!_bubbleRoi.HasValue) return; int w = (int)CanvasWidth, h = (int)CanvasHeight; if (w <= 0 || h <= 0) return; _bubbleMask = new System.Windows.Media.Imaging.WriteableBitmap(w, h, 96, 96, PixelFormats.Bgra32, null); if (_bubbleMaskImage == null) { EnsureMeasureOverlay(); _bubbleMaskImage = new Image { IsHitTestVisible = false, Opacity = 0.45, Stretch = Stretch.Fill }; _bubbleMaskImage.SetBinding(Image.WidthProperty, new System.Windows.Data.Binding("CanvasWidth") { Source = this }); _bubbleMaskImage.SetBinding(Image.HeightProperty, new System.Windows.Data.Binding("CanvasHeight") { Source = this }); _measureOverlay.Children.Add(_bubbleMaskImage); } _bubbleMaskImage.Source = _bubbleMask; } private void ApplyBrushAt(Point pos) { // 画笔/橡皮:下一步实现 } private void UpdateBubbleResult() { 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; RaiseMeasureCompleted(roi.TopLeft, roi.BottomRight, voidRate, 1, "BubbleVoid"); } /// 魔棒:在点击位置做 flood fill public void WandFloodFill(Point pos) { if (_bubbleMask == null || !_bubbleRoi.HasValue || ImageSource == null) return; 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; } /// 清除气泡测量所有状态 public void ClearBubbleMeasure() { if (_measureOverlay != null) { if (_bubbleRoiRect != null) { _measureOverlay.Children.Remove(_bubbleRoiRect); _bubbleRoiRect = null; } if (_bubbleRoiHandle != null) { _measureOverlay.Children.Remove(_bubbleRoiHandle); _bubbleRoiHandle = null; } if (_bubbleMaskImage != null) { _measureOverlay.Children.Remove(_bubbleMaskImage); _bubbleMaskImage = null; } } _bubbleRoi = null; _bubbleMask = null; _bubbleRoiStart = null; _bubbleRoiDragging = false; _bubbleRoiMoving = false; _bubbleRoiResizing = false; _bubbleBrushDragging = false; _bubbleTool = BubbleSubTool.Roi; } // 气泡工具切换事件 public static readonly RoutedEvent BubbleToolChangedEvent = EventManager.RegisterRoutedEvent(nameof(BubbleToolChanged), RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(PolygonRoiCanvas)); public event RoutedEventHandler BubbleToolChanged { add { AddHandler(BubbleToolChangedEvent, value); } remove { RemoveHandler(BubbleToolChangedEvent, value); } } private void RaiseBubbleToolChanged() { RaiseEvent(new RoutedEventArgs(BubbleToolChangedEvent)); } // ── 重编号 ── private void RenumberAll() { 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(); } // ── 事件 ── public static readonly RoutedEvent MeasureCompletedEvent = EventManager.RegisterRoutedEvent(nameof(MeasureCompleted), RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(PolygonRoiCanvas)); public event RoutedEventHandler MeasureCompleted { add { AddHandler(MeasureCompletedEvent, value); } remove { RemoveHandler(MeasureCompletedEvent, value); } } private void RaiseMeasureCompleted(Point p1, Point p2, double distance, int totalCount, string measureType) { RaiseEvent(new MeasureCompletedEventArgs(MeasureCompletedEvent, p1, p2, distance, totalCount) { MeasureType = measureType }); } public static readonly RoutedEvent MeasureStatusChangedEvent = EventManager.RegisterRoutedEvent(nameof(MeasureStatusChanged), RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(PolygonRoiCanvas)); public event RoutedEventHandler MeasureStatusChanged { add { AddHandler(MeasureStatusChangedEvent, value); } remove { RemoveHandler(MeasureStatusChangedEvent, value); } } private void RaiseMeasureStatusChanged(string message) { RaiseEvent(new MeasureStatusEventArgs(MeasureStatusChangedEvent, message)); } #endregion Measurement #region Adorner Management private void UpdateAdorner() { // 移除旧的Adorner if (currentAdorner != null) { var adornerLayer = AdornerLayer.GetAdornerLayer(mainCanvas); if (adornerLayer != null) { adornerLayer.Remove(currentAdorner); } currentAdorner = null; } // 为选中的ROI添加Adorner if (SelectedROI != null && SelectedROI.IsSelected) { // 查找对应的UI元素 var container = FindROIVisual(SelectedROI); if (container != null) { var adornerLayer = AdornerLayer.GetAdornerLayer(mainCanvas); if (adornerLayer != null) { double scaleFactor = 1.0 / ZoomScale; if (SelectedROI is PolygonROI polygonROI) { currentAdorner = new PolygonAdorner(container, scaleFactor, polygonROI); } if (currentAdorner != null) { adornerLayer.Add(currentAdorner); } } } } } private UIElement? FindROIVisual(ROIShape roi) { // 在ItemsControl中查找对应的视觉元素 var itemsControl = FindVisualChild(mainCanvas); if (itemsControl != null) { for (int i = 0; i < itemsControl.Items.Count; i++) { if (itemsControl.Items[i] == roi) { // 尝试获取容器 var container = itemsControl.ItemContainerGenerator.ContainerFromIndex(i) as ContentPresenter; // 如果容器还没生成,尝试强制生成 if (container == null) { // 强制生成容器 itemsControl.UpdateLayout(); container = itemsControl.ItemContainerGenerator.ContainerFromIndex(i) as ContentPresenter; } if (container != null) { // 查找实际的形状元素(只支持多边形) if (roi is PolygonROI) { return FindVisualChild(container); } } } } } return null; } private T? FindVisualChild(DependencyObject parent) where T : DependencyObject { for (int i = 0; i < VisualTreeHelper.GetChildrenCount(parent); i++) { var child = VisualTreeHelper.GetChild(parent, i); if (child is T result) { return result; } var childOfChild = FindVisualChild(child); if (childOfChild != null) { return childOfChild; } } return null; } #endregion Adorner Management #region Mouse Events private void Canvas_MouseWheel(object sender, MouseWheelEventArgs e) { double oldZoom = ZoomScale; double newZoom = e.Delta > 0 ? oldZoom * ZoomStep : oldZoom / ZoomStep; newZoom = Math.Max(0.1, Math.Min(10.0, newZoom)); if (Math.Abs(newZoom - oldZoom) > 0.001) { ZoomScale = newZoom; // RenderTransformOrigin="0.5,0.5" 保证以图像中心等比缩放 // 拖拽平移偏移保持不变 } e.Handled = true; } private void Canvas_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) { // 气泡测量模式:ROI 拖拽 / 画笔涂抹 if (CurrentMeasureMode == Models.MeasureMode.BubbleMeasure) { var pos = e.GetPosition(mainCanvas); if (_bubbleTool == BubbleSubTool.Roi) { // 检查是否点击了右下角手柄 if (_bubbleRoiHandle != null && _bubbleRoi.HasValue) { var hx = Canvas.GetLeft(_bubbleRoiHandle) + _bubbleRoiHandle.Width / 2; var hy = Canvas.GetTop(_bubbleRoiHandle) + _bubbleRoiHandle.Height / 2; if (Math.Abs(pos.X - hx) < 10 && Math.Abs(pos.Y - hy) < 10) { _bubbleRoiResizing = true; mainCanvas.CaptureMouse(); e.Handled = true; return; } } // 检查是否点击了 ROI 内部(拖动) if (_bubbleRoi.HasValue && _bubbleRoi.Value.Contains(pos)) { _bubbleRoiMoving = true; _bubbleRoiDragOffset = new Point(pos.X - _bubbleRoi.Value.X, pos.Y - _bubbleRoi.Value.Y); mainCanvas.CaptureMouse(); e.Handled = true; return; } // 没有 ROI 时才画新的,有 ROI 时点击外部不拦截(让图像正常拖动) if (!_bubbleRoi.HasValue) { _bubbleRoiStart = pos; _bubbleRoiDragging = true; EnsureBubbleRoiVisuals(); mainCanvas.CaptureMouse(); e.Handled = true; return; } // 有 ROI 但点击了外部 → 不拦截,走正常拖动逻辑 } if ((_bubbleTool == BubbleSubTool.Brush || _bubbleTool == BubbleSubTool.Eraser) && _bubbleRoi.HasValue) { _bubbleBrushDragging = true; ApplyBrushAt(pos); mainCanvas.CaptureMouse(); e.Handled = true; return; } // 魔棒在 MouseUp(CanvasClicked)中处理 } lastMousePosition = e.GetPosition(imageDisplayGrid); isDragging = false; mainCanvas.CaptureMouse(); } 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); Vector delta = currentPosition - lastMousePosition; if (delta.Length > 5) { isDragging = true; PanOffsetX += delta.X; PanOffsetY += delta.Y; lastMousePosition = currentPosition; } } } 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); } isDragging = false; } 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 if (sender is FrameworkElement element && element.DataContext is ROIShape roi) { SelectedROI = roi; e.Handled = true; } } #endregion Mouse Events #region Public Methods public void ResetView() { PanOffsetX = 0; PanOffsetY = 0; if (imageDisplayGrid == null || CanvasWidth <= 0 || CanvasHeight <= 0) { ZoomScale = 1.0; return; } // 延迟到布局完成后计算,确保 ActualWidth/Height 准确 Dispatcher.BeginInvoke(new Action(() => { double viewW = imageDisplayGrid.ActualWidth; double viewH = imageDisplayGrid.ActualHeight; if (viewW > 0 && viewH > 0) { ZoomScale = Math.Min(viewW / CanvasWidth, viewH / CanvasHeight); } else { ZoomScale = 1.0; } PanOffsetX = 0; PanOffsetY = 0; }), System.Windows.Threading.DispatcherPriority.Render); } private void BtnZoomIn_Click(object sender, RoutedEventArgs e) { double newZoom = ZoomScale * 1.2; if (newZoom <= 10.0) { ZoomScale = newZoom; PanOffsetX = 0; PanOffsetY = 0; } } private void BtnZoomOut_Click(object sender, RoutedEventArgs e) { double newZoom = ZoomScale / 1.2; if (newZoom >= 0.1) { ZoomScale = newZoom; PanOffsetX = 0; PanOffsetY = 0; } } private void BtnReset_Click(object sender, RoutedEventArgs e) { ResetView(); } #endregion Public Methods #region Events public static readonly RoutedEvent CanvasClickedEvent = EventManager.RegisterRoutedEvent(nameof(CanvasClicked), RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(PolygonRoiCanvas)); public event RoutedEventHandler CanvasClicked { add { AddHandler(CanvasClickedEvent, value); } remove { RemoveHandler(CanvasClickedEvent, value); } } protected virtual void OnCanvasClicked(Point position) { var args = new CanvasClickedEventArgs(CanvasClickedEvent, position); RaiseEvent(args); } public static readonly RoutedEvent RightClickEvent = EventManager.RegisterRoutedEvent(nameof(RightClick), RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(PolygonRoiCanvas)); public event RoutedEventHandler RightClick { add { AddHandler(RightClickEvent, value); } remove { RemoveHandler(RightClickEvent, value); } } protected virtual void OnRightClick() { RaiseEvent(new RoutedEventArgs(RightClickEvent)); } #endregion Events } }