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

2036 lines
85 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using XP.ImageProcessing.RoiControl.Models;
using System;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;
namespace XP.ImageProcessing.RoiControl.Controls
{
public partial class PolygonRoiCanvas : UserControl
{
private bool isDragging = false;
private Point lastMousePosition;
private const double ZoomStep = 1.2;
private Adorner? currentAdorner;
public PolygonRoiCanvas()
{
InitializeComponent();
Loaded += PolygonRoiCanvas_Loaded;
}
private void PolygonRoiCanvas_Loaded(object sender, RoutedEventArgs e)
{
// 监听ROI集合变化
if (ROIItems != null)
{
ROIItems.CollectionChanged += ROIItems_CollectionChanged;
foreach (var roi in ROIItems)
{
roi.PropertyChanged += ROI_PropertyChanged;
// 如果是多边形ROI,监听Points集合变化
if (roi is PolygonROI polygonROI)
{
polygonROI.Points.CollectionChanged += Points_CollectionChanged;
}
}
}
}
private void ROIItems_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
if (e.NewItems != null)
{
foreach (ROIShape roi in e.NewItems)
{
roi.PropertyChanged += ROI_PropertyChanged;
// 如果是多边形ROI,监听Points集合变化
if (roi is PolygonROI polygonROI)
{
polygonROI.Points.CollectionChanged += Points_CollectionChanged;
}
}
}
if (e.OldItems != null)
{
foreach (ROIShape roi in e.OldItems)
{
roi.PropertyChanged -= ROI_PropertyChanged;
// 取消监听Points集合变化
if (roi is PolygonROI polygonROI)
{
polygonROI.Points.CollectionChanged -= Points_CollectionChanged;
}
}
}
}
private void Points_CollectionChanged(object? sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
// 只在删除或添加顶点时更新Adorner,拖拽时的Replace操作不触发更新
if (e.Action == System.Collections.Specialized.NotifyCollectionChangedAction.Remove ||
e.Action == System.Collections.Specialized.NotifyCollectionChangedAction.Add)
{
// Points集合变化时,如果当前选中的是多边形ROI,更新Adorner
if (SelectedROI is PolygonROI polygonROI && sender == polygonROI.Points)
{
// 使用Dispatcher延迟更新,确保UI已经处理完Points的变化
Dispatcher.BeginInvoke(new Action(() =>
{
UpdateAdorner();
}), System.Windows.Threading.DispatcherPriority.Render);
}
}
// Replace操作(拖拽时)不需要重建Adorner,只需要让现有Adorner重新布局
}
private void ROI_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(ROIShape.IsSelected))
{
UpdateAdorner();
}
// 监听Points属性变化(整个集合替换的情况)
else if (e.PropertyName == "Points" && sender is PolygonROI)
{
UpdateAdorner();
}
}
#region Dependency Properties
public static readonly DependencyProperty ImageSourceProperty =
DependencyProperty.Register(nameof(ImageSource), typeof(ImageSource), typeof(PolygonRoiCanvas),
new PropertyMetadata(null, OnImageSourceChanged));
public ImageSource? ImageSource
{
get => (ImageSource?)GetValue(ImageSourceProperty);
set => SetValue(ImageSourceProperty, value);
}
private static void OnImageSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var control = (PolygonRoiCanvas)d;
if (e.NewValue is BitmapSource bitmap)
{
// 使用像素尺寸,避免 DPI 不同导致 DIP 尺寸与实际像素不一致
control.CanvasWidth = bitmap.PixelWidth;
control.CanvasHeight = bitmap.PixelHeight;
control.ResetView();
}
else if (e.NewValue is ImageSource imageSource)
{
control.CanvasWidth = imageSource.Width;
control.CanvasHeight = imageSource.Height;
control.ResetView();
}
// 图像尺寸变化后刷新十字线
if (control.ShowCrosshair)
control.AddCrosshair();
}
public static readonly DependencyProperty ROIItemsProperty =
DependencyProperty.Register(nameof(ROIItems), typeof(ObservableCollection<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));
public double CanvasWidth
{
get => (double)GetValue(CanvasWidthProperty);
set => SetValue(CanvasWidthProperty, value);
}
public static readonly DependencyProperty CanvasHeightProperty =
DependencyProperty.Register(nameof(CanvasHeight), typeof(double), typeof(PolygonRoiCanvas),
new PropertyMetadata(600.0));
public double CanvasHeight
{
get => (double)GetValue(CanvasHeightProperty);
set => SetValue(CanvasHeightProperty, value);
}
public static readonly DependencyProperty SelectedROIProperty =
DependencyProperty.Register(nameof(SelectedROI), typeof(ROIShape), typeof(PolygonRoiCanvas),
new PropertyMetadata(null, OnSelectedROIChanged));
public ROIShape? SelectedROI
{
get => (ROIShape?)GetValue(SelectedROIProperty);
set => SetValue(SelectedROIProperty, value);
}
private static void OnSelectedROIChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var control = (PolygonRoiCanvas)d;
// 更新IsSelected状态
if (e.OldValue is ROIShape oldROI)
{
oldROI.IsSelected = false;
}
if (e.NewValue is ROIShape newROI)
{
newROI.IsSelected = true;
}
control.UpdateAdorner();
}
#endregion Dependency Properties
#region Measurement Config
public static readonly DependencyProperty PixelSizeProperty =
DependencyProperty.Register(nameof(PixelSize), typeof(double), typeof(PolygonRoiCanvas),
new PropertyMetadata(1.0));
/// <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);
}
private Line _crosshairH, _crosshairV;
private static void OnShowCrosshairChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var c = (PolygonRoiCanvas)d;
if ((bool)e.NewValue)
c.AddCrosshair();
else
c.RemoveCrosshair();
}
private void AddCrosshair()
{
RemoveCrosshair();
double w = CanvasWidth, h = CanvasHeight;
if (w <= 0 || h <= 0) return;
_crosshairH = new Line { X1 = 0, Y1 = h / 2, X2 = w, Y2 = h / 2, Stroke = Brushes.Red, StrokeThickness = 1, Opacity = 0.7, IsHitTestVisible = false };
_crosshairV = new Line { X1 = w / 2, Y1 = 0, X2 = w / 2, Y2 = h, Stroke = Brushes.Red, StrokeThickness = 1, Opacity = 0.7, IsHitTestVisible = false };
mainCanvas.Children.Add(_crosshairH);
mainCanvas.Children.Add(_crosshairV);
}
private void RemoveCrosshair()
{
if (_crosshairH != null) { mainCanvas.Children.Remove(_crosshairH); _crosshairH = null; }
if (_crosshairV != null) { mainCanvas.Children.Remove(_crosshairV); _crosshairV = null; }
}
#endregion Crosshair
#region Measurement
public static readonly DependencyProperty CurrentMeasureModeProperty =
DependencyProperty.Register(nameof(CurrentMeasureMode), typeof(Models.MeasureMode), typeof(PolygonRoiCanvas),
new PropertyMetadata(Models.MeasureMode.None, OnMeasureModeChanged));
public Models.MeasureMode CurrentMeasureMode
{
get => (Models.MeasureMode)GetValue(CurrentMeasureModeProperty);
set => SetValue(CurrentMeasureModeProperty, value);
}
// 保留 IsMeasuring 作为便捷属性
public bool IsMeasuring => CurrentMeasureMode != Models.MeasureMode.None;
private Canvas _measureOverlay;
private readonly System.Collections.Generic.List<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;
// 气泡测量状态
public enum BubbleSubTool { Roi, Wand, Brush, Eraser }
private BubbleSubTool _bubbleTool = BubbleSubTool.Roi;
private Rectangle _bubbleRoiRect;
private Ellipse _bubbleRoiHandle; // 右下角调整手柄
private TextBlock _bubbleResultLabel; // ROI 上方结果标签
private Rect? _bubbleRoi;
private Point? _bubbleRoiStart;
private bool _bubbleRoiDragging;
private bool _bubbleRoiMoving; // 拖动整个 ROI
private bool _bubbleRoiResizing; // 右下角调整大小
private Point _bubbleRoiDragOffset;
private Image _bubbleMaskImage;
private System.Windows.Media.Imaging.WriteableBitmap _bubbleMask;
private int _bubbleThreshold = 128;
private int _bubbleBrushSize = 5;
private bool _bubbleBrushDragging;
private readonly System.Collections.Generic.Stack<byte[]> _bubbleUndoStack = new();
public void SetBubbleTool(BubbleSubTool tool) => _bubbleTool = tool;
public void SetBubbleThreshold(int val) => _bubbleThreshold = val;
public void SetBubbleBrushSize(int val) => _bubbleBrushSize = val;
public Rect? BubbleRoi => _bubbleRoi;
// 拖拽状态
private Ellipse _mDraggingDot;
private object _mDraggingOwner;
private string _mDraggingRole;
private static void OnMeasureModeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var c = (PolygonRoiCanvas)d;
var newMode = (Models.MeasureMode)e.NewValue;
if (newMode != Models.MeasureMode.None)
{
c.EnsureMeasureOverlay();
}
else
{
// 退出测量模式:清除未完成的临时元素
c.ClearPendingElements();
}
}
private void ClearPendingElements()
{
if (_measureOverlay == null) return;
if (_pendingDot != null) { _measureOverlay.Children.Remove(_pendingDot); _pendingDot = null; }
_pendingPoint = null;
if (_ptlTempDot1 != null) { _measureOverlay.Children.Remove(_ptlTempDot1); _ptlTempDot1 = null; }
if (_ptlTempDot2 != null) { _measureOverlay.Children.Remove(_ptlTempDot2); _ptlTempDot2 = null; }
if (_ptlTempLine != null) { _measureOverlay.Children.Remove(_ptlTempLine); _ptlTempLine = null; }
_ptlTempL1 = _ptlTempL2 = null; _ptlClickCount = 0;
if (_angleTempVDot != null) { _measureOverlay.Children.Remove(_angleTempVDot); _angleTempVDot = null; }
if (_angleTempADot != null) { _measureOverlay.Children.Remove(_angleTempADot); _angleTempADot = null; }
if (_angleTempLineA != null) { _measureOverlay.Children.Remove(_angleTempLineA); _angleTempLineA = null; }
_angleTempV = _angleTempA = null; _angleClickCount = 0;
if (_bgaPendingDot != null) { _measureOverlay.Children.Remove(_bgaPendingDot); _bgaPendingDot = null; }
_bgaPendingCenter = null;
}
private void EnsureMeasureOverlay()
{
if (_measureOverlay != null) return;
_measureOverlay = new Canvas { IsHitTestVisible = true, Background = Brushes.Transparent };
_measureOverlay.SetBinding(Canvas.WidthProperty, new System.Windows.Data.Binding("CanvasWidth") { Source = this });
_measureOverlay.SetBinding(Canvas.HeightProperty, new System.Windows.Data.Binding("CanvasHeight") { Source = this });
mainCanvas.Children.Add(_measureOverlay);
}
private void RemoveMeasureOverlay()
{
if (_measureOverlay != null) { mainCanvas.Children.Remove(_measureOverlay); _measureOverlay = null; }
_ppGroups.Clear(); _ptlGroups.Clear(); _angleGroups.Clear(); _frGroups.Clear(); _bgaGroups.Clear();
_pendingDot = null; _pendingPoint = null;
_ptlTempDot1 = _ptlTempDot2 = null; _ptlTempLine = null;
_ptlTempL1 = _ptlTempL2 = null; _ptlClickCount = 0;
_angleTempVDot = _angleTempADot = null; _angleTempLineA = null;
_angleTempV = _angleTempA = null; _angleClickCount = 0;
_mDraggingDot = null; _mDraggingOwner = null;
}
public void ClearMeasurements() => RemoveMeasureOverlay();
public int MeasureCount => _ppGroups.Count + _ptlGroups.Count + _angleGroups.Count + _frGroups.Count + _bgaGroups.Count;
// ── 点击分发 ──
private void HandleMeasureClick(Point pos)
{
if (_measureOverlay == null) EnsureMeasureOverlay();
if (_measureOverlay == null) return;
if (CurrentMeasureMode == Models.MeasureMode.PointDistance)
HandlePointDistanceClick(pos);
else if (CurrentMeasureMode == Models.MeasureMode.PointToLine)
HandlePointToLineClick(pos);
else if (CurrentMeasureMode == Models.MeasureMode.Angle)
HandleAngleClick(pos);
else if (CurrentMeasureMode == Models.MeasureMode.FillRate)
HandleFillRateClick(pos);
else if (CurrentMeasureMode == Models.MeasureMode.BgaVoid)
HandleBgaVoidClick(pos);
// BubbleMeasure 的点击在 MouseDown/Move/Up 中处理(拖拽画 ROI 和画笔)
}
// ── 点点距 ──
private void HandlePointDistanceClick(Point pos)
{
if (!_pendingPoint.HasValue)
{
_pendingPoint = pos;
_pendingDot = CreateMDot(Brushes.Red);
_measureOverlay.Children.Add(_pendingDot);
SetDotPos(_pendingDot, pos);
RaiseMeasureStatusChanged($"点点距 - 第一点: ({pos.X:F0}, {pos.Y:F0}),请点击第二个点");
}
else
{
var g = CreatePPGroup(_pendingPoint.Value, pos);
_ppGroups.Add(g);
_measureOverlay.Children.Remove(_pendingDot);
RenumberAll();
_pendingDot = null; _pendingPoint = null;
RaiseMeasureCompleted(g.P1, g.P2, g.Distance, MeasureCount, "PointDistance");
CurrentMeasureMode = Models.MeasureMode.None;
}
}
private Models.MeasureGroup CreatePPGroup(Point p1, Point p2)
{
var g = new Models.MeasureGroup { P1 = p1, P2 = p2 };
g.Line = new Line { Stroke = Brushes.Lime, StrokeThickness = 2, IsHitTestVisible = false };
g.Label = new TextBlock { Foreground = Brushes.Yellow, FontSize = 13, FontWeight = FontWeights.Bold, IsHitTestVisible = false };
g.Dot1 = CreateMDot(Brushes.Red);
g.Dot2 = CreateMDot(Brushes.Blue);
foreach (UIElement el in new UIElement[] { g.Line, g.Label, g.Dot1, g.Dot2 })
_measureOverlay.Children.Add(el);
SetDotPos(g.Dot1, p1); SetDotPos(g.Dot2, p2);
g.UpdateLine(); g.UpdateLabel(FormatDistance(g.Distance));
return g;
}
// ── 点线距 ──
private void HandlePointToLineClick(Point pos)
{
_ptlClickCount++;
if (_ptlClickCount == 1)
{
_ptlTempL1 = pos;
_ptlTempDot1 = CreateMDot(Brushes.Lime);
_measureOverlay.Children.Add(_ptlTempDot1);
SetDotPos(_ptlTempDot1, pos);
RaiseMeasureStatusChanged($"点线距 - 直线端点1: ({pos.X:F0}, {pos.Y:F0}),请点击直线端点2");
}
else if (_ptlClickCount == 2)
{
_ptlTempL2 = pos;
_ptlTempDot2 = CreateMDot(Brushes.Lime);
_measureOverlay.Children.Add(_ptlTempDot2);
SetDotPos(_ptlTempDot2, pos);
_ptlTempLine = new Line { Stroke = Brushes.Lime, StrokeThickness = 2, IsHitTestVisible = false,
X1 = _ptlTempL1.Value.X, Y1 = _ptlTempL1.Value.Y, X2 = pos.X, Y2 = pos.Y };
_measureOverlay.Children.Add(_ptlTempLine);
RaiseMeasureStatusChanged($"点线距 - 直线已定义,请点击测量点");
}
else if (_ptlClickCount == 3)
{
// 完成:创建正式组,移除临时元素
var g = CreatePTLGroup(_ptlTempL1.Value, _ptlTempL2.Value, pos);
_ptlGroups.Add(g);
RenumberAll();
if (_ptlTempDot1 != null) _measureOverlay.Children.Remove(_ptlTempDot1);
if (_ptlTempDot2 != null) _measureOverlay.Children.Remove(_ptlTempDot2);
if (_ptlTempLine != null) _measureOverlay.Children.Remove(_ptlTempLine);
_ptlTempDot1 = _ptlTempDot2 = null; _ptlTempLine = null;
_ptlTempL1 = _ptlTempL2 = null; _ptlClickCount = 0;
var foot = g.FootPoint;
RaiseMeasureCompleted(g.P, foot, g.Distance, MeasureCount, "PointToLine");
CurrentMeasureMode = Models.MeasureMode.None;
}
}
private Models.PointToLineGroup CreatePTLGroup(Point l1, Point l2, Point p)
{
var g = new Models.PointToLineGroup { L1 = l1, L2 = l2, P = p };
g.MainLine = new Line { Stroke = Brushes.Lime, StrokeThickness = 2, IsHitTestVisible = false };
g.ExtLine = new Line { Stroke = Brushes.Lime, StrokeThickness = 1, IsHitTestVisible = false,
StrokeDashArray = new DoubleCollection { 4, 2 }, Visibility = Visibility.Collapsed };
g.PerpLine = new Line { Stroke = Brushes.Yellow, StrokeThickness = 1, IsHitTestVisible = false,
StrokeDashArray = new DoubleCollection { 4, 2 } };
g.FootDot = new Ellipse { Width = 8, Height = 8, Fill = Brushes.Cyan, Stroke = Brushes.White, StrokeThickness = 1, IsHitTestVisible = false };
g.Label = new TextBlock { Foreground = Brushes.Yellow, FontSize = 13, FontWeight = FontWeights.Bold, IsHitTestVisible = false };
g.DotL1 = CreateMDot(Brushes.Lime);
g.DotL2 = CreateMDot(Brushes.Lime);
g.DotP = CreateMDot(Brushes.Red);
foreach (UIElement el in new UIElement[] { g.MainLine, g.ExtLine, g.PerpLine, g.FootDot, g.Label, g.DotL1, g.DotL2, g.DotP })
_measureOverlay.Children.Add(el);
SetDotPos(g.DotL1, l1); SetDotPos(g.DotL2, l2); SetDotPos(g.DotP, p);
g.UpdateVisuals(FormatDistance(g.Distance));
return g;
}
// ── 角度测量 ──
private void HandleAngleClick(Point pos)
{
_angleClickCount++;
if (_angleClickCount == 1)
{
_angleTempV = pos;
_angleTempVDot = CreateMDot(Brushes.Red);
_measureOverlay.Children.Add(_angleTempVDot);
SetDotPos(_angleTempVDot, pos);
RaiseMeasureStatusChanged($"角度测量 - 顶点: ({pos.X:F0}, {pos.Y:F0}),请点击射线端点A");
}
else if (_angleClickCount == 2)
{
_angleTempA = pos;
_angleTempADot = CreateMDot(Brushes.Orange);
_measureOverlay.Children.Add(_angleTempADot);
SetDotPos(_angleTempADot, pos);
_angleTempLineA = new Line { Stroke = Brushes.Lime, StrokeThickness = 1, IsHitTestVisible = false,
X1 = _angleTempV.Value.X, Y1 = _angleTempV.Value.Y, X2 = pos.X, Y2 = pos.Y };
_measureOverlay.Children.Add(_angleTempLineA);
RaiseMeasureStatusChanged($"角度测量 - 射线A已定义,请点击射线端点B");
}
else if (_angleClickCount == 3)
{
var g = CreateAngleGroup(_angleTempV.Value, _angleTempA.Value, pos);
_angleGroups.Add(g);
RenumberAll();
// 移除临时元素
if (_angleTempVDot != null) _measureOverlay.Children.Remove(_angleTempVDot);
if (_angleTempADot != null) _measureOverlay.Children.Remove(_angleTempADot);
if (_angleTempLineA != null) _measureOverlay.Children.Remove(_angleTempLineA);
_angleTempVDot = _angleTempADot = null; _angleTempLineA = null;
_angleTempV = _angleTempA = null; _angleClickCount = 0;
RaiseMeasureCompleted(g.V, g.B, g.AngleDeg, MeasureCount, "Angle");
CurrentMeasureMode = Models.MeasureMode.None;
}
}
private Models.AngleGroup CreateAngleGroup(Point v, Point a, Point b)
{
var g = new Models.AngleGroup { V = v, A = a, B = b };
g.LineA = new Line { Stroke = Brushes.Lime, StrokeThickness = 1, IsHitTestVisible = false };
g.LineB = new Line { Stroke = Brushes.Lime, StrokeThickness = 1, IsHitTestVisible = false };
g.Arc = new Path { Stroke = Brushes.Yellow, StrokeThickness = 1.5, IsHitTestVisible = false };
g.Label = new TextBlock { Foreground = Brushes.Yellow, FontSize = 13, FontWeight = FontWeights.Bold, IsHitTestVisible = false };
g.DotV = CreateMDot(Brushes.Red);
g.DotA = CreateMDot(Brushes.Orange);
g.DotB = CreateMDot(Brushes.Cyan);
foreach (UIElement el in new UIElement[] { g.LineA, g.LineB, g.Arc, g.Label, g.DotV, g.DotA, g.DotB })
_measureOverlay.Children.Add(el);
SetDotPos(g.DotV, v); SetDotPos(g.DotA, a); SetDotPos(g.DotB, b);
g.UpdateVisuals();
return g;
}
// ── 通孔填锡率 ──
private void HandleFillRateClick(Point pos)
{
// 单击放置:自动生成4个椭圆的初始位置
double offset = 70;
var e1 = new Point(pos.X, pos.Y + offset / 2);
var e2 = new Point(pos.X + 20, pos.Y - offset / 2);
var e3 = e1; // 起点=底部
var e4 = new Point(pos.X + 10, pos.Y - offset / 4);
var g = CreateFillRateGroup(e1, e2, e3, e4);
_frGroups.Add(g);
RenumberAll();
double rate = g.FillRate;
RaiseMeasureCompleted(g.E3, g.E4, rate, MeasureCount, "FillRate");
CurrentMeasureMode = Models.MeasureMode.None;
}
private Models.FillRateGroup CreateFillRateGroup(Point e1, Point e2, Point e3, Point e4)
{
var g = new Models.FillRateGroup { E1 = e1, E2 = e2, E3 = e3, E4 = e4 };
g.PathE1 = CreateEllipsePath(Brushes.DodgerBlue, false);
g.PathE2 = CreateEllipsePath(Brushes.Cyan, false);
g.PathE3 = CreateEllipsePath(Brushes.Yellow, true);
g.PathE4 = CreateEllipsePath(Brushes.Lime, false);
g.FullLine = new Line { Stroke = Brushes.Red, StrokeThickness = 1, IsHitTestVisible = false };
g.FillLine = new Line { Stroke = Brushes.Lime, StrokeThickness = 2, IsHitTestVisible = false };
g.Label = new TextBlock { FontSize = 13, FontWeight = FontWeights.Bold, Cursor = Cursors.Hand };
g.Label.SetValue(ContextMenuService.IsEnabledProperty, false);
g.Label.PreviewMouseRightButtonUp += (s, ev) => { ShowTHTLimitEditor(g); ev.Handled = true; };
g.DotE1 = CreateMDot(Brushes.DodgerBlue);
g.DotE2 = CreateMDot(Brushes.Cyan);
g.DotE3 = CreateMDot(Brushes.Yellow);
g.DotE4 = CreateMDot(Brushes.Lime);
// 轴手柄(小方块样式区分)
g.E1AH = CreateAxisHandle(Brushes.DodgerBlue); g.E1BH = CreateAxisHandle(Brushes.DodgerBlue);
g.E2AH = CreateAxisHandle(Brushes.Cyan); g.E2BH = CreateAxisHandle(Brushes.Cyan);
g.E3AH = CreateAxisHandle(Brushes.Yellow); g.E3BH = CreateAxisHandle(Brushes.Yellow);
g.E4AH = CreateAxisHandle(Brushes.Lime); g.E4BH = CreateAxisHandle(Brushes.Lime);
foreach (var el in g.AllElements)
_measureOverlay.Children.Add(el);
SetDotPos(g.DotE1, e1); SetDotPos(g.DotE2, e2);
SetDotPos(g.DotE3, e3); SetDotPos(g.DotE4, e4);
g.UpdateVisuals();
return g;
}
private Ellipse CreateAxisHandle(Brush fill)
{
var h = new Ellipse { Width = 8, Height = 8, Fill = fill, Stroke = Brushes.White, StrokeThickness = 1, Cursor = Cursors.SizeAll };
h.SetValue(ContextMenuService.IsEnabledProperty, false);
h.MouseLeftButtonDown += MDot_Down;
h.MouseMove += MDot_Move;
h.MouseLeftButtonUp += MDot_Up;
h.PreviewMouseRightButtonUp += MDot_RightClick;
return h;
}
private static Path CreateEllipsePath(Brush stroke, bool dashed)
{
var p = new Path { Stroke = stroke, StrokeThickness = 1.5, IsHitTestVisible = false };
if (dashed) p.StrokeDashArray = new DoubleCollection { 4, 2 };
return p;
}
// ── BGA 空隙测量 ──
private void HandleBgaVoidClick(Point pos)
{
if (_measureOverlay == null) EnsureMeasureOverlay();
if (_measureOverlay == null) return;
// 第一次进入:创建新的 BGA 组
if (_bgaCurrent == null)
{
_bgaCurrent = new Models.BgaVoidGroup();
var currentGroup = _bgaCurrent; // 局部变量供闭包捕获
_bgaCurrent.Label = new TextBlock { FontSize = 13, FontWeight = FontWeights.Bold, Cursor = System.Windows.Input.Cursors.Hand, Visibility = Visibility.Collapsed };
_bgaCurrent.Label.SetValue(ContextMenuService.IsEnabledProperty, false);
_bgaCurrent.Label.PreviewMouseRightButtonUp += (s, ev) => { ShowBgaLimitEditor(currentGroup); ev.Handled = true; };
_measureOverlay.Children.Add(_bgaCurrent.Label);
_bgaDrawBall = false;
RaiseMeasureStatusChanged("BGA空隙 - 点击画气泡圆心(右键切换为画焊球)");
}
if (!_bgaPendingCenter.HasValue)
{
// 第一次点击:圆心
_bgaPendingCenter = pos;
_bgaPendingDot = CreateMDot(_bgaDrawBall ? Brushes.Lime : Brushes.Orange);
_measureOverlay.Children.Add(_bgaPendingDot);
SetDotPos(_bgaPendingDot, pos);
RaiseMeasureStatusChanged(_bgaDrawBall ? "BGA - 焊球圆心已定,点击边缘定半径" : "BGA - 气泡圆心已定,点击边缘定半径");
}
else
{
// 第二次点击:半径
double r = Math.Max(5, Models.FillRateGroup.Dist(pos, _bgaPendingCenter.Value));
var circle = CreateBgaCircle(_bgaPendingCenter.Value, r, _bgaDrawBall);
if (_bgaDrawBall)
{
// 如果已有焊球,移除旧的
if (_bgaCurrent.Ball != null)
{
_measureOverlay.Children.Remove(_bgaCurrent.Ball.Shape);
_measureOverlay.Children.Remove(_bgaCurrent.Ball.CenterDot);
_measureOverlay.Children.Remove(_bgaCurrent.Ball.EdgeDot);
}
_bgaCurrent.Ball = circle;
// 画完焊球 → 完成本组,计算结果
_bgaGroups.Add(_bgaCurrent);
RenumberAll();
_bgaCurrent.UpdateLabel();
_bgaCurrent.Label.Cursor = System.Windows.Input.Cursors.Hand;
RaiseMeasureCompleted(_bgaCurrent.Ball.Center, _bgaCurrent.Ball.Center, _bgaCurrent.VoidRate, MeasureCount, "BgaVoid");
_bgaCurrent = null;
_bgaDrawBall = false;
CurrentMeasureMode = Models.MeasureMode.None;
}
else
{
_bgaCurrent.Voids.Add(circle);
RaiseMeasureStatusChanged($"BGA - 已画 {_bgaCurrent.Voids.Count} 个气泡,继续画气泡或右键切换画焊球");
}
// 清除临时点
if (_bgaPendingDot != null) { _measureOverlay.Children.Remove(_bgaPendingDot); _bgaPendingDot = null; }
_bgaPendingCenter = null;
}
}
/// <summary>右键切换气泡/焊球模式(在 BGA 测量模式下)</summary>
private void HandleBgaRightClick()
{
if (CurrentMeasureMode != Models.MeasureMode.BgaVoid || _bgaCurrent == null) return;
_bgaDrawBall = !_bgaDrawBall;
RaiseMeasureStatusChanged(_bgaDrawBall ? "BGA - 已切换为画焊球模式" : "BGA - 已切换为画气泡模式");
}
private Models.BgaCircle CreateBgaCircle(Point center, double radius, bool isBall)
{
var c = new Models.BgaCircle { Center = center, Radius = radius, IsBall = isBall };
c.Shape = new Ellipse
{
Stroke = isBall ? Brushes.Lime : Brushes.Orange,
StrokeThickness = isBall ? 2 : 1.5,
Fill = Brushes.Transparent,
IsHitTestVisible = false
};
c.CenterDot = CreateMDot(isBall ? Brushes.Lime : Brushes.Orange);
c.EdgeDot = CreateMDot(isBall ? Brushes.Lime : Brushes.Orange);
c.EdgeDot.Width = 8; c.EdgeDot.Height = 8;
c.EdgeDot.Cursor = System.Windows.Input.Cursors.SizeAll;
_measureOverlay.Children.Add(c.Shape);
_measureOverlay.Children.Add(c.CenterDot);
_measureOverlay.Children.Add(c.EdgeDot);
c.UpdateVisuals();
return c;
}
// BGA Limit 编辑(和 FillRate 类似)
private void ShowBgaLimitEditor(Models.BgaVoidGroup g)
{
if (_measureOverlay == null || g == null) return;
RemoveTHTEditor();
double left = Canvas.GetLeft(g.Label);
double top = Canvas.GetTop(g.Label) + 22;
_thtEditLabel = new TextBlock
{
Text = "VoidLimit(%):",
FontSize = 11, Foreground = Brushes.White,
Background = new SolidColorBrush(Color.FromArgb(180, 0, 0, 0)),
Padding = new Thickness(3, 1, 3, 1)
};
Canvas.SetLeft(_thtEditLabel, left);
Canvas.SetTop(_thtEditLabel, top);
_measureOverlay.Children.Add(_thtEditLabel);
_thtEditBox = new TextBox
{
Text = g.VoidLimit.ToString("F1"),
Width = 60, Height = 22, FontSize = 12,
Background = Brushes.White, BorderBrush = Brushes.Orange, BorderThickness = new Thickness(2),
Padding = new Thickness(2, 0, 2, 0)
};
Canvas.SetLeft(_thtEditBox, left + 85);
Canvas.SetTop(_thtEditBox, top);
_measureOverlay.Children.Add(_thtEditBox);
_thtEditBox.Focus();
_thtEditBox.SelectAll();
_thtEditBox.KeyDown += (s, ev) =>
{
if (ev.Key == System.Windows.Input.Key.Enter)
{
if (double.TryParse(_thtEditBox.Text, out double val))
{
g.VoidLimit = System.Math.Clamp(val, 0, 100);
g.UpdateLabel();
RaiseMeasureCompleted(g.Ball?.Center ?? default, g.Ball?.Center ?? default, g.VoidRate, MeasureCount, "BgaVoid");
}
RemoveTHTEditor();
}
else if (ev.Key == System.Windows.Input.Key.Escape) RemoveTHTEditor();
};
_thtEditBox.LostFocus += (s, ev) => RemoveTHTEditor();
}
// ── 共用:圆点创建、定位、拖拽、删除 ──
private Ellipse CreateMDot(Brush fill)
{
var dot = new Ellipse { Width = 12, Height = 12, Fill = fill, Stroke = Brushes.White, StrokeThickness = 1.5, Cursor = Cursors.Hand };
dot.SetValue(ContextMenuService.IsEnabledProperty, false);
dot.MouseLeftButtonDown += MDot_Down;
dot.MouseMove += MDot_Move;
dot.MouseLeftButtonUp += MDot_Up;
dot.PreviewMouseRightButtonUp += MDot_RightClick;
return dot;
}
private static void SetDotPos(Ellipse dot, Point pos)
{
Canvas.SetLeft(dot, pos.X - dot.Width / 2);
Canvas.SetTop(dot, pos.Y - dot.Height / 2);
}
private void MDot_Down(object sender, MouseButtonEventArgs e)
{
if (sender is not Ellipse dot) return;
// 查找点点距组
foreach (var g in _ppGroups)
{
if (g.Dot1 == dot) { _mDraggingOwner = g; _mDraggingRole = "Dot1"; break; }
if (g.Dot2 == dot) { _mDraggingOwner = g; _mDraggingRole = "Dot2"; break; }
}
// 查找点线距组
if (_mDraggingOwner == null)
{
foreach (var g in _ptlGroups)
{
if (g.DotL1 == dot) { _mDraggingOwner = g; _mDraggingRole = "DotL1"; break; }
if (g.DotL2 == dot) { _mDraggingOwner = g; _mDraggingRole = "DotL2"; break; }
if (g.DotP == dot) { _mDraggingOwner = g; _mDraggingRole = "DotP"; break; }
}
}
// 查找角度组
if (_mDraggingOwner == null)
{
foreach (var g in _angleGroups)
{
if (g.DotV == dot) { _mDraggingOwner = g; _mDraggingRole = "DotV"; break; }
if (g.DotA == dot) { _mDraggingOwner = g; _mDraggingRole = "DotA"; break; }
if (g.DotB == dot) { _mDraggingOwner = g; _mDraggingRole = "DotB"; break; }
}
}
// 查找填锡率组
if (_mDraggingOwner == null)
{
foreach (var g in _frGroups)
{
if (g.DotE1 == dot) { _mDraggingOwner = g; _mDraggingRole = "E1"; break; }
if (g.DotE2 == dot) { _mDraggingOwner = g; _mDraggingRole = "E2"; break; }
if (g.DotE3 == dot) { _mDraggingOwner = g; _mDraggingRole = "E3"; break; }
if (g.DotE4 == dot) { _mDraggingOwner = g; _mDraggingRole = "E4"; break; }
if (g.E1AH == dot) { _mDraggingOwner = g; _mDraggingRole = "E1A"; break; }
if (g.E1BH == dot) { _mDraggingOwner = g; _mDraggingRole = "E1B"; break; }
if (g.E2AH == dot) { _mDraggingOwner = g; _mDraggingRole = "E2A"; break; }
if (g.E2BH == dot) { _mDraggingOwner = g; _mDraggingRole = "E2B"; break; }
if (g.E3AH == dot) { _mDraggingOwner = g; _mDraggingRole = "E3A"; break; }
if (g.E3BH == dot) { _mDraggingOwner = g; _mDraggingRole = "E3B"; break; }
if (g.E4AH == dot) { _mDraggingOwner = g; _mDraggingRole = "E4A"; break; }
if (g.E4BH == dot) { _mDraggingOwner = g; _mDraggingRole = "E4B"; break; }
}
}
// 查找 BGA 组
if (_mDraggingOwner == null)
{
foreach (var g in _bgaGroups)
{
if (g.Ball?.CenterDot == dot) { _mDraggingOwner = g; _mDraggingRole = "BallCenter"; break; }
if (g.Ball?.EdgeDot == dot) { _mDraggingOwner = g; _mDraggingRole = "BallEdge"; break; }
bool found = false;
for (int vi = 0; vi < g.Voids.Count; vi++)
{
if (g.Voids[vi].CenterDot == dot) { _mDraggingOwner = g; _mDraggingRole = $"V{vi}Center"; found = true; break; }
if (g.Voids[vi].EdgeDot == dot) { _mDraggingOwner = g; _mDraggingRole = $"V{vi}Edge"; found = true; break; }
}
if (found) break;
}
}
if (_mDraggingOwner != null) { _mDraggingDot = dot; dot.CaptureMouse(); e.Handled = true; }
}
private void MDot_Move(object sender, MouseEventArgs e)
{
if (_mDraggingDot == null || _mDraggingOwner == null || _measureOverlay == null) return;
if (e.LeftButton != MouseButtonState.Pressed) return;
var pos = e.GetPosition(_measureOverlay);
SetDotPos(_mDraggingDot, pos);
if (_mDraggingOwner is Models.MeasureGroup ppg)
{
if (_mDraggingRole == "Dot1") ppg.P1 = pos; else ppg.P2 = pos;
ppg.UpdateLine(); ppg.UpdateLabel(FormatDistance(ppg.Distance));
RaiseMeasureCompleted(ppg.P1, ppg.P2, ppg.Distance, MeasureCount, "PointDistance");
}
else if (_mDraggingOwner is Models.PointToLineGroup ptlg)
{
if (_mDraggingRole == "DotL1") ptlg.L1 = pos;
else if (_mDraggingRole == "DotL2") ptlg.L2 = pos;
else if (_mDraggingRole == "DotP") ptlg.P = pos;
ptlg.UpdateVisuals(FormatDistance(ptlg.Distance));
var foot = ptlg.FootPoint;
RaiseMeasureCompleted(ptlg.P, foot, ptlg.Distance, MeasureCount, "PointToLine");
}
else if (_mDraggingOwner is Models.AngleGroup ag)
{
if (_mDraggingRole == "DotV") ag.V = pos;
else if (_mDraggingRole == "DotA") ag.A = pos;
else if (_mDraggingRole == "DotB") ag.B = pos;
ag.UpdateVisuals();
RaiseMeasureCompleted(ag.V, ag.B, ag.AngleDeg, MeasureCount, "Angle");
}
else if (_mDraggingOwner is Models.FillRateGroup frg)
{
if (_mDraggingRole == "E1") frg.E1 = pos;
else if (_mDraggingRole == "E2") frg.E2 = pos;
else if (_mDraggingRole == "E3") frg.E3 = pos;
else if (_mDraggingRole == "E4") frg.E4 = pos;
else if (_mDraggingRole == "E1A") frg.E1A = Math.Max(10, Models.FillRateGroup.Dist(pos, frg.E1));
else if (_mDraggingRole == "E1B") frg.E1B = Math.Max(10, Models.FillRateGroup.Dist(pos, frg.E1));
else if (_mDraggingRole == "E2A") frg.E2A = Math.Max(10, Models.FillRateGroup.Dist(pos, frg.E2));
else if (_mDraggingRole == "E2B") frg.E2B = Math.Max(10, Models.FillRateGroup.Dist(pos, frg.E2));
else if (_mDraggingRole == "E3A") frg.E3A = Math.Max(10, Models.FillRateGroup.Dist(pos, frg.E3));
else if (_mDraggingRole == "E3B") frg.E3B = Math.Max(10, Models.FillRateGroup.Dist(pos, frg.E3));
else if (_mDraggingRole == "E4A") frg.E4A = Math.Max(10, Models.FillRateGroup.Dist(pos, frg.E4));
else if (_mDraggingRole == "E4B") frg.E4B = Math.Max(10, Models.FillRateGroup.Dist(pos, frg.E4));
frg.UpdateVisuals();
RaiseMeasureCompleted(frg.E3, frg.E4, frg.FillRate, MeasureCount, "FillRate");
}
else if (_mDraggingOwner is Models.BgaVoidGroup bgag)
{
if (_mDraggingRole == "BallCenter" && bgag.Ball != null)
{
bgag.Ball.Center = pos; bgag.Ball.UpdateVisuals();
}
else if (_mDraggingRole == "BallEdge" && bgag.Ball != null)
{
bgag.Ball.Radius = Math.Max(5, Models.FillRateGroup.Dist(pos, bgag.Ball.Center));
bgag.Ball.UpdateVisuals();
}
else if (_mDraggingRole.StartsWith("V") && _mDraggingRole.Length > 1)
{
// 解析 V{index}Center 或 V{index}Edge
string rest = _mDraggingRole.Substring(1);
bool isEdge = rest.EndsWith("Edge");
string idxStr = isEdge ? rest.Replace("Edge", "") : rest.Replace("Center", "");
if (int.TryParse(idxStr, out int vi) && vi >= 0 && vi < bgag.Voids.Count)
{
if (isEdge)
bgag.Voids[vi].Radius = Math.Max(5, Models.FillRateGroup.Dist(pos, bgag.Voids[vi].Center));
else
bgag.Voids[vi].Center = pos;
bgag.Voids[vi].UpdateVisuals();
}
}
bgag.UpdateLabel();
RaiseMeasureCompleted(bgag.Ball?.Center ?? default, bgag.Ball?.Center ?? default, bgag.VoidRate, MeasureCount, "BgaVoid");
}
e.Handled = true;
}
private void MDot_Up(object sender, MouseButtonEventArgs e)
{
if (_mDraggingDot != null) { _mDraggingDot.ReleaseMouseCapture(); _mDraggingDot = null; _mDraggingOwner = null; e.Handled = true; }
}
private void MDot_RightClick(object sender, MouseButtonEventArgs e)
{
if (sender is not Ellipse dot || _measureOverlay == null) return;
// 点点距删除
foreach (var g in _ppGroups)
{
if (g.Dot1 == dot || g.Dot2 == dot)
{
foreach (var el in new UIElement[] { g.Dot1, g.Dot2, g.Line, g.Label })
_measureOverlay.Children.Remove(el);
_ppGroups.Remove(g);
RenumberAll();
RaiseMeasureStatusChanged($"已删除测量 | 剩余 {MeasureCount} 条");
e.Handled = true; return;
}
}
// 点线距删除
foreach (var g in _ptlGroups)
{
if (g.DotL1 == dot || g.DotL2 == dot || g.DotP == dot)
{
foreach (var el in new UIElement[] { g.DotL1, g.DotL2, g.DotP, g.MainLine, g.ExtLine, g.PerpLine, g.FootDot, g.Label })
_measureOverlay.Children.Remove(el);
_ptlGroups.Remove(g);
RenumberAll();
RaiseMeasureStatusChanged($"已删除测量 | 剩余 {MeasureCount} 条");
e.Handled = true; return;
}
}
// 角度删除
foreach (var g in _angleGroups)
{
if (g.DotV == dot || g.DotA == dot || g.DotB == dot)
{
foreach (var el in new UIElement[] { g.DotV, g.DotA, g.DotB, g.LineA, g.LineB, g.Arc, g.Label })
_measureOverlay.Children.Remove(el);
_angleGroups.Remove(g);
RenumberAll();
RaiseMeasureStatusChanged($"已删除测量 | 剩余 {MeasureCount} 条");
e.Handled = true; return;
}
}
// 填锡率删除
foreach (var g in _frGroups)
{
bool match = g.DotE1 == dot || g.DotE2 == dot || g.DotE3 == dot || g.DotE4 == dot
|| g.E1AH == dot || g.E1BH == dot || g.E2AH == dot || g.E2BH == dot
|| g.E3AH == dot || g.E3BH == dot || g.E4AH == dot || g.E4BH == dot;
if (match)
{
foreach (var el in g.AllElements)
_measureOverlay.Children.Remove(el);
_frGroups.Remove(g);
RenumberAll();
RaiseMeasureStatusChanged($"已删除测量 | 剩余 {MeasureCount} 条");
e.Handled = true; return;
}
}
// BGA 删除
foreach (var g in _bgaGroups)
{
bool match = g.Ball?.CenterDot == dot || g.Ball?.EdgeDot == dot;
if (!match)
{
foreach (var v in g.Voids)
{
if (v.CenterDot == dot || v.EdgeDot == dot) { match = true; break; }
}
}
if (match)
{
foreach (var el in g.AllElements)
_measureOverlay.Children.Remove(el);
_bgaGroups.Remove(g);
RenumberAll();
RaiseMeasureStatusChanged($"已删除测量 | 剩余 {MeasureCount} 条");
e.Handled = true; return;
}
}
}
// ── 气泡测量辅助 ──
private void EnsureBubbleRoiVisuals()
{
EnsureMeasureOverlay();
if (_bubbleRoiRect == null)
{
_bubbleRoiRect = new Rectangle
{
Stroke = Brushes.Red,
StrokeThickness = 1.5,
Fill = Brushes.Transparent,
Visibility = Visibility.Collapsed,
IsHitTestVisible = false
};
_measureOverlay.Children.Add(_bubbleRoiRect);
}
if (_bubbleRoiHandle == null)
{
_bubbleRoiHandle = new Ellipse
{
Width = 10, Height = 10,
Fill = Brushes.Red, Stroke = Brushes.White, StrokeThickness = 1,
Cursor = Cursors.SizeNWSE,
Visibility = Visibility.Collapsed,
IsHitTestVisible = false // 命中测试由 MouseDown 中的距离判断处理
};
_measureOverlay.Children.Add(_bubbleRoiHandle);
}
if (_bubbleResultLabel == null)
{
_bubbleResultLabel = new TextBlock
{
FontSize = 13, FontWeight = FontWeights.Bold,
IsHitTestVisible = false,
Visibility = Visibility.Collapsed
};
_measureOverlay.Children.Add(_bubbleResultLabel);
}
}
private void SyncBubbleRoiVisuals()
{
if (_bubbleRoiRect == null || !_bubbleRoi.HasValue) return;
var r = _bubbleRoi.Value;
Canvas.SetLeft(_bubbleRoiRect, r.X);
Canvas.SetTop(_bubbleRoiRect, r.Y);
_bubbleRoiRect.Width = r.Width;
_bubbleRoiRect.Height = r.Height;
_bubbleRoiRect.Visibility = Visibility.Visible;
if (_bubbleRoiHandle != null)
{
Canvas.SetLeft(_bubbleRoiHandle, r.Right - _bubbleRoiHandle.Width / 2);
Canvas.SetTop(_bubbleRoiHandle, r.Bottom - _bubbleRoiHandle.Height / 2);
_bubbleRoiHandle.Visibility = Visibility.Visible;
}
}
private void InitBubbleMask()
{
if (!_bubbleRoi.HasValue) return;
int w = (int)CanvasWidth, h = (int)CanvasHeight;
if (w <= 0 || h <= 0) return;
_bubbleMask = new System.Windows.Media.Imaging.WriteableBitmap(w, h, 96, 96,
PixelFormats.Bgra32, null);
if (_bubbleMaskImage == null)
{
EnsureMeasureOverlay();
_bubbleMaskImage = new Image
{
IsHitTestVisible = false,
Opacity = 0.45,
Stretch = Stretch.Fill
};
_bubbleMaskImage.SetBinding(Image.WidthProperty, new System.Windows.Data.Binding("CanvasWidth") { Source = this });
_bubbleMaskImage.SetBinding(Image.HeightProperty, new System.Windows.Data.Binding("CanvasHeight") { Source = this });
_measureOverlay.Children.Add(_bubbleMaskImage);
}
_bubbleMaskImage.Source = _bubbleMask;
}
private void ApplyBrushAt(Point pos)
{
if (_bubbleMask == null || !_bubbleRoi.HasValue) return;
var roi = _bubbleRoi.Value;
int w = _bubbleMask.PixelWidth, h = _bubbleMask.PixelHeight;
int cx = (int)pos.X, cy = (int)pos.Y;
int r = _bubbleBrushSize;
bool erase = _bubbleTool == BubbleSubTool.Eraser;
int roiX0 = Math.Max(0, (int)roi.X), roiY0 = Math.Max(0, (int)roi.Y);
int roiX1 = Math.Min(w, (int)roi.Right), roiY1 = Math.Min(h, (int)roi.Bottom);
// 计算笔刷影响的矩形区域
int x0 = Math.Max(roiX0, cx - r), y0 = Math.Max(roiY0, cy - r);
int x1 = Math.Min(roiX1, cx + r + 1), y1 = Math.Min(roiY1, cy + r + 1);
if (x0 >= x1 || y0 >= y1) return;
int regionW = x1 - x0, regionH = y1 - y0;
int stride = w * 4;
// 读取整行范围的像素
var pixels = new byte[regionW * regionH * 4];
_bubbleMask.CopyPixels(new System.Windows.Int32Rect(x0, y0, regionW, regionH), pixels, regionW * 4, 0);
int r2 = r * r;
for (int py2 = y0; py2 < y1; py2++)
{
for (int px2 = x0; px2 < x1; px2++)
{
int dx = px2 - cx, dy = py2 - cy;
if (dx * dx + dy * dy > r2) continue;
int idx = ((py2 - y0) * regionW + (px2 - x0)) * 4;
if (erase)
{
pixels[idx + 0] = 0; pixels[idx + 1] = 0;
pixels[idx + 2] = 0; pixels[idx + 3] = 0;
}
else
{
pixels[idx + 0] = 0; // B
pixels[idx + 1] = 140; // G
pixels[idx + 2] = 255; // R
pixels[idx + 3] = 180; // A
}
}
}
_bubbleMask.WritePixels(new System.Windows.Int32Rect(x0, y0, regionW, regionH), pixels, regionW * 4, 0);
}
private void UpdateBubbleResult()
{
if (_bubbleMask == null || !_bubbleRoi.HasValue) return;
var roi = _bubbleRoi.Value;
int w = _bubbleMask.PixelWidth, h = _bubbleMask.PixelHeight;
int stride = w * 4;
var pixels = new byte[stride * h];
_bubbleMask.CopyPixels(pixels, stride, 0);
int roiX0 = Math.Max(0, (int)roi.X), roiY0 = Math.Max(0, (int)roi.Y);
int roiX1 = Math.Min(w, (int)roi.Right), roiY1 = Math.Min(h, (int)roi.Bottom);
int roiArea = 0, voidArea = 0;
for (int y = roiY0; y < roiY1; y++)
for (int x = roiX0; x < roiX1; x++)
{
roiArea++;
int idx = (y * w + x) * 4;
if (pixels[idx + 3] > 0) voidArea++; // alpha > 0 表示已标记
}
double voidRate = roiArea > 0 ? voidArea * 100.0 / roiArea : 0;
// 更新 ROI 上方标签
if (_bubbleResultLabel != null)
{
string cls = voidRate <= 25.0 ? "PASS" : "FAIL";
_bubbleResultLabel.Text = $"Void: {voidRate:F1}% | {cls}";
_bubbleResultLabel.Foreground = cls == "PASS" ? Brushes.Lime : Brushes.Red;
Canvas.SetLeft(_bubbleResultLabel, roi.X);
Canvas.SetTop(_bubbleResultLabel, roi.Y - 20);
_bubbleResultLabel.Visibility = Visibility.Visible;
}
RaiseMeasureCompleted(roi.TopLeft, roi.BottomRight, voidRate, 1, "BubbleVoid");
}
/// <summary>魔棒:在点击位置做 flood fill</summary>
public void WandFloodFill(Point pos)
{
if (_bubbleMask == null || !_bubbleRoi.HasValue || ImageSource == null) return;
// 保存快照用于撤销
SaveMaskSnapshot();
var roi = _bubbleRoi.Value;
int px = (int)pos.X, py = (int)pos.Y;
if (!roi.Contains(pos)) return;
// 获取灰度像素
var gray = GetGrayscalePixels();
if (gray == null) return;
int w = _bubbleMask.PixelWidth, h = _bubbleMask.PixelHeight;
if (px < 0 || px >= w || py < 0 || py >= h) return;
int seedVal = gray[py * w + px];
int lo = _bubbleThreshold, hi = _bubbleThreshold;
// BFS flood fill
var visited = new bool[w * h];
var queue = new System.Collections.Generic.Queue<(int x, int y)>();
queue.Enqueue((px, py));
visited[py * w + px] = true;
int roiX0 = Math.Max(0, (int)roi.X), roiY0 = Math.Max(0, (int)roi.Y);
int roiX1 = Math.Min(w, (int)roi.Right), roiY1 = Math.Min(h, (int)roi.Bottom);
var filled = new System.Collections.Generic.List<(int x, int y)>();
while (queue.Count > 0)
{
var (cx, cy) = queue.Dequeue();
int val = gray[cy * w + cx];
// 阈值判断:与种子点灰度差在阈值范围内
if (Math.Abs(val - seedVal) > lo) continue;
// 必须在 ROI 内
if (cx < roiX0 || cx >= roiX1 || cy < roiY0 || cy >= roiY1) continue;
filled.Add((cx, cy));
// 四邻域
int[] dx = { -1, 1, 0, 0 }, dy = { 0, 0, -1, 1 };
for (int d = 0; d < 4; d++)
{
int nx = cx + dx[d], ny = cy + dy[d];
if (nx >= roiX0 && nx < roiX1 && ny >= roiY0 && ny < roiY1 && !visited[ny * w + nx])
{
visited[ny * w + nx] = true;
queue.Enqueue((nx, ny));
}
}
}
if (filled.Count == 0) return;
// 写入 mask(橙色半透明)
int stride = w * 4;
var maskPixels = new byte[stride * h];
_bubbleMask.CopyPixels(maskPixels, stride, 0);
foreach (var (fx, fy) in filled)
{
int idx = (fy * w + fx) * 4;
maskPixels[idx + 0] = 0; // B
maskPixels[idx + 1] = 140; // G
maskPixels[idx + 2] = 255; // R (橙色)
maskPixels[idx + 3] = 180; // A
}
_bubbleMask.WritePixels(new System.Windows.Int32Rect(0, 0, w, h), maskPixels, stride, 0);
UpdateBubbleResult();
}
/// <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; }
}
_bubbleRoi = null;
_bubbleMask = null;
_bubbleRoiStart = null;
_bubbleRoiDragging = false;
_bubbleRoiMoving = false;
_bubbleRoiResizing = false;
_bubbleBrushDragging = false;
_bubbleTool = BubbleSubTool.Roi;
_bubbleUndoStack.Clear();
}
// 气泡工具切换事件
public static readonly RoutedEvent BubbleToolChangedEvent =
EventManager.RegisterRoutedEvent(nameof(BubbleToolChanged), RoutingStrategy.Bubble,
typeof(RoutedEventHandler), typeof(PolygonRoiCanvas));
public event RoutedEventHandler BubbleToolChanged
{
add { AddHandler(BubbleToolChangedEvent, value); }
remove { RemoveHandler(BubbleToolChangedEvent, value); }
}
private void RaiseBubbleToolChanged()
{
RaiseEvent(new RoutedEventArgs(BubbleToolChangedEvent));
}
// ── 重编号 ──
private void RenumberAll()
{
for (int i = 0; i < _ppGroups.Count; i++)
{
_ppGroups[i].Index = i + 1;
_ppGroups[i].UpdateLine();
_ppGroups[i].UpdateLabel(FormatDistance(_ppGroups[i].Distance));
}
for (int i = 0; i < _ptlGroups.Count; i++)
{
_ptlGroups[i].Index = i + 1;
_ptlGroups[i].UpdateVisuals(FormatDistance(_ptlGroups[i].Distance));
}
for (int i = 0; i < _angleGroups.Count; i++)
{
_angleGroups[i].Index = i + 1;
_angleGroups[i].UpdateVisuals();
}
for (int i = 0; i < _frGroups.Count; i++)
{
_frGroups[i].Index = i + 1;
_frGroups[i].UpdateVisuals();
}
for (int i = 0; i < _bgaGroups.Count; i++)
{
_bgaGroups[i].Index = i + 1;
_bgaGroups[i].UpdateLabel();
}
}
// ── 填锡率阈值编辑 ──
private TextBox _thtEditBox;
private TextBlock _thtEditLabel;
private void RemoveTHTEditor()
{
if (_thtEditBox != null && _measureOverlay != null)
_measureOverlay.Children.Remove(_thtEditBox);
if (_thtEditLabel != null && _measureOverlay != null)
_measureOverlay.Children.Remove(_thtEditLabel);
_thtEditBox = null;
_thtEditLabel = null;
}
private void ShowTHTLimitEditor(Models.FillRateGroup g)
{
if (_measureOverlay == null) return;
// 移除旧的编辑框
if (_thtEditBox != null)
{
_measureOverlay.Children.Remove(_thtEditBox);
_thtEditBox = null;
}
if (_thtEditLabel != null)
{
_measureOverlay.Children.Remove(_thtEditLabel);
_thtEditLabel = null;
}
double left = Canvas.GetLeft(g.Label);
double top = Canvas.GetTop(g.Label) + 22;
// 参数名称提示
_thtEditLabel = new TextBlock
{
Text = "THTLimit(%):",
FontSize = 11,
Foreground = Brushes.White,
Background = new SolidColorBrush(Color.FromArgb(180, 0, 0, 0)),
Padding = new Thickness(3, 1, 3, 1)
};
Canvas.SetLeft(_thtEditLabel, left);
Canvas.SetTop(_thtEditLabel, top);
_measureOverlay.Children.Add(_thtEditLabel);
// 输入框
_thtEditBox = new TextBox
{
Text = g.THTLimit.ToString("F1"),
Width = 60,
Height = 22,
FontSize = 12,
Background = Brushes.White,
BorderBrush = Brushes.Orange,
BorderThickness = new Thickness(2),
Padding = new Thickness(2, 0, 2, 0)
};
Canvas.SetLeft(_thtEditBox, left + 80);
Canvas.SetTop(_thtEditBox, top);
_measureOverlay.Children.Add(_thtEditBox);
_thtEditBox.Focus();
_thtEditBox.SelectAll();
// 回车确认
_thtEditBox.KeyDown += (s, ev) =>
{
if (ev.Key == System.Windows.Input.Key.Enter)
{
if (double.TryParse(_thtEditBox.Text, out double val))
{
g.THTLimit = System.Math.Clamp(val, 0, 100);
g.UpdateVisuals();
RaiseMeasureCompleted(g.E3, g.E4, g.FillRate, MeasureCount, "FillRate");
}
RemoveTHTEditor();
}
else if (ev.Key == System.Windows.Input.Key.Escape)
{
RemoveTHTEditor();
}
};
// 失焦也关闭
_thtEditBox.LostFocus += (s, ev) => RemoveTHTEditor();
}
// ── 事件 ──
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 Canvas_MouseWheel(object sender, MouseWheelEventArgs e)
{
double oldZoom = ZoomScale;
double newZoom = e.Delta > 0 ? oldZoom * ZoomStep : oldZoom / ZoomStep;
newZoom = Math.Max(0.1, Math.Min(10.0, newZoom));
if (Math.Abs(newZoom - oldZoom) > 0.001)
{
ZoomScale = newZoom;
// RenderTransformOrigin="0.5,0.5" 保证以图像中心等比缩放
// 拖拽平移偏移保持不变
}
e.Handled = true;
}
private void Canvas_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
// 气泡测量模式:ROI 拖拽 / 画笔涂抹
if (CurrentMeasureMode == Models.MeasureMode.BubbleMeasure)
{
var pos = e.GetPosition(mainCanvas);
if (_bubbleTool == BubbleSubTool.Roi)
{
// 检查是否点击了右下角手柄
if (_bubbleRoiHandle != null && _bubbleRoi.HasValue)
{
var hx = Canvas.GetLeft(_bubbleRoiHandle) + _bubbleRoiHandle.Width / 2;
var hy = Canvas.GetTop(_bubbleRoiHandle) + _bubbleRoiHandle.Height / 2;
if (Math.Abs(pos.X - hx) < 10 && Math.Abs(pos.Y - hy) < 10)
{
_bubbleRoiResizing = true;
mainCanvas.CaptureMouse();
e.Handled = true;
return;
}
}
// 检查是否点击了 ROI 内部(拖动)
if (_bubbleRoi.HasValue && _bubbleRoi.Value.Contains(pos))
{
_bubbleRoiMoving = true;
_bubbleRoiDragOffset = new Point(pos.X - _bubbleRoi.Value.X, pos.Y - _bubbleRoi.Value.Y);
mainCanvas.CaptureMouse();
e.Handled = true;
return;
}
// 没有 ROI 时才画新的,有 ROI 时点击外部不拦截(让图像正常拖动)
if (!_bubbleRoi.HasValue)
{
_bubbleRoiStart = pos;
_bubbleRoiDragging = true;
EnsureBubbleRoiVisuals();
mainCanvas.CaptureMouse();
e.Handled = true;
return;
}
// 有 ROI 但点击了外部 → 不拦截,走正常拖动逻辑
}
if ((_bubbleTool == BubbleSubTool.Brush || _bubbleTool == BubbleSubTool.Eraser) && _bubbleRoi.HasValue)
{
// 只在 ROI 内才启动画笔,ROI 外不拦截,让图像正常拖动
if (_bubbleRoi.Value.Contains(pos))
{
SaveMaskSnapshot();
_bubbleBrushDragging = true;
ApplyBrushAt(pos);
mainCanvas.CaptureMouse();
e.Handled = true;
return;
}
}
// 魔棒在 MouseUpCanvasClicked)中处理
}
lastMousePosition = e.GetPosition(imageDisplayGrid);
isDragging = false;
mainCanvas.CaptureMouse();
}
private void Canvas_MouseMove(object sender, MouseEventArgs e)
{
// 气泡测量:ROI 拖拽画新矩形
if (_bubbleRoiDragging && _bubbleRoiStart.HasValue && _bubbleRoiRect != null)
{
var pos = e.GetPosition(mainCanvas);
double x = Math.Min(_bubbleRoiStart.Value.X, pos.X);
double y = Math.Min(_bubbleRoiStart.Value.Y, pos.Y);
double w = Math.Abs(pos.X - _bubbleRoiStart.Value.X);
double h = Math.Abs(pos.Y - _bubbleRoiStart.Value.Y);
Canvas.SetLeft(_bubbleRoiRect, x);
Canvas.SetTop(_bubbleRoiRect, y);
_bubbleRoiRect.Width = w;
_bubbleRoiRect.Height = h;
_bubbleRoiRect.Visibility = Visibility.Visible;
if (_bubbleRoiHandle != null) _bubbleRoiHandle.Visibility = Visibility.Collapsed;
e.Handled = true;
return;
}
// 气泡测量:拖动 ROI
if (_bubbleRoiMoving && _bubbleRoi.HasValue)
{
var pos = e.GetPosition(mainCanvas);
double nx = pos.X - _bubbleRoiDragOffset.X;
double ny = pos.Y - _bubbleRoiDragOffset.Y;
_bubbleRoi = new Rect(nx, ny, _bubbleRoi.Value.Width, _bubbleRoi.Value.Height);
SyncBubbleRoiVisuals();
e.Handled = true;
return;
}
// 气泡测量:右下角调整大小
if (_bubbleRoiResizing && _bubbleRoi.HasValue)
{
var pos = e.GetPosition(mainCanvas);
double w = Math.Max(20, pos.X - _bubbleRoi.Value.X);
double h = Math.Max(20, pos.Y - _bubbleRoi.Value.Y);
_bubbleRoi = new Rect(_bubbleRoi.Value.X, _bubbleRoi.Value.Y, w, h);
SyncBubbleRoiVisuals();
e.Handled = true;
return;
}
// 气泡测量:画笔/橡皮拖拽
if (_bubbleBrushDragging && _bubbleRoi.HasValue)
{
var pos = e.GetPosition(mainCanvas);
ApplyBrushAt(pos);
e.Handled = true;
return;
}
if (e.LeftButton == MouseButtonState.Pressed && mainCanvas.IsMouseCaptured)
{
Point currentPosition = e.GetPosition(imageDisplayGrid);
Vector delta = currentPosition - lastMousePosition;
if (delta.Length > 5)
{
isDragging = true;
PanOffsetX += delta.X;
PanOffsetY += delta.Y;
lastMousePosition = currentPosition;
}
}
}
private void Canvas_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
// 气泡测量:ROI 拖拽完成
if (_bubbleRoiDragging && _bubbleRoiStart.HasValue)
{
var pos = e.GetPosition(mainCanvas);
double x = Math.Min(_bubbleRoiStart.Value.X, pos.X);
double y = Math.Min(_bubbleRoiStart.Value.Y, pos.Y);
double w = Math.Abs(pos.X - _bubbleRoiStart.Value.X);
double h = Math.Abs(pos.Y - _bubbleRoiStart.Value.Y);
_bubbleRoiDragging = false;
_bubbleRoiStart = null;
mainCanvas.ReleaseMouseCapture();
if (w > 5 && h > 5)
{
_bubbleRoi = new Rect(x, y, w, h);
SyncBubbleRoiVisuals();
InitBubbleMask();
RaiseMeasureStatusChanged($"ROI 已设置: {w:F0}×{h:F0},可拖动/调整大小,或在面板切换魔棒工具");
}
e.Handled = true;
return;
}
// 气泡测量:拖动/调整完成
if (_bubbleRoiMoving || _bubbleRoiResizing)
{
_bubbleRoiMoving = false;
_bubbleRoiResizing = false;
mainCanvas.ReleaseMouseCapture();
if (_bubbleRoi.HasValue)
RaiseMeasureStatusChanged($"ROI: {_bubbleRoi.Value.Width:F0}×{_bubbleRoi.Value.Height:F0}");
e.Handled = true;
return;
}
// 气泡测量:画笔/橡皮松开
if (_bubbleBrushDragging)
{
_bubbleBrushDragging = false;
mainCanvas.ReleaseMouseCapture();
UpdateBubbleResult();
e.Handled = true;
return;
}
mainCanvas.ReleaseMouseCapture();
if (!isDragging)
{
Point clickPosition = e.GetPosition(mainCanvas);
if (IsMeasuring)
{
HandleMeasureClick(clickPosition);
// 魔棒点击
if (CurrentMeasureMode == Models.MeasureMode.BubbleMeasure && _bubbleTool == BubbleSubTool.Wand && _bubbleRoi.HasValue)
{
WandFloodFill(clickPosition);
}
}
OnCanvasClicked(clickPosition);
}
isDragging = false;
}
private void Canvas_MouseRightButtonDown(object sender, MouseButtonEventArgs e)
{
// BGA 模式下右键切换气泡/焊球
if (CurrentMeasureMode == Models.MeasureMode.BgaVoid && _bgaCurrent != null)
{
HandleBgaRightClick();
e.Handled = true;
return;
}
// 右键点击完成多边形
OnRightClick();
// 不设 e.Handled,让 ContextMenu 正常弹出
}
private void Canvas_PreviewMouseRightButtonUp(object sender, MouseButtonEventArgs e)
{
// BGA 模式下阻止 ContextMenu 弹出
if (CurrentMeasureMode == Models.MeasureMode.BgaVoid && _bgaCurrent != null)
{
e.Handled = true;
}
}
private void ROI_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
// 选择ROI
if (sender is FrameworkElement element && element.DataContext is ROIShape roi)
{
SelectedROI = roi;
e.Handled = true;
}
}
#endregion Mouse Events
#region Public Methods
public void ResetView()
{
PanOffsetX = 0;
PanOffsetY = 0;
if (imageDisplayGrid == null || CanvasWidth <= 0 || CanvasHeight <= 0)
{
ZoomScale = 1.0;
return;
}
// 延迟到布局完成后计算,确保 ActualWidth/Height 准确
Dispatcher.BeginInvoke(new Action(() =>
{
double viewW = imageDisplayGrid.ActualWidth;
double viewH = imageDisplayGrid.ActualHeight;
if (viewW > 0 && viewH > 0)
{
ZoomScale = Math.Min(viewW / CanvasWidth, viewH / CanvasHeight);
}
else
{
ZoomScale = 1.0;
}
PanOffsetX = 0;
PanOffsetY = 0;
}), System.Windows.Threading.DispatcherPriority.Render);
}
private void BtnZoomIn_Click(object sender, RoutedEventArgs e)
{
double newZoom = ZoomScale * 1.2;
if (newZoom <= 10.0)
{
ZoomScale = newZoom;
PanOffsetX = 0;
PanOffsetY = 0;
}
}
private void BtnZoomOut_Click(object sender, RoutedEventArgs e)
{
double newZoom = ZoomScale / 1.2;
if (newZoom >= 0.1)
{
ZoomScale = newZoom;
PanOffsetX = 0;
PanOffsetY = 0;
}
}
private void BtnReset_Click(object sender, RoutedEventArgs e)
{
ResetView();
}
#endregion Public Methods
#region Events
public static readonly RoutedEvent CanvasClickedEvent =
EventManager.RegisterRoutedEvent(nameof(CanvasClicked), RoutingStrategy.Bubble,
typeof(RoutedEventHandler), typeof(PolygonRoiCanvas));
public event RoutedEventHandler CanvasClicked
{
add { AddHandler(CanvasClickedEvent, value); }
remove { RemoveHandler(CanvasClickedEvent, value); }
}
protected virtual void OnCanvasClicked(Point position)
{
var args = new CanvasClickedEventArgs(CanvasClickedEvent, position);
RaiseEvent(args);
}
public static readonly RoutedEvent RightClickEvent =
EventManager.RegisterRoutedEvent(nameof(RightClick), RoutingStrategy.Bubble,
typeof(RoutedEventHandler), typeof(PolygonRoiCanvas));
public event RoutedEventHandler RightClick
{
add { AddHandler(RightClickEvent, value); }
remove { RemoveHandler(RightClickEvent, value); }
}
protected virtual void OnRightClick()
{
RaiseEvent(new RoutedEventArgs(RightClickEvent));
}
#endregion Events
}
}