Code Review :代码审查 + 产物体积 −12.2%(890,880 → 782,336 B)+ 面板结构收敛 + 离屏验证工具入库 #3

Merged
LuChiChick merged 22 commits from Code_Review into dev 2026-11-04 09:54:20 +00:00
22 Commits
Author SHA1 Message Date
DeepSeek V4.1 Flash 0c26da07d2 refactor(panels): 面板静态特征收敛为唯一登记表 + 标记面板绘制分段(§17.6 第 1/2 项)
- 新增 pvdetail::PanelKindTraits + PanelTraitsOf(唯一 switch):标题 / 排版类别 /
  滚动字段 / 面板状态 / 功能开关同表登记;新增面板只改「枚举 + 一行」,
  -Wswitch 仍在唯一登记点生效。PanelTitle 改为「前缀 + rangeSuffix 标志」,
  不再依赖 switch(标题降级为数据,不再需要函数指针)。
- PaintPanelMarkerGroups(约 340 行)拆成 6 个具名函数,主函数只保留绘制顺序
  (普通分组栏 -> 条目 -> 表头 -> 吸顶栏 -> 滚动条 -> 透明度行);纯搬移、0 字节变化。
- 顺手修正该函数体遗留的多余缩进。

验证:release/debug/tools 零 warning;UnitTests 260/19;OffscreenChecks -all 7/133;
panels -mode compare 7 面板像素哈希逐项不变;rowgeom compare 965+7 / 1268+7 键 0 差异。
产物 781,824 -> 782,336 B(+512,全部来自登记表)。
2026-11-04 17:22:06 +08:00
DeepSeek V4.1 Flash a9f1563896 refactor(panels): 几何算式收敛为单一来源 + 参数行提取(§17.6 第 1/2 项)
第 1 项:宽度算式与行高累加各自合并成一份来源
- 新增 `PanelColumnMetrics` + `MeasurePanelColumns(lane, kind)`:全仓唯一的一次"逐信号量宽"。
  * `EdgePanelWidth(laneIndex, kind)` 退化为"量一次 + 算一次"(`EdgePanelWidthFor`);
  * `EdgePanelLayoutFor` 量一次后,宽度与列几何(表格列 / 滑块轨道)都读同一份度量。
  * 改前两者各写一份同名同形的 UiMeasureTextWidth 链,注释只写着"同一组算式 ⇒ 不会漂移",
    那是靠人守的约定,改一处漏一处就是"面板变宽而列不动"(文字压到轨道上)。
- 新增 `PanelRowPlan` + `PlanListPanelRows(lane, kind)`:行高累加与行几何的唯一来源。
  * 改前 `EdgePanelLayoutFor` 对同一批信号遍历两趟(先算总高、再出行几何),
    两处判定(可见/勾选/收起/统计行/参数行数)必须逐字一致 —— 这类"两趟必须同步修改"
    正是"改一次布局就出错"的另一半机制。
  * 现在一个 {ref, checked, collapsed, hasStats, sliderRows} 数组同时喂高度算式与几何生成。
- 合并前先做等价性判别:`valueColumn` 两处算法不同但结果相同(100000 最宽)⇒ 可合并;
  `sliderLabel` 与 `resampleLabel` 不可合并(重采样标签更长,并进会让均值/弱峰变宽)。

第 2 项:`PaintPanelListRows` 的通用参数行提取
- 新增 `PaintPanelParamRows(g, layout, row, kind, PanelParamState{…})`:中心轴统计行 +
  两条滑块分支(均值/弱峰),`paintCounts`/`paintMeanSlider`/`paintPeakSlider` 三个 lambda
  与 `valueColumn` 随之迁入。
- 逐行状态打包进 `PanelParamState`(解析结果 / 两种条目指针 / 网格下标)⇒ 参数表 5 个,
  而不是 9 个散参数(签名比函数体更难读)。
- `PaintPanelListRows` 由约 300 行降到约 120 行,只负责"条目栏 + 按 kind 分派"。
- 顺手把机械搬移遗留的"整个函数体多缩进 2 个 tab"修正。

过程记录(两次锚点误伤,已写入文档避免重犯)
- 锚点 `// 两行滑块` 在 `EdgePanelWidthFor` 内也匹配 ⇒ 误删宽度函数尾部;
- 锚点 `float rowY = ...` 在表格分支先出现 ⇒ 误删「边沿计数」的行发射。
两次都是编译器立刻报错,从 Temp 备份回滚后重做。规则:整段替换的锚点必须唯一,
同名语句多次出现时先定位所属区块再取偏移。

第 3 项(体积)按用户指示暂停投入。

验证(双基线均为"零差异")
- make release / make debug / make -C Tools:零 warning;产物 782,336 → 781,824 B(−512)
- UnitTests:checks=260 failed=0 cases=19
- OffscreenChecks -all:7 检查 / 133 断言全 PASS
- panels -mode compare:7 面板像素哈希逐项不变(含 resample=2BFC74B3D9B3239C)
- rowgeom -mode compare:965+7 键 0 差异;-data MF4-Record.MF4 时 1268+7 键 0 差异

文档:Plan §十二、Progress 第 40 轮、Report §17、agent_mem 第 40 轮
2026-11-04 16:36:47 +08:00
DeepSeek V4.1 Flash 1463f9d3f8 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
2026-11-04 16:18:56 +08:00
DeepSeek V4.1 Flash f07ac74988 refactor(panels): 删除 rowPoints/rowIterations 死列 + 提取 PaintPanelResampleRows
对应 Code_Review_Report §14.5 第 1 项与第 3 项。

