refactor(panels): 平行数组合并为 std::vector<PanelRow>(消除错位与字段重载)

对应 Code_Review_Report §15.6 第 1 项(方案 B:有结构,无列视图)。

结构
- EdgePanelLayout 的 16 个平行 std::vector 合并为 std::vector<PanelRow> rows,
  PanelRow 带 17 个具名成员:ref / header / nameRect / nameClose / triangle / check /
  stats / levelTrack / levelValue / levelReset / iterTrack / iterValue / iterReset /
  infoMaxInterval / infoMinInterval / intervalField / frequencyField。
- 唯一新增入口:AppendRow(ref, header, nameRect, triangle, check)(返回新行引用)与
  AppendMarkerGroupRow(ref, nameRect, nameClose)(「自由标记」分组行)。
  参数行/重采样行一律 layout.rows.back().xxx = … 赋值。
- 原 int rows(可见信号数)与新容器同名 ⇒ 改名 signalCount,并注明它与 rows.size()
  在 MARK 面板里不相等(只有有固定点的信号才有行)。

消掉的两类问题
1. 某分支漏 push 一列 ⇒ 该列之后所有行读到别人的矩形(静默错位)。现在一行一次成型。
2. rowRefs 字段重载(列表式 = 行信号 / MARK = 分组信号,其余 12 列必须为空)。
   现在每行自带 ref,语义由“填哪些字段”表达;不变式简化为语义断言
   (每行必须有名称区;MARK 行不得带 header/check/stats/滑块/重采样字段)。

顺带清理
- 合并后 rowIndex < layout.rowXxx.size() 全部变成恒真,批量删除 50+ 处
  (含 15 元组长度数组、? x : gd::RectF() 三元判空、if(...){extendRow(...)} 包裹)。
- 调试框与缓存键改为逐行遍历(新增字段只需在 PanelRow 与这两处循环各加一行)。

等价性证明(rowgeom 键名刻意保持不变,故可逐键比对)
  old keys = 919, new keys = 965
  仅存在于旧 = 0;已有键值变化 = 20(全部是 ".count":0 -> N)
  仅存在于新 = 46(全部是新增列的 [NNN],值 0.00,0.00,0.00,0.00)
⇒ 逐行逐字段的真实几何零位移;7 个面板像素哈希(含 resample=2BFC74B3D9B3239C)逐项不变。

验证
- make release:零 warning;784,896 → 782,336 B(−2,560);make debug / make -C Tools 同样零 warning
- UnitTests:checks=260 failed=0 cases=19
- OffscreenChecks -all:7 检查 / 133 断言(上轮 127)全 PASS
- panels -mode compare:7 面板像素哈希逐项不变
- rowgeom -mode compare:965+7 键 0 差异;-data MF4-Record.MF4 时 1268+7 键 0 差异
- 读点:旧 243 处 layout.rowXxx[…] → 现 95 处 layout.rows[…](84 命中判定 / 11 绘制)

过程记录:清理恒真判空时曾用整段替换把「均值滤波」双滑块段误换成「弱峰」双滑块,
编译器报 brace 不平衡,用备份回滚后重做 —— 整段替换必须先精确读出上下文。

文档:Plan §十一、Progress 第 39 轮、Report §16、agent_mem 第 39 轮、Tools/README §4.1/§4.4
This commit is contained in:
DeepSeek V4.1 Flash committed 2026-11-04 16:18:56 +08:00
1 parent f07ac74988
commit 1463f9d3f8
11 files changed
+672 -591

No files matched your search

