已合并 PR 38: 合并角度测量

集成测量工具:点点距、点线距、角度测量;十字辅助线;右键菜单;图像自适应窗口优化
This commit is contained in:
LI Wei.lw
2026-04-24 14:39:45 +08:00
6 changed files with 211 additions and 12 deletions
+1
View File
@@ -30,6 +30,7 @@ bld/
lib/ lib/
XP.ImageProcessing/ XP.ImageProcessing/
ImageProcessing.sln ImageProcessing.sln
ExternalLibraries/
# 排除 Libs 目录中的 DLL 和 PDB 文件(但保留目录结构) # 排除 Libs 目录中的 DLL 和 PDB 文件(但保留目录结构)
XplorePlane/Libs/Hardware/*.dll XplorePlane/Libs/Hardware/*.dll
@@ -344,6 +344,7 @@ namespace XP.ImageProcessing.RoiControl.Controls
private Canvas _measureOverlay; private Canvas _measureOverlay;
private readonly System.Collections.Generic.List<Models.MeasureGroup> _ppGroups = new(); 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.PointToLineGroup> _ptlGroups = new();
private readonly System.Collections.Generic.List<Models.AngleGroup> _angleGroups = new();
// 点点距临时状态 // 点点距临时状态
private Ellipse _pendingDot; private Ellipse _pendingDot;
@@ -355,10 +356,16 @@ namespace XP.ImageProcessing.RoiControl.Controls
private Line _ptlTempLine; private Line _ptlTempLine;
private Point? _ptlTempL1, _ptlTempL2; private Point? _ptlTempL1, _ptlTempL2;
// 角度测量临时状态
private int _angleClickCount;
private Ellipse _angleTempVDot, _angleTempADot;
private Line _angleTempLineA;
private Point? _angleTempV, _angleTempA;
// 拖拽状态 // 拖拽状态
private Ellipse _mDraggingDot; private Ellipse _mDraggingDot;
private object _mDraggingOwner; // MeasureGroup 或 PointToLineGroup private object _mDraggingOwner;
private string _mDraggingRole; // "Dot1","Dot2","DotL1","DotL2","DotP" private string _mDraggingRole;
private static void OnMeasureModeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) 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 (_ptlTempDot1 != null) { _measureOverlay.Children.Remove(_ptlTempDot1); _ptlTempDot1 = null; }
if (_ptlTempDot2 != null) { _measureOverlay.Children.Remove(_ptlTempDot2); _ptlTempDot2 = null; } if (_ptlTempDot2 != null) { _measureOverlay.Children.Remove(_ptlTempDot2); _ptlTempDot2 = null; }
if (_ptlTempLine != null) { _measureOverlay.Children.Remove(_ptlTempLine); _ptlTempLine = null; } if (_ptlTempLine != null) { _measureOverlay.Children.Remove(_ptlTempLine); _ptlTempLine = null; }
_ptlTempL1 = _ptlTempL2 = null; _ptlTempL1 = _ptlTempL2 = null; _ptlClickCount = 0;
_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() private void EnsureMeasureOverlay()
@@ -399,16 +409,17 @@ namespace XP.ImageProcessing.RoiControl.Controls
private void RemoveMeasureOverlay() private void RemoveMeasureOverlay()
{ {
if (_measureOverlay != null) { mainCanvas.Children.Remove(_measureOverlay); _measureOverlay = null; } if (_measureOverlay != null) { mainCanvas.Children.Remove(_measureOverlay); _measureOverlay = null; }
_ppGroups.Clear(); _ppGroups.Clear(); _ptlGroups.Clear(); _angleGroups.Clear();
_ptlGroups.Clear();
_pendingDot = null; _pendingPoint = null; _pendingDot = null; _pendingPoint = null;
_ptlTempDot1 = _ptlTempDot2 = null; _ptlTempLine = null; _ptlTempDot1 = _ptlTempDot2 = null; _ptlTempLine = null;
_ptlTempL1 = _ptlTempL2 = null; _ptlClickCount = 0; _ptlTempL1 = _ptlTempL2 = null; _ptlClickCount = 0;
_angleTempVDot = _angleTempADot = null; _angleTempLineA = null;
_angleTempV = _angleTempA = null; _angleClickCount = 0;
_mDraggingDot = null; _mDraggingOwner = null; _mDraggingDot = null; _mDraggingOwner = null;
} }
public void ClearMeasurements() => RemoveMeasureOverlay(); 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); HandlePointDistanceClick(pos);
else if (CurrentMeasureMode == Models.MeasureMode.PointToLine) else if (CurrentMeasureMode == Models.MeasureMode.PointToLine)
HandlePointToLineClick(pos); HandlePointToLineClick(pos);
else if (CurrentMeasureMode == Models.MeasureMode.Angle)
HandleAngleClick(pos);
} }
// ── 点点距 ── // ── 点点距 ──
@@ -523,6 +536,65 @@ namespace XP.ImageProcessing.RoiControl.Controls
return g; 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) private Ellipse CreateMDot(Brush fill)
@@ -561,6 +633,16 @@ namespace XP.ImageProcessing.RoiControl.Controls
if (g.DotP == dot) { _mDraggingOwner = g; _mDraggingRole = "DotP"; 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) { _mDraggingDot = dot; dot.CaptureMouse(); e.Handled = true; } if (_mDraggingOwner != null) { _mDraggingDot = dot; dot.CaptureMouse(); e.Handled = true; }
} }
@@ -586,6 +668,14 @@ namespace XP.ImageProcessing.RoiControl.Controls
var foot = ptlg.FootPoint; var foot = ptlg.FootPoint;
RaiseMeasureCompleted(ptlg.P, foot, ptlg.Distance, MeasureCount, "PointToLine"); 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; e.Handled = true;
} }
@@ -622,6 +712,18 @@ namespace XP.ImageProcessing.RoiControl.Controls
e.Handled = true; return; 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, None,
PointDistance, PointDistance,
PointToLine PointToLine,
Angle
} }
} }
+1 -1
View File
@@ -455,7 +455,7 @@ namespace XplorePlane.ViewModels
private void ExecuteAngleMeasure() private void ExecuteAngleMeasure()
{ {
_logger.Info("角度测量功能已触发"); _logger.Info("角度测量功能已触发");
// TODO: 实现角度测量逻辑 _eventAggregator.GetEvent<MeasurementToolEvent>().Publish(MeasurementToolMode.Angle);
} }
private void ExecuteThroughHoleFillRateMeasure() private void ExecuteThroughHoleFillRateMeasure()
@@ -25,9 +25,17 @@ namespace XplorePlane.Views
{ {
if (e is MeasureCompletedEventArgs args && DataContext is ViewportPanelViewModel vm) if (e is MeasureCompletedEventArgs args && DataContext is ViewportPanelViewModel vm)
{ {
vm.MeasurementResult = $"{args.Distance:F2} px"; string typeLabel = args.MeasureType switch
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} 条测量"; "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) => RoiCanvas.MeasureStatusChanged += (s, e) =>
@@ -52,6 +60,7 @@ namespace XplorePlane.Views
{ {
MeasurementToolMode.PointDistance => XP.ImageProcessing.RoiControl.Models.MeasureMode.PointDistance, MeasurementToolMode.PointDistance => XP.ImageProcessing.RoiControl.Models.MeasureMode.PointDistance,
MeasurementToolMode.PointLineDistance => XP.ImageProcessing.RoiControl.Models.MeasureMode.PointToLine, MeasurementToolMode.PointLineDistance => XP.ImageProcessing.RoiControl.Models.MeasureMode.PointToLine,
MeasurementToolMode.Angle => XP.ImageProcessing.RoiControl.Models.MeasureMode.Angle,
_ => XP.ImageProcessing.RoiControl.Models.MeasureMode.None _ => XP.ImageProcessing.RoiControl.Models.MeasureMode.None
}; };
}, Prism.Events.ThreadOption.UIThread); }, Prism.Events.ThreadOption.UIThread);