第 1 项:删除 EdgePanelLayout::rowPoints / rowIterations(行数组 18 → 16)
- 写入方只有 AppendRow(推空矩形),读取方只有调试框 AddAll 与 PanelSlotKey 的 mix 循环,
  绘制方零(旧式“点数 / 迭代次数”输入框早被滑块版 rowLevelTracks / rowIterTracks 取代)。
- 连带同步:AppendRow、调试框、缓存键、rowgeom 导出、panels/panelscroll 长度不变式、
  文档里的列数(18/14 → 16/12)。

等价性由基线证伪,而不是靠人工 grep:
  old lines = 983, new lines = 919
  only in OLD = 64, only in NEW = 0
  only-in-OLD lines NOT belonging to rowPoints/rowIterations = 0
即除被删两列外,其余 919 条键逐字节相同。

第 3 项(前半):提取 PlotView::PaintPanelResampleRows
- PaintPanelListRows 里 `if (resamplePanel) { …; continue; }` 共 132 行整块提取,
  签名 (g, layout, rowIndex, laneIndex, ref, item);两个局部 lambda
  (paintInfoRow 只读统计行 / paintInputRow 联动输入框)随之迁移。
- maxInterval / minInterval 两个纯拷贝局部量直接用 item.maxInterval / item.minInterval。
- 通用参数行(中心轴统计 + 两行滑块)未提取:需 9 个参数,签名比函数体更难读。

§14.5 第 2 项(完整结构合并)本轮不做,方案已定并写入文档:
- 列视图(PanelColumn<T>)方案否决:拷贝忘重绑 = 静默悬垂,需额外自检兜住;
- 若要做就做“vector<PanelRow> + 具名成员访问(无视图)”,242 处读点(127 处命中判定),
  必须单独一轮。它属整洁性收益(漏推一列已被 AppendRow + 长度不变式挡住),不是紧急项。

验证
- make release:零 warning;785,408 → 784,896 B(−512);make debug 同样零 warning
- make -C Tools:零 warning
- UnitTests:checks=260 failed=0 cases=19
- OffscreenChecks -all:7 检查 / 127 断言全 PASS
- panels -mode compare:7 面板像素哈希 + 几何逐项不变(resample=2BFC74B3D9B3239C)
- rowgeom -mode compare:919 + 7 键,0 差异(基线已按 16 列重录)
- PlotViewPaint.cpp 5575 → 5565;PlotViewPanelPaint.cpp 1016 → 1020

文档:Plan §十、Progress 第 38 轮、Report §15、agent_mem 第 38 轮、Tools/README §4.4
2026-11-04 16:02:15 +08:00
DeepSeek V4.1 Flash 353c553721 test(tools): rowgeom 逐行几何基线 + 长度不变式按类别判定
新增 `OffscreenChecks -run rowgeom`(§13.6 第 1 项),把 7 个面板的每个矩形的
每个字段导出为文本基线并逐项比对,作为"完整结构合并 / 重写布局算式"的前置护栏。

两层覆盖
- 细节层:6 信号 1 网格、7 面板全开、参数行展开、3 个真实固定点、折起+展开各一份,
  983 条键(-data MF4-Record.MF4 时 1304 条),失败时直接给出字段名+行号+新旧值;
- 规模层:全部信号合并成 1 个网格(MF4 = 566 行),每面板一个 FNV-1a 摘要 + 行数,
  任何一行变动都会改哈希,同时不产生几十万行基线。

负向自检(证明护栏不是空转)
- 篡改基线副本 `mark.markerRowCount=3 → 2` ⇒ `CHANGED … mismatches=1` + FAIL。

顺带修正第 36 轮定得过严的不变式
- `rowRefs` 是重载字段:MARK 面板用它表示"信号分组",真正的条目行在 markerRows,
  且只填 rowNameRects / rowNameCloses,其余 14 列必须为空;
  ⇒ `panels` / `panelscroll` 的"18 列全等"改为按 `PanelStyle::markerGroups` 分流判定,
  失败时打印 15 个计数字段便于定位。
- 导出器/不变式一律**逐列按自身长度**遍历(按 rowRefs.size() 索引其它列会越界,
  这是本轮第一次 0xC0000005 的根因)。

测试用例里的两处修正
- `MERGE_LANES` 之后补 `SynchronizeView(true)`:`SetMode` 只重建 TabConfig 的网格配置,
  `PlotView::m_lanes` 要等下一次布局才刷新;
- 场景 B 重新播固定点:`ShowSignals()` 先 `SetAllVisible(false)`,而"全部不可见 ⇒ 丢弃网格"
  是 RebuildLanes 的既有设计,网格级状态随之清空(不是缺陷),因此不能跨场景依赖。

验证
- make release:零 warning,785,408 B 不变(本轮只动工具/检查与文档)
- make -C Tools clean && make -C Tools:零 warning
- UnitTests:checks=260 failed=0 cases=19
- OffscreenChecks -all:7 检查 / 127 断言全 PASS(原 6 / 101)
- rowgeom -mode compare:默认数据 983+7 键、MF4 1304+7 键,0 差异
- panels -mode compare:7 个面板像素哈希 + 几何逐项不变

