2026-07-03 14:16:15 +08:00
|
|
|
|
using System.ComponentModel;
|
2026-06-30 12:10:29 +08:00
|
|
|
|
using System.Windows;
|
|
|
|
|
|
using System.Windows.Controls;
|
|
|
|
|
|
using System.Windows.Media;
|
|
|
|
|
|
using ScottPlot;
|
2026-07-03 14:16:15 +08:00
|
|
|
|
using ScottPlot.WPF;
|
2026-06-30 12:10:29 +08:00
|
|
|
|
using SSPCTester.Devices.Interfaces;
|
2026-07-05 13:33:55 +08:00
|
|
|
|
using SSPCTester.Logic.Calculation;
|
2026-06-30 12:10:29 +08:00
|
|
|
|
|
|
|
|
|
|
namespace SSPCTester.UI.Controls;
|
|
|
|
|
|
|
|
|
|
|
|
public partial class CurvePlotControl : UserControl
|
|
|
|
|
|
{
|
2026-07-03 14:16:15 +08:00
|
|
|
|
private readonly bool _isDesignMode;
|
|
|
|
|
|
private WpfPlot? _plot;
|
|
|
|
|
|
|
2026-06-30 12:10:29 +08:00
|
|
|
|
public CurvePlotControl()
|
|
|
|
|
|
{
|
|
|
|
|
|
InitializeComponent();
|
2026-07-03 14:16:15 +08:00
|
|
|
|
_isDesignMode = DesignerProperties.GetIsInDesignMode(this);
|
|
|
|
|
|
if (_isDesignMode)
|
|
|
|
|
|
{
|
|
|
|
|
|
DesignPlaceholder.Visibility = Visibility.Visible;
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-06-30 12:10:29 +08:00
|
|
|
|
Loaded += (_, _) => InitPlot();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public static readonly DependencyProperty CurveProperty = DependencyProperty.Register(
|
|
|
|
|
|
nameof(Curve), typeof(CurveData), typeof(CurvePlotControl),
|
|
|
|
|
|
new PropertyMetadata(null, (d, e) => ((CurvePlotControl)d).Render()));
|
|
|
|
|
|
|
|
|
|
|
|
public static readonly DependencyProperty IsRisingProperty = DependencyProperty.Register(
|
|
|
|
|
|
nameof(IsRising), typeof(bool), typeof(CurvePlotControl),
|
|
|
|
|
|
new PropertyMetadata(true, (d, e) => ((CurvePlotControl)d).Render()));
|
|
|
|
|
|
|
|
|
|
|
|
public static readonly DependencyProperty VMaxProperty = DependencyProperty.Register(
|
|
|
|
|
|
nameof(VMax), typeof(double), typeof(CurvePlotControl),
|
|
|
|
|
|
new PropertyMetadata(28.0, (d, e) => ((CurvePlotControl)d).Render()));
|
|
|
|
|
|
|
|
|
|
|
|
public static readonly DependencyProperty ViewStartMsProperty = DependencyProperty.Register(
|
|
|
|
|
|
nameof(ViewStartMs), typeof(double?), typeof(CurvePlotControl),
|
|
|
|
|
|
new PropertyMetadata(null, (d, e) => ((CurvePlotControl)d).Render()));
|
|
|
|
|
|
|
|
|
|
|
|
public static readonly DependencyProperty ViewEndMsProperty = DependencyProperty.Register(
|
|
|
|
|
|
nameof(ViewEndMs), typeof(double?), typeof(CurvePlotControl),
|
|
|
|
|
|
new PropertyMetadata(null, (d, e) => ((CurvePlotControl)d).Render()));
|
|
|
|
|
|
|
|
|
|
|
|
public CurveData? Curve { get => (CurveData?)GetValue(CurveProperty); set => SetValue(CurveProperty, value); }
|
|
|
|
|
|
public bool IsRising { get => (bool)GetValue(IsRisingProperty); set => SetValue(IsRisingProperty, value); }
|
|
|
|
|
|
public double VMax { get => (double)GetValue(VMaxProperty); set => SetValue(VMaxProperty, value); }
|
|
|
|
|
|
public double? ViewStartMs { get => (double?)GetValue(ViewStartMsProperty); set => SetValue(ViewStartMsProperty, value); }
|
|
|
|
|
|
public double? ViewEndMs { get => (double?)GetValue(ViewEndMsProperty); set => SetValue(ViewEndMsProperty, value); }
|
|
|
|
|
|
|
|
|
|
|
|
private void InitPlot()
|
|
|
|
|
|
{
|
2026-07-03 14:16:15 +08:00
|
|
|
|
_plot ??= CreateRuntimePlot();
|
|
|
|
|
|
_plot.Plot.Title("");
|
|
|
|
|
|
_plot.Plot.XLabel("t (ms)");
|
|
|
|
|
|
_plot.Plot.YLabel("V");
|
|
|
|
|
|
_plot.Plot.Axes.AntiAlias(true);
|
|
|
|
|
|
_plot.Plot.FigureBackground.Color = ScottPlot.Color.FromHex("#FBFCFD");
|
|
|
|
|
|
_plot.Plot.DataBackground.Color = ScottPlot.Color.FromHex("#FBFCFD");
|
|
|
|
|
|
_plot.Plot.Grid.MajorLineColor = ScottPlot.Color.FromHex("#DFE4EA");
|
2026-06-30 12:10:29 +08:00
|
|
|
|
Render();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-03 14:16:15 +08:00
|
|
|
|
private WpfPlot CreateRuntimePlot()
|
|
|
|
|
|
{
|
|
|
|
|
|
var plot = new WpfPlot();
|
|
|
|
|
|
RootContainer.Children.Clear();
|
|
|
|
|
|
RootContainer.Children.Add(plot);
|
|
|
|
|
|
return plot;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-30 12:10:29 +08:00
|
|
|
|
private void Render()
|
|
|
|
|
|
{
|
2026-07-03 14:16:15 +08:00
|
|
|
|
if (_isDesignMode || !IsLoaded || _plot == null) return;
|
|
|
|
|
|
_plot.Plot.Clear();
|
2026-06-30 12:10:29 +08:00
|
|
|
|
var c = Curve;
|
|
|
|
|
|
if (c == null || c.Voltage.Length == 0)
|
|
|
|
|
|
{
|
2026-07-03 14:16:15 +08:00
|
|
|
|
_plot.Refresh();
|
2026-06-30 12:10:29 +08:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
var points = BuildViewPoints(c);
|
|
|
|
|
|
if (points.Count == 0)
|
|
|
|
|
|
{
|
2026-07-03 14:16:15 +08:00
|
|
|
|
_plot.Refresh();
|
2026-06-30 12:10:29 +08:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var xs = new double[points.Count];
|
|
|
|
|
|
var ys = new double[points.Count];
|
|
|
|
|
|
// 以触发时刻为 0;x 单位 ms
|
|
|
|
|
|
for (int i = 0; i < points.Count; i++)
|
|
|
|
|
|
{
|
|
|
|
|
|
xs[i] = points[i].TimeMs;
|
|
|
|
|
|
ys[i] = points[i].Voltage;
|
|
|
|
|
|
}
|
|
|
|
|
|
var color = IsRising ? "#1A73C4" : "#D23B3B";
|
2026-07-03 14:16:15 +08:00
|
|
|
|
var sig = _plot.Plot.Add.ScatterLine(xs, ys, ScottPlot.Color.FromHex(color));
|
2026-06-30 12:10:29 +08:00
|
|
|
|
sig.LineWidth = 2;
|
|
|
|
|
|
sig.MarkerSize = 0;
|
|
|
|
|
|
|
|
|
|
|
|
// 阈值线
|
2026-07-05 13:33:55 +08:00
|
|
|
|
AddThresholdLine(VMax * 0.9, "90%", xs[0]);
|
|
|
|
|
|
AddThresholdLine(VMax * 0.1, "10%", xs[0]);
|
|
|
|
|
|
AddTransitionMark(c, xs, ys);
|
2026-06-30 12:10:29 +08:00
|
|
|
|
|
|
|
|
|
|
// 触发线 t=0
|
|
|
|
|
|
if (!HasViewWindow(out double start, out double end) || (start <= 0 && end >= 0))
|
|
|
|
|
|
{
|
2026-07-03 14:16:15 +08:00
|
|
|
|
var vLine = _plot.Plot.Add.VerticalLine(0);
|
2026-06-30 12:10:29 +08:00
|
|
|
|
vLine.Color = ScottPlot.Color.FromHex("#98A1AD");
|
|
|
|
|
|
vLine.LinePattern = ScottPlot.LinePattern.Dotted;
|
|
|
|
|
|
vLine.LineWidth = 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-03 14:16:15 +08:00
|
|
|
|
_plot.Plot.Axes.AutoScale();
|
|
|
|
|
|
_plot.Refresh();
|
2026-06-30 12:10:29 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-05 13:33:55 +08:00
|
|
|
|
private void AddThresholdLine(double volts, string text, double labelX)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (!double.IsFinite(volts)) return;
|
|
|
|
|
|
|
|
|
|
|
|
var hLine = _plot!.Plot.Add.HorizontalLine(volts);
|
|
|
|
|
|
hLine.Color = ScottPlot.Color.FromHex("#98A1AD");
|
|
|
|
|
|
hLine.LinePattern = ScottPlot.LinePattern.Dashed;
|
|
|
|
|
|
hLine.LineWidth = 1;
|
|
|
|
|
|
|
|
|
|
|
|
var label = _plot.Plot.Add.Text(text, labelX, volts);
|
|
|
|
|
|
label.LabelFontColor = ScottPlot.Color.FromHex("#98A1AD");
|
|
|
|
|
|
label.LabelFontSize = 10;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void AddTransitionMark(CurveData curve, double[] xs, double[] ys)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (!double.IsFinite(VMax) || VMax <= 0) return;
|
|
|
|
|
|
|
|
|
|
|
|
var mark = IsRising
|
|
|
|
|
|
? CurveMetrics.AnalyzeRising(curve, VMax)
|
|
|
|
|
|
: CurveMetrics.AnalyzeFalling(curve, VMax);
|
|
|
|
|
|
if (!mark.IsValid) return;
|
|
|
|
|
|
|
|
|
|
|
|
bool hasWindow = HasViewWindow(out double viewStart, out double viewEnd);
|
|
|
|
|
|
string startLabel = IsRising ? "TON" : "TOFF";
|
|
|
|
|
|
string endLabel = IsRising ? "T90" : "T10";
|
|
|
|
|
|
|
|
|
|
|
|
if (IsVisibleInView(mark.StartTimeMs, hasWindow, viewStart, viewEnd))
|
|
|
|
|
|
AddMarkerLine(mark.StartTimeMs, mark.StartThresholdV, startLabel, "#1C8A2E");
|
|
|
|
|
|
|
|
|
|
|
|
if (IsVisibleInView(mark.EndTimeMs, hasWindow, viewStart, viewEnd))
|
|
|
|
|
|
AddMarkerLine(mark.EndTimeMs, mark.EndThresholdV, endLabel, "#D93025");
|
|
|
|
|
|
|
|
|
|
|
|
double middleTime = (mark.StartTimeMs + mark.EndTimeMs) / 2.0;
|
|
|
|
|
|
if (IsVisibleInView(middleTime, hasWindow, viewStart, viewEnd))
|
|
|
|
|
|
{
|
|
|
|
|
|
double minY = ys.Min();
|
|
|
|
|
|
double maxY = ys.Max();
|
|
|
|
|
|
double labelY = minY + (maxY - minY) * 0.86;
|
|
|
|
|
|
var duration = _plot!.Plot.Add.Text($"{startLabel}-{endLabel} {mark.DurationMs:F3} ms", middleTime, labelY);
|
|
|
|
|
|
duration.LabelFontColor = ScottPlot.Color.FromHex("#2F3A45");
|
|
|
|
|
|
duration.LabelFontSize = 11;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void AddMarkerLine(double timeMs, double thresholdV, string text, string colorHex)
|
|
|
|
|
|
{
|
|
|
|
|
|
var color = ScottPlot.Color.FromHex(colorHex);
|
|
|
|
|
|
var line = _plot!.Plot.Add.VerticalLine(timeMs);
|
|
|
|
|
|
line.Color = color;
|
|
|
|
|
|
line.LineWidth = 2;
|
|
|
|
|
|
|
|
|
|
|
|
var label = _plot.Plot.Add.Text(text, timeMs, thresholdV);
|
|
|
|
|
|
label.LabelFontColor = color;
|
|
|
|
|
|
label.LabelFontSize = 11;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private static bool IsVisibleInView(double timeMs, bool hasWindow, double viewStart, double viewEnd) =>
|
|
|
|
|
|
double.IsFinite(timeMs) && (!hasWindow || (timeMs >= viewStart && timeMs <= viewEnd));
|
|
|
|
|
|
|
2026-06-30 12:10:29 +08:00
|
|
|
|
private List<(double TimeMs, double Voltage)> BuildViewPoints(CurveData curve)
|
|
|
|
|
|
{
|
|
|
|
|
|
var points = new List<(double TimeMs, double Voltage)>(curve.Voltage.Length);
|
|
|
|
|
|
bool hasWindow = HasViewWindow(out double start, out double end);
|
|
|
|
|
|
|
|
|
|
|
|
for (int i = 0; i < curve.Voltage.Length; i++)
|
|
|
|
|
|
{
|
|
|
|
|
|
double timeMs = (curve.TimeAt(i) - curve.TriggerTimeSec) * 1000.0;
|
|
|
|
|
|
if (!hasWindow || (timeMs >= start && timeMs <= end))
|
|
|
|
|
|
points.Add((timeMs, curve.Voltage[i]));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return points;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private bool HasViewWindow(out double start, out double end)
|
|
|
|
|
|
{
|
|
|
|
|
|
start = ViewStartMs ?? 0;
|
|
|
|
|
|
end = ViewEndMs ?? 0;
|
|
|
|
|
|
return ViewStartMs.HasValue && ViewEndMs.HasValue
|
|
|
|
|
|
&& double.IsFinite(start) && double.IsFinite(end) && end > start;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>导出为 PNG 字节。</summary>
|
|
|
|
|
|
public byte[] ExportPng(int width = 800, int height = 300)
|
|
|
|
|
|
{
|
2026-07-03 14:16:15 +08:00
|
|
|
|
if (_plot == null)
|
|
|
|
|
|
return Array.Empty<byte>();
|
|
|
|
|
|
|
|
|
|
|
|
return _plot.Plot.GetImageBytes(width, height, ImageFormat.Png);
|
2026-06-30 12:10:29 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|