SSPC-Tester/SSPCTester.Logic/Report/CurveImageRenderer.cs
2026-07-05 13:33:55 +08:00

243 lines
8.3 KiB
C#

using ScottPlot;
using SSPCTester.Devices.Interfaces;
using SSPCTester.Logic.Calculation;
namespace SSPCTester.Logic.Report;
public static class CurveImageRenderer
{
public static byte[] RenderRisingChannel(
int channel,
CurveData curve,
double? viewStartMs = null,
double? viewEndMs = null,
int width = 900,
int height = 360) =>
RenderSingleChannel(
channel,
curve,
"ON",
Color.FromHex("#1A73C4"),
true,
viewStartMs,
viewEndMs,
width,
height);
public static byte[] RenderFallingChannel(
int channel,
CurveData curve,
double? viewStartMs = null,
double? viewEndMs = null,
int width = 900,
int height = 360) =>
RenderSingleChannel(
channel,
curve,
"OFF",
Color.FromHex("#D23B3B"),
false,
viewStartMs,
viewEndMs,
width,
height);
public static byte[] RenderChannel(
int channel,
CurveData rising,
CurveData falling,
double? viewStartMs = null,
double? viewEndMs = null,
int width = 900,
int height = 360)
{
var plot = new Plot();
double start = viewStartMs ?? 0;
double end = viewEndMs ?? 0;
bool hasWindow = IsValidWindow(viewStartMs, viewEndMs);
plot.Title(hasWindow
? $"CH{channel:00} ON / OFF Curve ({start:F3}-{end:F3} ms)"
: $"CH{channel:00} ON / OFF Curve");
plot.XLabel("t (ms)");
plot.YLabel("V");
plot.FigureBackground.Color = Color.FromHex("#FBFCFD");
plot.DataBackground.Color = Color.FromHex("#FBFCFD");
plot.Grid.MajorLineColor = Color.FromHex("#DFE4EA");
AddCurve(plot, rising, Color.FromHex("#1A73C4"), "ON", viewStartMs, viewEndMs);
AddCurve(plot, falling, Color.FromHex("#D23B3B"), "OFF", viewStartMs, viewEndMs);
double vMax = CurveMetrics.EstimateVMax(rising.Voltage);
AddThreshold(plot, vMax * 0.9, "90%");
AddThreshold(plot, vMax * 0.1, "10%");
AddTransitionMark(plot, rising, vMax, true, viewStartMs, viewEndMs);
AddTransitionMark(plot, falling, vMax, false, viewStartMs, viewEndMs);
if (!hasWindow || (start <= 0 && end >= 0))
{
var trigger = plot.Add.VerticalLine(0);
trigger.Color = Color.FromHex("#98A1AD");
trigger.LinePattern = LinePattern.Dotted;
trigger.LineWidth = 1;
}
plot.ShowLegend();
plot.Axes.AutoScale();
return plot.GetImageBytes(width, height, ImageFormat.Png);
}
private static byte[] RenderSingleChannel(
int channel,
CurveData curve,
string phaseLabel,
Color color,
bool isRising,
double? viewStartMs,
double? viewEndMs,
int width,
int height)
{
var plot = new Plot();
double start = viewStartMs ?? 0;
double end = viewEndMs ?? 0;
bool hasWindow = IsValidWindow(viewStartMs, viewEndMs);
plot.Title(hasWindow
? $"CH{channel:00} {phaseLabel} Curve ({start:F3}-{end:F3} ms)"
: $"CH{channel:00} {phaseLabel} Curve");
plot.XLabel("t (ms)");
plot.YLabel("V");
plot.FigureBackground.Color = Color.FromHex("#FBFCFD");
plot.DataBackground.Color = Color.FromHex("#FBFCFD");
plot.Grid.MajorLineColor = Color.FromHex("#DFE4EA");
AddCurve(plot, curve, color, phaseLabel, viewStartMs, viewEndMs);
double vMax = CurveMetrics.EstimateVMax(curve.Voltage);
AddThreshold(plot, vMax * 0.9, "90%");
AddThreshold(plot, vMax * 0.1, "10%");
AddTransitionMark(plot, curve, vMax, isRising, viewStartMs, viewEndMs);
if (!hasWindow || (start <= 0 && end >= 0))
{
var trigger = plot.Add.VerticalLine(0);
trigger.Color = Color.FromHex("#98A1AD");
trigger.LinePattern = LinePattern.Dotted;
trigger.LineWidth = 1;
}
plot.ShowLegend();
plot.Axes.AutoScale();
return plot.GetImageBytes(width, height, ImageFormat.Png);
}
private static void AddCurve(Plot plot, CurveData curve, Color color, string label, double? viewStartMs, double? viewEndMs)
{
if (curve.Voltage.Length == 0) return;
var points = BuildViewPoints(curve, viewStartMs, viewEndMs);
if (points.Count == 0) return;
var xs = new double[points.Count];
var ys = new double[points.Count];
for (int i = 0; i < points.Count; i++)
{
xs[i] = points[i].TimeMs;
ys[i] = points[i].Voltage;
}
var signal = plot.Add.ScatterLine(xs, ys, color);
signal.LineWidth = 2;
signal.MarkerSize = 0;
signal.LegendText = label;
}
private static List<(double TimeMs, double Voltage)> BuildViewPoints(CurveData curve, double? viewStartMs, double? viewEndMs)
{
var points = new List<(double TimeMs, double Voltage)>(curve.Voltage.Length);
double start = viewStartMs ?? 0;
double end = viewEndMs ?? 0;
bool hasWindow = IsValidWindow(viewStartMs, viewEndMs);
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 static bool IsValidWindow(double? viewStartMs, double? viewEndMs) =>
viewStartMs is double start && viewEndMs is double end
&& double.IsFinite(start) && double.IsFinite(end) && end > start;
private static void AddThreshold(Plot plot, double volts, string label)
{
if (!double.IsFinite(volts)) return;
var line = plot.Add.HorizontalLine(volts);
line.Color = Color.FromHex("#98A1AD");
line.LinePattern = LinePattern.Dashed;
line.LineWidth = 1;
line.LegendText = label;
}
private static void AddTransitionMark(
Plot plot,
CurveData curve,
double vMax,
bool isRising,
double? viewStartMs,
double? viewEndMs)
{
if (!double.IsFinite(vMax) || vMax <= 0) return;
var mark = isRising
? CurveMetrics.AnalyzeRising(curve, vMax)
: CurveMetrics.AnalyzeFalling(curve, vMax);
if (!mark.IsValid) return;
bool hasWindow = IsValidWindow(viewStartMs, viewEndMs);
double start = viewStartMs ?? 0;
double end = viewEndMs ?? 0;
string startLabel = isRising ? "TON" : "TOFF";
string endLabel = isRising ? "T90" : "T10";
if (IsVisibleInView(mark.StartTimeMs, hasWindow, start, end))
AddMarkerLine(plot, mark.StartTimeMs, mark.StartThresholdV, startLabel, Color.FromHex("#1C8A2E"));
if (IsVisibleInView(mark.EndTimeMs, hasWindow, start, end))
AddMarkerLine(plot, mark.EndTimeMs, mark.EndThresholdV, endLabel, Color.FromHex("#D93025"));
double middleTime = (mark.StartTimeMs + mark.EndTimeMs) / 2.0;
if (IsVisibleInView(middleTime, hasWindow, start, end))
{
var points = BuildViewPoints(curve, viewStartMs, viewEndMs);
if (points.Count == 0) return;
double minY = points.Min(x => x.Voltage);
double maxY = points.Max(x => x.Voltage);
double labelY = minY + (maxY - minY) * 0.86;
var duration = plot.Add.Text($"{startLabel}-{endLabel} {mark.DurationMs:F3} ms", middleTime, labelY);
duration.LabelFontColor = Color.FromHex("#2F3A45");
duration.LabelFontSize = 11;
}
}
private static void AddMarkerLine(Plot plot, double timeMs, double thresholdV, string text, Color color)
{
var line = plot.Add.VerticalLine(timeMs);
line.Color = color;
line.LineWidth = 2;
var label = 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));
}