文档:Code_Review_Plan §九、Progress 第 37 轮、Report §14、agent_mem 第 37 轮、Tools/README §4.1/4.3/4.4
2026-11-04 15:49:25 +08:00
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
DeepSeek V4.1 Flash 85aaed55f6 refactor(panels): 行追加收敛为 AppendRow 唯一入口(消除平行数组错位隐患)
背景:Code Review 报告 §12.7 建议 1。"改一次面板布局就出错"的**机制性**原因,
是 `EdgePanelLayout` 的每一行对应 **18 个按行平行的 `std::vector`**,
而每个排版分支都要在同一位置、按同一顺序 push 恰好 18 项。只要某个分支少推一项,
那一列之后的所有行都会读到**别人的**矩形(`panels` 的长度不变式只能事后报警)。

本次做法(比"合并成一个 PanelRow 结构体 + 314 处引用"风险低得多、收益相同)
* `EdgePanelLayout::AppendRow(ref, header, nameRect, triangle, check)`:
  **唯一**允许新增行的入口,18 个数组一次补齐(参数行/重采样行一律为空矩形)。
* 三个 push 块改为:
  * 边沿计数表格:13 次 push → **1 次 `AppendRow`**;
  * 列表式:13 次 push → **1 次 `AppendRow`**;参数行/重采样行改为
    `rowXxx.back() = …` **赋值**(长度天然对齐,不再需要 push);
  * 「未勾选/收起」分支里那 6+4 次补空 push **整个删掉**(AppendRow 已补过)。
* ⇒ 新增一列只需改 `AppendRow` 一处(新增行的分支不必逐个补);
  新增行必须走 `AppendRow`(新增列不会漏)。

为什么没有做"完整合并成 std::vector<PanelRow> + 列视图"
* 列视图方案需要给 `EdgePanelLayout` 写自定义拷贝构造/赋值(视图持有 `panelRows` 的地址),
  而该类型是 `EdgePanelLayoutFor()` 的返回值、又被存进 `std::vector`,
  拷贝路径是热路径 —— 一旦拷贝语义写错,会产生**比原缺陷更隐蔽**的悬垂视图。
* 本轮先消除"错位"这个真实隐患(AppendRow 已做到),
  完整结构合并留到有"逐行几何基线"检查之后再做(已记入后续建议)。

验证
* `panels -mode compare` → 7 个面板像素哈希 + 几何**逐项不变**。
* `panelscroll`(MF4-Record,566 行)→ 7 种面板的行数组长度全部等于 566,行为不变。
* `OffscreenChecks -all` → 6/6 PASS,`checks=108 failed=0`。
* `UnitTests` → `checks=260 failed=0`。
* release 全量零 error / 零 warning;产物 786,432 → **785,408 B**(−1,024)。
2026-11-04 15:18:01 +08:00
DeepSeek V4.1 Flash 38fbd4b465 refactor(panels): 拆分 PaintEdgeCursor 的三段内容绘制到独立分片
背景:Code Review 报告 §12.7 建议 2。`PaintEdgeCursor` 原本约 1900 行,其中
"按面板排版类别绘制内容"占 1000 行以上,三段(表格 / 列表 / 自由标记)几乎互不相干,
却共享同一个函数的全部局部变量 —— 任何一段的改动都要在 1900 行里定位,
且很容易误用另一段的变量。

改动
* 新增 `Components/PlotView/Src/PlotViewPanelPaint.cpp`(1016 行,机械搬移、零逻辑改动):
  * `PaintPanelCountTableRows` —— 边沿计数表格:固定表头 + 可滚动数据行。
  * `PaintPanelListRows`      —— 列表式:条目栏 + 展开后的参数行(均值/中心轴/弱峰/重采样/截取)。
  * `PaintPanelMarkerGroups`  —— 自由标记:分组栏 + 表头 + 条目 + 吸顶。
  每个函数**只接收它真正需要的东西**(`layout` + `tabLane` + 少数几个参数),
  不再"整个函数都看得见"。
* `PaintEdgeCursor` 从 1900 → 约 900 行;三处块替换为一行调用,
  骨架(边框 / 标题栏 / 折起三角 / GX 徽标 / ✕)与底部按钮**仍留在一处**(对所有面板同源)。
* 滚条绘制抽为 `pvdetail::PaintPanelScrollBar(g, layout, theme)`(三个分支共用)。

顺带修掉报告里承诺但未落地的一处
* `PaintEdgeCursor` 的标题仍是 7 层三元式,与 `EdgePanelWidth` 里**另一条**链并存
  (那条漏了 MARK/CLIP)。现统一为 `PanelTitle(kind)` 唯一来源。

验证(像素基线全部不变,证明是纯搬移)
* `panels -mode compare` → 7 个面板像素哈希 + 几何**逐项不变**。
* `OffscreenChecks -all` → 6/6 PASS,`checks=108 failed=0`。
* `UnitTests` → `checks=260 failed=0`。
* `panelscroll`(MF4,566 信号)→ 6/7 面板可滚,行为不变。
* release 全量零 error / 零 warning;产物 786,944 → **786,432 B**(−512)。
2026-11-04 15:13:00 +08:00
DeepSeek V4.1 Flash 35b67b216e test(tools): 默认数据/报告路径同时支持在仓库根目录运行
问题:`OffscreenChecks` 的默认 `-data` 只探测 `../TestData/...`,默认 `-report`
也固定写 `../Temp/tools/...`;在**仓库根目录**运行时会静默报
"no data file"(退出码 2),容易被误认为工具坏了(本轮实际踩到)。

