滚动过渡动画根治 + 快速连点跟手 + 分组名单击展开 + 光标吸附同源 + 按钮淡出对齐 + 滚动位置复位

一、滚动过渡动画卡顿(用户报告:甚至不如手动拖滚动条)—— 三处独立根因
1) WM_TIMER(kScrollTimerId) 无条件整窗失效 ⇒ WM_PAINT 看到更新区域不在绘图区内 ⇒ 把
   m_chromeDirty 置真 ⇒ 每一帧都重绘标签栏/工具栏/面板。
   修法:用「推进之前」的面板动画状态决定失效范围 —— 面板动画(滚动到目标信号 / 分组折起展开)
   确实要改面板像素 ⇒ 整窗失效;只有绘图区动画(网格滚动到可视区 / 回到最上方)⇒ 只失效绘图区。
   实测:绘图区动画帧 13.6ms → 3.5ms;手动拖滚动条参照 4.2ms。
   ⚠️ 未改变 §9.41 的回送结构(仍是 if (fullFrame) {整窗} else {绘图区})。
2) 图层预热不足:上百个离屏网格长期不建图层,长距离滚动动画扫进去时每一帧都有十几个网格
   没有图层像素,只能靠「网格底衬兜底」重画(约 0.5ms/网格 ⇒ +8ms/帧)。
   修法:LaneVisuallyRelevant(laneIndex, prefetchScreens) 增加 ±1 屏预取带(AnyVisibleLanePending 用),
   并新增 PrefetchStepAllowed()(kPrefetchMinIntervalMs=32 ⇒ 约 31 次/秒)限速地继续请求重绘,
   让整批图层在静置期间逐步建好。
   实测:lane-only + VERY_BIG_DATA 帧 13.9ms → 5.3ms;signal-click + VERY_BIG_DATA 18.3ms → 10.3ms。
3) 网格滚动量原本参与「宽基底可用性」判据(上一轮已修:SceneContentHash(..., includeLaneScroll=false))。

二、光标吸附改用「放大自动点线」的同一判定(用户 2026-10-17 修订)
AutoPointLineNeeded 改名 MinorCellSparse(中性名,同一判定两处用途):任一小格内有效数据点 < 5 即为稀疏。
- 「放大自动切点线」与「光标吸附」共用它;判定完全不看绘制方式 ⇒ 即使用户手动改过显示样式也照样吸附。
- 鼠标下方网格内既有符合又有不符合的信号时,只有符合的那条的数据点参与吸附;没有符合的则自由移动。
- 删除旧的整屏判据 SignalSnapEnabled 与 kSnapVisibleCellsPerPoint(死代码)。

三、「回到最上方」按钮淡出与「浮动提示」一致
kBackToTopFadeOutMs = kHintFadeMs(500ms)+ 线性 alpha,到达顶部立即开始淡出(不停留)。
淡入保留 170ms ease-out。实测 50% 时刻 alpha = 0.500(线性),旧 ease-out 会是 0.875。

四、快速连点跟手(根因:CS_DBLCLKS 合成 WM_LBUTTONDBLCLK 导致按下丢失)
组件各自实现 DoubleClickIsPlainPress(x, y)(该落点有没有双击语义),宿主 WM_LBUTTONDBLCLK 据此
选择走 OnLButtonDown(当作按下)还是 OnDoubleClick:
- PlotView:浮动面板(含复选框/滑块/复位/✕/标题栏)、「回到最上方」按钮、绘图区滚动条、GX 徽标 ⇒ 按下;
  时间光标线/读数标签/图例条目名/绘图区空白 ⇒ 保留双击。
- SignalPanel:样式区数值框、「目标」行名称与 GX 标签、行名称区域 ⇒ 保留双击;其余全部 ⇒ 按下。
- 弹出菜单 / 拾色器打开时,WM_LBUTTONDBLCLK 也按各自 OnLButtonDown 走。

五、分组行:名称恢复单击展开收起 + 只有快速双击进入改名
- OnLButtonDown 分组分支不再排除 RowPart::BODY:三角 /「组X」/ 名称本体统一折叠展开。
- OnDoubleClick 名称区域:只有快速双击(kGroupRenameDoubleClickMs=300,比系统 500ms 紧)才进入改名;
  超阈值的「慢双击」只当又一次单击;快速双击前先撤销按下那一次的折叠切换,保证进编辑时状态与点击前一致。

