面板限高与滚动改造 + 边沿统计不再卡在“统计中…” + 组名折起真正收起

一、浮动面板(用户报告:下方空白、滚动时画到面板外、未改造的面板让一切交互卡顿)
1. ⭐ 根因:旧「逐条目」排布没有高度上限 —— 288 条信号时面板 13010px(绘图区 846px)
   ⇒ PanelRectInsideLayer 恒假 ⇒ 面板像素缓存一次都存不进去 ⇒ 每帧整块重画。
   现在 5 个面板全部限高 + 可滚动,旧的 itemized 布局/绘制/交互整体删除。
2. 无按钮面板(中心轴边沿计数)不再预留「按钮行 + pad」⇒ 底部空白 55px → 8.8px(1 个 pad)。
3. ⚠️ 统计文字改用 UiTextClipped:GDI 文本不受 GDI+ SetClip 约束,滚动时画到标题栏之外。
4. 「弱峰调整」改造为与均值滤波同款的分组列表(条目栏 + 右侧复选框 + 展开后的两行滑块
   + 复位按钮 + 底部「构建为新信号」);新增 Lane::weakPeakScroll / SetLaneWeakPeakScroll,
   WeakPeakItem 增加 collapsed。
5. 「边沿计数」保留两行排版,但内容区同样限高、可滚动(新增 Lane::edgeCountScroll)。
6. 鼠标指针:列表式面板的条目栏**只有已勾选**才显示手型(未勾选点击无效果,保持箭头);
   参数行控件只在参数行展开时给手型 / 左右箭头。
7. 内部标志 m_meanDragCenter / m_editMeanCenter(bool)改为 m_meanDragKind / m_editMeanKind
   (EdgePanelKind),以便 MEAN / CENTER / PEAK / COUNT 共用同一套滚动与滑块拖拽。

二、边沿统计“全部统计中…且永不更新”(288 信号合并到同一网格)
1. ⭐ 根因:累积状态上限固定 64 < 信号数 288 ⇒ 新建状态总淘汰未完成的 ⇒ 没有任何状态能扫完。
   新增 EdgeScanStateLimit() = max(64, 全部信号数 × 2 + 16)。
2. 去掉每状态 60000 的固定配额下限(状态一多就把整帧预算耗光),改为 budget / 未完成状态数;
   分母只数未完成状态,短信号扫完后立刻把预算让给长信号。

三、组名折起后“三角已收起但信号行仍在、仍可点选”
1. ⭐ 根因:RequestEdgeScanFrame() 在每个未完成状态里各 Post 一次(单帧最多 288 次),
   把优先级最低的 WM_TIMER 饿死 ⇒ AdvanceGroupAnimations 永不执行 ⇒ 折起动画永不结束。
   修法:每帧最多 Post 一次(m_edgeScanRequestFrame)。
2. 另一道保险:SignalPanel::EnsureRows() 开头推进一次动画 —— 动画进度只依赖挂钟时间,
   不依赖消息能否按时到达。

四、验证(离屏探针 .preflight/p20,全部 SW_HIDE + 合成消息,不占用鼠标/键盘/屏幕)
  p20.exe  9/9:面板限高且像素缓存真的写进去了、288 个统计全部 complete、中心轴面板底部留白、
               组名折起后行集合无该组信号、两组都折起后 signalRows == 0(真实消息 + WM_TIMER)。
  p20c.exe 11/11:弱峰面板限高/可滚动/缓存命中、展开与收起、滚动生效、条目栏手型规则、
               中心轴滚动时标题栏 0 像素变化(A/B 关掉裁剪 ⇒ 805px)、288 信号空闲帧 2.6ms。
  make release / make debug 零 warning。
This commit is contained in:
DeepSeek V4.1 Flash committed 2026-11-02 10:00:24 +08:00
1 parent 334015c1a9
commit 7a6fcd68e2
9 files changed
+914 -795

No files matched your search

