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:
1 parent
221f9d2410
commit
353c553721
6 files changed
+811
-38
No files matched your search
@@ -271,3 +271,39 @@
|
|||||||
|
|
||||||
本轮先用 `AppendRow()` 把**真实隐患**("某分支漏推一列")消除,
|
本轮先用 `AppendRow()` 把**真实隐患**("某分支漏推一列")消除,
|
||||||
完整结构合并留待"逐行几何基线"检查就绪后再做。
|
完整结构合并留待"逐行几何基线"检查就绪后再做。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 九、第 37 轮追加(2026-11-04):逐行几何基线(§13.6 第 1 项)
|
||||||
|
|
||||||
|
> 触发:用户要求「按照你的建议继续推进」。§13.6 第 1 项 = 逐行几何基线,
|
||||||
|
> 它是 §13.3「完整结构合并」与「重写布局算式」的前置护栏。
|
||||||
|
|
||||||
|
| 节点 | 内容 | 验收标准 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| **P29** | 新增 `OffscreenChecks -run rowgeom`:把 7 个面板**每个矩形的每个字段**导出为文本基线,逐项比对 | `-mode record`/`-mode compare` 无差异;**篡改基线必须报错**(负向自检) |
|
||||||
|
| **P30** | 修正长度不变式的判据:从"18 列全等"改为**按排版类别**判定 | `panels` / `panelscroll` 仍全绿,且对 `MARK` 面板不再误判 |
|
||||||
|
|
||||||
|
### 9.1 基线的两层覆盖
|
||||||
|
|
||||||
|
| 场景 | 数据 | 记录内容 | 用途 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| **A:细节** | 前 6 条信号合并成 1 个网格,7 个面板全开、参数行展开、3 个固定点、折起/展开各一份 | 每行每列的精确矩形(`Temp/tools/rows-baseline/rowgeom-baseline.txt`,983 条键) | 定位级:一眼看出"哪一行哪一列偏了几个像素" |
|
||||||
|
| **B:规模** | **全部**信号合并成 1 个网格(MF4 = 566 行) | 每个面板一个 FNV-1a 摘要哈希 + 行数/固定点行数 | 覆盖级:任何一行变动都会改哈希(不产生几十万行基线) |
|
||||||
|
|
||||||
|
### 9.2 本轮新增硬约束
|
||||||
|
|
||||||
|
1. **改面板几何(宽高算式 / 行距 / 内边距 / 折起规则)前后必须跑**
|
||||||
|
`OffscreenChecks -run rowgeom -mode record|compare`。
|
||||||
|
基线是**检测器**不是黄金图:变化是**预期**时要重新 record,并在提交信息里说明原因。
|
||||||
|
2. **`rowRefs` 是重载的**:「列表式面板」用 `rowRefs` 表示"每个信号一行",
|
||||||
|
而「自由标记面板」用它表示"每个信号一个分组",只填 `rowNameRects` / `rowNameCloses`,
|
||||||
|
其余 14 列**必须为空**(真正的条目行在 `markerRows`)。因此任何"平行数组长度"不变式
|
||||||
|
**必须按 `PanelStyle` 分类判定**,不能写"全部 18 列全等"。
|
||||||
|
3. **`ExecuteToolbarAction(MERGE_LANES / SPLIT_LANES)` 之后、读 `LaneCount()` 之前,
|
||||||
|
必须先 `SynchronizeView(true)`**:`SetMode()` 只重建 `TabConfig` 的网格配置,
|
||||||
|
`PlotView::m_lanes` 要等下一次布局/绘制才刷新。
|
||||||
|
4. 检查里**不要依赖"上一个场景的网格级状态还在"**:`Session::ShowSignals()` 先
|
||||||
|
`SetAllVisible(false)`,而"全部不可见 ⇒ 丢弃网格"是既有设计
|
||||||
|
(`RebuildLanes` 的 MERGED 分支),固定点/面板开关会随之清空 —— 该场景要**重新播**。
|
||||||
|
|
||||||
@@ -410,3 +410,84 @@ Makefile 按目录通配收集源文件,**无需登记新文件**。
|
|||||||
| `OffscreenChecks -all` | 6/6 PASS,`checks=101 failed=0`(断言合并后略减) |
|
| `OffscreenChecks -all` | 6/6 PASS,`checks=101 failed=0`(断言合并后略减) |
|
||||||
| `panels -mode compare` | 7 个面板像素哈希 + 几何**逐项不变**(两次重构全程守住) |
|
| `panels -mode compare` | 7 个面板像素哈希 + 几何**逐项不变**(两次重构全程守住) |
|
||||||
| `panelscroll`(MF4,566 行) | 18 个行数组长度全部 = 566,行为不变 |
|
| `panelscroll`(MF4,566 行) | 18 个行数组长度全部 = 566,行为不变 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 第 37 轮(2026-11-04):逐行几何基线(§13.6 第 1 项)
|
||||||
|
|
||||||
|
### P29 新增 `rowgeom` 检查 —— ✅ 完成
|
||||||
|
|
||||||
|
`Tools/OffscreenChecks/Src/Checks.cpp` 新增注册键 `rowgeom`,做法:
|
||||||
|
|
||||||
|
1. **场景 A(细节基线)**:切双光标 → 前 6 条信号 → `MERGE_LANES` → 7 个面板全开、
|
||||||
|
第 2 条信号勾选(同一面板里同时有"展开的参数行"与"未勾选的行")→ 加 3 个真实固定点
|
||||||
|
→ 导出 7 个面板 **展开态**全字段 → 全部折起再导一份 → 复原。
|
||||||
|
2. **场景 B(规模基线)**:全部信号合并成 1 个网格(默认数据 3 行;`-data MF4-Record.MF4` = 566 行)
|
||||||
|
→ 每面板一个 FNV-1a 摘要 + `rows` / `markerRows` 计数。
|
||||||
|
3. **导出格式**(纯文本、逐行 `键=值`,便于 `git diff` 与人工定位):
|
||||||
|
|
||||||
|
```text
|
||||||
|
mean.rowRefs.count=3
|
||||||
|
mean.rowNameRects[001]=1155.75,221.25,512.50,25.00
|
||||||
|
mark.markerGroupCount=2
|
||||||
|
mark.markerGroup[000].header=1155.75,171.25,512.50,27.50
|
||||||
|
mark.markerRow[000].item=0/1
|
||||||
|
mark.markerRowCount=3
|
||||||
|
```
|
||||||
|
|
||||||
|
⚠️ 因为 `rowRefs` 是**重载**的(见下),导出器按**每一列自己的长度**遍历,
|
||||||
|
不能"取 `rowRefs.size()` 当行数统一遍历全部 18 列"。
|
||||||
|
4. **`-mode record` / `-mode compare`**:比对双向(缺键 = MISSING,多键 = ADDED,值变 = CHANGED),
|
||||||
|
最多打印 20 条差异;差异数为 0 才算 PASS。
|
||||||
|
|
||||||
|
### 过程中两次崩溃(都已定位,结论写进 `agent_mem.md`)
|
||||||
|
|
||||||
|
| # | 现象 | 根因 | 处置 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| 1 | `LaneCount()` 断言失败 | `SetMode(MERGED)` 只重建 `TabConfig` 的网格配置,`PlotView::m_lanes` 要等下一次布局/绘制才刷新 | `MERGE_LANES` 之后补 `SynchronizeView(true)` + `RenderFrame()` 再断言 |
|
||||||
|
| 2 | 0xC0000005(导出时越界) | 假设"18 个行数组等长",但 **`MARK` 面板复用 `rowRefs` 表示信号分组**,只填 `rowNameRects` / `rowNameCloses`,其余 14 列为空 ⇒ 按 `rowRefs.size()` 索引其它列直接越界 | 导出器改为**逐列按自身长度**遍历 |
|
||||||
|
|
||||||
|
### P30 长度不变式的判据修正 —— ✅ 完成
|
||||||
|
|
||||||
|
`panels` / `panelscroll` 里第 36 轮定的"18 列必须全部等于行数"对 `MARK` 面板**是错的**
|
||||||
|
(它对新结构是误判,只是既有场景恰好没触发)。改为按 `PanelStyleOf(kind).markerGroups` 分流:
|
||||||
|
|
||||||
|
* `markerGroups == true`:`rowNameRects` / `rowNameCloses` 等于分组数,
|
||||||
|
且 `rowHeaders` / `rowChecks` / `rowLevelTracks` / `rowIterTracks` **必须为空**;
|
||||||
|
* 其余面板:17 列(`rowRefs` + `rowHeaders` … `rowFrequencyField`)全部等于行数。
|
||||||
|
|
||||||
|
失败时打印 15 个计数字段,便于一眼看出是哪一列错了。
|
||||||
|
|
||||||
|
### 基线是否真的能抓回归(负向自检)
|
||||||
|
|
||||||
|
把基线副本里 `mark.markerRowCount=3` 改成 `2` 再跑 `-mode compare`:
|
||||||
|
|
||||||
|
```text
|
||||||
|
[rowgeom detail] CHANGED mark.markerRowCount: baseline=2 current=3
|
||||||
|
[FAIL] rowgeom detail geometry is unchanged
|
||||||
|
SUMMARY checks=32 failed=1 groups=1 result=FAIL
|
||||||
|
```
|
||||||
|
|
||||||
|
⇒ 检测器**不是空转**的。(正式基线所在目录始终是 `record` 生成的,未被改动。)
|
||||||
|
|
||||||
|
### 一个"顺手查清"的行为(不是缺陷)
|
||||||
|
|
||||||
|
`rowgeom` 第一次跑出 `mark.hash … rows=0`:场景 B 里固定点没了。追查结论:
|
||||||
|
`Session::ShowSignals()` 先 `SetAllVisible(false)`,而"全部不可见 ⇒ 丢弃网格"
|
||||||
|
是 `RebuildLanes` 的**既有设计**(MERGED 分支的 `lanes.clear()`),
|
||||||
|
网格级状态(固定点 / 面板开关 / 滚动位置)随之清空 —— 与 `CLEAR_ALL` 行为一致,**不是新缺陷**。
|
||||||
|
因此场景 B 改为**重新播**固定点(`addFreeMarkers(2)` lambda),
|
||||||
|
场景 B 的 MARK 面板现在是 `rows=2 markerRows=3`,规模覆盖也补上了。
|
||||||
|
|
||||||
|
### 本轮验证
|
||||||
|
|
||||||
|
| 项 | 结果 |
|
||||||
|
| --- | --- |
|
||||||
|
| `make release`(全量) | 零 error / 零 warning;产物 **785,408 B 不变**(本轮只动工具与检查) |
|
||||||
|
| `make -C Tools` | 零 error / 零 warning |
|
||||||
|
| `UnitTests` | `checks=260 failed=0 cases=19` |
|
||||||
|
| `OffscreenChecks -all` | **7/7 PASS**,`checks=127 failed=0`(第 36 轮为 6/6、101) |
|
||||||
|
| `rowgeom -mode compare`(默认数据) | `detail 983 键 + scale 7 键`,0 差异 |
|
||||||
|
| `rowgeom -mode compare -data MF4-Record.MF4` | `detail 1304 键 + scale 7 键`(566 行),0 差异 |
|
||||||
|
| `panels -mode compare` | 7 个面板像素哈希 + 几何逐项不变 |
|
||||||
|
|
||||||
@@ -555,3 +555,58 @@ Tools/README.md 目录结构、作用、测试项、验收标准、A/B
|
|||||||
可在 `AppendRow` 里保留但标注"仅为兼容保留",或在下一轮与文档一起清掉。
|
可在 `AppendRow` 里保留但标注"仅为兼容保留",或在下一轮与文档一起清掉。
|
||||||
3. **`PaintPanelListRows` 仍约 470 行**:可再按"条目栏 / 参数行 / 重采样行"拆三段,
|
3. **`PaintPanelListRows` 仍约 470 行**:可再按"条目栏 / 参数行 / 重采样行"拆三段,
|
||||||
但当前已不在"改一次就出错"的路径上,优先级低于第 1 项。
|
但当前已不在"改一次就出错"的路径上,优先级低于第 1 项。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 十四、第 37 轮:逐行几何基线(§13.6 第 1 项)
|
||||||
|
|
||||||
|
### 14.1 结论
|
||||||
|
|
||||||
|
| §13.6 建议 | 结果 |
|
||||||
|
| --- | --- |
|
||||||
|
| 1. 逐行几何基线 | ✅ 新增 `OffscreenChecks -run rowgeom`(`record` / `compare`),并做了**负向自检**证明它能抓回归 |
|
||||||
|
| 2. `rowPoints` / `rowIterations` 清理 | ⏸ 未做(本轮与该建议无耦合,留给下一轮) |
|
||||||
|
| 3. `PaintPanelListRows` 再拆 | ⏸ 未做(纯可读性,优先级低) |
|
||||||
|
|
||||||
|
`-all` 从 6 个检查 / 101 条断言 → **7 个检查 / 127 条断言**,全绿。
|
||||||
|
|
||||||
|
### 14.2 基线的两层设计(为什么不是"一行一个文件")
|
||||||
|
|
||||||
|
| 层 | 场景 | 记录 | 键数 | 用途 |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| 细节 | 6 信号 1 网格、7 面板全开、参数行展开、3 个固定点、折起/展开各一份 | 每个矩形的每个字段 | 983(MF4 上 1304) | **定位**:直接看出哪一行哪一列偏了几像素 |
|
||||||
|
| 规模 | 全部信号 1 网格(MF4 = 566 行) | 每面板一个 FNV-1a 摘要 + 行数 | 7 | **覆盖**:任何一行变动都改哈希,且不产生几十万行基线 |
|
||||||
|
|
||||||
|
两层互补:哈希层保证"改动一定被看见",明细层保证"看见了能定位"。
|
||||||
|
|
||||||
|
### 14.3 这次护栏暴露的两个结构事实
|
||||||
|
|
||||||
|
1. **`rowRefs` 是重载的字段**。
|
||||||
|
「列表式」面板里 `rowRefs[i]` = 第 i 行的信号;
|
||||||
|
「自由标记」面板里 `rowRefs[i]` = 第 i 个**信号分组**,而它真正的条目行在 `markerRows`,
|
||||||
|
且只填 `rowNameRects` / `rowNameCloses`,另外 14 列**必须为空**。
|
||||||
|
⇒ 第 36 轮定的"18 列全等"不变式对 `MARK` 是**错的**,本轮改为按 `PanelStyle` 分流判定。
|
||||||
|
同一个"字段重载"也让导出器必须**逐列按自身长度**遍历(否则越界崩溃 —— 这正是本轮第一次
|
||||||
|
`0xC0000005` 的来源)。
|
||||||
|
2. **`SetMode()` 不同步 `m_lanes`**。`ExecuteToolbarAction(MERGE_LANES/SPLIT_LANES)` 之后
|
||||||
|
`TabConfig` 的网格配置已是新的,但 `PlotView::m_lanes` 要等下一次布局/绘制才刷新 ——
|
||||||
|
任何"合并后立刻读 `LaneCount()`"的代码都必须先 `SynchronizeView(true)`。
|
||||||
|
|
||||||
|
### 14.4 一个被误判成缺陷的既有行为
|
||||||
|
|
||||||
|
场景 B 一开始 `mark` 面板是空行:`Session::ShowSignals()` **先** `SetAllVisible(false)`,
|
||||||
|
而"全部不可见 ⇒ 丢弃网格"是 `RebuildLanes` 的既有设计(与工具栏「清空」一致),
|
||||||
|
网格级状态(固定点 / 面板开关 / 滚动位置)随之清空。⇒ 不是新缺陷;
|
||||||
|
检查改为在该场景**重新播**固定点,顺带把规模覆盖补全(`rows=2 markerRows=3`)。
|
||||||
|
|
||||||
|
### 14.5 下一步(按价值排序)
|
||||||
|
|
||||||
|
1. **`rowPoints` / `rowIterations` 清理**(确认已无绘制方使用,仅 `BuildBoxes` / 命中键残留):
|
||||||
|
这是当前唯一"写了没人读"的列,删掉能减少每次 `AppendRow` 的 2 次 push 与 2 个 vector。
|
||||||
|
判据:`rowgeom -mode compare`(细节基线会逐项证明几何没变)+ `panels -mode compare`。
|
||||||
|
2. **完整结构合并(`std::vector<PanelRow>` + 列视图)**:前置护栏(本轮)已就绪,
|
||||||
|
但 §13.3 的**失败可见性**问题仍未解决 —— 列视图拷贝后忘记重绑是静默悬垂。
|
||||||
|
若要做,必须同时给 `EdgePanelLayout` 加"视图自检"(例如保存 `rows` 容量并与视图长度比对),
|
||||||
|
否则不建议动。
|
||||||
|
3. `PaintPanelListRows` 再拆三段(纯可读性)。
|
||||||
|
|
||||||
@@ -10564,3 +10564,77 @@ release/debug **零警告**;Release `TargetCheck.exe` **890,880 B**(< 1 MB
|
|||||||
* **逐行几何基线**(导出每行 18 个矩形做逐项比对)—— 完整结构合并与布局算式重写的前置护栏。
|
* **逐行几何基线**(导出每行 18 个矩形做逐项比对)—— 完整结构合并与布局算式重写的前置护栏。
|
||||||
* `rowPoints` / `rowIterations` 已无绘制方使用(旧式输入框排版早已删除),可标注/清理。
|
* `rowPoints` / `rowIterations` 已无绘制方使用(旧式输入框排版早已删除),可标注/清理。
|
||||||
* `PaintPanelListRows` 仍约 470 行,可再拆三段,但已不在"改一次就出错"的路径上。
|
* `PaintPanelListRows` 仍约 470 行,可再拆三段,但已不在"改一次就出错"的路径上。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2026-11-04 · 第 37 轮(`Code_Review` 分支):逐行几何基线(§13.6 第 1 项)
|
||||||
|
|
||||||
|
> 提交:见本轮提交信息;规则原文 `Documents/Code_Review_Report.md` §14、`Code_Review_Plan.md` §九、
|
||||||
|
> `Code_Review_Progress.md` 第 37 轮、`Tools/README.md` §4.1/§4.3/§4.4。
|
||||||
|
|
||||||
|
### 1. ✅ 新增 `OffscreenChecks -run rowgeom`(`record` / `compare`)
|
||||||
|
|
||||||
|
两层覆盖:
|
||||||
|
* **细节层**(6 信号 1 网格、7 面板全开、参数行展开、3 个固定点、折起+展开各一份):
|
||||||
|
每个矩形的每个字段一条键,`Temp/tools/rows-baseline/rowgeom-baseline.txt` 共 **983** 条
|
||||||
|
(`-data MF4-Record.MF4` 时 1304 条)。
|
||||||
|
* **规模层**(全部信号 1 网格,MF4 = 566 行):每面板一个 FNV-1a 摘要 + 行数,7 条键。
|
||||||
|
|
||||||
|
键格式:`<panel>.<field>` / `<panel>.<列名>[NNN]=x,y,w,h` / `mark.markerGroup[NNN].header=…`。
|
||||||
|
比对双向(MISSING / ADDED / CHANGED),差异 0 才 PASS。
|
||||||
|
|
||||||
|
### 2. 🔴 新硬规则一:`rowRefs` 是**重载字段**,长度不变式必须按类别判定
|
||||||
|
|
||||||
|
| 面板类别 | `rowRefs` 含义 | 真正的行在 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 列表式(mean/center/peak/resample/clip) | 每个信号一行 | `rowRefs` 自身 + 其余 15 列 |
|
||||||
|
| 边沿计数(count) | 每个信号一行(表格行) | 同上 |
|
||||||
|
| 「自由标记」(mark) | 每个**信号分组**一项 | `markerRows` / `markerGroups` |
|
||||||
|
|
||||||
|
MARK 面板**只填** `rowNameRects` / `rowNameCloses`,其余 14 列**必须为空**。
|
||||||
|
⇒ 第 36 轮定的"18 列全部等于行数"对 MARK 是**错的**,本轮已改为:
|
||||||
|
|
||||||
|
* `PanelStyle::markerGroups == true` ⇒ `rowNameRects`/`rowNameCloses` 等于分组数,
|
||||||
|
且 `rowHeaders`/`rowChecks`/`rowLevelTracks`/`rowIterTracks` 必须为空;
|
||||||
|
* 其余面板 ⇒ `rowRefs` + 16 列全部等于行数。
|
||||||
|
|
||||||
|
任何"遍历 18 列"的代码(导出器、不变式、批量处理)**都必须逐列按自身长度**遍历,
|
||||||
|
不能拿 `rowRefs.size()` 当统一行数(否则 MARK 面板直接越界 —— 本轮第一次 `0xC0000005`)。
|
||||||
|
|
||||||
|
### 3. 🔴 新硬规则二:`SetMode()` 不同步 `m_lanes`
|
||||||
|
|
||||||
|
`ExecuteToolbarAction(MERGE_LANES / SPLIT_LANES)`(以及 `TabConfig::SetMode`)只重建
|
||||||
|
`TabConfig` 的网格配置,`PlotView::m_lanes` 要等下一次布局/绘制才刷新。
|
||||||
|
⇒ **合并/拆分后、读 `LaneCount()` 或 `EdgePanelLayoutFor()` 之前,必须先
|
||||||
|
`app.SynchronizeView(true)`(必要时再 `RenderFrame()`)**。
|
||||||
|
|
||||||
|
### 4. ⚠️ 已知行为(不是缺陷):全部不可见 ⇒ 丢弃网格 ⇒ 网格级状态清空
|
||||||
|
|
||||||
|
`Session::ShowSignals()` 先 `SetAllVisible(false)`;`RebuildLanes` 的 MERGED 分支在
|
||||||
|
"无可见信号"时 `lanes.clear()`(与工具栏「清空」一致)。
|
||||||
|
⇒ 固定点 / 面板开关 / 面板滚动位置 / 面板显隐 **全部丢失**,不是新 BUG。
|
||||||
|
检查里跨场景依赖这些状态时,必须**重新播**(`rowgeom` 用 `addFreeMarkers(wanted)` lambda)。
|
||||||
|
|
||||||
|
### 5. 基线的定位
|
||||||
|
|
||||||
|
**检测器,不是黄金图。** 预期内的几何变化要重新 `-mode record`,并在提交信息里写明原因。
|
||||||
|
负向自检已做:篡改基线中 `mark.markerRowCount=3 → 2`,`-mode compare` 会
|
||||||
|
`CHANGED … mismatches=1` 并 FAIL ⇒ 护栏不是空转。
|
||||||
|
|
||||||
|
### 6. 本轮数字
|
||||||
|
|
||||||
|
* Release **785,408 B 不变**(本轮只动工具/检查与文档);零 warning。
|
||||||
|
* `UnitTests` 260 断言 / 19 用例;`OffscreenChecks -all` **7 检查 / 127 断言**(原 6/101)。
|
||||||
|
* `rowgeom -mode compare`:默认数据 983+7 键、MF4 1304+7 键,0 差异。
|
||||||
|
* `panels -mode compare` 7 个面板像素哈希 + 几何逐项不变。
|
||||||
|
|
||||||
|
### 7. 遗留(下一步候选,按价值)
|
||||||
|
|
||||||
|
1. **`rowPoints` / `rowIterations` 清理**(确认已无绘制方使用,仅 `BuildBoxes` / 命中键残留)——
|
||||||
|
当前唯一"写了没人读"的列,删掉可省每次 `AppendRow` 的 2 次 push。
|
||||||
|
判据:`rowgeom -mode compare` + `panels -mode compare`。
|
||||||
|
2. **完整结构合并(`std::vector<PanelRow>` + 列视图)**:护栏已就绪,但 §13.3 的
|
||||||
|
**失败可见性**问题未解(拷贝后忘重绑 = 静默悬垂)。若要做,必须同时给
|
||||||
|
`EdgePanelLayout` 加"视图自检"(比对 `rows` 容量与视图长度)。
|
||||||
|
3. `PaintPanelListRows` 再拆三段(纯可读性)。
|
||||||
|
|
||||||
@@ -262,6 +262,254 @@ namespace jdi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @brief 把矩形格式化成 `l,t,w,h`(2 位小数;坐标是设备像素,2 位足够)。 */
|
||||||
|
std::string FormatRect(const gd::RectF& rect)
|
||||||
|
{
|
||||||
|
char buffer[128] = {};
|
||||||
|
std::snprintf(buffer, sizeof(buffer), "%.2f,%.2f,%.2f,%.2f", rect.X, rect.Y, rect.Width,
|
||||||
|
rect.Height);
|
||||||
|
return std::string(buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @brief 把一段文本混进 FNV-1a 哈希(用于"多行规模"场景只存一个摘要)。 */
|
||||||
|
void MixText(std::uint64_t& hash, const std::string& text)
|
||||||
|
{
|
||||||
|
for (const char character : text)
|
||||||
|
{
|
||||||
|
hash ^= static_cast<std::uint64_t>(static_cast<unsigned char>(character));
|
||||||
|
hash *= 1099511628211ull;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @brief 追加 `key=rect` 一行。 */
|
||||||
|
void AppendRectLine(std::string& text, const std::string& key, const gd::RectF& rect)
|
||||||
|
{
|
||||||
|
text += key;
|
||||||
|
text += '=';
|
||||||
|
text += FormatRect(rect);
|
||||||
|
text += '\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 把一个面板布局的**全部几何**追加成文本(逐字段、逐行、逐列)。
|
||||||
|
*
|
||||||
|
* 这是「逐行几何基线」的核心:面板改布局时,像素哈希只能告诉你"有变化",
|
||||||
|
* 而这份文本能告诉你**哪一个字段、哪一行、哪一列**变了。
|
||||||
|
* 有了它,`EdgePanelLayout` 的完整结构合并(平行数组 → `PanelRow` 单数组)
|
||||||
|
* 与布局算式重写才具备"逐项证明等价"的能力。
|
||||||
|
*
|
||||||
|
* ⚠️ 键名必须稳定:一旦改名,历史基线全部失效(`-mode record` 重新记录即可)。
|
||||||
|
*
|
||||||
|
* @param [IN,OUT] text 目标文本(追加)。
|
||||||
|
* @param [IN] prefix 该面板的键前缀(= 面板种类名)。
|
||||||
|
* @param [IN] layout 待导出的布局。
|
||||||
|
*/
|
||||||
|
void AppendLayoutDump(std::string& text, const std::string& prefix,
|
||||||
|
const PlotView::EdgePanelLayout& layout)
|
||||||
|
{
|
||||||
|
const auto key = [&prefix](const char* name) { return prefix + "." + name; };
|
||||||
|
|
||||||
|
// ---- 面板骨架 ----
|
||||||
|
AppendRectLine(text, key("panel"), layout.panel);
|
||||||
|
AppendRectLine(text, key("title"), layout.title);
|
||||||
|
AppendRectLine(text, key("close"), layout.close);
|
||||||
|
AppendRectLine(text, key("disclosure"), layout.disclosure);
|
||||||
|
AppendRectLine(text, key("badge"), layout.badge);
|
||||||
|
AppendRectLine(text, key("edit"), layout.edit);
|
||||||
|
AppendRectLine(text, key("button"), layout.button);
|
||||||
|
AppendRectLine(text, key("content"), layout.content);
|
||||||
|
AppendRectLine(text, key("tableHeader"), layout.tableHeader);
|
||||||
|
AppendRectLine(text, key("scrollTrack"), layout.scrollTrack);
|
||||||
|
AppendRectLine(text, key("scrollThumb"), layout.scrollThumb);
|
||||||
|
AppendRectLine(text, key("opacityTrack"), layout.opacityTrack);
|
||||||
|
AppendRectLine(text, key("opacityValue"), layout.opacityValue);
|
||||||
|
AppendRectLine(text, key("showDelta"), layout.showDelta);
|
||||||
|
AppendRectLine(text, key("showEnum"), layout.showEnum);
|
||||||
|
AppendRectLine(text, key("clipBuildButton"), layout.clipBuildButton);
|
||||||
|
AppendRectLine(text, key("clipDestructiveButton"), layout.clipDestructiveButton);
|
||||||
|
text += key("collapsed") + "=" + (layout.collapsed ? "1" : "0") + "\n";
|
||||||
|
text += key("rows") + "=" + std::to_string(layout.rows) + "\n";
|
||||||
|
text += key("markerContentHeight") + "=" + std::to_string(layout.markerContentHeight) + "\n";
|
||||||
|
text += key("scrollOffset") + "=" + std::to_string(layout.scrollOffset) + "\n";
|
||||||
|
|
||||||
|
// ---- 表格三列 ----
|
||||||
|
for (int column = 0; column < 3; ++column)
|
||||||
|
{
|
||||||
|
const PlotView::EdgePanelLayout::TableColumn& cell = layout.tableColumns[column];
|
||||||
|
char name[32] = {};
|
||||||
|
std::snprintf(name, sizeof(name), "tableColumn[%d]", column);
|
||||||
|
text += key(name) + "=" + std::to_string(cell.left) + "/" +
|
||||||
|
std::to_string(cell.width) + "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- 每一行 / 每一列 ----
|
||||||
|
// ⚠️ **每一列只按自己的长度导出**,不要假设所有列等长:
|
||||||
|
// 「自由标记」面板复用 `rowRefs` 表示"信号分组",只填
|
||||||
|
// `rowNameRects` / `rowNameCloses`,其余 14 列**全为空**
|
||||||
|
// (这一点正是 `rowgeom` 第一次运行就抓出来的结构事实)。
|
||||||
|
// 按 `rowRefs.size()` 统一索引会越界崩溃。
|
||||||
|
const auto dumpRects = [&text, &key](const char* name, const std::vector<gd::RectF>& rects) {
|
||||||
|
text += key(name) + ".count=" + std::to_string(rects.size()) + "\n";
|
||||||
|
for (std::size_t index = 0; index < rects.size(); ++index)
|
||||||
|
{
|
||||||
|
char cell[64] = {};
|
||||||
|
std::snprintf(cell, sizeof(cell), "[%03d]", static_cast<int>(index));
|
||||||
|
AppendRectLine(text, key(name) + cell, rects[index]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
text += key("rowRefs") + ".count=" + std::to_string(layout.rowRefs.size()) + "\n";
|
||||||
|
for (std::size_t index = 0; index < layout.rowRefs.size(); ++index)
|
||||||
|
{
|
||||||
|
char cell[64] = {};
|
||||||
|
std::snprintf(cell, sizeof(cell), "[%03d]", static_cast<int>(index));
|
||||||
|
text += key("rowRefs") + cell + "=" +
|
||||||
|
std::to_string(layout.rowRefs[index].groupIndex) + "/" +
|
||||||
|
std::to_string(layout.rowRefs[index].signalIndex) + "\n";
|
||||||
|
}
|
||||||
|
dumpRects("rowHeaders", layout.rowHeaders);
|
||||||
|
dumpRects("rowNameRects", layout.rowNameRects);
|
||||||
|
dumpRects("rowNameCloses", layout.rowNameCloses);
|
||||||
|
dumpRects("rowTriangles", layout.rowTriangles);
|
||||||
|
dumpRects("rowChecks", layout.rowChecks);
|
||||||
|
dumpRects("rowPoints", layout.rowPoints);
|
||||||
|
dumpRects("rowIterations", layout.rowIterations);
|
||||||
|
dumpRects("rowStats", layout.rowStats);
|
||||||
|
dumpRects("rowLevelTracks", layout.rowLevelTracks);
|
||||||
|
dumpRects("rowLevelValues", layout.rowLevelValues);
|
||||||
|
dumpRects("rowLevelResets", layout.rowLevelResets);
|
||||||
|
dumpRects("rowIterTracks", layout.rowIterTracks);
|
||||||
|
dumpRects("rowIterValues", layout.rowIterValues);
|
||||||
|
dumpRects("rowIterResets", layout.rowIterResets);
|
||||||
|
dumpRects("rowInfoMaxInterval", layout.rowInfoMaxInterval);
|
||||||
|
dumpRects("rowInfoMinInterval", layout.rowInfoMinInterval);
|
||||||
|
dumpRects("rowIntervalField", layout.rowIntervalField);
|
||||||
|
dumpRects("rowFrequencyField", layout.rowFrequencyField);
|
||||||
|
|
||||||
|
// ---- 自由标记:分组栏 / 表头 / 条目 ----
|
||||||
|
text += key("markerGroupCount") + "=" + std::to_string(layout.markerGroups.size()) + "\n";
|
||||||
|
for (std::size_t group = 0; group < layout.markerGroups.size(); ++group)
|
||||||
|
{
|
||||||
|
const PlotView::EdgePanelLayout::MarkerGroup& entry = layout.markerGroups[group];
|
||||||
|
char name[48] = {};
|
||||||
|
std::snprintf(name, sizeof(name), "markerGroup[%03d]", static_cast<int>(group));
|
||||||
|
const std::string groupKey = key(name);
|
||||||
|
AppendRectLine(text, groupKey + ".header", entry.header);
|
||||||
|
AppendRectLine(text, groupKey + ".tableHeader", entry.tableHeader);
|
||||||
|
AppendRectLine(text, groupKey + ".triangle", entry.triangle);
|
||||||
|
AppendRectLine(text, groupKey + ".check", entry.check);
|
||||||
|
AppendRectLine(text, groupKey + ".close", entry.close);
|
||||||
|
text += groupKey + ".collapsed=" + (entry.collapsed ? "1" : "0") + "\n";
|
||||||
|
text += groupKey + ".pinned=" + (entry.pinned ? "1" : "0") + "\n";
|
||||||
|
}
|
||||||
|
text += key("markerRowCount") + "=" + std::to_string(layout.markerRows.size()) + "\n";
|
||||||
|
for (std::size_t row = 0; row < layout.markerRows.size(); ++row)
|
||||||
|
{
|
||||||
|
const PlotView::EdgePanelLayout::MarkerRow& entry = layout.markerRows[row];
|
||||||
|
char name[48] = {};
|
||||||
|
std::snprintf(name, sizeof(name), "markerRow[%03d]", static_cast<int>(row));
|
||||||
|
const std::string rowKey = key(name);
|
||||||
|
text += rowKey + ".item=" + std::to_string(entry.itemIndex) + "/" +
|
||||||
|
std::to_string(entry.serial) + "\n";
|
||||||
|
AppendRectLine(text, rowKey + ".rect", entry.rect);
|
||||||
|
AppendRectLine(text, rowKey + ".close", entry.close);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/** @brief 统计文本的行数(用于打印导出规模)。 */
|
||||||
|
std::size_t CountLines(const std::string& text)
|
||||||
|
{
|
||||||
|
std::size_t lines = 0;
|
||||||
|
for (const char character : text)
|
||||||
|
{
|
||||||
|
if (character == '\n')
|
||||||
|
{
|
||||||
|
++lines;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return lines;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 逐行比对一份"键值基线"(`key=value`),报告缺失 / 多出 / 值变化。
|
||||||
|
*
|
||||||
|
* 与 `panels` 里的比对相比,这里刻意把**每一处差异都打印出来**(上限 20 条):
|
||||||
|
* 几何基线的价值就在于"哪一行的哪一列变了",只报一个总数等于没用。
|
||||||
|
*
|
||||||
|
* @param [IN] path 基线文件路径。
|
||||||
|
* @param [IN] current 当前导出文本。
|
||||||
|
* @param [IN] label 报告用的名字。
|
||||||
|
*/
|
||||||
|
void CompareBaselineText(const std::string& path, const std::string& current, const char* label)
|
||||||
|
{
|
||||||
|
const std::string baseline = ReadTextFile(path);
|
||||||
|
Expect(!baseline.empty(), std::string(label) + " baseline exists (run -mode record first)");
|
||||||
|
if (baseline.empty())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const std::vector<std::pair<std::string, std::string>> expectedEntries = ParseBaseline(baseline);
|
||||||
|
const std::vector<std::pair<std::string, std::string>> currentEntries = ParseBaseline(current);
|
||||||
|
ExpectEqual(static_cast<long long>(currentEntries.size()),
|
||||||
|
static_cast<long long>(expectedEntries.size()),
|
||||||
|
std::string(label) + " entry count");
|
||||||
|
|
||||||
|
std::map<std::string, std::string> currentMap;
|
||||||
|
for (const auto& entry : currentEntries)
|
||||||
|
{
|
||||||
|
currentMap[entry.first] = entry.second;
|
||||||
|
}
|
||||||
|
|
||||||
|
int reported = 0;
|
||||||
|
int mismatches = 0;
|
||||||
|
for (const auto& expected : expectedEntries)
|
||||||
|
{
|
||||||
|
const auto found = currentMap.find(expected.first);
|
||||||
|
if (found == currentMap.end())
|
||||||
|
{
|
||||||
|
++mismatches;
|
||||||
|
if (reported < 20)
|
||||||
|
{
|
||||||
|
EmitLine("[%s] MISSING %s (baseline=%s)\n", label, expected.first.c_str(),
|
||||||
|
expected.second.c_str());
|
||||||
|
++reported;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (found->second != expected.second)
|
||||||
|
{
|
||||||
|
++mismatches;
|
||||||
|
if (reported < 20)
|
||||||
|
{
|
||||||
|
EmitLine("[%s] CHANGED %s: baseline=%s current=%s\n", label, expected.first.c_str(),
|
||||||
|
expected.second.c_str(), found->second.c_str());
|
||||||
|
++reported;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 反过来查"多出来的键"(新增几何字段时会出现,属于预期变化)。
|
||||||
|
std::map<std::string, std::string> expectedMap;
|
||||||
|
for (const auto& entry : expectedEntries)
|
||||||
|
{
|
||||||
|
expectedMap[entry.first] = entry.second;
|
||||||
|
}
|
||||||
|
for (const auto& entry : currentEntries)
|
||||||
|
{
|
||||||
|
if (expectedMap.find(entry.first) == expectedMap.end())
|
||||||
|
{
|
||||||
|
++mismatches;
|
||||||
|
if (reported < 20)
|
||||||
|
{
|
||||||
|
EmitLine("[%s] ADDED %s=%s\n", label, entry.first.c_str(), entry.second.c_str());
|
||||||
|
++reported;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EmitLine("[%s] compared %d entries, %d mismatch(es)\n", label,
|
||||||
|
static_cast<int>(expectedEntries.size()), mismatches);
|
||||||
|
Expect(mismatches == 0, std::string(label) + " geometry is unchanged");
|
||||||
|
}
|
||||||
|
|
||||||
// ---- 检查实现 -------------------------------------------------------
|
// ---- 检查实现 -------------------------------------------------------
|
||||||
|
|
||||||
int CheckSmoke(Session& session, const CheckOptions& options)
|
int CheckSmoke(Session& session, const CheckOptions& options)
|
||||||
@@ -417,23 +665,35 @@ namespace jdi
|
|||||||
rect.y + rect.height <= plotRect.y + plotRect.height + 1;
|
rect.y + rect.height <= plotRect.y + plotRect.height + 1;
|
||||||
Expect(inside, std::string("panel inside plot area: ") + PanelKindName(kind));
|
Expect(inside, std::string("panel inside plot area: ") + PanelKindName(kind));
|
||||||
|
|
||||||
// 不变式:`EdgePanelLayout` 的**平行数组长度必须全部等于行数**。
|
// 不变式:行的几何数组必须与行数**形状一致**。
|
||||||
// 这 18 个按行平行的 vector 现在由 `AppendRow()` 一次补齐
|
//
|
||||||
//(新增行的唯一入口),因此**不允许**任何一列短于行数 ——
|
// ⚠️⚠️ 判据必须**按排版类别**区分 —— 旧版"18 个数组全部全等"是**错的**:
|
||||||
// 短了就说明有分支绕过 `AppendRow` 手写 push("某列读到别人矩形"的根因)。
|
// 「自由标记」面板复用 `rowRefs` 表示"信号分组",只填
|
||||||
// 成本极低(O(1) 比长度),但能挡住整类错位缺陷。
|
// `rowNameRects` / `rowNameCloses`,其余 14 列**必须为空**
|
||||||
|
// (它真正的行在 `markerRows` / `markerGroups` 里)。
|
||||||
|
// 这是 `rowgeom` 第一次带真实固定点运行时越界崩溃所暴露的结构事实。
|
||||||
|
const PanelStyle rowStyle = PanelStyleOf(kind);
|
||||||
const std::size_t rowCount = layout.rowRefs.size();
|
const std::size_t rowCount = layout.rowRefs.size();
|
||||||
const std::size_t parallelSizes[] = {
|
|
||||||
layout.rowRefs.size(), layout.rowHeaders.size(),
|
|
||||||
layout.rowNameRects.size(), layout.rowTriangles.size(),
|
|
||||||
layout.rowChecks.size(), layout.rowPoints.size(),
|
|
||||||
layout.rowIterations.size(), layout.rowStats.size(),
|
|
||||||
layout.rowLevelTracks.size(), layout.rowLevelValues.size(),
|
|
||||||
layout.rowLevelResets.size(), layout.rowIterTracks.size(),
|
|
||||||
layout.rowIterValues.size(), layout.rowIterResets.size(),
|
|
||||||
layout.rowInfoMaxInterval.size(), layout.rowInfoMinInterval.size(),
|
|
||||||
layout.rowIntervalField.size(), layout.rowFrequencyField.size()};
|
|
||||||
bool parallel = true;
|
bool parallel = true;
|
||||||
|
if (rowStyle.markerGroups)
|
||||||
|
{
|
||||||
|
parallel = layout.rowNameRects.size() == rowCount &&
|
||||||
|
layout.rowNameCloses.size() == rowCount && layout.rowHeaders.empty() &&
|
||||||
|
layout.rowChecks.empty() && layout.rowLevelTracks.empty() &&
|
||||||
|
layout.rowIterTracks.empty();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
const std::size_t parallelSizes[] = {
|
||||||
|
layout.rowHeaders.size(), layout.rowNameRects.size(),
|
||||||
|
layout.rowTriangles.size(), layout.rowChecks.size(),
|
||||||
|
layout.rowPoints.size(), layout.rowIterations.size(),
|
||||||
|
layout.rowStats.size(), layout.rowLevelTracks.size(),
|
||||||
|
layout.rowLevelValues.size(), layout.rowLevelResets.size(),
|
||||||
|
layout.rowIterTracks.size(), layout.rowIterValues.size(),
|
||||||
|
layout.rowIterResets.size(), layout.rowInfoMaxInterval.size(),
|
||||||
|
layout.rowInfoMinInterval.size(), layout.rowIntervalField.size(),
|
||||||
|
layout.rowFrequencyField.size()};
|
||||||
for (const std::size_t size : parallelSizes)
|
for (const std::size_t size : parallelSizes)
|
||||||
{
|
{
|
||||||
if (size != rowCount)
|
if (size != rowCount)
|
||||||
@@ -442,18 +702,16 @@ namespace jdi
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if (!parallel)
|
if (!parallel)
|
||||||
{
|
{
|
||||||
EmitLine("[panels] %s row arrays: refs=%d hdr=%d name=%d tri=%d chk=%d pts=%d it=%d "
|
EmitLine("[panels] %s row arrays: refs=%d name=%d nameClose=%d hdr=%d chk=%d lvl=%d/%d/%d "
|
||||||
"stat=%d lvl=%d/%d/%d iter=%d/%d/%d info=%d/%d fld=%d/%d\n",
|
"iter=%d/%d/%d info=%d/%d fld=%d/%d\n",
|
||||||
PanelKindName(kind), static_cast<int>(layout.rowRefs.size()),
|
PanelKindName(kind), static_cast<int>(layout.rowRefs.size()),
|
||||||
static_cast<int>(layout.rowHeaders.size()),
|
|
||||||
static_cast<int>(layout.rowNameRects.size()),
|
static_cast<int>(layout.rowNameRects.size()),
|
||||||
static_cast<int>(layout.rowTriangles.size()),
|
static_cast<int>(layout.rowNameCloses.size()),
|
||||||
|
static_cast<int>(layout.rowHeaders.size()),
|
||||||
static_cast<int>(layout.rowChecks.size()),
|
static_cast<int>(layout.rowChecks.size()),
|
||||||
static_cast<int>(layout.rowPoints.size()),
|
|
||||||
static_cast<int>(layout.rowIterations.size()),
|
|
||||||
static_cast<int>(layout.rowStats.size()),
|
|
||||||
static_cast<int>(layout.rowLevelTracks.size()),
|
static_cast<int>(layout.rowLevelTracks.size()),
|
||||||
static_cast<int>(layout.rowLevelValues.size()),
|
static_cast<int>(layout.rowLevelValues.size()),
|
||||||
static_cast<int>(layout.rowLevelResets.size()),
|
static_cast<int>(layout.rowLevelResets.size()),
|
||||||
@@ -465,7 +723,7 @@ namespace jdi
|
|||||||
static_cast<int>(layout.rowIntervalField.size()),
|
static_cast<int>(layout.rowIntervalField.size()),
|
||||||
static_cast<int>(layout.rowFrequencyField.size()));
|
static_cast<int>(layout.rowFrequencyField.size()));
|
||||||
}
|
}
|
||||||
Expect(parallel, std::string("all 18 row arrays match row count: ") + PanelKindName(kind));
|
Expect(parallel, std::string("row arrays match row count: ") + PanelKindName(kind));
|
||||||
|
|
||||||
char buffer[160] = {};
|
char buffer[160] = {};
|
||||||
std::snprintf(buffer, sizeof(buffer), "%s=%016llX,%d,%d,%d,%d\n", PanelKindName(kind),
|
std::snprintf(buffer, sizeof(buffer), "%s=%016llX,%d,%d,%d,%d\n", PanelKindName(kind),
|
||||||
@@ -592,21 +850,37 @@ namespace jdi
|
|||||||
initial.scrollTrack.Width > 0.0f ? 1 : 0,
|
initial.scrollTrack.Width > 0.0f ? 1 : 0,
|
||||||
static_cast<int>(initial.rowRefs.size()));
|
static_cast<int>(initial.rowRefs.size()));
|
||||||
|
|
||||||
// 多信号场景下额外守住"18 个平行数组长度全部等于行数"
|
// 多信号场景下同样守住"行数组形状一致"(判据按排版类别区分:
|
||||||
//(`AppendRow()` 是新增行的唯一入口 ⇒ 任何一列短了就是绕过它的证据)。
|
// MARK 只用 rowRefs / rowNameRects / rowNameCloses,详见 `panels` 检查里的说明)。
|
||||||
{
|
{
|
||||||
|
const PanelStyle rowStyle = PanelStyleOf(kind);
|
||||||
const std::size_t rows = initial.rowRefs.size();
|
const std::size_t rows = initial.rowRefs.size();
|
||||||
const std::size_t sizes[] = {initial.rowHeaders.size(), initial.rowNameRects.size(),
|
bool aligned = true;
|
||||||
initial.rowTriangles.size(), initial.rowChecks.size(),
|
if (rowStyle.markerGroups)
|
||||||
initial.rowPoints.size(), initial.rowIterations.size(),
|
{
|
||||||
initial.rowStats.size(), initial.rowLevelTracks.size(),
|
aligned = initial.rowNameRects.size() == rows &&
|
||||||
initial.rowLevelValues.size(), initial.rowLevelResets.size(),
|
initial.rowNameCloses.size() == rows && initial.rowHeaders.empty() &&
|
||||||
initial.rowIterTracks.size(), initial.rowIterValues.size(),
|
initial.rowChecks.empty();
|
||||||
initial.rowIterResets.size(), initial.rowInfoMaxInterval.size(),
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
const std::size_t sizes[] = {initial.rowHeaders.size(),
|
||||||
|
initial.rowNameRects.size(),
|
||||||
|
initial.rowTriangles.size(),
|
||||||
|
initial.rowChecks.size(),
|
||||||
|
initial.rowPoints.size(),
|
||||||
|
initial.rowIterations.size(),
|
||||||
|
initial.rowStats.size(),
|
||||||
|
initial.rowLevelTracks.size(),
|
||||||
|
initial.rowLevelValues.size(),
|
||||||
|
initial.rowLevelResets.size(),
|
||||||
|
initial.rowIterTracks.size(),
|
||||||
|
initial.rowIterValues.size(),
|
||||||
|
initial.rowIterResets.size(),
|
||||||
|
initial.rowInfoMaxInterval.size(),
|
||||||
initial.rowInfoMinInterval.size(),
|
initial.rowInfoMinInterval.size(),
|
||||||
initial.rowIntervalField.size(),
|
initial.rowIntervalField.size(),
|
||||||
initial.rowFrequencyField.size()};
|
initial.rowFrequencyField.size()};
|
||||||
bool aligned = true;
|
|
||||||
for (const std::size_t size : sizes)
|
for (const std::size_t size : sizes)
|
||||||
{
|
{
|
||||||
if (size != rows)
|
if (size != rows)
|
||||||
@@ -615,7 +889,8 @@ namespace jdi
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Expect(aligned, std::string("all 18 row arrays match row count: ") + name);
|
}
|
||||||
|
Expect(aligned, std::string("row arrays match row count: ") + name);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (maxScroll <= 1.0f)
|
if (maxScroll <= 1.0f)
|
||||||
@@ -672,6 +947,225 @@ namespace jdi
|
|||||||
return CheckFailed();
|
return CheckFailed();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 逐行几何基线:把每个面板的**每一个矩形**导出成文本并逐项比对。
|
||||||
|
*
|
||||||
|
* 为什么需要它(Code Review 报告 §13.6 建议 1):
|
||||||
|
* * 像素哈希只能回答"有没有变化",回答不了"**哪一行哪一列**变了";
|
||||||
|
* * `EdgePanelLayout` 仍是 18 个平行数组,任何"合并成单一 `PanelRow`"或
|
||||||
|
* "重写布局算式"的重构都必须先能**逐项证明等价**;
|
||||||
|
* * 面板改布局时的真实缺陷形态(某列读到别人的矩形)会让**成片的**行错位,
|
||||||
|
* 而像素基线在面板互相重叠时还会被"下层露出"掩盖。
|
||||||
|
*
|
||||||
|
* 两个场景:
|
||||||
|
* A. **明细**:6 条信号合并到 1 个网格,7 种面板全部展开,
|
||||||
|
* 并把第 1/2 条信号勾选上(让参数行同时覆盖"展开"与"未勾选"两种形态);
|
||||||
|
* 另有一条 `*.collapsed` 用例覆盖"折起只剩标题栏"。
|
||||||
|
* B. **规模**:全部信号合并到 1 个网格,只记录每个面板的几何摘要哈希 ——
|
||||||
|
* 566 行的场景下逐行文本会有上万行(不适合入库当基线),
|
||||||
|
* 但"行数 + 全部矩形的滚动哈希"足以发现任何错位。
|
||||||
|
*/
|
||||||
|
int CheckRowGeometry(Session& session, const CheckOptions& options)
|
||||||
|
{
|
||||||
|
Application& app = session.App();
|
||||||
|
PlotView& plot = app.m_plot;
|
||||||
|
|
||||||
|
static const EdgePanelKind kinds[] = {EdgePanelKind::COUNT, EdgePanelKind::CENTER,
|
||||||
|
EdgePanelKind::MEAN, EdgePanelKind::PEAK,
|
||||||
|
EdgePanelKind::MARK, EdgePanelKind::RESAMPLE,
|
||||||
|
EdgePanelKind::CLIP};
|
||||||
|
|
||||||
|
// 双光标(「选区截取」需要它才有内容)。
|
||||||
|
if (app.ActiveTab().CursorMode() != PlotCursorMode::DOUBLE)
|
||||||
|
{
|
||||||
|
for (int step = 0; step < 4 && app.ActiveTab().CursorMode() != PlotCursorMode::DOUBLE; ++step)
|
||||||
|
{
|
||||||
|
plot.CycleCursorMode();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- 场景 A:6 条信号合并成 1 个网格 ----
|
||||||
|
session.ShowFirstSignals(6);
|
||||||
|
if (session.LaneCount() > 1)
|
||||||
|
{
|
||||||
|
app.ExecuteToolbarAction(ToolbarAction::MERGE_LANES);
|
||||||
|
// `SetMode` 只重建 TabConfig 的网格配置;`PlotView::m_lanes` 要等下一次
|
||||||
|
// 布局/绘制才刷新 ⇒ 必须**先同步一次**再断言,否则读到的是合并前的数量。
|
||||||
|
app.SynchronizeView(true);
|
||||||
|
session.RenderFrame();
|
||||||
|
}
|
||||||
|
Expect(session.LaneCount() == 1, "signals merged into one lane");
|
||||||
|
|
||||||
|
// 收集可见信号(在开启功能**之前**:功能开关不改变可见性,
|
||||||
|
// 但 `Toggle*` 内部可能重建网格,持有 `Lane&` 跨调用是危险的)。
|
||||||
|
std::vector<SignalRef> visibleRefs;
|
||||||
|
{
|
||||||
|
const Lane& lane = app.ActiveTab().Lanes()[0];
|
||||||
|
for (const SignalRef& ref : lane.signals)
|
||||||
|
{
|
||||||
|
if (app.ActiveTab().IsVisible(ref))
|
||||||
|
{
|
||||||
|
visibleRefs.push_back(ref);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Expect(!visibleRefs.empty(), "merged lane has visible signals");
|
||||||
|
if (visibleRefs.size() < 3)
|
||||||
|
{
|
||||||
|
// 小样本文件(如 SCATTER_TEST.csv 只有 1 条信号)覆盖度较低,但检查仍然有效;
|
||||||
|
// 想要完整覆盖请用 `-data ..\TestData\MF4-Record.MF4`(566 条信号)。
|
||||||
|
EmitLine("[rowgeom] note: only %d visible signal(s); use a bigger -data for full coverage\n",
|
||||||
|
static_cast<int>(visibleRefs.size()));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const EdgePanelKind kind : kinds)
|
||||||
|
{
|
||||||
|
EnsureLaneFeature(plot, 0, kind);
|
||||||
|
}
|
||||||
|
// 把第 2 条信号也勾选上:同一面板里同时存在"展开的参数行"与"未勾选的行"。
|
||||||
|
if (visibleRefs.size() >= 2)
|
||||||
|
{
|
||||||
|
plot.ToggleLaneMeanFilterSignal(0, visibleRefs[1]);
|
||||||
|
plot.ToggleLaneEdgeCenterSignal(0, visibleRefs[0]);
|
||||||
|
plot.ToggleLaneWeakPeakSignal(0, visibleRefs[1]);
|
||||||
|
plot.ToggleLaneResampleSignal(0, visibleRefs[1]);
|
||||||
|
plot.ToggleLaneClipSignal(0, visibleRefs[1]);
|
||||||
|
}
|
||||||
|
// 「自由标记」需要真实固定点才会出现分组栏与条目行。
|
||||||
|
//
|
||||||
|
// ⚠️ 每次写入后都要**重新取** `Lanes()[0]`:`AddFreeMarker` 内部会整理/插入条目,
|
||||||
|
// 跨调用持有 `Lane&` 会在条目扩容时失效。
|
||||||
|
// ⚠️ 不能依赖"上一个场景加过的固定点还在":`ShowSignals()` 会先
|
||||||
|
// `SetAllVisible(false)`,而"全部不可见 ⇒ 丢弃空网格"是既有设计
|
||||||
|
// (`RebuildLanes` 的 MERGED 分支),网格级状态(含固定点)随之清空。
|
||||||
|
const auto addFreeMarkers = [&app](std::size_t wanted)
|
||||||
|
{
|
||||||
|
std::vector<SignalRef> refs;
|
||||||
|
{
|
||||||
|
const Lane& lane = app.ActiveTab().Lanes()[0];
|
||||||
|
for (const SignalRef& ref : lane.signals)
|
||||||
|
{
|
||||||
|
if (refs.size() >= wanted)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (app.ActiveTab().IsVisible(ref))
|
||||||
|
{
|
||||||
|
refs.push_back(ref);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (std::size_t index = 0; index < refs.size(); ++index)
|
||||||
|
{
|
||||||
|
const SignalGroup* group = app.m_dataSet.GroupAt(refs[index].groupIndex);
|
||||||
|
if (group == nullptr ||
|
||||||
|
refs[index].signalIndex >= static_cast<int>(group->signals.size()))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const SignalData& signal =
|
||||||
|
group->signals[static_cast<std::size_t>(refs[index].signalIndex)];
|
||||||
|
const TimeAxis& axis = group->AxisOf(signal);
|
||||||
|
if (axis.time.empty())
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
app.ActiveTab().Lanes()[0].AddFreeMarker(refs[index], axis.minTime);
|
||||||
|
if (index == 0)
|
||||||
|
{
|
||||||
|
const double t1 = axis.minTime + (axis.maxTime - axis.minTime) * 0.35;
|
||||||
|
app.ActiveTab().Lanes()[0].AddFreeMarker(refs[index], t1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
addFreeMarkers(2);
|
||||||
|
Expect(session.RenderFrame(), "render with dense row geometry");
|
||||||
|
|
||||||
|
std::string detail;
|
||||||
|
for (const EdgePanelKind kind : kinds)
|
||||||
|
{
|
||||||
|
const PlotView::EdgePanelLayout layout = plot.EdgePanelLayoutFor(0, kind);
|
||||||
|
AppendLayoutDump(detail, PanelKindName(kind), layout);
|
||||||
|
}
|
||||||
|
// 折起态也纳入基线(折起时内容区/按钮/滚动条必须全部为空)。
|
||||||
|
for (const EdgePanelKind kind : kinds)
|
||||||
|
{
|
||||||
|
plot.ToggleLanePanelCollapsed(0, kind);
|
||||||
|
}
|
||||||
|
Expect(session.RenderFrame(), "render with all panels collapsed");
|
||||||
|
for (const EdgePanelKind kind : kinds)
|
||||||
|
{
|
||||||
|
const PlotView::EdgePanelLayout layout = plot.EdgePanelLayoutFor(0, kind);
|
||||||
|
Expect(layout.collapsed, std::string("panel reports collapsed: ") + PanelKindName(kind));
|
||||||
|
ExpectNear(layout.panel.Height, layout.title.Height, 0.01,
|
||||||
|
std::string("collapsed panel height equals title height: ") + PanelKindName(kind));
|
||||||
|
ExpectNear(layout.content.Width, 0.0, 0.01,
|
||||||
|
std::string("collapsed panel has no content area: ") + PanelKindName(kind));
|
||||||
|
AppendLayoutDump(detail, std::string(PanelKindName(kind)) + ".collapsed", layout);
|
||||||
|
}
|
||||||
|
// 复原(后续检查依赖面板展开)。
|
||||||
|
for (const EdgePanelKind kind : kinds)
|
||||||
|
{
|
||||||
|
plot.ToggleLanePanelCollapsed(0, kind);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- 场景 B:全部信号合并成 1 个网格(只存摘要哈希)----
|
||||||
|
session.ShowFirstSignals(0);
|
||||||
|
if (session.LaneCount() > 1)
|
||||||
|
{
|
||||||
|
app.ExecuteToolbarAction(ToolbarAction::MERGE_LANES);
|
||||||
|
app.SynchronizeView(true);
|
||||||
|
}
|
||||||
|
for (const EdgePanelKind kind : kinds)
|
||||||
|
{
|
||||||
|
EnsureLaneFeature(plot, 0, kind);
|
||||||
|
}
|
||||||
|
// 固定点必须在本场景**重新加**(见 `addFreeMarkers` 上方的说明)。
|
||||||
|
addFreeMarkers(2);
|
||||||
|
Expect(session.RenderFrame(), "render with every signal in one lane");
|
||||||
|
|
||||||
|
|
||||||
|
std::string scale;
|
||||||
|
for (const EdgePanelKind kind : kinds)
|
||||||
|
{
|
||||||
|
const PlotView::EdgePanelLayout layout = plot.EdgePanelLayoutFor(0, kind);
|
||||||
|
std::string dump;
|
||||||
|
AppendLayoutDump(dump, PanelKindName(kind), layout);
|
||||||
|
std::uint64_t hash = 1469598103934665603ull;
|
||||||
|
MixText(hash, dump);
|
||||||
|
char line[160] = {};
|
||||||
|
std::snprintf(line, sizeof(line), "%s.hash=%016llX rows=%d markerRows=%d\n",
|
||||||
|
PanelKindName(kind), static_cast<unsigned long long>(hash),
|
||||||
|
static_cast<int>(layout.rowRefs.size()),
|
||||||
|
static_cast<int>(layout.markerRows.size()));
|
||||||
|
scale += line;
|
||||||
|
}
|
||||||
|
EmitLine("[rowgeom] detail lines=%d scale=%s", static_cast<int>(CountLines(detail)),
|
||||||
|
scale.c_str());
|
||||||
|
|
||||||
|
// ---- 记录 / 比对 ----
|
||||||
|
const std::string detailPath = Join(options.outDir, "rowgeom-baseline.txt");
|
||||||
|
const std::string scalePath = Join(options.outDir, "rowgeom-scale-baseline.txt");
|
||||||
|
if (options.mode == "record")
|
||||||
|
{
|
||||||
|
Expect(WriteTextFile(detailPath, detail), "write rowgeom detail baseline");
|
||||||
|
Expect(WriteTextFile(scalePath, scale), "write rowgeom scale baseline");
|
||||||
|
EmitLine("[rowgeom] baselines recorded: %s / %s\n", detailPath.c_str(),
|
||||||
|
scalePath.c_str());
|
||||||
|
}
|
||||||
|
else if (options.mode == "compare")
|
||||||
|
{
|
||||||
|
CompareBaselineText(detailPath, detail, "rowgeom detail");
|
||||||
|
CompareBaselineText(scalePath, scale, "rowgeom scale");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
EmitLine("[rowgeom] (hint) use -mode record / -mode compare for geometry A/B\n");
|
||||||
|
}
|
||||||
|
return CheckFailed();
|
||||||
|
}
|
||||||
|
|
||||||
int CheckEdgeStats(Session& session, const CheckOptions& options)
|
int CheckEdgeStats(Session& session, const CheckOptions& options)
|
||||||
{
|
{
|
||||||
session.ShowFirstSignals(1);
|
session.ShowFirstSignals(1);
|
||||||
@@ -953,6 +1447,8 @@ namespace jdi
|
|||||||
{"panels", "all 7 experiment panels: geometry invariants + pixel baseline A/B", CheckPanels},
|
{"panels", "all 7 experiment panels: geometry invariants + pixel baseline A/B", CheckPanels},
|
||||||
{"panelscroll", "panel content scrolling: every kind scrolls, clamps, and keeps content fixed",
|
{"panelscroll", "panel content scrolling: every kind scrolls, clamps, and keeps content fixed",
|
||||||
CheckPanelScroll},
|
CheckPanelScroll},
|
||||||
|
{"rowgeom", "per-field/per-row geometry baseline for all 7 panels (record/compare)",
|
||||||
|
CheckRowGeometry},
|
||||||
{"edgestats", "edge counts vs independent reference implementation", CheckEdgeStats},
|
{"edgestats", "edge counts vs independent reference implementation", CheckEdgeStats},
|
||||||
{"perf", "render / panel layout / edge query timings", CheckPerf},
|
{"perf", "render / panel layout / edge query timings", CheckPerf},
|
||||||
};
|
};
|
||||||
|
|||||||
+35
-4
@@ -147,6 +147,9 @@ Tools\OffscreenChecks\Build\OffscreenChecks.exe -list
|
|||||||
... -run smoke # 单个检查
|
... -run smoke # 单个检查
|
||||||
... -run panels -mode record # 记录面板像素基线
|
... -run panels -mode record # 记录面板像素基线
|
||||||
... -run panels -mode compare # 与基线比对(重构前后 A/B)
|
... -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 帧
|
... -run frames -frames 12 # 连续抓 12 帧
|
||||||
... -data "..\TestData\MF4-Record.MF4" -run perf
|
... -data "..\TestData\MF4-Record.MF4" -run perf
|
||||||
... -out ..\Temp\tools\run1 # 换输出目录(避免覆盖基线)
|
... -out ..\Temp\tools\run1 # 换输出目录(避免覆盖基线)
|
||||||
@@ -171,8 +174,9 @@ Tools\OffscreenChecks\Build\OffscreenChecks.exe -list
|
|||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `smoke` | 建隐藏窗口 → 加载 → 显示 3 条信号 → 渲染 → 抓帧 → 落盘 `smoke.png` | 客户区/绘图区尺寸合理、绘图区墨迹 > 1000 像素、**同一状态连续两帧逐像素相同**(渲染确定性) |
|
| `smoke` | 建隐藏窗口 → 加载 → 显示 3 条信号 → 渲染 → 抓帧 → 落盘 `smoke.png` | 客户区/绘图区尺寸合理、绘图区墨迹 > 1000 像素、**同一状态连续两帧逐像素相同**(渲染确定性) |
|
||||||
| `frames` | 沿信号列表逐行移动"鼠标",每帧抓图 + 区域哈希 + 耗时 | 平均帧 < 500ms、帧序列**至少出现一次可见变化**;输出 `frame-XXX.png` 供事后分析 |
|
| `frames` | 沿信号列表逐行移动"鼠标",每帧抓图 + 区域哈希 + 耗时 | 平均帧 < 500ms、帧序列**至少出现一次可见变化**;输出 `frame-XXX.png` 供事后分析 |
|
||||||
| `panels` | 打开全部 7 个实验性面板(含双光标下的「选区截取」) | 每个面板:功能已启用、有尺寸、**必须被绘图区夹住**、**18 个行数组长度全部等于行数**;`-mode record` 写 `panels-baseline.txt`,`-mode compare` 逐面板比对**像素哈希 + 几何** |
|
| `panels` | 打开全部 7 个实验性面板(含双光标下的「选区截取」) | 每个面板:功能已启用、有尺寸、**必须被绘图区夹住**、**行数组与行数形状一致**(按排版类别判定,见 §4.4);`-mode record` 写 `panels-baseline.txt`,`-mode compare` 逐面板比对**像素哈希 + 几何** |
|
||||||
| `panelscroll` | 全部信号合并到同一网格后,逐一滚动 7 种面板的内容区 | 可滚动时:滚到底/滚回顶、`scrollOffset` 夹到 `maxScroll`、滑块到达/返回轨道端点、内容区几何不动;不可滚动时不得出现滚动条。另守住 18 个行数组与行数完全一致 |
|
| `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` 内的**独立参考实现**(朴素逐点)**逐位相等**;同参数重复查询结果稳定 |
|
| `edgestats` | 3 个阈值下的边沿计数(普通计数)+ 中心轴计数 | 与 `Checks.cpp` 内的**独立参考实现**(朴素逐点)**逐位相等**;同参数重复查询结果稳定 |
|
||||||
| `perf` | 整帧中位数、`EdgePanelLayoutFor` 单次耗时、198 个统计状态的整轮查询耗时 | 整帧 < 500ms、面板几何 < 200µs/次、统计查询 < 20µs/次;数字用于前后对比 |
|
| `perf` | 整帧中位数、`EdgePanelLayoutFor` 单次耗时、198 个统计状态的整轮查询耗时 | 整帧 < 500ms、面板几何 < 200µs/次、统计查询 < 20µs/次;数字用于前后对比 |
|
||||||
|
|
||||||
@@ -191,7 +195,31 @@ OffscreenChecks.exe -run panels -mode compare -out ..\Temp\tools\panel-before
|
|||||||
⚠️ **像素哈希是"本机、本 DPI、本数据文件"的基线**,跨机器/跨缩放不可比。
|
⚠️ **像素哈希是"本机、本 DPI、本数据文件"的基线**,跨机器/跨缩放不可比。
|
||||||
它适合"我改了一处,其它像素有没有动"的**本地 A/B**,不适合入库当黄金图。
|
它适合"我改了一处,其它像素有没有动"的**本地 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` | 关键状态截图 |
|
| `smoke.png` / `panels.png` / `perf.png` | 关键状态截图 |
|
||||||
| `frame-000.png` … | 连续帧(问题分析用) |
|
| `frame-000.png` … | 连续帧(问题分析用) |
|
||||||
| `panels-baseline.txt` | 面板像素基线(`-mode record` 生成) |
|
| `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` |
|
| 任何数据层 / 解析 / 公式 / 变换 | `UnitTests.exe` |
|
||||||
| 绘图、面板、布局、缓存键 | `-run smoke -run panels`(+ 需要时 `-mode compare`) |
|
| 绘图、面板、布局、缓存键 | `-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 edgestats -run perf` |
|
||||||
| 性能相关 | `-run perf`(前后各跑一次,比 `us/query` 与 `median ms`) |
|
| 性能相关 | `-run perf`(前后各跑一次,比 `us/query` 与 `median ms`) |
|
||||||
| 完全不确定影响面 | `-all`(约几秒到几十秒)+ `UnitTests.exe` |
|
| 完全不确定影响面 | `-all`(约几秒到几十秒)+ `UnitTests.exe` |
|
||||||
|
|||||||
Reference in new issue
Block a user