自适应范围/时机澄清 + 适应视图与 Y 轴自适应互不干扰 + 吸附跟随时间偏移
一、澄清「自适应范围与时机」(并修正记忆里不完善的表述)
* 准确说法:「额外要求视界内至少有一个可见数据点」这句不完善,完整规则是:
- 首个网格显示内容时对它做完整自适应 —— **Y 轴与时间轴都**匹配自己的数据范围;
- 只有一个数据点的信号必须显示在网格**中间**,保证其可见
(FitSinglePointSignal:数据点居中、时间跨度 1s、Y 轴跨度 1);
- **后续新增网格只做 Y 轴自适应,不再调整时间轴**。
* Documents/agent_mem.md 相应章节(§9.30 正文、§11.2 第 58/60 条)已按此改写。
二、修复「适应视图」与「Y 轴自适应」互相干扰
* 根因:两条自适应路径的留白不同 —— 整段自适应(ComputeLaneRange → PadRange)用 6%,
「选区 Y 轴自适应」(AutoRangeLaneToView)用 5%。视界未变时两者本应完全相等,
于是「点过适应视图 → 打开 Y 轴自适应」会算出不同区间、网格被无意义重绘
(实测切一次 143513 像素),反向操作同样(137326 像素)。
* 修法:新增 DataModel.hpp 的 PadValueRange() 作为**唯一**留白实现(含退化区间规则
max(1, |值|×0.1)),PadRange 与 AutoRangeLaneToView 共用它。
* 结果:「适应视图 → 开自适应 → 关自适应 → 再适应视图」四个步骤上生效区间逐位相同,
绘图区逐像素 0 差异;而真正改变视界(缩放)时仍然正常重绘。
三、修复「适应视图」在单点信号上让数据点“飞了”
* FitAll() 开头先尝试 FitSinglePointSignal():仅一个网格、且唯一可见信号只有一个有效
数据点时,走与首次加载完全相同的单点初始视图(数据点居中、时间跨度 1s、Y 跨度 1),
并保持幂等(重复点击像素不变)。
四、修复光标吸附未考虑「调整 → 时间轴偏移」
* 根因:采样点 k 画在 time[k] + offsetTime 处,而光标存的也是显示时间,
但吸附一直在源时间空间查找 —— 设过时间轴偏移后吸附会落到历史位置。
* 修法:SignalSnapEnabled / HasVisibleSample / NearestSnapSample / LocalSampleSpacing
全部增加 timeShift 形参:可见性与点数统计比较 time[k] + timeShift;
NearestSnapSample 内部用 time - timeShift 回源空间查找、返回时再加回去;
UpdateAutoScatterStyles(自动点线)同步传入 offsetTime,保持与吸附判定同源。
五、验证(离屏 .preflight/check:SW_HIDE 窗口、不注入真实鼠标键盘、进程/线程 BELOW_NORMAL)
39/39 通过(连跑 3 次稳定):
[A] 四个步骤的生效区间逐位相同 + 绘图区逐像素 0 差异(对照组:真缩放重绘 79581 像素);
[B] 单点信号的适应视图与首次加载一致(1s/1、居中、幂等);
[C] 首个网格自适应时间轴、后续新增/隐藏信号都不再改动时间轴;
[D] 偏移 +0.3 时吸到“画面上的点”,偏移 0 行为不变,偏移把点移出视界时不吸附;
[E] 回归:单点默认样式(点线 + 最大线宽)与“2…16 点不再改点线”均保持。
A/B:回退三项修复 ⇒ 7 项如实失败(含 143513 / 137326 像素的重绘症状)。
make release 零警告(用户正运行 Target.exe,链接用临时 -o 名字验证通过)。
六、规则同步
Documents/agent_mem.md 新增 §9.31、§11.2 第 58–63 条(含修正后的自适应范围/时机规则)
与变更日志。
This commit is contained in:
1 parent
57cc714c1d
commit
68d8b7ede6
5 files changed
+202
-80
No files matched your search
@@ -1324,6 +1324,15 @@ namespace jdi
|
||||
return;
|
||||
}
|
||||
|
||||
// 「仅有一个网格、且网格内只显示了一个**单点**信号」时,适应效果必须与
|
||||
// “首次加载并勾选该单点信号”时完全一致(数据点居中、时间跨度 1s、Y 跨度 1)。
|
||||
// 否则整段自适应会让时间轴覆盖整条时间列 —— 数据点被挤到网格边缘、看起来“飞了”
|
||||
//(用户 2026-10-15 报告)。
|
||||
if (FitSinglePointSignal())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
double low = 0.0;
|
||||
double high = 1.0;
|
||||
TimeBounds(low, high);
|
||||
@@ -1836,7 +1845,8 @@ namespace jdi
|
||||
return std::max(1, static_cast<int>(std::llround(cells)));
|
||||
}
|
||||
|
||||
bool PlotView::SignalSnapEnabled(const TimeAxis& axis, const SignalData& signal) const
|
||||
bool PlotView::SignalSnapEnabled(const TimeAxis& axis, const SignalData& signal,
|
||||
double timeShift) const
|
||||
{
|
||||
const int cells = GridMinorCellCount();
|
||||
if (cells <= 0 || signal.finiteCount == 0)
|
||||
@@ -1857,7 +1867,7 @@ namespace jdi
|
||||
}
|
||||
std::size_t begin = 0;
|
||||
std::size_t end = 0;
|
||||
VisibleIndexRange(axis, count, begin, end);
|
||||
VisibleIndexRange(axis, count, begin, end, timeShift);
|
||||
if (end <= begin)
|
||||
{
|
||||
return false;
|
||||
@@ -1869,7 +1879,7 @@ namespace jdi
|
||||
if (signal.finiteCount < threshold)
|
||||
{
|
||||
// 有效点总数本来就不足阈值 ⇒ 只需确认视界内确实有点(稀疏点已缓存时二分判定)。
|
||||
return HasVisibleSample(axis, signal, begin, end);
|
||||
return HasVisibleSample(axis, signal, begin, end, timeShift);
|
||||
}
|
||||
|
||||
// 一般信号:统计**当前视界内**的有效点数量(攒够阈值立刻早退,代价有界)。
|
||||
@@ -1883,8 +1893,15 @@ namespace jdi
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (!std::isfinite(axis.time[index]) || axis.time[index] < Tab().ViewMin() ||
|
||||
axis.time[index] > Tab().ViewMax())
|
||||
// ⚠️ 必须按**绘制时刻**判断可见性(`t + offsetTime`),否则存在时间轴偏移时
|
||||
// 会把“已经移出视界”的采样点算进来(用户报告吸附到历史位置)。
|
||||
const double time = axis.time[index];
|
||||
if (!std::isfinite(time))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const double displayTime = time + timeShift;
|
||||
if (displayTime < Tab().ViewMin() || displayTime > Tab().ViewMax())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
@@ -1897,12 +1914,12 @@ namespace jdi
|
||||
}
|
||||
|
||||
bool PlotView::HasVisibleSample(const TimeAxis& axis, const SignalData& signal, std::size_t begin,
|
||||
std::size_t end) const
|
||||
std::size_t end, double timeShift) const
|
||||
{
|
||||
const double viewMin = Tab().ViewMin();
|
||||
const double viewMax = Tab().ViewMax();
|
||||
const double viewMin = Tab().ViewMin() - timeShift;
|
||||
const double viewMax = Tab().ViewMax() - timeShift;
|
||||
|
||||
// 稀疏信号(有效点已缓存且升序):二分即可。
|
||||
// 稀疏信号(有效点已缓存且升序):二分即可(整体平移不改变有序性)。
|
||||
if (!signal.sparseTimes.empty())
|
||||
{
|
||||
const std::vector<double>& times = signal.sparseTimes;
|
||||
@@ -1925,21 +1942,25 @@ namespace jdi
|
||||
}
|
||||
|
||||
bool PlotView::NearestSnapSample(const TimeAxis& axis, const SignalData& signal, double time,
|
||||
double& candidate) const
|
||||
double timeShift, double& candidate) const
|
||||
{
|
||||
const double viewMin = Tab().ViewMin();
|
||||
const double viewMax = Tab().ViewMax();
|
||||
// ⚠️ `time` / `candidate` 都是**显示时间**(画面上的时刻):采样点 k 画在 `time[k] + timeShift`
|
||||
// 处,而光标存的也是显示时间,因此查找与可见性判定都要整体平移(否则存在
|
||||
// 「调整 → 时间轴偏移」时吸附会落在历史位置上,用户 2026-10-15 报告)。
|
||||
const double viewMin = Tab().ViewMin() - timeShift;
|
||||
const double viewMax = Tab().ViewMax() - timeShift;
|
||||
|
||||
// 稀疏信号:有效点已在加载时缓存(按时间升序)⇒ 二分取最近的一个。
|
||||
if (!signal.sparseTimes.empty())
|
||||
{
|
||||
const std::vector<double>& times = signal.sparseTimes;
|
||||
const std::vector<double>::const_iterator it = std::lower_bound(times.begin(), times.end(), time);
|
||||
const double target = time - timeShift;
|
||||
const std::vector<double>::const_iterator it = std::lower_bound(times.begin(), times.end(), target);
|
||||
bool found = false;
|
||||
double bestDistance = std::numeric_limits<double>::infinity();
|
||||
for (int side = 0; side < 2; ++side)
|
||||
{
|
||||
// side 0 = 第一个 >= time 的点;side 1 = 它前面的那个点。
|
||||
// side 0 = 第一个 >= target 的点;side 1 = 它前面的那个点。
|
||||
if (side == 1 && it == times.begin())
|
||||
{
|
||||
continue;
|
||||
@@ -1954,11 +1975,11 @@ namespace jdi
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const double distance = std::fabs(value - time);
|
||||
const double distance = std::fabs((value + timeShift) - time);
|
||||
if (distance < bestDistance)
|
||||
{
|
||||
bestDistance = distance;
|
||||
candidate = value;
|
||||
candidate = value + timeShift;
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
@@ -1972,7 +1993,7 @@ namespace jdi
|
||||
{
|
||||
return false;
|
||||
}
|
||||
const int center = FindNearestSample(axis, time);
|
||||
const int center = FindNearestSample(axis, time - timeShift);
|
||||
if (center < 0)
|
||||
{
|
||||
return false;
|
||||
@@ -1997,13 +2018,13 @@ namespace jdi
|
||||
{
|
||||
continue;
|
||||
}
|
||||
// 只接受**可见区间内**的采样点:视界之外的采样点不是合法的落点
|
||||
// 只接受**可见区间内**的采样点(按绘制时刻判断):视界之外的采样点不是合法落点
|
||||
//(否则吸附会把光标一把甩到视界边缘并钉住)。
|
||||
if (sampleTime < viewMin || sampleTime > viewMax)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
candidate = sampleTime;
|
||||
candidate = sampleTime + timeShift;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -2011,14 +2032,16 @@ namespace jdi
|
||||
}
|
||||
|
||||
void PlotView::LocalSampleSpacing(const TimeAxis& axis, const SignalData& signal, double time,
|
||||
double& spacing) const
|
||||
double timeShift, double& spacing) const
|
||||
{
|
||||
spacing = 0.0;
|
||||
// `time` 是**显示时间**,而缓存/采样数组是源时间 ⇒ 查询统一换算回源空间。
|
||||
const double target = time - timeShift;
|
||||
if (signal.sparseTimes.size() >= 2)
|
||||
{
|
||||
const std::vector<double>& times = signal.sparseTimes;
|
||||
const std::size_t found =
|
||||
static_cast<std::size_t>(std::lower_bound(times.begin(), times.end(), time) - times.begin());
|
||||
static_cast<std::size_t>(std::lower_bound(times.begin(), times.end(), target) - times.begin());
|
||||
const std::size_t left = (found > 0) ? (found - 1) : 0;
|
||||
const std::size_t right = std::min(times.size() - 1, left + 1);
|
||||
if (right > left)
|
||||
@@ -2045,7 +2068,7 @@ namespace jdi
|
||||
{
|
||||
return;
|
||||
}
|
||||
const int index = FindNearestSample(axis, time);
|
||||
const int index = FindNearestSample(axis, target);
|
||||
if (index < 0)
|
||||
{
|
||||
return;
|
||||
@@ -2113,18 +2136,24 @@ namespace jdi
|
||||
}
|
||||
const SignalData* signal = m_dataSet->Resolve(ref);
|
||||
const TimeAxis* axis = m_dataSet->Axis(ref);
|
||||
const SignalStyle* style = m_dataSet->Style(ref);
|
||||
if (signal == nullptr || axis == nullptr)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
// ⚠️ 一切吸附判定与查找都在**显示时间**空间进行:采样点 k 画在
|
||||
// `time[k] + offsetTime` 处,而光标存的也是显示时间。
|
||||
// 少了这一步,信号一旦设置过「调整 → 时间轴偏移」,吸附就会落在**历史位置**
|
||||
// (用户 2026-10-15 报告“存在时间轴偏移调整时无法吸附到正确位置”)。
|
||||
const double timeShift = (style != nullptr) ? style->offsetTime : 0.0;
|
||||
// 只有“一屏内数据点少于 N×4”的信号参与吸附;判据随视界实时重算,
|
||||
// 因此疏密变化的信号在稀疏区自动吸附、移到稠密区自动恢复自由移动。
|
||||
if (!SignalSnapEnabled(*axis, *signal))
|
||||
if (!SignalSnapEnabled(*axis, *signal, timeShift))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
double candidate = 0.0;
|
||||
if (!NearestSnapSample(*axis, *signal, time, candidate))
|
||||
if (!NearestSnapSample(*axis, *signal, time, timeShift, candidate))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
@@ -2132,7 +2161,7 @@ namespace jdi
|
||||
// 吸附半径再受“半个局部采样间距”限制(点很稀疏时不被吸死)。
|
||||
double limitTime = radiusTime;
|
||||
double spacingTime = 0.0;
|
||||
LocalSampleSpacing(*axis, *signal, candidate, spacingTime);
|
||||
LocalSampleSpacing(*axis, *signal, candidate, timeShift, spacingTime);
|
||||
if (spacingTime > 0.0)
|
||||
{
|
||||
limitTime = std::min(limitTime, spacingTime * 0.5);
|
||||
@@ -4338,23 +4367,11 @@ namespace jdi
|
||||
return false;
|
||||
}
|
||||
|
||||
// 与自动自适应保持一致的上下留白,避免波形顶到网格边界。
|
||||
if (high > low)
|
||||
{
|
||||
const double pad = (high - low) * 0.05;
|
||||
low -= pad;
|
||||
high += pad;
|
||||
}
|
||||
else
|
||||
{
|
||||
// ⚠️ 可见区间内只有一个取值(单点信号 / 恒定值):给一个**可读**的跨度,
|
||||
// 而不是几乎为 0 的退化区间 —— 退化区间会让幅值网格线挤成一团、
|
||||
// 数据点也看不出位置(用户报告“单值信号的全局自适应不生效”)。
|
||||
// 这里的留白与 `TabConfig::ComputeLaneRange` 的 `PadRange` 完全一致。
|
||||
const double magnitude = std::max(1.0, std::fabs(low) * 0.1);
|
||||
low -= magnitude;
|
||||
high += magnitude;
|
||||
}
|
||||
// ⚠️ 留白必须与整段自适应(`TabConfig::ComputeLaneRange` → `PadRange`)**共用同一规则**
|
||||
//(`PadValueRange`):否则「点过适应视图后再打开 Y 轴自适应」会因为留白不同
|
||||
//(6% vs 5%)而算出不一样的区间,网格被无意义地重绘一次;
|
||||
// 反向(关掉 Y 轴自适应再点适应视图)同样会变。
|
||||
PadValueRange(low, high);
|
||||
|
||||
Lane& lane = m_tab->Lanes()[static_cast<std::size_t>(laneIndex)];
|
||||
lane.autoRange = false;
|
||||
@@ -4452,7 +4469,8 @@ namespace jdi
|
||||
// 判定与**光标吸附完全同源**(用户 2026-10-15 明确要求):
|
||||
// 只有“一屏内数据点少于 N×4”的信号才自动切「点线」,
|
||||
// 不再满足时(适应视图、或把视界移到信号的稠密区)自动恢复原样式。
|
||||
if (SignalSnapEnabled(*axis, *data))
|
||||
// ⚠️ 可见性同样按**显示时间**判断(含 `offsetTime`),与吸附保持一致。
|
||||
if (SignalSnapEnabled(*axis, *data, style->offsetTime))
|
||||
{
|
||||
if (!style->autoScatterActive)
|
||||
{
|
||||
|
||||
Reference in new issue
Block a user