改动:
* 默认数据文件候选同时包含 `../TestData/X` 与 `TestData/X`;
* 报告打不开时,若路径以 `../Temp/` 开头则去掉前缀重试一次;
* `Tools/README.md` 补充说明两种调用方式等价,显式 `-data/-out/-report`
  仍以当前工作目录为准。

验证:在仓库根目录执行 `Tools\OffscreenChecks\Build\OffscreenChecks.exe -all`
→ 6/6 检查 PASS,`checks=108 failed=0`;在 `Tools/` 下行为不变。
2026-11-04 14:39:27 +08:00
DeepSeek V4.1 Flash b58b427f62 review(panels+size): 面板结构收敛收尾、审查清理、新增平行数组护栏
一、审查清理
* `PlotView::FreeMarkerValue` 的 `const Lane&` 形参从未使用(函数体里只有一句
  `(void)lane;`)⇒ 删除形参与该语句,更新 2 个调用点。
* `EdgePanelLayoutFor` 里「选区截取是否就绪(双光标)」算了 3 遍 ⇒ 收敛为函数顶部
  一次 `clipReady`。它同时决定面板高度 footer、条目行可见性、按钮几何,
  三处各算一遍时只要有一处条件写错,就是"底部多一段空白"或"没双光标却能滚动"。

二、新增护栏(离屏检查)
* `panels`:新增「平行数组长度一致」不变式 —— `EdgePanelLayout` 的 13 个按行平行
  vector 必须与行数一致;4 个「重采样」专属数组允许为空、非空则必须对齐。
  这是"某列读到别人矩形"这类错位缺陷的唯一低成本护栏(O(1))。
* `panelscroll`:同样加一份(大文件、566 行时才会暴露的问题)。
* `panels` 输出增加 rows 数,便于定位。

三、文档
* `Documents/Code_Review_Plan.md` 新增 §七(第 35 轮节点与硬约束)。
* `Documents/Code_Review_Progress.md` 新增第 35 轮(面板架构评估 + 三次重构记录)。
* `Documents/Code_Review_Report.md` 新增 §12(评估结论 + gdtoa 最终结论 + 护栏价值)。
* `Documents/agent_mem.md` 新增第 35 轮六条规则。
* `Tools/README.md`:补 `panelscroll`、`integer_replacement_matches_std`,
  断言数更新为 260 / 108,最小集表补充面板相关条目。

验证:
* `make release`(删 .o 全量)零 error / 零 warning,产物 **786,944 B**。
* `make -C Tools` 零 error / 零 warning。
* `UnitTests` → checks=260 failed=0 cases=19。
* `OffscreenChecks -all` → 6/6 检查 PASS,checks=108 failed=0。
* `panels -mode compare` → 7 个面板像素哈希与几何逐项不变。
2026-11-04 14:37:59 +08:00
DeepSeek V4.1 Flash 34365b3c1c perf(size): 用自写整数格式化替换全部 std::to_wstring / std::to_string
背景:Code Review 报告 §10.3 / §11.5 建议「替换 std::to_wstring / std::to_string,
可去掉 gdtoa 链(≈33 KB)」。本轮把它做完,并给出这条建议的**真实边界**。

一、新增 `Utf8Util::FormatInteger*`(手写十进制展开,不经 printf)
* `FormatInteger(long long)` / `FormatIntegerUnsigned(unsigned long long)` /
  `FormatIntegerNarrow(long long)`(后者供 MatLoader 拼 HDF5 成员路径)。
* 语义与 `std::to_wstring` / `std::to_string` 完全一致:`0` → `"0"`、负数带 `-`、
  `LLONG_MIN` 用模 2^64 语义取绝对值(不会溢出)。
* 这样做的直接理由:预编译 libstdc++ 的 `to_string` 内部转调 `__mingw_swprintf`,
  工程里只要留一处,MinGW 的 printf 引擎(`mingw_wpformat.o`)+ gdtoa 就必然被链入。

二、全仓替换(76 处 `to_wstring` + 2 处 `to_string`,20 个文件)
* `MatLoader.cpp` 的 `TrimNumber` 整数分支也改为 `FormatInteger`(它曾是产物里
  **最后一个**引用 MinGW printf 的调用点)。
* 需要的地方补 `#include "Utf8Util.hpp"`(仅 `MatLoader.cpp` 一个新 include,其余由
  `PlotViewInternal` / `SignalPanelInternal` / `ApplicationInternal` / `DataModelInternal`
  已经包含)。

三、⚠️ 实测结论:gdtoa **去不掉**(把这件事彻底查清并写进 Makefile 注释)
* 只替换 `to_wstring`/`to_string`:**−1,024 B**(787,968 → 786,944)。
* 再叠加 `-D__USE_MINGW_ANSI_STDIO=0`(把 `swprintf` 让给系统 UCRT):**0 字节**。
* 根因(定位到文件与行):libstdc++ 的
  `lib/gcc/.../include/c++/x86_64-w64-mingw32/bits/os_defines.h:48-51` **无条件**执行
  `#undef __USE_MINGW_ANSI_STDIO` + `#define __USE_MINGW_ANSI_STDIO 1`
  (注释:libstdc++ depends on POSIX-definitions of those functions)。
  `g++ -E -dM` 实测最终值恒为 1 ⇒ **任何 C++ TU 的 `swprintf` / `strtod` 都是 MinGW 版本**,
  命令行无法覆盖。
