From 71c6ff91cbbf80b7c0e9684d17a68959f9e957b9 Mon Sep 17 00:00:00 2001 From: "DeepSeek V4.1 Flash" <> Date: Thu, 29 Oct 2026 13:48:25 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=EF=BC=9A=E9=80=82=E5=BA=94?= =?UTF-8?q?=E8=A7=86=E5=9B=BE=E4=B8=8D=E8=B7=9F=E9=9A=8F=E6=97=B6=E9=97=B4?= =?UTF-8?q?=E8=BD=B4=E5=81=8F=E7=A7=BB=20+=20=E6=B4=BE=E7=94=9F=E4=BF=A1?= =?UTF-8?q?=E5=8F=B7=E6=9E=84=E9=80=A0=E6=88=90=E7=A9=BA=E4=BF=A1=E5=8F=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 为什么 用户 2026-10-29 报告两条功能异常: 1. 给某条信号设置「调整 → 时间轴」偏移后,再点「适应视图」,视界没有跟着平移, 曲线整体跑到视界之外; 2. 用「均值滤波 / 弱峰调整」的「构建为新信号」派生出来的信号是**空的**(有一个网格但画不出任何东西)。 ## 根因 ### ① 时间边界没有叠加 offsetTime 采样实际画在 `t + SignalStyle::offsetTime` 处,但"取景用的时间范围"只统计了时间轴的 源时刻下限/上限(`axis.minTime/maxTime`),三处都漏了偏移: - `TabConfig::ComputeVisibleTimeBounds`(「适应视图」/视图裁剪的时间范围来源) - `PlotView::CollectFitContributions`(`minTime`/`maxTime` 与单值点的 `pointTime`) - `PlotView::FitTimeToLane`(网格右键「按网格时间轴展开」) 于是偏移后取景仍停留在原始时间范围。 ### ② 派生信号把时间偏移"烘"进采样值 `Application::BuildDerivedSignal` 的 2c 步按 `time[i] - offsetTime` 重采样,把平移写进采样值。 派生信号与源信号**共用同一条文件时间轴**,所以平移出轴范围的那一段只能写成 NaN: 偏移 ≥ 数据跨度时整条信号变成全 NaN(= 空信号),偏移很小时也会在两端留下与实际曲线不符的空洞。 预览曲线本来就用**源样式的 `offsetTime`** 绘制(`PaintDerivedCurve` 里 `TimeToX(time + offsetTime)`), 因此把偏移留在派生信号的样式上,显示与预览一致,且采样数据一点都不会丢。 ## 改了什么 1. `Utilities/Src/DataModelTab.cpp`:`ComputeVisibleTimeBounds` 逐信号叠加 `style->offsetTime` (`low = min(low, axis.minTime + off)`、`high = max(...)`)。 2. `Components/PlotView/Src/PlotViewFeatures.cpp`: - `CollectFitContributions` 的 `minTime/maxTime` 叠加偏移;单值点的 `pointTime` 改为**显示时刻** (`time + offsetTime`),视界居中与边界让位都以它为准; - `FitTimeToLane` 同样叠加偏移。 3. `Components/Application/Src/ApplicationCommands.cpp`: - 删除 2c(重采样烘值),时间偏移改由**新信号样式**表达(3b 变成唯一路径); - 同步重写两处注释说明"为什么不再烘值"。 4. 文档:`DataModel.hpp::ComputeVisibleTimeBounds`、`PlotView.hpp::{TimeBounds,FitContribution}` 的注释更新为"显示时间(含 offsetTime)"。 5. `Documents/agent_mem.md`:追加第十四轮,并把两条旧规则显式标注为**已作废** (时间边界只取源时刻 / 派生信号烘值),写明现行做法与理由。 ## 副作用(有意为之) 派生信号若源自"带时间轴偏移"的信号,它的「调整」面板里「时间轴」会显示该偏移值。 旧实现为了让面板显示默认值而烘值,代价是数据被裁掉甚至整条变空 —— 这个取舍不可接受。 ## 怎么验证 新增离屏探针 `.preflight/derivecheck`(16 条断言,不占鼠标/键盘/屏幕): ``` INFO signals=10 axis=[0.000000, 1.260000] span=1.260000 mean: values=1009 finiteCount=1009 -> 有数据、范围不越界 weakPeak: values=1009 finiteCount=1009 -> 有数据 offset=0.126000 fitView=[0.126000, 1.386000] (期望 [0.126, 1.386])-> 适应视图跟随偏移 offset-derived: values=1009 finite=1009 (修复前 908) huge-offset-derived: values=1009 finite=1009 styleOffset=2.520000 (修复前全 NaN) 16 passed, 0 failed ``` 回归(全部通过): ``` make release / make debug 零 error / 零 warning uicheck 69/69 lbcheck 21/21 lbresid 5/5 lbzorder 4/4 lbcovr 7/7 lbpanel 10/10 derivecheck 16/16 rendercheck: 8 个场景哈希与上一轮逐一相同 + FUNC 7/7, exit=0 A FAED59DF284153DB B E6443BFE4E3955F8 C 96A0AA8C402F1729 D E3D5DBD998E43720 E DA0583CB13C26A6F F FFB822159F604D1D G 19F8DE8FE3A8DF18 H DBBDDE4CB2C97C92 ``` 产物仍是 **788,992 B**(单一 exe,未拆 DLL,未改优化等级)。 --- .../Application/Src/ApplicationCommands.cpp | 75 +++++-------------- Components/PlotView/Inc/PlotView.hpp | 9 ++- Components/PlotView/Src/PlotViewFeatures.cpp | 22 ++++-- Documents/agent_mem.md | 35 +++++++++ Utilities/Inc/DataModel.hpp | 7 +- Utilities/Src/DataModelTab.cpp | 11 ++- 6 files changed, 91 insertions(+), 68 deletions(-) diff --git a/Components/Application/Src/ApplicationCommands.cpp b/Components/Application/Src/ApplicationCommands.cpp index 97be47a..725666c 100644 --- a/Components/Application/Src/ApplicationCommands.cpp +++ b/Components/Application/Src/ApplicationCommands.cpp @@ -650,7 +650,6 @@ namespace jdi return false; } const SignalData& source = group->signals[static_cast(request.signalIndex)]; - const TimeAxis& axis = group->AxisOf(source); const std::wstring baseName = source.name; // 0. 先把源信号的「调整」变换快照下来(下面 `InsertDerivedSignal` 会移动信号存储, @@ -663,10 +662,13 @@ namespace jdi // * 数值域变换(系数 → 绝对值 → 垂直镜像 → Y 偏移)**与**水平镜像(数据反序) // 全部作用在派生采样值上; // * 于是新信号**用默认外观**绘制出来就与预览曲线逐位一致。 - // * ⚠️ **时间轴偏移也一并烘进数据**(见 2c:按 `time[i] - offsetTime` 重采样)。 - // 新信号与源信号共用同一个文件时间轴,因此时间平移只能写进采样值 —— 这样 - // 新信号**不启用任何「调整」**就与预览逐点重合(用户明确要求)。 - // 只有"无法烘值"的退化情形才退回把 `offsetTime` 带在样式上(见 3b)。 + // * ⚠️ **只有「数值域」的变换烘进采样值;时间轴偏移留在样式上**(见 3b)。 + // 曾经把 `offsetTime` 也按 `time[i] - offsetTime` 重采样烘进采样值("新信号 + // 不启用任何调整"),但那是**有损**的:平移出时间轴范围的那一段只能写成 NaN, + // 偏移稍大(≥ 数据跨度)就连一个有效采样都不剩 —— 用户点「构建为新信号」 + // 得到的是一条**空信号**(2026-10-29 报告)。派生信号与源信号共用同一条文件 + // 时间轴,无法用采样值表达超出轴范围的平移,因此时间偏移只能由样式表达。 + // 预览曲线本来也是用源样式的 `offsetTime` 绘制的,两条路径**显示完全一致**。 SignalStyle sourceTransform; bool hasSourceStyle = false; { @@ -726,54 +728,15 @@ namespace jdi } } - // 2c. 时间轴偏移:把 `offsetTime` 也**重采样烘进采样值**。 + // 2c.(已删除)时间轴偏移**不再**按 `time[i] - offsetTime` 重采样烘进采样值。 // - // 预览曲线是按「源信号样式」绘制的,它的时间轴偏移会把曲线整体横向平移。 - // 派生信号与源信号**共用同一个文件时间轴**,所以时间平移本无法写进"时间列"; - // 但用户明确要求:**新信号不启用任何「调整」也要与预览逐点重合** - //(新信号的「调整」面板必须是默认值,不允许靠"继承 offsetTime"来表达)。 - // 因此这里把平移直接写进数据: - // * 新信号第 i 个采样 = 预览曲线在 `time[i]` 时刻显示的值 - // = 已变换的曲线在 `time[i] - offsetTime` 处的取值(取最近的采样点); - // * 平移后落在数据范围之外的采样置 NaN —— 与预览"该处没有曲线"完全一致。 - // ⚠️ 一次整段重采样需要一份等长的临时缓冲(千万级采样 ≈ 数十 MB),用完立即释放; - // 这是"构建为新信号"的一次性操作,不在交互路径上。 - // ⚠️ 时间轴非单调时无法按下标单调推进取样(只能整表扫描),此时退回"继承 - // `offsetTime`"(见 3b):两条路径的**显示效果相同**,只是偏移留在样式里。 - bool timeShiftBaked = false; - if (hasSourceStyle && sourceTransform.offsetTime != 0.0 && axis.monotonic) - { - const std::vector& times = axis.time; - const std::size_t total = derived.values.size(); - if (total > 1 && times.size() >= total) - { - const double shift = sourceTransform.offsetTime; - std::vector shifted(total, std::numeric_limits::quiet_NaN()); - // 目标时刻 `times[index] - shift` 随 index 单调不减 → 游标只前进,整体 O(n)。 - std::size_t probe = 0; - for (std::size_t index = 0; index < total; ++index) - { - const double target = times[index] - shift; - if (!std::isfinite(target) || target < times[0] || target > times[total - 1]) - { - continue; // 平移后越界:该处没有曲线 - } - while (probe + 1 < total && times[probe + 1] < target) - { - ++probe; - } - std::size_t nearest = probe; - if (probe + 1 < total && - std::fabs(times[probe + 1] - target) < std::fabs(times[probe] - target)) - { - nearest = probe + 1; - } - shifted[index] = derived.values[nearest]; - } - derived.values.swap(shifted); - timeShiftBaked = true; - } - } + // 旧实现为了让"新信号的「调整」面板保持默认"而把平移烘进数据,代价是**有损**: + // 平移出时间轴范围的那一段只能写成 NaN,偏移 ≥ 数据跨度时**整条信号全变 NaN** + //(用户 2026-10-29 报告的"派生出来是一条空信号");即便偏移很小,也会在两端 + // 留下与实际曲线不符的空洞。 + // 派生信号与源信号共用同一条文件时间轴,无法用采样值表达超出轴范围的平移, + // 因此时间偏移改由**新信号样式**表达(见 3b)——预览曲线本来就用源样式的 + // `offsetTime` 绘制,所以两条路径的**显示完全一致**,且数据一点都不会丢。 // 3. 插入到原信号的下方(数据层 + 各标签页引用重映射)。 int newIndex = -1; @@ -789,10 +752,10 @@ namespace jdi tab.RemapAfterSignalInsert(request.groupIndex, newIndex); } - // 3b. 时间轴偏移正常情况下已烘进采样值(见 2c),新信号的「调整」全部保持默认。 - // ⚠️ 只有"无法烘值"的退化情形(时间轴非单调 / 时间列缺失)才退回到把 - // `offsetTime` 带在新信号样式上 —— 无论如何都要让**显示**与预览一致。 - if (hasSourceStyle && sourceTransform.offsetTime != 0.0 && !timeShiftBaked) + // 3b. 时间轴偏移:**只**写进新信号的样式(不再烘进采样值,见 2c 的说明)。 + // 绘制路径本来就把采样画在 `t + offsetTime` 处,因此新信号的曲线与预览曲线 + // 逐点重合,同时采样数据保持完整(不会因为平移越界而变成 NaN)。 + if (hasSourceStyle && sourceTransform.offsetTime != 0.0) { if (SignalStyle* derivedStyle = m_dataSet.Style(SignalRef{request.groupIndex, newIndex})) { diff --git a/Components/PlotView/Inc/PlotView.hpp b/Components/PlotView/Inc/PlotView.hpp index 80b4823..0029a5e 100644 --- a/Components/PlotView/Inc/PlotView.hpp +++ b/Components/PlotView/Inc/PlotView.hpp @@ -544,10 +544,10 @@ namespace jdi struct FitContribution { SignalRef ref; /**< 信号引用。 */ - double minTime = 0.0; /**< 时间轴下限(源时刻,与 `ComputeVisibleTimeBounds` 同源)。 */ - double maxTime = 0.0; /**< 时间轴上限。 */ + double minTime = 0.0; /**< **显示时间**下限(= 时间轴下限 + 该信号的 `offsetTime`)。 */ + double maxTime = 0.0; /**< **显示时间**上限。 */ bool singlePoint = false; /**< 是否只有一个有效数据点(画出来就是一个圆点)。 */ - double pointTime = 0.0; /**< 单值时:数据点的源时刻。 */ + double pointTime = 0.0; /**< 单值时:数据点的**显示时刻**(源时刻 + `offsetTime`)。 */ double pointValue = 0.0; /**< 单值时:**绘制变换后**的取值。 */ float dotRadius = 0.0f; /**< 单值时:散点半径(设备像素)。 */ }; @@ -1336,6 +1336,9 @@ namespace jdi /** * @brief 计算当前视图应使用的时间范围。 + * + * 返回的是**显示时间**(采样实际画在 `t + SignalStyle::offsetTime` 处), + * 因此"适应视图"与视图裁剪都会把时间轴偏移算进去。 * @param [OUT] low 时间下限。 * @param [OUT] high 时间上限。 */ diff --git a/Components/PlotView/Src/PlotViewFeatures.cpp b/Components/PlotView/Src/PlotViewFeatures.cpp index 236ab36..ad80c44 100644 --- a/Components/PlotView/Src/PlotViewFeatures.cpp +++ b/Components/PlotView/Src/PlotViewFeatures.cpp @@ -224,12 +224,19 @@ namespace jdi continue; } + // ⚠️ 该信号自己的时间轴偏移:采样画在 `t + offsetTime` 处, + // 因此"这段数据在屏幕上占用的时间范围"与"单值数据点的时刻"都要加上它。 + // 少了这一项,「适应视图」在信号被时间偏移后仍按原始时间范围取景 + // (用户 2026-10-29 报告的缺陷)。 + const SignalStyle* style = m_dataSet->Style(ref); + const double timeShift = (style != nullptr) ? style->offsetTime : 0.0; + FitContribution item; item.ref = ref; // 与 `ComputeVisibleTimeBounds` 同源:时间边界用**时间轴**的范围 //(一个分组的多条信号可能共用一条轴,轴范围才是"这一段数据"的边界)。 - item.minTime = axis->minTime; - item.maxTime = axis->maxTime; + item.minTime = axis->minTime + timeShift; + item.maxTime = axis->maxTime + timeShift; item.singlePoint = (data->finiteCount == 1); if (item.singlePoint) { @@ -241,8 +248,8 @@ namespace jdi } else { - const SignalStyle* style = m_dataSet->Style(ref); - item.pointTime = time; + // 同样是**显示时刻**(`time + offsetTime`):视界的居中与边界让位都以它为准。 + item.pointTime = time + timeShift; item.pointValue = (style != nullptr) ? ApplyDrawTransform(*style, value) : value; item.dotRadius = SinglePointDotRadius(ResolveSignal(ref)); @@ -706,8 +713,11 @@ namespace jdi { continue; } - low = std::min(low, axis->minTime); - high = std::max(high, axis->maxTime); + // 与 `TimeBounds` 同源:叠加该信号自己的时间轴偏移(采样画在 t + offsetTime 处)。 + const SignalStyle* style = m_dataSet->Style(ref); + const double timeShift = (style != nullptr) ? style->offsetTime : 0.0; + low = std::min(low, axis->minTime + timeShift); + high = std::max(high, axis->maxTime + timeShift); } if (!std::isfinite(low) || !std::isfinite(high) || !(high > low)) diff --git a/Documents/agent_mem.md b/Documents/agent_mem.md index 1dbd9d7..b51007b 100644 --- a/Documents/agent_mem.md +++ b/Documents/agent_mem.md @@ -8465,3 +8465,38 @@ rendercheck: A FAED59DF284153DB / B E6443BFE4E3955F8 / C 96A0AA8C402F1729 / D E3 "超在哪一段(`.text` / `.rdata` / `.xdata`+`.pdata` / `.rsrc`)、由哪个改动贡献"。 查段构成的标准命令:`objdump -h Build\Release\JustDataInspector.exe`。 当前 788,992 B,余量 259,584 B。 + +## 2026-10-29 第十四轮(本机):「适应视图」跟随时间轴偏移 + 派生信号不再为空 + +> 用户报告两条功能异常(都源于「调整 → 时间轴」偏移 + 派生信号烘值): +> ① 给信号设置时间轴偏移后点「适应视图」,视界没有跟着平移; +> ② 用「均值滤波 / 弱峰调整 → 构建为新信号」派生出来的信号是**空的**。 + +### 1. 硬规则变更(**推翻旧结论,务必按此**) + +| # | 已作废的规则 | 现行做法 | +| --- | --- | --- | +| 1 | 时间边界只取 `axis.minTime/maxTime`(源时刻) | **必须叠加该信号自己的 `SignalStyle::offsetTime`**(采样画在 `t + offsetTime` 处)。三处入口:`TabConfig::ComputeVisibleTimeBounds`、`PlotView::CollectFitContributions`(`minTime/maxTime` 与 `pointTime`)、`PlotView::FitTimeToLane` | +| 2 | 派生信号把 `offsetTime` 按 `time[i] - offsetTime` **重采样烘进采样值**(2026-10-21 的"烘值"规则) | **不再烘值**:时间偏移一律写进**新信号的样式**(`derivedStyle->offsetTime`)。理由:烘值有损 —— 平移出时间轴范围的那一段只能写 NaN,偏移 ≥ 数据跨度时整条信号全 NaN(= 空信号);而且即便偏移很小,两端也会出现与预览不符的空洞 | + +### 2. 根因与验证 + +* **①** `TimeBounds()` → `ComputeVisibleTimeBounds()` 只统计 `axis.minTime/maxTime`, + `FitContribution::{minTime,maxTime,pointTime}` 同样是源时刻 ⇒ 偏移后取景仍停留在原始时间范围, + 曲线整体跑到视界之外。 +* **②** `BuildDerivedSignal` 的 2c 步把偏移烘进采样值。派生信号与源信号**共用同一条文件时间轴**, + 无法用采样值表达超出轴范围的平移 ⇒ 只能丢成 NaN。 + 预览曲线本来就用源样式的 `offsetTime` 绘制(`PaintDerivedCurve`:`TimeToX(time[index] + style->offsetTime)`), + 所以**把偏移留在派生信号的样式上,显示与预览逐字节一致,且数据一点不丢**。 +* 新探针 `.preflight/derivecheck`(16/16):无偏移的均值滤波/弱峰调整有数据且范围不越界; + 单信号 +10% 偏移后 `FitAll` 视界 = `[min+off, max+off]`;偏移 10% 与 200% 时派生信号都保持 + `finite == values.size()`,且样式上带着偏移。 +* 回归:`uicheck` 69、`lbcheck` 21、`lbresid` 5、`lbzorder` 4、`lbcovr` 7、`lbpanel` 10、 + `derivecheck` 16 全过;`rendercheck` 8 场景哈希**与上一轮逐一相同** + FUNC 7/7;零 warning; + 产物仍是 **788,992 B**。 + +### 3. 副作用(已知、可接受) + +派生信号如果源自一个"带时间轴偏移"的信号,它的「调整」面板里「时间轴」会显示该偏移值 +(不再像以前那样强行"默认值")。这是**刻意**的:时间偏移无法无损地写进采样值, +把偏移诚实显示出来比"面板是默认值但数据被裁掉/变空"要好。 diff --git a/Utilities/Inc/DataModel.hpp b/Utilities/Inc/DataModel.hpp index 77ae8ff..37b6844 100644 --- a/Utilities/Inc/DataModel.hpp +++ b/Utilities/Inc/DataModel.hpp @@ -1463,7 +1463,12 @@ namespace jdi void ComputeLaneRange(const DataSet& dataSet, int laneIndex, double& yMin, double& yMax) const; /** - * @brief 计算全部可见信号覆盖的时间范围。 + * @brief 计算全部可见信号覆盖的**显示时间**范围。 + * + * ⚠️ 返回的是"画出来的位置",即每条信号的 `axis.minTime/maxTime` 已叠加它自己的 + * `SignalStyle::offsetTime`(采样画在 `t + offsetTime` 处)。视界要覆盖的正是这个范围, + * 少了偏移就会出现"设置时间轴偏移后点适应视图,曲线跑到视界之外"。 + * * @param [IN] dataSet 数据集合。 * @param [OUT] minTime 输出下限。 * @param [OUT] maxTime 输出上限。 diff --git a/Utilities/Src/DataModelTab.cpp b/Utilities/Src/DataModelTab.cpp index 164212c..8054ced 100644 --- a/Utilities/Src/DataModelTab.cpp +++ b/Utilities/Src/DataModelTab.cpp @@ -741,8 +741,15 @@ namespace jdi { continue; } - low = std::min(low, axis.minTime); - high = std::max(high, axis.maxTime); + // ⚠️ **必须叠加该信号自己的时间轴偏移**(`SignalStyle::offsetTime`): + // 采样实际画在 `t + offsetTime` 处,视界要覆盖的是「画出来的位置」。 + // 少了这一项,「调整 → 时间轴」设置了偏移后点「适应视图」仍会用原始时间范围, + // 用户看到的是曲线整体偏移到视界之外(用户 2026-10-29 报告的缺陷)。 + const SignalStyle* style = dataSet.Style( + SignalRef{static_cast(groupIndex), static_cast(signalIndex)}); + const double timeShift = (style != nullptr) ? style->offsetTime : 0.0; + low = std::min(low, axis.minTime + timeShift); + high = std::max(high, axis.maxTime + timeShift); } }