多层加载拖拽卡顿根治 + 高亮命中线段中段 / 散点高亮 + 选区放大也高亮 + 自动点线判定与规则修正

一、三明治加载后拖拽视界卡顿(39.2ms → 8.2ms 均值,47.9ms → 10.3ms 最坏)—— 四个叠加根因
1) RenderLaneLayerRegion 每次「横向补条」都调用两次 BuildLaneVisuals()(对全部网格算
   ComputeLaneRange,O(网格数))⇒ 253 网格时单帧 RollWideBuffer = 26.8ms。
   修法:进入前快照 m_lanes / m_laneContentHeight,结束时直接拷回(它只渲染一个网格)。
2) 横向「重新居中」本来就不是正确性要求:BlitWideBuffer 按 (ViewMin - layer.wideMin) * scale
   算源偏移,不滚动也画得对 ⇒ 新增 kLayerRollQuantumX=32(不足一个量子不滚)+
   kLayerRollsPerFrame=3(每帧最多重居中 3 个网格,分帧摊销)。
3) 位移过大时旧实现「立刻整块重画」:巨型信号(25M 采样)一次 20~130ms,实测拖拽最坏帧 28ms。
   修法:kLayerRollMaxShiftPixels=160,超出就作废图层 + 登记待重画,本帧由 BlitWideBuffer 的
   网格底衬兜底(正是用户要求的“没来得及画信号就按只有网格叠底继续拖”);
   并且 RollWideBuffer 只处理可视/预取带内的网格(离屏网格不做像素工作)。
4) 包络带路径也支持横向补条裁剪:包络是逐像素列独立计算的,只构建条带覆盖的列即可。
   旧实现对包络带总是构建整幅可见范围 ⇒ 巨型信号一条 32px 竖带要 5ms。

二、鼠标悬停「两点之间的连线」无法高亮(放大到自动点线后)
根因:HitTestSignal 的横向窗口用 upper_bound(fx+tol) 取顶点区间,只扫 [first, last) 的线段;
放大后相邻点相距几十像素,鼠标落在两点中间时两端都在窗口外 ⇒ windowFirst >= windowLast ⇒ 跳过。
修法:线段扫描上界改为 min(stop, windowLast + 1)(必须包含“一端在窗口内、另一端在窗口外”的线段);
散点命中的顶点范围也向两侧各扩一个。实测 11 段长线段中点命中 0/11 → 11/11。

三、散点模式补充高亮(用户要求:原线段/点线高亮“去掉数据点之间连线的部分”)
PaintSignalHalo 原来对 DrawMode::SCATTER 直接 return ⇒ 只有拖拽提示、没有高亮。
现在给每个参与绘制的散点套一圈半透明强调色圆环(步长与绘制同源 kMaxScatterPoints)。
线段 / 点线保持原有的折线光晕。列表 / 图例 / 样式区「目标」行联动高亮走同一条路径,自动生效。

四、「选区放大」模式恢复悬停高亮(仍不允许拖信号)
OnMouseMove 去掉 ZoomMode() == NONE 的门控;OnLButtonDown 在 SELECT 模式下仍直接进入 RUBBER_SELECT。
该模式下的提示文案只显示信号名(去掉 “· 按住拖拽可移入其它网格” 尾缀)。

五、自动点线误判(MF4 + VERY_BIG_DATA 同时加载时 MF4 信号默认就被切点线)
根因:MinorCellSparse 把整个视界切成小格。VERY_BIG_DATA 跨度 ~130s、MF4 数据只覆盖 ~11s ⇒
MF4 信号在 90% 的格子里本来就没有任何采样点 ⇒ “存在一个格子内 < 5 个点”恒成立。
(用户猜测是“用了抽稀点”,实际是“空格子”,但现象一致。)
修法:先用可见数据的显示时间跨度 [dataMin, dataMax] 收窄参与的格子(firstCell..lastCell),
鸽巢判定的分母改用参与格子数;端点 NaN 时做 64 步有界探测并回退到轴区间。
实测:248 个 MF4/中等信号被误判的数量 248 → 0。

六、自动点线规则收窄(用户本次要求)
只有 DrawMode::LINE 的信号在放大时自动切 LINE_SCATTER;SCATTER 不切换(散点已足够提示数据点位置)、
LINE_SCATTER 不动;用户手动改过样式(autoScatterUserOverride)时不切换,避免“改完下一帧又被改回去”。
退出时(用户未手动改过)恢复原来的 LINE。
实测:LINE → 放大后 mode=2 auto=1 → 适应视图后 mode=0 auto=0;SCATTER → 放大后 mode=1 auto=0。

验证(全部离屏,不占用鼠标/键盘/屏幕)
- 新增 .preflight/perf/hoverdrag_main.cpp(hoverdrag.exe)20/20:三明治加载后的点选跳转 + 拖拽逐帧耗时、
  线段中点命中、散点高亮像素、选区放大悬停命中、自动点线判定与规则。
- 既有回归:verify 49/49、panelanim 158/158、datacheck 69/69、edgecheck 46/46、errcheck 19/19、
  gridanim 93/93、lanescroll 23/23。
- 探针的绝对毫秒阈值改为相对基准(同一基线代码在不同时段可差 2~3 倍,避免假失败)。
- make release / make debug 零错误零警告(用 TARGET_FILE_NAME=TargetCheck/TargetCheckD 避免与运行中实例冲突)。

文档:Documents/agent_mem.md 新增 §9.45、§11.2 第 141–150 条与变更日志一行。
This commit is contained in:
DeepSeek-V4.1-Flash committed 2026-10-18 01:08:11 +08:00
1 parent 6b15575829
commit fd606a6f80
5 files changed
+394 -70