+125
View File
@@ -9061,3 +9061,128 @@ make release / make debug 零 warning
(`RowPart::CHECK`)—— 想测折叠必须点名称区。
* ⚠️ 「行集合签名是否稳定」不能作为断言:动画**结束那一帧**签名必然变化一次(动画标记位
从 5 → 0),这是正确的。要断言的是"动画结束后 `m_rowsSignature == RowsSignature()`"。
---
## 2026-11-02 第二十轮(本机):面板限高/裁剪/弱峰列表化 + 边沿统计"统计中…"根治 + 组名折起无法完成
> 用户复现的四组问题(`VERY_BIG_DATA.CSV` + `MF4-Record.MF4` 全选后合并成同一网格):
> ①「中心轴边沿计数」面板下方有**无意义空白**;② 勾选全部信号后滚动,**绘制超出面板**;
> ③ 启用「边沿计数」后来自 VERY_BIG_DATA 的信号**永远"统计中…"**;
> ④ 折起分组后**三角已收起但下属信号行仍在、仍可点选**(点第二个组时第一个组才真正收起);
> ⑤ 启用**尚未改造**的实验性功能后一切交互(光标/动画)都很卡。
### 1. ⭐⭐ 根本原因:**面板高度没有上限 ⇒ 面板像素缓存永远存不进去**
`itemized`(旧式逐条目)排布按「每勾选信号 2 行 + 每行 22 逻辑像素」累加,288 条信号时
面板高达 **13010px**(绘图区只有 846px)。`PanelRectInsideLayer()` 因此恒为假 ⇒
**面板像素缓存一次都没写进去** ⇒ 每帧整块重画(288 行 × 十来个 GDI+ 原语)。
「边沿计数」与(改造前的)「弱峰调整」都是这个排布,所以"只要开这两个功能就卡"。
**硬规则(新)**:**任何**实验性功能面板的高度都必须被绘图区夹住:
`budget = plotH - (标题 + pad + 底部区)`,`view = min(内容高, maxContent)`,
内容超出时右侧出现**滚动条**。`itemized` 排布已**整体删除**(布局 + 绘制 + 交互),
现在 5 个面板全部是「有高度上限 + 可滚动」的实现。
### 2. 四个面板的滚动偏移(`Lane`)
| 面板 | 偏移字段 | setter |
| --- | --- | --- |
| 均值滤波 | `meanFilterScroll` | `SetLaneMeanFilterScroll` |
| 中心轴边沿计数 | `edgeCenterScroll` | `SetLaneEdgeCenterScroll` |
| **弱峰调整(新)** | **`weakPeakScroll`** | **`SetLaneWeakPeakScroll`** |
| **边沿计数(新)** | **`edgeCountScroll`** | **`SetLaneEdgeCountScroll`** |
前三个是**列表式**(信号色叠底条目栏 + 右侧复选框 + 展开后的两行滑块);
「边沿计数」保留旧式两行排版,但同样有 `layout.content`(裁剪 + 滚动条 + 滚轮 / 拖动)。
### 3. ⚠️⚠️ 无按钮面板的底部留白(用户指出的"无意义空白")
旧高度算式**无条件**按 `标题 + pad + 内容 + pad + 按钮 + pad` 预留,而 `viewHeight`
又无条件减去 `kEdgePanelButtonHeight`;「中心轴边沿计数」没有「构建为新信号」按钮 ⇒
内容区下方白留了 **按钮高度 30 + pad 7 = 46 逻辑像素**(实测 55px @125%)。
现改为:
```
footer = hasButton ? (buttonHeight + pad) : 0
height = 标题 + pad + view + pad + footer
viewHeight = height - 标题 - 2*pad - footer // = view
budget = plotH - (标题 + pad + pad + footer)
```
### 4. ⚠️⚠️ "绘制超出面板" = **GDI 文字不受 GDI+ 裁剪约束**
`paintCounts`(上升沿 / 下降沿)用的是 `UiText`(`DrawTextW`)⇒ `g.SetClip(layout.content)`
对它**无效**,滚动时数字会画到标题栏 / 面板之外。现统一改 `UiTextClipped(..., layout.content)`。
**A/B 已证**:改回 `UiText` 后,标题栏区域像素差 **805px**(x1..241、y12..24)。
**硬规则**:凡"可滚动内容区"里的文字一律 `UiTextClipped`(含名称、标签、数值、统计、提示)。
`UiFillRoundRect` 的 GDI 快路径(≥20000px² 且不透明)同样不认裁剪 —— 条目栏宽 ≈ 420px,
面积 < 20000,目前安全;**宽度再放大时要复核**。
### 5. ⭐⭐ 「统计中…」永不更新 = **状态上限 64 < 信号数** + 预算分摊不公平
`EdgeScanFor` 的累积状态上限原本固定 `kEdgeScanStateLimit = 64`,
而面板会在同一帧为**每条信号**各创建一个状态(288 > 64)⇒ 新建时总淘汰队首(未完成的)
⇒ **没有任何状态能扫完** ⇒ 永远"统计中…"。
- 新增 `PlotView::EdgeScanStateLimit()`:`max(64, 信号总数 × 2 + 16)`(每信号最多两类状态)。
- **每帧配额去掉固定下限** `kEdgeScanPairsPerStateMin(60000)`:状态一多,下限会让最前面的
几个状态吃掉整帧预算,其余永远 0。现为 `budget / share`(至少 1),**`share` 只数「未完成」
状态**(否则完成状态仍占分母,长信号每帧只分到 1/300,实测要 20 分钟)。
- 实测:288 条信号(含 5 条 25.8M 采样)——短信号 1~2 帧内完成,长信号随后独享整帧预算。
### 6. ⭐⭐ 组名折起"三角已收起、行还在" = **`WM_TIMER` 被饿死**
`RequestEdgeScanFrame()` 原本在**每个未完成状态**里各 Post 一次
⇒ 288 条信号时单帧 **288 次 `PostMessage`** + 高帧率重绘 ⇒ 优先级更低的 `WM_TIMER`
拿不到执行机会 ⇒ `AdvanceGroupAnimations()` 永远不跑 ⇒ 折起动画永不结束 ⇒
行集合里的信号行永远不会被移除(三角却是从 `IsGroupCollapsed()` 直接画的,所以"看起来收起了")。
点击下一个组时,那次点击触发的绘制里又推进一次动画,于是"上一组这时才真正收起"。
修法两条(都要):
1. `RequestEdgeScanFrame()` **每帧最多一次**(`m_edgeScanRequestFrame == m_edgeScanFrame` 直接返回);
2. `SignalPanel::EnsureRows()` 开头:`if (!m_groupAnims.empty()) AdvanceGroupAnimations();`
—— 动画进度只依赖挂钟时间,**不应依赖消息能否按时到达**。
### 7. 「弱峰调整」改为列表式(与均值滤波同款)
- `WeakPeakItem` 新增 `collapsed`;面板 = 条目栏(三角 + 名称 + 复选框)+ 两行滑块
(调整幅度 ±400% / 迭代次数 1…自定上限)+ 右侧复位 + 底部「构建为新信号」。
- 交互全部复用 `MEAN_*` 拖拽模式(滚动用 `MEAN_SCROLL`),滑块拖动仍用
`PEAK_LEVEL / PEAK_ITERATIONS`,上限编辑仍用 `PEAK_ITERATIONS_MAX + m_peakEditCurrentMax`。
- ⚠️ `m_meanDragCenter`(bool) / `m_editMeanCenter`(bool) → **`m_meanDragKind` / `m_editMeanKind`**
(`EdgePanelKind`):现在有 MEAN / CENTER / PEAK / COUNT 四个写入目标。
- **鼠标指针**:列表式面板的条目栏只有**已勾选**时才显示手型;未勾选时保持箭头
(此时点击没有任何效果,用户 2026-11-02 明确要求)。
参数行的数值 / 复位 / 轨道也只在参数行展开(`rowLevelTracks.Width > 0`)时才给手型/左右箭头。
### 8. 验证(`.preflight/p20`,离屏 `SW_HIDE` + 合成消息)
合成数据:5 列 + 283 列两个 CSV → 两组 → 全部显示 → 合并成 1 个网格(288 信号)。
```
p20.exe 9/9
合并/信号数;COUNT 面板高 375 ≤ 绘图区 846 且 **像素缓存真的写进去了**(slot.key != 0);
288 个边沿统计全部 complete(states=288);
CENTER 面板底部留白 8.8px(= 1 个 pad,修前 55px);
组名折起后行集合无该组信号行;两组都折起后 signalRows == 0(真实 WM_LBUTTONDOWN/UP +
WM_TIMER(kScrollTimerId=3) + Render)
p20c.exe 11/11
PEAK 面板限高/可滚动/缓存命中;勾选展开两行滑块、收起后内容缩小;滚动偏移生效;
MEAN 条目栏:已勾选→手型、未勾选→箭头;
**CENTER 滚动到统计行越过内容区顶部时,标题栏区域 0 像素变化**(A/B 关掉裁剪 ⇒ 805px);
288 信号 + 面板打开时空闲帧 avg 2.6ms
make release / make debug 零 warning
```
### 9. 踩坑
- ⚠️ PowerShell 5.1 按 ANSI 读 `.ps1` ⇒ **脚本里不能出现中文**(连 `'条目栏'` 这种
单引号短串都会把解析器带崩,报"字符串缺少终止符")。断言数据放 UTF-8 文件、脚本纯 ASCII。
- ⚠️ `SetLaneMeanFilter/SetLaneWeakPeak/SetLaneEdgeCenterCount(lane, true)` 会**自动勾选
该网格第一个信号** ⇒ 断言"面板初始全未勾选"必然失败;要测未勾选行请用第 2 行。
- ⚠️ 面板圆角外侧的 1~2px 是"面板 AA 边缘 vs 缓存贴图(圆角裁剪)"的合法差异,
取像素做 A/B 时要**避开圆角 8px**,否则会误报 7 个像素。
- ⚠️ `git diff --numstat` 出现整文件级别的增删时要先怀疑行尾/BOM 被 PowerShell 改掉;
本轮所有写回都用 `[System.IO.File]::WriteAllText(... UTF8Encoding($false))`。