+37
View File
@@ -343,3 +343,40 @@
| 通用参数行(中心轴统计 + 两行滑块) | ⏸ 未提取:依赖 3 个 kind 标志 + 2 套拖拽/编辑状态 + 2 个滑块 lambda,提取需传 9 个参数,可读性收益低于参数表本身 |
| 条目栏(三角 + 名称 + 复选框 + 悬停) | 保留在 `PaintPanelListRows` 里(它是三种排版的公共前缀) |
---
## 十一、第 39 轮追加(2026-11-04):平行数组合并为 `std::vector<PanelRow>`(§15.6 第 1 项)
> 触发:用户要求「继续按照上轮的建议继续推进」。上一轮结论是"若要做就做**方案 B**:
> `std::vector<PanelRow>` + 具名成员访问(**无视图**),并且必须单独一轮"。
| 节点 | 内容 | 验收标准 |
| --- | --- | --- |
| **P33** | `EdgePanelLayout` 的 16 个平行 `std::vector` 合并为 `std::vector<PanelRow> rows` | `rowgeom -mode compare`:**0** 条键消失、**0** 条已有键的值变化;新增键**全部**是空矩形 |
| **P34** | 唯一入口收敛为 `AppendRow` / `AppendMarkerGroupRow`;`int rows` 改名 `signalCount` | 编译零 warning;7 个面板像素哈希**逐项不变** |
| **P35** | 删掉因合并而变成恒真的 50+ 处 `rowIndex < layout.rows.size()` 判空 | 全量检查 133 断言全绿 |
### 11.1 本轮新增硬约束
1. **行的几何**只能通过 `rows[i].<字段>` 访问;**禁止**再加回平行数组,也**禁止**引入
`PanelColumn<T>` 列视图(拷贝 `EdgePanelLayout` 后忘重绑 = 静默悬垂,没有检查可以抓到)。
2. **新增一个行字段** = `PanelRow` 加一个成员 + `AppendRow` 里赋默认值(现在连这一步都不用,
成员自带默认构造)+ 需要绘制的地方读 `rows[i].字段`。**不需要**再同步任何"列清单"。
3. **`signalCount` ≠ `rows.size()`**:前者是"网格里的可见信号数"(`EdgePanelLayoutFor` 开头统计),
后者是"本面板实际产生的行数"。两者在「自由标记」面板里**不相等**(只有有固定点的信号才有行)。
4. **`rowgeom` 的键名保持 `<panel>.<旧列名>[NNN]`**:这样重构前后的基线可以逐字节比对,
等价性不靠人工核对。
### 11.2 等价性是怎么证明的(可复用的流程)
```text
old keys = 919, new keys = 965
仅存在于旧 = 0 ← 没有任何键消失
已有键的值变化 = 20 ← 全部是 ".count" 键:0 -> N(结构变化,不是几何变化)
仅存在于新 = 46 ← 全部是 <column>[NNN],且值全为 0.00,0.00,0.00,0.00
```
含义:旧结构里"某列根本没被 push"的那些行,现在每行都拥有该字段(值为空矩形)。
**逐行逐字段的真实几何一位都没变**,7 个面板的像素哈希也逐项未变。
+79
View File
@@ -550,3 +550,82 @@ only-in-OLD lines NOT belonging to rowPoints/rowIterations = 0
| `rowgeom -mode compare` | 919 + 7 键,0 差异 |
| 行数 | `PlotViewPaint.cpp` 5575 → 5565;`PlotViewPanelPaint.cpp` 1016 → 1020(净增 4 行:函数头 + 文档) |
---
## 第 39 轮(2026-11-04):平行数组合并为 `std::vector<PanelRow>`(§15.6 第 1 项)
> 这是"面板改一次就出错"的**最后一个机制性隐患**:旧的 16 个平行数组靠"每个布局分支
> 按同一顺序 push 恰好 N 项"维持对齐,任何一处漏推都是静默错位。
### P33 合并成 `std::vector<PanelRow>`
```cpp
struct PanelRow
{
SignalRef ref; // 该行的信号(MARK = 分组的代表信号)
gd::RectF header; // 信号色叠底条目栏 / 分组栏
gd::RectF nameRect; // 名称区
gd::RectF nameClose; // 名称右侧 ✕(仅 MARK)
gd::RectF triangle; // 展开/收起三角
gd::RectF check; // 右端复选框
gd::RectF stats; // 「中心轴」上升/下降沿统计行
gd::RectF levelTrack / levelValue / levelReset; // 滑块 1
gd::RectF iterTrack / iterValue / iterReset; // 滑块 2
gd::RectF infoMaxInterval / infoMinInterval; // 「重采样」两行只读统计
gd::RectF intervalField / frequencyField; // 「重采样」两行输入框
};
std::vector<PanelRow> rows; // 唯一的行容器
```
* 唯一入口:`AppendRow(ref, header, nameRect, triangle, check)`(返回新行引用)
与 `AppendMarkerGroupRow(ref, nameRect, nameClose)`(「自由标记」分组行)。
两者都**没有**"补齐其余列"这一步 —— 因为字段天生就在同一行里。
* 参数行 / 重采样行从 `layout.rowXxx.back() = …` 变成 `layout.rows.back().xxx = …`。
* 原来的 `int rows`(可见信号数)与新容器同名 ⇒ 改名 **`signalCount`**,
并注明它与 `rows.size()` 在 MARK 面板里**不相等**(只有有固定点的信号才有行)。
### P34 顺带消失的两类问题
| 旧问题 | 现在 |
| --- | --- |
| 某分支漏推一列 ⇒ 该列之后所有行读到别人的矩形 | `rows.push_back(row)` 一次成型,**不可能**错位 |
| `rowRefs` 字段重载(MARK 借它表示"分组",其余 12 列必须为空,长度不变式要按类别分流判定) | 每行自带 `ref`,语义由"填哪些字段"表达;不变式简化为"每行都要有名称区 + MARK 行不得带参数行字段" |
### P35 清理恒真判空
合并后 `rowIndex < layout.rowXxx.size()` 一律变成 `rowIndex < layout.rows.size()`,
而它们全都在 `for (… rowIndex < layout.rows.size(); …)` 循环体内 ⇒ **恒真**。
用正则批量删除 50+ 处(含 `a.size() != rows` 的 15 元组长度数组、`? x : gd::RectF()` 三元判空、
`if (rowIndex < …) { extendRow(...); }` 包裹),并逐文件编译验证。
⚠️ 中途有一次误操作:把"均值滤波双滑块"那段替换成了"弱峰双滑块",
编译器报 brace 不平衡 + 大面积类型错误 ⇒ 用备份 `Temp/PanelPaint.pre-cleanup.cpp` 回滚后重做。
**教训**:这类"整段替换"必须先用 `read_file` 精确读出 5 行上下文,不要凭记忆拼 oldString。
### 等价性证明(本轮最关键的一步)
```
old keys = 919, new keys = 965
仅存在于旧 = 0 ← 没有任何键消失
已有键的值变化 = 20 ← 全部是 ".count":0 -> N
仅存在于新 = 46 ← 全部是 <column>[NNN],值全为 0.00,0.00,0.00,0.00
```
⇒ 旧结构里"某列没被 push"的那些行,现在每行都拥有该字段但值为空矩形;
**逐行逐字段的真实几何一位都没变**,7 个面板的像素哈希(含 `resample=2BFC74B3D9B3239C`)
也逐项未变。之后按新结构重录正式基线(965 条键)。
### 本轮验证
| 项 | 结果 |
| --- | --- |
| `make release`(全量) | 零 error / 零 warning;产物 784,896 → **782,336 B**(−2,560) |
| `make debug` / `make -C Tools` | 零 error / 零 warning |
| `UnitTests` | `checks=260 failed=0 cases=19` |
| `OffscreenChecks -all` | 7/7 PASS,`checks=133 failed=0`(上轮 127:新增行结构断言) |
| `panels -mode compare` | 7 个面板像素哈希**逐项不变** |
| `rowgeom -mode compare` | 965 + 7 键,0 差异(默认数据);1268 + 7 键(MF4 566 行),0 差异 |
| 读点数 | 旧的 243 处 `layout.rowXxx[…]` 全部消失;现在 95 处 `layout.rows[…]`(84 在命中判定、11 在绘制) |
| 行数 | `PlotView.hpp` 4795;`PlotViewInteraction.cpp` 4399;`PlotViewPanelPaint.cpp` 935;`PlotViewPaint.cpp` 5534;`PlotViewPanels.cpp` 1729 |
+70
View File
@@ -683,3 +683,73 @@ B 的**剩余收益**主要是"单一分配 + 少 15 个 vector 头"(`EdgePane
让"MARK 面板 12 列为空"的注释消失。
3. 面板几何算式重写(§13.6 遗留):基线已就绪,但需先完成 1 或 2。
---
## 十六、第 39 轮:平行数组合并为 `std::vector<PanelRow>`(§15.6 第 1 项,方案 B)
### 16.1 结论
| §15.6 建议 | 结果 |
| --- | --- |
| 1. 方案 B(`vector<PanelRow>` 无视图) | ✅ 完成:16 个平行数组 → `std::vector<PanelRow> rows`(17 个具名成员) |
| 2. 在 B 之上拆分 `rowRefs` 双语义 | ✅ **顺带完成**:`rows[i].ref` 取代重载的 `rowRefs`,MARK 分组行改走 `AppendMarkerGroupRow` |
| 3. 面板几何算式重写 | ⏸ 未做(本轮只做结构合并,算式未动) |
### 16.2 结构对比
| | 旧(16 个平行数组) | 新(`std::vector<PanelRow>`) |
| --- | --- | --- |
| 新增一列 | 改 `AppendRow` + 14 处布局分支 + 绘制 + 缓存键 + 调试框 + 检查的 15 元组 | 只在 `PanelRow` 加一个成员 |
| 漏推一列 | 该列之后**所有行**读到别人的矩形(静默) | 不可能(一行一次成型) |
| `rowRefs` 语义 | 列表式 = 行信号;MARK = 分组信号(同一字段两种含义) | `rows[i].ref` 一种含义;MARK 的分组行只填 `nameRect`/`nameClose` |
| 长度不变式 | 15 个 `size()` 组成的判据,且 MARK 要单独分流 | 恒真 ⇒ 改为语义断言(每行都要有名称区;MARK 行不得带参数行字段) |
| `EdgePanelLayout` 每实例 | 16 个 vector 头(16×24 B)+ 各自独立分配 | 1 个 vector 头 |
### 16.3 为什么这次敢做(前一轮不敢)
上一轮的顾虑是"列视图拷贝后忘重绑 = 静默悬垂"。方案 B **没有视图**:
所有访问都是 `rows[i].check`,`PanelRow` 是值类型、随 `rows` 一起拷贝,**结构上不可能悬垂**。
代价是 243 处读点必须机械改写(不省),收益是"漏推一列"与"字段重载"两类问题**从根上消失**。
### 16.4 等价性证明(本轮方法论的核心)
`rowgeom` 的键名刻意保持 `<panel>.<旧列名>[NNN]`,于是重构前后可以直接逐键比对:
```
old keys = 919, new keys = 965
仅存在于旧 = 0 ← 没有任何键消失
已有键的值变化 = 20 ← 全部是 ".count" 键:0 -> N(结构变化)
仅存在于新 = 46 ← 全部是新增列的 [NNN],值 0.00,0.00,0.00,0.00
```
再叠加 7 个面板的像素哈希逐项不变 ⇒ **渲染与几何都没有任何位移**。
这比"编译通过 + 肉眼看一眼"强得多:它把"等价重构"变成了**可证伪**的断言。
### 16.5 过程中唯一的一次事故
清理恒真判空时用"整段替换"把「均值滤波」双滑块段落误替换成「弱峰」双滑块,
编译器立刻报 brace 不平衡;用备份回滚后重做。
**结论:整段替换必须先精确读出上下文,oldString 不能凭记忆拼。**
### 16.6 验证
| 项 | 结果 |
| --- | --- |
| `make release` | 零 warning;784,896 → **782,336 B**(−2,560) |
| `make debug` / `make -C Tools` | 零 warning |
| `UnitTests` | 260 / 19 PASS |
| `OffscreenChecks -all` | 7 / **133** PASS(上轮 127) |
| `panels -mode compare` | 7 面板像素哈希逐项不变 |
| `rowgeom -mode compare` | 965 + 7 键 / 1268 + 7 键(MF4),0 差异 |
### 16.7 下一步(按价值排序)
1. **面板几何算式重写**(§13.6 遗留):结构合并与逐行基线都已就绪,现在"改算式"是**安全的**;
建议先做 `EdgePanelWidth`(逐信号量宽)与 `EdgePanelLayoutFor` 的行高累加,
判据 = `rowgeom -mode compare` + `panels -mode compare` 双基线归零。
2. **`PaintPanelListRows` 的通用参数行**提取(上轮因需 9 个参数而搁置):
现在 `row` 是一个整体、`extendRow` 已成组,可把"参数行"收成一个函数,
参数表从 9 个降到 `(g, layout, row, kind, state)` 5 个。
3. 若还要压体积:`EdgePanelLayout` 已无冗余列,剩余空间主要在字符串/浮点格式化(见 §11 的 gdtoa 结论)。
+63
View File
@@ -10695,3 +10695,66 @@ B 属**整洁性收益**(漏推一列已被 `AppendRow` + 长度不变式挡
`rows[].ref` + `markerGroups[]`,让"MARK 面板 12 列为空"的注释消失。
2. 面板几何算式重写(§13.6 遗留):基线已就绪,但需先完成 1。
---
## 2026-11-04 · 第 39 轮(`Code_Review` 分支):平行数组合并为 `std::vector<PanelRow>`(§16.6 第 1 项,方案 B)
> 规则原文:`Documents/Code_Review_Report.md` §16、`Code_Review_Plan.md` §十一、
> `Code_Review_Progress.md` 第 39 轮。
### 1. ✅ `EdgePanelLayout` 的 16 个平行 `std::vector` → `std::vector<PanelRow> rows`
```cpp
struct PanelRow {
SignalRef ref; header; nameRect; nameClose; triangle; check; stats;
levelTrack; levelValue; levelReset; iterTrack; iterValue; iterReset;
infoMaxInterval; infoMinInterval; intervalField; frequencyField;
};
std::vector<PanelRow> rows;
```
唯一入口:`AppendRow(ref, header, nameRect, triangle, check)`、
`AppendMarkerGroupRow(ref, nameRect, nameClose)`(MARK 分组行)。
**参数行一律 `layout.rows.back().xxx = …` 赋值。**
### 2. 🔴 硬规则(本轮新增)
1. 行的几何**只能**通过 `rows[i].<字段>` 访问。**禁止**加回平行数组;
**禁止**引入 `PanelColumn<T>` 列视图(拷贝后忘重绑 = 静默悬垂,没有检查能抓到)。
2. **新增字段**只需在 `PanelRow` 加一个成员 —— 不再需要同步"列清单"
(`AppendRow` / 调试框 / 缓存键 / 检查里的 15 元组都已不存在)。
3. **`signalCount`(可见信号数)≠ `rows.size()`(本面板行数)**:MARK 面板里只有
"有固定点的信号"才有行。原 `int rows` 已改名 `signalCount`。
4. 长度不变式在结构上与恒真 ⇒ 已改为**语义断言**:每行必须有名称区;
MARK 行不得带 `header`/`check`/`stats`/滑块/重采样字段(防止把列表式布局误用到它上面)。
### 3. ⭐ `rowgeom` 键名刻意保持不变 ⇒ 等价重构**可证伪**
```
old keys = 919, new keys = 965
仅存在于旧 = 0 ; 已有键值变化 = 20(全是 ".count": 0 -> N)
仅存在于新 = 46(全是新增列 [NNN],值 0.00,0.00,0.00,0.00)
```
再叠加 7 面板像素哈希逐项不变 ⇒ 渲染/几何零位移。
**以后做"等价重构"都照这个流程:先保键名,再比对。**
### 4. ⚠️ 事故与教训
清理恒真判空时用"整段替换"把「均值滤波」双滑块段误换成「弱峰」双滑块,编译器立刻报
brace 不平衡。用备份回滚后重做。
**整段替换必须先 `read_file` 精确读出上下文,oldString 不能凭记忆拼。**
### 5. 本轮数字
* Release 784,896 → **782,336 B**(−2,560);release/debug/tools 全零 warning。
* `UnitTests` 260/19;`OffscreenChecks -all` 7 检查 / **133** 断言全 PASS(上轮 127)。
* `panels -mode compare` 7 面板像素哈希逐项不变;`rowgeom compare` 965+7 / 1268+7 键 0 差异。
* 读点:旧 243 处 `layout.rowXxx[…]` → 现 95 处 `layout.rows[…]`(84 命中判定 / 11 绘制)。
### 6. 下一步
1. **面板几何算式重写**(§13.6 遗留)—— 结构与双基线都已就绪,现在改算式是安全的;
建议先动 `EdgePanelWidth`(逐信号量宽)与 `EdgePanelLayoutFor` 的行高累加。
2. `PaintPanelListRows` 的"通用参数行"提取:现在参数表可从 9 个降到 5 个
(`g, layout, row, kind, state`)。
3. 体积剩余空间主要在字符串/浮点格式化(gdtoa 30+ KB 不可去掉,见 §11)。