test(tools): rowgeom 逐行几何基线 + 长度不变式按类别判定

新增 `OffscreenChecks -run rowgeom`(§13.6 第 1 项),把 7 个面板的每个矩形的
每个字段导出为文本基线并逐项比对,作为"完整结构合并 / 重写布局算式"的前置护栏。

两层覆盖
- 细节层:6 信号 1 网格、7 面板全开、参数行展开、3 个真实固定点、折起+展开各一份,
  983 条键(-data MF4-Record.MF4 时 1304 条),失败时直接给出字段名+行号+新旧值;
- 规模层:全部信号合并成 1 个网格(MF4 = 566 行),每面板一个 FNV-1a 摘要 + 行数,
  任何一行变动都会改哈希,同时不产生几十万行基线。

负向自检(证明护栏不是空转)
- 篡改基线副本 `mark.markerRowCount=3 → 2` ⇒ `CHANGED … mismatches=1` + FAIL。

顺带修正第 36 轮定得过严的不变式
- `rowRefs` 是重载字段:MARK 面板用它表示"信号分组",真正的条目行在 markerRows,
  且只填 rowNameRects / rowNameCloses,其余 14 列必须为空;
  ⇒ `panels` / `panelscroll` 的"18 列全等"改为按 `PanelStyle::markerGroups` 分流判定,
  失败时打印 15 个计数字段便于定位。
- 导出器/不变式一律**逐列按自身长度**遍历(按 rowRefs.size() 索引其它列会越界,
  这是本轮第一次 0xC0000005 的根因)。

测试用例里的两处修正
- `MERGE_LANES` 之后补 `SynchronizeView(true)`:`SetMode` 只重建 TabConfig 的网格配置,
  `PlotView::m_lanes` 要等下一次布局才刷新;
- 场景 B 重新播固定点:`ShowSignals()` 先 `SetAllVisible(false)`,而"全部不可见 ⇒ 丢弃网格"
  是 RebuildLanes 的既有设计,网格级状态随之清空(不是缺陷),因此不能跨场景依赖。

验证
- make release:零 warning,785,408 B 不变(本轮只动工具/检查与文档)
- make -C Tools clean && make -C Tools:零 warning
- UnitTests:checks=260 failed=0 cases=19
- OffscreenChecks -all:7 检查 / 127 断言全 PASS(原 6 / 101)
- rowgeom -mode compare:默认数据 983+7 键、MF4 1304+7 键,0 差异
- panels -mode compare:7 个面板像素哈希 + 几何逐项不变

文档:Code_Review_Plan §九、Progress 第 37 轮、Report §14、agent_mem 第 37 轮、Tools/README §4.1/4.3/4.4
This commit is contained in:
DeepSeek V4.1 Flash committed 2026-11-04 15:49:25 +08:00
1 parent 221f9d2410
commit 353c553721
6 files changed
+821 -48

No files matched your search

