# 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//Build/.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` + 逐字段线性扫描。 * **改法**:容器改 `std::deque`(指针稳定)+ `mutable std::unordered_multimap` 按身份哈希索引,桶内仍逐字段比对(碰撞安全);新建 / 淘汰 / 定向作废 / 清空四处同步索引。 * **正确性**:`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 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` + 列视图),理由: 列视图需要 `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`,做法: 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 个面板像素哈希 + 几何逐项不变 | --- ## 第 38 轮(2026-11-04):删死列 + 拆重采样参数行(§14.5 第 1 / 第 3 项) ### P31 删除 `rowPoints` / `rowIterations` —— ✅ 完成 审计(§14.5 第 1 项的前提)在三处确认它们是**死列**: | 位置 | 原用途 | 现状 | | --- | --- | --- | | 写入 | `AppendRow` 里推两个空矩形 | 这是**唯一**写入点,从未被赋过非空矩形 | | 读取 | `PlotViewPaint.cpp` 调试框 `AddAll`、`PanelSlotKey` 的 mix 循环 | 都是"遍历即忘",不产生任何可见效果 | | 绘制 | 无 | 旧式"点数 / 迭代次数"输入框早被滑块版(`rowLevelTracks` / `rowIterTracks`)取代 | 改动:删两个 `std::vector`(`EdgePanelLayout` 少 48 字节)、`AppendRow` 少两次 `push_back`、 调试框与缓存键各少两个循环、`rowgeom` 导出与两处长度不变式各少两项(16 列)。 **等价性被基线证明**(这是本轮最值得记的一条方法论): ```text 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 行:函数头 + 文档) |