UI 重构:补充进度台账、阶段报告与共享记忆同步

- Documents/UI_Refactor_Progress.md 补齐 P2 续(PlotViewPanels.cpp)的完成状态、七个分片行数、
  逐像素 A/B 哈希与"回退到旧文件做 A/B 时必须先删陈旧 .o"的复现要求。
- 新增 Documents/UI_Refactor_Report.md:第一阶段(P0/P1/P2)交付物、关键数字、
  基础层契约、验证方式、过程中修掉的 4 个自身缺陷、明确未做项与下一步排序。
- Documents/agent_mem.md:§11.2 追加第 235-240 条(基础层位置与边界、只沿命中链派发、
  Paint 三条契约、布局缓存纪律、PlotView 七分片硬约束、uicheck 回归网),
  并在 §10 变更日志追加本阶段一行。

验证:make release / make debug 零警告;Tools/uicheck 62 passed / 0 failed;
Target.exe 877,568 B(< 900KB)。
This commit is contained in:
DeepSeek V4.1 Flash committed 2026-10-28 12:22:09 +08:00
1 parent e610cea481
commit 7e1852c2d2
2 files changed
+206

No files matched your search

+173
View File
@@ -0,0 +1,173 @@
# UI 重构报告(第一阶段:P0 / P1 / P2)
日期:2026-10-28 分支:`UI_Refactor`(从 `dev` 切出;`dev` 又由 `main@df715a0` 切出)
所有提交**只在本地仓库**,`main` / `dev` 未受影响。计划见 `UI_Refactor_Plan.md`,台账见 `UI_Refactor_Progress.md`。
---
## 1. 本次交付了什么
| 阶段 | 交付物 | 状态 |
| --- | --- | --- |
| **P0** | 入库离屏回归网 `Tools/uicheck`(62 项断言,`build.ps1` + `main.cpp`) | ✅ |
| **P1** | UI 基础层 `Components/Shared/{Inc,Src}/UiLayout.*` + `UiWidget.*`(约 1 440 行含注释) | ✅ |
| **P2** | `PlotView` 实现按关注点重分为**七个分片**;`PlotViewPaint.cpp` 9 080 → **4 913** 行(**−46%**) | ✅ |
三个提交:
```
ec3f93f UI 重构 P2 续:把布局与命中几何从绘制分片拆出(PlotViewPanels.cpp,逐像素 0 差异)
b532046 UI 重构 P2:PlotViewPaint.cpp 按关注点分片(机械搬移,逐像素 0 差异)
8bdf48c UI 重构 P0+P1:新增布局/控件基础层与离屏回归网(对产品像素零影响)
```
---
## 2. 关键数字
| 指标 | 重构前 | 重构后 |
| --- | --- | --- |
| `PlotViewPaint.cpp` | 9 080 行 | **4 913 行(−46%)** |
| 最大源文件 | 9 080(`PlotViewPaint.cpp`) | 4 913(`PlotViewPaint.cpp`) |
| `PlotView` 实现文件数 | 4 | **7**(职责单一) |
| 自动化 UI 探针 | **0** | **62 项断言**(入库、可复跑) |
| 产物 | 873 984 B | **877 568 B**(+3.6 KB,< 900 KB 阈值) |
| 产品像素差异 | — | **0**(三场景渲染哈希逐一相同) |
---
## 3. 新增的 UI 基础层(P1)
### 3.1 `UiLayout`:布局原语 + 布局缓存
| 能力 | 说明 |
| --- | --- |
| 矩形原语 | `UiRectIntersect` / `UiRectUnion` / `UiRectDeflate` / `UiRectHasArea` |
| **设备像素内侧对齐** | `UiRectSnapInward`(上界 `floor`、下界 `ceil`)—— 离屏位图贴图源矩形必须用它,否则补丁会多算一行盖到相邻网格(历史 1px 接缝 BUG) |
| 对齐 | `UiAlignInRect`(BEGIN/CENTER/END/**STRETCH**、超尺寸夹取、偏移后置) |
| Stack | `UiStackExtent` / `UiStackRects`(固定尺寸 + `kUiSizeAuto` 均分剩余、**绝不产生负尺寸**) |
| Flow | `UiFlowRects`(换行 + `overflow` 下标,供图例 `+N` 使用) |
| Abs | `UiAbsRect`(绘图区叠加物的主力布局) |
| 缓存 | `UiLayoutCache`:`std::deque` 保证引用稳定、**定向失效**、按"最久未命中"淘汰 |
> 设计取舍:**不实现 Grid**(绘图区不是表格,用不上);**Card 只是皮肤**不是布局;
> 真正需要的是 **Abs(主力)/ Anchor / Stack / Flow / Measure-driven**。
### 3.2 `UiWidget`:控件树基类
```mermaid
flowchart TD
H[UiHost(P5)<br/>共享 DIB · 脏区并集 · 一次回送 · 动画 tick]
H --> R[UiWidget 根]
R --> A[普通控件:直接画进共享缓冲]
R --> B[高成本叶子:自持位图,完成即 BitBlt]
A --> S[SurfaceView{dc, rect, stride, bits, version}]
B --> S
```
已实现的契约:
| 契约 | 内容 |
| --- | --- |
| 两段式布局 | `Measure`(自下而上、带缓存、失效只沿祖先链)/ `Arrange`(自上而下、回调 `OnArrange` 摆子节点) |
| 脏区下探绘制 | `dirty` 为空 = 全画;**与子树矩形无交集 ⇒ 整棵子树 return**;有效裁剪 = 祖先裁剪切 ∩ 自身 ∩ 脏区 |
| 裁剪契约 | 进入时收紧 GDI+ 裁剪并暴露 `ctx.clip`;**GDI 绘制不受其约束** ⇒ 派生类必须把 `ctx.clip` 传给 `UiTextClipped` 一类辅助函数 |
| 事件派发 | capture(拖拽期直投)→ modal(只向模态子树)→ **只沿命中链**(最深命中者 → 祖先兜底);键盘沿焦点链;`{consumed, redraw, dirty}` |
| 交互仲裁 | `CaptureMouse` / `ReleaseMouse`(仅持有者可释放)/ `SetModal` / `SetHitTransparent` / `RequestFocus` |
| 光标链 | `QUERY_CURSOR` 由最上层认领者返回,未认领交给宿主 |
| 动画 | `AdvanceAnimation(nowMs)` 逐节点下发同一时间戳并汇总"是否仍有动画" |
| 缓存键 | `ContentKey()`(派生类提供)+ `BaseContentKey()`(矩形/可见性),父层只聚合 |
**三条禁区(写进 `agent_mem.md` §11.2 第 235–240 条)**
1. 波形数据→像素管线(网格/曲线/采样点)**不做成控件节点**;
2. **不是每个控件自持位图**(内存按面积线性增长:128 网格已 ≈250 MB 量级);
3. 禁止"父脏 ⇒ 子树全脏"(会把"只重绘一个网格/面板"打回整块重绘)。
---
## 4. 分片重构(P2)
### 4.1 七个分片
| 文件 | 行数 | 职责 |
| --- | --- | --- |
| `PlotViewPaint.cpp` | 4 913 | 绘制(`Paint*` / `Render*` / 输入框绘制) |
| `PlotViewInteraction.cpp` | 3 430 | 命中、鼠标/键盘、光标形状、面板层序 |
| `PlotViewFeatures.cpp` | 3 266 | 光标读数、提示、缩放、自适应、派生曲线、边沿统计 |
| `PlotViewRaster.cpp` | 1 876 | 光晕层 / 场景缓存 / 装饰层 / 宽基底图层与预算 |
| `PlotViewPanels.cpp` | 1 359 | 面板布局、图例徽标几何、自由标记取值与探测 |
| `PlotView.cpp` | 889 | 视图核心(构造、布局、坐标映射、网格结构) |
| `PlotViewCacheKey.cpp` | 474 | 内容指纹与缓存键 |
### 4.2 为什么能"零风险"搬移
- `PlotViewInternal.hpp` 里 `pvdetail` 的工具函数**全部 `inline`** ⇒ 跨 TU 安全的;
- 所有搬移的都是**成员函数定义**,声明仍在 `PlotView.hpp`;
- 因此**不需要改任何 include,也不需要改 `Makefile`**(按 basename 自动收集 `.cpp`)。
### 4.3 验证方式(重点)
**不是"能编译就算过"**,而是**逐像素 0 差异**:
| 场景 | 内容 |
| --- | --- |
| A | 默认全览(4 条信号) |
| B | 深放大 0.90–1.10 + 自由标记(含固定点、透明度 0.8)+ 均值滤波(点数 2 / 迭代 1)+ 弱峰调整(60% / 2 次)+ 时间光标 |
| C | 在 B 基础上再平移一帧(触发宽基底图层滚动/补条路径) |
渲染后备缓冲区后取 FNV 哈希:
```
迁移前 A 2166933039626F66 B C444ECFEB18FD73D C 7984446D8E7A4418
迁移后 A 2166933039626F66 B C444ECFEB18FD73D C 7984446D8E7A4418 ← 三场景完全一致
```
哈希与 `Tools/uicheck` 都在每次搬移后重跑;`make release` / `make debug` 零警告。
---
## 5. 过程中修掉的自身缺陷
| # | 缺陷 | 修法 |
| --- | --- | --- |
| 1 | `UiWidget` 头文件漏声明 `AdvanceAnimationSelf` / `AnimationActiveSelf`(`.cpp` 已引用) | 补 protected 虚函数 |
| 2 | 最初的派发是"遍历同层兄弟、未消费就继续" ⇒ **一次点击可能被视觉上位于下方的控件也处理** | 改为"只沿命中链派发 + 祖先兜底" |
| 3 | 探针用**绝对次数**断言事件派发次数(被前面用例的计数污染) | 全部改成**增量断言** |
| 4 | 分片后回退做 A/B 时,`Build/<Config>/` 残留 `PlotViewRaster.o` / `PlotViewCacheKey.o`,链接探针因重复符号失败 | 记录为复现步骤的硬要求:先删陈旧 `.o` |
另外顺手清理:`agent_mem.md` 侧新增 §11.2 第 235–240 条 + §10 变更日志行;
`documents/refactor_report_2026-10-16.md`(过时报告)在 `dev` 分支删除,结论已并入 `agent_mem.md`。
---
## 6. 明确未做的事(避免误解)
| 未做 | 原因 |
| --- | --- |
| 迁移 Toolbar / TabBar / SignalPanel / PlotView 到 `UiWidget` | 属 P3/P4;需逐组件迁移、每个组件单独 A/B,本轮只打地基 |
| `UiHost` + 共享 32bpp DIB + `SurfaceView` | 属 P5;需同时把 56 处 `InvalidateRect` 收敛成脏区并集,必须与迁移同批验证 |
| 引入多线程光栅 | 本项目实测 **1.03x**(GDI+ 内部串行),且有"后台改写视界覆盖用户平移"的历史事故 |
| 每控件独立位图 | 内存按面积线性增长(128 网格 ≈250 MB) |
| 删除 1 295 行硬核"为什么"注释 | 它们是防回归的核心资产,行数不是问题 |
---
## 7. 下一步建议(按收益/风险排序)
1. **P3:Toolbar 迁移**(最小、最易验证)→ 用 `uicheck` + 新旧像素对比收口。
2. **P4:浮动面板迁移**:`EdgePanelLayoutFor`(651 行)→ 真控件(Stack + Abs + Measure-driven),
同时消掉面板里 10 多个平行数组(`rowRefs/rowChecks/rowPoints/rowOrder/rowLevelTracks/...`)。
⚠️ 每个面板单独一轮:吸顶/滚动/滚动条/滑块复位/行悬停压暗/开关联动是 6 轮补丁叠出的隐式约定。
3. **P5:共享 DIB + 脏区并集**(`CreateCompatibleBitmap` → top-down 32bpp `CreateDIBSection`,
`SurfaceView` 带 `version` 作废机制)。
4. 每阶段一个可独立回滚的提交;`main` / `dev` 只接受"`uicheck` 全绿 + 像素 A/B 通过"的提交。
---
## 8. 一句话结论
> 本阶段没有改动任何产品行为:**新增了一套可复用、可测试的布局/控件基础层**,
> 并把 9 080 行的"万能绘制文件"按关注点降到 4 913 行(−46%),
> 两者都用**逐像素 0 差异**与 **62 项离屏断言**证明。后续每个组件迁移都可以复用这套回归网,
> 从而把"AI 写得快"转换成"你敢合多少"。
+33
View File
@@ -3023,6 +3023,8 @@ else if (panelOnly) { /* 只回送“本帧重绘过的区域并集” */ }
| 2026-10-27 | DeepSeek-V4.1-Flash(**本机**) | **浮动面板事件穿透 + 「自由标记」总是预览最近信号 + 固定点位于 GX/图例之下 + 预览曲线取值插值 + 体积红线 900KB(详见 §12 末尾「2026-10-27 第六轮」与 §11.2 第 230–234 条)**。① **面板右键/中键穿透**:新增 `PlotView::OnRButtonDown`(`TopPanelAtPoint` 命中即置顶并消费,宿主不再弹网格 Y 轴菜单);`OnMButtonDown` 在面板上不再进入平移。② **自由标记探测去掉“必须贴近曲线”的门槛**:两个候选(折线垂直距离 / 同一时刻采样点纵向距离)取更近者 ⇒ 网格内只有一条信号、鼠标离曲线很远时也能预览并单击固定(A/B:恢复旧门限 ⇒ 5 项断言失败)。③ **固定标记点必须位于 GX 徽标与图例之下**:用 `gd::Region::Exclude` 把图例托底与徽标矩形从 `PaintFreeMarker` 的绘制区里排除,胶囊文字在相交时改走 GDI+(GDI 文字不认裁剪);实测图例区域像素差 **0**(旧的“画完再重绘图例”方案残留 240px 抗锯齿差)。④ **预览曲线取值点悬空**:`paintDerivedCapsule` 改为在光标时刻对派生曲线**按时间插值**(先 `ApplyDrawTransform` 再线性插值,与折线逐点一致);阶跃区实测标注点与曲线差 **0.0px**(旧实现偏 36–39px),中心轴/均值滤波/弱峰调整共用同一段代码。⑤ **体积红线由 800KB 放宽到 900KB**(到达阈值时提醒用户即可,不是禁止超过)。验证:离屏临时探针 **25/25**(已删除,不留在仓库);`make release`/`make debug` 零警告;`Target.exe` **860,672 字节(840.5 KiB)**。 |
| 2026-10-28 | DeepSeek-V4.1-Flash(**UI_Refactor 分支**) | **UI 重构第一阶段(P0/P1/P2,全部只提交到本地 `UI_Refactor` 分支,不影响 `main`)**:① 新增 `Components/Shared/{Inc,Src}/UiLayout.*`(矩形原语 / 对齐 / 设备像素内侧对齐 / Stack / Flow / 布局缓存)与 `UiWidget.*`(控件树基类:两段式布局、脏区下探绘制、只沿命中链派发、capture/modal/hover/focus 仲裁、光标认领链、统一动画 tick、内容指纹入口)。② 新增入库离屏回归网 `Tools/uicheck`(62 项断言,链接 `Build/<Config>/*.o` 排除 `Main.o`)。③ `PlotView` 实现按关注点重分为七个分片:`PlotViewPaint.cpp` 9 080 → **4 913** 行(−46%),新增 `PlotViewRaster.cpp`(1 876)、`PlotViewCacheKey.cpp`(474)、`PlotViewPanels.cpp`(1 359);搬移**逐像素 0 差异**(三场景渲染哈希 `A 2166933039626F66 / B C444ECFEB18FD73D / C 7984446D8E7A4418` 迁移前后完全一致)。详见 §11.2 第 235–240 条与 `Documents/UI_Refactor_Plan.md` / `UI_Refactor_Progress.md` / `UI_Refactor_Report.md`。 |
## 11. 跨设备协作须知(起源设备视角)
> **本文件是唯一权威记忆**。所有设备 / Agent 共用它,**不要再新建 `agent_mem_origin.md` 等旁支记忆文件**。
@@ -4081,6 +4083,37 @@ else if (panelOnly) { /* 只回送“本帧重绘过的区域并集” */ }
234. ⚠️ **产物体积红线已放宽到 900KB**(用户 2026-10-27):**到达 900KB 必须提醒用户**,
以便确认功能带来的体积膨胀是否值得;这不是“不许超过”,旧 800KB 红线作废。
235. ⚠️⚠️ **新增 UI 基础层的位置与边界**(2026-10-28,`UI_Refactor`):
`Components/Shared/{Inc,Src}/UiLayout.*`(纯几何 + 布局缓存)与 `UiWidget.*`(控件树基类)。
它们**不依赖任何功能组件**(只有 `Theme.hpp` / `UiLayout.hpp`),符合既有的
`Components/* → Components/Shared → Utilities` 依赖方向。
⚠️ 新增 `.cpp` 放在 `Components/Shared/Src` 即可,**无需改 `Makefile`**(按 basename 自动收集)。
236. ⚠️⚠️ **`UiWidget` 的事件语义:只沿命中链派发,绝不分发给同层兄弟**。
`Dispatch` 先判 capture(拖拽期事件直投捕获者)→ 再判 modal(只向模态子树派发)→
位置事件沿"最深命中者 → 祖先"冒泡(命中者优先,未消费才交给祖先作兜底);
键盘/字符沿**焦点链**(无焦点交给根,保证宿主级快捷键仍可用)。
早期实现"遍历同层兄弟、未消费就继续" ⇒ **一次点击可能被视觉上位于下方的控件也处理**(已修)。
237. ⚠️⚠️ **`UiWidget::Paint` 的三条契约**:
① `ctx.dirty` 为空 = 脏区未知 ⇒ 全画;非空 ⇒ 只保证该区域被更新;
② 脏区与子树矩形**无交集 ⇒ 整棵子树直接 return**(这是框架里唯一真正省时间的机制,
禁止任何派生类退化成"父脏 ⇒ 子树全脏");
③ 有效裁剪 = 祖先裁剪切 ∩ 自身矩形 ∩ 脏区;**GDI 绘制不受该裁剪约束** ⇒
派生类必须把 `ctx.clip` 传给 `UiTextClipped` 一类带 `ETO_CLIPPED` 的辅助函数。
238. ⚠️ **`UiLayoutCache` 用 `std::deque` 保证"`Store` 之后已有引用仍有效"**;
淘汰按"最久未被命中";`Invalidate` 必须是**定向**失效(与 `m_edgeScans` 同一纪律),
**禁止**为了让命中率好看而整体 `Clear()`。
239. ⚠️⚠️ **`PlotView` 实现分片已按"关注点"拆分(七个文件)**,不再按函数名前缀:
`Paint 4.9k / Interaction 3.4k / Features 3.3k / Raster 1.9k / Panels 1.4k / Core 0.9k / CacheKey 0.5k`
(`PlotViewPaint.cpp` 由 9 080 行降到 4 913 行)。硬约束:
* `PlotView::Paint` / `PaintLane` **只允许**定义在 `PlotViewPaint.cpp`;
* 像素缓存与每帧预算在 `PlotViewRaster.cpp`;内容指纹/缓存键在 `PlotViewCacheKey.cpp`;
面板布局与自由标记几何/探测在 `PlotViewPanels.cpp`;
* 机械搬移的判据是**逐像素 A/B 哈希一致**(不是"能编译")。
⚠️ 搬移后若用旧文件回退做 A/B,**必须先删掉 `Build/<Config>/` 里对应的陈旧 `.o`**,
否则链接会因重复符号失败。
240. ⚠️ **回归网已入库**:`Tools/uicheck`(`build.ps1` 链接 `Build/<Config>/*.o`,排除 `Main.o`)。
改 UI 基础层或迁移组件前先跑它(当前 62 项断言);A/B 校验时把它当"判别力证据"。
### 11.3 离屏验证方法学(本机偏好,强烈建议沿用)
- **不要占用用户的鼠标 / 键盘 / 屏幕**:优先 `.preflight/uitest`(离屏 harness:`SW_HIDE` 窗口 + 直接注入消息 + `BitBlt` 从 `m_backDc` 截图)或 `.preflight/perf`(离屏计时 / 逐像素对比)。