12938764b1
- 新增 EdgeLineFitProcessor 算子(卡尺边缘检测 + 最小二乘/RANSAC直线拟合) - 新增 EdgeLineFitPanel 辅助面板(参数配置、交互绘制卡尺) - 支持任意角度旋转的卡尺区域,4个手柄控制长度/宽度 - 支持多次拟合累积显示,关闭面板后结果保留 - 极性箭头标识搜索方向(B→D / D→B / 双向) - 卡尺亮绿色1px,拟合直线蓝色2px - Ribbon快捷工具组新增「直线拟合」按钮 - 添加中英文本地化资源
632 lines
26 KiB
C#
632 lines
26 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Windows;
|
|
using System.Windows.Controls;
|
|
using System.Windows.Media;
|
|
using System.Windows.Media.Imaging;
|
|
using System.Windows.Shapes;
|
|
using Emgu.CV;
|
|
using Emgu.CV.Structure;
|
|
using Prism.Commands;
|
|
using Prism.Mvvm;
|
|
using XP.ImageProcessing.Processors;
|
|
using XP.ImageProcessing.RoiControl.Controls;
|
|
using XplorePlane.Services.MainViewport;
|
|
using Brushes = System.Windows.Media.Brushes;
|
|
using Ellipse = System.Windows.Shapes.Ellipse;
|
|
using Point = System.Windows.Point;
|
|
|
|
namespace XplorePlane.ViewModels.ImageProcessing
|
|
{
|
|
/// <summary>
|
|
/// 边缘查找拟合直线 ViewModel
|
|
/// 支持多次拟合,每次点击"画卡尺"开始一次新的测量,结果累积保留
|
|
/// 关闭面板时保留所有结果,仅清除当前正在编辑的临时卡尺
|
|
/// </summary>
|
|
public class EdgeLineFitViewModel : BindableBase
|
|
{
|
|
private readonly IMainViewportService _viewportService;
|
|
private PolygonRoiCanvas _canvas;
|
|
private Canvas _mainCanvas;
|
|
|
|
// 当前正在编辑的搜索线
|
|
private Point _lineStart;
|
|
private Point _lineEnd;
|
|
private double _halfWidth = 30;
|
|
private bool _lineDefined;
|
|
|
|
// 当前编辑中的临时可视化(卡尺框+手柄,拟合前可调整)
|
|
private readonly List<UIElement> _tempOverlays = new();
|
|
|
|
// 已完成的拟合结果(永久保留在画布上)
|
|
// 不由本类管理生命周期,关闭面板后仍保留
|
|
private readonly List<UIElement> _committedOverlays = new();
|
|
|
|
// 手柄位置
|
|
private Point _handleStartPos, _handleEndPos, _handleTopPos, _handleBottomPos;
|
|
|
|
// 交互状态
|
|
private enum DragTarget { None, Start, End, Top, Bottom }
|
|
private DragTarget _dragging = DragTarget.None;
|
|
private bool _isDrawingLine;
|
|
private int _drawClickCount;
|
|
private int _fitCount;
|
|
|
|
private const double HandleSize = 12;
|
|
private const double HitRadius = 10;
|
|
private static readonly SolidColorBrush CaliperStroke;
|
|
private static readonly SolidColorBrush CaliperFill;
|
|
private static readonly SolidColorBrush FitLineBrush;
|
|
private static readonly SolidColorBrush HandleFill;
|
|
|
|
static EdgeLineFitViewModel()
|
|
{
|
|
CaliperStroke = new SolidColorBrush(Color.FromRgb(0, 255, 0));
|
|
CaliperStroke.Freeze();
|
|
CaliperFill = new SolidColorBrush(Color.FromArgb(20, 0, 255, 0));
|
|
CaliperFill.Freeze();
|
|
FitLineBrush = new SolidColorBrush(Color.FromRgb(30, 144, 255));
|
|
FitLineBrush.Freeze();
|
|
HandleFill = new SolidColorBrush(Color.FromArgb(220, 255, 255, 255));
|
|
HandleFill.Freeze();
|
|
}
|
|
|
|
public EdgeLineFitViewModel(IMainViewportService viewportService)
|
|
{
|
|
_viewportService = viewportService;
|
|
FitCommand = new DelegateCommand(ExecuteFit, () => _lineDefined);
|
|
ClearAllCommand = new DelegateCommand(ExecuteClearAll);
|
|
DrawCaliperCommand = new DelegateCommand(ExecuteDrawCaliper);
|
|
}
|
|
|
|
// ── 命令 ──
|
|
public DelegateCommand FitCommand { get; }
|
|
public DelegateCommand ClearAllCommand { get; }
|
|
public DelegateCommand DrawCaliperCommand { get; }
|
|
|
|
// ── 参数 ──
|
|
private int _caliperCount = 20;
|
|
public int CaliperCount
|
|
{
|
|
get => _caliperCount;
|
|
set { if (SetProperty(ref _caliperCount, value)) RedrawTemp(); }
|
|
}
|
|
|
|
private int _displayWidth = 60;
|
|
public int DisplayWidth
|
|
{
|
|
get => _displayWidth;
|
|
set
|
|
{
|
|
if (SetProperty(ref _displayWidth, Math.Max(10, value)))
|
|
{
|
|
_halfWidth = _displayWidth / 2.0;
|
|
RedrawTemp();
|
|
}
|
|
}
|
|
}
|
|
|
|
private string _edgePolarity = "Both";
|
|
public string EdgePolarity
|
|
{
|
|
get => _edgePolarity;
|
|
set { if (SetProperty(ref _edgePolarity, value)) RedrawTemp(); }
|
|
}
|
|
|
|
private int _edgeThreshold = 20;
|
|
public int EdgeThreshold { get => _edgeThreshold; set => SetProperty(ref _edgeThreshold, value); }
|
|
|
|
private double _sigma = 1.0;
|
|
public double Sigma { get => _sigma; set => SetProperty(ref _sigma, value); }
|
|
|
|
private string _fitMethod = "RANSAC";
|
|
public string FitMethod { get => _fitMethod; set => SetProperty(ref _fitMethod, value); }
|
|
|
|
private double _ransacThreshold = 2.0;
|
|
public double RansacThreshold { get => _ransacThreshold; set => SetProperty(ref _ransacThreshold, value); }
|
|
|
|
private string _resultText = "就绪 - 点击「画卡尺」开始";
|
|
public string ResultText { get => _resultText; set => SetProperty(ref _resultText, value); }
|
|
|
|
// ── 初始化 ──
|
|
public void SetCanvas(PolygonRoiCanvas canvas)
|
|
{
|
|
_canvas = canvas;
|
|
_mainCanvas = FindChild<Canvas>(canvas, "mainCanvas");
|
|
}
|
|
|
|
/// <summary>面板关闭时调用:仅清除临时编辑状态,保留已拟合结果</summary>
|
|
public void OnPanelClosed()
|
|
{
|
|
UnregisterAll();
|
|
ClearTempOverlays(); // 清除正在编辑的卡尺手柄
|
|
// _committedOverlays 保留在画布上不清除
|
|
}
|
|
|
|
// ══════════════════════════════════════════════════════════════
|
|
// 命令实现
|
|
// ══════════════════════════════════════════════════════════════
|
|
|
|
/// <summary>开始一次新的卡尺绘制(不影响已有结果)</summary>
|
|
private void ExecuteDrawCaliper()
|
|
{
|
|
// 清除当前临时编辑
|
|
ClearTempOverlays();
|
|
UnregisterAll();
|
|
_lineDefined = false;
|
|
_dragging = DragTarget.None;
|
|
FitCommand.RaiseCanExecuteChanged();
|
|
_drawClickCount = 0;
|
|
_isDrawingLine = true;
|
|
ResultText = "请在图像上点击搜索线起点";
|
|
RegisterInteraction();
|
|
}
|
|
|
|
/// <summary>执行拟合,将结果提交为永久显示</summary>
|
|
private void ExecuteFit()
|
|
{
|
|
if (!_lineDefined) return;
|
|
|
|
var imageSource = _viewportService?.CurrentDisplayImage as BitmapSource;
|
|
if (imageSource == null) { ResultText = "错误:无可用图像"; return; }
|
|
|
|
try
|
|
{
|
|
BitmapSource source = imageSource;
|
|
if (imageSource.Format != PixelFormats.Gray8)
|
|
source = new FormatConvertedBitmap(imageSource, PixelFormats.Gray8, null, 0);
|
|
|
|
int w = source.PixelWidth, h = source.PixelHeight;
|
|
int stride = w;
|
|
byte[] px = new byte[h * stride];
|
|
source.CopyPixels(px, stride, 0);
|
|
|
|
using var img = new Image<Gray, byte>(w, h);
|
|
for (int y = 0; y < h; y++)
|
|
for (int x = 0; x < w; x++)
|
|
img.Data[y, x, 0] = px[y * stride + x];
|
|
|
|
var proc = new EdgeLineFitProcessor();
|
|
proc.SetParameter("StartX", (int)_lineStart.X);
|
|
proc.SetParameter("StartY", (int)_lineStart.Y);
|
|
proc.SetParameter("EndX", (int)_lineEnd.X);
|
|
proc.SetParameter("EndY", (int)_lineEnd.Y);
|
|
proc.SetParameter("CaliperCount", CaliperCount);
|
|
proc.SetParameter("CaliperWidth", (int)(_halfWidth * 2));
|
|
proc.SetParameter("EdgePolarity", EdgePolarity);
|
|
proc.SetParameter("EdgeThreshold", EdgeThreshold);
|
|
proc.SetParameter("Sigma", Sigma);
|
|
proc.SetParameter("FitMethod", FitMethod);
|
|
proc.SetParameter("RansacThreshold", RansacThreshold);
|
|
|
|
var result = proc.Process(img);
|
|
var od = proc.OutputData;
|
|
|
|
if (od.ContainsKey("LineFitResult"))
|
|
{
|
|
var fr = od["LineFitResult"] as LineFitResult;
|
|
if (fr != null && fr.Success)
|
|
{
|
|
_fitCount++;
|
|
// 将当前卡尺从临时转为永久
|
|
CommitCurrentCaliper();
|
|
// 绘制拟合结果(永久)
|
|
DrawFitResult(fr, _fitCount);
|
|
ResultText = $"[#{_fitCount}] 拟合成功\n角度: {fr.AngleDegrees:F2}°\n" +
|
|
$"内点: {fr.Inliers.Count}/{fr.EdgePointCount}\n" +
|
|
$"误差: {fr.FitError:F3} px\n\n点击「画卡尺」继续下一条";
|
|
// 拟合完成后清除编辑状态,准备下一次
|
|
_lineDefined = false;
|
|
FitCommand.RaiseCanExecuteChanged();
|
|
UnregisterAll();
|
|
}
|
|
else
|
|
{
|
|
int ec = od.ContainsKey("EdgePointCount") ? (int)od["EdgePointCount"] : 0;
|
|
ResultText = $"拟合失败\n边缘点: {ec}\n请调整参数或拖拽手柄";
|
|
}
|
|
}
|
|
result.Dispose();
|
|
}
|
|
catch (Exception ex) { ResultText = $"异常: {ex.Message}"; }
|
|
}
|
|
|
|
/// <summary>清除所有(包括已拟合的结果)</summary>
|
|
private void ExecuteClearAll()
|
|
{
|
|
ClearTempOverlays();
|
|
// 清除所有已提交的结果
|
|
if (_mainCanvas != null)
|
|
{
|
|
foreach (var el in _committedOverlays)
|
|
_mainCanvas.Children.Remove(el);
|
|
}
|
|
_committedOverlays.Clear();
|
|
_fitCount = 0;
|
|
UnregisterAll();
|
|
_lineDefined = false;
|
|
_dragging = DragTarget.None;
|
|
FitCommand.RaiseCanExecuteChanged();
|
|
ResultText = "已清除所有结果";
|
|
}
|
|
|
|
// ══════════════════════════════════════════════════════════════
|
|
// 提交当前卡尺为永久显示
|
|
// ══════════════════════════════════════════════════════════════
|
|
|
|
/// <summary>将当前临时卡尺可视化转为永久(去掉手柄,保留边框和等分线)</summary>
|
|
private void CommitCurrentCaliper()
|
|
{
|
|
if (_mainCanvas == null) return;
|
|
|
|
// 移除临时元素
|
|
foreach (var el in _tempOverlays)
|
|
_mainCanvas.Children.Remove(el);
|
|
_tempOverlays.Clear();
|
|
|
|
// 重新绘制卡尺(无手柄,作为永久元素)
|
|
double dx = _lineEnd.X - _lineStart.X, dy = _lineEnd.Y - _lineStart.Y;
|
|
double len = Math.Sqrt(dx * dx + dy * dy);
|
|
if (len < 2) return;
|
|
|
|
double ux = dx / len, uy = dy / len;
|
|
double px = -uy, py = ux;
|
|
double hw = _halfWidth;
|
|
|
|
var c1 = new Point(_lineStart.X + px * hw, _lineStart.Y + py * hw);
|
|
var c2 = new Point(_lineEnd.X + px * hw, _lineEnd.Y + py * hw);
|
|
var c3 = new Point(_lineEnd.X - px * hw, _lineEnd.Y - py * hw);
|
|
var c4 = new Point(_lineStart.X - px * hw, _lineStart.Y - py * hw);
|
|
|
|
// 矩形边框(半透明,不抢眼)
|
|
var border = new Polygon
|
|
{
|
|
Points = new System.Windows.Media.PointCollection { c1, c2, c3, c4 },
|
|
Stroke = CaliperStroke, StrokeThickness = 1, Opacity = 0.5,
|
|
Fill = System.Windows.Media.Brushes.Transparent, IsHitTestVisible = false
|
|
};
|
|
_mainCanvas.Children.Add(border);
|
|
_committedOverlays.Add(border);
|
|
|
|
// 等分线
|
|
int count = CaliperCount;
|
|
double step = len / (count + 1);
|
|
for (int i = 1; i <= count; i++)
|
|
{
|
|
double cx = _lineStart.X + ux * step * i, cy = _lineStart.Y + uy * step * i;
|
|
var line = new Line
|
|
{
|
|
X1 = cx + px * hw, Y1 = cy + py * hw,
|
|
X2 = cx - px * hw, Y2 = cy - py * hw,
|
|
Stroke = CaliperStroke, StrokeThickness = 1, Opacity = 0.3, IsHitTestVisible = false
|
|
};
|
|
_mainCanvas.Children.Add(line);
|
|
_committedOverlays.Add(line);
|
|
}
|
|
}
|
|
|
|
// ══════════════════════════════════════════════════════════════
|
|
// 绘制拟合结果(永久)
|
|
// ══════════════════════════════════════════════════════════════
|
|
|
|
private void DrawFitResult(LineFitResult fr, int index)
|
|
{
|
|
if (_mainCanvas == null) return;
|
|
|
|
// 拟合直线(蓝色)
|
|
AddCommitted(new Line
|
|
{
|
|
X1 = fr.Endpoint1.X, Y1 = fr.Endpoint1.Y,
|
|
X2 = fr.Endpoint2.X, Y2 = fr.Endpoint2.Y,
|
|
Stroke = FitLineBrush, StrokeThickness = 2, IsHitTestVisible = false
|
|
});
|
|
|
|
// 内点
|
|
foreach (var pt in fr.Inliers)
|
|
AddCommitted(MakeDot(pt.X, pt.Y, Brushes.Lime));
|
|
// 外点
|
|
foreach (var pt in fr.Outliers)
|
|
AddCommitted(MakeDot(pt.X, pt.Y, Brushes.Red));
|
|
|
|
// 标注
|
|
var lbl = new TextBlock
|
|
{
|
|
Text = $"#{index} ∠{fr.AngleDegrees:F2}° Err:{fr.FitError:F2}px",
|
|
Foreground = FitLineBrush, FontSize = 11, FontWeight = FontWeights.Bold, IsHitTestVisible = false
|
|
};
|
|
Canvas.SetLeft(lbl, (fr.Endpoint1.X + fr.Endpoint2.X) / 2 + 5);
|
|
Canvas.SetTop(lbl, (fr.Endpoint1.Y + fr.Endpoint2.Y) / 2 - 18);
|
|
AddCommitted(lbl);
|
|
}
|
|
|
|
private Ellipse MakeDot(double x, double y, SolidColorBrush fill)
|
|
{
|
|
var d = new Ellipse { Width = 5, Height = 5, Fill = fill, IsHitTestVisible = false };
|
|
Canvas.SetLeft(d, x - 2.5); Canvas.SetTop(d, y - 2.5);
|
|
return d;
|
|
}
|
|
|
|
private void AddCommitted(UIElement el)
|
|
{
|
|
_mainCanvas.Children.Add(el);
|
|
_committedOverlays.Add(el);
|
|
}
|
|
|
|
// ══════════════════════════════════════════════════════════════
|
|
// 临时卡尺可视化(编辑中,带手柄)
|
|
// ══════════════════════════════════════════════════════════════
|
|
|
|
private void RedrawTemp()
|
|
{
|
|
if (!_lineDefined || _mainCanvas == null) return;
|
|
ClearTempOverlays();
|
|
DrawTempCaliper();
|
|
}
|
|
|
|
private void DrawTempCaliper()
|
|
{
|
|
if (_mainCanvas == null) return;
|
|
|
|
double dx = _lineEnd.X - _lineStart.X, dy = _lineEnd.Y - _lineStart.Y;
|
|
double len = Math.Sqrt(dx * dx + dy * dy);
|
|
if (len < 2) return;
|
|
|
|
double ux = dx / len, uy = dy / len;
|
|
double px = -uy, py = ux;
|
|
double hw = _halfWidth;
|
|
|
|
var c1 = new Point(_lineStart.X + px * hw, _lineStart.Y + py * hw);
|
|
var c2 = new Point(_lineEnd.X + px * hw, _lineEnd.Y + py * hw);
|
|
var c3 = new Point(_lineEnd.X - px * hw, _lineEnd.Y - py * hw);
|
|
var c4 = new Point(_lineStart.X - px * hw, _lineStart.Y - py * hw);
|
|
|
|
// 矩形
|
|
AddTemp(new Polygon
|
|
{
|
|
Points = new System.Windows.Media.PointCollection { c1, c2, c3, c4 },
|
|
Stroke = CaliperStroke, StrokeThickness = 1, Fill = CaliperFill, IsHitTestVisible = false
|
|
});
|
|
|
|
// 搜索线虚线
|
|
AddTemp(new Line
|
|
{
|
|
X1 = _lineStart.X, Y1 = _lineStart.Y, X2 = _lineEnd.X, Y2 = _lineEnd.Y,
|
|
Stroke = CaliperStroke, StrokeThickness = 1,
|
|
StrokeDashArray = new DoubleCollection { 4, 3 }, IsHitTestVisible = false
|
|
});
|
|
|
|
// 等分线
|
|
int count = CaliperCount;
|
|
double step = len / (count + 1);
|
|
for (int i = 1; i <= count; i++)
|
|
{
|
|
double cx = _lineStart.X + ux * step * i, cy = _lineStart.Y + uy * step * i;
|
|
AddTemp(new Line
|
|
{
|
|
X1 = cx + px * hw, Y1 = cy + py * hw,
|
|
X2 = cx - px * hw, Y2 = cy - py * hw,
|
|
Stroke = CaliperStroke, StrokeThickness = 1, Opacity = 0.4, IsHitTestVisible = false
|
|
});
|
|
}
|
|
|
|
// 极性箭头
|
|
DrawPolarityArrow(px, py);
|
|
|
|
// 手柄位置
|
|
double midX = (_lineStart.X + _lineEnd.X) / 2, midY = (_lineStart.Y + _lineEnd.Y) / 2;
|
|
_handleStartPos = _lineStart;
|
|
_handleEndPos = _lineEnd;
|
|
_handleTopPos = new Point(midX + px * hw, midY + py * hw);
|
|
_handleBottomPos = new Point(midX - px * hw, midY - py * hw);
|
|
|
|
// 绘制手柄
|
|
AddTemp(MakeHandleVisual(_handleStartPos));
|
|
AddTemp(MakeHandleVisual(_handleEndPos));
|
|
AddTemp(MakeHandleVisual(_handleTopPos));
|
|
AddTemp(MakeHandleVisual(_handleBottomPos));
|
|
}
|
|
|
|
private void DrawPolarityArrow(double px, double py)
|
|
{
|
|
double midX = (_lineStart.X + _lineEnd.X) / 2, midY = (_lineStart.Y + _lineEnd.Y) / 2;
|
|
double arrowLen = Math.Min(_halfWidth * 0.6, 16);
|
|
|
|
if (EdgePolarity == "Both")
|
|
{
|
|
DrawArrow(midX, midY, px, py, arrowLen);
|
|
DrawArrow(midX, midY, -px, -py, arrowLen);
|
|
}
|
|
else if (EdgePolarity == "DarkToBright")
|
|
DrawArrow(midX, midY, px, py, arrowLen);
|
|
else
|
|
DrawArrow(midX, midY, -px, -py, arrowLen);
|
|
|
|
string txt = EdgePolarity switch { "BrightToDark" => "B→D", "DarkToBright" => "D→B", _ => "↔" };
|
|
var tb = new TextBlock { Text = txt, Foreground = CaliperStroke, FontSize = 10, IsHitTestVisible = false };
|
|
Canvas.SetLeft(tb, midX + px * (_halfWidth + 12));
|
|
Canvas.SetTop(tb, midY + py * (_halfWidth + 12) - 7);
|
|
AddTemp(tb);
|
|
}
|
|
|
|
private void DrawArrow(double fx, double fy, double dx, double dy, double length)
|
|
{
|
|
double tx = fx + dx * length, ty = fy + dy * length;
|
|
AddTemp(new Line { X1 = fx, Y1 = fy, X2 = tx, Y2 = ty, Stroke = CaliperStroke, StrokeThickness = 1.5, IsHitTestVisible = false });
|
|
double ang = Math.Atan2(dy, dx), hl = 5;
|
|
double a1 = ang + 2.5, a2 = ang - 2.5;
|
|
AddTemp(new Line { X1 = tx, Y1 = ty, X2 = tx + Math.Cos(a1) * hl, Y2 = ty + Math.Sin(a1) * hl, Stroke = CaliperStroke, StrokeThickness = 1.5, IsHitTestVisible = false });
|
|
AddTemp(new Line { X1 = tx, Y1 = ty, X2 = tx + Math.Cos(a2) * hl, Y2 = ty + Math.Sin(a2) * hl, Stroke = CaliperStroke, StrokeThickness = 1.5, IsHitTestVisible = false });
|
|
}
|
|
|
|
private Ellipse MakeHandleVisual(Point pos)
|
|
{
|
|
var h = new Ellipse
|
|
{
|
|
Width = HandleSize, Height = HandleSize,
|
|
Fill = HandleFill, Stroke = CaliperStroke, StrokeThickness = 2,
|
|
IsHitTestVisible = false
|
|
};
|
|
Canvas.SetLeft(h, pos.X - HandleSize / 2);
|
|
Canvas.SetTop(h, pos.Y - HandleSize / 2);
|
|
return h;
|
|
}
|
|
|
|
private void AddTemp(UIElement el) { _mainCanvas.Children.Add(el); _tempOverlays.Add(el); }
|
|
private void ClearTempOverlays()
|
|
{
|
|
if (_mainCanvas == null) return;
|
|
foreach (var el in _tempOverlays) _mainCanvas.Children.Remove(el);
|
|
_tempOverlays.Clear();
|
|
}
|
|
|
|
// ══════════════════════════════════════════════════════════════
|
|
// 统一鼠标交互
|
|
// ══════════════════════════════════════════════════════════════
|
|
|
|
private bool _interactionRegistered;
|
|
|
|
private void RegisterInteraction()
|
|
{
|
|
if (_canvas == null || _interactionRegistered) return;
|
|
_canvas.PreviewMouseLeftButtonDown += OnMouseDown;
|
|
_canvas.PreviewMouseMove += OnMouseMove;
|
|
_canvas.PreviewMouseLeftButtonUp += OnMouseUp;
|
|
_interactionRegistered = true;
|
|
}
|
|
|
|
private void UnregisterAll()
|
|
{
|
|
if (_canvas == null || !_interactionRegistered) return;
|
|
_canvas.PreviewMouseLeftButtonDown -= OnMouseDown;
|
|
_canvas.PreviewMouseMove -= OnMouseMove;
|
|
_canvas.PreviewMouseLeftButtonUp -= OnMouseUp;
|
|
_interactionRegistered = false;
|
|
_isDrawingLine = false;
|
|
_dragging = DragTarget.None;
|
|
}
|
|
|
|
private void OnMouseDown(object sender, System.Windows.Input.MouseButtonEventArgs e)
|
|
{
|
|
if (_mainCanvas == null) return;
|
|
var pos = e.GetPosition(_mainCanvas);
|
|
|
|
// 绘制模式
|
|
if (_isDrawingLine)
|
|
{
|
|
_drawClickCount++;
|
|
if (_drawClickCount == 1)
|
|
{
|
|
_lineStart = pos;
|
|
ResultText = "请点击搜索线终点";
|
|
}
|
|
else if (_drawClickCount == 2)
|
|
{
|
|
_lineEnd = pos;
|
|
_isDrawingLine = false;
|
|
_lineDefined = true;
|
|
_halfWidth = DisplayWidth / 2.0;
|
|
FitCommand.RaiseCanExecuteChanged();
|
|
RedrawTemp();
|
|
ResultText = $"搜索线已定义 ({Len():F0}px)\n拖拽手柄调整,点击「拟合」执行";
|
|
}
|
|
e.Handled = true;
|
|
return;
|
|
}
|
|
|
|
// 拖拽模式
|
|
if (_lineDefined)
|
|
{
|
|
var target = HitTestHandle(pos);
|
|
if (target != DragTarget.None)
|
|
{
|
|
_dragging = target;
|
|
_canvas.CaptureMouse();
|
|
e.Handled = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
private void OnMouseMove(object sender, System.Windows.Input.MouseEventArgs e)
|
|
{
|
|
if (_dragging == DragTarget.None || _mainCanvas == null) return;
|
|
var pos = e.GetPosition(_mainCanvas);
|
|
|
|
switch (_dragging)
|
|
{
|
|
case DragTarget.Start:
|
|
_lineStart = pos;
|
|
break;
|
|
case DragTarget.End:
|
|
_lineEnd = pos;
|
|
break;
|
|
case DragTarget.Top:
|
|
case DragTarget.Bottom:
|
|
double dist = PointToLineDist(pos, _lineStart, _lineEnd);
|
|
_halfWidth = Math.Max(5, dist);
|
|
SetProperty(ref _displayWidth, (int)(_halfWidth * 2), nameof(DisplayWidth));
|
|
break;
|
|
}
|
|
|
|
ClearTempOverlays();
|
|
DrawTempCaliper();
|
|
e.Handled = true;
|
|
}
|
|
|
|
private void OnMouseUp(object sender, System.Windows.Input.MouseButtonEventArgs e)
|
|
{
|
|
if (_dragging == DragTarget.None) return;
|
|
_dragging = DragTarget.None;
|
|
_canvas.ReleaseMouseCapture();
|
|
ResultText = $"搜索线: {Len():F0}px, 宽度: {(int)(_halfWidth * 2)}px\n点击「拟合」执行";
|
|
e.Handled = true;
|
|
}
|
|
|
|
private DragTarget HitTestHandle(Point pos)
|
|
{
|
|
if (Dist(pos, _handleStartPos) <= HitRadius) return DragTarget.Start;
|
|
if (Dist(pos, _handleEndPos) <= HitRadius) return DragTarget.End;
|
|
if (Dist(pos, _handleTopPos) <= HitRadius) return DragTarget.Top;
|
|
if (Dist(pos, _handleBottomPos) <= HitRadius) return DragTarget.Bottom;
|
|
return DragTarget.None;
|
|
}
|
|
|
|
// ══════════════════════════════════════════════════════════════
|
|
// 辅助
|
|
// ══════════════════════════════════════════════════════════════
|
|
|
|
private double Len()
|
|
{
|
|
double dx = _lineEnd.X - _lineStart.X, dy = _lineEnd.Y - _lineStart.Y;
|
|
return Math.Sqrt(dx * dx + dy * dy);
|
|
}
|
|
|
|
private static double Dist(Point a, Point b)
|
|
{
|
|
double dx = a.X - b.X, dy = a.Y - b.Y;
|
|
return Math.Sqrt(dx * dx + dy * dy);
|
|
}
|
|
|
|
private static double PointToLineDist(Point p, Point a, Point b)
|
|
{
|
|
double abx = b.X - a.X, aby = b.Y - a.Y;
|
|
double len2 = abx * abx + aby * aby;
|
|
if (len2 < 1e-6) return Dist(p, a);
|
|
return Math.Abs(abx * (a.Y - p.Y) - aby * (a.X - p.X)) / Math.Sqrt(len2);
|
|
}
|
|
|
|
private static T FindChild<T>(DependencyObject parent, string name) where T : FrameworkElement
|
|
{
|
|
int count = VisualTreeHelper.GetChildrenCount(parent);
|
|
for (int i = 0; i < count; i++)
|
|
{
|
|
var child = VisualTreeHelper.GetChild(parent, i);
|
|
if (child is T t && t.Name == name) return t;
|
|
var r = FindChild<T>(child, name);
|
|
if (r != null) return r;
|
|
}
|
|
return null;
|
|
}
|
|
}
|
|
}
|