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

+17 -19
View File
@@ -380,7 +380,7 @@ namespace jdi
{
return layout;
}
layout.rows = rowCount;
layout.signalCount = rowCount;
const float width = EdgePanelWidth(laneIndex, kind);
const float titleHeight = kEdgePanelTitleHeight * m_scale;
@@ -735,7 +735,7 @@ namespace jdi
{
continue;
}
// ⚠️ 行的追加走 `AppendRow()`(**唯一入口**):18 个平行数组一次补齐,
// ⚠️ 行的追加走 `AppendRow()`(**唯一入口**):一行的全部字段一次成型,
// 不会出现"某个分支漏推一列 ⇒ 该列之后所有行读到别人的矩形"。
layout.AppendRow(ref, gd::RectF(contentLeft, rowY, innerWidth, rowH),
gd::RectF(contentLeft + 4.0f * m_scale, rowY,
@@ -877,9 +877,9 @@ namespace jdi
itemCollapsed = (item != nullptr && item->collapsed);
}
// 组头(信号色叠底):整行宽度,左侧三角(仅勾选时)+ 名称,右端复选框。
// ⚠️ 行的追加走 `AppendRow()`(**唯一入口**):18 个平行数组一次补齐,
// ⚠️ 行的追加走 `AppendRow()`(**唯一入口**):一行的全部字段一次成型,
// 不会出现"某个分支漏推一列 ⇒ 该列之后所有行读到别人的矩形"。
// 下面的参数行 / 重采样行一律用 `rowXxx.back() = …` **赋值**(长度天然对齐)。
// 下面的参数行 / 重采样行一律用 `layout.rows.back().xxx = …` **赋值**(同一行,天然对齐)。
layout.AppendRow(
ref, gd::RectF(contentLeft, rowY, innerWidth, groupRow),
gd::RectF(contentLeft, rowY, innerWidth, groupRow),
@@ -903,43 +903,43 @@ namespace jdi
// 「重采样」四行:① 最大间隔(只读)② 最小间隔(只读)
// ③ 重采样间隔(输入)④ 重采样频率(输入,与③联动)。
// 每行 = 左侧标签 + 右侧数值/输入框区域。
layout.rowInfoMaxInterval.back() = gd::RectF(contentLeft, rowY, innerWidth, sliderRow);
layout.rows.back().infoMaxInterval = gd::RectF(contentLeft, rowY, innerWidth, sliderRow);
rowY += sliderRow;
layout.rowInfoMinInterval.back() = gd::RectF(contentLeft, rowY, innerWidth, sliderRow);
layout.rows.back().infoMinInterval = gd::RectF(contentLeft, rowY, innerWidth, sliderRow);
rowY += sliderRow;
// 输入行的**数值矩形**靠右对齐,宽度取剩余可用宽度(绘制框与编辑框同源)。
const float fieldLeft = contentLeft + rowLabelColumn + 8.0f * m_scale;
const float fieldWidth =
std::max(24.0f * m_scale, innerWidth - rowLabelColumn - 8.0f * m_scale);
layout.rowIntervalField.back() = gd::RectF(fieldLeft, rowY, fieldWidth, sliderRow);
layout.rows.back().intervalField = gd::RectF(fieldLeft, rowY, fieldWidth, sliderRow);
rowY += sliderRow;
layout.rowFrequencyField.back() = gd::RectF(fieldLeft, rowY, fieldWidth, sliderRow);
layout.rows.back().frequencyField = gd::RectF(fieldLeft, rowY, fieldWidth, sliderRow);
rowY += sliderRow;
continue;
}
// 「中心轴边沿计数」:参数行最上面是**同一行**的「上升沿 / 下降沿」统计。
if (centerAxis && !layout.rowStats.empty())
if (centerAxis && !layout.rows.empty())
{
layout.rowStats.back() = gd::RectF(contentLeft, rowY, innerWidth, groupRow);
layout.rows.back().stats = gd::RectF(contentLeft, rowY, innerWidth, groupRow);
rowY += groupRow;
}
const gd::RectF pointsTrack(trackLeft, rowY + (sliderRow - trackHeight) * 0.5f,
trackWidth, trackHeight);
layout.rowLevelTracks.back() = pointsTrack;
layout.rowLevelValues.back() =
layout.rows.back().levelTrack = pointsTrack;
layout.rows.back().levelValue =
gd::RectF(pointsTrack.X + pointsTrack.Width + 6.0f * m_scale, rowY, valueColumn,
sliderRow);
layout.rowLevelResets.back() =
layout.rows.back().levelReset =
gd::RectF(resetLeft, rowY + (sliderRow - resetSize) * 0.5f, resetSize, resetSize);
rowY += sliderRow;
const gd::RectF iterTrack(trackLeft, rowY + (sliderRow - trackHeight) * 0.5f,
trackWidth, trackHeight);
layout.rowIterTracks.back() = iterTrack;
layout.rowIterValues.back() =
layout.rows.back().iterTrack = iterTrack;
layout.rows.back().iterValue =
gd::RectF(iterTrack.X + iterTrack.Width + 6.0f * m_scale, rowY, valueColumn,
sliderRow);
layout.rowIterResets.back() =
layout.rows.back().iterReset =
gd::RectF(resetLeft, rowY + (sliderRow - resetSize) * 0.5f, resetSize, resetSize);
rowY += sliderRow;
}
@@ -1136,9 +1136,7 @@ namespace jdi
// 只登记"在可视内容区里"的分组栏与条目(滚出视界的条目不得抢走
// 底部按钮 / 滑块的点击)。
layout.markerGroups.push_back(group);
layout.rowRefs.push_back(ref);
layout.rowNameRects.push_back(group.header);
layout.rowNameCloses.push_back(group.close);
layout.AppendMarkerGroupRow(ref, group.header, group.close);
}
rowY += groupRow;
if (group.collapsed)