From 5d56137ce5481d3fb9ddbc1c0e4f3bd534393c75 Mon Sep 17 00:00:00 2001 From: zhaotielin Date: Sun, 5 Jul 2026 13:33:55 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E6=9B=B2=E7=BA=BF=E6=A0=87?= =?UTF-8?q?=E8=AE=B0=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Docs/曲线算法说明文档.md | 108 +++++++++++++++++ SSPCTester.Logic/Calculation/CurveMetrics.cs | 112 ++++++++++++++++-- SSPCTester.Logic/Report/CurveImageRenderer.cs | 63 ++++++++++ SSPCTester.Tests/CalculationTests.cs | 70 ++++++++++- .../Controls/CurvePlotControl.xaml.cs | 72 +++++++++-- .../ViewModels/CurveTestViewModel.cs | 51 ++++++++ SSPCTester.UI/Views/CurveTestView.xaml | 4 + 7 files changed, 460 insertions(+), 20 deletions(-) create mode 100644 Docs/曲线算法说明文档.md 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}" />