对应 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
37 KiB
Code Review 进度(分支 Code_Review)
每个节点完成后追加:节点号、改动摘要、验证方式、实测数字。 计划见
Documents/Code_Review_Plan.md;最终报告见Documents/Code_Review_Report.md。
基线(起点 764e5be)
| 项 | 数值 |
|---|---|
| Release 产物 | 890,880 B |
.text |
753,136 B |
.rdata |
36,936 B |
.pdata |
30,420 B |
.xdata |
29,496 B |
.rsrc |
20,656 B |
.idata |
12,856 B |
| Debug 产物 | 1,801,617 B |
| 源码规模 | 约 5.6 万行 |
关键发现(详见计划 §一):
dynamic_cast(Toolbar.cpp:370)使最小复现程序从 39,936 B 涨到 115,200 B ⇒ +75,264 B;map 中cp-demangle.o占 56,320 B。- 每个
EdgePanelKind在源码中出现 21–26 次(6 种面板 ⇒ 约 140 处手写分支)。
节点记录
(下面按完成顺序追加)
P1 移除 dynamic_cast + 关闭 RTTI —— ✅ 完成(提交 5de3763)
改动
Toolbar::BuildBoxes里唯一的dynamic_cast是多余强转(Button::widget本来就是UiButtonWidget*)。改为直接遍历m_buttons(与m_root->Children()同源同序)。- 仅关 RTTI 不够:
cp-demangle.o仍在链接里。真凶是 libstdc++ 的vterminate.o(引用__cxa_demangle),它被eh_terminate.o的std::terminate拉入。 ⇒ 在Utilities/Src/MemProbe.cpp里自行定义__gnu_cxx::__verbose_terminate_handler(), 归档成员vterminate.o不再被拉入,demangler 整体消失。 应用启动后本来就会std::set_terminate换成自己的处理函数,行为等效。 Makefile新增NO_RTTI=1(默认开)→CXXFLAGS += -fno-rtti。
实测
| 指标 | 改前 | 改后 | 差 |
|---|---|---|---|
| Release | 890,880 B | 829,440 B | −61,440 B |
.text |
753,136 B | 703,616 B | −49,520 B |
.rdata |
36,936 B | 26,616 B | −10,320 B |
| Debug | 1,801,617 B | 1,734,578 B | −67,039 B |
验证:release + debug 全量重建(删 *.o/*.d)零 error 零 warning;
map 中 cp-demangle.o 已无 .text 段分配。
P2 Release 关闭异步展开表 + 修正 Debug 构建参数 —— ✅ 完成(提交 94fc1c4)
改动
- 新增
RELEASE_ONLY_OPT = -fno-asynchronous-unwind-tables,只加在 Release 的两条 编译规则上。-fno-exceptions下绝大多数函数不再需要.pdata/.xdata。- 代价(已评估接受):Release 崩溃时 WER 拿不到可靠调用栈;不打印栈回溯的程序 功能不受影响,Debug 构建保留完整展开表。
-fno-unwind-tables(全关)会链接失败,不采用。
- 修正真实缺陷:
DEBUG_OPT = -O0定义了却从未被使用 —— Debug 的两条编译规则 误用$(RELEASE_OPT)(-Oz),且全程没有-g。所谓 "Debug 构建" 其实只是 "不 strip 的 Release",根本无法调试。现改用$(DEBUG_OPT) -g,链接也加-g。
实测
| 指标 | 改前 | 改后 |
|---|---|---|
| Release | 829,440 B | 793,088 B(−36,352) |
.pdata |
30,420 B | 12,684 B |
.xdata |
28,904 B | 10,156 B |
| Debug | 1,734,578 B | 5,992,064 B(真实 -O0 -g,属预期) |
验证:release 与 debug 均全量重建,零 error 零 warning(-O0 下也没有新警告)。
P7 面板状态/滚动量收敛为唯一派发点 —— ✅ 完成(提交 7a3fefb)
问题(屎山核心之一):Lane 里 7 种实验性功能各有 1 份 LanePanelState + 1 个滚动偏移
(14 个字段)。旧实现在每一处“按种类取用”的地方各写一遍嵌套三元式或 switch:
EdgePanelLayoutFor 5 条 7 层嵌套三元式;标题栏拖拽、✕ 关闭、滚轮、滚动条、错开序数各写一遍。
新增面板要改 20+ 处,漏一处就是「拖不动 / 关不掉 / 滚轮滚不到底」
(MARK、RESAMPLE、CLIP 均真实踩过)。
改动:PlotViewInternal.hpp 新增唯一派发点
LanePanelEnabled / LanePanelStateOf / LanePanelScrollOf(可写 + 只读)。
这些 switch 穷举 7 种且不写 default: ⇒ -Wswitch 在漏登记时编译报警
(已用最小程序验证 -Wswitch 确实会报)。
调用点:布局 5 条三元式 → 3 行;标题栏拖拽 7 分支 switch → 1 次写入;
滚轮/滚动条/✕ 关闭/折叠切换全部改为穷举无 default。
验证:release + debug 零 warning;产物 793,088 B 不变(等价变换,说明只是结构收敛)。
未做(受验证条件限制):尚未把 EdgePanelLayoutFor 的 957 行分支整体改成
“每面板一行元数据表”。原因:没有可用的离屏像素对比工具(.preflight 已删除,
且崩溃会弹模态框),纯逻辑重写无法自证“像素完全一致”。已通过上面的派发收敛
消除最容易出错的一类重复,剩余重复属于“几何算式本身”,风险高于收益。
P11 拆分 ApplicationCommands.cpp —— ✅ 完成(提交 8437100)
原文件 2,165 行,混杂六类职责。按职责纯搬移(零逻辑改动):
| 新文件 | 行数 | 内容 |
|---|---|---|
ApplicationTabs.cpp |
355 | 标签页新建/关闭/切换/重排、视图同步、窗口标题、标签栏回调、标签页右键菜单 |
ApplicationDerive.cpp |
786 | 派生烘焙、重采样、选区截取(新建/就地)、克隆(含带调整) |
ApplicationMenus.cpp |
409 | 绘图区右键菜单、菜单数值回写、主题切换、设置菜单、菜单结果派发 |
ApplicationCommands.cpp |
642 | 分组/信号增删、移动与重排、文件对话框、工具栏动作、键盘鼠标分发、布局边界绘制 |
Makefile 按目录通配收集源文件,无需登记新文件。
验证:release 零 error 零 warning;产物 793,088 → 794,112 B(+1,024,LTO 内联边界 变化,< 0.13%)。
P15 注释/文档校正 —— ✅ 完成(提交 f139440)
PlotView.hpp:删除指向已删除代码的FormulaEnvelopeEntry注释块。agent_mem.md§2:-Os→-Oz;补体积开关(NO_EXCEPTIONS/NO_RTTI/LTO/RELEASE_ONLY_OPT)与「改 Makefile 必须删.o全量重编」提醒;补msimg32; 忽略列表补Temp/、.preflight/、Tools/。agent_mem.md§3:目录结构按现状重写(删除不存在的CsvTable/、DataModel/、PaintUtil/等旧条目;写明Components/Shared与Utilities的真实职责和依赖方向)。agent_mem.md末尾新增「第 33 轮」记录。
P16 最终验证与报告 —— ✅ 完成
- release(全量)+ debug(全量
-O0 -g)零 error 零 warning。 - 最终产物 794,112 B(起点 890,880 B,−96,768,−10.9%)。
cp-demangle.o已无.text段分配;__gnu_cxx::__verbose_terminate_handler定义在本产物内(0x140057ed1);导入 DLL 无新增。- 报告:
Documents/Code_Review_Report.md。
未完成/搁置的节点(均已在报告中说明原因)
- P8
SceneContentHash混入收敛:无逐像素验证手段,合并会改变哈希值,风险不划算。 - P9
EdgePanelLayoutFor表驱动化:同上(957 行几何分支,无法自证像素一致)。 - P10 缓存五件套抽象:同上,且热路径复杂。
- P12
PlotView.hpp拆分:嵌套类型外移需改 70+ 处引用,收益仅为可读性。 - P13/P14 UI 基础层与绘制边界核对:已做依赖方向与引用统计,未发现重叠或反向依赖, 因此不再拆分。
- 性能项:未发现新的可复现热点;已记录 3 个"还能再快一点"的点(见报告 §6.2)。
第 34 轮(2026-11-04):验证基础设施 + §10 后续建议落地
T1 验证工具入库 —— ✅ 完成(提交 97e9163)
改动
.gitignore:删除整目录忽略的Tools/,改为Tools/**/Build/(只忽略各工具产物)。- 新增
Tools/Makefile:make/make UnitTests/make OffscreenChecks/make clean/make help; 工具链接本体../Build/Release/*.o(排除Main.o),-MMD -MP跟踪头文件依赖。 - 新增
Tools/UnitTests(Inc+Src+Build)与Tools/OffscreenChecks(同构)与Tools/README.md。
踩到并修掉的两个构建陷阱
- 工具源码也加
-flto会让 GCC 15.2.0 在链接阶段choose_baseaddr(i386.cc:7447)ICE ⇒ 工具源码不带-flto,只在链接参数里带-flto -flto-partition=one。 $(foreach)生成的工具目标早于all出现 ⇒ make 把第一个显式目标当默认目标, 直接make只会构建UnitTests⇒ 显式.DEFAULT_GOAL := all。
实测:make -C Tools 零 warning;两个产物分别在 Tools/<Tool>/Build/<Tool>.exe。
T2 单元测试 —— ✅ 完成(219 条断言 / 18 个用例)
覆盖:TryParseDouble 与 libc strtod 逐位一致(6 万条随机十进制串)、
输入接受性(尾随字符 / 十六进制浮点 / inf / nan)、FormatDecimal* 永不科学计数法、
NiceStep / BuildTicks、公式语法与语义、FormulaProgram 与 EvaluateFormula 逐位一致、
ApplyDrawTransform 顺序、TransformValueRange、滑动平均多阶等价性、
DataSet::FormulaTransformFor 值域与分组极值(逐点参考实现比对、命中同指针、样式变化失效)、
以及新增的 stdio_mingw_vs_ucrt_formats(见 T6)。
过程中修正了 5 处"我以为的语义"(都以源码为准,测试跟着产品改):
9.999999999999999 属 >15 位有效数字(允许 1 ULP);1..2 其实合法;
FormulaIsIdentity 不去空格;非法公式求值时 EvaluateFormula 原样返回采样值(不是 NaN)。
T3 离屏验证 —— ✅ 完成(5 个检查 / 71 条断言)
smoke:初始化 + 加载 + 渲染 + 落盘;同一状态两帧逐像素相同(实测差异 0 像素)。frames:连续帧抓取(PNG + 区域 FNV 哈希 + 耗时),帧序列至少出现一次可见变化。panels:7 个面板全部展开后的几何不变式(必须被绘图区夹住)+ 像素基线-mode record/compare。edgestats:边沿统计与工具内独立朴素实现逐位比对(阈值 2.4875/4.975/7.4625 → 20/19、20/19、20/19 完全一致;中心轴 1200/1179 也能完成)。perf:整帧中位数、EdgePanelLayoutFor单次、198 个统计状态的整轮查询耗时。
全程离屏:窗口 SW_HIDE、"鼠标"用 Application::HandleMouseMove() 合成注入、
像素从后备缓冲区读取;进程用 std::_Exit() 收尾(避免单例析构晚于 GdiplusShutdown)。
T4 EdgeScanFor 状态查找哈希化 —— ⛔ 实测后回退(建议 #2)
- 改动前:
mutable std::vector<EdgeScanState>+ 逐字段线性扫描。 - 改法:容器改
std::deque(指针稳定)+mutable std::unordered_multimap<uint64_t, EdgeScanState*>按身份哈希索引,桶内仍逐字段比对(碰撞安全);新建 / 淘汰 / 定向作废 / 清空四处同步索引。 - 正确性:
edgestats全绿(真实数据的计数与独立参考实现逐位相同),panels像素基线不变。 - 性能(198 个状态,
MF4-Record.MF4):整轮查询0.097 → 0.073 us/query(一轮 198 次查询省 ≈4.8 μs)。 - 体积代价:
788,480 → 794,112 B(+5,632 B)。 - 结论:回退。5.6 KB 的体积换 ≈5 μs/帧(约占 5 ms 整帧的 0.1%)不成立 —— 这与 §6.2 原本的判断一致,本轮把它从"估计"变成了实测。 ⚠️ 同时修正了 §6.2 的量级:报告原估"288 信号时 ≈0.3 ms/帧",实测 198 状态整轮 ≈20 μs/帧, 差一个数量级(线性扫描的常数很小)。
T5 EdgePanelLayoutFor 表驱动化 —— ⏸ 只交付"可验证手段",未重写
- 第 33 轮已把按种类分派收敛成唯一派发点(
LanePanelEnabled/LanePanelStateOf/LanePanelScrollOf,且switch不写default:让-Wswitch兜底)。 - 剩余的重复是 957 行几何算式本身(每个面板的内容行高、表头、按钮、滚动区各不相同), 无法用"每面板一行元数据"表达。
- 本轮实测该函数 1.9~2.5 μs/次(2000 轮 × 7 种),按每帧约 14 次调用计约 30 μs/帧, 不是热点。
- ⇒ 重写的收益是"可读性",代价是 957 行几何公式的可回归风险。
本轮交付的是让这类重写变得可验证的工具:
-run panels -mode record/compare会逐面板比对几何 + 像素哈希(7 个面板、含内容区/滚动条/按钮)。
T6 wcstod 自写替身(省 gdtoa)—— ⛔ 实测 0 字节收益(建议 #4)
- 报告 §1.3 的估计是"≈20 KB"。本轮先做最小实验而不是先写代码:
在
Makefile里加-D__USE_MINGW_ANSI_STDIO=0(把swprintf交给系统 UCRT),全量重编。 - 实测:产物仍然 788,480 B,一个字节都没变;map 里
mingw_wpformat.o依旧被链接。 - 原因(用 3 行最小程序确认):预编译的 libstdc++ 自己引用
__mingw_swprintf—— 只要工程里还调用std::to_wstring/std::to_string(全仓约 80 处,其中若干处实参是浮点), 链接器就必须把 MinGW 的 printf 引擎(含 gdtoa)拉进来。 - ⇒ 真正的 33 KB 收益要以"替换全部 80 处
std::to_*string(含浮点格式语义)"为代价, 风险与收益不匹配,不实施。该实验已回退,只在Makefile里留下一段说明防止重复尝试。 - 顺带产出可复用的验收测试
stdio_mingw_vs_ucrt_formats: 同一批真实格式(%.*f/%.6g/%g/%.4e/%.2f %ls/%llu/%lld/%02X/%.2f%%) × 13.6 万次调用做逐字符比对,0 处差异。将来若真做迁移,这条测试就是验收标准。
本轮验证汇总
| 项 | 结果 |
|---|---|
make release(删 .o 全量) |
零 error、零 warning |
make -C Tools |
零 error、零 warning |
Tools/UnitTests/Build/UnitTests.exe |
checks=219 failed=0 cases=18 |
Tools/OffscreenChecks/Build/OffscreenChecks.exe -all |
5 个检查全 PASS,checks=71 failed=0 |
| Release 产物 | 788,480 B(与第 33 轮末一致;T4/T6 均回退) |
第 35 轮(2026-11-04):浮动面板架构评估 + 收敛 + 整数格式化
评估:浮动面板 / 二级菜单 / 右键菜单算不算"公共组件"?
先回答用户的问题(结论来自读码,不是印象):
| 对象 | 现状 | 是否已是"公共组件" |
|---|---|---|
| 右键菜单 / 二级菜单 | Components/Shared/PopupMenu(+ Layout / Paint 两分片) |
是。同一份实现服务绘图区网格菜单、信号菜单、分组菜单、标签页菜单、工具栏设置菜单;新增菜单只需描述条目数组 |
| 拾色器 | Components/Shared/ColorPickerPopup |
是。独立的浮层组件,自带编辑框/滑块/色域/命中 |
| 实验室功能浮动面板 | 全部在 PlotView 内部(EdgePanelLayoutFor 算几何 + PaintEdgeCursor 画 + OnLButtonDown/Move/Up 命中),共用一份"面板骨架"(标题栏 = 折起三角 + GX 徽标 + 标题 + ✕、可滚动内容区 + 滚动条、底部按钮行) |
骨架是公共的,但"按种类分支"的代码被抄了多份 |
为什么不把面板做成 UiWidget 树节点(像 PopupMenu 那样)?
三条硬约束决定了成本远大于收益:
- 面板的像素要进
m_panelDc图层缓存(键 =PanelGlobalKey ⊕ PanelSlotKey), 而UiWidget有自己的 dirty/capture 体系,两套缓存叠加会互相失效(历史教训:SignalPanel行位图缓存与控件树就被刻意分开维护)。 - 面板几何必须由
EdgePanelLayoutFor唯一给出(绘制 / 命中 / 光标 / 调试盒 / 缓存键 五处共用),改成控件树就要把这套"几何同源"拆掉。 - 面板绘制要往
m_panelDc里BitBlt抓像素、要AddOccluder挖洞、要按m_panelOrder逐层设置文本层号 —— 这些都是PlotView的内部协议。
⇒ 正确的公共化方向是把"按种类分支"收敛成唯一派发点,而不是把面板搬进控件树。
P20 面板「特征层」(提交 abcf3e6)
审计出同一套"按种类分支"被抄了多份:
| 重复项 | 位置 | 份数 |
|---|---|---|
分类布尔量(listStyle / countTable / hasButton …) |
EdgePanelWidth / EdgePanelLayoutFor / PaintEdgeCursor |
3 |
| 滚动条几何 + 滚动量夹取 | 布局里表格 / 列表 / 自由标记三分支 | 3 |
| 滚动条绘制(6 行) | 绘制里同样三分支 | 3 |
SetLane*Scroll 实现 |
PlotViewFeatures.cpp |
7 |
| 按种类转发滚动写入 | 滚轮 / 拖滚动条 | 2 |
| 标题文本 | EdgePanelWidth / PaintEdgeCursor |
2(内容还不一致) |
新增 pvdetail::PanelStyle + PanelStyleOf(kind)、PanelScrollMetricsFor(...)、
PlotView::PanelTitle(kind),并让上面全部调用点改成读它们。
顺带修掉一处潜伏错误:EdgePanelWidth 的标题链漏了 MARK/CLIP(按"边沿计数"量宽)。
⚠️ 过程中踩到并修掉一个真实回归:把 hasButton 直接映射到 PanelStyle::buildButton 后,
「选区截取」的底部按钮行高归零 ⇒ 面板高度 124 → 87 px。像素基线当场报 MISMATCH。
修正方式:区分 footerButtons()(有按钮行)与 buildButton(是"构建为新信号")。
P21 滚动收敛(提交 abcf3e6)
- 7 个
SetLane*Scroll→ 一行转发到新的SetLanePanelScroll(laneIndex, kind, scroll)。 - 滚轮 / 拖滚动条两条
switch/if链 → 直接调它(读与写从此同一个入口)。 EdgePanelLayoutFor里 5 层滚动量三元式 →LanePanelScrollOf(lane, kind)(该派发点第 33 轮就存在,布局一直没用它)。
新增离屏检查 panelscroll:7 种面板逐一验证"滚到底 / 滚回顶 / 夹取 / 内容区不动 /
不可滚动时无滚动条"。用 MF4-Record.MF4(566 信号并入同一网格)实测 6/7 可滚
(自由标记无固定点,不可滚是正确行为)。
P22 整数格式化替换(提交 34365b3)
- 新增
Utf8Util::FormatInteger/FormatIntegerUnsigned/FormatIntegerNarrow(手写十进制展开,不经 printf)。 - 全仓替换 76 处
std::to_wstring+ 2 处std::to_string(20 个文件);MatLoader::TrimNumber的整数分支也改(它曾是最后一个引用 MinGW printf 的点)。 - 实测:−1,024 B(787,968 → 786,944)。
- ⚠️ gdtoa 那条 ≈33 KB 的链仍然去不掉,原因已定位到具体文件行:
libstdc++ 的
x86_64-w64-mingw32/bits/os_defines.h无条件#undef __USE_MINGW_ANSI_STDIO+#define … 1("libstdc++ depends on POSIX-definitions"),g++ -E -dM实测最终值恒为 1 ⇒ 任何 C++ TU 的swprintf/strtod必然是 MinGW 版本。 叠加-D__USE_MINGW_ANSI_STDIO=0实测 0 字节收益,已回退并在 Makefile 留下结论。 要真省这 33 KB 只能自己重写「浮点 → 十进制(正确舍入)」与浮点解析的特殊分支 ⇒ 不做。
P23 其余审查清理
PlotView::FreeMarkerValue的const Lane&形参从未使用(函数体里只有一句(void)lane;) ⇒ 删除形参与该语句,更新 2 个调用点。- 「选区截取是否就绪(双光标)」在布局里算了 3 遍 ⇒ 收敛到函数顶部一次 (它同时决定面板高度 footer、条目行、按钮几何,写错任一处就是"底部多一段空白"或"没双光标却能滚动")。
本轮验证
| 项 | 结果 |
|---|---|
make release(删 .o 全量) |
零 error / 零 warning;产物 786,944 B(起点 787,968,−1,024) |
make -C Tools |
零 error / 零 warning |
UnitTests |
checks=260 failed=0 cases=19(新增 integer_replacement_matches_std,41 条断言) |
OffscreenChecks -all |
6 个检查全 PASS,checks=87 failed=0(新增 panelscroll) |
panels -mode compare |
7 个面板像素哈希 + 几何逐项不变(三次重构全程守住) |
panelscroll(MF4,566 信号) |
6/7 面板可滚,夹取与滑块位置全部符合预期 |
下一步建议(面板方向)
EdgePanelLayout的平行数组:rowHeaders/rowNameRects/rowTriangles/rowChecks/rowPoints/rowIterations/rowStats/rowLevel*/rowIter*/rowInfo*/rowIntervalField/rowFrequencyField共 17 个按行平行的vector, 全仓引用 314 处。每加一种参数行都必须在每个分支里按同一顺序 push 恰好 17 项, 漏一项 = 某列读到别人的矩形(这正是"改布局就出错"的机制)。 建议合并为struct PanelRow { … }+std::vector<PanelRow> rows。 本轮未做:314 处引用、风险高,需要先补"逐行几何基线"检查。PaintEdgeCursor拆分:该函数约 1600 行,其中三段"内容绘制"(表格 / 列表 / 自由标记) 各 90–450 行,可机械提取为 3 个成员函数(PaintPanelCountTableRows/PaintPanelListRows/PaintPanelMarkerGroups)。判据:panels -mode compare全绿。 本轮未做:与 P22 抢时间,且属纯可读性收益。- 给平行数组补检查:在
panels里增加"每个 row 的 17 项长度一致"断言 (成本极低,可作为第 1 项的前置护栏)—— 已记入待办。
第 36 轮(2026-11-04):§12.7 两项建议落地
P26 拆分 PaintEdgeCursor —— ✅ 完成(提交 38fbd4b)
- 新增
Components/PlotView/Src/PlotViewPanelPaint.cpp(1016 行,机械搬移、零逻辑改动):PaintPanelCountTableRows—— 边沿计数表格(固定表头 + 可滚动行);PaintPanelListRows—— 列表式(均值/中心轴/弱峰/重采样/截取)条目栏 + 参数行;PaintPanelMarkerGroups—— 自由标记(分组栏 + 表头 + 条目 + 吸顶)。 每个函数只接收它真正需要的参数(layout/tabLane/kind/laneIndex), 不再"整个函数都看得见"。
PlotViewPaint.cpp:6554 → 5575 行(PaintEdgeCursor由约 1900 行降到约 900 行); 三处块替换为一行调用,骨架(边框/标题栏/折起三角/GX 徽标/✕)与底部按钮仍在一处。- 滚动条绘制抽为
pvdetail::PaintPanelScrollBar(g, layout, theme)。 - 顺带补上第 35 轮承诺未落地的一处:
PaintEdgeCursor的标题仍是 7 层三元式 (与EdgePanelWidth里另一条链并存,后者漏了 MARK/CLIP)⇒ 统一为PanelTitle(kind)。
过程中"机械搬移"踩到的三个真实问题(都已修)
- 提取块里引用的
paintCounts/paintPanelScrollBar是局部 lambda ⇒ 前者改为新函数内的局部 lambda(只服务于该段)、后者提升为 pvdetail 内联函数。 - 「标记透明度」滑块用到的
panel/pad是外层局部变量 ⇒ 改为从layout.panel派生。 tabLanes在 marker 段里是被写的(回写各列实测宽度)⇒ 参数必须非 const。
P27 平行数组错位隐患 —— ✅ 用 AppendRow() 消除(提交 85aaed5)
审计发现 EdgePanelLayout 每行对应 18 个平行 vector,而四个布局分支各自手写 push:
- 边沿计数表格:13 次 push;
- 列表式基行:13 次 push;
- 「未勾选/收起」分支:又补 6(+重采样 4)次空 push;
- 「重采样展开」/「普通展开」分支:各自再 push 6(+4)次。 任何一处漏推 ⇒ 该列之后所有行读到别人的矩形。
改法:
- 新增
EdgePanelLayout::AppendRow(ref, header, nameRect, triangle, check)—— 唯一入口, 18 个数组一次补齐(其余列为空矩形)。 - 三个 push 块 → 各 1 次
AppendRow;参数行/重采样行改为rowXxx.back() = …赋值; 「未勾选/收起」分支的 6+4 次补空 push 整段删除(AppendRow 已补过)。 - ⇒ "新增一列"只需改
AppendRow一处;"新增一行"必须走它(结构上不会漏列)。
⚠️ 没做完整结构合并(std::vector<PanelRow> + 列视图),理由:
列视图需要 EdgePanelLayout 自定义拷贝构造/赋值来重绑视图,而该类型是
EdgePanelLayoutFor() 的返回值、又被存进 std::vector —— 拷贝是热路径,
拷贝语义写错会产生比原缺陷更隐蔽的悬垂视图(原缺陷至少会被长度不变式抓到)。
先消除真实隐患,结构合并留待"逐行几何基线"检查就绪。
P28 收紧不变式
panels / panelscroll 里的长度检查从"13 个必须等长 + 4 个可空"改为
18 个全部必须等于行数 —— AppendRow() 成为唯一入口后,"某列短了"
就是"有人绕过它手写 push"的直接证据。
本轮验证
| 项 | 结果 |
|---|---|
make release(全量) |
零 error / 零 warning;产物 786,432 → 785,408 B |
make -C Tools |
零 error / 零 warning |
UnitTests |
checks=260 failed=0 cases=19 |
OffscreenChecks -all |
6/6 PASS,checks=101 failed=0(断言合并后略减) |
panels -mode compare |
7 个面板像素哈希 + 几何逐项不变(两次重构全程守住) |
panelscroll(MF4,566 行) |
18 个行数组长度全部 = 566,行为不变 |
第 37 轮(2026-11-04):逐行几何基线(§13.6 第 1 项)
P29 新增 rowgeom 检查 —— ✅ 完成
Tools/OffscreenChecks/Src/Checks.cpp 新增注册键 rowgeom,做法:
-
场景 A(细节基线):切双光标 → 前 6 条信号 →
MERGE_LANES→ 7 个面板全开、 第 2 条信号勾选(同一面板里同时有"展开的参数行"与"未勾选的行")→ 加 3 个真实固定点 → 导出 7 个面板 展开态全字段 → 全部折起再导一份 → 复原。 -
场景 B(规模基线):全部信号合并成 1 个网格(默认数据 3 行;
-data MF4-Record.MF4= 566 行) → 每面板一个 FNV-1a 摘要 +rows/markerRows计数。 -
导出格式(纯文本、逐行
键=值,便于git diff与人工定位):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 列"。 -
-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:
[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 个面板像素哈希 + 几何逐项不变 |
第 38 轮(2026-11-04):删死列 + 拆重采样参数行(§14.5 第 1 / 第 3 项)
P31 删除 rowPoints / rowIterations —— ✅ 完成
审计(§14.5 第 1 项的前提)在三处确认它们是死列:
| 位置 | 原用途 | 现状 |
|---|---|---|
| 写入 | AppendRow 里推两个空矩形 |
这是唯一写入点,从未被赋过非空矩形 |
| 读取 | PlotViewPaint.cpp 调试框 AddAll、PanelSlotKey 的 mix 循环 |
都是"遍历即忘",不产生任何可见效果 |
| 绘制 | 无 | 旧式"点数 / 迭代次数"输入框早被滑块版(rowLevelTracks / rowIterTracks)取代 |
改动:删两个 std::vector<gd::RectF>(EdgePanelLayout 少 48 字节)、AppendRow 少两次 push_back、
调试框与缓存键各少两个循环、rowgeom 导出与两处长度不变式各少两项(16 列)。
等价性被基线证明(这是本轮最值得记的一条方法论):
old lines = 983, new lines = 919
only in OLD = 64, only in NEW = 0
only-in-OLD lines NOT belonging to rowPoints/rowIterations = 0
- 983 − 919 = 64,正好是两列在 7 个面板上的键数(
count键 + 每行键); - 其余 919 条键逐字节相同 ⇒ "这两列从未有非空值"的判断不是靠眼睛,而是靠基线证伪。
- 随后用新代码重新
record正式基线(rowgeom-baseline.txt),再次compare归零。
P32 提取 PaintPanelResampleRows —— ✅ 完成
PaintPanelListRows 里 if (resamplePanel) { …四行… continue; } 这 132 行整块提取为
PlotView::PaintPanelResampleRows(g, layout, rowIndex, laneIndex, ref, item):
- 两个局部 lambda(
paintInfoRow只读统计行 /paintInputRow联动输入框)随之迁移; maxInterval/minInterval两个"拷贝一份再说"的局部量直接用item.maxInterval/item.minInterval(原来是纯拷贝,无语义差别);- 调用点只剩 3 行(含注释),
PaintPanelListRows现在只有"条目栏 + 通用参数行"两段。
§14.5 第 3 项的完成度(诚实记录)
| 段落 | 状态 | 理由 |
|---|---|---|
| 重采样行 | ✅ 已提取 | 排版与其它面板完全不同、入口/出口干净(continue 收尾) |
| 通用参数行(中心轴统计 + 两行滑块) | ⏸ 未提取 | 依赖 3 个 kind 标志 + 2 套状态(m_dragMode/m_editTarget)+ 2 个滑块 lambda,签名要约 9 个参数,可读性收益低于参数表成本 |
| 条目栏 | 保留 | 三角 + 名称 + 复选框 + 悬停是三种排版的公共前缀 |
本轮验证
| 项 | 结果 |
|---|---|
make release(全量) |
零 error / 零 warning;产物 785,408 → 784,896 B(−512) |
make -C Tools |
零 error / 零 warning |
UnitTests |
checks=260 failed=0 cases=19 |
OffscreenChecks -all |
7/7 PASS,checks=127 failed=0 |
panels -mode compare |
7 个面板像素哈希 + 几何逐项不变(含 resample=2BFC74B3D9B3239C) |
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>
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 |