六、信号重现时网格滚动回到最上方
“有过需滚动条的网格 → 没有任何信号显示 → 又出现需滚动条数量的网格”这一类情形必须从最上方开始看。
修法:Application::SynchronizeView 的「首次出现可见信号」分支里,在 FitAll() 之后 tab.LaneScroll() = 0。

验证(全部离屏,不占用鼠标/键盘/屏幕)
- grabdanim(.preflight/perf/gridanim_main.cpp)93/93:真实消息循环帧距 + 逐帧渲染耗时(与手动拖滚动条
  对比)、光标吸附新判定、按钮淡出线性、快速连点路由、分组名单击/双击/慢双击、滚动位置复位。
- 既有回归:verify 49/49、panelanim 158/158(按新分组名规则更新 8 条断言)、datacheck 69/69、
  edgecheck 46/46、errcheck 19/19。
- make release / make debug 零错误零警告(用 TARGET_FILE_NAME=TargetCheck/TargetCheckD 避免与正在运行的实例冲突)。

文档:Documents/agent_mem.md 新增 §9.43、§11.2 第 125–133 条与变更日志一行。
This commit is contained in:
DeepSeek-V4.1-Flash committed 2026-10-17 03:17:42 +08:00
1 parent 34375481de
commit 436dcbfd52
12 files changed
+618 -182

No files matched your search

