SSPC-Tester/SSPCTester.UI/Controls/CurvePlotControl.xaml.cs
2026-07-05 13:33:55 +08:00

216 lines
8.0 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System.ComponentModel;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using ScottPlot;
using ScottPlot.WPF;
using SSPCTester.Devices.Interfaces;
using SSPCTester.Logic.Calculation;
namespace SSPCTester.UI.Controls;
public partial class CurvePlotControl : UserControl
{
private readonly bool _isDesignMode;
private WpfPlot? _plot;
public CurvePlotControl()
{
InitializeComponent();
_isDesignMode = DesignerProperties.GetIsInDesignMode(this);
if (_isDesignMode)
{
DesignPlaceholder.Visibility = Visibility.Visible;
return;
}
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()
{
_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");
Render();
}
private WpfPlot CreateRuntimePlot()
{
var plot = new WpfPlot();
RootContainer.Children.Clear();
RootContainer.Children.Add(plot);
return plot;
}
private void Render()
{
if (_isDesignMode || !IsLoaded || _plot == null) return;
_plot.Plot.Clear();
var c = Curve;
if (c == null || c.Voltage.Length == 0)
{
_plot.Refresh();
return;
}
var points = BuildViewPoints(c);
if (points.Count == 0)
{
_plot.Refresh();
return;
}
var xs = new double[points.Count];
var ys = new double[points.Count];
// 以触发时刻为 0x 单位 ms
for (int i = 0; i < points.Count; i++)
{
xs[i] = points[i].TimeMs;
ys[i] = points[i].Voltage;
}
var color = IsRising ? "#1A73C4" : "#D23B3B";
var sig = _plot.Plot.Add.ScatterLine(xs, ys, ScottPlot.Color.FromHex(color));
sig.LineWidth = 2;
sig.MarkerSize = 0;
// 阈值线
AddThresholdLine(VMax * 0.9, "90%", xs[0]);
AddThresholdLine(VMax * 0.1, "10%", xs[0]);
AddTransitionMark(c, xs, ys);
// 触发线 t=0
if (!HasViewWindow(out double start, out double end) || (start <= 0 && end >= 0))
{
var vLine = _plot.Plot.Add.VerticalLine(0);
vLine.Color = ScottPlot.Color.FromHex("#98A1AD");
vLine.LinePattern = ScottPlot.LinePattern.Dotted;
vLine.LineWidth = 1;
}
_plot.Plot.Axes.AutoScale();
_plot.Refresh();
}
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));
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)
{
if (_plot == null)
return Array.Empty<byte>();
return _plot.Plot.GetImageBytes(width, height, ImageFormat.Png);
}
}