diff --git a/Docs/曲线算法说明文档.md b/Docs/曲线算法说明文档.md
new file mode 100644
index 0000000..9da3df4
--- /dev/null
+++ b/Docs/曲线算法说明文档.md
@@ -0,0 +1,108 @@
+# 算法说明文档
+
+## 1. ThresholdV1 阈值算法
+
+上升曲线和下降曲线都使用同一类阈值判断算法:根据名义高电平 `Vhigh` 计算 90% 阈值和 10% 阈值,再在曲线中查找对应的时间点。
+
+核心阈值:
+
+```text
+90% 阈值 = Vhigh * R90
+10% 阈值 = Vhigh * Rstart
+```
+
+在参数模板中:
+
+```text
+90% 阈值 = 3.0 * 0.9 = 2.7 V
+10% 阈值 = 3.0 * 0.1 = 0.3 V
+```
+
+曲线图中显示的两条水平虚线分别对应 90% 阈值线和 10% 阈值线。
+
+## 2. 曲线内标记时间线
+
+### 开启曲线:开启 - 上升
+
+开启曲线用于分析信号从低电平上升到高电平的过程。
+
+```text
+低电平 -> 开启起点 T0 -> 上升 -> T90 -> 高电平
+```
+
+判断逻辑:
+
+1. 查找信号第一次达到 10% 阈值的位置,作为开启起点 `T0`。
+2. 从 `T0` 之后继续查找信号第一次达到 90% 阈值的位置,作为 `T90`。
+3. 计算开启上升时间:
+
+ ```text
+ RiseTime = T90 - T0
+ ```
+
+| 标记 | 含义 |
+| --- | --- |
+| `T0` | 开启起点,信号达到 10% 阈值 |
+| `T90` | 信号达到 90% 阈值 |
+| `RiseTime` | 开启上升时间 |
+
+### 关断曲线:关断 - 下降
+
+关断曲线用于分析信号从高电平下降到低电平的过程。
+
+```text
+高电平 -> 关断起点 Toff -> 下降 -> T10 -> 低电平
+```
+
+判断逻辑:
+
+1. 确认曲线中存在高电平区域。
+2. 查找信号下降到 90% 阈值的位置,作为关断起点 `Toff`。
+3. 从 `Toff` 之后继续查找信号下降到 10% 阈值的位置,作为 `T10`。
+4. 计算关断下降时间:
+
+ ```text
+ FallTime = T10 - Toff
+ ```
+
+| 标记 | 含义 |
+| --- | --- |
+| `Toff` | 关断起点,信号下降到 90% 阈值 |
+| `T10` | 信号下降到 10% 阈值 |
+| `FallTime` | 关断下降时间 |
+
+## 3. 参数模板
+
+参数固定在代码变量中,模板如下:
+
+```text
+Vhigh: 3.0
+R90: 0.9
+Rstart: 0.1
+StableN: 1
+Win: 5
+Sigma: 300
+MedianFilterCheckBox: 5
+```
+
+### 参数含义
+
+| 参数 | 模板值 | 含义 |
+| --- | --- | --- |
+| `Vhigh` | `3.0` | 名义高电平电压 |
+| `R90` | `0.9` | 90% 阈值比例 |
+| `Rstart` | `0.1` | 10% 阈值比例 |
+| `StableN` | `1` | 连续满足阈值的采样点数量 |
+| `Win` | `5` | 搜索/判断窗口点数 |
+| `Sigma` | `300` | 算法阈值或窗口相关参数,按代码变量固定 |
+| `MedianFilterCheckBox` | `5` | 中值滤波窗口点数 |
+
+## 4. 输出字段
+
+| 字段 | 开启曲线含义 | 关断曲线含义 |
+| --- | --- | --- |
+| `StartIndex` | `T0` 对应采样点索引 | `Toff` 对应采样点索引 |
+| `EndIndex` | `T90` 对应采样点索引 | `T10` 对应采样点索引 |
+| `T0` | 开启起点时间 | 关断起点时间 |
+| `T90` | 上升到 90% 的时间 | 下降到 10% 的时间 |
+| `RiseTime` | `T90 - T0` | `T10 - Toff` |
diff --git a/SSPCTester.Logic/Calculation/CurveMetrics.cs b/SSPCTester.Logic/Calculation/CurveMetrics.cs
index 1b12112..e6983c4 100644
--- a/SSPCTester.Logic/Calculation/CurveMetrics.cs
+++ b/SSPCTester.Logic/Calculation/CurveMetrics.cs
@@ -47,11 +47,8 @@ public static class CurveMetrics
///
public static double RiseTimeMs(CurveData curve, double vMax, double lowRatio = 0.1, double highRatio = 0.9)
{
- int iLow = FirstReachIndex(curve.Voltage, vMax * lowRatio, rising: true);
- if (iLow < 0) return double.NaN;
- int iHigh = FirstReachIndex(curve.Voltage, vMax * highRatio, rising: true, startIndex: iLow);
- if (iHigh < 0) return double.NaN;
- return (curve.TimeAt(iHigh) - curve.TimeAt(iLow)) * 1000.0;
+ var mark = AnalyzeRising(curve, vMax, lowRatio, highRatio);
+ return mark.IsValid ? mark.DurationMs : double.NaN;
}
///
@@ -59,11 +56,84 @@ public static class CurveMetrics
///
public static double FallTimeMs(CurveData curve, double vMax, double highRatio = 0.9, double lowRatio = 0.1)
{
- int iHigh = FirstReachIndex(curve.Voltage, vMax * highRatio, rising: false);
- if (iHigh < 0) return double.NaN;
- int iLow = FirstReachIndex(curve.Voltage, vMax * lowRatio, rising: false, startIndex: iHigh);
- if (iLow < 0) return double.NaN;
- return (curve.TimeAt(iLow) - curve.TimeAt(iHigh)) * 1000.0;
+ var mark = AnalyzeFalling(curve, vMax, highRatio, lowRatio);
+ return mark.IsValid ? mark.DurationMs : double.NaN;
+ }
+
+ /// Rising curve 10% - 90% transition mark.
+ public static CurveTransitionMark AnalyzeRising(CurveData curve, double vMax, double lowRatio = 0.1, double highRatio = 0.9)
+ {
+ double startThreshold = 0;
+ double endThreshold = vMax * highRatio;
+ int triggerIndex = TriggerIndex(curve);
+ int endIndex = FirstReachIndex(curve.Voltage, endThreshold, rising: true, startIndex: triggerIndex);
+ if (endIndex < 0) return CurveTransitionMark.Invalid(startThreshold, endThreshold);
+
+ int startIndex = LastReachIndex(curve.Voltage, startThreshold, rising: false, startIndex: triggerIndex, endIndex: endIndex);
+ if (startIndex < 0) startIndex = FirstReachIndex(curve.Voltage, startThreshold, rising: true, startIndex: triggerIndex);
+ if (startIndex < 0 || startIndex > endIndex) return CurveTransitionMark.Invalid(startThreshold, endThreshold);
+
+ return BuildTransitionMark(curve, startIndex, endIndex, startThreshold, endThreshold);
+ }
+
+ /// Falling curve 90% - 10% transition mark.
+ public static CurveTransitionMark AnalyzeFalling(CurveData curve, double vMax, double highRatio = 0.9, double lowRatio = 0.1)
+ {
+ double startThreshold = vMax;
+ double endThreshold = vMax * lowRatio;
+ int triggerIndex = TriggerIndex(curve);
+ int endIndex = FirstReachIndex(curve.Voltage, endThreshold, rising: false, startIndex: triggerIndex);
+ if (endIndex < 0) return CurveTransitionMark.Invalid(startThreshold, endThreshold);
+
+ int startIndex = LastReachIndex(curve.Voltage, vMax * 0.98, rising: true, startIndex: triggerIndex, endIndex: endIndex);
+ if (startIndex < 0) startIndex = triggerIndex;
+ if (startIndex > endIndex) return CurveTransitionMark.Invalid(startThreshold, endThreshold);
+
+ return BuildTransitionMark(curve, startIndex, endIndex, startThreshold, endThreshold);
+ }
+
+ private static int TriggerIndex(CurveData curve)
+ {
+ if (curve.TriggerSampleIndex > 0)
+ return Math.Min(curve.TriggerSampleIndex, Math.Max(curve.Voltage.Length - 1, 0));
+
+ if (curve.SampleRateHz <= 0)
+ return 0;
+
+ int index = (int)Math.Round(curve.TriggerTimeSec * curve.SampleRateHz);
+ return Math.Clamp(index, 0, Math.Max(curve.Voltage.Length - 1, 0));
+ }
+
+ private static int LastReachIndex(double[] v, double vTarget, bool rising, int startIndex, int endIndex)
+ {
+ if (v == null || v.Length == 0) return -1;
+ startIndex = Math.Clamp(startIndex, 0, v.Length - 1);
+ endIndex = Math.Clamp(endIndex, 0, v.Length - 1);
+ for (int i = endIndex; i >= startIndex; i--)
+ {
+ if (rising ? v[i] >= vTarget : v[i] <= vTarget) return i;
+ }
+ return -1;
+ }
+
+ private static CurveTransitionMark BuildTransitionMark(
+ CurveData curve,
+ int startIndex,
+ int endIndex,
+ double startThreshold,
+ double endThreshold)
+ {
+ double startTimeMs = (curve.TimeAt(startIndex) - curve.TriggerTimeSec) * 1000.0;
+ double endTimeMs = (curve.TimeAt(endIndex) - curve.TriggerTimeSec) * 1000.0;
+ return new CurveTransitionMark(
+ startIndex,
+ endIndex,
+ startTimeMs,
+ endTimeMs,
+ endTimeMs - startTimeMs,
+ startThreshold,
+ endThreshold,
+ true);
}
/// 从波形最大值估算 Vmax(去掉前后噪声,取 95 百分位以上的均值)。
@@ -78,3 +148,25 @@ public static class CurveMetrics
return n > 0 ? sum / n : sorted[^1];
}
}
+
+public readonly record struct CurveTransitionMark(
+ int StartIndex,
+ int EndIndex,
+ double StartTimeMs,
+ double EndTimeMs,
+ double DurationMs,
+ double StartThresholdV,
+ double EndThresholdV,
+ bool IsValid)
+{
+ public static CurveTransitionMark Invalid(double startThresholdV, double endThresholdV) =>
+ new(
+ -1,
+ -1,
+ double.NaN,
+ double.NaN,
+ double.NaN,
+ startThresholdV,
+ endThresholdV,
+ false);
+}
diff --git a/SSPCTester.Logic/Report/CurveImageRenderer.cs b/SSPCTester.Logic/Report/CurveImageRenderer.cs
index c2a9619..98b5590 100644
--- a/SSPCTester.Logic/Report/CurveImageRenderer.cs
+++ b/SSPCTester.Logic/Report/CurveImageRenderer.cs
@@ -18,6 +18,7 @@ public static class CurveImageRenderer
curve,
"ON",
Color.FromHex("#1A73C4"),
+ true,
viewStartMs,
viewEndMs,
width,
@@ -35,6 +36,7 @@ public static class CurveImageRenderer
curve,
"OFF",
Color.FromHex("#D23B3B"),
+ false,
viewStartMs,
viewEndMs,
width,
@@ -68,6 +70,8 @@ public static class CurveImageRenderer
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))
{
@@ -87,6 +91,7 @@ public static class CurveImageRenderer
CurveData curve,
string phaseLabel,
Color color,
+ bool isRising,
double? viewStartMs,
double? viewEndMs,
int width,
@@ -110,6 +115,7 @@ public static class CurveImageRenderer
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))
{
@@ -176,4 +182,61 @@ public static class CurveImageRenderer
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));
}
diff --git a/SSPCTester.Tests/CalculationTests.cs b/SSPCTester.Tests/CalculationTests.cs
index 272cf33..89c4dab 100644
--- a/SSPCTester.Tests/CalculationTests.cs
+++ b/SSPCTester.Tests/CalculationTests.cs
@@ -41,12 +41,29 @@ public class CurveMetricsTests
}
[Fact]
- public void RiseTimeMs_TenToNinety_AboutTwoPointTwo()
+ public void RiseTimeMs_TonToNinety_AboutNinetyPercentTime()
{
var c = MakeRising();
double rt = CurveMetrics.RiseTimeMs(c, 28);
// ln(0.9/0.1) = ln 9 ≈ 2.197;rt = tau * 2.197 ≈ 1.099 ms
- Assert.InRange(rt, 1.05, 1.15);
+ Assert.InRange(rt, 1.1, 1.2);
+ }
+
+ [Fact]
+ public void AnalyzeRising_ReturnsTonToNinetyIndexesAndDuration()
+ {
+ var c = MakeRising();
+ var mark = CurveMetrics.AnalyzeRising(c, 28);
+ int triggerIndex = (int)Math.Round(c.TriggerTimeSec * c.SampleRateHz);
+ int expectedEnd = CurveMetrics.FirstReachIndex(c.Voltage, 28 * 0.9, rising: true, triggerIndex);
+ int expectedStart = triggerIndex;
+
+ Assert.True(mark.IsValid);
+ Assert.Equal(expectedStart, mark.StartIndex);
+ Assert.Equal(expectedEnd, mark.EndIndex);
+ Assert.Equal(0, mark.StartThresholdV, 6);
+ Assert.Equal((c.TimeAt(expectedEnd) - c.TimeAt(expectedStart)) * 1000.0, mark.DurationMs, 6);
+ Assert.Equal(mark.DurationMs, CurveMetrics.RiseTimeMs(c, 28), 6);
}
[Fact]
@@ -57,6 +74,55 @@ public class CurveMetricsTests
Assert.InRange(off, 1.1, 1.2);
}
+ [Fact]
+ public void AnalyzeFalling_ReturnsStableHighToTenIndexesAndDuration()
+ {
+ var c = MakeFalling();
+ var mark = CurveMetrics.AnalyzeFalling(c, 28);
+ int triggerIndex = (int)Math.Round(c.TriggerTimeSec * c.SampleRateHz);
+ int expectedEnd = CurveMetrics.FirstReachIndex(c.Voltage, 28 * 0.1, rising: false, triggerIndex);
+ int expectedStart = triggerIndex;
+ for (int i = triggerIndex; i <= expectedEnd; i++)
+ {
+ if (c.Voltage[i] >= 28 * 0.98)
+ expectedStart = i;
+ }
+
+ Assert.True(mark.IsValid);
+ Assert.Equal(expectedStart, mark.StartIndex);
+ Assert.Equal(expectedEnd, mark.EndIndex);
+ Assert.Equal(28, mark.StartThresholdV, 6);
+ Assert.Equal((c.TimeAt(expectedEnd) - c.TimeAt(expectedStart)) * 1000.0, mark.DurationMs, 6);
+ Assert.Equal(mark.DurationMs, CurveMetrics.FallTimeMs(c, 28), 6);
+ }
+
+ [Fact]
+ public void AnalyzeTransition_ReturnsInvalidWhenThresholdsAreNotReached()
+ {
+ var risingCurve = new CurveData
+ {
+ SampleRateHz = 100_000,
+ TriggerTimeSec = 0,
+ Voltage = Enumerable.Repeat(0.0, 100).ToArray()
+ };
+ var fallingCurve = new CurveData
+ {
+ SampleRateHz = 100_000,
+ TriggerTimeSec = 0,
+ Voltage = Enumerable.Repeat(28.0, 100).ToArray()
+ };
+
+ var rising = CurveMetrics.AnalyzeRising(risingCurve, 28);
+ var falling = CurveMetrics.AnalyzeFalling(fallingCurve, 28);
+
+ Assert.False(rising.IsValid);
+ Assert.False(falling.IsValid);
+ Assert.Equal(-1, rising.StartIndex);
+ Assert.Equal(-1, falling.EndIndex);
+ Assert.True(double.IsNaN(CurveMetrics.RiseTimeMs(risingCurve, 28)));
+ Assert.True(double.IsNaN(CurveMetrics.FallTimeMs(fallingCurve, 28)));
+ }
+
[Fact]
public void EstimateVMax_NearActualMax()
{
diff --git a/SSPCTester.UI/Controls/CurvePlotControl.xaml.cs b/SSPCTester.UI/Controls/CurvePlotControl.xaml.cs
index bac2b0f..2667de1 100644
--- a/SSPCTester.UI/Controls/CurvePlotControl.xaml.cs
+++ b/SSPCTester.UI/Controls/CurvePlotControl.xaml.cs
@@ -5,6 +5,7 @@ using System.Windows.Media;
using ScottPlot;
using ScottPlot.WPF;
using SSPCTester.Devices.Interfaces;
+using SSPCTester.Logic.Calculation;
namespace SSPCTester.UI.Controls;
@@ -103,14 +104,9 @@ public partial class CurvePlotControl : UserControl
sig.MarkerSize = 0;
// 阈值线
- double threshold = IsRising ? VMax * 0.9 : VMax * 0.1;
- var hLine = _plot.Plot.Add.HorizontalLine(threshold);
- hLine.Color = ScottPlot.Color.FromHex("#98A1AD");
- hLine.LinePattern = ScottPlot.LinePattern.Dashed;
- hLine.LineWidth = 1;
- var label = _plot.Plot.Add.Text($"{(IsRising ? 90 : 10)}%", xs[0], threshold);
- label.LabelFontColor = ScottPlot.Color.FromHex("#98A1AD");
- label.LabelFontSize = 10;
+ 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))
@@ -125,6 +121,66 @@ public partial class CurvePlotControl : UserControl
_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);
diff --git a/SSPCTester.UI/ViewModels/CurveTestViewModel.cs b/SSPCTester.UI/ViewModels/CurveTestViewModel.cs
index b7b7d61..0ffa436 100644
--- a/SSPCTester.UI/ViewModels/CurveTestViewModel.cs
+++ b/SSPCTester.UI/ViewModels/CurveTestViewModel.cs
@@ -146,6 +146,22 @@ public partial class CurveTestViewModel : ObservableObject
SaveProjectQuietly();
}
+ [RelayCommand]
+ private void ZoomRisingCurveToMarks()
+ {
+ if (SelectedRow == null || RisingCurve == null) return;
+
+ double vMax = CurveMetrics.EstimateVMax(RisingCurve.Voltage);
+ if (!TryBuildMarkWindow(CurveMetrics.AnalyzeRising(RisingCurve, vMax), RisingCurve, out double start, out double end))
+ return;
+
+ RisingViewStartMs = start;
+ RisingViewEndMs = end;
+ SelectedRow.RisingViewStartMs = start;
+ SelectedRow.RisingViewEndMs = end;
+ SaveProjectQuietly();
+ }
+
[RelayCommand]
private void ResetFallingCurveView()
{
@@ -157,6 +173,41 @@ public partial class CurveTestViewModel : ObservableObject
SaveProjectQuietly();
}
+ [RelayCommand]
+ private void ZoomFallingCurveToMarks()
+ {
+ if (SelectedRow == null || FallingCurve == null) return;
+
+ double vMax = RisingCurve != null
+ ? CurveMetrics.EstimateVMax(RisingCurve.Voltage)
+ : CurveMetrics.EstimateVMax(FallingCurve.Voltage);
+ if (!TryBuildMarkWindow(CurveMetrics.AnalyzeFalling(FallingCurve, vMax), FallingCurve, out double start, out double end))
+ return;
+
+ FallingViewStartMs = start;
+ FallingViewEndMs = end;
+ SelectedRow.FallingViewStartMs = start;
+ SelectedRow.FallingViewEndMs = end;
+ SaveProjectQuietly();
+ }
+
+ private static bool TryBuildMarkWindow(CurveTransitionMark mark, CurveData curve, out double start, out double end)
+ {
+ start = 0;
+ end = 0;
+ if (!mark.IsValid || !double.IsFinite(mark.StartTimeMs) || !double.IsFinite(mark.EndTimeMs))
+ return false;
+
+ double span = Math.Abs(mark.EndTimeMs - mark.StartTimeMs);
+ double padding = Math.Max(span * 0.15, 0.05);
+ double minTimeMs = -curve.TriggerTimeSec * 1000.0;
+ double maxTimeMs = (curve.DurationSec - curve.TriggerTimeSec) * 1000.0;
+
+ start = Math.Max(minTimeMs, Math.Min(mark.StartTimeMs, mark.EndTimeMs) - padding);
+ end = Math.Min(maxTimeMs, Math.Max(mark.StartTimeMs, mark.EndTimeMs) + padding);
+ return double.IsFinite(start) && double.IsFinite(end) && end > start;
+ }
+
private void SaveProjectQuietly()
{
var project = Project;
diff --git a/SSPCTester.UI/Views/CurveTestView.xaml b/SSPCTester.UI/Views/CurveTestView.xaml
index 21590e6..b076f7a 100644
--- a/SSPCTester.UI/Views/CurveTestView.xaml
+++ b/SSPCTester.UI/Views/CurveTestView.xaml
@@ -89,6 +89,8 @@
FontFamily="{StaticResource FontMono}" />
+
@@ -118,6 +120,8 @@
FontFamily="{StaticResource FontMono}" />
+