2715 lines
114 KiB
C#
2715 lines
114 KiB
C#
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;
|
||
}
|
||
}
|
||
// 魔棒在 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;
|
||
}
|
||
// 气泡测量:圆形 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
|
||
}
|
||
} |