# Code Review 计划(分支 `Code_Review`) > 目标:在不引入性能回归、不改变用户可见行为的前提下,对全项目做一次**结构 + 性能 + 体积** > 的查漏补缺,并把「改一个功能要动 40 处」的屎山点收敛成表驱动/单一实现。 > > 基线(本计划开始时): > - 分支 `Code_Review`,起点 `764e5be`(dev 已删除过时的 `UI_Refactor_*` 三份文档)。 > - `make release` 产物 **890,880 B**(`.text` 753,136 B、`.rdata` 36,936 B、`.rsrc` 20,656 B、 > `.pdata` 30,420 B、`.xdata` 29,496 B、`.idata` 12,856 B)。 > - 源码约 **5.6 万行**;最大单文件 `PlotViewPaint.cpp` 6,561 行、`PlotView.hpp` 4,654 行。 > > 约束: > - 每完成一个节点**编译验证(release + debug 零警告)**并**提交**,同时更新 > `Documents/Code_Review_Progress.md`。 > - 性能相关节点必须给出**同一次运行内的相对量**(历史教训:绝对毫秒阈值不可靠)。 > - 不占用用户的鼠标/键盘/屏幕(只用纯控制台探针或源码级核对)。 --- ## 一、已确认的问题(本计划的事实基础) ### A. 体积 | 项 | 实测 | 说明 | | --- | --- | --- | | `dynamic_cast`(`Toolbar.cpp:370`) | **+75,264 B** | 最小复现程序:39,936 B → 115,200 B。它会链入 RTTI 元数据 + `cp-demangle.o`(map 里 **56,320 B**)。而该处 `Button::widget` **本来就是 `UiButtonWidget*`** ⇒ 转换完全多余。 | | `-fno-rtti` | 未启用 | 移除上一条后即可开启,进一步去掉 `type_info` 表。 | | `.pdata` + `.xdata` | 60 KB | `-fno-exceptions` 已开;SEH 展开表仍存在。可尝试 `-fno-asynchronous-unwind-tables`(历史上链接失败,需复核)。 | | `libmingwex` 的 `strtodg/gdtoa/mingw_wpformat` | 27.8 KB | 来自 `wcstod` / `wprintf` 类调用;可评估用自写解析替代 `wcstod` 的收益(公式/输入框解析)。 | | `.rsrc` | 20.7 KB | 图标(PNG 帧)+ manifest + 版本信息,已优化过。 | | 重复常量/重复实现 | 多处 | `kFormulaEnvelopeBuckets` 已清理;仍有 `ClampDouble` 两份实现等。 | ### B. 性能(结构性热点) 1. **面板开关/参数处理是 6 个 `EdgePanelKind` × ~20–26 处 switch 分支**(实测每个 kind 出现 21–26 次,全部由手写 `if/switch` 维护)。这是**新增功能必然漏改 2–3 处**的根本原因 (历史已发生 4 次:`MARK` 漏拖动、`RESAMPLE` 漏拖动+关闭、`CLIP` 漏滚轮+缓存键)。 2. **`SceneContentHash` 里 6 段几乎相同的逐面板混入循环**(`PlotViewCacheKey.cpp`)。 3. **`EdgePanelLayoutFor` 是巨型分支**,`EdgePanelLayout` 结构体带 20+ 个按面板类型区分的 矩形数组;高度算式与几何算式分开写(历史上多减一个 pad 造成空白空白 BUG)。 4. **`PlotView` 有 30+ 个缓存**,每个都手写「键 + DC + 位图 + 贴图 + 失效」五件套; 失效路径散落在 8 个文件里,`m_sceneValid=false` 出现 6 处。 5. `ApplicationCommands.cpp` **2,165 行**混杂:标签页、数据派生、菜单、工具栏、键盘、鼠标。 6. `PlotView.hpp` 4,654 行(声明 + 内联实现),单文件认知负担极大。 ### C. 过时/错误注释与命名(抽样已确认) - `agent_mem.md` §3「目录结构」仍写着已不存在的 `Utilities/CsvTable/`、`Utilities/DataModel/` 等目录;§2 写 `-Os` 而实际是 `-Oz`。 - 源码中留有历史轮次的「本轮」「上一轮」叙述(对当前读者无意义),以及被推翻方案的注释。 - `PlotViewInternal.hpp` 里 `kFormulaEnvelope*` 注释已随实现迁移,需同步。 --- ## 二、节点计划 > 优先级:**S**(安全、收益明确、风险低)→ **A**(结构调整,中风险,需逐步验证)→ > **B**(深度重构,必须有可验证的前后一致性)。 ### 阶段 0:建立基线(节点 P0) - P0.1 记录基线:release/debug 体积、`.text/.rdata` 等节表、关键路径的可复跑测量程序 (纯控制台,链接 `Build/Release/*.o`,不入库)。**已完成基线的部分见下表**。 - P0.2 建立 `Documents/Code_Review_Progress.md`,每节点完成后追加一行结论 + 数字。 ### 阶段 1:体积(节点 P1–P5,安全优先) - **P1 移除 `dynamic_cast` + 开启 `-fno-rtti`** - 把 `Toolbar::BuildBoxes` 的 `dynamic_cast` 改为直接用 `Button::widget`(已是该类型)或给 `UiWidget` 加虚函数 `ContentBounds()`。 - Makefile `CXXFLAGS += -fno-rtti`;全仓确认无 `typeid`/`dynamic_cast`/`type_info`。 - 预期 **−70~80 KB**。验收:产物下降 ≥60 KB;功能逐项核对(工具栏按钮、布局边界调试)。 - **P2 体积审计第二轮** - 复核 `-fno-asynchronous-unwind-tables`(历史上链接失败,试 `-fno-unwind-tables` 组合)。 - 评估 `wcstod` 替身(自写定点/浮点解析,仅在公式与输入框使用)去掉 `strtodg/gdtoa/gethex/hexnan`(约 20 KB)。 - 评估 `--gc-sections` 在 LTO 下是否已生效(读 map 确认无死节)。 - 验收:逐项给出「测得的字节数 + 是否采纳」的结论,**不接受「感觉变小」**。 - **P3 重复实现归并** - `ClampDouble` 两份(`PlotViewInternal.hpp` / `UiLayout.cpp`)→ 统一到 `Shared`。 - 扫描全部 `static`/匿名命名空间重复工具函数,合并为单一实现。 - 重复字符串/常量字面量、重复的三元式开关表。 - **P4 死代码与未使用成员清理** - 用非 LTO 对象 + `nm` 反查「定义但从未被引用」的函数;删除确认无引用的实现。 - 清理 `PlotView.hpp` 中已无人读取的成员/字段。 - **P5 资源与启动数据** - `.rsrc` 复核(图标帧数、manifest、版本字符串);`.idata` 导入表复核。 ### 阶段 2:性能(节点 P6–P10) - **P6 建立可复跑的渲染性能探针(纯控制台)** - 目标:能在**不弹窗口**的情况下测量 `BuildLaneVisuals` / `SceneContentHash` / `EdgePanelLayoutFor` / 面板绘制 / 信号面板绘制的耗时。 - 方式:链接 `Build/Release/*.o` + 手工构造 `DataSet`/`TabConfig`,只调用纯函数与 `PlotView` 的非窗口方法(避免 `Application::Initialize` 触发模态框,见历史教训)。 - 所有断言用**同一进程内的相对量**。 - **P7 面板元数据表驱动(核心)** - 新增 `EdgePanelSpec`(每 kind 一行:命令 id、默认尺寸、是否列表型、footer 类型、 滚动字段偏移、面板状态字段偏移、绘制函数指针、命中函数指针)。 - 把 6×~20 处 `switch (kind)` 收敛为「查表 + 3 个通用实现(列表型 / 表格型 / 固定行型)」。 - 验收:**逐像素**对比改造前后面板(离屏渲染哈希);新增一个假 kind 只需加一行表项即可 被绘制/命中/拖动/关闭/滚动全覆盖(用编译期断言或探针验证)。 - **P8 `SceneContentHash` 混入收敛** - 把 6 段重复循环改为「按面板表遍历 + 每面板一个 `MixContent()`」。 - 验收:改造前后哈希值**逐位相同**(同一状态)。 - **P9 `EdgePanelLayoutFor` 表驱动 + 高度/几何同源** - 高度算式与内容几何共用同一份 `PanelMetrics` 计算(消除重复减 pad 类 BUG)。 - 验收:探针打印「内容区下边界 vs 按钮上边界」的像素差,与改造前一致。 - **P10 缓存五件套抽象(低风险版)** - 抽出 `OffscreenCache`(DC/位图/键/贴图/失效),**只替换三处结构完全相同的** (derived / decor / panel),保留 scene 与 lane layer 不动(热路径最复杂)。 - 验收:逐像素一致 + 帧时间不回退。 ### 阶段 3:架构(节点 P11–P14) - **P11 `ApplicationCommands.cpp` 拆分** - → `ApplicationTabs.cpp`(标签页)/ `ApplicationDerive.cpp`(派生/重采样/截取/克隆)/ `ApplicationCommands.cpp`(命令分发)/ `ApplicationMenus.cpp`(上下文菜单与设置菜单)。 - 纯搬移,零行为变更;Makefile 无需改动(按目录通配收集)。 - **P12 `PlotView.hpp` 拆分** - 把 `EdgePanelLayout`、派生曲线缓存条目、图层结构体等**纯数据结构**搬到 `PlotViewTypes.hpp`;把 `PlotView` 的内联小函数按关注点分块放到 `PlotViewInline.hpp`。 - 目标:`PlotView.hpp` ≤ 2,500 行。 - **P13 UI 基础层职责核对** - 核对 `UiWidget` / `UiComponent` / `UiLayout` / `UiBox` / `UiSurface` / `UiButtonWidget` / `UiControls` 的边界是否重叠;合并确认冗余的(只保留一套矩形/命中/布局原语)。 - 明确依赖方向:`Components/*` → `Components/Shared` → `Utilities`(校验无反向)。 - **P14 绘制/功能组件边界** - `PlotViewPaint` / `Raster` / `Panels` / `Features` / `Interaction` 的职责再核对; 把「面板几何」与「面板绘制」的交叉依赖理顺(几何 → 共享 `PanelMetrics`)。 - 把散落在 `PlotView` 里的「颜色/线宽/胶囊样式」常量收敛到 `Theme`/`PaintUtil`。 ### 阶段 4:注释与命名清理(节点 P15) - **P15 注释/命名审计** - 删除「本轮/上一轮」叙述、被推翻方案的描述、与实现不符的注释。 - 统一命名:`*Dc`/`*DC`、`Lane*` vs `Grid*`、`Edge*` vs `Panel*` 等不一致处。 - 确保所有注释与当前实现一致(重点:`PlotViewInternal.hpp`、`PlotViewCacheKey.cpp`)。 - 同步更新 `agent_mem.md` 的 §2/§3(已经不准确的目录结构与 `-Os`)。 ### 阶段 5:验证与报告(节点 P16) - **P16 最终验证与报告** - release + debug 零警告;产物体积对比;关键路径探针全部通过。 - 生成 `Documents/Code_Review_Report.md`:逐节点写「改了什么 / 为什么 / 效果 / 未做的事与原因」,并给出体积与性能的前后对照表。 --- ## 三、验收标准(硬性) 1. `make release` 与 `make debug` **零 error、零 warning**。 2. 产物体积**必须下降**(目标:从 890,880 B 降到 ≤ 780 KB;若某项无法达成,在报告里 说明实测字节数与原因)。 3. 不改变任何用户可见行为(面板几何、绘制像素、交互语义一致)。 4. 每条性能结论都必须有**同进程相对量**或**逐像素/逐位一致性**证据。 5. 每个节点单独提交,提交信息用中文、说明改动与验证方式。 --- ## 四、风险与对策 | 风险 | 对策 | | --- | --- | | 面板表驱动改造引入像素差异 | 改造前后用离屏渲染哈希对比;先改 `CLIP`/`MARK` 两个最简单面板做样板,再推广 | | `-fno-rtti` 破坏第三方/库代码 | 全仓 grep 确认;先单独提交,失败可独立回退 | | 缓存抽象拖慢热路径 | 只抽象非热路径(derived/decor/panel),scene/lane 不动;每次改完测帧时间 | | 大文件拆分时误改逻辑 | 纯搬移(不重写函数体),逐文件 diff 核对;先拆 `ApplicationCommands` | | 探针触发模态「内存不足」框 | 绝不在探针里调 `Application::Initialize()`(历史教训);只构造数据层/纯函数 | --- ## 五、进度索引 进度与每节点的实测数字见 `Documents/Code_Review_Progress.md`。 最终结论见 `Documents/Code_Review_Report.md`。 --- ## 六、第 34 轮追加(2026-11-04):验证基础设施 + 后续建议落地 > 触发:§三/§10 之后剩下的三项建议(`EdgeScanFor` 哈希化、`EdgePanelLayoutFor` 表驱动化、 > `wcstod` 自写替身)都卡在**"没有可自证的验证手段"**上。 > 用户要求:先把验证工具做成仓库内的正式资产(`Tools/` 入库),再动被建议的代码。 ### 6.1 节点与验收标准 | 节点 | 内容 | 验收标准 | | --- | --- | --- | | **T1** | `Tools/` 解除忽略;新增 `Tools/Makefile`;两个工具目录 + `README.md` | `make -C Tools` 构建全部、`make UnitTests` 构建单个、`make clean` 只清工具产物;改头文件/增删源文件自动重编重链 | | **T2** | `Tools/UnitTests`:数据层语义单元测试 | `result=PASS`,断言数 ≥ 200 | | **T3** | `Tools/OffscreenChecks`:隐藏窗口渲染 + 帧序列 + 面板像素基线 + 边沿统计参考实现 + 性能 | 零失败;全程不显示窗口、不动真实鼠标/键盘 | | **T4** | 建议 #2:`EdgeScanFor` 状态查找哈希化 | **先量后改**;性能收益必须大于体积代价,否则回退 | | **T5** | 建议 #3:`EdgePanelLayoutFor` 表驱动化 | 面板像素基线 A/B **逐像素一致**才允许合入 | | **T6** | 建议 #4:`wcstod` 自写替身(省 gdtoa 链) | 体积收益可测且数值语义逐位一致 | ### 6.2 本轮新增的硬约束 1. **工具不得把产物写进 `Tools/`**:截图 / 基线 / 报告一律写 `Temp/tools/`(已被忽略); `Tools/*/Build/` 同样不入库(其它协作者本地 `make -C Tools` 重建)。 2. **工具与本体 ABI 必须一致**:`-fno-exceptions`、`-fno-rtti`、链接时 `-flto -flto-partition=one`; 工具源码自身**不**编成 LTO 字节码(GCC 15.2.0 会在 `choose_baseaddr` 处 ICE)。 3. **"先量后改"**:任何以性能 / 体积为名的改动,必须在同一台机器、同一次会话内给出前后数字; 收益低于噪声、或换来体积上涨的,一律回退,并把数字写进 `Code_Review_Report.md`。 4. **像素基线只做本地 A/B**:哈希与 DPI/字体/数据文件绑定,**不入库、不当黄金图**。 --- ## 七、第 35 轮追加(2026-11-04):浮动面板架构评估与收敛 + 整数格式化替换 > 触发:用户指出「7 个实验性功能面板观感/骨架高度一致,但每新增一种或改一次布局 > 总会出意料之外的错误」,要求先**评估**(能否抽公共基类 / 公共组件,浮动面板、 > 二级菜单、右键菜单是否算公共组件),再动手;同时把 §11.5 的 > 「替换 `std::to_wstring` / `std::to_string`」做掉。 ### 7.1 节点与验收标准 | 节点 | 内容 | 验收标准 | | --- | --- | --- | | **P20** | 面板「特征层」:唯一分类 / 标题 / 滚动几何来源 | 面板像素基线与几何**逐项不变** | | **P21** | 滚动设置收敛为唯一实现(7 个包装 + 2 条调用链) | `panelscroll` 新检查全绿 | | **P22** | 全仓替换整数格式化(76 + 2 处) | 面板像素不变 + 单元测试逐字符对照 | | **P23** | 其余审查清理(垃圾形参 / 重复判定) | 零 warning、像素不变 | ### 7.2 本轮新增硬约束 1. **新增一种实验性功能面板,只允许改三个地方**: `LanePanelScrollOf` / `PanelStyleOf` / `PanelTitle`(都在 `PlotViewInternal.hpp` / `PlotViewPanels.cpp`)。三者都是穷举 `switch` 且**不写 `default:`**, 漏登记会被 `-Wswitch` 编译报警。 **禁止**再在宽度 / 布局 / 绘制里各写一份 `kind == XXX` 组合判断。 2. **底部"有按钮行"与"是构建按钮"是两个概念**(`PanelStyle::footerButtons()` vs `buildButton`)。混用会让面板高度与内容区高度不一致。 3. **整数转文本一律用 `Utf8Util::FormatInteger*`**,不要用 `std::to_wstring` / `std::to_string`(它们把 MinGW printf 引擎拉进产物,且 libstdc++ 会强制 `__USE_MINGW_ANSI_STDIO=1`,命令行关不掉 —— 见 Makefile 注释)。 4. **改面板几何/绘制前后必须跑** `OffscreenChecks -run panels -mode record/compare`; 本轮它当场抓到了"截取面板少了 37px 高度"的回归。 --- ## 八、第 36 轮追加(2026-11-04):§12.7 两项建议的落地 > 触发:用户要求「继续按照上轮报告的后续建议推进」。§12.7 列了两项: > ① `EdgePanelLayout` 平行数组合并;② `PaintEdgeCursor` 拆分。 | 节点 | 内容 | 结论 | | --- | --- | --- | | **P26** | 拆分 `PaintEdgeCursor` 三段内容绘制 | ✅ 完成(新建 `PlotViewPanelPaint.cpp`) | | **P27** | 平行数组错位隐患 | ✅ 用 `AppendRow()` 唯一入口消除(**未做**完整结构合并,理由见下) | | **P28** | 收紧不变式 | ✅ 18 个数组**全部**必须等于行数(不再允许"重采样专属列可为空") | ### 8.1 本轮新增硬约束 1. **新增一种面板行,只能走 `EdgePanelLayout::AppendRow(...)`**。 18 个平行数组由它一次补齐;参数行 / 重采样行一律 `rowXxx.back() = …` **赋值**。 **禁止**在任何布局分支里手写 `rowXxx.push_back(...)`。 2. **面板内容绘制按排版类别分文件**:骨架(边框/标题栏/✕/按钮)在 `PlotViewPaint.cpp`, 内容在 `PlotViewPanelPaint.cpp`。新增一种面板 = 加一个 `PanelStyleOf` 分支 + 在对应内容函数里加一支。 3. **`OffscreenChecks -run panels` 的长度不变式现在是"全等"判定**: 任何一列短于行数即失败(这条检查是"绕过 `AppendRow`"的唯一探测器)。 ### 8.2 为什么没有做「完整合并成 `std::vector` + 列视图」 技术上可行(`PanelColumn` 持有 `std::vector*` + 成员指针, 读点只需在 `layout.rowXxx` 后加一对括号),但 **`EdgePanelLayout` 需要自定义拷贝构造/赋值** 来重新绑定视图;而它是 `EdgePanelLayoutFor()` 的返回值、又被存进 `std::vector`, 拷贝是热路径 —— 拷贝语义一旦写错,会产生**比原缺陷更隐蔽的悬垂视图**(原缺陷至少会被 长度不变式抓到,悬垂指针不会)。 本轮先用 `AppendRow()` 把**真实隐患**("某分支漏推一列")消除, 完整结构合并留待"逐行几何基线"检查就绪后再做。 --- ## 九、第 37 轮追加(2026-11-04):逐行几何基线(§13.6 第 1 项) > 触发:用户要求「按照你的建议继续推进」。§13.6 第 1 项 = 逐行几何基线, > 它是 §13.3「完整结构合并」与「重写布局算式」的前置护栏。 | 节点 | 内容 | 验收标准 | | --- | --- | --- | | **P29** | 新增 `OffscreenChecks -run rowgeom`:把 7 个面板**每个矩形的每个字段**导出为文本基线,逐项比对 | `-mode record`/`-mode compare` 无差异;**篡改基线必须报错**(负向自检) | | **P30** | 修正长度不变式的判据:从"18 列全等"改为**按排版类别**判定 | `panels` / `panelscroll` 仍全绿,且对 `MARK` 面板不再误判 | ### 9.1 基线的两层覆盖 | 场景 | 数据 | 记录内容 | 用途 | | --- | --- | --- | --- | | **A:细节** | 前 6 条信号合并成 1 个网格,7 个面板全开、参数行展开、3 个固定点、折起/展开各一份 | 每行每列的精确矩形(`Temp/tools/rows-baseline/rowgeom-baseline.txt`,983 条键) | 定位级:一眼看出"哪一行哪一列偏了几个像素" | | **B:规模** | **全部**信号合并成 1 个网格(MF4 = 566 行) | 每个面板一个 FNV-1a 摘要哈希 + 行数/固定点行数 | 覆盖级:任何一行变动都会改哈希(不产生几十万行基线) | ### 9.2 本轮新增硬约束 1. **改面板几何(宽高算式 / 行距 / 内边距 / 折起规则)前后必须跑** `OffscreenChecks -run rowgeom -mode record|compare`。 基线是**检测器**不是黄金图:变化是**预期**时要重新 record,并在提交信息里说明原因。 2. **`rowRefs` 是重载的**:「列表式面板」用 `rowRefs` 表示"每个信号一行", 而「自由标记面板」用它表示"每个信号一个分组",只填 `rowNameRects` / `rowNameCloses`, 其余 14 列**必须为空**(真正的条目行在 `markerRows`)。因此任何"平行数组长度"不变式 **必须按 `PanelStyle` 分类判定**,不能写"全部 18 列全等"。 3. **`ExecuteToolbarAction(MERGE_LANES / SPLIT_LANES)` 之后、读 `LaneCount()` 之前, 必须先 `SynchronizeView(true)`**:`SetMode()` 只重建 `TabConfig` 的网格配置, `PlotView::m_lanes` 要等下一次布局/绘制才刷新。 4. 检查里**不要依赖"上一个场景的网格级状态还在"**:`Session::ShowSignals()` 先 `SetAllVisible(false)`,而"全部不可见 ⇒ 丢弃网格"是既有设计 (`RebuildLanes` 的 MERGED 分支),固定点/面板开关会随之清空 —— 该场景要**重新播**。 --- ## 十、第 38 轮追加(2026-11-04):§14.5 第 1 项(删死列)+ 第 3 项前半(拆重采样行) > 触发:用户要求「按照上一轮报告中的 §14.5 下一步(按价值排序)继续推进」。 | 节点 | 内容 | 验收标准 | | --- | --- | --- | | **P31** | 删除 `EdgePanelLayout::rowPoints` / `rowIterations`(写入方只剩 `AppendRow`,读取方只剩调试框与缓存键,几何恒为空) | `rowgeom -mode compare` 的差异**只**来自这两列(64 条键,0 条其它差异);`panels -mode compare` 像素不变 | | **P32** | 从 `PaintPanelListRows` 提取 `PaintPanelResampleRows`("四行只读统计 + 联动输入框"这一段) | 面板像素哈希(尤其 `resample`)与逐行几何**逐项不变** | ### 10.1 为什么"删掉一列"也可以证明 `rowgeom` 的两层基线正好给出**可证伪**的等价性: * 旧基线 983 行、新基线 919 行;集合差 = 只在旧里的 **64** 行、只在新里的 **0** 行; * 那 64 行**全部**属于 `rowPoints` / `rowIterations`(脚本按正则断言,非人工核对); * ⇒ 其余 919 条键**逐字节相同**,也就是说"删掉的两列从没被赋予过非空矩形"这一判断被基线证实。 ### 10.2 本轮新增硬约束 1. **`EdgePanelLayout` 行相关的平行数组现在是 16 个**(原 18)。任何"列清单" (`rowgeom` 导出、长度不变式、调试框、缓存键)都要同步;`AppendRow` 里少推一列 仍会被长度不变式当场抓到。 2. **只读统计/输入框这类"专属排版"要独立成函数**:`PaintPanelResampleRows` 的四行 与其它面板的滑块参数行没有共同点,混在同一个行循环里只会让三种排版互相纠缠。 3. **§14.5 第 2 项(完整结构合并)仍处于"闸门关闭"状态**,未做(理由见 Report §15.3)。 ### 10.3 §14.5 第 3 项的完成度 | 段落 | 状态 | | --- | --- | | 重采样行(四行只读+输入) | ✅ 已提取为 `PaintPanelResampleRows` | | 通用参数行(中心轴统计 + 两行滑块) | ⏸ 未提取:依赖 3 个 kind 标志 + 2 套拖拽/编辑状态 + 2 个滑块 lambda,提取需传 9 个参数,可读性收益低于参数表本身 | | 条目栏(三角 + 名称 + 复选框 + 悬停) | 保留在 `PaintPanelListRows` 里(它是三种排版的公共前缀) | --- ## 十一、第 39 轮追加(2026-11-04):平行数组合并为 `std::vector`(§15.6 第 1 项) > 触发:用户要求「继续按照上轮的建议继续推进」。上一轮结论是"若要做就做**方案 B**: > `std::vector` + 具名成员访问(**无视图**),并且必须单独一轮"。 | 节点 | 内容 | 验收标准 | | --- | --- | --- | | **P33** | `EdgePanelLayout` 的 16 个平行 `std::vector` 合并为 `std::vector 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` 列视图(拷贝 `EdgePanelLayout` 后忘重绑 = 静默悬垂,没有检查可以抓到)。 2. **新增一个行字段** = `PanelRow` 加一个成员 + `AppendRow` 里赋默认值(现在连这一步都不用, 成员自带默认构造)+ 需要绘制的地方读 `rows[i].字段`。**不需要**再同步任何"列清单"。 3. **`signalCount` ≠ `rows.size()`**:前者是"网格里的可见信号数"(`EdgePanelLayoutFor` 开头统计), 后者是"本面板实际产生的行数"。两者在「自由标记」面板里**不相等**(只有有固定点的信号才有行)。 4. **`rowgeom` 的键名保持 `.<旧列名>[NNN]`**:这样重构前后的基线可以逐字节比对, 等价性不靠人工核对。 ### 11.2 等价性是怎么证明的(可复用的流程) ```text old keys = 919, new keys = 965 仅存在于旧 = 0 ← 没有任何键消失 已有键的值变化 = 20 ← 全部是 ".count" 键:0 -> N(结构变化,不是几何变化) 仅存在于新 = 46 ← 全部是 [NNN],且值全为 0.00,0.00,0.00,0.00 ``` 含义:旧结构里"某列根本没被 push"的那些行,现在每行都拥有该字段(值为空矩形)。 **逐行逐字段的真实几何一位都没变**,7 个面板的像素哈希也逐项未变。 --- ## 十二、第 40 轮追加(2026-11-04):几何算式重写 + 参数行提取(§16.7 第 1 / 第 2 项) > 触发:用户要求「继续上一轮报告中 §16.7 的 1 和 2 建议」,并明确第 3 点(体积)暂不投入, > 等以后功能更新产生新的冗余时再优化。 | 节点 | 内容 | 验收标准 | | --- | --- | --- | | **P36** | 宽度算式与列几何共用一份 `PanelColumnMetrics`(`MeasurePanelColumns` + `EdgePanelWidthFor`) | `panels` 7 个面板像素哈希 + 矩形**逐项不变** | | **P37** | 行高累加与行几何共用一份 `PanelRowPlan`(`PlanListPanelRows`):两趟遍历合一 | `rowgeom` 965+7 键 0 差异;`panelscroll` 全绿 | | **P38** | `PaintPanelListRows` 的通用参数行提取为 `PaintPanelParamRows`(5 参数) | 双基线归零;`PaintPanelListRows` 由约 300 行降到约 120 行 | ### 12.1 本轮新增硬约束 1. **宽度算式与列几何只能有一份来源**:任何"这个面板的标签列 / 数值列有多宽"都从 `PanelColumnMetrics` 取。**禁止**在 `EdgePanelLayoutFor` 里再写 `UiMeasureTextWidth` 链 (旧代码注释写着"与 `EdgePanelWidth` 同一组算式 ⇒ 不会漂移"——那是靠人守的约定, 改一处漏一处就是"面板变宽而列不动",表现为文字压到轨道上)。 2. **行高与行几何只能有一份来源**:都读 `PlanListPanelRows` 的返回值(`PanelRowPlan`)。 **禁止**再出现"第一趟只算高度、第二趟才算几何"的双遍历 —— 两趟的判定条件必须逐字一致, 任何一处写错就表现为底部留白 / 最后一行被裁 / 没内容却能滚动。 3. **`PanelRowPlan::sliderRows`** 用整数表达"展开几个参数行"(0/2/4), `hasStats` 表达「中心轴」多一行统计 —— 这两个量是"高度"与"几何"共享的**唯一判定**。 4. **参数行绘制**走 `PaintPanelParamRows(g, layout, row, kind, PanelParamState{…})`; 逐行状态打包进 `PanelParamState`(解析结果 / 两种条目指针 / 网格下标), 目的是把参数表压在 5 个 —— 一个个透传会让签名比函数体更难读。