* 要真省这 33 KB 只能自己实现「浮点 → 十进制(正确舍入)」与「浮点解析的
  十六进制/inf/nan 分支」,等价于重写 gdtoa。**判定:不做**(风险 >> 收益)。
* 该开关已回退,Makefile 里保留一段说明,避免后续接手者重复这两次实验。

四、验证
* 新增单元测试 `integer_replacement_matches_std`:12 个有符号样本(含 LLONG_MIN/MAX)
  + 5 个无符号样本(含 ULLONG_MAX),与 `std::to_wstring` **逐字符对照**,
  并校验窄/宽版本内容一致。
* `UnitTests`:`checks=260 failed=0 cases=19`(新增 41 条断言)。
* `OffscreenChecks -all`:6 个检查全 PASS,`checks=87 failed=0`。
* `panels -mode compare`:7 个面板像素哈希与几何**逐项不变**
  (整数文本从 `to_wstring` 换到手写实现后,渲染结果零差异 —— 这是最强的语义证据)。
* release 全量零 error / 零 warning;产物 787,968 → **786,944 B**。
2026-11-04 14:31:02 +08:00
DeepSeek V4.1 Flash abcf3e6c44 refactor(panels): 浮动面板抽出唯一"面板特征层",消除 3 处分类/滚动几何/滚动设置的重复
背景(用户观察):7 个实验性功能都有自己的浮动面板,观感与骨架高度一致
(标题栏 = 折起三角 + GX 徽标 + 标题 + ✕,内容区可滚动 + 滚动条,底部按钮行),
但每新增一种面板或改动布局,总会出"意料之外"的缺陷。

审计结论:骨架绘制本来只有一份,真正的问题是**同一套"按种类分支"的代码被抄了多份**:

| 重复项 | 位置 | 份数 |
| --- | --- | --- |
| 分类布尔量(listStyle / countTable / hasButton…) | EdgePanelWidth / EdgePanelLayoutFor / PaintEdgeCursor | 3 |
| 滚动条几何 + 滚动量夹取 | 布局里表格 / 列表 / 自由标记三个分支 | 3 |
| 滚动条绘制(6 行) | 绘制里同样三个分支 | 3 |
| SetLane*Scroll 实现 | PlotViewFeatures.cpp | 7 |
| 按种类转发滚动写入 | 滚轮 / 拖滚动条 | 2 |
| 标题文本 | EdgePanelWidth 的三元式链 / 绘制里的三元式链 | 2(内容还不一致!) |

一、新增"面板特征层"(PlotViewInternal.hpp,pvdetail)
* `PanelStyle PanelStyleOf(EdgePanelKind)`:唯一分类来源,`listRows` / `countTable` /
  `markerGroups` + `buildButton` / `twoButtons` / `markerFooter`,`switch` 穷举不写 default
  (新增面板漏登记 ⇒ `-Wswitch` 编译报警)。
  其中专门区分 `footerButtons()`(底部有按钮行)与 `buildButton`(是"构建为新信号")——
  混为一谈会让「选区截取」底部少预留一行高度(本轮实测 124 → 87 px,被像素基线当场抓住)。
* `PanelScrollMetricsFor(...)`:滚动条轨道/滑块几何 + 滚动量夹取 + 内容宽度扣减的唯一实现。
* `PlotView::PanelTitle(EdgePanelKind)`:标题唯一来源。
  ⚠️ 修掉一处**潜伏**错误:`EdgePanelWidth` 的标题链漏了 MARK/CLIP(两者按"边沿计数"量宽),
  目前两种标题恰好等宽所以看不出来,但任何文案改动都会让宽度与标题不匹配。

二、收敛调用点
* `EdgePanelWidth` / `EdgePanelLayoutFor`(表格 / 列表 / 自由标记三分支)/ `PaintEdgeCursor`
  全部改用 `PanelStyleOf` 与 `PanelTitle`;滚动几何改用 `PanelScrollMetricsFor`。
* `EdgePanelLayoutFor` 的 5 层滚动量三元式 → `LanePanelScrollOf(lane, kind)`(该派发点第 33 轮
  就已存在,但布局这里一直没用它)。
* `PaintEdgeCursor` 的滚动条绘制抽成 `paintPanelScrollBar(layout)`,三处调用。
* 7 个 `SetLane*Scroll` 改为一行转发到新的 `SetLanePanelScroll(laneIndex, kind, scroll)`;
  滚轮与"拖滚动条"两条 `switch`/`if` 链改为直接调它(读与写从此同一个入口)。

三、新增离屏检查 `panelscroll`(护栏)
* 对 7 种面板逐一:可滚动时滚到底 / 滚回顶,断言 `scrollOffset` 夹取到 `maxScroll`、
  滑块到达/返回轨道端点、内容区几何不随滚动改变;不可滚动时断言不出现滚动条。
* 用 `MF4-Record.MF4`(566 信号合并到同一网格)实测 6/7 面板可滚(自由标记无固定点,正确)。
  没有这条检查,本次收敛就等于"改完只能靠肉眼"。

验证(本机 GCC 15.2.0 UCRT / 125% DPI)
* `panels -mode compare`:7 个面板的**像素哈希与几何逐项不变**(零差异)。
  这条检查当场抓到了上面 `footerButtons` 的回归,证明基线有效。
