diff --git a/.gitignore b/.gitignore index 0b8391a..a13a714 100644 --- a/.gitignore +++ b/.gitignore @@ -30,6 +30,7 @@ bld/ lib/ XP.ImageProcessing/ ImageProcessing.sln +ExternalLibraries/ # 排除 Libs 目录中的 DLL 和 PDB 文件(但保留目录结构) XplorePlane/Libs/Hardware/*.dll diff --git a/XP.ImageProcessing.RoiControl/Controls/PolygonRoiCanvas.xaml.cs b/XP.ImageProcessing.RoiControl/Controls/PolygonRoiCanvas.xaml.cs index 134be77..a95274f 100644 --- a/XP.ImageProcessing.RoiControl/Controls/PolygonRoiCanvas.xaml.cs +++ b/XP.ImageProcessing.RoiControl/Controls/PolygonRoiCanvas.xaml.cs @@ -344,6 +344,7 @@ namespace XP.ImageProcessing.RoiControl.Controls private Canvas _measureOverlay; private readonly System.Collections.Generic.List _ppGroups = new(); private readonly System.Collections.Generic.List _ptlGroups = new(); + private readonly System.Collections.Generic.List _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; + } + } } // ── 事件 ── diff --git a/XP.ImageProcessing.RoiControl/Models/AngleGroup.cs b/XP.ImageProcessing.RoiControl/Models/AngleGroup.cs new file mode 100644 index 0000000..85a3077 --- /dev/null +++ b/XP.ImageProcessing.RoiControl/Models/AngleGroup.cs @@ -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 +{ + /// 一次角度测量的所有视觉元素(顶点V + 射线端点A/B + 两条射线 + 弧线 + 标签) + 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; + } + } +} diff --git a/XP.ImageProcessing.RoiControl/Models/MeasureMode.cs b/XP.ImageProcessing.RoiControl/Models/MeasureMode.cs index c21bce4..5de1d9d 100644 --- a/XP.ImageProcessing.RoiControl/Models/MeasureMode.cs +++ b/XP.ImageProcessing.RoiControl/Models/MeasureMode.cs @@ -4,6 +4,7 @@ namespace XP.ImageProcessing.RoiControl.Models { None, PointDistance, - PointToLine + PointToLine, + Angle } } diff --git a/XplorePlane/ViewModels/Main/MainViewModel.cs b/XplorePlane/ViewModels/Main/MainViewModel.cs index 0b3087c..1722de5 100644 --- a/XplorePlane/ViewModels/Main/MainViewModel.cs +++ b/XplorePlane/ViewModels/Main/MainViewModel.cs @@ -455,7 +455,7 @@ namespace XplorePlane.ViewModels private void ExecuteAngleMeasure() { _logger.Info("角度测量功能已触发"); - // TODO: 实现角度测量逻辑 + _eventAggregator.GetEvent().Publish(MeasurementToolMode.Angle); } private void ExecuteThroughHoleFillRateMeasure() diff --git a/XplorePlane/Views/Main/ViewportPanelView.xaml.cs b/XplorePlane/Views/Main/ViewportPanelView.xaml.cs index bd03651..9438a2f 100644 --- a/XplorePlane/Views/Main/ViewportPanelView.xaml.cs +++ b/XplorePlane/Views/Main/ViewportPanelView.xaml.cs @@ -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);