diff --git a/XP.ImageProcessing.RoiControl/Controls/PolygonRoiCanvas.xaml.cs b/XP.ImageProcessing.RoiControl/Controls/PolygonRoiCanvas.xaml.cs index a95274f..716eede 100644 --- a/XP.ImageProcessing.RoiControl/Controls/PolygonRoiCanvas.xaml.cs +++ b/XP.ImageProcessing.RoiControl/Controls/PolygonRoiCanvas.xaml.cs @@ -345,6 +345,7 @@ namespace XP.ImageProcessing.RoiControl.Controls 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 readonly System.Collections.Generic.List _frGroups = new(); // 点点距临时状态 private Ellipse _pendingDot; @@ -409,7 +410,7 @@ namespace XP.ImageProcessing.RoiControl.Controls private void RemoveMeasureOverlay() { if (_measureOverlay != null) { mainCanvas.Children.Remove(_measureOverlay); _measureOverlay = null; } - _ppGroups.Clear(); _ptlGroups.Clear(); _angleGroups.Clear(); + _ppGroups.Clear(); _ptlGroups.Clear(); _angleGroups.Clear(); _frGroups.Clear(); _pendingDot = null; _pendingPoint = null; _ptlTempDot1 = _ptlTempDot2 = null; _ptlTempLine = null; _ptlTempL1 = _ptlTempL2 = null; _ptlClickCount = 0; @@ -419,7 +420,7 @@ namespace XP.ImageProcessing.RoiControl.Controls } public void ClearMeasurements() => RemoveMeasureOverlay(); - public int MeasureCount => _ppGroups.Count + _ptlGroups.Count + _angleGroups.Count; + public int MeasureCount => _ppGroups.Count + _ptlGroups.Count + _angleGroups.Count + _frGroups.Count; // ── 点击分发 ── @@ -434,6 +435,8 @@ namespace XP.ImageProcessing.RoiControl.Controls HandlePointToLineClick(pos); else if (CurrentMeasureMode == Models.MeasureMode.Angle) HandleAngleClick(pos); + else if (CurrentMeasureMode == Models.MeasureMode.FillRate) + HandleFillRateClick(pos); } // ── 点点距 ── @@ -595,6 +598,77 @@ namespace XP.ImageProcessing.RoiControl.Controls return g; } + // ── 通孔填锡率 ── + + private void HandleFillRateClick(Point pos) + { + // 单击放置:自动生成4个椭圆的初始位置 + double offset = 70; + var e1 = new Point(pos.X, pos.Y + offset / 2); + var e2 = new Point(pos.X + 20, pos.Y - offset / 2); + var e3 = e1; // 起点=底部 + var e4 = new Point(pos.X + 10, pos.Y - offset / 4); + + var g = CreateFillRateGroup(e1, e2, e3, e4); + _frGroups.Add(g); + + double rate = g.FillRate; + RaiseMeasureCompleted(g.E3, g.E4, rate, MeasureCount, "FillRate"); + CurrentMeasureMode = Models.MeasureMode.None; + } + + private Models.FillRateGroup CreateFillRateGroup(Point e1, Point e2, Point e3, Point e4) + { + var g = new Models.FillRateGroup { E1 = e1, E2 = e2, E3 = e3, E4 = e4 }; + + g.PathE1 = CreateEllipsePath(Brushes.DodgerBlue, false); + g.PathE2 = CreateEllipsePath(Brushes.Cyan, false); + g.PathE3 = CreateEllipsePath(Brushes.Yellow, true); + g.PathE4 = CreateEllipsePath(Brushes.Lime, false); + + g.FullLine = new Line { Stroke = Brushes.Red, StrokeThickness = 1, IsHitTestVisible = false }; + g.FillLine = new Line { Stroke = Brushes.Lime, StrokeThickness = 2, IsHitTestVisible = false }; + + g.Label = new TextBlock { FontSize = 13, FontWeight = FontWeights.Bold, IsHitTestVisible = false }; + + g.DotE1 = CreateMDot(Brushes.DodgerBlue); + g.DotE2 = CreateMDot(Brushes.Cyan); + g.DotE3 = CreateMDot(Brushes.Yellow); + g.DotE4 = CreateMDot(Brushes.Lime); + + // 轴手柄(小方块样式区分) + g.E1AH = CreateAxisHandle(Brushes.DodgerBlue); g.E1BH = CreateAxisHandle(Brushes.DodgerBlue); + g.E2AH = CreateAxisHandle(Brushes.Cyan); g.E2BH = CreateAxisHandle(Brushes.Cyan); + g.E3AH = CreateAxisHandle(Brushes.Yellow); g.E3BH = CreateAxisHandle(Brushes.Yellow); + g.E4AH = CreateAxisHandle(Brushes.Lime); g.E4BH = CreateAxisHandle(Brushes.Lime); + + foreach (var el in g.AllElements) + _measureOverlay.Children.Add(el); + + SetDotPos(g.DotE1, e1); SetDotPos(g.DotE2, e2); + SetDotPos(g.DotE3, e3); SetDotPos(g.DotE4, e4); + g.UpdateVisuals(); + return g; + } + + private Ellipse CreateAxisHandle(Brush fill) + { + var h = new Ellipse { Width = 8, Height = 8, Fill = fill, Stroke = Brushes.White, StrokeThickness = 1, Cursor = Cursors.SizeAll }; + h.SetValue(ContextMenuService.IsEnabledProperty, false); + h.MouseLeftButtonDown += MDot_Down; + h.MouseMove += MDot_Move; + h.MouseLeftButtonUp += MDot_Up; + h.PreviewMouseRightButtonUp += MDot_RightClick; + return h; + } + + private static Path CreateEllipsePath(Brush stroke, bool dashed) + { + var p = new Path { Stroke = stroke, StrokeThickness = 1.5, IsHitTestVisible = false }; + if (dashed) p.StrokeDashArray = new DoubleCollection { 4, 2 }; + return p; + } + // ── 共用:圆点创建、定位、拖拽、删除 ── private Ellipse CreateMDot(Brush fill) @@ -643,6 +717,25 @@ namespace XP.ImageProcessing.RoiControl.Controls if (g.DotB == dot) { _mDraggingOwner = g; _mDraggingRole = "DotB"; break; } } } + // 查找填锡率组 + if (_mDraggingOwner == null) + { + foreach (var g in _frGroups) + { + if (g.DotE1 == dot) { _mDraggingOwner = g; _mDraggingRole = "E1"; break; } + if (g.DotE2 == dot) { _mDraggingOwner = g; _mDraggingRole = "E2"; break; } + if (g.DotE3 == dot) { _mDraggingOwner = g; _mDraggingRole = "E3"; break; } + if (g.DotE4 == dot) { _mDraggingOwner = g; _mDraggingRole = "E4"; break; } + if (g.E1AH == dot) { _mDraggingOwner = g; _mDraggingRole = "E1A"; break; } + if (g.E1BH == dot) { _mDraggingOwner = g; _mDraggingRole = "E1B"; break; } + if (g.E2AH == dot) { _mDraggingOwner = g; _mDraggingRole = "E2A"; break; } + if (g.E2BH == dot) { _mDraggingOwner = g; _mDraggingRole = "E2B"; break; } + if (g.E3AH == dot) { _mDraggingOwner = g; _mDraggingRole = "E3A"; break; } + if (g.E3BH == dot) { _mDraggingOwner = g; _mDraggingRole = "E3B"; break; } + if (g.E4AH == dot) { _mDraggingOwner = g; _mDraggingRole = "E4A"; break; } + if (g.E4BH == dot) { _mDraggingOwner = g; _mDraggingRole = "E4B"; break; } + } + } if (_mDraggingOwner != null) { _mDraggingDot = dot; dot.CaptureMouse(); e.Handled = true; } } @@ -676,6 +769,23 @@ namespace XP.ImageProcessing.RoiControl.Controls ag.UpdateVisuals(); RaiseMeasureCompleted(ag.V, ag.B, ag.AngleDeg, MeasureCount, "Angle"); } + else if (_mDraggingOwner is Models.FillRateGroup frg) + { + if (_mDraggingRole == "E1") frg.E1 = pos; + else if (_mDraggingRole == "E2") frg.E2 = pos; + else if (_mDraggingRole == "E3") frg.E3 = pos; + else if (_mDraggingRole == "E4") frg.E4 = pos; + else if (_mDraggingRole == "E1A") frg.E1A = Math.Max(10, Models.FillRateGroup.Dist(pos, frg.E1)); + else if (_mDraggingRole == "E1B") frg.E1B = Math.Max(10, Models.FillRateGroup.Dist(pos, frg.E1)); + else if (_mDraggingRole == "E2A") frg.E2A = Math.Max(10, Models.FillRateGroup.Dist(pos, frg.E2)); + else if (_mDraggingRole == "E2B") frg.E2B = Math.Max(10, Models.FillRateGroup.Dist(pos, frg.E2)); + else if (_mDraggingRole == "E3A") frg.E3A = Math.Max(10, Models.FillRateGroup.Dist(pos, frg.E3)); + else if (_mDraggingRole == "E3B") frg.E3B = Math.Max(10, Models.FillRateGroup.Dist(pos, frg.E3)); + else if (_mDraggingRole == "E4A") frg.E4A = Math.Max(10, Models.FillRateGroup.Dist(pos, frg.E4)); + else if (_mDraggingRole == "E4B") frg.E4B = Math.Max(10, Models.FillRateGroup.Dist(pos, frg.E4)); + frg.UpdateVisuals(); + RaiseMeasureCompleted(frg.E3, frg.E4, frg.FillRate, MeasureCount, "FillRate"); + } e.Handled = true; } @@ -724,6 +834,21 @@ namespace XP.ImageProcessing.RoiControl.Controls e.Handled = true; return; } } + // 填锡率删除 + foreach (var g in _frGroups) + { + bool match = g.DotE1 == dot || g.DotE2 == dot || g.DotE3 == dot || g.DotE4 == dot + || g.E1AH == dot || g.E1BH == dot || g.E2AH == dot || g.E2BH == dot + || g.E3AH == dot || g.E3BH == dot || g.E4AH == dot || g.E4BH == dot; + if (match) + { + foreach (var el in g.AllElements) + _measureOverlay.Children.Remove(el); + _frGroups.Remove(g); + RaiseMeasureStatusChanged($"已删除测量 | 剩余 {MeasureCount} 条"); + e.Handled = true; return; + } + } } // ── 事件 ── diff --git a/XP.ImageProcessing.RoiControl/Models/FillRateGroup.cs b/XP.ImageProcessing.RoiControl/Models/FillRateGroup.cs new file mode 100644 index 0000000..3b2d6b1 --- /dev/null +++ b/XP.ImageProcessing.RoiControl/Models/FillRateGroup.cs @@ -0,0 +1,148 @@ +using System; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Media; +using System.Windows.Shapes; + +namespace XP.ImageProcessing.RoiControl.Models +{ + /// 通孔填锡率测量组(4个椭圆中心 + 连线 + 标签) + public class FillRateGroup + { + // 四个椭圆中心的可拖拽圆点 + public Ellipse DotE1 { get; set; } // 底部(蓝) + public Ellipse DotE2 { get; set; } // 顶部(青) + public Ellipse DotE3 { get; set; } // 填锡起点(黄) + public Ellipse DotE4 { get; set; } // 填锡终点(绿) + + // 长轴/短轴手柄(每个椭圆2个) + public Ellipse E1AH { get; set; } + public Ellipse E1BH { get; set; } + public Ellipse E2AH { get; set; } + public Ellipse E2BH { get; set; } + public Ellipse E3AH { get; set; } + public Ellipse E3BH { get; set; } + public Ellipse E4AH { get; set; } + public Ellipse E4BH { get; set; } + + // 椭圆路径 + public Path PathE1 { get; set; } + public Path PathE2 { get; set; } + public Path PathE3 { get; set; } + public Path PathE4 { get; set; } + + // 连线:E1→E2(全高度),E3→E4(填锡高度) + public Line FullLine { get; set; } + public Line FillLine { get; set; } + + public TextBlock Label { get; set; } + + // 四个椭圆中心坐标 + public Point E1 { get; set; } + public Point E2 { get; set; } + public Point E3 { get; set; } + public Point E4 { get; set; } + + // 椭圆轴参数(默认值,后续可拖拽调整) + public double E1A { get; set; } = 60; + public double E1B { get; set; } = 50; + public double E1Angle { get; set; } + public double E2A { get; set; } = 60; + public double E2B { get; set; } = 50; + public double E2Angle { get; set; } + public double E3A { get; set; } = 60; + public double E3B { get; set; } = 50; + public double E3Angle { get; set; } + public double E4A { get; set; } = 55; + public double E4B { get; set; } = 45; + public double E4Angle { get; set; } + + public double THTLimit { get; set; } = 75.0; + + public double FillRate + { + get + { + double fullDx = E2.X - E1.X, fullDy = E2.Y - E1.Y; + double fullDist = Math.Sqrt(fullDx * fullDx + fullDy * fullDy); + double fillDx = E4.X - E3.X, fillDy = E4.Y - E3.Y; + double fillDist = Math.Sqrt(fillDx * fillDx + fillDy * fillDy); + return fullDist > 0 ? Math.Clamp(fillDist / fullDist * 100.0, 0, 100) : 0; + } + } + + public string Classification => FillRate >= THTLimit ? "PASS" : "FAIL"; + + public void UpdateVisuals() + { + UpdateEllipsePath(PathE1, E1, E1A, E1B, E1Angle); + UpdateEllipsePath(PathE2, E2, E2A, E2B, E2Angle); + UpdateEllipsePath(PathE3, E3, E3A, E3B, E3Angle); + UpdateEllipsePath(PathE4, E4, E4A, E4B, E4Angle); + + FullLine.X1 = E1.X; FullLine.Y1 = E1.Y; + FullLine.X2 = E2.X; FullLine.Y2 = E2.Y; + FullLine.Visibility = Visibility.Visible; + + FillLine.X1 = E3.X; FillLine.Y1 = E3.Y; + FillLine.X2 = E4.X; FillLine.Y2 = E4.Y; + FillLine.Visibility = Visibility.Visible; + + // 轴手柄定位 + SetHandle(E1AH, EdgePoint(E1, E1A, E1Angle)); + SetHandle(E1BH, EdgePoint(E1, E1B, E1Angle + 90)); + SetHandle(E2AH, EdgePoint(E2, E2A, E2Angle)); + SetHandle(E2BH, EdgePoint(E2, E2B, E2Angle + 90)); + SetHandle(E3AH, EdgePoint(E3, E3A, E3Angle)); + SetHandle(E3BH, EdgePoint(E3, E3B, E3Angle + 90)); + SetHandle(E4AH, EdgePoint(E4, E4A, E4Angle)); + SetHandle(E4BH, EdgePoint(E4, E4B, E4Angle + 90)); + + double rate = FillRate; + string cls = Classification; + Label.Text = $"{rate:F1}% {cls}"; + Label.Foreground = cls == "PASS" ? Brushes.Lime : Brushes.Red; + double labelX = Math.Max(Math.Max(E1.X, E2.X), Math.Max(E3.X, E4.X)) + 15; + double labelY = (E1.Y + E2.Y) / 2; + Canvas.SetLeft(Label, labelX); + Canvas.SetTop(Label, labelY - 10); + Label.Visibility = Visibility.Visible; + } + + /// 获取所有 UI 元素(用于添加/移除) + public UIElement[] AllElements => new UIElement[] + { + PathE1, PathE2, PathE3, PathE4, FullLine, FillLine, Label, + DotE1, DotE2, DotE3, DotE4, + E1AH, E1BH, E2AH, E2BH, E3AH, E3BH, E4AH, E4BH + }; + + private static Point EdgePoint(Point center, double radius, double angleDeg) + { + double rad = angleDeg * Math.PI / 180.0; + return new Point(center.X + radius * Math.Cos(rad), center.Y + radius * Math.Sin(rad)); + } + + private static void SetHandle(Ellipse h, Point pos) + { + if (h == null) return; + Canvas.SetLeft(h, pos.X - h.Width / 2); + Canvas.SetTop(h, pos.Y - h.Height / 2); + } + + public static double Dist(Point a, Point b) + { + double dx = b.X - a.X, dy = b.Y - a.Y; + return Math.Sqrt(dx * dx + dy * dy); + } + + private static void UpdateEllipsePath(Path path, Point center, double a, double b, double angleDeg) + { + var eg = new EllipseGeometry(center, a, b); + if (Math.Abs(angleDeg) > 0.01) + eg.Transform = new RotateTransform(angleDeg, center.X, center.Y); + path.Data = eg; + path.Visibility = Visibility.Visible; + } + } +} diff --git a/XP.ImageProcessing.RoiControl/Models/MeasureMode.cs b/XP.ImageProcessing.RoiControl/Models/MeasureMode.cs index 5de1d9d..f872803 100644 --- a/XP.ImageProcessing.RoiControl/Models/MeasureMode.cs +++ b/XP.ImageProcessing.RoiControl/Models/MeasureMode.cs @@ -5,6 +5,7 @@ namespace XP.ImageProcessing.RoiControl.Models None, PointDistance, PointToLine, - Angle + Angle, + FillRate } } diff --git a/XplorePlane/ViewModels/Main/MainViewModel.cs b/XplorePlane/ViewModels/Main/MainViewModel.cs index 1722de5..b730afc 100644 --- a/XplorePlane/ViewModels/Main/MainViewModel.cs +++ b/XplorePlane/ViewModels/Main/MainViewModel.cs @@ -35,6 +35,13 @@ namespace XplorePlane.ViewModels set => SetProperty(ref _licenseInfo, value); } + private string _statusMessage = "就绪"; + public string StatusMessage + { + get => _statusMessage; + set => SetProperty(ref _statusMessage, value); + } + public ObservableCollection NavigationTree { get; set; } // 导航命令 @@ -461,7 +468,7 @@ namespace XplorePlane.ViewModels private void ExecuteThroughHoleFillRateMeasure() { _logger.Info("通孔填锡率测量功能已触发"); - // TODO: 实现通孔填锡率测量逻辑 + _eventAggregator.GetEvent().Publish(MeasurementToolMode.ThroughHoleFillRate); } #endregion