集成测量工具:点点距、点线距、角度测量;十字辅助线;右键菜单;图像自适应窗口优化
This commit is contained in:
@@ -30,6 +30,7 @@ bld/
|
||||
lib/
|
||||
XP.ImageProcessing/
|
||||
ImageProcessing.sln
|
||||
ExternalLibraries/
|
||||
|
||||
# 排除 Libs 目录中的 DLL 和 PDB 文件(但保留目录结构)
|
||||
XplorePlane/Libs/Hardware/*.dll
|
||||
|
||||
@@ -344,6 +344,7 @@ namespace XP.ImageProcessing.RoiControl.Controls
|
||||
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 Ellipse _pendingDot;
|
||||
@@ -355,10 +356,16 @@ namespace XP.ImageProcessing.RoiControl.Controls
|
||||
private Line _ptlTempLine;
|
||||
private Point? _ptlTempL1, _ptlTempL2;
|
||||
|
||||
// 角度测量临时状态
|
||||
private int _angleClickCount;
|
||||
private Ellipse _angleTempVDot, _angleTempADot;
|
||||
private Line _angleTempLineA;
|
||||
private Point? _angleTempV, _angleTempA;
|
||||
|
||||
// 拖拽状态
|
||||
private Ellipse _mDraggingDot;
|
||||
private object _mDraggingOwner; // MeasureGroup 或 PointToLineGroup
|
||||
private string _mDraggingRole; // "Dot1","Dot2","DotL1","DotL2","DotP"
|
||||
private object _mDraggingOwner;
|
||||
private string _mDraggingRole;
|
||||
|
||||
private static void OnMeasureModeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
@@ -383,8 +390,11 @@ namespace XP.ImageProcessing.RoiControl.Controls
|
||||
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;
|
||||
_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;
|
||||
}
|
||||
|
||||
private void EnsureMeasureOverlay()
|
||||
@@ -399,16 +409,17 @@ namespace XP.ImageProcessing.RoiControl.Controls
|
||||
private void RemoveMeasureOverlay()
|
||||
{
|
||||
if (_measureOverlay != null) { mainCanvas.Children.Remove(_measureOverlay); _measureOverlay = null; }
|
||||
_ppGroups.Clear();
|
||||
_ptlGroups.Clear();
|
||||
_ppGroups.Clear(); _ptlGroups.Clear(); _angleGroups.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;
|
||||
public int MeasureCount => _ppGroups.Count + _ptlGroups.Count + _angleGroups.Count;
|
||||
|
||||
// ── 点击分发 ──
|
||||
|
||||
@@ -421,6 +432,8 @@ namespace XP.ImageProcessing.RoiControl.Controls
|
||||
HandlePointDistanceClick(pos);
|
||||
else if (CurrentMeasureMode == Models.MeasureMode.PointToLine)
|
||||
HandlePointToLineClick(pos);
|
||||
else if (CurrentMeasureMode == Models.MeasureMode.Angle)
|
||||
HandleAngleClick(pos);
|
||||
}
|
||||
|
||||
// ── 点点距 ──
|
||||
@@ -523,6 +536,65 @@ namespace XP.ImageProcessing.RoiControl.Controls
|
||||
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);
|
||||
|
||||
// 移除临时元素
|
||||
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 Ellipse CreateMDot(Brush fill)
|
||||
@@ -561,6 +633,16 @@ namespace XP.ImageProcessing.RoiControl.Controls
|
||||
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) { _mDraggingDot = dot; dot.CaptureMouse(); e.Handled = true; }
|
||||
}
|
||||
|
||||
@@ -586,6 +668,14 @@ namespace XP.ImageProcessing.RoiControl.Controls
|
||||
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");
|
||||
}
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
@@ -622,6 +712,18 @@ namespace XP.ImageProcessing.RoiControl.Controls
|
||||
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);
|
||||
RaiseMeasureStatusChanged($"已删除测量 | 剩余 {MeasureCount} 条");
|
||||
e.Handled = true; return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── 事件 ──
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
using System;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Shapes;
|
||||
|
||||
namespace XP.ImageProcessing.RoiControl.Models
|
||||
{
|
||||
/// <summary>一次角度测量的所有视觉元素(顶点V + 射线端点A/B + 两条射线 + 弧线 + 标签)</summary>
|
||||
public class AngleGroup
|
||||
{
|
||||
public Ellipse DotV { get; set; } // 顶点
|
||||
public Ellipse DotA { get; set; } // 射线端点A
|
||||
public Ellipse DotB { get; set; } // 射线端点B
|
||||
public Line LineA { get; set; } // 射线VA
|
||||
public Line LineB { get; set; } // 射线VB
|
||||
public Path Arc { get; set; } // 角度弧线
|
||||
public TextBlock Label { get; set; }
|
||||
public Point V { get; set; }
|
||||
public Point A { get; set; }
|
||||
public Point B { get; set; }
|
||||
|
||||
public double AngleDeg
|
||||
{
|
||||
get
|
||||
{
|
||||
double vax = A.X - V.X, vay = A.Y - V.Y;
|
||||
double vbx = B.X - V.X, vby = B.Y - V.Y;
|
||||
double lenA = Math.Sqrt(vax * vax + vay * vay);
|
||||
double lenB = Math.Sqrt(vbx * vbx + vby * vby);
|
||||
if (lenA < 0.001 || lenB < 0.001) return 0;
|
||||
double dot = vax * vbx + vay * vby;
|
||||
double cos = Math.Clamp(dot / (lenA * lenB), -1.0, 1.0);
|
||||
return Math.Acos(cos) * 180.0 / Math.PI;
|
||||
}
|
||||
}
|
||||
|
||||
public void UpdateVisuals()
|
||||
{
|
||||
// 射线
|
||||
LineA.X1 = V.X; LineA.Y1 = V.Y; LineA.X2 = A.X; LineA.Y2 = A.Y;
|
||||
LineA.Visibility = Visibility.Visible;
|
||||
LineB.X1 = V.X; LineB.Y1 = V.Y; LineB.X2 = B.X; LineB.Y2 = B.Y;
|
||||
LineB.Visibility = Visibility.Visible;
|
||||
|
||||
// 弧线
|
||||
double vax = A.X - V.X, vay = A.Y - V.Y;
|
||||
double vbx = B.X - V.X, vby = B.Y - V.Y;
|
||||
double lenA = Math.Sqrt(vax * vax + vay * vay);
|
||||
double lenB = Math.Sqrt(vbx * vbx + vby * vby);
|
||||
|
||||
if (lenA < 1 || lenB < 1) { Arc.Visibility = Visibility.Collapsed; return; }
|
||||
|
||||
double arcRadius = Math.Min(30, Math.Min(lenA, lenB) * 0.3);
|
||||
if (arcRadius < 8) arcRadius = 8;
|
||||
|
||||
double angleARad = Math.Atan2(vay, vax);
|
||||
double angleBRad = Math.Atan2(vby, vbx);
|
||||
double cross = vax * vby - vay * vbx;
|
||||
double angleDeg = AngleDeg;
|
||||
bool isLargeArc = angleDeg > 180;
|
||||
var sweepDir = cross >= 0 ? SweepDirection.Clockwise : SweepDirection.Counterclockwise;
|
||||
|
||||
var startPt = new Point(V.X + arcRadius * Math.Cos(angleARad), V.Y + arcRadius * Math.Sin(angleARad));
|
||||
var endPt = new Point(V.X + arcRadius * Math.Cos(angleBRad), V.Y + arcRadius * Math.Sin(angleBRad));
|
||||
|
||||
var arcSeg = new ArcSegment(endPt, new Size(arcRadius, arcRadius), 0, isLargeArc, sweepDir, true);
|
||||
var fig = new PathFigure(startPt, new[] { arcSeg }, false);
|
||||
Arc.Data = new PathGeometry(new[] { fig });
|
||||
Arc.Visibility = Visibility.Visible;
|
||||
|
||||
// 标签位置
|
||||
double midAngle = (angleARad + angleBRad) / 2.0;
|
||||
double testX = Math.Cos(midAngle), testY = Math.Sin(midAngle);
|
||||
double testCross = vax * testY - vay * testX;
|
||||
if ((cross >= 0 && testCross < 0) || (cross < 0 && testCross >= 0))
|
||||
midAngle += Math.PI;
|
||||
|
||||
double labelDist = arcRadius + 16;
|
||||
Label.Text = $"{angleDeg:F1}°";
|
||||
Canvas.SetLeft(Label, V.X + labelDist * Math.Cos(midAngle) - 15);
|
||||
Canvas.SetTop(Label, V.Y + labelDist * Math.Sin(midAngle) - 8);
|
||||
Label.Visibility = Visibility.Visible;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ namespace XP.ImageProcessing.RoiControl.Models
|
||||
{
|
||||
None,
|
||||
PointDistance,
|
||||
PointToLine
|
||||
PointToLine,
|
||||
Angle
|
||||
}
|
||||
}
|
||||
|
||||
@@ -455,7 +455,7 @@ namespace XplorePlane.ViewModels
|
||||
private void ExecuteAngleMeasure()
|
||||
{
|
||||
_logger.Info("角度测量功能已触发");
|
||||
// TODO: 实现角度测量逻辑
|
||||
_eventAggregator.GetEvent<MeasurementToolEvent>().Publish(MeasurementToolMode.Angle);
|
||||
}
|
||||
|
||||
private void ExecuteThroughHoleFillRateMeasure()
|
||||
|
||||
@@ -25,9 +25,17 @@ namespace XplorePlane.Views
|
||||
{
|
||||
if (e is MeasureCompletedEventArgs args && DataContext is ViewportPanelViewModel vm)
|
||||
{
|
||||
vm.MeasurementResult = $"{args.Distance:F2} px";
|
||||
string typeLabel = args.MeasureType == "PointToLine" ? "点线距" : "点点距";
|
||||
vm.ImageInfo = $"{typeLabel}: {args.Distance:F2} px | ({args.P1.X:F0},{args.P1.Y:F0}) → ({args.P2.X:F0},{args.P2.Y:F0}) | 共 {args.TotalCount} 条测量";
|
||||
string typeLabel = args.MeasureType switch
|
||||
{
|
||||
"PointToLine" => "点线距",
|
||||
"Angle" => "角度",
|
||||
_ => "点点距"
|
||||
};
|
||||
string valueText = args.MeasureType == "Angle"
|
||||
? $"{args.Distance:F2}°"
|
||||
: $"{args.Distance:F2} px";
|
||||
vm.MeasurementResult = valueText;
|
||||
vm.ImageInfo = $"{typeLabel}: {valueText} | 共 {args.TotalCount} 条测量";
|
||||
}
|
||||
};
|
||||
RoiCanvas.MeasureStatusChanged += (s, e) =>
|
||||
@@ -52,6 +60,7 @@ namespace XplorePlane.Views
|
||||
{
|
||||
MeasurementToolMode.PointDistance => XP.ImageProcessing.RoiControl.Models.MeasureMode.PointDistance,
|
||||
MeasurementToolMode.PointLineDistance => XP.ImageProcessing.RoiControl.Models.MeasureMode.PointToLine,
|
||||
MeasurementToolMode.Angle => XP.ImageProcessing.RoiControl.Models.MeasureMode.Angle,
|
||||
_ => XP.ImageProcessing.RoiControl.Models.MeasureMode.None
|
||||
};
|
||||
}, Prism.Events.ThreadOption.UIThread);
|
||||
|
||||
Reference in New Issue
Block a user