Files
Just-DataInspector/Documents/Code_Review_Progress.md
T
DeepSeek V4.1 Flash 221f9d2410 test(tools): 行数组不变式收紧为"18 个全部等于行数"
背景:`AppendRow()`(提交 85aaed5)已经成为新增面板行的唯一入口,
18 个平行数组由它一次补齐。因此旧的宽松判据("13 个必须等长 +
4 个重采样专属列允许为空")不再需要 —— 任何一列短于行数,
就是"有人绕过 AppendRow 手写 push"的直接证据。

改动:
* `panels` 检查:改为断言 18 个数组长度**全部**等于 `rowRefs.size()`,
  失败时打印全部 18 列的长度便于定位。
* `panelscroll` 检查:同样收紧(大文件 566 行时才会暴露的问题)。
* `Tools/README.md`:同步不变式描述。

文档:
* `Documents/Code_Review_Plan.md` 新增 §八(第 36 轮节点与硬约束)。
* `Documents/Code_Review_Progress.md` 新增第 36 轮(两次重构 + 三个搬移陷阱)。
* `Documents/Code_Review_Report.md` 新增 §13(拆分职责表、为何不做完整合并、
  失败可见性对比、后续建议)。
* `Documents/agent_mem.md` 新增第 36 轮规则。

验证:
* `make release` 全量零 error / 零 warning,产物 **785,408 B**。
* `UnitTests` → checks=260 failed=0;`OffscreenChecks -all` → 6/6 PASS,checks=101 failed=0。
* `panels -mode compare` → 7 个面板像素哈希与几何逐项不变。
* `panelscroll`(MF4 566 行)→ 18 个行数组全部 = 566。
2026-11-04 15:20:07 +08:00

25 KiB
Raw Blame History

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)

改动

  1. Toolbar::BuildBoxes 里唯一的 dynamic_cast 是多余强转(Button::widget 本来就是 UiButtonWidget*)。改为直接遍历 m_buttons(与 m_root->Children() 同源同序)。
  2. 仅关 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 换成自己的处理函数,行为等效。
  3. 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)

改动

  1. 新增 RELEASE_ONLY_OPT = -fno-asynchronous-unwind-tables,只加在 Release 的两条 编译规则上。-fno-exceptions 下绝大多数函数不再需要 .pdata/.xdata。
    • 代价(已评估接受):Release 崩溃时 WER 拿不到可靠调用栈;不打印栈回溯的程序 功能不受影响,Debug 构建保留完整展开表。
    • -fno-unwind-tables(全关)会链接失败,不采用。
  2. 修正真实缺陷: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)

改动

  1. .gitignore:删除整目录忽略的 Tools/,改为 Tools/**/Build/(只忽略各工具产物)。
  2. 新增 Tools/Makefile:make / make UnitTests / make OffscreenChecks / make clean / make help; 工具链接本体 ../Build/Release/*.o(排除 Main.o),-MMD -MP 跟踪头文件依赖。
  3. 新增 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 那样)? 三条硬约束决定了成本远大于收益:

  1. 面板的像素要进 m_panelDc 图层缓存(键 = PanelGlobalKey ⊕ PanelSlotKey), 而 UiWidget 有自己的 dirty/capture 体系,两套缓存叠加会互相失效(历史教训: SignalPanel 行位图缓存与控件树就被刻意分开维护)。
  2. 面板几何必须由 EdgePanelLayoutFor 唯一给出(绘制 / 命中 / 光标 / 调试盒 / 缓存键 五处共用),改成控件树就要把这套"几何同源"拆掉。
  3. 面板绘制要往 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 面板可滚,夹取与滑块位置全部符合预期

下一步建议(面板方向)

  1. 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 处引用、风险高,需要先补"逐行几何基线"检查。
  2. PaintEdgeCursor 拆分:该函数约 1600 行,其中三段"内容绘制"(表格 / 列表 / 自由标记) 各 90–450 行,可机械提取为 3 个成员函数(PaintPanelCountTableRows / PaintPanelListRows / PaintPanelMarkerGroups)。判据:panels -mode compare 全绿。 本轮未做:与 P22 抢时间,且属纯可读性收益。
  3. 给平行数组补检查:在 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)。

过程中"机械搬移"踩到的三个真实问题(都已修)

  1. 提取块里引用的 paintCounts / paintPanelScrollBar 是局部 lambda ⇒ 前者改为新函数内的局部 lambda(只服务于该段)、后者提升为 pvdetail 内联函数。
  2. 「标记透明度」滑块用到的 panel / pad 是外层局部变量 ⇒ 改为从 layout.panel 派生。
  3. 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,行为不变