合并 Feature/TURBO-586-FillRate 分支:新增BGA空隙测量和气泡测量功能
This commit is contained in:
@@ -41,178 +41,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Emgu.CV.runtime.windows/4.10.0.5680": {
|
||||
"dependencies": {
|
||||
"Emgu.CV": "4.10.0.5680",
|
||||
"Emgu.runtime.windows.msvc.rt.arm64": "19.42.34435",
|
||||
"Emgu.runtime.windows.msvc.rt.x64": "19.42.34435",
|
||||
"Emgu.runtime.windows.msvc.rt.x86": "19.42.34435"
|
||||
},
|
||||
"runtimeTargets": {
|
||||
"runtimes/win-arm64/native/cvextern.dll": {
|
||||
"rid": "win-arm64",
|
||||
"assetType": "native",
|
||||
"fileVersion": "4.10.0.5680"
|
||||
},
|
||||
"runtimes/win-x64/native/cvextern.dll": {
|
||||
"rid": "win-x64",
|
||||
"assetType": "native",
|
||||
"fileVersion": "4.10.0.5680"
|
||||
},
|
||||
"runtimes/win-x64/native/libusb-1.0.dll": {
|
||||
"rid": "win-x64",
|
||||
"assetType": "native",
|
||||
"fileVersion": "0.0.0.0"
|
||||
},
|
||||
"runtimes/win-x64/native/opencv_videoio_ffmpeg4100_64.dll": {
|
||||
"rid": "win-x64",
|
||||
"assetType": "native",
|
||||
"fileVersion": "2024.5.0.0"
|
||||
},
|
||||
"runtimes/win-x86/native/cvextern.dll": {
|
||||
"rid": "win-x86",
|
||||
"assetType": "native",
|
||||
"fileVersion": "4.10.0.5680"
|
||||
},
|
||||
"runtimes/win-x86/native/libusb-1.0.dll": {
|
||||
"rid": "win-x86",
|
||||
"assetType": "native",
|
||||
"fileVersion": "0.0.0.0"
|
||||
},
|
||||
"runtimes/win-x86/native/opencv_videoio_ffmpeg4100.dll": {
|
||||
"rid": "win-x86",
|
||||
"assetType": "native",
|
||||
"fileVersion": "2024.5.0.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Emgu.runtime.windows.msvc.rt.arm64/19.42.34435": {
|
||||
"runtimeTargets": {
|
||||
"runtimes/win-arm64/native/concrt140.dll": {
|
||||
"rid": "win-arm64",
|
||||
"assetType": "native",
|
||||
"fileVersion": "14.42.34433.0"
|
||||
},
|
||||
"runtimes/win-arm64/native/msvcp140.dll": {
|
||||
"rid": "win-arm64",
|
||||
"assetType": "native",
|
||||
"fileVersion": "14.42.34433.0"
|
||||
},
|
||||
"runtimes/win-arm64/native/msvcp140_1.dll": {
|
||||
"rid": "win-arm64",
|
||||
"assetType": "native",
|
||||
"fileVersion": "14.42.34433.0"
|
||||
},
|
||||
"runtimes/win-arm64/native/msvcp140_2.dll": {
|
||||
"rid": "win-arm64",
|
||||
"assetType": "native",
|
||||
"fileVersion": "14.42.34433.0"
|
||||
},
|
||||
"runtimes/win-arm64/native/msvcp140_atomic_wait.dll": {
|
||||
"rid": "win-arm64",
|
||||
"assetType": "native",
|
||||
"fileVersion": "14.42.34433.0"
|
||||
},
|
||||
"runtimes/win-arm64/native/msvcp140_codecvt_ids.dll": {
|
||||
"rid": "win-arm64",
|
||||
"assetType": "native",
|
||||
"fileVersion": "14.42.34433.0"
|
||||
},
|
||||
"runtimes/win-arm64/native/vcruntime140.dll": {
|
||||
"rid": "win-arm64",
|
||||
"assetType": "native",
|
||||
"fileVersion": "14.42.34433.0"
|
||||
},
|
||||
"runtimes/win-arm64/native/vcruntime140_1.dll": {
|
||||
"rid": "win-arm64",
|
||||
"assetType": "native",
|
||||
"fileVersion": "14.42.34433.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Emgu.runtime.windows.msvc.rt.x64/19.42.34435": {
|
||||
"runtimeTargets": {
|
||||
"runtimes/win-x64/native/concrt140.dll": {
|
||||
"rid": "win-x64",
|
||||
"assetType": "native",
|
||||
"fileVersion": "14.42.34433.0"
|
||||
},
|
||||
"runtimes/win-x64/native/msvcp140.dll": {
|
||||
"rid": "win-x64",
|
||||
"assetType": "native",
|
||||
"fileVersion": "14.42.34433.0"
|
||||
},
|
||||
"runtimes/win-x64/native/msvcp140_1.dll": {
|
||||
"rid": "win-x64",
|
||||
"assetType": "native",
|
||||
"fileVersion": "14.42.34433.0"
|
||||
},
|
||||
"runtimes/win-x64/native/msvcp140_2.dll": {
|
||||
"rid": "win-x64",
|
||||
"assetType": "native",
|
||||
"fileVersion": "14.42.34433.0"
|
||||
},
|
||||
"runtimes/win-x64/native/msvcp140_atomic_wait.dll": {
|
||||
"rid": "win-x64",
|
||||
"assetType": "native",
|
||||
"fileVersion": "14.42.34433.0"
|
||||
},
|
||||
"runtimes/win-x64/native/msvcp140_codecvt_ids.dll": {
|
||||
"rid": "win-x64",
|
||||
"assetType": "native",
|
||||
"fileVersion": "14.42.34433.0"
|
||||
},
|
||||
"runtimes/win-x64/native/vcruntime140.dll": {
|
||||
"rid": "win-x64",
|
||||
"assetType": "native",
|
||||
"fileVersion": "14.42.34433.0"
|
||||
},
|
||||
"runtimes/win-x64/native/vcruntime140_1.dll": {
|
||||
"rid": "win-x64",
|
||||
"assetType": "native",
|
||||
"fileVersion": "14.42.34433.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Emgu.runtime.windows.msvc.rt.x86/19.42.34435": {
|
||||
"runtimeTargets": {
|
||||
"runtimes/win-x86/native/concrt140.dll": {
|
||||
"rid": "win-x86",
|
||||
"assetType": "native",
|
||||
"fileVersion": "14.42.34433.0"
|
||||
},
|
||||
"runtimes/win-x86/native/msvcp140.dll": {
|
||||
"rid": "win-x86",
|
||||
"assetType": "native",
|
||||
"fileVersion": "14.42.34433.0"
|
||||
},
|
||||
"runtimes/win-x86/native/msvcp140_1.dll": {
|
||||
"rid": "win-x86",
|
||||
"assetType": "native",
|
||||
"fileVersion": "14.42.34433.0"
|
||||
},
|
||||
"runtimes/win-x86/native/msvcp140_2.dll": {
|
||||
"rid": "win-x86",
|
||||
"assetType": "native",
|
||||
"fileVersion": "14.42.34433.0"
|
||||
},
|
||||
"runtimes/win-x86/native/msvcp140_atomic_wait.dll": {
|
||||
"rid": "win-x86",
|
||||
"assetType": "native",
|
||||
"fileVersion": "14.42.34433.0"
|
||||
},
|
||||
"runtimes/win-x86/native/msvcp140_codecvt_ids.dll": {
|
||||
"rid": "win-x86",
|
||||
"assetType": "native",
|
||||
"fileVersion": "14.42.34433.0"
|
||||
},
|
||||
"runtimes/win-x86/native/vcruntime140.dll": {
|
||||
"rid": "win-x86",
|
||||
"assetType": "native",
|
||||
"fileVersion": "14.42.34433.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Microsoft.Bcl.AsyncInterfaces/1.1.1": {
|
||||
"runtime": {
|
||||
"lib/netstandard2.1/Microsoft.Bcl.AsyncInterfaces.dll": {
|
||||
@@ -1868,7 +1696,6 @@
|
||||
"XP.Common/1.0.0": {
|
||||
"dependencies": {
|
||||
"Emgu.CV": "4.10.0.5680",
|
||||
"Emgu.CV.runtime.windows": "4.10.0.5680",
|
||||
"Microsoft.Data.Sqlite": "10.0.3",
|
||||
"Prism.Wpf": "9.0.537",
|
||||
"Serilog": "4.3.1",
|
||||
@@ -1910,34 +1737,6 @@
|
||||
"path": "emgu.cv/4.10.0.5680",
|
||||
"hashPath": "emgu.cv.4.10.0.5680.nupkg.sha512"
|
||||
},
|
||||
"Emgu.CV.runtime.windows/4.10.0.5680": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-WzBJWWENbF1oQKBo92ODtt9V/Eh4MFo8Y3MxxpWdoKJnZAjStxlE5epy3rB7442ZPAIRAiAaqPPZfrdml5Q9vQ==",
|
||||
"path": "emgu.cv.runtime.windows/4.10.0.5680",
|
||||
"hashPath": "emgu.cv.runtime.windows.4.10.0.5680.nupkg.sha512"
|
||||
},
|
||||
"Emgu.runtime.windows.msvc.rt.arm64/19.42.34435": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-tb+JdhPLkX0MsMweKhL8zY/XUm+opxl6FLP7fyMf93EqUrUmR3YAyxmzcTyDyndh4XQ4bl+Eain9ugm7y/e26g==",
|
||||
"path": "emgu.runtime.windows.msvc.rt.arm64/19.42.34435",
|
||||
"hashPath": "emgu.runtime.windows.msvc.rt.arm64.19.42.34435.nupkg.sha512"
|
||||
},
|
||||
"Emgu.runtime.windows.msvc.rt.x64/19.42.34435": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-3aIW16hRIV05a9oRW899LjPhdea/FJamPwZulwNB2P9YItwq+6XMqMeV3lg80giIQalAHQlVRVbwC54N6q3NMw==",
|
||||
"path": "emgu.runtime.windows.msvc.rt.x64/19.42.34435",
|
||||
"hashPath": "emgu.runtime.windows.msvc.rt.x64.19.42.34435.nupkg.sha512"
|
||||
},
|
||||
"Emgu.runtime.windows.msvc.rt.x86/19.42.34435": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-NrCzZrq+S9jJOLWgh0ybVeHEQgillrASOFhlvV7FRrW1ufxCI3uZgySIpsGYSWQaJYpi5A4jfTP6Ekcjl3shDw==",
|
||||
"path": "emgu.runtime.windows.msvc.rt.x86/19.42.34435",
|
||||
"hashPath": "emgu.runtime.windows.msvc.rt.x86.19.42.34435.nupkg.sha512"
|
||||
},
|
||||
"Microsoft.Bcl.AsyncInterfaces/1.1.1": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
|
||||
@@ -41,7 +41,8 @@
|
||||
MouseLeftButtonDown="Canvas_MouseLeftButtonDown"
|
||||
MouseLeftButtonUp="Canvas_MouseLeftButtonUp"
|
||||
MouseMove="Canvas_MouseMove"
|
||||
MouseRightButtonDown="Canvas_MouseRightButtonDown">
|
||||
MouseRightButtonDown="Canvas_MouseRightButtonDown"
|
||||
PreviewMouseRightButtonUp="Canvas_PreviewMouseRightButtonUp">
|
||||
|
||||
<!-- 背景图像 -->
|
||||
<Image x:Name="backgroundImage"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,87 @@
|
||||
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 int Index { 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 = (Index > 0 ? $"#{Index} " : "") + $"{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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Shapes;
|
||||
|
||||
namespace XP.ImageProcessing.RoiControl.Models
|
||||
{
|
||||
/// <summary>BGA 空隙测量中的一个圆(气泡或焊球)</summary>
|
||||
public class BgaCircle
|
||||
{
|
||||
public Ellipse Shape { get; set; } // 圆形轮廓
|
||||
public Ellipse CenterDot { get; set; } // 中心拖拽点
|
||||
public Ellipse EdgeDot { get; set; } // 边缘拖拽点(调半径)
|
||||
public Point Center { get; set; }
|
||||
public double Radius { get; set; }
|
||||
public bool IsBall { get; set; } // true=焊球, false=气泡
|
||||
|
||||
public double Area => Math.PI * Radius * Radius;
|
||||
|
||||
public void UpdateVisuals()
|
||||
{
|
||||
double d = Radius * 2;
|
||||
Shape.Width = d; Shape.Height = d;
|
||||
Canvas.SetLeft(Shape, Center.X - Radius);
|
||||
Canvas.SetTop(Shape, Center.Y - Radius);
|
||||
|
||||
Canvas.SetLeft(CenterDot, Center.X - CenterDot.Width / 2);
|
||||
Canvas.SetTop(CenterDot, Center.Y - CenterDot.Height / 2);
|
||||
|
||||
var edgePt = new Point(Center.X + Radius, Center.Y);
|
||||
Canvas.SetLeft(EdgeDot, edgePt.X - EdgeDot.Width / 2);
|
||||
Canvas.SetTop(EdgeDot, edgePt.Y - EdgeDot.Height / 2);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>一次 BGA 空隙测量组(1个焊球 + N个气泡 + 标签)</summary>
|
||||
public class BgaVoidGroup
|
||||
{
|
||||
public BgaCircle Ball { get; set; }
|
||||
public List<BgaCircle> Voids { get; } = new();
|
||||
public TextBlock Label { get; set; }
|
||||
public int Index { get; set; }
|
||||
public double VoidLimit { get; set; } = 25.0;
|
||||
|
||||
public double VoidRate
|
||||
{
|
||||
get
|
||||
{
|
||||
if (Ball == null || Ball.Area < 1) return 0;
|
||||
double totalVoid = 0;
|
||||
foreach (var v in Voids) totalVoid += v.Area;
|
||||
return Math.Clamp(totalVoid / Ball.Area * 100.0, 0, 100);
|
||||
}
|
||||
}
|
||||
|
||||
public string Classification => VoidRate <= VoidLimit ? "PASS" : "FAIL";
|
||||
|
||||
public void UpdateLabel()
|
||||
{
|
||||
if (Label == null || Ball == null) return;
|
||||
double rate = VoidRate;
|
||||
string cls = Classification;
|
||||
Label.Text = (Index > 0 ? $"#{Index} " : "") +
|
||||
$"Void: {rate:F1}% | Limit: {VoidLimit:F1}% | {cls}";
|
||||
Label.Foreground = cls == "PASS" ? Brushes.Lime : Brushes.Red;
|
||||
|
||||
Canvas.SetLeft(Label, Ball.Center.X + Ball.Radius + 10);
|
||||
Canvas.SetTop(Label, Ball.Center.Y - 10);
|
||||
Label.Visibility = Visibility.Visible;
|
||||
}
|
||||
|
||||
/// <summary>获取所有 UI 元素</summary>
|
||||
public List<UIElement> AllElements
|
||||
{
|
||||
get
|
||||
{
|
||||
var list = new List<UIElement>();
|
||||
if (Ball != null) { list.Add(Ball.Shape); list.Add(Ball.CenterDot); list.Add(Ball.EdgeDot); }
|
||||
foreach (var v in Voids) { list.Add(v.Shape); list.Add(v.CenterDot); list.Add(v.EdgeDot); }
|
||||
if (Label != null) list.Add(Label);
|
||||
return list;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
using System;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Shapes;
|
||||
|
||||
namespace XP.ImageProcessing.RoiControl.Models
|
||||
{
|
||||
/// <summary>通孔填锡率测量组(4个椭圆中心 + 连线 + 标签)</summary>
|
||||
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 int Index { get; set; }
|
||||
|
||||
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 = (Index > 0 ? $"#{Index} " : "") + $"Fill: {rate:F1}% | THTLimit: {THTLimit: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;
|
||||
}
|
||||
|
||||
/// <summary>获取所有 UI 元素(用于添加/移除)</summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -34,10 +34,12 @@ namespace XP.ImageProcessing.RoiControl.Models
|
||||
|
||||
public void UpdateLabel(string distanceText = null)
|
||||
{
|
||||
Label.Text = distanceText ?? $"{Distance:F2} px";
|
||||
Label.Text = (Index > 0 ? $"#{Index} " : "") + (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;
|
||||
}
|
||||
|
||||
public int Index { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,10 @@ namespace XP.ImageProcessing.RoiControl.Models
|
||||
{
|
||||
None,
|
||||
PointDistance,
|
||||
PointToLine
|
||||
PointToLine,
|
||||
Angle,
|
||||
FillRate,
|
||||
BgaVoid,
|
||||
BubbleMeasure
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ namespace XP.ImageProcessing.RoiControl.Models
|
||||
public Point L1 { get; set; }
|
||||
public Point L2 { get; set; }
|
||||
public Point P { get; set; }
|
||||
public int Index { get; set; }
|
||||
|
||||
public double Distance
|
||||
{
|
||||
@@ -85,7 +86,7 @@ namespace XP.ImageProcessing.RoiControl.Models
|
||||
FootDot.Visibility = Visibility.Visible;
|
||||
|
||||
// 标签
|
||||
Label.Text = distanceText ?? $"{Distance:F2} px";
|
||||
Label.Text = (Index > 0 ? $"#{Index} " : "") + (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;
|
||||
|
||||
@@ -378,7 +378,8 @@ namespace XplorePlane
|
||||
|
||||
// 注册视图和视图模型
|
||||
containerRegistry.RegisterForNavigation<MainWindow>();
|
||||
containerRegistry.Register<MainViewModel>();
|
||||
containerRegistry.RegisterSingleton<MainViewModel>();
|
||||
containerRegistry.RegisterSingleton<ViewportPanelViewModel>();
|
||||
containerRegistry.RegisterSingleton<NavigationPropertyPanelViewModel>();
|
||||
|
||||
// 注册图像处理服务与视图
|
||||
|
||||
@@ -11,7 +11,9 @@ namespace XplorePlane.Events
|
||||
PointDistance,
|
||||
PointLineDistance,
|
||||
Angle,
|
||||
ThroughHoleFillRate
|
||||
ThroughHoleFillRate,
|
||||
BgaVoid,
|
||||
BubbleMeasure
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -15,6 +15,7 @@ using XplorePlane.ViewModels.Cnc;
|
||||
using XplorePlane.Views;
|
||||
using XplorePlane.Views.Cnc;
|
||||
using XP.Common.Logging.Interfaces;
|
||||
using XP.Common.GeneralForm.Views;
|
||||
using XP.Common.PdfViewer.Interfaces;
|
||||
using XP.Hardware.MotionControl.Abstractions;
|
||||
|
||||
@@ -31,6 +32,97 @@ namespace XplorePlane.ViewModels
|
||||
private readonly CncEditorViewModel _cncEditorViewModel;
|
||||
private readonly CncPageView _cncPageView;
|
||||
|
||||
public string LicenseInfo
|
||||
{
|
||||
get => _licenseInfo;
|
||||
set => SetProperty(ref _licenseInfo, value);
|
||||
}
|
||||
|
||||
private string _statusMessage = "就绪";
|
||||
public string StatusMessage
|
||||
{
|
||||
get => _statusMessage;
|
||||
set => SetProperty(ref _statusMessage, value);
|
||||
}
|
||||
|
||||
public ObservableCollection<object> NavigationTree { get; set; }
|
||||
|
||||
// 导航命令
|
||||
public DelegateCommand NavigateHomeCommand { get; set; }
|
||||
public DelegateCommand NavigateInspectCommand { get; set; }
|
||||
public DelegateCommand OpenFileCommand { get; set; }
|
||||
public DelegateCommand ExportCommand { get; set; }
|
||||
public DelegateCommand ClearCommand { get; set; }
|
||||
public DelegateCommand EditPropertiesCommand { get; set; }
|
||||
|
||||
// 窗口打开命令
|
||||
public DelegateCommand OpenImageProcessingCommand { get; }
|
||||
public DelegateCommand LoadImageCommand { get; }
|
||||
public DelegateCommand OpenPipelineEditorCommand { get; }
|
||||
public DelegateCommand OpenCncEditorCommand { get; }
|
||||
public DelegateCommand OpenMatrixEditorCommand { get; }
|
||||
public DelegateCommand OpenToolboxCommand { get; }
|
||||
public DelegateCommand OpenLibraryVersionsCommand { get; }
|
||||
public DelegateCommand OpenUserManualCommand { get; }
|
||||
public DelegateCommand OpenCameraSettingsCommand { get; }
|
||||
public DelegateCommand NewCncProgramCommand { get; }
|
||||
public DelegateCommand SaveCncProgramCommand { get; }
|
||||
public DelegateCommand LoadCncProgramCommand { get; }
|
||||
public DelegateCommand InsertReferencePointCommand { get; }
|
||||
public DelegateCommand InsertSavePositionCommand { get; }
|
||||
public DelegateCommand InsertCompleteProgramCommand { get; }
|
||||
public DelegateCommand InsertInspectionMarkerCommand { get; }
|
||||
public DelegateCommand InsertInspectionModuleCommand { get; }
|
||||
public DelegateCommand InsertSaveNodeCommand { get; }
|
||||
public DelegateCommand InsertPauseDialogCommand { get; }
|
||||
public DelegateCommand InsertWaitDelayCommand { get; }
|
||||
|
||||
// 硬件命令
|
||||
public DelegateCommand AxisResetCommand { get; }
|
||||
public DelegateCommand OpenDetectorConfigCommand { get; }
|
||||
public DelegateCommand OpenMotionDebugCommand { get; }
|
||||
public DelegateCommand OpenPlcAddrConfigCommand { get; }
|
||||
public DelegateCommand OpenRaySourceConfigCommand { get; }
|
||||
public DelegateCommand WarmUpCommand { get; }
|
||||
|
||||
// 测量命令
|
||||
public DelegateCommand PointDistanceMeasureCommand { get; }
|
||||
public DelegateCommand PointLineDistanceMeasureCommand { get; }
|
||||
public DelegateCommand AngleMeasureCommand { get; }
|
||||
public DelegateCommand ThroughHoleFillRateMeasureCommand { get; }
|
||||
public DelegateCommand BgaVoidMeasureCommand { get; }
|
||||
public DelegateCommand BubbleMeasureCommand { get; }
|
||||
|
||||
// 辅助线命令
|
||||
public DelegateCommand ToggleCrosshairCommand { get; }
|
||||
|
||||
// 设置命令
|
||||
public DelegateCommand OpenLanguageSwitcherCommand { get; }
|
||||
public DelegateCommand OpenRealTimeLogViewerCommand { get; }
|
||||
|
||||
/// <summary>右侧图像区域内容 | Right-side image panel content</summary>
|
||||
public object ImagePanelContent
|
||||
{
|
||||
get => _imagePanelContent;
|
||||
set => SetProperty(ref _imagePanelContent, value);
|
||||
}
|
||||
|
||||
/// <summary>右侧图像区域宽度 | Right-side image panel width</summary>
|
||||
public GridLength ImagePanelWidth
|
||||
{
|
||||
get => _imagePanelWidth;
|
||||
set => SetProperty(ref _imagePanelWidth, value);
|
||||
}
|
||||
|
||||
/// <summary>主视图区宽度 | Main viewport width</summary>
|
||||
public GridLength ViewportPanelWidth
|
||||
{
|
||||
get => _viewportPanelWidth;
|
||||
set => SetProperty(ref _viewportPanelWidth, value);
|
||||
}
|
||||
|
||||
// 窗口引用(单例窗口防止重复打开)
|
||||
|
||||
private Window _motionDebugWindow;
|
||||
private Window _detectorConfigWindow;
|
||||
private Window _plcAddrConfigWindow;
|
||||
@@ -92,6 +184,11 @@ namespace XplorePlane.ViewModels
|
||||
PointLineDistanceMeasureCommand = new DelegateCommand(ExecutePointLineDistanceMeasure);
|
||||
AngleMeasureCommand = new DelegateCommand(ExecuteAngleMeasure);
|
||||
ThroughHoleFillRateMeasureCommand = new DelegateCommand(ExecuteThroughHoleFillRateMeasure);
|
||||
BgaVoidMeasureCommand = new DelegateCommand(ExecuteBgaVoidMeasure);
|
||||
BubbleMeasureCommand = new DelegateCommand(ExecuteBubbleMeasure);
|
||||
|
||||
// 辅助线命令
|
||||
|
||||
ToggleCrosshairCommand = new DelegateCommand(() =>
|
||||
_eventAggregator.GetEvent<ToggleCrosshairEvent>().Publish());
|
||||
|
||||
@@ -113,92 +210,6 @@ namespace XplorePlane.ViewModels
|
||||
_logger.Info("MainViewModel 已初始化");
|
||||
}
|
||||
|
||||
public string LicenseInfo
|
||||
{
|
||||
get => _licenseInfo;
|
||||
set => SetProperty(ref _licenseInfo, value);
|
||||
}
|
||||
|
||||
public ObservableCollection<object> NavigationTree { get; set; }
|
||||
|
||||
public DelegateCommand NavigateHomeCommand { get; set; }
|
||||
public DelegateCommand NavigateInspectCommand { get; set; }
|
||||
public DelegateCommand OpenFileCommand { get; set; }
|
||||
public DelegateCommand ExportCommand { get; set; }
|
||||
public DelegateCommand ClearCommand { get; set; }
|
||||
public DelegateCommand EditPropertiesCommand { get; set; }
|
||||
|
||||
public DelegateCommand OpenImageProcessingCommand { get; }
|
||||
public DelegateCommand LoadImageCommand { get; }
|
||||
public DelegateCommand OpenPipelineEditorCommand { get; }
|
||||
public DelegateCommand OpenCncEditorCommand { get; }
|
||||
public DelegateCommand OpenMatrixEditorCommand { get; }
|
||||
public DelegateCommand OpenToolboxCommand { get; }
|
||||
public DelegateCommand OpenLibraryVersionsCommand { get; }
|
||||
public DelegateCommand OpenUserManualCommand { get; }
|
||||
public DelegateCommand OpenCameraSettingsCommand { get; }
|
||||
public DelegateCommand NewCncProgramCommand { get; }
|
||||
public DelegateCommand SaveCncProgramCommand { get; }
|
||||
public DelegateCommand LoadCncProgramCommand { get; }
|
||||
public DelegateCommand InsertReferencePointCommand { get; }
|
||||
public DelegateCommand InsertSavePositionCommand { get; }
|
||||
public DelegateCommand InsertCompleteProgramCommand { get; }
|
||||
public DelegateCommand InsertInspectionMarkerCommand { get; }
|
||||
public DelegateCommand InsertInspectionModuleCommand { get; }
|
||||
public DelegateCommand InsertSaveNodeCommand { get; }
|
||||
public DelegateCommand InsertPauseDialogCommand { get; }
|
||||
public DelegateCommand InsertWaitDelayCommand { get; }
|
||||
|
||||
public DelegateCommand AxisResetCommand { get; }
|
||||
public DelegateCommand OpenDetectorConfigCommand { get; }
|
||||
public DelegateCommand OpenMotionDebugCommand { get; }
|
||||
public DelegateCommand OpenPlcAddrConfigCommand { get; }
|
||||
public DelegateCommand OpenRaySourceConfigCommand { get; }
|
||||
public DelegateCommand WarmUpCommand { get; }
|
||||
|
||||
public DelegateCommand PointDistanceMeasureCommand { get; }
|
||||
public DelegateCommand PointLineDistanceMeasureCommand { get; }
|
||||
public DelegateCommand AngleMeasureCommand { get; }
|
||||
public DelegateCommand ThroughHoleFillRateMeasureCommand { get; }
|
||||
public DelegateCommand ToggleCrosshairCommand { get; }
|
||||
|
||||
public DelegateCommand OpenLanguageSwitcherCommand { get; }
|
||||
public DelegateCommand OpenRealTimeLogViewerCommand { get; }
|
||||
public DelegateCommand UseLiveDetectorSourceCommand { get; }
|
||||
|
||||
public bool IsMainViewportRealtimeEnabled
|
||||
{
|
||||
get => _mainViewportService.IsRealtimeDisplayEnabled;
|
||||
set
|
||||
{
|
||||
if (_mainViewportService.IsRealtimeDisplayEnabled == value)
|
||||
return;
|
||||
|
||||
_mainViewportService.SetRealtimeDisplayEnabled(value);
|
||||
RaisePropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsUsingLiveDetectorSource => _mainViewportService.CurrentSourceMode == MainViewportSourceMode.LiveDetector;
|
||||
|
||||
public object ImagePanelContent
|
||||
{
|
||||
get => _imagePanelContent;
|
||||
set => SetProperty(ref _imagePanelContent, value);
|
||||
}
|
||||
|
||||
public GridLength ImagePanelWidth
|
||||
{
|
||||
get => _imagePanelWidth;
|
||||
set => SetProperty(ref _imagePanelWidth, value);
|
||||
}
|
||||
|
||||
public GridLength ViewportPanelWidth
|
||||
{
|
||||
get => _viewportPanelWidth;
|
||||
set => SetProperty(ref _viewportPanelWidth, value);
|
||||
}
|
||||
|
||||
private void ShowWindow(Window window, string name)
|
||||
{
|
||||
window.Owner = Application.Current.MainWindow;
|
||||
@@ -436,30 +447,106 @@ namespace XplorePlane.ViewModels
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 测量命令实现
|
||||
|
||||
private bool CheckImageLoaded()
|
||||
{
|
||||
try
|
||||
{
|
||||
var viewportVm = _containerProvider.Resolve<ViewportPanelViewModel>();
|
||||
if (viewportVm?.ImageSource != null) return true;
|
||||
}
|
||||
catch { }
|
||||
HexMessageBox.Show("请先加载图像", MessageBoxButton.OK, MessageBoxImage.Information);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
private void ExecutePointDistanceMeasure()
|
||||
{
|
||||
if (!CheckImageLoaded()) return;
|
||||
_logger.Info("点点距测量功能已触发");
|
||||
_eventAggregator.GetEvent<MeasurementToolEvent>().Publish(MeasurementToolMode.PointDistance);
|
||||
}
|
||||
|
||||
private void ExecutePointLineDistanceMeasure()
|
||||
{
|
||||
if (!CheckImageLoaded()) return;
|
||||
_logger.Info("点线距测量功能已触发");
|
||||
_eventAggregator.GetEvent<MeasurementToolEvent>().Publish(MeasurementToolMode.PointLineDistance);
|
||||
}
|
||||
|
||||
private void ExecuteAngleMeasure()
|
||||
{
|
||||
if (!CheckImageLoaded()) return;
|
||||
_logger.Info("角度测量功能已触发");
|
||||
_eventAggregator.GetEvent<MeasurementToolEvent>().Publish(MeasurementToolMode.Angle);
|
||||
}
|
||||
|
||||
private void ExecuteThroughHoleFillRateMeasure()
|
||||
{
|
||||
if (!CheckImageLoaded()) return;
|
||||
_logger.Info("通孔填锡率测量功能已触发");
|
||||
_eventAggregator.GetEvent<MeasurementToolEvent>().Publish(MeasurementToolMode.ThroughHoleFillRate);
|
||||
}
|
||||
|
||||
private Window _bgaMeasurePanel;
|
||||
|
||||
private void ExecuteBgaVoidMeasure()
|
||||
{
|
||||
if (!CheckImageLoaded()) return;
|
||||
_logger.Info("BGA空隙测量功能已触发");
|
||||
_eventAggregator.GetEvent<MeasurementToolEvent>().Publish(MeasurementToolMode.BgaVoid);
|
||||
|
||||
if (_bgaMeasurePanel != null && _bgaMeasurePanel.IsVisible)
|
||||
{
|
||||
_bgaMeasurePanel.Activate();
|
||||
return;
|
||||
}
|
||||
|
||||
_bgaMeasurePanel = new Views.ImageProcessing.BgaMeasurePanel
|
||||
{
|
||||
Owner = System.Windows.Application.Current.MainWindow
|
||||
};
|
||||
_bgaMeasurePanel.Closed += (s, e) =>
|
||||
{
|
||||
_eventAggregator.GetEvent<MeasurementToolEvent>().Publish(MeasurementToolMode.None);
|
||||
};
|
||||
_bgaMeasurePanel.Show();
|
||||
}
|
||||
|
||||
private Window _bubbleMeasurePanel;
|
||||
|
||||
private void ExecuteBubbleMeasure()
|
||||
{
|
||||
if (!CheckImageLoaded()) return;
|
||||
_logger.Info("气泡测量功能已触发");
|
||||
|
||||
// 进入气泡测量模式
|
||||
_eventAggregator.GetEvent<MeasurementToolEvent>().Publish(MeasurementToolMode.BubbleMeasure);
|
||||
|
||||
// 弹出工具面板
|
||||
if (_bubbleMeasurePanel != null && _bubbleMeasurePanel.IsVisible)
|
||||
{
|
||||
_bubbleMeasurePanel.Activate();
|
||||
return;
|
||||
}
|
||||
|
||||
_bubbleMeasurePanel = new Views.ImageProcessing.BubbleMeasurePanel
|
||||
{
|
||||
Owner = System.Windows.Application.Current.MainWindow
|
||||
};
|
||||
_bubbleMeasurePanel.Closed += (s, e) =>
|
||||
{
|
||||
// 关闭面板时退出气泡测量模式
|
||||
_eventAggregator.GetEvent<MeasurementToolEvent>().Publish(MeasurementToolMode.None);
|
||||
};
|
||||
_bubbleMeasurePanel.Show();
|
||||
|
||||
}
|
||||
|
||||
private void ExecuteOpenLanguageSwitcher()
|
||||
{
|
||||
try
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
<Window
|
||||
x:Class="XplorePlane.Views.ImageProcessing.BgaMeasurePanel"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
Title="BGA空隙测量"
|
||||
Width="240" Height="260"
|
||||
ResizeMode="NoResize"
|
||||
WindowStartupLocation="CenterOwner"
|
||||
Topmost="True"
|
||||
ShowInTaskbar="False">
|
||||
<Window.Resources>
|
||||
<Style x:Key="ToolToggleStyle" TargetType="RadioButton">
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="RadioButton">
|
||||
<Border x:Name="Bd" Background="#EEEEEE" BorderBrush="#CCCCCC"
|
||||
BorderThickness="1" CornerRadius="3" Padding="14,5" Cursor="Hand">
|
||||
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center" />
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsChecked" Value="True">
|
||||
<Setter TargetName="Bd" Property="Background" Value="#0078D7" />
|
||||
<Setter TargetName="Bd" Property="BorderBrush" Value="#005A9E" />
|
||||
<Setter Property="Foreground" Value="White" />
|
||||
</Trigger>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter TargetName="Bd" Property="Background" Value="#DDDDDD" />
|
||||
</Trigger>
|
||||
<MultiTrigger>
|
||||
<MultiTrigger.Conditions>
|
||||
<Condition Property="IsMouseOver" Value="True" />
|
||||
<Condition Property="IsChecked" Value="True" />
|
||||
</MultiTrigger.Conditions>
|
||||
<Setter TargetName="Bd" Property="Background" Value="#006CBE" />
|
||||
</MultiTrigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
</Window.Resources>
|
||||
<StackPanel Margin="10">
|
||||
<!-- 绘制模式 -->
|
||||
<TextBlock Text="绘制模式" FontWeight="SemiBold" Margin="0,0,0,4" />
|
||||
<WrapPanel>
|
||||
<RadioButton x:Name="RbVoid" Content="画气泡" IsChecked="True" Margin="0,0,6,4"
|
||||
Style="{StaticResource ToolToggleStyle}" />
|
||||
<RadioButton x:Name="RbBall" Content="画焊球" Margin="0,0,0,4"
|
||||
Style="{StaticResource ToolToggleStyle}" />
|
||||
</WrapPanel>
|
||||
<TextBlock Text="左键点两次画圆(圆心+半径),右键删除" FontSize="10" Foreground="Gray" Margin="0,2,0,0" />
|
||||
|
||||
<Separator Margin="0,8" />
|
||||
|
||||
<!-- VoidLimit -->
|
||||
<TextBlock Text="VoidLimit(%)" Margin="0,0,0,4" />
|
||||
<DockPanel>
|
||||
<TextBox x:Name="TbVoidLimit" DockPanel.Dock="Right" Width="50" Text="25.0"
|
||||
VerticalContentAlignment="Center" Margin="6,0,0,0" />
|
||||
<Slider x:Name="SliderVoidLimit" Minimum="0" Maximum="100" Value="25"
|
||||
VerticalAlignment="Center" />
|
||||
</DockPanel>
|
||||
|
||||
<Separator Margin="0,8" />
|
||||
|
||||
<!-- 结果 -->
|
||||
<TextBlock x:Name="TbResult" Text="空隙率: --" FontSize="14" FontWeight="SemiBold" Margin="0,0,0,8" />
|
||||
|
||||
<!-- 操作 -->
|
||||
<WrapPanel HorizontalAlignment="Center">
|
||||
<Button Content="完成" Padding="14,4" Click="Finish_Click" />
|
||||
</WrapPanel>
|
||||
</StackPanel>
|
||||
</Window>
|
||||
@@ -0,0 +1,74 @@
|
||||
using System.Windows;
|
||||
using XP.ImageProcessing.RoiControl.Controls;
|
||||
|
||||
namespace XplorePlane.Views.ImageProcessing
|
||||
{
|
||||
public partial class BgaMeasurePanel : Window
|
||||
{
|
||||
private PolygonRoiCanvas _canvas;
|
||||
|
||||
public BgaMeasurePanel()
|
||||
{
|
||||
InitializeComponent();
|
||||
Loaded += OnLoaded;
|
||||
}
|
||||
|
||||
private void OnLoaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
var mainWin = Owner as MainWindow;
|
||||
if (mainWin != null)
|
||||
_canvas = FindChild<PolygonRoiCanvas>(mainWin);
|
||||
}
|
||||
catch { }
|
||||
|
||||
// 模式切换:通知 canvas 切换气泡/焊球
|
||||
RbVoid.Checked += (s, ev) =>
|
||||
{
|
||||
if (_canvas != null) _canvas.SetBgaDrawBall(false);
|
||||
};
|
||||
RbBall.Checked += (s, ev) =>
|
||||
{
|
||||
if (_canvas != null) _canvas.SetBgaDrawBall(true);
|
||||
};
|
||||
|
||||
// VoidLimit 同步
|
||||
SliderVoidLimit.ValueChanged += (s, ev) =>
|
||||
{
|
||||
TbVoidLimit.Text = SliderVoidLimit.Value.ToString("F1");
|
||||
_canvas?.SetBgaVoidLimit(SliderVoidLimit.Value);
|
||||
};
|
||||
|
||||
// 监听测量完成事件更新结果
|
||||
if (_canvas != null)
|
||||
{
|
||||
_canvas.MeasureCompleted += (s, ev) =>
|
||||
{
|
||||
if (ev is MeasureCompletedEventArgs args && args.MeasureType == "BgaVoid")
|
||||
{
|
||||
TbResult.Text = $"空隙率: {args.Distance:F1}%";
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private void Finish_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
Close();
|
||||
}
|
||||
|
||||
private static T FindChild<T>(DependencyObject parent) where T : DependencyObject
|
||||
{
|
||||
int count = System.Windows.Media.VisualTreeHelper.GetChildrenCount(parent);
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var child = System.Windows.Media.VisualTreeHelper.GetChild(parent, i);
|
||||
if (child is T t) return t;
|
||||
var result = FindChild<T>(child);
|
||||
if (result != null) return result;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
<Window
|
||||
x:Class="XplorePlane.Views.ImageProcessing.BubbleMeasurePanel"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
Title="气泡测量工具"
|
||||
Width="260" Height="340"
|
||||
ResizeMode="NoResize"
|
||||
WindowStartupLocation="CenterOwner"
|
||||
Topmost="True"
|
||||
ShowInTaskbar="False">
|
||||
<Window.Resources>
|
||||
<Style x:Key="ToolToggleStyle" TargetType="RadioButton">
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="RadioButton">
|
||||
<Border x:Name="Bd" Background="#EEEEEE" BorderBrush="#CCCCCC"
|
||||
BorderThickness="1" CornerRadius="3" Padding="10,4" Cursor="Hand">
|
||||
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center" />
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsChecked" Value="True">
|
||||
<Setter TargetName="Bd" Property="Background" Value="#0078D7" />
|
||||
<Setter TargetName="Bd" Property="BorderBrush" Value="#005A9E" />
|
||||
<Setter Property="Foreground" Value="White" />
|
||||
</Trigger>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter TargetName="Bd" Property="Background" Value="#DDDDDD" />
|
||||
</Trigger>
|
||||
<MultiTrigger>
|
||||
<MultiTrigger.Conditions>
|
||||
<Condition Property="IsMouseOver" Value="True" />
|
||||
<Condition Property="IsChecked" Value="True" />
|
||||
</MultiTrigger.Conditions>
|
||||
<Setter TargetName="Bd" Property="Background" Value="#006CBE" />
|
||||
</MultiTrigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
</Window.Resources>
|
||||
<StackPanel Margin="10">
|
||||
<!-- 工具选择 -->
|
||||
<TextBlock Text="工具" FontWeight="SemiBold" Margin="0,0,0,4" />
|
||||
<WrapPanel>
|
||||
<RadioButton x:Name="RbRoi" Content="ROI" IsChecked="True" Margin="0,0,4,4"
|
||||
Style="{StaticResource ToolToggleStyle}" />
|
||||
<RadioButton x:Name="RbWand" Content="魔棒" Margin="0,0,4,4"
|
||||
Style="{StaticResource ToolToggleStyle}" />
|
||||
<RadioButton x:Name="RbBrush" Content="画笔" Margin="0,0,4,4"
|
||||
Style="{StaticResource ToolToggleStyle}" />
|
||||
<RadioButton x:Name="RbEraser" Content="橡皮擦" Margin="0,0,0,4"
|
||||
Style="{StaticResource ToolToggleStyle}" />
|
||||
</WrapPanel>
|
||||
|
||||
<Separator Margin="0,6" />
|
||||
|
||||
<!-- 阈值 -->
|
||||
<TextBlock Text="灰度阈值(与点击点的最大灰度差)" Margin="0,0,0,4" FontSize="11" Foreground="Gray" />
|
||||
<DockPanel>
|
||||
<TextBox x:Name="TbThreshold" DockPanel.Dock="Right" Width="45" Text="128"
|
||||
VerticalContentAlignment="Center" Margin="6,0,0,0" />
|
||||
<Slider x:Name="SliderThreshold" Minimum="0" Maximum="255" Value="128"
|
||||
VerticalAlignment="Center" />
|
||||
</DockPanel>
|
||||
|
||||
<!-- 画笔大小 -->
|
||||
<TextBlock Text="画笔/橡皮大小" Margin="0,8,0,4" />
|
||||
<DockPanel>
|
||||
<TextBox x:Name="TbBrushSize" DockPanel.Dock="Right" Width="45" Text="5"
|
||||
VerticalContentAlignment="Center" Margin="6,0,0,0" />
|
||||
<Slider x:Name="SliderBrushSize" Minimum="1" Maximum="30" Value="5"
|
||||
VerticalAlignment="Center" />
|
||||
</DockPanel>
|
||||
|
||||
<Separator Margin="0,8" />
|
||||
|
||||
<!-- 结果 -->
|
||||
<TextBlock Text="测量结果" FontWeight="SemiBold" Margin="0,0,0,4" />
|
||||
<TextBlock x:Name="TbResult" Text="空隙率: --" FontSize="14" Margin="0,0,0,4" />
|
||||
<DockPanel Margin="0,0,0,8">
|
||||
<TextBlock Text="VoidLimit(%):" VerticalAlignment="Center" />
|
||||
<TextBox x:Name="TbVoidLimit" Width="50" Text="25.0" Margin="6,0,0,0"
|
||||
VerticalContentAlignment="Center" />
|
||||
</DockPanel>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<WrapPanel HorizontalAlignment="Center">
|
||||
<Button Content="撤销" Padding="12,4" Margin="0,0,8,0" Click="Undo_Click" />
|
||||
<Button Content="清除标记" Padding="12,4" Margin="0,0,8,0" Click="ClearMask_Click" />
|
||||
<Button Content="完成" Padding="12,4" Click="Finish_Click" />
|
||||
</WrapPanel>
|
||||
</StackPanel>
|
||||
</Window>
|
||||
@@ -0,0 +1,98 @@
|
||||
using System.Windows;
|
||||
using Prism.Ioc;
|
||||
using XP.ImageProcessing.RoiControl.Controls;
|
||||
using XplorePlane.ViewModels;
|
||||
|
||||
namespace XplorePlane.Views.ImageProcessing
|
||||
{
|
||||
public partial class BubbleMeasurePanel : Window
|
||||
{
|
||||
private PolygonRoiCanvas _canvas;
|
||||
|
||||
public BubbleMeasurePanel()
|
||||
{
|
||||
InitializeComponent();
|
||||
Loaded += OnLoaded;
|
||||
}
|
||||
|
||||
private void OnLoaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
// 获取主界面的 RoiCanvas
|
||||
try
|
||||
{
|
||||
var mainWin = Owner as MainWindow;
|
||||
if (mainWin != null)
|
||||
{
|
||||
_canvas = FindChild<PolygonRoiCanvas>(mainWin);
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
|
||||
// 工具切换
|
||||
RbRoi.Checked += (s, ev) => _canvas?.SetBubbleTool(PolygonRoiCanvas.BubbleSubTool.Roi);
|
||||
RbWand.Checked += (s, ev) => _canvas?.SetBubbleTool(PolygonRoiCanvas.BubbleSubTool.Wand);
|
||||
RbBrush.Checked += (s, ev) => _canvas?.SetBubbleTool(PolygonRoiCanvas.BubbleSubTool.Brush);
|
||||
RbEraser.Checked += (s, ev) => _canvas?.SetBubbleTool(PolygonRoiCanvas.BubbleSubTool.Eraser);
|
||||
|
||||
// 阈值同步
|
||||
SliderThreshold.ValueChanged += (s, ev) =>
|
||||
{
|
||||
TbThreshold.Text = ((int)SliderThreshold.Value).ToString();
|
||||
_canvas?.SetBubbleThreshold((int)SliderThreshold.Value);
|
||||
};
|
||||
|
||||
// 画笔大小同步
|
||||
SliderBrushSize.ValueChanged += (s, ev) =>
|
||||
{
|
||||
TbBrushSize.Text = ((int)SliderBrushSize.Value).ToString();
|
||||
_canvas?.SetBubbleBrushSize((int)SliderBrushSize.Value);
|
||||
};
|
||||
|
||||
// 监听 canvas 的工具切换事件(ROI 画完后自动切换时同步面板)
|
||||
if (_canvas != null)
|
||||
{
|
||||
_canvas.BubbleToolChanged += (s, ev) =>
|
||||
{
|
||||
// 同步面板 radio button
|
||||
};
|
||||
|
||||
_canvas.MeasureCompleted += (s, ev) =>
|
||||
{
|
||||
if (ev is MeasureCompletedEventArgs args && args.MeasureType == "BubbleVoid")
|
||||
{
|
||||
TbResult.Text = $"空隙率: {args.Distance:F1}%";
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private void ClearMask_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
_canvas?.ClearBubbleMeasure();
|
||||
TbResult.Text = "空隙率: --";
|
||||
}
|
||||
|
||||
private void Undo_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
_canvas?.UndoBubble();
|
||||
}
|
||||
|
||||
private void Finish_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
Close();
|
||||
}
|
||||
|
||||
private static T FindChild<T>(DependencyObject parent) where T : DependencyObject
|
||||
{
|
||||
int count = System.Windows.Media.VisualTreeHelper.GetChildrenCount(parent);
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var child = System.Windows.Media.VisualTreeHelper.GetChild(parent, i);
|
||||
if (child is T t) return t;
|
||||
var result = FindChild<T>(child);
|
||||
if (result != null) return result;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -444,6 +444,24 @@
|
||||
SmallImage="/Assets/Icons/tools.png"
|
||||
Text="PLC 地址" />
|
||||
</StackPanel>
|
||||
|
||||
<!-- 第三列: BGA空隙测量 + 气泡测量 -->
|
||||
<StackPanel>
|
||||
<telerik:RadRibbonButton
|
||||
telerik:ScreenTip.Description="BGA焊球空隙率检测"
|
||||
telerik:ScreenTip.Title="BGA空隙测量"
|
||||
Command="{Binding BgaVoidMeasureCommand}"
|
||||
Size="Medium"
|
||||
SmallImage="/Assets/Icons/bga.png"
|
||||
Text="BGA空隙" />
|
||||
<telerik:RadRibbonButton
|
||||
telerik:ScreenTip.Description="手动气泡测量(魔棒+画笔)"
|
||||
telerik:ScreenTip.Title="气泡测量"
|
||||
Command="{Binding BubbleMeasureCommand}"
|
||||
Size="Medium"
|
||||
SmallImage="/Assets/Icons/pores.png"
|
||||
Text="气泡测量" />
|
||||
</StackPanel>
|
||||
</telerik:RadRibbonGroup>
|
||||
<telerik:RadRibbonGroup Header="多语言">
|
||||
<telerik:RadRibbonGroup.Variants>
|
||||
@@ -554,7 +572,7 @@
|
||||
FontFamily="Microsoft YaHei UI"
|
||||
FontSize="11"
|
||||
Foreground="White"
|
||||
Text="就绪" />
|
||||
Text="{Binding StatusMessage}" />
|
||||
|
||||
<TextBlock
|
||||
Grid.Column="1"
|
||||
|
||||
@@ -15,25 +15,51 @@ namespace XplorePlane.Views
|
||||
{
|
||||
public partial class ViewportPanelView : UserControl
|
||||
{
|
||||
private MainViewModel _mainVm;
|
||||
|
||||
private void SetStatus(string msg)
|
||||
{
|
||||
if (_mainVm == null)
|
||||
{
|
||||
try { _mainVm = ContainerLocator.Current?.Resolve<MainViewModel>(); } catch { }
|
||||
}
|
||||
if (_mainVm != null) _mainVm.StatusMessage = msg;
|
||||
}
|
||||
|
||||
public ViewportPanelView()
|
||||
{
|
||||
InitializeComponent();
|
||||
DataContextChanged += OnDataContextChanged;
|
||||
|
||||
// 测量事件 → 更新状态栏
|
||||
// 测量事件 → 更新主界面状态栏
|
||||
RoiCanvas.MeasureCompleted += (s, e) =>
|
||||
{
|
||||
if (e is MeasureCompletedEventArgs args && DataContext is ViewportPanelViewModel vm)
|
||||
if (e is MeasureCompletedEventArgs args)
|
||||
{
|
||||
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" => "角度",
|
||||
"FillRate" => "填锡率",
|
||||
"BgaVoid" => "BGA空隙",
|
||||
"BubbleVoid" => "气泡空隙",
|
||||
_ => "点点距"
|
||||
};
|
||||
string valueText = args.MeasureType switch
|
||||
{
|
||||
"Angle" => $"{args.Distance:F2}°",
|
||||
"FillRate" => $"{args.Distance:F1}%",
|
||||
"BgaVoid" => $"{args.Distance:F1}%",
|
||||
"BubbleVoid" => $"{args.Distance:F1}%",
|
||||
_ => $"{args.Distance:F2} px"
|
||||
};
|
||||
SetStatus($"{typeLabel}: {valueText} | 共 {args.TotalCount} 条测量");
|
||||
}
|
||||
};
|
||||
RoiCanvas.MeasureStatusChanged += (s, e) =>
|
||||
{
|
||||
if (e is MeasureStatusEventArgs args && DataContext is ViewportPanelViewModel vm)
|
||||
vm.ImageInfo = args.Message;
|
||||
if (e is MeasureStatusEventArgs args)
|
||||
SetStatus(args.Message);
|
||||
};
|
||||
|
||||
// 十字辅助线:直接订阅 Prism 事件
|
||||
@@ -52,6 +78,10 @@ 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,
|
||||
MeasurementToolMode.ThroughHoleFillRate => XP.ImageProcessing.RoiControl.Models.MeasureMode.FillRate,
|
||||
MeasurementToolMode.BgaVoid => XP.ImageProcessing.RoiControl.Models.MeasureMode.BgaVoid,
|
||||
MeasurementToolMode.BubbleMeasure => XP.ImageProcessing.RoiControl.Models.MeasureMode.BubbleMeasure,
|
||||
_ => XP.ImageProcessing.RoiControl.Models.MeasureMode.None
|
||||
};
|
||||
}, Prism.Events.ThreadOption.UIThread);
|
||||
@@ -82,10 +112,8 @@ namespace XplorePlane.Views
|
||||
{
|
||||
RoiCanvas.ClearMeasurements();
|
||||
if (DataContext is ViewportPanelViewModel vm)
|
||||
{
|
||||
vm.ResetMeasurementState();
|
||||
vm.ImageInfo = "已清除所有测量";
|
||||
}
|
||||
SetStatus("已清除所有测量");
|
||||
}
|
||||
|
||||
private void SaveOriginalImage_Click(object sender, RoutedEventArgs e)
|
||||
|
||||
Reference in New Issue
Block a user