* `OffscreenChecks -all`:6 个检查全 PASS,`checks=87 failed=0`。
* `UnitTests`:`checks=219 failed=0`。
* release 全量零 error / 零 warning;产物 788,480 → **787,968 B**(−512)。
2026-11-04 14:20:57 +08:00
DeepSeek V4.1 Flash 5c48bf922e review(follow-ups): 落地上轮建议并给出实测结论(#2/#4 回退、#3 不重写)
一、Tools 完善
* OffscreenChecks `edgestats`:改为自动挑"最有起伏"的信号。旧实现固定取
  `signals[0]`,而 MF4 的第一条信号是常量(6000)⇒ 三个阈值的断言退化成
  "0 == 0" 的空转。现在扫描前 200 条挑 `max-min` 最大者,SCATTER_TEST 上
  三个阈值分别得到 (20,19)/(20,19)/(20,19),与独立参考实现逐位相同。
* UnitTests 新增 `stdio_mingw_vs_ucrt_formats`:把 MinGW ANSI stdio 与系统
  UCRT 的 printf 在**本程序实际用到的全部格式**(`%.*f` / `%.6g` / `%g` /
  `%.4e` / `%.2f %ls` / `%llu` / `%lld` / `%02X` / `%.2f%%`)上做逐字符比对,
  13.6 万次调用 **0 处差异**(覆盖 1e-40~1e40)。它是"把 swprintf 让给 UCRT"
  这条体积优化的验收标准。
* Tools/README.md:同步新增用例与断言数量(219 项 / 18 用例)。

二、第 33 轮报告 §10 后续建议的结论(详细数字见 Progress 第 34 轮)

1. **`EdgeScanFor` 状态查找哈希化:实测后回退。**
   实现方式为 `deque` + `unordered_multimap<keyHash, EdgeScanState*>`(桶内仍
   逐字段比对,碰撞安全;新建/淘汰/定向作废/清空四处同步索引),功能与像素
   验证全绿。但 198 个状态的整轮查询只从 0.097 降到 0.073 us/query(一轮省
   ≈4.8 us),而产物体积 +5,632 B。⇒ 回退。
   同时修正报告的量级:原估"288 信号 ≈0.3ms/帧",实测 ≈20us/帧。

2. **`EdgePanelLayoutFor` 表驱动化:不做。** 实测 1.9~2.5 us/次(每帧约 14 次
   ≈30us),不是热点;第 33 轮已消除"按种类分派"的重复,剩余 957 行是几何算式
   本身,无法表驱动。本轮改为交付"让这类重写可验证"的手段:
   `-run panels -mode record/compare` 逐面板比对几何 + 像素哈希。

3. **`wcstod` 自写替身:实测 0 字节收益。** 只加
   `-D__USE_MINGW_ANSI_STDIO=0` 全量重编后产物仍是 788,480 B、`mingw_wpformat.o`
   仍被链接。原因是预编译 libstdc++ 自己引用 `__mingw_swprintf`:只要还调用
   `std::to_wstring` / `std::to_string`(全仓约 80 处,若干处实参是浮点),
   gdtoa 必然被链入。⇒ 回退,并在 Makefile 留下说明防止重复尝试。

三、文档
* `Documents/Code_Review_Plan.md` 新增 §六(第 34 轮节点与验收标准)。
* `Documents/Code_Review_Progress.md` 新增第 34 轮逐节点记录(含全部实测数字)。
* `Documents/Code_Review_Report.md` 新增 §11(结论表 + 两处量级修正 + 更新的后续建议)。
* `Documents/agent_mem.md` 新增第 34 轮规则:Tools 约定、两个构建陷阱、
  两条"别再重复做"的实验结论。

验证:
* `make release`(删 .o 全量)零 error / 零 warning,产物 **788,480 B**(与改动前一致)。
* `make -C Tools` 零 error / 零 warning。
* `UnitTests.exe` → checks=219 failed=0 cases=18。
* `OffscreenChecks.exe -all` → 5/5 检查 PASS,checks=71 failed=0。
2026-11-04 11:52:10 +08:00
DeepSeek V4.1 Flash 97e9163314 test(tools): 重建单元测试与离屏验证工具(Tools/ 入库)
背景:第 33 轮 Code Review 的三项后续建议(EdgeScanFor 查找哈希化、
EdgePanelLayoutFor 表驱动化、wcstod 自写替身)都卡在"没有可自证的验证手段"上。
本轮先补齐基础设施,再动被建议的代码。

一、Tools/ 从忽略改为入库(只忽略各工具的 Build/ 与 Temp/ 临时产物)
* `.gitignore`:删除 `Tools/` 整目录忽略,改为 `Tools/**/Build/`;
  截图 / 基线 / 报告一律写 `Temp/tools/`(`Temp/` 早已忽略)。

二、新增 Tools/Makefile(统一构建入口,不碰应用本体)
* 目标:`make` 构建全部、`make UnitTests` / `make OffscreenChecks` 单个、
  `make clean` 清工具产物、`make help`。
* 工具链接应用本体 `../Build/Release/*.o`(排除入口 `Main.o`),
  因此 ABI 开关必须与本体一致:`-fno-exceptions` / `-fno-rtti` /
  链接 `-flto -flto-partition=one`。
* 工具源文件自身**不**编译成 LTO 字节码(否则 GCC 15.2.0 在
  `choose_baseaddr` 处 ICE),只在链接阶段带 LTO 参数。
* `-MMD -MP` + `-include`:改头文件 / 增删源文件自动重编重链。
* 修正一个构建陷阱:`$(foreach)` 生成的工具目标会早于 `all`,
  make 会把第一个显式目标当默认目标 ⇒ 显式 `.DEFAULT_GOAL := all`。

三、Tools/UnitTests:纯数据层单元测试(218 项断言,17 个用例)
* 极简自研框架(`Inc/UnitTestFramework.hpp`):无异常、无 RTTI、
  支持布尔 / 整数 / 近似 / **逐位相等** / 宽字符串断言。
* 覆盖:`TryParseDouble` 与 libc `strtod` 逐位一致性(6 万条随机串)、
  接受性语义、`FormatDecimal*` 永不科学计数法、`NiceStep/BuildTicks`、
  公式语法/语义/归一化、批量求值 `FormulaProgram` 与逐点求值逐位一致、
  `ApplyDrawTransform` 顺序、`TransformValueRange`、
  `ComputeMovingAverageIterated` 阶数等价性、
  `DataSet::FormulaTransformFor` 的值域与分组极值(逐点参考实现比对)。

四、Tools/OffscreenChecks:隐藏窗口离屏验证(5 个检查)
* 窗口 `SW_HIDE`;"鼠标"用 `Application::HandleMouseMove()` 合成注入;
  像素从后备缓冲区读取(不依赖窗口可见性),可保存 PNG 与计算区域 FNV 哈希。
* `smoke`:初始化 / 加载 / 渲染 / 落盘,并断言"同状态两帧逐像素相同"。
* `frames`:连续帧抓取(PNG + 哈希 + 耗时),用于问题分析。
* `panels`:7 个实验性面板全部展开后的几何不变式 + **像素基线 A/B**
  (`-mode record` / `-mode compare`)。
* `edgestats`:边沿统计与工具内**独立参考实现**逐位比对,
  覆盖阈值变化与重复查询稳定性。
* `perf`:整帧中位数、`EdgePanelLayoutFor` 单次耗时、
  198 个统计状态的整轮查询耗时(用于优化前后对比)。

五、文档
* `Tools/README.md`:目录结构、构建/运行方式、每个工具的作用、测试项与
  验收标准、临时产物位置、A/B 基线用法、扩展步骤与已知限制。

实测(本机 GCC 15.2.0 UCRT / 125% DPI):
* `UnitTests.exe` → `checks=218 failed=0`
* `OffscreenChecks -all` → 全部 PASS;`SCATTER_TEST.csv` 整帧 ~5ms;
  `MF4-Record.MF4`(283 信号)198 个统计状态整轮查询 0.097 us/query。
2026-11-04 11:41:08 +08:00
DeepSeek-V4.1-Flash c5d7a6fb91 docs(review): 新增 Code Review 最终报告
Documents/Code_Review_Report.md:逐项说明本轮改了什么、为什么、效果如何,
含体积节表前后对照(890,880 -> 794,112 B,-96,768 / -10.9%)、
Debug 构建缺陷(DEBUG_OPT 从未生效)、面板派发收敛与编译期保护、
ApplicationCommands 拆分、性能审查结论(含 3 个记录未做的优化点)、
注释/文档校正、验证清单、提交清单与后续建议。

同时把 Code_Review_Progress.md 补齐 P15/P16 与“搁置节点”的原因说明。
2026-11-04 03:08:45 +08:00
DeepSeek-V4.1-Flash f13944034a docs(review): 清理过时注释并校正项目记忆中的构建/目录说明
- PlotView.hpp:删除“已迁移到数据层的公式包络缓存”这个指向已删代码的注释块。
- agent_mem.md §2:修正 -Os -> -Oz;补充体积相关开关(NO_EXCEPTIONS / NO_RTTI /
  LTO / RELEASE_ONLY_OPT)与“改 Makefile 必须删 .o 全量重编”的提醒;
  链接库列表补 msimg32;忽略列表补 Temp/、.preflight/、Tools/。
- agent_mem.md §3:目录结构按现状重写(Shared/ 已承载通用控件与 PopupMenu/
  ColorPickerPopup;Utilities 现为数据与格式层:DataModel/DataLoader/CsvLoader/
  Mf4Loader/MatLoader/MatH5/Deflate/FormulaEval/Utf8Util/MemProbe;
  删掉早已不存在的 CsvTable/、DataModel/、PaintUtil/ 等旧条目),并写明依赖方向。
- agent_mem.md 末尾新增第 33 轮(本轮审查)记录。
- Code_Review_Progress.md 追加 P15(注释/文档校正)。
2026-11-04 03:07:48 +08:00
DeepSeek-V4.1-Flash 8437100586 refactor(application): 拆分 2165 行的 ApplicationCommands.cpp 为 4 个职责分片
原文件混杂了标签页、派生信号、菜单、工具栏、键盘鼠标、布局调试绘制六类职责,
是本项目最大的“大杂烩”单文件之一。按职责纯搬移(零逻辑改动):

- 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%,属结构性拆分不可避免的噪声)。
2026-11-04 03:02:36 +08:00
DeepSeek-V4.1-Flash 7a3fefb9c2 refactor(plotview): 面板状态/滚动量收敛为唯一派发点,消除 20+ 处手写分支
问题:`Lane` 里 7 种实验性功能各自持有一份 `LanePanelState` 与滚动偏移(共 14 个字段)。
旧实现在每个需要“按种类取用”的地方各写一遍多层嵌套三元式或 switch:
 - EdgePanelLayoutFor 的 enabled / open / collapsed / storedX / storedY
   共 5 条 7 层嵌套三元式(历史上多次因多/少一个右括号编译失败);
 - 标题栏拖拽、✕ 关闭、滚轮翻页、滚动条拖动、错开序数统计各自再写一遍。
