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); } }