diff --git a/XP.ImageProcessing.RoiControl/Controls/MeasureEventArgs.cs b/XP.ImageProcessing.RoiControl/Controls/MeasureEventArgs.cs index 5a811d7..7278450 100644 --- a/XP.ImageProcessing.RoiControl/Controls/MeasureEventArgs.cs +++ b/XP.ImageProcessing.RoiControl/Controls/MeasureEventArgs.cs @@ -9,6 +9,7 @@ namespace XP.ImageProcessing.RoiControl.Controls public Point P2 { get; } public double Distance { get; } public int TotalCount { get; } + public string MeasureType { get; set; } public MeasureCompletedEventArgs(RoutedEvent routedEvent, Point p1, Point p2, double distance, int totalCount) : base(routedEvent) diff --git a/XP.ImageProcessing.RoiControl/Controls/PolygonRoiCanvas.xaml.cs b/XP.ImageProcessing.RoiControl/Controls/PolygonRoiCanvas.xaml.cs index dd87143..c3672a5 100644 --- a/XP.ImageProcessing.RoiControl/Controls/PolygonRoiCanvas.xaml.cs +++ b/XP.ImageProcessing.RoiControl/Controls/PolygonRoiCanvas.xaml.cs @@ -243,6 +243,42 @@ namespace XP.ImageProcessing.RoiControl.Controls #endregion Dependency Properties + #region Measurement Config + + public static readonly DependencyProperty PixelSizeProperty = + DependencyProperty.Register(nameof(PixelSize), typeof(double), typeof(PolygonRoiCanvas), + new PropertyMetadata(1.0)); + + /// 每像素对应的物理尺寸(= 探测器像素尺寸 / 放大倍率),默认 1.0 + 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")); + + /// 测量单位,默认 "px",可设为 "mm"/"μm"/"cm" + public string MeasureUnit + { + get => (string)GetValue(MeasureUnitProperty); + set => SetValue(MeasureUnitProperty, value); + } + + /// 将像素距离转换为物理距离文本 + 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 = @@ -288,43 +324,65 @@ namespace XP.ImageProcessing.RoiControl.Controls #region Measurement - public static readonly DependencyProperty IsMeasuringProperty = - DependencyProperty.Register(nameof(IsMeasuring), typeof(bool), typeof(PolygonRoiCanvas), - new PropertyMetadata(false, OnIsMeasuringChanged)); + public static readonly DependencyProperty CurrentMeasureModeProperty = + DependencyProperty.Register(nameof(CurrentMeasureMode), typeof(Models.MeasureMode), typeof(PolygonRoiCanvas), + new PropertyMetadata(Models.MeasureMode.None, OnMeasureModeChanged)); - public bool IsMeasuring + public Models.MeasureMode CurrentMeasureMode { - get => (bool)GetValue(IsMeasuringProperty); - set => SetValue(IsMeasuringProperty, value); + 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 _measureGroups = new(); + private readonly System.Collections.Generic.List _ppGroups = new(); + private readonly System.Collections.Generic.List _ptlGroups = new(); + + // 点点距临时状态 private Ellipse _pendingDot; private Point? _pendingPoint; - private Ellipse _mDraggingDot; - private Models.MeasureGroup _mDraggingGroup; - private bool _mDraggingIsDot1; - private static void OnIsMeasuringChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + // 点线距临时状态 + private int _ptlClickCount; + private Ellipse _ptlTempDot1, _ptlTempDot2; + private Line _ptlTempLine; + private Point? _ptlTempL1, _ptlTempL2; + + // 拖拽状态 + private Ellipse _mDraggingDot; + private object _mDraggingOwner; // MeasureGroup 或 PointToLineGroup + private string _mDraggingRole; // "Dot1","Dot2","DotL1","DotL2","DotP" + + private static void OnMeasureModeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { var c = (PolygonRoiCanvas)d; - if ((bool)e.NewValue) + var newMode = (Models.MeasureMode)e.NewValue; + if (newMode != Models.MeasureMode.None) { c.EnsureMeasureOverlay(); } else { - // 退出测量模式:只清除未完成的临时点,保留已完成的测量线 - if (c._pendingDot != null && c._measureOverlay != null) - { - c._measureOverlay.Children.Remove(c._pendingDot); - c._pendingDot = null; - } - c._pendingPoint = null; + // 退出测量模式:清除未完成的临时元素 + 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; + } + private void EnsureMeasureOverlay() { if (_measureOverlay != null) return; @@ -336,77 +394,134 @@ namespace XP.ImageProcessing.RoiControl.Controls private void RemoveMeasureOverlay() { - if (_measureOverlay != null) - { - mainCanvas.Children.Remove(_measureOverlay); - _measureOverlay = null; - } - _measureGroups.Clear(); - _pendingDot = null; - _pendingPoint = null; - _mDraggingDot = null; - _mDraggingGroup = null; + if (_measureOverlay != null) { mainCanvas.Children.Remove(_measureOverlay); _measureOverlay = null; } + _ppGroups.Clear(); + _ptlGroups.Clear(); + _pendingDot = null; _pendingPoint = null; + _ptlTempDot1 = _ptlTempDot2 = null; _ptlTempLine = null; + _ptlTempL1 = _ptlTempL2 = null; _ptlClickCount = 0; + _mDraggingDot = null; _mDraggingOwner = null; } - /// 清除所有测量绘制 - public void ClearMeasurements() - { - RemoveMeasureOverlay(); - } + public void ClearMeasurements() => RemoveMeasureOverlay(); + public int MeasureCount => _ppGroups.Count + _ptlGroups.Count; - /// 获取当前测量组数量 - public int MeasureCount => _measureGroups.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); + } + + // ── 点点距 ── + + private void HandlePointDistanceClick(Point pos) + { if (!_pendingPoint.HasValue) { _pendingPoint = pos; - _pendingDot = CreateMeasureDot(Brushes.Red); + _pendingDot = CreateMDot(Brushes.Red); _measureOverlay.Children.Add(_pendingDot); - SetMeasureDotPos(_pendingDot, pos); - RaiseMeasureStatusChanged($"点点距测量 - 第一点: ({pos.X:F0}, {pos.Y:F0}),请点击第二个点"); + SetDotPos(_pendingDot, pos); + RaiseMeasureStatusChanged($"点点距 - 第一点: ({pos.X:F0}, {pos.Y:F0}),请点击第二个点"); } else { - var g = CreateMeasureGroup(_pendingPoint.Value, pos); - _measureGroups.Add(g); + var g = CreatePPGroup(_pendingPoint.Value, pos); + _ppGroups.Add(g); _measureOverlay.Children.Remove(_pendingDot); - _pendingDot = null; - _pendingPoint = null; - RaiseMeasureCompleted(g); - - // 完成一条测量后自动退出测量模式 - IsMeasuring = false; + _pendingDot = null; _pendingPoint = null; + RaiseMeasureCompleted(g.P1, g.P2, g.Distance, MeasureCount, "PointDistance"); + CurrentMeasureMode = Models.MeasureMode.None; } } - private Models.MeasureGroup CreateMeasureGroup(Point p1, Point p2) + 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 }; - _measureOverlay.Children.Add(g.Line); - g.Label = new TextBlock { Foreground = Brushes.Yellow, FontSize = 13, FontWeight = FontWeights.Bold, IsHitTestVisible = false }; - _measureOverlay.Children.Add(g.Label); - - g.Dot1 = CreateMeasureDot(Brushes.Red); - g.Dot2 = CreateMeasureDot(Brushes.Blue); - _measureOverlay.Children.Add(g.Dot1); - _measureOverlay.Children.Add(g.Dot2); - - SetMeasureDotPos(g.Dot1, p1); - SetMeasureDotPos(g.Dot2, p2); - g.UpdateLine(); - g.UpdateLabel(); + 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 Ellipse CreateMeasureDot(Brush fill) + // ── 点线距 ── + + 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); + + 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 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); @@ -417,7 +532,7 @@ namespace XP.ImageProcessing.RoiControl.Controls return dot; } - private static void SetMeasureDotPos(Ellipse dot, Point pos) + private static void SetDotPos(Ellipse dot, Point pos) { Canvas.SetLeft(dot, pos.X - dot.Width / 2); Canvas.SetTop(dot, pos.Y - dot.Height / 2); @@ -426,51 +541,87 @@ namespace XP.ImageProcessing.RoiControl.Controls private void MDot_Down(object sender, MouseButtonEventArgs e) { if (sender is not Ellipse dot) return; - foreach (var g in _measureGroups) + // 查找点点距组 + foreach (var g in _ppGroups) { - if (g.Dot1 == dot) { _mDraggingGroup = g; _mDraggingIsDot1 = true; break; } - if (g.Dot2 == dot) { _mDraggingGroup = g; _mDraggingIsDot1 = false; break; } + if (g.Dot1 == dot) { _mDraggingOwner = g; _mDraggingRole = "Dot1"; break; } + if (g.Dot2 == dot) { _mDraggingOwner = g; _mDraggingRole = "Dot2"; break; } } - if (_mDraggingGroup != null) { _mDraggingDot = dot; dot.CaptureMouse(); e.Handled = true; } + // 查找点线距组 + 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) { _mDraggingDot = dot; dot.CaptureMouse(); e.Handled = true; } } private void MDot_Move(object sender, MouseEventArgs e) { - if (_mDraggingDot == null || _mDraggingGroup == null || _measureOverlay == null) return; + if (_mDraggingDot == null || _mDraggingOwner == null || _measureOverlay == null) return; if (e.LeftButton != MouseButtonState.Pressed) return; var pos = e.GetPosition(_measureOverlay); - SetMeasureDotPos(_mDraggingDot, pos); - if (_mDraggingIsDot1) _mDraggingGroup.P1 = pos; else _mDraggingGroup.P2 = pos; - _mDraggingGroup.UpdateLine(); - _mDraggingGroup.UpdateLabel(); - RaiseMeasureCompleted(_mDraggingGroup); + 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"); + } e.Handled = true; } private void MDot_Up(object sender, MouseButtonEventArgs e) { - if (_mDraggingDot != null) { _mDraggingDot.ReleaseMouseCapture(); _mDraggingDot = null; _mDraggingGroup = null; e.Handled = true; } + 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; - Models.MeasureGroup target = null; - foreach (var g in _measureGroups) + + // 点点距删除 + foreach (var g in _ppGroups) { - if (g.Dot1 == dot || g.Dot2 == dot) { target = g; break; } + 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); + 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); + RaiseMeasureStatusChanged($"已删除测量 | 剩余 {MeasureCount} 条"); + e.Handled = true; return; + } } - if (target == null) return; - _measureOverlay.Children.Remove(target.Dot1); - _measureOverlay.Children.Remove(target.Dot2); - _measureOverlay.Children.Remove(target.Line); - _measureOverlay.Children.Remove(target.Label); - _measureGroups.Remove(target); - RaiseMeasureStatusChanged($"已删除测量线 | 剩余 {_measureGroups.Count} 条"); - e.Handled = true; } - // 测量事件 + // ── 事件 ── + public static readonly RoutedEvent MeasureCompletedEvent = EventManager.RegisterRoutedEvent(nameof(MeasureCompleted), RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(PolygonRoiCanvas)); @@ -481,9 +632,9 @@ namespace XP.ImageProcessing.RoiControl.Controls remove { RemoveHandler(MeasureCompletedEvent, value); } } - private void RaiseMeasureCompleted(Models.MeasureGroup g) + private void RaiseMeasureCompleted(Point p1, Point p2, double distance, int totalCount, string measureType) { - RaiseEvent(new MeasureCompletedEventArgs(MeasureCompletedEvent, g.P1, g.P2, g.Distance, _measureGroups.Count)); + RaiseEvent(new MeasureCompletedEventArgs(MeasureCompletedEvent, p1, p2, distance, totalCount) { MeasureType = measureType }); } public static readonly RoutedEvent MeasureStatusChangedEvent = diff --git a/XP.ImageProcessing.RoiControl/Models/MeasureGroup.cs b/XP.ImageProcessing.RoiControl/Models/MeasureGroup.cs index c1550d8..2e5670d 100644 --- a/XP.ImageProcessing.RoiControl/Models/MeasureGroup.cs +++ b/XP.ImageProcessing.RoiControl/Models/MeasureGroup.cs @@ -32,9 +32,9 @@ namespace XP.ImageProcessing.RoiControl.Models Line.Visibility = Visibility.Visible; } - public void UpdateLabel(string unit = "px") + public void UpdateLabel(string distanceText = null) { - Label.Text = $"{Distance:F2} {unit}"; + Label.Text = distanceText ?? $"{Distance:F2} px"; Canvas.SetLeft(Label, (P1.X + P2.X) / 2 + 8); Canvas.SetTop(Label, (P1.Y + P2.Y) / 2 - 18); Label.Visibility = Visibility.Visible; diff --git a/XP.ImageProcessing.RoiControl/Models/MeasureMode.cs b/XP.ImageProcessing.RoiControl/Models/MeasureMode.cs new file mode 100644 index 0000000..c21bce4 --- /dev/null +++ b/XP.ImageProcessing.RoiControl/Models/MeasureMode.cs @@ -0,0 +1,9 @@ +namespace XP.ImageProcessing.RoiControl.Models +{ + public enum MeasureMode + { + None, + PointDistance, + PointToLine + } +} diff --git a/XP.ImageProcessing.RoiControl/Models/PointToLineGroup.cs b/XP.ImageProcessing.RoiControl/Models/PointToLineGroup.cs new file mode 100644 index 0000000..29163e4 --- /dev/null +++ b/XP.ImageProcessing.RoiControl/Models/PointToLineGroup.cs @@ -0,0 +1,96 @@ +using System; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Media; +using System.Windows.Shapes; + +namespace XP.ImageProcessing.RoiControl.Models +{ + /// 一次点线距测量的所有视觉元素(直线两端点 + 测量点 + 垂线 + 标签) + public class PointToLineGroup + { + public Ellipse DotL1 { get; set; } // 直线端点1 + public Ellipse DotL2 { get; set; } // 直线端点2 + public Ellipse DotP { get; set; } // 测量点 + public Line MainLine { get; set; } // 原始线段(实线) + public Line ExtLine { get; set; } // 延长线(虚线) + public Line PerpLine { get; set; } // 垂线(测量点→垂足) + public Ellipse FootDot { get; set; } // 垂足 + public TextBlock Label { get; set; } + public Point L1 { get; set; } + public Point L2 { get; set; } + public Point P { get; set; } + + public double Distance + { + get + { + double abx = L2.X - L1.X, aby = L2.Y - L1.Y; + double abLen = Math.Sqrt(abx * abx + aby * aby); + if (abLen < 0.001) return 0; + return Math.Abs(abx * (L1.Y - P.Y) - aby * (L1.X - P.X)) / abLen; + } + } + + public Point FootPoint + { + get + { + double abx = L2.X - L1.X, aby = L2.Y - L1.Y; + double abLen2 = abx * abx + aby * aby; + if (abLen2 < 0.001) return L1; + double t = ((P.X - L1.X) * abx + (P.Y - L1.Y) * aby) / abLen2; + return new Point(L1.X + t * abx, L1.Y + t * aby); + } + } + + public void UpdateVisuals(string distanceText) + { + double abx = L2.X - L1.X, aby = L2.Y - L1.Y; + double abLen2 = abx * abx + aby * aby; + double t = abLen2 < 0.001 ? 0 : ((P.X - L1.X) * abx + (P.Y - L1.Y) * aby) / abLen2; + var foot = FootPoint; + + // 主直线:始终画原始线段 + MainLine.X1 = L1.X; MainLine.Y1 = L1.Y; + MainLine.X2 = L2.X; MainLine.Y2 = L2.Y; + MainLine.Visibility = Visibility.Visible; + + // 延长线:垂足在线段外时画虚线延伸 + if (t < 0) + { + ExtLine.X1 = foot.X; ExtLine.Y1 = foot.Y; + ExtLine.X2 = L1.X; ExtLine.Y2 = L1.Y; + ExtLine.Visibility = Visibility.Visible; + } + else if (t > 1) + { + ExtLine.X1 = L2.X; ExtLine.Y1 = L2.Y; + ExtLine.X2 = foot.X; ExtLine.Y2 = foot.Y; + ExtLine.Visibility = Visibility.Visible; + } + else + { + ExtLine.Visibility = Visibility.Collapsed; + } + + // 垂线 + PerpLine.X1 = P.X; PerpLine.Y1 = P.Y; + PerpLine.X2 = foot.X; PerpLine.Y2 = foot.Y; + PerpLine.Visibility = Visibility.Visible; + + // 垂足 + Canvas.SetLeft(FootDot, foot.X - FootDot.Width / 2); + Canvas.SetTop(FootDot, foot.Y - FootDot.Height / 2); + FootDot.Visibility = Visibility.Visible; + + // 标签 + Label.Text = distanceText ?? $"{Distance:F2} px"; + Canvas.SetLeft(Label, (P.X + foot.X) / 2 + 8); + Canvas.SetTop(Label, (P.Y + foot.Y) / 2 - 18); + Label.Visibility = Visibility.Visible; + } + + public void UpdateVisuals() => UpdateVisuals(null); + } +} diff --git a/XplorePlane/ViewModels/Main/MainViewModel.cs b/XplorePlane/ViewModels/Main/MainViewModel.cs index 96fc3bd..d88fe5f 100644 --- a/XplorePlane/ViewModels/Main/MainViewModel.cs +++ b/XplorePlane/ViewModels/Main/MainViewModel.cs @@ -449,7 +449,7 @@ namespace XplorePlane.ViewModels private void ExecutePointLineDistanceMeasure() { _logger.Info("点线距测量功能已触发"); - // TODO: 实现点线距测量逻辑 + _eventAggregator.GetEvent().Publish(MeasurementToolMode.PointLineDistance); } private void ExecuteAngleMeasure() diff --git a/XplorePlane/Views/Main/ViewportPanelView.xaml.cs b/XplorePlane/Views/Main/ViewportPanelView.xaml.cs index 04b621a..bd03651 100644 --- a/XplorePlane/Views/Main/ViewportPanelView.xaml.cs +++ b/XplorePlane/Views/Main/ViewportPanelView.xaml.cs @@ -26,7 +26,8 @@ namespace XplorePlane.Views if (e is MeasureCompletedEventArgs args && DataContext is ViewportPanelViewModel vm) { vm.MeasurementResult = $"{args.Distance:F2} px"; - vm.ImageInfo = $"点点距: {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 == "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} 条测量"; } }; RoiCanvas.MeasureStatusChanged += (s, e) => @@ -47,7 +48,12 @@ namespace XplorePlane.Views // 测量模式:直接订阅 Prism 事件 ea?.GetEvent().Subscribe(mode => { - RoiCanvas.IsMeasuring = mode != MeasurementToolMode.None; + RoiCanvas.CurrentMeasureMode = mode switch + { + MeasurementToolMode.PointDistance => XP.ImageProcessing.RoiControl.Models.MeasureMode.PointDistance, + MeasurementToolMode.PointLineDistance => XP.ImageProcessing.RoiControl.Models.MeasureMode.PointToLine, + _ => XP.ImageProcessing.RoiControl.Models.MeasureMode.None + }; }, Prism.Events.ThreadOption.UIThread); } catch { } @@ -63,12 +69,7 @@ namespace XplorePlane.Views private void OnVmPropertyChanged(object sender, PropertyChangedEventArgs e) { - if (sender is not ViewportPanelViewModel vm) return; - - if (e.PropertyName == nameof(ViewportPanelViewModel.CurrentMeasurementMode)) - { - RoiCanvas.IsMeasuring = vm.CurrentMeasurementMode != MeasurementToolMode.None; - } + // 测量模式和十字线通过 Prism 事件直接驱动,不再依赖 PropertyChanged } #region 右键菜单