新增一种面板要改 20+ 处,漏一处就是「面板拖不动 / 关不掉 / 滚轮滚不到底」
(MARK、RESAMPLE、CLIP 三种面板都真实踩过)。

改法(PlotViewInternal.hpp 新增唯一派发点):
- LanePanelEnabled(lane, kind)
- LanePanelStateOf(lane, kind)    可写/只读
- LanePanelScrollOf(lane, kind)   可写/只读
这些 switch **穷举全部 7 种且不写 default**,因此 -Wswitch(属 -Wall)会在新增
EdgePanelKind 而漏登记时直接报警 —— 已用最小程序验证 -Wswitch 确实会报。

调用点改造:
- EdgePanelLayoutFor:5 条嵌套三元式 -> 3 行访问器调用;局部 kOrder[] -> kEdgePanelKinds。
- 标题栏拖拽:整个 7 分支 switch -> 一次访问器写入。
- 滚轮翻页:current 的三元式链 -> LanePanelScrollOf;switch 补全 MEAN/MARK 并去掉 default。
- 滚动条拖动:switch 补全 MEAN/MARK 并去掉 default。
- ✕ 关闭:去掉 `case COUNT: default:` 的兜底,改为穷举。
- SetLanePanelCollapsed / LanePanelCollapsed:两个 switch -> 一次访问器。
- 滚轮“是否可滚面板”的 kind 列举改为 `hoveredKind != MARK`。