No files matched your search

+93 -12
View File
@@ -769,23 +769,98 @@ namespace jdi
{
return false;
}
// 鸽巢原理:有效点总数都不足“每格 5 个” ⇒ 必然存在一个稀疏小格(O(1) 充分判定)。
const std::size_t need = static_cast<std::size_t>(cells) *
static_cast<std::size_t>(kSparseMinPointsPerCell);
if (signal.finiteCount < need)
// ⚠️⚠️ **判定只在这个信号【实际有数据】的时间段内进行**(用户 2026-10-17 报告):
// 同时加载 `VERY_BIG_DATA`(视界跨度 ~130s)与 MF4(数据只覆盖 ~11s)时,
// 旧实现把**整个视界**切成小格 —— MF4 信号在 90% 的格子里本来就没有任何采样点,
// 于是“存在一个格子内 < 5 个点”恒成立 ⇒ **默认状态下就被错判成稀疏、自动切点线**。
// 现在先用「可见数据的显示时间跨度」把格子范围收窄,只对这些格子做判定。
double dataMin = viewMin;
double dataMax = viewMax;
{
return true;
// 绝大多数信号的两端采样本就有效 ⇒ **零扫描**快速路径(每帧 × 上百个信号,这很重要)。
const bool edgeOk =
std::isfinite(signal.values[begin]) && std::isfinite(axis.time[begin]) &&
std::isfinite(signal.values[end - 1]) && std::isfinite(axis.time[end - 1]);
if (edgeOk)
{
dataMin = axis.time[begin];
dataMax = axis.time[end - 1];
}
else
{
// 端点无效(NaN)时向内侧做**有界**探测(NaN 只出现在缺数据段,几步内就能找到);
// 探测不到就回退到整段轴区间(宁可多算几格,也不要放弃判定)。
const std::size_t probe = 64u;
const std::size_t forwardCap = std::min(end, begin + probe);
bool found = false;
for (std::size_t index = begin; index < forwardCap; ++index)
{
if (std::isfinite(signal.values[index]) && std::isfinite(axis.time[index]))
{
dataMin = axis.time[index];
found = true;
break;
}
}
if (!found)
{
dataMin = axis.time[begin];
}
const std::size_t backwardFloor =
(end > probe) ? std::max(begin, end - probe) : begin;
found = false;
for (std::size_t index = end; index-- > backwardFloor;)
{
if (std::isfinite(signal.values[index]) && std::isfinite(axis.time[index]))
{
dataMax = axis.time[index];
found = true;
break;
}
}
if (!found)
{
dataMax = axis.time[end - 1];
}
}
dataMin = std::max(dataMin, viewMin);
dataMax = std::min(dataMax, viewMax);
if (!(dataMax > dataMin))
{
return false;
}
}
const double cellSpan = span / static_cast<double>(cells);
if (!(cellSpan > 0.0) || !std::isfinite(cellSpan))
{
return false;
}
// 只覆盖「有数据的显示时间范围」所涉及的格子(闭区间)。
int firstCell = static_cast<int>(std::floor((dataMin - viewMin) / cellSpan));
int lastCell = static_cast<int>(std::floor((dataMax - viewMin) / cellSpan));
firstCell = std::max(0, std::min(firstCell, cells - 1));
lastCell = std::max(0, std::min(lastCell, cells - 1));
if (lastCell < firstCell)
{
return false;
}
const int consideredCells = lastCell - firstCell + 1;
// 鸽巢原理:有效点总数都不足“每格 5 个” ⇒ 必然存在一个稀疏小格(O(1) 充分判定)。
// ⚠️ 分母必须用**实际参与的格子数**(不是整个视界的格子数)。
const std::size_t need =
static_cast<std::size_t>(consideredCells) * static_cast<std::size_t>(kSparseMinPointsPerCell);
if (signal.finiteCount < need)
{
return true;
}
// 时间轴单调 ⇒ 每个小格用两次二分取出它的样本区间(代价与采样数无关)。
if (axis.monotonic)
{
for (int cell = 0; cell < cells; ++cell)
for (int cell = firstCell; cell <= lastCell; ++cell)
{
const double cellMin = viewMin + cellSpan * static_cast<double>(cell);
const double cellMax =
@@ -852,7 +927,7 @@ namespace jdi
++buckets[slot];
}
}
for (int cell = 0; cell < cells; ++cell)
for (int cell = firstCell; cell <= lastCell; ++cell)
{
if (buckets[static_cast<std::size_t>(cell)] < kSparseMinPointsPerCell)
{
@@ -3054,15 +3129,21 @@ namespace jdi
{
if (!style->autoScatterActive)
{
// ⚠️ **只有「线段」模式的信号才自动切「点线」**(用户 2026-10-17 要求):
// 本来就是「散点」的信号放大后**不**自动切换(散点已经足够提示数据点位置,
// 再叠上线段反而干扰);已经是「点线」的也不需要动。
// ⚠️ 用户在此期间手动改过样式时也不切换,避免“改完下一帧又被改回去”。
if (style->mode != DrawMode::LINE || style->autoScatterUserOverride)
{
style->autoScatterActive = false;
continue;
}
// 进入阈值:保存原绘制方式并自动切到「点线」提示用户数据点位置。
style->autoScatterSavedMode = style->mode;
style->autoScatterActive = true;
style->autoScatterUserOverride = false;
if (style->mode != DrawMode::LINE_SCATTER)
{
style->mode = DrawMode::LINE_SCATTER;
changed = true;
}
style->mode = DrawMode::LINE_SCATTER;
changed = true;
}
}
else if (style->autoScatterActive)