+35 -4
View File
@@ -147,6 +147,9 @@ Tools\OffscreenChecks\Build\OffscreenChecks.exe -list
... -run smoke # 单个检查
... -run panels -mode record # 记录面板像素基线
... -run panels -mode compare # 与基线比对(重构前后 A/B)
... -run rowgeom -mode record # 记录逐行几何基线(默认数据)
... -run rowgeom -mode compare # 逐项比对几何(含每行每列矩形)
... -data "..\TestData\MF4-Record.MF4" -run rowgeom -mode record # 566 行规模基线
... -run frames -frames 12 # 连续抓 12 帧
... -data "..\TestData\MF4-Record.MF4" -run perf
... -out ..\Temp\tools\run1 # 换输出目录(避免覆盖基线)
@@ -171,8 +174,9 @@ Tools\OffscreenChecks\Build\OffscreenChecks.exe -list
| --- | --- | --- |
| `smoke` | 建隐藏窗口 → 加载 → 显示 3 条信号 → 渲染 → 抓帧 → 落盘 `smoke.png` | 客户区/绘图区尺寸合理、绘图区墨迹 > 1000 像素、**同一状态连续两帧逐像素相同**(渲染确定性) |
| `frames` | 沿信号列表逐行移动"鼠标",每帧抓图 + 区域哈希 + 耗时 | 平均帧 < 500ms、帧序列**至少出现一次可见变化**;输出 `frame-XXX.png` 供事后分析 |
| `panels` | 打开全部 7 个实验性面板(含双光标下的「选区截取」) | 每个面板:功能已启用、有尺寸、**必须被绘图区夹住**、**18 个行数组长度全部等于行数**;`-mode record` 写 `panels-baseline.txt`,`-mode compare` 逐面板比对**像素哈希 + 几何** |
| `panelscroll` | 全部信号合并到同一网格后,逐一滚动 7 种面板的内容区 | 可滚动时:滚到底/滚回顶、`scrollOffset` 夹到 `maxScroll`、滑块到达/返回轨道端点、内容区几何不动;不可滚动时不得出现滚动条。另守住 18 个行数组与行数完全一致 |
| `panels` | 打开全部 7 个实验性面板(含双光标下的「选区截取」) | 每个面板:功能已启用、有尺寸、**必须被绘图区夹住**、**行数组与行数形状一致**(按排版类别判定,见 §4.4);`-mode record` 写 `panels-baseline.txt`,`-mode compare` 逐面板比对**像素哈希 + 几何** |
| `panelscroll` | 全部信号合并到同一网格后,逐一滚动 7 种面板的内容区 | 可滚动时:滚到底/滚回顶、`scrollOffset` 夹到 `maxScroll`、滑块到达/返回轨道端点、内容区几何不动;不可滚动时不得出现滚动条。另守住行数组与行数形状一致 |
| `rowgeom` | **逐行几何基线**:7 个面板按「细节 / 规模」两层导出全部矩形并比对 | 细节层(6 信号 1 网格、参数行展开、3 个固定点、折起+展开)导出**每个矩形的每个字段**共 983 条键;规模层(全部信号 1 网格,MF4 = 566 行)导出每面板 FNV-1a 摘要 + 行数。`-mode record` / `-mode compare` 双向比对(MISSING / ADDED / CHANGED) |
| `edgestats` | 3 个阈值下的边沿计数(普通计数)+ 中心轴计数 | 与 `Checks.cpp` 内的**独立参考实现**(朴素逐点)**逐位相等**;同参数重复查询结果稳定 |
| `perf` | 整帧中位数、`EdgePanelLayoutFor` 单次耗时、198 个统计状态的整轮查询耗时 | 整帧 < 500ms、面板几何 < 200µs/次、统计查询 < 20µs/次;数字用于前后对比 |
@@ -191,7 +195,31 @@ OffscreenChecks.exe -run panels -mode compare -out ..\Temp\tools\panel-before
⚠️ **像素哈希是"本机、本 DPI、本数据文件"的基线**,跨机器/跨缩放不可比。
它适合"我改了一处,其它像素有没有动"的**本地 A/B**,不适合入库当黄金图。
### 4.3 输出(全部在 `Temp/tools/`,已 gitignore)
### 4.3 几何基线(`rowgeom`)与像素基线的分工
| | 像素基线(`panels`) | 几何基线(`rowgeom`) |
| --- | --- | --- |
| 回答的问题 | “用户看到的画面变了吗” | “**哪个面板的哪一行哪一列**变了吗” |
| 粒度 | 整个面板区域一个哈希 | 每个矩形的每个字段(`mean.rowNameRects[001]=x,y,w,h`) |
| 定位能力 | 只能告诉你“变了” | 直接给出字段名、行号、新旧值 |
| 规模覆盖 | 固定场景 | 另加“全部信号 1 网格”(MF4 = 566 行)的摘要哈希 |
两者都要跑:像素基线防止“几何没变但画错了”,几何基线防止“画得没错但位置挪了”。
**基线是检测器,不是黄金图**:预期内的几何变化要重新 `-mode record`,并在提交信息里写明原因。
### 4.4 长度不变式为什幺要“按类别”判定
`EdgePanelLayout::rowRefs` 是**重载字段**:
* 列表式面板(均值/中心轴/弱峰/重采样/截取):`rowRefs[i]` = 第 i 行的信号;
* 「自由标记」面板:`rowRefs[i]` = 第 i 个**信号分组**,真正的条目行在 `markerRows`,
且只填 `rowNameRects` / `rowNameCloses`,其余 14 列**必须为空**。
所以判据按 `PanelStyleOf(kind).markerGroups` 分流(见 `Checks.cpp` 里 `panels` 的注释)。
早先“18 列全等”的写法对 MARK 面板是**错的**,只因既有场景恰好未触发才没暴露。
### 4.5 输出(全部在 `Temp/tools/`,已 gitignore)
| 文件 | 内容 |
| --- | --- |
@@ -200,6 +228,8 @@ OffscreenChecks.exe -run panels -mode compare -out ..\Temp\tools\panel-before
| `smoke.png` / `panels.png` / `perf.png` | 关键状态截图 |
| `frame-000.png` … | 连续帧(问题分析用) |
| `panels-baseline.txt` | 面板像素基线(`-mode record` 生成) |
| `rowgeom-baseline.txt` | 逐行几何明细基线(`-mode record` 生成,约 1000 条键) |
| `rowgeom-scale-baseline.txt` | 规模场景的每面板摘要哈希(`-mode record` 生成,7 条键) |
---
@@ -209,7 +239,8 @@ OffscreenChecks.exe -run panels -mode compare -out ..\Temp\tools\panel-before
| --- | --- |
| 任何数据层 / 解析 / 公式 / 变换 | `UnitTests.exe` |
| 绘图、面板、布局、缓存键 | `-run smoke -run panels`(+ 需要时 `-mode compare`) |
| 面板内容滚动 / 面板几何 / 新增面板种类 | `-run panels -run panelscroll`(大文件加 `-data ..\TestData\MF4-Record.MF4`) |
| **面板几何 / 行距 / 内边距 / 折起规则 / 布局算式** | `-run panels -mode compare` + `-run rowgeom -mode compare`(改动前先 `-mode record`) |
| 面板内容滚动 / 新增面板种类 | `-run panels -run panelscroll`(大文件加 `-data ..\TestData\MF4-Record.MF4`) |
| 边沿统计 / 中心轴 / 扫描预算 | `-run edgestats -run perf` |
| 性能相关 | `-run perf`(前后各跑一次,比 `us/query` 与 `median ms`) |
| 完全不确定影响面 | `-all`(约几秒到几十秒)+ `UnitTests.exe` |