+115 -20
View File
@@ -1129,10 +1129,49 @@ namespace jdi
const int x = GET_X_LPARAM(lParam);
const int y = GET_Y_LPARAM(lParam);
// ⚠️ **工具栏必须在这里也当作“按下”处理**:窗口类带 `CS_DBLCLKS`,快速连点同一个
// 按钮时,第二次(及之后的快速按下)会被 Windows 合成为 `WM_LBUTTONDBLCLK`。
// 工具栏没有双击语义,若这里不处理,那一次按下就完全丢失 ——
// 用户看到的正是“连点好几下、开关状态只切换了几次”。
// ⚠️⚠️ **没有双击语义的控件必须把这次「双击」当作一次普通按下**:
// 窗口类带 `CS_DBLCLKS`,快速连点同一个控件时第二次(及之后)按下会被 Windows
// 合成为 `WM_LBUTTONDBLCLK`。若这里不处理,那一次按下就完全丢失 ——
// 用户看到的正是“连点好几下、状态只切换了几次”(不跟手)。
// 之前只给工具栏打了这个补丁;本轮(用户 2026-10-17)扩展到:
// 弹出菜单 / 拾色器 / 信号面板(复选框、全部显示隐藏、绝对值、折叠三角、「组X」…)/
// 绘图区(浮动面板上的复选框与按钮、滚动条、GX 徽标)。
// 组件各自用 `DoubleClickIsPlainPress(x, y)` 判定“这个落点有没有双击语义”。
// 弹出菜单打开时接管输入:菜单条目只有单击语义。
if (m_menu.IsOpen())
{
if (m_menu.IsOnEditField(x, y))
{
m_menu.OnEditMouseDown(x, y);
SetCapture(hwnd);
m_activeTarget = Target::EDIT_FIELD;
InvalidateRect(hwnd, nullptr, FALSE);
return 0;
}
MenuResult result;
m_menu.OnLButtonDown(x, y, result);
InvalidateRect(hwnd, nullptr, FALSE);
if (result.valid)
{
DispatchMenuResult(result);
}
return 0;
}
// 拾色器打开时接管输入(低一行于菜单、高于常规组件):其内部只有拖动语义。
if (m_colorPicker.IsOpen())
{
if (m_colorPicker.ContainsPoint(x, y))
{
m_colorPicker.OnLButtonDown(x, y);
SetCapture(hwnd);
InvalidateRect(hwnd, nullptr, FALSE);
return 0;
}
}
// 工具栏没有双击语义,若这里不当作按下,那一次按下就完全丢失。
if (m_toolbar.HitTest(x, y))
{
SetFocus(hwnd);
@@ -1144,6 +1183,55 @@ namespace jdi
return 0;
}
// 信号面板 / 绘图区:只有“改名”等少数落点保留双击语义,其余当作按下。
if (m_panel.HitTest(x, y))
{
SetFocus(hwnd);
if (m_panel.DoubleClickIsPlainPress(x, y))
{
SetCapture(hwnd);
m_activeTarget = Target::PANEL;
if (m_panel.OnLButtonDown(x, y))
{
ApplyPendingPlotLaneScroll();
InvalidateRect(hwnd, nullptr, TRUE);
}
return 0;
}
bool changed = m_panel.OnDoubleClick(x, y);
if (changed)
{
InvalidateRect(hwnd, nullptr, TRUE);
}
ApplyPendingPanelScroll();
return 0;
}
if (m_plot.HitTest(x, y))
{
SetFocus(hwnd);
if (m_plot.DoubleClickIsPlainPress(x, y))
{
SetCapture(hwnd);
m_activeTarget = Target::PLOT;
if (m_plot.OnLButtonDown(x, y))
{
ApplyPendingRename();
ApplyPendingDerivedBuild();
InvalidateRect(hwnd, nullptr, FALSE);
}
ApplyPendingPanelScroll();
UpdateDragCursor(x, y);
return 0;
}
if (m_plot.OnDoubleClick(x, y))
{
InvalidateRect(hwnd, nullptr, TRUE);
}
ApplyPendingPanelScroll();
ApplyPendingRename();
return 0;
}
bool changed = false;
if (m_tabBar.HitTest(x, y))
{
@@ -1155,20 +1243,10 @@ namespace jdi
Relayout();
}
}
else if (m_panel.HitTest(x, y))
{
changed = m_panel.OnDoubleClick(x, y);
}
else if (m_plot.HitTest(x, y))
{
changed = m_plot.OnDoubleClick(x, y);
}
if (changed)
{
InvalidateRect(hwnd, nullptr, TRUE);
}
// 联动:双击图例改名也会选中该信号 ⇒ 同步把列表滚动到它。
ApplyPendingPanelScroll();
return 0;
}
case WM_RBUTTONDOWN:
@@ -1413,18 +1491,35 @@ namespace jdi
return 0;
}
// 信号列表与绘图区动画(「滚动到目标信号」/「分组折起、展开」/「网格滚动到可视区」/
//「回到最上方」按钮的淡入淡出):推进一步并整窗重绘(Render 会在动画结束后停表)。
// ⚠️ 这里**保持整窗失效**(不要改成“只失效动画所在的区域”):面板与绘图区的
// 回送范围由 `Render` 按 `fullFrame` 决定,任何“精确失效”都可能让屏幕上留下
// 未刷新的区域(历史上就因此让滚动条/时间轴不跟手);而整窗失效下动画帧
// 也只有 ~6-8ms(124 网格),完全在 16ms 预算内。
//「回到最上方」按钮的淡入淡出):推进一步并重绘(Render 会在动画结束后停表)。
// ⚠️ **失效范围必须与 `Render` 的绘制/回送判定完全配套**(用户 2026-10-17 报告:
// “滚动过渡动画甚至不如手动拖滚动条”)。旧实现无条件整窗失效 ⇒ `WM_PAINT` 看到
// 更新区域不在绘图区内 ⇒ 把 `m_chromeDirty` 置真 ⇒ **每一帧都重绘标签栏/工具栏/面板**
// (实测 +4.7ms/帧:纯绘图区帧 6.7ms → 11.7ms),动画因此挤不进 15.6ms 的定时器预算
// 而掉帧、位置跳动。
// 现在的判据:
// * 面板动画(滚动到目标信号 / 分组折起展开)**确实**要改面板像素 ⇒ 整窗失效;
// * 只有绘图区动画(网格滚动到可视区 / 回到最上方)⇒ **只失效绘图区**,
// `WM_PAINT` 判定为“更新区域在绘图区内” ⇒ `m_chromeDirty` 保持 false ⇒
// `Render` 只重绘并只回送绘图区(回送仍有无条件 else 兜底,未改 §9.41 的结构)。
// ⚠️ 用**推进之前**的面板动画状态判定:动画结束的那一帧仍整窗失效,
// 保证面板的最终位置被完整重绘并回送。
if (wParam == kScrollTimerId)
{
const bool panelAnimating =
m_panel.ScrollAnimationActive() || m_panel.GroupAnimationActive();
m_panel.AdvanceScrollAnimation();
m_panel.AdvanceGroupAnimations();
m_plot.AdvanceLaneScrollAnimation();
m_plot.AdvanceBackToTopAnimation();
InvalidateRect(hwnd, nullptr, FALSE);
if (panelAnimating)
{
InvalidateRect(hwnd, nullptr, FALSE);
}
else
{
InvalidatePlotRect(hwnd, m_plotRect);
}
return 0;
}
break;