重构(UI): 统一组件协议 + 去手写链路 + 大文件按职责拆分 + 代码审查 + 剥离 Tools

## 1. 新增 UiComponent 顶层组件协议(不是负优化)

Components/Shared/Inc/UiComponent.hpp:纯接口、无状态,8 个方法
SetScale / Layout / Area / Paint / HitTest / OnMouseLeave / IsEditing / QueryCursor。

Toolbar / TabBar / SignalPanel / PlotView 本来就各自拥有完全同名同签名的成员,
因此改造 = 加 override + 补 Area()(PlotView 另加一个非 const 的 QueryCursor 转发,
因为它自己的查询会惰性建立行缓存),实现代码零改动、静态派发、体积无增长。

刻意不把 UiWidget 当作它的基类:UiWidget 是"树节点"(父给尺寸、父下探脏区、
命中沿树链派发),而 PlotView(4 层位图缓存)/ SignalPanel(行位图缓存)是
"地盘宿主"。强行上树会推翻"父脏则子全脏"的前提,历史上正是这类耦合导致过
"后台渲染覆盖平移结果""命中矩形错位"的事故。头文件里已写明这条理由。
Toolbar/TabBar 是两者的交集:对外实现 UiComponent,对内持有一棵 UiWidget 树。

## 2. 宿主侧:四条手写链 -> 组件表

Application 新增 ComponentEntry{ component, target, chrome } 与 m_components
(底->顶:plot -> panel -> toolbar -> tabBar),BuildComponentTable() 装配一次。

- ApplyDpi        6 行逐个 SetScale  ->  1 个循环
- Relayout        手算三列 x 坐标    ->  UiStackRects(内容区, HORIZONTAL, {panelW, splitW, AUTO})
- WM_MOUSELEAVE   逐个 OnMouseLeave   ->  循环累积 chromeChanged / plotChanged
- WM_SETCURSOR    if/else if 链      ->  从表尾向前找首个命中者,取其 QueryCursor

顺带兑现历史缺口:UiLayout::UiStackRects 自引入以来第一次有了生产调用点。

有意保留显式的三条链(WM_LBUTTONDOWN / WM_LBUTTONUP / WM_MOUSEMOVE):它们各自带
组件专属后置动作(提交重命名、构建派生信号、面板联动滚动、更新拖拽光标、
plotOnlyMove 决定局部失效还是整帧失效)。塞进表里只能靠 lambda 字段,
可读性反而下降 —— 属于用户明确禁止的"过度抽象",因此不做。

## 3. 大文件按职责拆分(纯整函数搬移,逐像素 0 差异)

SignalPanelInteraction.cpp 2329 -> 831 行,拆出:
  SignalPanelEdit.cpp   616  内联编辑(筛选框、组名/信号名、数值、颜色与透明度)
  SignalPanelScroll.cpp 205  动画与滚动(折起、联动滚动、滚动到目标信号)
  SignalPanelMenu.cpp   527  弹出菜单(系数/偏移、镜像、右键菜单与命令回执)

Shared/PopupMenu.cpp 1901 -> 706 行,拆出:
  PopupMenuLayout.cpp 753  尺寸测量、项矩形排版、滚动可见性
  PopupMenuPaint.cpp  397  背景/阴影/项绘制
  Inc/PopupMenuInternal.hpp 103   pmdetail 常量 + inline 辅助(跨 TU 安全)

拆分判据是"职责"而非行数;共享常量一律收敛到 *Internal.hpp。

## 4. 代码审查(修 5 处)

- SignalPanelInteraction.cpp 头注释仍写"4 片",实际 6 片 -> 改为完整清单
- PopupMenuLayout.cpp 机械搬移时把构造函数一起搬走 -> 重复定义,只保留在交互分片
- PopupMenu.cpp 的 #include "PopupMenuInternal.hpp" 落进 namespace jdi 内 -> 移到文件头
- PopupMenuPaint.cpp 缺 UiControls.hpp(UiDrawEditFieldContent 未声明)-> 补 include
- 全仓复查历史删除符号残留引用(m_paintPlot / ClampCursorsAfterPan /
  PaintFreeMarkerTooltip / CaptureDecorCache / FitSinglePointSignal / CsvTable /
  DataFile / .preflight / structcheck)-> 零命中

