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

一、滚动过渡动画卡顿(用户报告:甚至不如手动拖滚动条)—— 三处独立根因
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

+161
View File
@@ -2627,6 +2627,123 @@ else if (panelOnly) { /* 只回送“本帧重绘过的区域并集” */ }
---
### 9.43 滚动过渡动画根治 + 快速连点跟手 + 分组名单击展开 + 光标吸附同源 + 按钮淡出对齐 + 滚动位置复位(本轮)
> 用户 2026-10-17 第七轮(本机)。六项需求:
> ① 鼠标拖光标时**鼠标下方网格内**按「进入自动点线」**同一判定**的数据点吸附(用户改过显示样式也吸附);
> ② 滚动过渡动画(点选最后一个信号 / 「回到最上方」)卡顿,甚至不如手拖滚动条;
> ③ 「回到最上方」按钮的**渐变消失**要和「选区放大浮动提示」一致;
> ④ 「组X」快速点击不跟手 + 分组**名称**恢复单击展开收起(只有**快速双击**才进入改名);
> ⑤ 所有复选框 / 「全部显示隐藏」/ 「绝对值」快速点击不跟手;
> ⑥ 「有过需滚动条的网格 → 没有信号 → 又出现需滚动条数量的网格」时滚动条**回到最上方**。
> 离屏验证:`gridanim` **93/93**(含真实消息循环帧距 / 逐帧渲染耗时对比);`verify` 49/49、
> `panelanim` **158/158**(按新规则更新 8 条断言)、`datacheck` 69/69、`edgecheck` 46/46、`errcheck` 19/19;
> `make release` / `make debug` 零警告。
#### 一、滚动过渡动画卡顿 —— **三处独立根因**(用「与手动拖滚动条对比」定位)
新增评估手段(用户要求「找出精准评估动画流畅性的方式」):**同一个二进制内对比**
「手动拖滚动条逐帧渲染」与「动画帧渲染」,并用**真实消息循环**统计帧距/逐帧耗时。
1. ⚠️⚠️ **`WM_TIMER` 的失效范围把绘图区动画变成「整窗重绘 + 整窗回送」**:
旧实现无条件 `InvalidateRect(hwnd, nullptr, FALSE)` ⇒ `WM_PAINT` 看到更新区域**不在绘图区内**
⇒ 把 `m_chromeDirty` 置真 ⇒ **每帧重绘标签栏/工具栏/面板**。实测纯绘图区帧 **4.4ms → 8.6ms(带整窗装饰)**,
「回到最上方」动画帧 **13.6ms → 3.5ms**(修好后)。
✓ 修法:用**推进之前**的面板动画状态决定失效范围 ——
* 面板动画(滚动到目标信号 / 分组折起展开)**确实**要改面板像素 ⇒ 整窗失效;
* 只有绘图区动画(网格滚动到可视区 / 回到最上方)⇒ **只失效绘图区**
(`WM_PAINT` 判定为“更新区域在绘图区内” ⇒ `m_chromeDirty` 保持 false ⇒ 只重绘、只回送绘图区)。
⚠️ 这不违反 §9.41:回送分支仍是 `if (fullFrame) {整窗} else {绘图区}`,**未改成“按更新区域决定画什么”**。
2. ⚠️⚠️ **图层预热不足 ⇒ 快速滚动时每帧要靠「网格底衬兜底」重画十几份网格**:
「全部信号各自一块网格」时待重画队列有上百个**离屏**网格。上一轮为避免高频重绘饿死动画定时器,
把「是否再请求一帧」限定为「还有**可视**网格待补」⇒ 离屏网格长期不建图层;
一旦用户点最后一个信号(长距离滚动动画)扫进这片区域,**每一帧都有十几个网格没有图层像素**,
只能就地补画网格底衬 —— 实测 **+8ms/帧**(`BlitWideBuffer()` 只画网格不打波形,代价 ~0.5ms/网格)。
✓ 修法(两步):
* `LaneVisuallyRelevant(laneIndex, prefetchScreens)`:新增**预取带**(±1 屏高),
`AnyVisibleLanePending()` 用它 ⇒ 静置时把**附近**网格的图层先建好;
* `PrefetchStepAllowed()`(`kPrefetchMinIntervalMs = 32`):队列里只剩离屏网格时,
以约 31 次/秒**限速**继续请求重绘 ⇒ 整批图层在静置期间(130 个网格约 4 秒)逐步建好,
同时不会把动画的 `WM_TIMER`(15.5ms 周期)挤出预算。
**实测(相同二进制、相同起始状态)**:`E4b lane-only+big` 帧 **13.9ms → 5.3ms**;
`E4 signal-click+big` **18.3ms → 10.3ms**。
3. 「网格滚动到可视区 / 回到最上方」每帧都在改 `LaneScroll()`,而它原本参与「宽基底可用性」判据
⇒ 每一帧都退化成整幅重绘(上一轮已修:`SceneContentHash(..., includeLaneScroll=false)`)。
**最终实测(124 网格;手动拖滚动条 = 参照)**:
| 场景 | 修前 | 修后 |
| --- | --- | --- |
| 手动拖滚动条(参照) | 6.6ms | **4.2ms** |
| 「回到最上方」动画 | 13.6ms / 帧距 22.6ms | **3.5ms / 帧距 15.6ms** |
| 点选最后信号(面板+绘图区,含 VERY_BIG_DATA)| 18.6ms | **10.3ms** |
| 点选最后信号(仅绘图区,含 VERY_BIG_DATA)| 14.8ms | **5.3ms** |
⚠️ 「点选最后信号」的剩余 10.3ms 里约 4.5ms 是**面板动画必须重绘面板+工具栏**的固有代价
(`E1d` 直接驱动同一套帧 ≈ 7.8ms,说明绘制本身在预算内,残余来自消息循环/调度)。
#### 二、光标吸附改用「自动点线」的**同一判定**(用户 2026-10-17 修订)
- `AutoPointLineNeeded` → 改名 **`MinorCellSparse`**(中性名,**同一判定两处用途**):
「放大自动切点线」(`UpdateAutoScatterStyles`)与「光标吸附」(`SnapTimeToSample`)。
- 判据:任一小格内有效数据点 < `kSparseMinPointsPerCell`(=5);
⚠️ 判定**完全不看绘制方式** ⇒ **即使用户手动改过显示样式也照样吸附**(用户明确要求)。
- 网格内**既有符合又有不符合**的信号时,只有**符合**的那条的数据点进入比较;
网格内**没有**符合条件的信号时,光标**完全自由移动**。
- 删除旧的整屏判据 `SignalSnapEnabled` 与 `kSnapVisibleCellsPerPoint`(成为死代码)。
- ⚠️ §9.30 记的「自动点线 / 吸附同源」在本轮**重新成立**(判据换成了小格判据)。
#### 三、「回到最上方」按钮淡出与「浮动提示」一致(用户要求)
- `kBackToTopFadeOutMs = kHintFadeMs`(500ms)+ **线性** alpha;到达顶部**立即开始**淡出(不停留)。
- 淡入保留原来的 `kBackToTopFadeInMs`(170ms,ease-out)—— 用户只要求淡出对齐。
- 实测:50% 时刻 alpha = **0.500**(线性),旧 ease-out 会是 0.875。
#### 四、快速连点跟手:`WM_LBUTTONDBLCLK` 必须当作一次「按下」(根因,覆盖全部控件)
- ⚠️⚠️ 窗口类带 `CS_DBLCLKS`:**快速连点同一个控件**时,第二次(及之后)按下会被 Windows 合成为
`WM_LBUTTONDBLCLK`。旧实现只给**工具栏**打了补丁(§9.32),其余控件(复选框 /「全部显示隐藏」/
「绝对值」/「组X」/ 折叠三角 / ✕ / GX 徽标 / 浮动面板上的复选框…)的那一次按下**完全丢失**
⇒ 用户看到的正是“连点好几下、状态只切换了几次”。
- ✓ 修法:组件各自提供 `DoubleClickIsPlainPress(x, y)`(**该落点有没有双击语义**),
宿主 `WM_LBUTTONDBLCLK` 据此决定走 `OnLButtonDown`(当作按下)还是 `OnDoubleClick`:
* `PlotView`:浮动面板(含复选框/滑块/复位/✕/标题栏)、「回到最上方」按钮、绘图区滚动条、GX 徽标 ⇒ 按下;
时间光标线 / 读数标签 / 图例条目名 / 绘图区空白 ⇒ 保留双击;
* `SignalPanel`:样式区数值框、「目标」行名称与 GX 标签、行**名称区域** ⇒ 保留双击;
**其余全部**(含列表之外的样式区按钮/筛选框/空白)⇒ 按下;
* 弹出菜单 / 拾色器打开时,`WM_LBUTTONDBLCLK` 也按各自 `OnLButtonDown` 走。
#### 五、分组行:名称恢复单击展开收起 + 只有**快速双击**进入改名(用户 2026-10-17)
- `OnLButtonDown` 的分组分支不再排除 `RowPart::BODY`:**三角 /「组X」/ 名称本体**统一折叠展开。
- `OnDoubleClick` 的名称区域:**只有快速双击**才进入改名 —— 阈值 `kGroupRenameDoubleClickMs = 300`
(比 Windows 的 `GetDoubleClickTime()`(通常 500ms)**更紧**,用户要求“降低灵敏度”);
超出阈值的“慢双击”只当**又一次单击**(再切一次折叠);快速双击则**先撤销**按下那一次对折叠状态的
切换(`m_groupNameClickWasCollapsed`)再进入改名,保证进编辑时折叠状态与点击前一致。
⚠️ 需要记录「最近一次名称点击的时刻 / 行 / 点击前折叠状态」三个成员。
#### 六、信号重现时网格滚动回到最上方(用户要求)
- ⚠️ 这一整类情形(“有几屏网格 → 没有任何信号显示 → 又出现几屏网格”)必须从最上方开始看。
旧实现保留了上一次的 `Tab().LaneScroll()` ⇒ 重新显示后**停在与取消显示时相同的滚动位置**。
- ✓ `Application::SynchronizeView` 的 `else if (!tab.ViewInitialized())`(= 首次出现可见信号)分支里
在 `FitAll()` **之后** `tab.LaneScroll() = 0.0f`(`FitAll` 本身不动滚动量,见 §11.2 第 112 条)。
#### 七、⚠️ 本轮方法论教训
1. ⚠️⚠️ **绝对不要用 PowerShell `Get-Content -Raw | Set-Content -Encoding UTF8` 改源码**(本轮又踩了一次):
PS 5.1 按 ANSI/GBK 解码 UTF-8 ⇒ 中文注释整份变乱码 + 加 BOM。
✓ **补救**:乱码是**无损可逆**的 —— 读成 UTF-8 字符串再按 `GetEncoding(936).GetBytes()` 写回,
即可还原原始 UTF-8 字节(再删掉 BOM 变成的 `?` 首字节)。✅ 但**正确做法**是:
用**编辑工具**改源码,或用 `[System.IO.File]::ReadAllText/WirteAllText(..., UTF8Encoding($false))`。
2. **动画耗时必须在同一二进制、同一起始状态下对比**:早期用「改代码 + 重编」做 A/B,
结果被两次运行之间**不同的预热状态**污染,得出「底衬兜底 +8ms」这种误导结论;
改成**运行时开关**(环境变量)在**同一次运行**里对比才可靠。
3. **对比参照物非常重要**:把「手动拖滚动条」当作流畅度的基准线,
才能一眼看出动画帧「贵在哪」(整窗重绘 vs 面板固有代价 vs 预热不足)。
---
## 10. 变更日志(本文件维护记录)
| 日期 | 设备/模型 | 摘要 |
@@ -2687,6 +2804,8 @@ else if (panelOnly) { /* 只回送“本帧重绘过的区域并集” */ }
| 2026-10-17 | DeepSeek-V4.1-Flash(**本机**) | **网格底衬永不空白 + 动画流畅性根治 + 光标随首个信号重现 + 「放大自动点线」新判据(详见 §9.42)**。① **网格底衬消失**:旧跨度图层的 `StretchBlt` 占位拉伸在深度放大时把一小段像素放大几十倍 ⇒ 整块网格变成**无网格的模糊色块**;叠加待重画队列**按下标顺序**消费(可视网格要等上百帧)⇒「消失 + 要等一会才刷新好」。修法:**删除占位拉伸**,改为对拿不到像素的网格**就地补画网格底衬**(`PaintLaneGridBacking` + `m_renderGridOnly`,代价极低可每帧执行),队列改**可视网格优先**,结算判据改 `LanesVisiblyReadyForView()`。② **动画卡顿(两处根因)**:**(a)**「`m_layerRebuildPending` 非空就每帧请求再送一帧」在上百个网格时挤出 100+ 次重绘、把优先级更低的 `WM_TIMER` **饿死**(实测 `frames=124/timerTicks=1`)⇒ 改用 `AnyVisibleLanePending()`(`frames=25/timerTicks=16`);**(b)** `SetTimer(...,16,...)` 被系统 tick **量化成 ~26ms(≈38fps)** ⇒ 周期改 **10ms**(实测 ~15.5ms ≈ 64fps,真实消息循环帧距 ~25ms → ~16ms);**(c)** 网格滚动量原本参与「宽基底可用性」判据 ⇒ 滚动动画每帧整幅重绘 ⇒ 新增 `SceneContentHash(..., includeLaneScroll)`(判据不计、场景缓存仍计),单帧 **18.36ms → 1.19ms**。③ **光标随首个信号重现**:新增 `ResetCursorPlacementOnFreshContent()`(与 `CycleCursorMode()` 首次启用同源),接入 `SynchronizeView` 的「首次出现可见信号」分支。④ **「放大自动点线」新判据**:任一小格内数据点 < 5 即切换(`AutoPointLineNeeded`;二分/鸽巢封顶);⚠️ 光标吸附仍用旧整屏判据(用户要求"其它规则不变")。验证:新增 `.preflight/perf/gridanim_main.cpp` **55/55**(含真实消息循环帧距统计、光标重现、点线规则);`verify` 49/49、`panelanim` 156/156、`datacheck` 69/69、`edgecheck` 46/46、`errcheck` 19/19;`make release` 零警告。⚠️ 教训:`cardBg == plotBg` ⇒ 颜色判据色盲;后备缓冲区持久 ⇒ 「看不到空白」不能证明没问题;网格分界线会让「有锐边」判据假绿(须要求同列多行)。§11.2 新增第 117–124 条。 |
| 2026-10-17 | DeepSeek-V4.1-Flash(**本机**) | **滚动过渡动画根治 + 快速连点跟手 + 分组名单击展开 + 光标吸附同源 + 按钮淡出对齐 + 滚动位置复位(详见 §9.43)**。① **滚动动画卡顿(三处根因)**:**(a)** `WM_TIMER(kScrollTimerId)` 无条件整窗失效 ⇒ `WM_PAINT` 把 `m_chromeDirty` 置真 ⇒ **每帧重绘标签栏/工具栏/面板**(绘图区动画帧 13.6→**3.5ms**;手动拖滚动条参照 4.2ms);**(b)** 图层预热不足 ⇒ 长距离滚动扫进未建图层的网格、每帧靠底衬重画十几份网格(+8ms/帧)⇒ 可视判定加 **±1 屏预取带** + `PrefetchStepAllowed()` 限速后台预热(`lane-only+VERY_BIG_DATA` 13.9→**5.3ms**);**(c)** 网格滚动量参与宽基底可用性判据(上一轮已修)。② **光标吸附改用「自动点线」同一判定**:`AutoPointLineNeeded` 改名 `MinorCellSparse`,吸附与自动点线共用;判定**不看绘制方式** ⇒ 用户改过样式也吸附;删除旧整屏判据 `SignalSnapEnabled`。③ **「回到最上方」按钮淡出与浮动提示一致**:`kBackToTopFadeOutMs = kHintFadeMs`(500ms) + **线性** alpha + 到达顶部立即淡出(实测 50% 时刻 alpha=0.500,旧 ease-out 为 0.875)。④ **快速连点跟手**:`CS_DBLCLKS` 会把快速连点的第 2 次按下合成为 `WM_LBUTTONDBLCLK`,旧实现只给工具栏打补丁 ⇒ 复选框/全部显示隐藏/绝对值/组X/折叠三角/浮动面板复选框的按下**丢失**;新增 `DoubleClickIsPlainPress(x,y)`(`PlotView` + `SignalPanel`),宿主据此选择走 `OnLButtonDown`。⑤ **分组行:名称恢复单击展开收起**(推翻 §9.37 ①),只有**快速双击**(`kGroupRenameDoubleClickMs=300`,比系统 500ms 紧)才进入改名,且进入前**撤销**按下那次的折叠切换。⑥ **信号重现时网格滚动回到最上方**(`SynchronizeView` 首次出现可见信号分支里 `LaneScroll()=0`)。验证:`gridanim` **93/93**(真实消息循环帧距 + 逐帧耗时对比 + 行为断言)、`verify` 49/49、`panelanim` **158/158**(按新规则更新 8 条断言)、`datacheck` 69/69、`edgecheck` 46/46、`errcheck` 19/19;`make release`/`make debug` 零警告。§11.2 新增第 125–133 条。
## 11. 跨设备协作须知(起源设备视角)
> **本文件是唯一权威记忆**。所有设备 / Agent 共用它,**不要再新建 `agent_mem_origin.md` 等旁支记忆文件**。
@@ -3295,6 +3414,48 @@ else if (panelOnly) { /* 只回送“本帧重绘过的区域并集” */ }
判断网格是否画出必须用结构判据(竖直锐边),且必须要求「同一列多行都有锐变」(否则网格**分界线**会让
每列都像有锐边)。⚠️ **后备缓冲区是持久的**:宽基底路径只把绘图区填成 plotBg(==cardBg),
本帧没被覆盖的网格**保留上一帧像素** ⇒ 探针「看不到空白」不能证明没问题。
125. ⚠️⚠️ **动画定时器的失效范围必须与 `Render` 的绘制/回送判定配套**(用户 2026-10-17 报告:
“滚动过渡动画甚至不如手动拖滚动条”):`WM_TIMER(kScrollTimerId)` **不能无条件整窗失效** ——
整窗更新区域会让 `WM_PAINT` 把 `m_chromeDirty` 置真 ⇒ **每帧重绘标签栏/工具栏/面板**。
✓ 用**推进之前**的面板动画状态判定:面板动画 ⇒ `InvalidateRect(nullptr)`;
只有绘图区动画 ⇒ `InvalidatePlotRect`(只重绘、只回送绘图区)。
⚠️ 这不违反第 109 条:回送分支仍是 `if (fullFrame) {整窗} else {绘图区}`,**没有**按更新区域决定画什么。
实测:绘图区动画帧 **13.6ms → 3.5ms**(手动拖滚动条参照 4.2ms)。
126. ⚠️⚠️ **「是否再请求一帧去补图层」必须兼顾两个极端**(`AnyVisibleLanePending` + `PrefetchStepAllowed`):
* 只看「严格可见」⇒ 离屏网格长期不建图层,长距离滚动动画扫进去时**每帧十几个网格没有图层像素**,
只能靠底衬兜底重画网格(实测 **+8ms/帧**,动画明显卡);因此可视判定要带 **±1 屏预取带**
(`LaneVisuallyRelevant(laneIndex, prefetchScreens)`);
* 只看「队列非空」⇒ 上百个离屏网格挤出一秒以上的高频重绘,把 `WM_TIMER`(优先级低于投递消息)**饿死**;
因此只剩离屏网格时要**限速**(`PrefetchStepAllowed()`,`kPrefetchMinIntervalMs = 32` ⇒ 约 31 次/秒)。
实测:`lane-only + VERY_BIG_DATA` 帧 **13.9ms → 5.3ms**。
127. ⚠️ **「宽基底快路径」对拿不到图层像素的网格要就地补画网格底衬**(见第 117 条),
但**底衬不是免费的**:约 **0.5ms/网格**(`PaintLane` 的网格部分:y/时间刻度 + 3 次批量 GDI)。
⇒ 必须靠第 126 条的预热把「可见但没建过图层的网格」数量压到接近 0。
128. ⚠️⚠️ **快速连点会丢一次按下**(`CS_DBLCLKS`):第二次及之后的快速按下被合成为 `WM_LBUTTONDBLCLK`。
**所有没有双击语义的控件**都必须把它当作一次「按下」(不只是工具栏,见第 84 条):
✓ 组件各自实现 `DoubleClickIsPlainPress(x, y)`,宿主据此在 `WM_LBUTTONDBLCLK` 里选择走
`OnLButtonDown` 还是 `OnDoubleClick`。覆盖:`PlotView`(浮动面板及其复选框/滑块/复位/✕/标题栏、
「回到最上方」按钮、绘图区滚动条、GX 徽标)与 `SignalPanel`(复选框、折叠三角、「组X」、✕、
GX 徽标、全部显示隐藏、绝对值、色块、绘制方式、滑块、调整按钮、筛选框、样式区/空白区域)。
⚠️ `SignalPanel::DoubleClickIsPlainPress` 对**列表之外**的落点必须返回 **true**(样式区按钮只有单击语义);
返回 false 会让连点被 `OnDoubleClick` 吃掉。
129. ⚠️ **分组行点击(用户 2026-10-17 修订,推翻 §9.37 ①)**:
**三角 / 「组X」标签 / 名称本体**都折叠展开分组(不再“点名称不折起”);
只有**快速双击**名称才进入改名,阈值 `kGroupRenameDoubleClickMs = 300`(**比 `GetDoubleClickTime()` 紧**),
超阈值的“慢双击”只当**又一次单击**;快速双击进入改名时要**先撤销**按下那一次造成的折叠切换
(`m_groupNameClickWasCollapsed`),保证进编辑时折叠状态与点击前一致。
130. ⚠️⚠️ **滚动过渡动画的三个独立根因**(排查时用「手动拖滚动条」当参照物):
① 定时器失效范围(第 125 条);② 图层预热不足(第 126 条);
③ 网格滚动量参与“宽基底可用性”判据(第 121 条)。
131. ⚠️ **「全部信号各自一块网格」时待重画队列天然有上百项**:任何「队列非空就做 X」的逻辑都要
重新审视(重绘请求、预热、结算判据…),否则要么饿死动画定时器、要么让画面长期停在兜底状态。
132. ⚠️⚠️ **不要用 PowerShell `Get-Content -Raw | Set-Content -Encoding UTF8` 改源码**(本轮再次踩到):
PS 5.1 按 ANSI/GBK 解码 UTF-8 ⇒ 中文注释整份变乱码 + BOM。✓ 乱码**无损可逆**:
读成 UTF-8 字符串 → `GetEncoding(936).GetBytes()` 写回即可还原(再删掉 BOM 变成的 `?` 首字节)。
正确做法:用编辑工具,或 `[System.IO.File]::ReadAllText/WriteAllText(..., UTF8Encoding($false))`。
133. ⚠️ **动画性能 A/B 必须在同一二进制、同一起始状态下对比**:靠“改代码重编”做 A/B 会被
**两次运行之间不同的预热状态**污染(本轮因此得出过“底衬兜底 +8ms”的误导结论);
应加**运行时开关**在**同一次运行**内切换。
### 11.3 离屏验证方法学(本机偏好,强烈建议沿用)