Files
XplorePlane/XP.ImageProcessing.RoiControl/Controls/PolygonRoiCanvas.xaml.cs
T

2715 lines
114 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;
/// <summary>比例尺内侧竖向刻度线的 X 坐标(像素,相对尺身左端),仅画线不标数字。</summary>
public ObservableCollection<double> ScaleBarMinorTickXs { get; } = new();
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;
}
}
}
RefreshScaleBar();
}
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)
{
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();
}
// 图像切换时清除测量和叠加层,但保留 ROI(ROI 是用户标注,应跟随图像持续显示)
control.ClearMeasurements();
// 图像尺寸变化后刷新十字线
if (control.ShowCrosshair)
control.AddCrosshair();
control.RefreshScaleBar();
}
public static readonly DependencyProperty ROIItemsProperty =
DependencyProperty.Register(nameof(ROIItems), typeof(ObservableCollection<ROIShape>), typeof(PolygonRoiCanvas),
new PropertyMetadata(null));
public ObservableCollection<ROIShape>? ROIItems
{
get => (ObservableCollection<ROIShape>?)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, OnCanvasOrScaleBarParameterChanged));
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, OnCanvasOrScaleBarParameterChanged));
public double CanvasHeight
{
get => (double)GetValue(CanvasHeightProperty);
set => SetValue(CanvasHeightProperty, value);
}
/// <summary>是否在图像上显示比例尺(叠在图像坐标系内,随缩放与平移移动)。</summary>
public static readonly DependencyProperty ShowScaleBarProperty =
DependencyProperty.Register(nameof(ShowScaleBar), typeof(bool), typeof(PolygonRoiCanvas),
new PropertyMetadata(false, OnCanvasOrScaleBarParameterChanged));
public bool ShowScaleBar
{
get => (bool)GetValue(ShowScaleBarProperty);
set => SetValue(ShowScaleBarProperty, value);
}
/// <summary>单像素对应的物理长度(mm),用于比例尺刻度换算。</summary>
public static readonly DependencyProperty ScaleBarMmPerPixelProperty =
DependencyProperty.Register(nameof(ScaleBarMmPerPixel), typeof(double), typeof(PolygonRoiCanvas),
new PropertyMetadata(0.139, OnCanvasOrScaleBarParameterChanged));
public double ScaleBarMmPerPixel
{
get => (double)GetValue(ScaleBarMmPerPixelProperty);
set => SetValue(ScaleBarMmPerPixelProperty, value);
}
public static readonly DependencyProperty ScaleBarLengthPixelsProperty =
DependencyProperty.Register(nameof(ScaleBarLengthPixels), typeof(double), typeof(PolygonRoiCanvas),
new PropertyMetadata(100.0));
public double ScaleBarLengthPixels
{
get => (double)GetValue(ScaleBarLengthPixelsProperty);
private set => SetValue(ScaleBarLengthPixelsProperty, value);
}
public static readonly DependencyProperty ScaleBarCaptionProperty =
DependencyProperty.Register(nameof(ScaleBarCaption), typeof(string), typeof(PolygonRoiCanvas),
new PropertyMetadata(string.Empty));
public string ScaleBarCaption
{
get => (string)GetValue(ScaleBarCaptionProperty);
private set => SetValue(ScaleBarCaptionProperty, value);
}
private static void OnCanvasOrScaleBarParameterChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
((PolygonRoiCanvas)d).RefreshScaleBar();
}
/// <summary>按图像宽度选取 1–2–5 标度,使比例尺约占画布宽度的约 22%。</summary>
private void RefreshScaleBar()
{
const double targetFrac = 0.22;
const double maxFrac = 0.88;
const double minPx = 24.0;
if (!ShowScaleBar || CanvasWidth < 16 || ScaleBarMmPerPixel <= 0 || double.IsNaN(ScaleBarMmPerPixel))
{
ScaleBarLengthPixels = minPx;
ScaleBarCaption = string.Empty;
ScaleBarMinorTickXs.Clear();
return;
}
double maxPx = Math.Max(minPx, CanvasWidth * maxFrac);
double idealPx = Math.Min(CanvasWidth * targetFrac, maxPx);
double idealMm = idealPx * ScaleBarMmPerPixel;
double niceMm = RoundToNiceLengthMm(idealMm);
double barPx = niceMm / ScaleBarMmPerPixel;
while (barPx > maxPx && niceMm > ScaleBarMmPerPixel * minPx * 1.5)
{
niceMm /= 2.0;
barPx = niceMm / ScaleBarMmPerPixel;
}
while (barPx < minPx && niceMm < idealMm * 200)
{
niceMm *= 2.0;
barPx = niceMm / ScaleBarMmPerPixel;
}
if (barPx < minPx)
barPx = minPx;
ScaleBarLengthPixels = barPx;
ScaleBarCaption = FormatScaleBarCaptionMm(barPx * ScaleBarMmPerPixel);
ScaleBarMinorTickXs.Clear();
int divisions = barPx >= 120 ? 10 : barPx >= 60 ? 6 : 4;
for (int i = 1; i < divisions; i++)
ScaleBarMinorTickXs.Add(barPx * i / divisions);
}
private static double RoundToNiceLengthMm(double mm)
{
if (mm <= 0 || double.IsNaN(mm) || double.IsInfinity(mm))
return 0.1;
var magnitude = Math.Pow(10.0, Math.Floor(Math.Log10(mm)));
var normalized = mm / magnitude;
var nice = normalized < 1.5 ? 1.0 : normalized < 3.5 ? 2.0 : normalized < 7.5 ? 5.0 : 10.0;
return nice * magnitude;
}
private static string FormatScaleBarCaptionMm(double mm)
{
if (mm >= 100) return $"{mm:F0} mm";
if (mm >= 10) return $"{mm:F1} mm";
if (mm >= 1) return $"{mm:F2} mm";
return $"{mm:F3} mm";
}
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));
/// <summary>每像素对应的物理尺寸(= 探测器像素尺寸 / 放大倍率),默认 1.0</summary>
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"));
/// <summary>测量单位,默认 "px",可设为 "mm"/"μm"/"cm"</summary>
public string MeasureUnit
{
get => (string)GetValue(MeasureUnitProperty);
set => SetValue(MeasureUnitProperty, value);
}
/// <summary>将像素距离转换为物理距离文本</summary>
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);
}
// ── 光标信息(像素坐标 + 灰度值)──
public static readonly DependencyProperty CursorInfoProperty =
DependencyProperty.Register(nameof(CursorInfo), typeof(string), typeof(PolygonRoiCanvas),
new PropertyMetadata("X: -- Y: -- Gray: --"));
/// <summary>鼠标在图像上的像素坐标和灰度值,格式: "X: 123 Y: 456 Gray: 128"</summary>
public string CursorInfo
{
get => (string)GetValue(CursorInfoProperty);
set => SetValue(CursorInfoProperty, 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;
double thickness = Math.Max(1, Math.Round(Math.Max(w, h) / 1000.0));
_crosshairH = new Line { X1 = 0, Y1 = h / 2, X2 = w, Y2 = h / 2, Stroke = Brushes.Red, StrokeThickness = thickness, Opacity = 0.7, IsHitTestVisible = false };
_crosshairV = new Line { X1 = w / 2, Y1 = 0, X2 = w / 2, Y2 = h, Stroke = Brushes.Red, StrokeThickness = thickness, 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
/// <summary>
/// 根据画布尺寸计算自适应线宽
/// </summary>
private double GetAdaptiveThickness(double baseThickness = 1.0)
{
double maxDim = Math.Max(CanvasWidth, CanvasHeight);
double scale = Math.Max(1, Math.Round(maxDim / 1000.0));
return baseThickness * scale;
}
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<Models.MeasureGroup> _ppGroups = new();
private readonly System.Collections.Generic.List<Models.PointToLineGroup> _ptlGroups = new();
private readonly System.Collections.Generic.List<Models.AngleGroup> _angleGroups = new();
private readonly System.Collections.Generic.List<Models.FillRateGroup> _frGroups = new();
private readonly System.Collections.Generic.List<Models.BgaVoidGroup> _bgaGroups = new();
// 点点距临时状态
private Ellipse _pendingDot;
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;
// 边缘查找拟合直线临时状态
private int _elfClickCount;
private Ellipse _elfTempDot1;
private Line _elfTempLine;
private Point? _elfTempStart;
// 气泡测量状态
public enum BubbleSubTool { Roi, RoiCircle, RoiPolygon, 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; // 右下角调整大小
// 圆形 ROI
private Ellipse _bubbleCircleShape;
private Point? _bubbleCircleCenter;
private double _bubbleCircleRadius;
private bool _bubbleCircleDragging;
private bool _bubbleCircleMoving; // 拖动整个圆形 ROI
private bool _bubbleCircleResizing; // 调整圆形 ROI 大小
private Point _bubbleCircleDragOffset;
// 多边形 ROI
private Polygon _bubblePolyShape;
private System.Collections.ObjectModel.ObservableCollection<Point> _bubblePolyPoints = new();
private Point _bubbleRoiDragOffset;
private Image _bubbleMaskImage;
private System.Windows.Media.Imaging.WriteableBitmap _bubbleMask;
private int _bubbleThreshold = 128;
private int _bubbleBrushSize = 5;
private double _bubbleVoidLimit = 25.0;
private bool _bubbleBrushDragging;
private readonly System.Collections.Generic.Stack<byte[]> _bubbleUndoStack = new();
public void SetBubbleTool(BubbleSubTool tool)
{
_bubbleTool = tool;
// 根据工具设置光标
UpdateBubbleToolCursor();
}
private void UpdateBubbleToolCursor()
{
if (mainCanvas == null) return;
if (CurrentMeasureMode != Models.MeasureMode.BubbleMeasure)
{
mainCanvas.Cursor = Cursors.Arrow;
return;
}
mainCanvas.Cursor = _bubbleTool switch
{
BubbleSubTool.Wand => Cursors.Cross,
BubbleSubTool.Brush => Cursors.Pen,
BubbleSubTool.Eraser => Cursors.No,
_ => Cursors.Arrow
};
}
public void SetBubbleThreshold(int val) => _bubbleThreshold = val;
public void SetBubbleBrushSize(int val) => _bubbleBrushSize = val;
public void SetBubbleVoidLimit(double val) { _bubbleVoidLimit = val; UpdateBubbleResult(); }
public void SetBubblePolyPoints(System.Collections.Generic.IList<Point> points)
{
_bubblePolyPoints.Clear();
foreach (var p in points) _bubblePolyPoints.Add(p);
if (_bubblePolyPoints.Count >= 3) InitBubbleMask();
}
public Rect? BubbleRoi => _bubbleRoi;
/// <summary>设置 BGA 测量的气泡/焊球绘制模式</summary>
public void SetBgaDrawBall(bool drawBall)
{
_bgaDrawBall = drawBall;
RaiseMeasureStatusChanged(drawBall ? "BGA - 画焊球模式" : "BGA - 画气泡模式");
}
/// <summary>设置所有 BGA 组的 VoidLimit 并刷新标签</summary>
public void SetBgaVoidLimit(double limit)
{
foreach (var g in _bgaGroups)
{
g.VoidLimit = limit;
g.UpdateLabel();
}
}
// 拖拽状态
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;
// 清理气泡测量状态
_bubbleRoiRect = null; _bubbleRoiHandle = null; _bubbleResultLabel = null;
_bubbleMaskImage = null; _bubbleMask = null;
_bubbleRoi = null; _bubbleRoiStart = null;
_bubbleRoiDragging = false; _bubbleRoiMoving = false;
_bubbleRoiResizing = false; _bubbleBrushDragging = false;
_bubbleCircleDragging = false; _bubbleCircleMoving = false; _bubbleCircleResizing = false;
_bubbleTool = BubbleSubTool.Roi;
_bubbleUndoStack.Clear();
// 清理外部叠加的结果图层(IsHitTestVisible=false 的 Image,排除背景图)
var toRemove = new System.Collections.Generic.List<UIElement>();
foreach (UIElement child in mainCanvas.Children)
{
if (child is Image img && img != backgroundImage && !img.IsHitTestVisible)
toRemove.Add(child);
}
foreach (var el in toRemove)
mainCanvas.Children.Remove(el);
}
public void ClearMeasurements() => RemoveMeasureOverlay();
/// <summary>从 mainCanvas 移除指定的 UI 元素(用于外部叠加层清理)</summary>
public void RemoveFromCanvas(System.Windows.UIElement element)
{
if (element != null && mainCanvas.Children.Contains(element))
mainCanvas.Children.Remove(element);
}
// ── 检测结果叠加层 ──
private Canvas _detectionOverlay;
/// <summary>
/// 设置检测结果叠加层 Canvas(由外部构建好后传入)。
/// </summary>
public void SetDetectionOverlayCanvas(Canvas overlayCanvas)
{
ClearDetectionOverlay();
if (overlayCanvas == null) return;
_detectionOverlay = overlayCanvas;
_detectionOverlay.IsHitTestVisible = false;
_detectionOverlay.SetBinding(Canvas.WidthProperty, new System.Windows.Data.Binding("CanvasWidth") { Source = this });
_detectionOverlay.SetBinding(Canvas.HeightProperty, new System.Windows.Data.Binding("CanvasHeight") { Source = this });
// 插入到 backgroundImage 之后(索引1),在 ROI 和测量层之下
int insertIndex = System.Math.Min(1, mainCanvas.Children.Count);
mainCanvas.Children.Insert(insertIndex, _detectionOverlay);
}
/// <summary>清除检测结果叠加层</summary>
public void ClearDetectionOverlay()
{
if (_detectionOverlay != null)
{
mainCanvas.Children.Remove(_detectionOverlay);
_detectionOverlay = null;
}
}
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);
else if (CurrentMeasureMode == Models.MeasureMode.EdgeLineFit)
HandleEdgeLineFitClick(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 = GetAdaptiveThickness(), IsHitTestVisible = false };
g.Label = new TextBlock { Foreground = Brushes.Yellow, FontSize = 13, FontWeight = FontWeights.Bold, IsHitTestVisible = false };
// 使用垂直线段代替圆点
g.PerpLine1 = new Line { Stroke = Brushes.Yellow, StrokeThickness = GetAdaptiveThickness(), IsHitTestVisible = true, Cursor = Cursors.Hand };
g.PerpLine2 = new Line { Stroke = Brushes.Yellow, StrokeThickness = GetAdaptiveThickness(), IsHitTestVisible = true, Cursor = Cursors.Hand };
// 保留圆点以兼容拖拽逻辑(但设为不可见)
g.Dot1 = CreateMDot(Brushes.Red);
g.Dot2 = CreateMDot(Brushes.Blue);
g.Dot1.Visibility = Visibility.Collapsed;
g.Dot2.Visibility = Visibility.Collapsed;
foreach (UIElement el in new UIElement[] { g.Line, g.Label, g.PerpLine1, g.PerpLine2, g.Dot1, g.Dot2 })
_measureOverlay.Children.Add(el);
// 设置圆点位置(用于拖拽计算)
SetDotPos(g.Dot1, p1); SetDotPos(g.Dot2, p2);
// 垂直线段1的拖拽处理 - 直接设置拖拽状态
g.PerpLine1.MouseLeftButtonDown += (s, e) =>
{
_mDraggingOwner = g;
_mDraggingRole = "Dot1";
_mDraggingDot = g.Dot1;
g.PerpLine1.CaptureMouse();
e.Handled = true;
};
g.PerpLine1.MouseMove += (s, e) =>
{
if (_mDraggingOwner != g || _mDraggingRole != "Dot1" || _measureOverlay == null) return;
if (e.LeftButton != MouseButtonState.Pressed) return;
var pos = e.GetPosition(_measureOverlay);
SetDotPos(g.Dot1, pos);
g.P1 = pos;
g.UpdateLine();
g.UpdateLabel(FormatDistance(g.Distance));
RaiseMeasureCompleted(g.P1, g.P2, g.Distance, MeasureCount, "PointDistance");
};
g.PerpLine1.MouseLeftButtonUp += (s, e) =>
{
if (_mDraggingOwner == g && _mDraggingRole == "Dot1")
{
_mDraggingOwner = null;
_mDraggingRole = null;
_mDraggingDot = null;
g.PerpLine1.ReleaseMouseCapture();
}
e.Handled = true;
};
// 垂直线段2的拖拽处理
g.PerpLine2.MouseLeftButtonDown += (s, e) =>
{
_mDraggingOwner = g;
_mDraggingRole = "Dot2";
_mDraggingDot = g.Dot2;
g.PerpLine2.CaptureMouse();
e.Handled = true;
};
g.PerpLine2.MouseMove += (s, e) =>
{
if (_mDraggingOwner != g || _mDraggingRole != "Dot2" || _measureOverlay == null) return;
if (e.LeftButton != MouseButtonState.Pressed) return;
var pos = e.GetPosition(_measureOverlay);
SetDotPos(g.Dot2, pos);
g.P2 = pos;
g.UpdateLine();
g.UpdateLabel(FormatDistance(g.Distance));
RaiseMeasureCompleted(g.P1, g.P2, g.Distance, MeasureCount, "PointDistance");
};
g.PerpLine2.MouseLeftButtonUp += (s, e) =>
{
if (_mDraggingOwner == g && _mDraggingRole == "Dot2")
{
_mDraggingOwner = null;
_mDraggingRole = null;
_mDraggingDot = null;
g.PerpLine2.ReleaseMouseCapture();
}
e.Handled = true;
};
g.UpdateLine(); g.UpdateLabel(FormatDistance(g.Distance));
return g;
}
// ── 点线距 ──
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 = GetAdaptiveThickness(), 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 = GetAdaptiveThickness(), IsHitTestVisible = false };
g.ExtLine = new Line { Stroke = Brushes.Lime, StrokeThickness = GetAdaptiveThickness(), IsHitTestVisible = false,
StrokeDashArray = new DoubleCollection { 4, 2 }, Visibility = Visibility.Collapsed };
g.PerpLine = new Line { Stroke = Brushes.Yellow, StrokeThickness = GetAdaptiveThickness(), IsHitTestVisible = false,
StrokeDashArray = new DoubleCollection { 4, 2 } };
g.FootDot = new Ellipse { Width = 2, Height = 2, Fill = Brushes.Cyan, Stroke = Brushes.White, StrokeThickness = 0.5, IsHitTestVisible = false };
g.Label = new TextBlock { Foreground = Brushes.Yellow, FontSize = 13, FontWeight = FontWeights.Bold, IsHitTestVisible = false };
g.DotL1 = CreateMDot(Brushes.Lime);
g.DotL2 = CreateMDot(Brushes.Lime);
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 HandleEdgeLineFitClick(Point pos)
{
_elfClickCount++;
if (_elfClickCount == 1)
{
_elfTempStart = pos;
_elfTempDot1 = CreateMDot(Brushes.Cyan);
_measureOverlay.Children.Add(_elfTempDot1);
SetDotPos(_elfTempDot1, pos);
RaiseMeasureStatusChanged($"直线拟合 - 搜索线起点: ({pos.X:F0}, {pos.Y:F0}),请点击搜索线终点");
}
else if (_elfClickCount == 2)
{
// 绘制搜索线
_elfTempLine = new Line
{
Stroke = Brushes.Cyan,
StrokeThickness = GetAdaptiveThickness(),
StrokeDashArray = new DoubleCollection { 4, 2 },
IsHitTestVisible = false,
X1 = _elfTempStart.Value.X,
Y1 = _elfTempStart.Value.Y,
X2 = pos.X,
Y2 = pos.Y
};
_measureOverlay.Children.Add(_elfTempLine);
// 触发完成事件,传递搜索线起止点
RaiseMeasureCompleted(_elfTempStart.Value, pos, 0, MeasureCount, "EdgeLineFit");
RaiseMeasureStatusChanged($"直线拟合 - 搜索线已定义: ({_elfTempStart.Value.X:F0},{_elfTempStart.Value.Y:F0}) → ({pos.X:F0},{pos.Y:F0})");
// 清理临时状态
if (_elfTempDot1 != null) _measureOverlay.Children.Remove(_elfTempDot1);
_elfTempDot1 = null;
_elfTempStart = null;
_elfClickCount = 0;
CurrentMeasureMode = Models.MeasureMode.None;
}
}
// ── 角度测量 ──
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 = GetAdaptiveThickness(), 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 = GetAdaptiveThickness(), IsHitTestVisible = false };
g.LineB = new Line { Stroke = Brushes.Lime, StrokeThickness = GetAdaptiveThickness(), IsHitTestVisible = false };
g.Arc = new Path { Stroke = Brushes.Yellow, StrokeThickness = GetAdaptiveThickness(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 = GetAdaptiveThickness(), IsHitTestVisible = false };
g.FillLine = new Line { Stroke = Brushes.Lime, StrokeThickness = GetAdaptiveThickness(), 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 = 2, Height = 2, Fill = fill, Stroke = Brushes.White, StrokeThickness = 0.5, Cursor = Cursors.SizeAll };
h.SetValue(ContextMenuService.IsEnabledProperty, false);
h.MouseLeftButtonDown += MDot_Down;
h.MouseMove += MDot_Move;
h.MouseLeftButtonUp += MDot_Up;
h.PreviewMouseRightButtonUp += MDot_RightClick;
return h;
}
private Path CreateEllipsePath(Brush stroke, bool dashed)
{
var p = new Path { Stroke = stroke, StrokeThickness = GetAdaptiveThickness(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;
}
}
/// <summary>右键切换气泡/焊球模式(在 BGA 测量模式下)</summary>
private void HandleBgaRightClick()
{
if (CurrentMeasureMode != Models.MeasureMode.BgaVoid || _bgaCurrent == null) return;
_bgaDrawBall = !_bgaDrawBall;
RaiseMeasureStatusChanged(_bgaDrawBall ? "BGA - 已切换为画焊球模式" : "BGA - 已切换为画气泡模式");
}
private Models.BgaCircle CreateBgaCircle(Point center, double radius, bool isBall)
{
var c = new Models.BgaCircle { Center = center, Radius = radius, IsBall = isBall };
c.Shape = new Ellipse
{
Stroke = isBall ? Brushes.Lime : Brushes.Orange,
StrokeThickness = isBall ? GetAdaptiveThickness(2) : GetAdaptiveThickness(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 = 2; c.EdgeDot.Height = 2;
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 = 2, Height = 2, Fill = fill, Stroke = Brushes.White, StrokeThickness = 0.5, Cursor = Cursors.Hand };
dot.SetValue(ContextMenuService.IsEnabledProperty, false);
dot.MouseLeftButtonDown += MDot_Down;
dot.MouseMove += MDot_Move;
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)
{
var allBga = new System.Collections.Generic.List<Models.BgaVoidGroup>(_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; }
}
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;
}
}
}
// ── 气泡测量辅助 ──
/// <summary>是否已有任何形状的气泡 ROI</summary>
private bool HasBubbleRoi =>
_bubbleRoi.HasValue || _bubbleCircleCenter.HasValue || _bubblePolyPoints.Count >= 3;
/// <summary>判断点是否在当前气泡 ROI 内</summary>
private bool IsInBubbleRoi(Point pos)
{
if (_bubbleRoi.HasValue)
return _bubbleRoi.Value.Contains(pos);
if (_bubbleCircleCenter.HasValue)
{
double dx = pos.X - _bubbleCircleCenter.Value.X;
double dy = pos.Y - _bubbleCircleCenter.Value.Y;
return dx * dx + dy * dy <= _bubbleCircleRadius * _bubbleCircleRadius;
}
if (_bubblePolyPoints.Count >= 3)
return IsPointInPolygon(pos, _bubblePolyPoints);
return false;
}
private static bool IsPointInPolygon(Point p, System.Collections.Generic.IList<Point> polygon)
{
bool inside = false;
int n = polygon.Count;
for (int i = 0, j = n - 1; i < n; j = i++)
{
if ((polygon[i].Y > p.Y) != (polygon[j].Y > p.Y) &&
p.X < (polygon[j].X - polygon[i].X) * (p.Y - polygon[i].Y) / (polygon[j].Y - polygon[i].Y) + polygon[i].X)
inside = !inside;
}
return inside;
}
/// <summary>获取当前 ROI 的外接矩形(用于掩码计算范围)</summary>
private Rect GetBubbleRoiBounds()
{
if (_bubbleRoi.HasValue) return _bubbleRoi.Value;
if (_bubbleCircleCenter.HasValue)
{
var c = _bubbleCircleCenter.Value;
var r = _bubbleCircleRadius;
return new Rect(c.X - r, c.Y - r, r * 2, r * 2);
}
if (_bubblePolyPoints.Count >= 3)
{
double minX = double.MaxValue, minY = double.MaxValue, maxX = double.MinValue, maxY = double.MinValue;
foreach (var pt in _bubblePolyPoints)
{
if (pt.X < minX) minX = pt.X;
if (pt.Y < minY) minY = pt.Y;
if (pt.X > maxX) maxX = pt.X;
if (pt.Y > maxY) maxY = pt.Y;
}
return new Rect(minX, minY, maxX - minX, maxY - minY);
}
return Rect.Empty;
}
private void EnsureBubbleRoiVisuals()
{
EnsureMeasureOverlay();
if (_bubbleRoiRect == null)
{
_bubbleRoiRect = new Rectangle
{
Stroke = Brushes.Red,
StrokeThickness = GetAdaptiveThickness(),
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 EnsureBubbleCircleVisuals()
{
EnsureMeasureOverlay();
if (_bubbleCircleShape == null)
{
_bubbleCircleShape = new Ellipse
{
Stroke = Brushes.Red,
StrokeThickness = GetAdaptiveThickness(),
Fill = Brushes.Transparent,
Visibility = Visibility.Collapsed,
IsHitTestVisible = false
};
_measureOverlay.Children.Add(_bubbleCircleShape);
}
}
private void UpdateBubbleCircleVisuals()
{
if (_bubbleCircleShape == null || !_bubbleCircleCenter.HasValue) return;
var c = _bubbleCircleCenter.Value;
var r = _bubbleCircleRadius;
_bubbleCircleShape.Width = r * 2;
_bubbleCircleShape.Height = r * 2;
Canvas.SetLeft(_bubbleCircleShape, c.X - r);
Canvas.SetTop(_bubbleCircleShape, c.Y - r);
_bubbleCircleShape.Visibility = Visibility.Visible;
}
private void UpdateBubblePolyVisuals()
{
EnsureMeasureOverlay();
if (_bubblePolyShape == null)
{
_bubblePolyShape = new Polygon
{
Stroke = Brushes.Red,
StrokeThickness = GetAdaptiveThickness(),
Fill = Brushes.Transparent,
IsHitTestVisible = false
};
_measureOverlay.Children.Add(_bubblePolyShape);
}
_bubblePolyShape.Points = new PointCollection(_bubblePolyPoints);
}
private void InitBubbleMask()
{
if (!HasBubbleRoi) 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 || !HasBubbleRoi) return;
var roi = GetBubbleRoiBounds();
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;
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;
// 检查是否在 ROI 内
if (!IsInBubbleRoi(new Point(px2, py2))) 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 || !HasBubbleRoi) return;
var roi = GetBubbleRoiBounds();
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++)
{
if (!IsInBubbleRoi(new Point(x, y))) continue;
roiArea++;
int idx = (y * w + x) * 4;
if (pixels[idx + 3] > 0) voidArea++;
}
double voidRate = roiArea > 0 ? voidArea * 100.0 / roiArea : 0;
if (_bubbleResultLabel != null)
{
string cls = voidRate <= _bubbleVoidLimit ? "PASS" : "FAIL";
_bubbleResultLabel.Text = $"Void: {voidRate:F1}% | Limit: {_bubbleVoidLimit: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");
}
/// <summary>魔棒:在点击位置做 flood fill</summary>
public void WandFloodFill(Point pos)
{
if (_bubbleMask == null || !HasBubbleRoi || ImageSource == null) return;
// 保存快照用于撤销
SaveMaskSnapshot();
if (!IsInBubbleRoi(pos)) return;
var roi = GetBubbleRoiBounds();
int px = (int)pos.X, py = (int)pos.Y;
// 获取灰度像素
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;
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;
if (!IsInBubbleRoi(new Point(cx, cy))) continue;
filled.Add((cx, cy));
int[] dx = { -1, 1, 0, 0 }, dy = { 0, 0, -1, 1 };
for (int d = 0; d < 4; d++)
{
int nx = cx + dx[d], ny = cy + dy[d];
if (nx >= 0 && nx < w && ny >= 0 && ny < h && !visited[ny * w + nx] && IsInBubbleRoi(new Point(nx, ny)))
{
visited[ny * w + nx] = true;
queue.Enqueue((nx, ny));
}
}
}
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();
}
/// <summary>从 ImageSource 提取灰度像素数组</summary>
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);
}
/// <summary>撤销上一次魔棒/画笔操作</summary>
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();
}
/// <summary>清除气泡测量所有状态</summary>
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; }
if (_bubbleCircleShape != null) { _measureOverlay.Children.Remove(_bubbleCircleShape); _bubbleCircleShape = null; }
if (_bubblePolyShape != null) { _measureOverlay.Children.Remove(_bubblePolyShape); _bubblePolyShape = null; }
}
_bubbleRoi = null;
_bubbleCircleCenter = null;
_bubbleCircleRadius = 0;
_bubbleCircleDragging = false;
_bubbleCircleMoving = false;
_bubbleCircleResizing = false;
_bubblePolyPoints.Clear();
_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();
}
// ── 事件 ──
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<ItemsControl>(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<Polygon>(container);
}
}
}
}
}
return null;
}
private T? FindVisualChild<T>(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<T>(child);
if (childOfChild != null)
{
return childOfChild;
}
}
return null;
}
#endregion Adorner Management
#region Mouse Events
private void UpdateCursorInfo(Point pos)
{
if (ImageSource == null)
{
CursorInfo = "X: -- Y: -- Gray: --";
return;
}
int px = (int)pos.X, py = (int)pos.Y;
string grayText = "--";
if (ImageSource is BitmapSource bmp && px >= 0 && py >= 0 && px < bmp.PixelWidth && py < bmp.PixelHeight)
{
try
{
var pixel = new byte[4];
var converted = new FormatConvertedBitmap(bmp, PixelFormats.Bgra32, null, 0);
converted.CopyPixels(new System.Windows.Int32Rect(px, py, 1, 1), pixel, 4, 0);
int gray = (int)(pixel[2] * 0.299 + pixel[1] * 0.587 + pixel[0] * 0.114);
grayText = gray.ToString();
}
catch { }
}
CursorInfo = $"X: {px} Y: {py} Gray: {grayText}";
}
private void Canvas_MouseLeave(object sender, MouseEventArgs e)
{
CursorInfo = "X: -- Y: -- Gray: --";
}
private void Canvas_MouseWheel(object sender, MouseWheelEventArgs e)
{
double oldZoom = ZoomScale;
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 时才画新的
if (!_bubbleRoi.HasValue)
{
_bubbleRoiStart = pos;
_bubbleRoiDragging = true;
EnsureBubbleRoiVisuals();
mainCanvas.CaptureMouse();
e.Handled = true;
return;
}
}
if (_bubbleTool == BubbleSubTool.RoiCircle)
{
// 检查是否点击了圆形 ROI
if (_bubbleCircleCenter.HasValue)
{
double dx = pos.X - _bubbleCircleCenter.Value.X;
double dy = pos.Y - _bubbleCircleCenter.Value.Y;
double dist = Math.Sqrt(dx * dx + dy * dy);
// 先检查是否点击圆内部(拖动),排除边缘附近区域
if (dist < _bubbleCircleRadius - 10)
{
_bubbleCircleMoving = true;
_bubbleCircleDragOffset = new Point(pos.X - _bubbleCircleCenter.Value.X, pos.Y - _bubbleCircleCenter.Value.Y);
mainCanvas.CaptureMouse();
e.Handled = true;
return;
}
// 再检查是否点击边缘附近(调整大小)
if (Math.Abs(dist - _bubbleCircleRadius) < 15)
{
_bubbleCircleResizing = true;
_bubbleRoiStart = pos;
mainCanvas.CaptureMouse();
e.Handled = true;
return;
}
}
// 没有圆形 ROI 时开始画
if (!_bubbleCircleCenter.HasValue)
{
_bubbleRoiStart = pos;
_bubbleCircleDragging = true;
EnsureBubbleCircleVisuals();
mainCanvas.CaptureMouse();
e.Handled = true;
return;
}
}
if (_bubbleTool == BubbleSubTool.RoiPolygon)
{
// 多边形 ROI 由外部面板通过 CanvasClickedEvent 处理,这里不拦截
}
if ((_bubbleTool == BubbleSubTool.Brush || _bubbleTool == BubbleSubTool.Eraser) && HasBubbleRoi)
{
if (IsInBubbleRoi(pos))
{
SaveMaskSnapshot();
_bubbleBrushDragging = true;
ApplyBrushAt(pos);
mainCanvas.CaptureMouse();
e.Handled = true;
return;
}
}
// 魔棒在 MouseUpCanvasClicked)中处理
}
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;
}
// 气泡测量:圆形 ROI 拖拽
if (_bubbleCircleDragging && _bubbleRoiStart.HasValue)
{
var pos = e.GetPosition(mainCanvas);
double dx = pos.X - _bubbleRoiStart.Value.X;
double dy = pos.Y - _bubbleRoiStart.Value.Y;
double r = Math.Sqrt(dx * dx + dy * dy);
if (_bubbleCircleShape != null)
{
_bubbleCircleShape.Width = r * 2;
_bubbleCircleShape.Height = r * 2;
Canvas.SetLeft(_bubbleCircleShape, _bubbleRoiStart.Value.X - r);
Canvas.SetTop(_bubbleCircleShape, _bubbleRoiStart.Value.Y - r);
_bubbleCircleShape.Visibility = Visibility.Visible;
}
e.Handled = true;
return;
}
// 气泡测量:圆形 ROI 拖动
if (_bubbleCircleMoving && _bubbleCircleCenter.HasValue)
{
var pos = e.GetPosition(mainCanvas);
_bubbleCircleCenter = new Point(pos.X - _bubbleCircleDragOffset.X, pos.Y - _bubbleCircleDragOffset.Y);
UpdateBubbleCircleVisuals();
e.Handled = true;
return;
}
// 气泡测量:圆形 ROI 调整大小
if (_bubbleCircleResizing && _bubbleCircleCenter.HasValue)
{
var pos = e.GetPosition(mainCanvas);
double dx = pos.X - _bubbleCircleCenter.Value.X;
double dy = pos.Y - _bubbleCircleCenter.Value.Y;
_bubbleCircleRadius = Math.Max(5, Math.Sqrt(dx * dx + dy * dy));
UpdateBubbleCircleVisuals();
InitBubbleMask();
e.Handled = true;
return;
}
// 气泡测量:画笔/橡皮拖拽
if (_bubbleBrushDragging && HasBubbleRoi)
{
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;
}
}
// 更新光标信息(像素坐标 + 灰度值)
UpdateCursorInfo(e.GetPosition(mainCanvas));
}
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;
}
// 气泡测量:圆形 ROI 拖拽完成
if (_bubbleCircleDragging && _bubbleRoiStart.HasValue)
{
var pos = e.GetPosition(mainCanvas);
double dx = pos.X - _bubbleRoiStart.Value.X;
double dy = pos.Y - _bubbleRoiStart.Value.Y;
double r = Math.Sqrt(dx * dx + dy * dy);
_bubbleCircleDragging = false;
mainCanvas.ReleaseMouseCapture();
if (r > 5)
{
_bubbleCircleCenter = _bubbleRoiStart.Value;
_bubbleCircleRadius = r;
_bubbleRoiStart = null;
InitBubbleMask();
RaiseMeasureStatusChanged($"圆形ROI 已设置: 半径={r:F0}px");
}
e.Handled = true;
return;
}
// 气泡测量:圆形 ROI 拖动完成
if (_bubbleCircleMoving)
{
_bubbleCircleMoving = false;
mainCanvas.ReleaseMouseCapture();
InitBubbleMask();
e.Handled = true;
return;
}
// 气泡测量:圆形 ROI 调整大小完成
if (_bubbleCircleResizing)
{
_bubbleCircleResizing = false;
_bubbleRoiStart = null;
mainCanvas.ReleaseMouseCapture();
InitBubbleMask();
RaiseMeasureStatusChanged($"圆形ROI 已调整: 半径={_bubbleCircleRadius:F0}px");
e.Handled = true;
return;
}
// 气泡测量:画笔/橡皮松开
if (_bubbleBrushDragging)
{
_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 && HasBubbleRoi)
{
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)
{
SuppressContextMenu = true;
e.Handled = true;
}
// 外部请求抑制右键菜单
if (SuppressContextMenu)
{
e.Handled = true;
}
// ROI 选中状态下,右键点击顶点附近时删除顶点并阻止菜单
if (SelectedROI is PolygonROI poly && poly.IsSelected && poly.IsEditable && poly.Points.Count > 3)
{
var pos = e.GetPosition(mainCanvas);
double threshold = 15;
int nearestIndex = -1;
double nearestDist = double.MaxValue;
for (int i = 0; i < poly.Points.Count; i++)
{
var pt = poly.Points[i];
double dx = pt.X - pos.X, dy = pt.Y - pos.Y;
double dist = Math.Sqrt(dx * dx + dy * dy);
if (dist < nearestDist) { nearestDist = dist; nearestIndex = i; }
}
if (nearestIndex >= 0 && nearestDist < threshold)
{
poly.Points.RemoveAt(nearestIndex);
SuppressContextMenu = true;
e.Handled = true;
}
}
}
/// <summary>设置为 true 可抑制下一次右键菜单弹出,由外部(如 ViewportPanelView)在 ContextMenuOpening 中检查</summary>
public bool SuppressContextMenu { get; set; }
private void ROI_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
// 选择ROI(仅可编辑时)
if (sender is FrameworkElement element && element.DataContext is ROIShape roi && roi.IsEditable)
{
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
}
}