## 5. Tools/ 剥离

git rm -r --cached Tools + .gitignore 新增 Tools/(本地文件保留,可随时复跑)。
ui_refactor 分支为全本地分支,另做 filter-branch 清除历史中的该目录。

## 6. 验证

- make release / make debug:零 error 零 warning
- rendercheck:A-H 八个场景哈希与改造前逐一相同
  (A 2166933039626F66 B C444ECFEB18FD73D C 7984446D8E7A4418 D BF855879CA12F289
   E 1C663E4118616016 F 0535292D8E07AE18 G 7A58172C46577729 H 23F390DD76141113)
  FUNC 7/7 PASS
- uicheck:69 passed, 0 failed
- TargetCheck.exe 891,392 B(< 900 KB 阈值)

## 7. 文档

Documents/UI_Refactor_Plan.md / UI_Refactor_Progress.md / UI_Refactor_Report.md
新增第五阶段章节;Documents/agent_mem.md 同步变更日志与「第七轮」硬约束。
This commit is contained in:
DeepSeek V4.1 Flash committed 2026-10-28 14:35:32 +08:00
1 parent 675c2a5f54
commit 0ec8055e95
20 files changed
+3287 -2678

No files matched your search

+54
View File
@@ -302,3 +302,57 @@ protected:
| 面板失效不全(贴旧像素) | `ContentKey()` 漏字段 | P4 里每个面板配一条"改 X 后像素必须变"的断言 |
| 体积超 900KB | 新框架代码量 | 每阶段结束量一次;超阈值先报告用户 |
| 布局级联死循环 | Measure 里改子节点 | 单元测试固定"Measure 一轮即收敛" |
---
# 第五阶段计划:统一组件协议 + 去手写链路 + 拆分收尾(2026-10-28)
> 状态:✅ 已完成(详见 `UI_Refactor_Progress.md` 第七轮 / `UI_Refactor_Report.md` 第五阶段)
## 目标
1. 让"顶层组件"有统一协议,宿主不再逐个点名;
2. 用同一套协议把 `SetScale` / `Layout` / `MouseLeave` / `SetCursor` 四条手写链改成循环;
3. 把剩余两个"千行以上"的文件按**职责**拆分(不再按函数名前缀);
4. 一轮代码审查(错误注释 / 过期注释 / 名实不符 / 残留引用);
5. 把误入库的临时验证工具 `Tools/` 从提交树剥离并写入 ignore。
## 设计决定(先证明不是负优化)
* **协议只做"同签名归一"**:`UiComponent` 是纯接口、无状态,四个组件本就各自拥有
完全同名同签名的方法 ⇒ 改造 = 加 `override` + 补 `Area()`,实现零改动、生成码几乎不变。
* **不把 `UiComponent` 做成 `UiWidget` 的基类**:树节点是"父给尺寸 + 父下探脏区",
而 `PlotView`(4 层位图缓存)/ `SignalPanel`(行位图缓存)是"地盘宿主"。
强行上树会推翻缓存前提(历史事故:后台渲染覆盖平移、命中矩形错位)。
* **不做的一律写明理由**:按下/松开/移动三条链带组件专属后置动作,保留显式分支。
## 任务清单
| # | 任务 | 验证手段 |
| --- | --- | --- |
| P5-1 | 新增 `Shared/Inc/UiComponent.hpp`,四组件继承 + `override` | 编译零警告 |
| P5-2 | `Application`:`ComponentEntry` 表 + `BuildComponentTable()` | — |
| P5-3 | `ApplyDpi` / `Relayout` / `WM_MOUSELEAVE` / `WM_SETCURSOR` 表驱动 | `uicheck` 69/69 |
| P5-4 | `Relayout` 内容区改用 `UiStackRects`(布局库首个生产调用点) | `rendercheck` 8 哈希不变 |
| P5-5 | `SignalPanelInteraction.cpp` 拆 4 片 + `SignalPanelEdit/Scroll/Menu.cpp` | 8 哈希不变 |
| P5-6 | `PopupMenu.cpp` 拆 3 片 + `PopupMenuInternal.hpp` | 8 哈希不变 |
| P5-7 | 代码审查(注释准确性、构造函数重复、include 位置) | 人工 + 编译 |
| P5-8 | `git rm -r --cached Tools` + `.gitignore` + 历史过滤器 | `git ls-files Tools` 为空 |
| P5-9 | 三份文档更新 + 提交 | `git status` 干净 |
## 风险与对策
| 风险 | 对策 |
| --- | --- |
| 表驱动改动影响绘制顺序(toolbar 与 tabBar 重叠区) | 表按"底→顶"排,`WM_SETCURSOR` 从表尾向前命中,与旧 `if` 顺序一致 |
| 拆分导致逐像素差异 | 纯整函数搬移;每拆一个文件立刻跑 `rendercheck` 比对 8 哈希 |
| `filter-branch` 会清掉工作区被剔除的路径 | 先备份 `Tools/` 到 `%TEMP%`,重写后还原为未跟踪文件 |
| 中文注释在 PS 5.1 下乱码 | 文档编辑走 VS Code 编辑工具(UTF-8),终端只做 ASCII 拼接 |
## 完成判据
* `make release` / `make debug` 零 error 零 warning;
* `Tools/rendercheck` 8 个场景哈希与基线**逐一相同** + 7 条 FUNC 断言 PASS;
* `Tools/uicheck` 69/69;
* `TargetCheck.exe` < 900 KB;
* `git ls-files Tools` 返回空、`git status` 干净、`main` / `dev` 未被触碰。
+94
View File
@@ -248,3 +248,97 @@ Tools/uicheck/uicheck.exe # 62 passed, 0 failed
(必须用**覆盖全部输入的键**,否则就是历史上"漏混字段 → 贴旧像素"那类静默 BUG)。
3. 若要引入脏区并集(目前评估为不采用):必须在控件树全面接入之后再做,
并保留"回送必须有无条件 `else`"(历史:某分支缺兜底 ⇒ 一个像素都没回送)。
---
## 第七轮(2026-10-28):统一组件协议 · 去手写链路 · 大文件按职责拆分 · 审查 · 工具剥离
### 进度总览
| # | 任务 | 状态 | 验证 |
| --- | --- | --- | --- |
| 1 | 新增 `UiComponent` 顶层协议,四组件 `override` | ✅ | 编译零警告 |
| 2 | `Application` 组件表 `m_components` + `BuildComponentTable` | ✅ | — |
| 3 | `ApplyDpi` / `Relayout` / `WM_MOUSELEAVE` / `WM_SETCURSOR` 四条链改表驱动 | ✅ | `uicheck` 69/69 |
| 4 | `UiStackRects` 首次用于生产代码(内容区三列) | ✅ | 像素哈希不变 |
| 5 | `SignalPanelInteraction.cpp` 2329 → 4 片 | ✅ | 8 哈希不变 |
| 6 | `PopupMenu.cpp` 1901 → 3 片 + `PopupMenuInternal.hpp` | ✅ | 8 哈希不变 |
| 7 | 代码审查(注释/名实不符/残留引用) | ✅ | 5 处修正 |
| 8 | `Tools/` 从提交树剥离 + `.gitignore` | ✅ | `git ls-files Tools` 为空 |
| 9 | 三份文档更新 | ✅ | — |
### 1. `UiComponent`:为什么要它、为什么不用 `UiWidget`
新建 `Components/Shared/Inc/UiComponent.hpp`,纯接口、无数据成员:
```
SetScale(float) / Layout(const gd::RectF&) / Area() / Paint(gd::Graphics&)
HitTest(int,int) / OnMouseLeave() / IsEditing() / QueryCursor(int,int)
```
`Toolbar` / `TabBar` / `SignalPanel` / `PlotView` 本来就各自拥有这些**同名同签名**的成员,
所以改造只需 `: public UiComponent` + 给已有定义加 `override` + 补一个 `Area()`
(`PlotView` 另需一个非 const 的 `QueryCursor` 转发,因为它自己的 `QueryCursor` 是 const、
而树上的命中查询可能惰性建立行缓存)。
**结论:这一步不是负优化** —— 生成代码几乎不变(全是静态派发,无虚表之外的额外开销),
却让宿主从"认识每一个组件"变成"认识一份表"。体积实测 `TargetCheck.exe` 891 392 B,
仍低于 900 KB 阈值;`Target.exe` 886 784 B 与改造前一致。
### 2. 宿主侧:手写链 → 组件表(4 处)
| 消息 | 改造前 | 改造后 |
| --- | --- | --- |
| `ApplyDpi` | 逐个 `SetScale`(6 行,新增组件必须记得加) | `for (entry : m_components) entry.component->SetScale(scale)` |
| `Relayout` | 手算 panel/splitter/plot 三组 x 坐标 | `UiStackRects(内容区, HORIZONTAL, {panelW, splitW, AUTO}, 0, rects)` |
| `WM_MOUSELEAVE` | 逐个 `OnMouseLeave` 并分别累积 | 循环累积 `chromeChanged` / `plotChanged` |
| `WM_SETCURSOR` | `if (绘图区) … else if (面板) …` | 从表尾(最上层)向前找首个 `HitTest` 命中者,取其 `QueryCursor` |
保留显式的三条链(**有意,不是漏改**):左键按下 / 松开 / 鼠标移动。
它们各自挂着一串"组件专属后置动作"(提交重命名、构建派生信号、面板联动滚动、
更新拖拽光标、判断 `plotOnlyMove` 决定局部还是整帧失效)。
把这些塞进表里要引入 lambda 或回调字段,可读性反而下降 —— 属于用户明确禁止的"过度抽象"。
### 3. 拆分对照
| 原文件 | 拆后 | 内容职责 |
| --- | --- | --- |
| `SignalPanelInteraction.cpp` 2 329 | **831** | 命中测试、鼠标/键盘派发、光标形状 |
| + `SignalPanelEdit.cpp` | 616 | 筛选框、组名/信号名、数值输入、颜色与透明度 |
| + `SignalPanelScroll.cpp` | 205 | 折起动画、联动滚动、滚动到目标信号 |
| + `SignalPanelMenu.cpp` | 527 | 系数/偏移、镜像、右键菜单与命令回执 |
| `PopupMenu.cpp` 1 901 | **706** | `OnEdit*` 交互分支 |
| + `PopupMenuLayout.cpp` | 753 | 尺寸测量、项矩形排版、滚动可见性 |
| + `PopupMenuPaint.cpp` | 397 | 背景/阴影/复用 `UiControls` 的项绘制 |
| + `Inc/PopupMenuInternal.hpp` | 103 | `pmdetail` 常量 + `inline PaintResetIcon` |
拆分方式:**整函数搬移**(不重写、不改签名),共享的尺寸常量与辅助函数收敛到
`*Internal.hpp`(`constexpr` / `inline`,跨 TU 安全)。
这样每个 TU 的行数落在 200–900 的可读区间,而逐像素输出保持 0 差异(哈希佐证)。
### 4. 代码审查结果
| 文件 | 问题 | 处理 |
| --- | --- | --- |
| `SignalPanelInteraction.cpp` | 头注释仍写"4 片:核心/渲染/交互与编辑" | 改为六分片清单(含本文件定位) |
| `PopupMenuLayout.cpp` | 构造函数被机械搬移导致重复定义 | 只保留在 `PopupMenu.cpp` |
| `PopupMenu.cpp` | `#include "PopupMenuInternal.hpp"` 落在 `namespace jdi` 内 | 移到文件头 |
| `PopupMenuPaint.cpp` | 缺 `UiControls.hpp`(`UiDrawEditFieldContent` 未声明) | 补 include |
| 全仓 | 历史删除符号残留引用 | grep 9 个符号,**零命中** |
### 5. `Tools/` 剥离
`git rm -r --cached Tools` + `.gitignore` 新增 `Tools/`,
并对 `UI_Refactor` 分支做 `filter-branch` 清除历史中的该目录(分支全本地、未推送,风险可控);
本地文件在剥离前已备份到 `%TEMP%` 并复原,`uicheck` / `rendercheck` 随时可复跑。
### 6. 验证数据
```
make release (TargetCheck) → 零 error / 零 warning
rendercheck: A 2166933039626F66 B C444ECFEB18FD73D C 7984446D8E7A4418 D BF855879CA12F289
E 1C663E4118616016 F 0535292D8E07AE18 G 7A58172C46577729 H 23F390DD76141113 (全部与基线一致)
FUNC 7/7 PASS
uicheck: 69 passed, 0 failed
体积: Build/Release/TargetCheck.exe = 891,392 B (< 900 KB)
```
+78
View File
@@ -202,3 +202,81 @@ Tools/uicheck/uicheck.exe
| 布局重复计算 | ⚠️ 小幅(`EdgePanelLayoutFor` 19 个调用点,待 P4 落地后才吃到 1~3ms/帧) |
| 拖拽/缩放帧耗时(45~80ms 那部分) | ❌ 不改善:瓶颈是 GDI+ 光栅化的像素量,架构不改变"要画多少像素" |
| 派发开销 | ❌ 不改善:µs 级,占预算 <0.1%;控件树改善的是**正确性**不是帧时间 |
---
# 第五阶段(2026-10-28):统一组件协议、去手写链路、按职责拆分
## 1. 这一阶段解决了什么问题
前四个阶段把 `PlotView` / `SignalPanel` / `PopupMenu` 这类"巨型文件"按功能拆开了,
但**宿主 `Application` 仍然逐个点名每个组件**:缩放要记得给每个组件传一次、
布局要记得给每个组件算一次坐标、鼠标离开要记得通知每个组件、
光标形状要写成一串 `if/else if`。每加一个顶层组件,就有四处必须同步修改——
漏掉任何一处都是"改了但没反应"的静默 bug。
本阶段把这个"人肉登记表"换成**真正的登记表**。
## 2. 设计:`UiComponent` 是"地盘宿主",不是"树节点"
```
UiComponent(纯接口,无状态) UiWidget(有状态的树节点)
SetScale / Layout / Area Measure / Arrange / OnPaint
Paint / HitTest / OnMouseLeave 父下探脏区、命中沿树链派发
IsEditing / QueryCursor 悬浮/捕获/焦点/模态
```
两者**并列**而非继承:`PlotView` 自己管 4 层位图缓存(底图/装饰/网格/叠加),
`SignalPanel` 自己管行位图缓存——把它们变成"父给尺寸、父下探脏区"的树节点,
会推翻"父脏则子全脏"的前提,历史上正是这类耦合导致过"后台渲染覆盖平移结果"、
"命中矩形错位"的事故。
`Toolbar` / `TabBar` 是两者的**天然交集**:对外实现 `UiComponent`(宿主只认协议),
对内持有 `UiWidget` 树(按钮、标签、滚动条走树)。这条分层让"小型控件复用树的派发能力"
与"大型组件保留自己的渲染策略"可以并存。
**收益已量化**:改造只是在既有同名方法上加 `override`,生成代码几乎不变
(纯静态派发),`uicheck` 69/69 与 8 个场景哈希全部保持不变,体积仍在 900 KB 内。
## 3. 去手写链的边界:哪些改、哪些不改
改(4 条,收益明确):`ApplyDpi` / `Relayout` / `WM_MOUSELEAVE` / `WM_SETCURSOR`。
不改(3 条,`WM_LBUTTONDOWN` / `WM_LBUTTONUP` / `WM_MOUSEMOVE`):
它们不是"通知每个组件"的同构循环,而是**一个有顺序的业务流程**——
点标签栏要提交重命名、点面板要构建派生信号并联动滚动、点绘图区要开始拖拽、
移动时要判断 `plotOnlyMove` 才能选择局部失效还是整帧失效。
把这些塞进表里只能靠 lambda 字段,阅读成本反超收益。**刻意保留显式代码。**
## 4. 拆分的度量
拆分判据换成"职责"之后,每个文件的定位能用一句话说清:
| 文件 | 一句话职责 |
| --- | --- |
| `SignalPanel.cpp` | 状态、行集合、布局与几何 |
| `SignalPanelPaint.cpp` | 把状态画成像素(含行位图缓存) |
| `SignalPanelInteraction.cpp` | 坐标 → 语义(命中、派发、光标) |
| `SignalPanelEdit.cpp` | 内联编辑的生命周期 |
| `SignalPanelScroll.cpp` | 动画与滚动联动 |
| `SignalPanelMenu.cpp` | 弹出菜单与其命令回执 |
`PopupMenu` 同理:`Layout`(几何)/ `Paint`(像素)/ `PopupMenu.cpp`(交互)。
纯搬移 + 逐像素哈希校验,保证"重构不改行为"。
## 5. 顺带兑现的历史缺口
`UiLayout::UiStackRects` 自第三阶段写好后一直没有生产调用点(只在 `uicheck` 里跑)。
本轮 `Relayout` 的内容区三列切分改用它,**布局库终于有了真实使用场景**,
也顺手删掉了一段手算坐标。
## 6. 遗留与建议
1. `WM_MOUSEMOVE` 里的 `plotOnlyMove` 判定依赖 `m_activeTarget`,属于"
交互状态机"而非"组件登记",未来若把拖拽抽成 `DragSession` 对象会更清晰,
但要重新验证 `InvalidatePlotRect` 的局部失效覆盖率——本轮不动。
2. `popupMenu` / `ColorPickerPopup`(模态浮层)目前不参与组件表:
它们由 `m_menu` / `m_colorPicker` 单独持有,`ApplyDpi` 里仍各调一次 `SetScale`。
若以后浮层数量增加,可给它们一个"浮层表",但两个成员值不值得抽象——存疑。
3. `Tools/` 已剥离;若将来要把它作为**对外可复现的验证资产**,建议单独开一个
`qa/` 目录并显式加入版本控制(同时更新 `.gitignore` 的例外规则)。
+83
View File
@@ -3025,6 +3025,9 @@ else if (panelOnly) { /* 只回送“本帧重绘过的区域并集” */ }
| 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`。 |
| 2026-10-28 | DeepSeek-V4.1-Flash(**UI_Refactor 分支**) | **UI 重构第五阶段:统一组件协议 + 去手写链路 + 大文件按职责拆分 + 代码审查 + `Tools/` 剥离(详见 §12 末尾「2026-10-28 第七轮」)**。① 新增 `Components/Shared/Inc/UiComponent.hpp`(纯接口、无状态):`SetScale / Layout / Area / Paint / HitTest / OnMouseLeave / IsEditing / QueryCursor`;`Toolbar` / `TabBar` / `SignalPanel` / `PlotView` 各自 `: public UiComponent`,**只加 `override` + 补 `Area()`,实现零改动**。② `Application` 新增 `ComponentEntry { UiComponent*; Target; bool chrome; }` 表 + `BuildComponentTable()`,把 `ApplyDpi` / `Relayout` / `WM_MOUSELEAVE` / `WM_SETCURSOR` 四条手写链改成循环;按下/松开/移动三条链**有意保留显式分支**(带组件专属后置动作)。③ `UiLayout::UiStackRects` 首次用于生产代码(内容区三列切分)。④ `SignalPanelInteraction.cpp` 2 329 → **831** 行(拆出 `SignalPanelEdit/Scroll/Menu.cpp`),`Shared/PopupMenu.cpp` 1 901 → **706** 行(拆出 `PopupMenuLayout.cpp` / `PopupMenuPaint.cpp` + 新增 `Inc/PopupMenuInternal.hpp`)。⑤ 代码审查修 5 处(过期头注释、重复构造函数、include 落进 namespace、缺 include、残留符号全仓零命中)。⑥ `Tools/` 从提交树剥离 + `.gitignore`,并对 `UI_Refactor` 做 `filter-branch` 清除历史。验证:`make release` 零错误零警告;`rendercheck` **8 场景哈希与基线逐一相同**、7 条 FUNC 全 PASS;`uicheck` **69 passed / 0 failed**;`TargetCheck.exe` **891,392 字节**。 |
## 11. 跨设备协作须知(起源设备视角)
> **本文件是唯一权威记忆**。所有设备 / Agent 共用它,**不要再新建 `agent_mem_origin.md` 等旁支记忆文件**。
@@ -7915,3 +7918,83 @@ DG3 data=0 cycle=0 (空组,一个字符串通道)
面板 / `LaneVisual` / `LegendHit` / `EdgePanelLayout` 都是 `PlotView` 的嵌套类型,探针里必须写 `PlotView::` 前缀。
- ⚠️ 本轮开始前发现工作区里 `PlotViewPaint.cpp` 与 `Documents/agent_mem.md` 被**回退到 `8f735d5`**(丢失第四/五轮全部改动),
已 `git checkout --` 恢复到 `HEAD` 后再施工;提交前务必确认这两份文件与 HEAD 一致。
## 本轮(2026-10-28 第七轮):统一组件协议 + 去手写链路 + 大文件按职责拆分
> 用户需求:① 全面放弃手写链路,UI 绘制尽量走"控件树 + 派生基类"(先确认不是负优化);
> ② 存在大量冗余的文件按职责拆分,但不为拆分而过度抽象;③ 一轮代码审查(错注释/过期注释/名实不符);
> ④ 把临时测试工具 `Tools/` 从提交树剥离并写入 ignore;⑤ 更新 Documents 下的计划/进程/报告。
### A. `UiComponent`:顶层组件的统一协议(新增 `Components/Shared/Inc/UiComponent.hpp`)
纯接口、**无状态**,7 个方法:`SetScale / Layout / Area / Paint / HitTest / OnMouseLeave / IsEditing`
(外加默认返回 `nullptr` 的 `QueryCursor`)。
`Toolbar` / `TabBar` / `SignalPanel` / `PlotView` 各自 `: public UiComponent`,
它们本来就有一模一样签名的方法 ⇒ **只加 `override` 与 `Area()`,实现零改动**。
⚠️ **不把 `UiWidget` 当这个基类**(四条契约冲突,已写进头文件注释):
`UiWidget` 是"树上的节点"(父给尺寸、父下探脏区、命中走树链、`OnPaint` 只画自己);
顶层大组件是"一块地盘的宿主"(`PlotView` 4 层自持位图缓存、`SignalPanel` 行位图缓存、
`PopupMenu`/拾色器是模态浮层)。两者是**兄弟关系**,不是父子。
`Toolbar`/`TabBar` 同时属于两者:对外实现 `UiComponent`,对内持有一棵 `UiWidget` 树。
### B. 宿主侧:手写链 → 组件表
`Application` 新增 `struct ComponentEntry { UiComponent* component; Target target; bool chrome; }`
与 `std::vector<ComponentEntry> m_components`(底→顶:plot → panel → toolbar → tabBar),
`BuildComponentTable()` 在 `Initialize` 里 `Setup` 之后装配一次。
被表驱动的四处(旧实现每处都逐个点名组件):
| 消息 | 旧 | 新 |
| --- | --- | --- |
| `ApplyDpi` | 6 行逐个 `SetScale` | 1 个循环 |
| `Relayout` | 4 行逐个 `Layout`(且手算三组坐标) | 循环 + `UiStackRects` 切分 |
| `WM_MOUSELEAVE` | 4 行逐个 `OnMouseLeave` + 分别累计 | 循环 + `chrome` 标志决定"整帧/只绘图区" |
| `WM_SETCURSOR` | `if (绘图区命中) … else if (面板) …` | 从表尾往前找首个命中者,其 `QueryCursor` 生效 |
**仍是显式的(有意)**:左键按下/抬起、鼠标移动、编辑态吸收、分隔条拖拽 ——
这些分支各自带"按下后要提交重命名 / 构建派生信号 / 联动滚动 / 更新拖拽光标"等**专属后续动作**,
强行表驱动会把它们变成藏在表里的 lambda,反而更难读(正是"过度抽象")。
顺带兑现一个历史缺口:`UiLayout` 的 `UiStackRects` 第一次有了**生产调用点**(内容区三列切分)。
### C. 大文件按职责拆分(纯搬移,逐像素 0 差异)
| 原文件 | 现状 | 说明 |
| --- | --- | --- |
| `SignalPanelInteraction.cpp` 2 329 行 | **831 行** | 拆出 `SignalPanelEdit.cpp`(616) / `SignalPanelScroll.cpp`(205) / `SignalPanelMenu.cpp`(527) |
| `Shared/PopupMenu.cpp` 1 901 行 | **706 行** | 拆出 `PopupMenuLayout.cpp`(753) / `PopupMenuPaint.cpp`(397) + 新增 `Inc/PopupMenuInternal.hpp`(103) 收敛尺寸常量与 `PaintResetIcon` |
拆分判据是**职责**,不是行数:面板按"编辑 / 动画滚动 / 菜单 / 交互"切;
菜单按"几何排版 / 绘制 / 交互"切。文件级常量与辅助函数搬进 `*Internal.hpp`
(命名空间 `pmdetail`,`constexpr`/`inline`),跨 TU 安全。
### D. 代码审查(本轮修掉的问题)
1. `SignalPanelInteraction.cpp` 头部注释写着"分片按职责划分为核心/渲染/交互与编辑"(只有 4 片),
实际已是 6 片 ⇒ 改为完整的六分片清单。
2. `PopupMenuLayout.cpp` 机械搬移时**把构造函数也搬了进去**(与 `PopupMenu.cpp` 重复定义)
⇒ 删重复、只保留在交互分片。
3. 拆分后 `PopupMenu.cpp` 的 `#include "PopupMenuInternal.hpp"` 一度落在 `namespace jdi` **内部**
(等价于声明 `jdi::jdi`)⇒ 移到文件头。
4. `PopupMenuPaint.cpp` 缺 `UiControls.hpp`(`UiDrawEditFieldContent` 未声明)⇒ 补 include。
5. 全仓复查了历史删除符号的残留引用(`m_paintPlot` / `ClampCursorsAfterPan` /
`PaintFreeMarkerTooltip` / `CaptureDecorCache` / `FitSinglePointSignal` / `CsvTable` / `DataFile` /
`.preflight` / `structcheck`)⇒ **零命中**,无需修改。
### E. 验证
* `make release` / `make debug` **零警告**;`TargetCheck.exe` **891 392 B**(< 900 KB),
`Target.exe` **886 784 B**(与改造前一致)。
* `Tools/rendercheck`:A–H 八个场景哈希与改造前**逐一相同**
(`A 2166933039626F66 … H 23F390DD76141113`),7 条功能断言全 PASS。
* `Tools/uicheck`:**69 passed / 0 failed**。
* 体积:`UiComponent` 是纯接口,静态派发几乎零开销;`TargetCheck` 比 `Target` 多出的
4.6 KB 来自 `PopupMenuInternal.hpp` 的 `inline` 函数在 3 个 TU 各实例化一份
—— 这是拆分的已知代价,仍远低于 900 KB 阈值。
### F. 工具目录剥离
`Tools/`(`uicheck` / `rendercheck`)已从**提交树**剥离并写入 `.gitignore`
(历史提交里也不含它 —— 见台账的「分支操作取证」)。本地文件保留,仍可随时复跑。