验证:release / debug 全量编译零 warning;产物大小不变(793,088 B,逻辑等价)。
2026-11-04 02:59:57 +08:00
DeepSeek-V4.1-Flash 94fc1c45da perf(size): Release 关闭异步展开表 + 修正 Debug 构建参数
1. Release 专用 `-fno-asynchronous-unwind-tables`(新增 RELEASE_ONLY_OPT,只加在
   Release 的两条编译规则上)。-fno-exceptions 下绝大多数函数不再需要 .pdata/.xdata:
   实测 829,440 -> 793,088 B(-36,352);.pdata 30,420 -> 12,684、.xdata 28,904 -> 10,156。
   Release 不再生成可靠的栈回溯(崩溃诊断);Debug 保留完整展开表。

2. 修正真实缺陷:Makefile 的 DEBUG_OPT = -O0 从未被使用 —— Debug 两条编译规则误用了
   $(RELEASE_OPT)(即 -Oz),且全程没有 -g,因此所谓 Debug 构建其实只是"不 strip 的
   Release"。现在 Debug 规则改用 $(DEBUG_OPT) -g,链接也加 -g。
   代价:Debug 产物 1,734,578 -> 5,992,064 B(真实 -O0 -g,属预期)。

release(全量)790KB 零警告;debug(全量 -O0 -g)零警告。
2026-11-04 02:52:13 +08:00
DeepSeek-V4.1-Flash 5de3763e72 perf(size): 移除多余的 dynamic_cast 并省掉 56KB 的 C++ 名字反修饰器
两处独立收益:

1. Toolbar::BuildBoxes 里唯一的 dynamic_cast 是**多余强转**(Button::widget 本来就是
   UiButtonWidget*),却让全项目必须开 RTTI。改为直接遍历 m_buttons(与控件树同源)。
   最小复现程序实测:39,936 B -> 115,200 B(+75,264 B)。
2. RTTI 关掉后 cp-demangle.o 仍然在链接里 —— 真凶是 libstdc++ 的 vterminate.o
   (引用 __cxa_demangle)。本文件自行定义 __gnu_cxx::__verbose_terminate_handler(),
   让它不再被拉入(本应用启动后会换成自己的 terminate 处理,行为等效)。

Makefile 新增 NO_RTTI=1(默认开启)-> CXXFLAGS += -fno-rtti。

实测:Release 890,880 -> 829,440 B(-61,440);.text 753,136 -> 703,616(-49,520);
.rdata 36,936 -> 26,616(-10,320);Debug 1,801,617 -> 1,734,578(-67,039)。
release/debug 全量重建零警告。
2026-11-04 02:48:59 +08:00
DeepSeek-V4.1-Flash 86ff0291ba docs(review): 新增代码审查计划与进度文档(Code_Review 分支)
- 删除已完成使命的 UI_Refactor_Plan/Progress/Report(已在 dev 提交 764e5be)。
- 新增 Documents/Code_Review_Plan.md:结构 + 性能 + 体积的节点化计划,
  含已确认的问题事实(dynamic_cast 在最小复现里 +75,264 B、
  cp-demangle.o 56,320 B、每个 EdgePanelKind 出现 21-26 次等)。
- 新增 Documents/Code_Review_Progress.md:基线数字与节点记录。
2026-11-04 02:44:23 +08:00
DeepSeek-V4.1-Flash 764e5bee71 chore(docs): 删除过时的 UI 重构阶段文档
Documents/UI_Refactor_Plan.md、UI_Refactor_Progress.md、UI_Refactor_Report.md
描述的是 2026-10-28 那一阶段的计划与结论,已被后续多轮实现推翻/覆盖,
且内容与当前代码不一致,继续保留会误导接手者。

删除后由 Documents/Code_Review_Plan.md / Code_Review_Progress.md /
Code_Review_Report.md 承接(下一分支 Code_Review)。
2026-11-04 02:41:53 +08:00