diff --git a/Documents/UI_Refactor_Report.md b/Documents/UI_Refactor_Report.md new file mode 100644 index 0000000..a0b45a3 --- /dev/null +++ b/Documents/UI_Refactor_Report.md @@ -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)
共享 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//` 残留 `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 写得快"转换成"你敢合多少"。 diff --git a/Documents/agent_mem.md b/Documents/agent_mem.md index 0359a1b..c2b4ee7 100644 --- a/Documents/agent_mem.md +++ b/Documents/agent_mem.md @@ -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//*.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//` 里对应的陈旧 `.o`**, + 否则链接会因重复符号失败。 +240. ⚠️ **回归网已入库**:`Tools/uicheck`(`build.ps1` 链接 `Build//*.o`,排除 `Main.o`)。 + 改 UI 基础层或迁移组件前先跑它(当前 62 项断言);A/B 校验时把它当"判别力证据"。 + ### 11.3 离屏验证方法学(本机偏好,强烈建议沿用) - **不要占用用户的鼠标 / 键盘 / 屏幕**:优先 `.preflight/uitest`(离屏 harness:`SW_HIDE` 窗口 + 直接注入消息 + `BitBlt` 从 `m_backDc` 截图)或 `.preflight/perf`(离屏计时 / 逐像素对比)。