修复:适应视图不跟随时间轴偏移 + 派生信号构造成空信号
## 为什么
用户 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,未改优化等级)。
This commit is contained in:
1 parent
044b87383b
commit
71c6ff91cb
6 files changed
+91
-68
No files matched your search
@@ -650,7 +650,6 @@ namespace jdi
|
||||
return false;
|
||||
}
|
||||
const SignalData& source = group->signals[static_cast<std::size_t>(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<double>& times = axis.time;
|
||||
const std::size_t total = derived.values.size();
|
||||
if (total > 1 && times.size() >= total)
|
||||
{
|
||||
const double shift = sourceTransform.offsetTime;
|
||||
std::vector<double> shifted(total, std::numeric_limits<double>::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}))
|
||||
{
|
||||
|
||||
Reference in new issue
Block a user