diff --git a/Documents/refactor_report_2026-10-16.md b/Documents/refactor_report_2026-10-16.md deleted file mode 100644 index 505dddc..0000000 --- a/Documents/refactor_report_2026-10-16.md +++ /dev/null @@ -1,508 +0,0 @@ -# 代码审计与解耦报告(2026-10-16) - -> 本轮任务:① 修两个 UI 交互缺陷;② 继续上一轮(`6eedbe3c5d`)之后的审计与解耦 —— 修正过时注释 / -> 函数与参数命名、消除数据结构冗余成员、抽出 `Components/Shared` 共享组件、按组件目录结构拆分 -> 巨型实现文件;③ 为二进制产物瘦身;④ 全部改动不得影响功能与性能。 -> -> 结论:**UI 缺陷已修复并 A/B 验证;数据模型去冗余(信号 / 分组只保留唯一名称)落地;全仓过时注释与 -> 命名批量修正;新增 `Components/Shared` 组件并迁入通用 UI 控件;四个大文件(PlotView / SignalPanel / -> Application / DataModel)按功能拆分为多个协作文件;产物体积 **−24.6%**(1 013 248 → 764 416 B)**。 -> 离屏测试链(49 + 69 + 46 + **156** + **19** 条断言)全绿、`make release` / `make debug` 零警告。 -> -> **`PlotView.cpp`(13 695 行 / 544 KB)的整体拆分已完成**:拆为 `PlotView.cpp`(787) + -> `PlotViewPaint.cpp`(6 367) + `PlotViewInteraction.cpp`(2 715) + `PlotViewFeatures.cpp`(3 005) + -> `PlotViewInternal.hpp`(936),最大单文件 544 KB → 270 KB(提交 `e6b7bf6`;详见 §7.1 与 §七)。 -> 本报告首版曾在 §TL;DR 处误记为“整体拆分本轮未执行”——那是提交 `0871eb0` 时刻的状态, -> 已被紧随其后的提交 `e6b7bf6` 推翻,此处已更正。 - ---- - -## 一、UI 交互修复(用户报告) - -### 1.1 分组折起热区:只有折叠三角与「组X」标签可折起 - -* **需求**:点分组**名称**不应折起该分组;只有点到折叠三角或「组X」标签才折起。 -* **实现**:`RowPart` 新增 `GROUP_TITLE`;`RowHitPart()` 为分组行先判折叠三角 / 复选框 / 关闭 ✕, - 再判「组X」标签矩形(`[checkRect.Right + 8*scale, +GroupLabelWidth)`)⇒ `GROUP_TITLE`, - 其余为 `BODY`;`OnLButtonDown` 只在 `DISCLOSURE` 或 `GROUP_TITLE` 时调用 `ToggleGroupFromRow()` - (`BODY` 直接返回、不折起)。 -* **附带修正**:双击「组X」标签不再进入改名(该区域现在是折起热区);分组名本体双击仍可改名。 - -### 1.2 分组改名输入框不再吞掉「N 项」计数 - -* **需求**:组名双击进入编辑时,输入框不得把组内信号个数包进去。 -* **根因**:`RowNameRect()`(分组行)的右边界原先取到关闭 ✕ 左侧,把右侧的项数文本一起框了进去。 -* **实现**:右边界改为 `项数左边缘 − 6*scale`;并新增 `GroupLabelText()` / `GroupStatsText()` - 两个“绘制与几何共用”的文本来源(`PaintGroupRow` 与 `RowNameRect` 都从它们取文本与宽度), - 彻底避免“名称可用宽度”与“实际绘制文本”漂移。 - -### 1.3 验证 - -`.preflight/perf/panelanim_main.cpp` 新增/更新断言:点分组名不折起(也不触发滚动)、 -扫掠定位「组X」热区、悬浮态与筛选态下折叠三角仍逐像素一致、改名输入框右边界早于项数文本。 -**156/156 通过**(连跑 5 次稳定;其中两条与墙钟时间有关的断言已改为对机器抖动宽容, -但不失去判别力);**A/B**(把 `OnLButtonDown` 的热区判断改回“任意行内区域都折起” -+ 把 `RowNameRect` 右边界改回“吞掉项数”)→ **10 项如实失败**,恢复后全绿。 - -> ✅ **完整的 A/B 实测(后续补记,见 §9.7)**:回退这两处修复后 -> `panelanim` **147/156(9 项如实失败)**、`make structcheck` **90/93(3 项如实失败)**; -> 恢复后 **156/156** 与 **93/93**。失败文案直接点名需求: -> `the group rename box stops before the item counter`、 -> `clicking the group name does not start collapsing` / `does not collapse the group`、 -> `single-click toggle gate accepts only DISCLOSURE / GROUP_TITLE (found 0)`。 -> 这两条要求现在还被 **`make structcheck` 第 [3] 组**的源码级断言长期守着(见 §9.7), -> 不依赖人来重读散文。 - ---- - -## 二、数据模型去冗余(用户指出的重点) - -### 2.1 `SignalData` / `SignalGroup` 只保留**一个**名称字段 - -* **原状**:`SignalData{ name(源名称,永不修改), displayName(用户重命名) }` + `Title()`; - `SignalGroup` 同构。 -* **论断依据**(用户):“整个程序中提到信号的名字时都只有一个描述对象,且信号靠下标定位、 - 从不靠名字匹配” ⇒ 两个字段是同一语义的两份副本。 -* **改动**: - * 删除 `displayName`,`name` 成为**唯一**名称字段(加载时取列名 / 通道名,用户重命名即写入它); - * 删除 `Title()` 访问器(31 处调用点改为直接用 `.name` / `->name`); - * `DataSet::SetSignalDisplayName` → **`RenameSignal`**、`SetGroupDisplayName` → **`RenameGroup`**; - ⚠️ 语义随之收紧:**空串 / 纯空白不再表示“恢复源名称”,而是被拒绝**(名称不再有“回退值”)。 -* **影响面**:`DataModel.hpp/.cpp`、`Application.cpp`、`PlotView.cpp`、`SignalPanel.cpp`、 - `datacheck` 探针。编译期全覆盖(无一处遗漏)。 -* **收益**:每条信号 / 每个分组少一个 `std::wstring`(24 字节 + 堆分配)与一条 `Title()` 判断分支; - MF4 的 124 信号场景即少 124 个字符串对象。 - -### 2.2 其它冗余成员清理 - -| 位置 | 问题 | 处理 | -| --- | --- | --- | -| `Application::m_loadingBytes` | **全仓既不读也不写**(体积展示实际由 `m_loadingTotalBytes` 承担),注释却写着“用于展示体积” | **删除** | -| `Application::m_loadingBytesRead/TotalBytes` | 语义清晰、成对使用 | 保留,补全注释 | - -> 其余结构体(`SignalStyle` / `TabConfig` / `Lane` / `TimeAxis` / `SignalRef` / `EdgeScanState` 等) -> 逐字段核对后未发现同类“同义多字段”现象;`Lane` 的“体量过大”属**内聚性**问题而非冗余, -> 已按 §7.3 / §9.1 抽出 `LanePanelState`(本轮完成)。 - ---- - -## 三、过时注释与命名的批量修正 - -上一轮把 `DataFile` 改名为 `SignalGroup`、`fileIndex` 改名为 `groupIndex`,但**注释没跟上** -(用户指出的重点,例如 `DerivedSignalRequest::groupIndex` 的注释仍写“原信号所属文件下标”)。 -本轮用“逐条比对的替换对 + 校验”的方式一次性清理: - -* **参数 / 成员注释**:`文件下标`→`分组下标`、`文件内信号下标`→`组内信号下标`、 - `原信号所属文件下标`→`原信号所属分组下标`、`所属文件下标`→`所属分组下标`、`目标文件下标`→`目标分组下标`; -* **措辞统一**:`文件分组`→`分组`、`该文件全部信号`→`该分组全部信号`、`移除该文件`→`移除该分组`、 - `打开 CSV 文件`→`打开数据文件`、`加载指定 CSV 文件`→`加载指定的数据文件`、 - `CSV 文件完整路径`→`数据文件完整路径`、`数据层 RemoveFile`→`数据层 RemoveGroup`、 - `按 [文件][信号] 索引`→`按 [分组][信号] 索引` …; -* **文不对题的参数名**:`VisibleIndexRange(const TimeAxis& file, …)` → 形参名 `axis` - (注释也从“目标文件”改为“目标时间轴”); - `Application::LoadFileAsync(…, SignalGroup& file, …)` → 形参名 `group`; -* **函数名残留的“File 语义”**(`RowKind::FILE` 上一轮已改,但函数名没跟上): - `PaintFileRow`→`PaintGroupRow`、`FileCloseRect`→`GroupCloseRect`、`OpenFileMenu`→`OpenGroupMenu`、 - `FileGroupLabelWidth`→`GroupLabelWidth`、`StyleEditField::FILE_NAME`→`GROUP_TITLE`; - 局部变量 `fileRow/fileHeight/isFile` → `groupRow/groupHeight/isGroup`; -* **格式损坏的注释块**:`DataModel.hpp` 有两处 `/**\t\t * @brief …`(块起始与正文被并到同一行, - 早期误编辑遗留)已还原为规范的多行块; -* **重复的文档块**:`Application::NewTab` 上方叠了两份 `@brief`(其中一份是废弃的简短描述)已删。 - -> ⚠️ 命名冲突踩坑:`StyleEditField::FILE_NAME` 一度改为 `GROUP_NAME`,而 **`nb30.h`(由 `windows.h` -> 引入)里 `#define GROUP_NAME 0x80`** ⇒ 枚举成员被宏替换,编译报 “expected identifier before -> numeric constant”。改名 `GROUP_TITLE` 解决。**新增标识符前需确认不是 Windows 头里的宏** -> (同类高风险名:`GROUP_NAME`、`TIME`、`SMALL`、`NEAR`、`FAR`)。 - ---- - -## 四、`Components/Shared`:通用 UI 控件组件(新增) - -### 4.1 动机与职责划分 - -用户要求:把在多个功能组件里到处复用的通用部分(按钮、输入框、色块、滑块、复选框……) -抽到 `Components` 下的一个 **Shared** 组件,其它组件直接复用。 - -本轮先落地**最典型、复用最广**的一类:**UI 控件**。 - -``` -Components/Shared/ - Inc/UiControls.hpp 控件声明(+ 复用的 UiInk / IconDirection 等类型) - Src/UiControls.cpp 控件实现 -``` - -迁入 `Components/Shared` 的控件(原先散落在 `Utilities/PaintUtil.*`): - -| 类别 | 函数 | -| --- | --- | -| 图标 | `UiDrawPlusIcon` / `UiDrawCrossIcon` / `UiDrawTriangleIcon` | -| 复选框 | `UiDrawCheckBox` | -| 滑块 | `UiSliderRatioFromX`、`UiDrawSlider` | -| 内联输入框 | `UiTextAtClipped` 之外的整套:`UiDrawEditField`、`UiDrawEditFieldContent`、`UiEditTextLeft`、`UiEditOffsetAt`、`UiEditFieldMouseDown`、`UiEditFieldMouseMove` | -| 颜色样块 | `UiDrawSwatch`、`UiSwatchNeedsOutline` | -| 取值胶囊配色 | `UiInk`、`UiColorLuminance`、`UiContrastInk` | - -`Utilities/PaintUtil.*` 保留**低层绘制原语**(矩形 / 圆角矩形 / 圆角路径 / 直线 / 批量实线 / -颜色混合 / 文本与测量 / 省略号)。 - -### 4.2 依赖方向(重要) - -``` -Components/*(功能组件) ──▶ Components/Shared(UI 控件) ──▶ Utilities(低层原语) -``` - -`Components/Shared` **只**依赖 `Utilities`,绝不反向依赖任何功能组件;`Utilities` 也**不再** -包含控件代码(`PaintUtil.hpp` 里那 16 个控件声明已移除)。调用方(`PlotView` / `SignalPanel` / -`TabBar` / `PopupMenu` / `ColorPickerPopup`)改为 `#include "UiControls.hpp"`。 - -### 4.3 落地方式与验证 - -* 用**纯代码搬移**脚本(按内容锚点定位、UTF-8 无 BOM 读写)完成迁移,中文注释逐字节保留; -* `PaintUtil.cpp` 953 → 571 行;新增 `UiControls.cpp` 381 行、`UiControls.hpp` 216 行; -* `Makefile` 的 `CXX_INCLUDES_PATHS` / `CXX_SOURCES_PATHS` 已登记 `Components/Shared/*`; -* 编译零警告;四套探针全绿(控件行为逐像素不变)。 - ---- - -## 五、二进制瘦身(实测) - -### 5.1 结论:**1 013 248 → 764 416 B(−24.6%,省 248 832 B)** - -| 项 | 大小 | -| --- | --- | -| `Target.exe`(Release `-Os -s`,本轮前) | 1 013 248 B | -| `Target.exe`(Release `-Os -s`,本轮后) | **764 416 B** | -| 差值 | **−248 832 B(−24.6%)** | -| (参考)`Target.exe`(Debug `-O0`,未 strip) | 13 849 441 B | - -### 5.2 真正的体积来源(实测,纠正“都是 PlotView.cpp 撑大的”这一推断) - -本轮前后各目标文件体积(`Build/Release/*.o`,单位 B): - -| 目标文件 | 本轮前 | 本轮后 | 说明 | -| --- | --- | --- | --- | -| `JustDataInspector.o`(**windres 资源**) | **305 826** | **46 730** | 图标条目改 PNG 存储 | -| `PlotView.o` | 406 527 | 406 527 | 最大的**代码**目标文件 | -| `DataModel.o` | 142 134 | 142 134 | | -| `Application.o` | 134 929 | 134 903 | 删了一个死成员 | -| `SignalPanel.o` | 106 165 | 106 165 | | -| `PopupMenu.o` | 49 305 | 49 305 | | -| `ColorPickerPopup.o` | 43 454 | 43 454 | | -| `CsvLoader.o` / `Mf4Loader.o` | 35 559 / 31 010 | 同 | | -| `UiControls.o` | —(原先在 `PaintUtil.o` 内) | 19 112 | 新增的共享控件 | -| `PaintUtil.o` | 29 412 | 29 412 | 只留低层原语(`nm` 已核实控件已全部移出) | - -> ⚠️ `.o` 体积受节对齐 / 重定位表 / 符号表主导,删掉几百行代码不一定会让 `.o` 变小; -> 判断“代码搬去了哪里”要看 `nm`,不要看 `.o` 字节数。 - -* **主因(≈85% 的可省体积):图标资源**。`Resources/JustDataInspector.ico` 原本 302 430 B, - 其中 **256×256@32bpp 那条以“未压缩 BMP”存储,单条 270 376 B**(占整包 27%)。 - Windows Vista 起 ICO **允许用 PNG 承载 256×256 条目** ⇒ 本轮把该条目重编码为 PNG: - **270 376 → 11 280 B**,整个 ico **302 430 → 43 334 B**,**观感完全不变** - (小尺寸 16/32/48/64 仍保留未压缩 BMP,因此即使某个老 API 不认 PNG 条目也仍有正确图标可用)。 - 实测结论:**真正把产物推到 1 MB 的是这一个图标条目,而不是 `PlotView.cpp`。** -* **次因:代码体积**。`PlotView.o` 406 KB 是最大的**代码**目标文件,这与 13.7 K 行单文件的事实一致; - 但**把大文件拆成多个 .cpp 并不会让机器码变小**(同一批函数与数据仍要进二进制), - 所以「拆分 ⇒ 瘦身」这一推断**不成立**(拆分解决的是可维护性,不是体积)。 -* **实测反向失败的优化(已回退)**:`-ffunction-sections -fdata-sections -Wl,--gc-sections` - 在本工程上**反而增大了 9 216 B**(1 013 248 → 1 022 464),原因是分段对齐开销大于被回收的死代码; - 已回退,`Makefile` 保持原样(仅新增 `Components/Shared` 登记)。 - ---- - -## 六、验证 - -| 套件 | 覆盖 | 结果 | -| --- | --- | --- | -| `.preflight/perf/panelanim_main.cpp` | 信号面板:卷尺式折起/展开动画(刚性平移、逐帧裁剪、同步滚动)、筛选态折起与计数、折叠三角、悬浮组名命中、「目标」行绘制/布局、**本轮新增的分组折起热区与改名输入框边界** | **156/156** | -| `.preflight/perf/verify_main.cpp` | PlotView 渲染/交互:Y 自适应、派生信号、图例命中、GX 徽标悬停、装饰层无残影、平移逐像素 | **49/49** | -| `.preflight/perf/datacheck_main.cpp` | 数据层:单轴 CSV、多时间轴 CSV、MF4(124 信号 / 3 轴)、轴共享与解析、**重命名写入唯一名称字段且空名被拒**、派生信号沿用源轴 | **69/69** | -| `.preflight/perf/edgecheck_main.cpp` | 边沿统计符号/分帧推进/面板排版 | **46/46** | -| A/B(本轮) | 回退两处 UI 修复 | **10 项如实失败** | -| 构建 | `make release` / `make debug` | **零警告** | - -全部离屏执行(`SW_HIDE` + 不注入真实输入 + 进程/线程 `BELOW_NORMAL`)。 - ---- - -## 七、遗留项与后续建议(逐项的最终处理状态) - -> 2026-10-16 更正:本行曾写“整体拆分本轮未执行”,那是提交 `0871eb0` 时刻的状态; -> 紧随其后的提交 **`e6b7bf6`(按组件目录结构把四个巨型实现文件拆分为多个协作文件)** -> 已把该项做完(详见 §7.1 / §7.2 与 §九)。 - -### 7.1 `PlotView.cpp`(13 695 行 / 544 KB)按功能拆分 —— **已完成** - -拆成 4 个协作文件 + 1 个组件私有头(共 6 个文件,最大单文件从 544 KB 降到 270 KB): - -| 文件 | 行数 | 职责 | -| --- | --- | --- | -| `Inc/PlotView.hpp` | — | 类声明(未改) | -| `Src/PlotView.cpp` | 787 | 视图核心:构造/析构、上下文、布局、坐标映射、视界与平移、网格结构与归属 | -| `Src/PlotViewPaint.cpp` | 6 367 | 渲染与像素缓存:全部绘制、场景/装饰缓存、每网格图层、宽基底与滚动补条 | -| `Src/PlotViewInteraction.cpp` | 2 715 | 交互与编辑:命中测试、鼠标/键盘、光标形状、内联输入框、浮动面板层序 | -| `Src/PlotViewFeatures.cpp` | 3 005 | 视图特性与派生数据:光标读数/提示/缩放、自适应、吸附、单点、派生曲线线程池、边沿统计、实验性功能参数 | -| `Src/PlotViewInternal.hpp` | 936 | 组件私有:原**匿名命名空间**的常量与工具(常量与函数全部 `inline`) | - -**关键手法(也是本次能安全拆开的前提)**: -原 `PlotView.cpp` 第 13–889 行是一个约 870 行的**匿名命名空间**(常量 + 工具函数),被所有成员函数广泛引用。 -若直接拆文件,各 .cpp 都拿不到这些工具。做法是: - -1. 把该块搬进 `PlotViewInternal.hpp`,从**匿名**命名空间改为**具名** `namespace pvdetail`; -2. 块内**每个函数与常量都标 `inline`**(含 `[[maybe_unused]]` 属性要先于 `inline`)⇒ - 多个翻译单元共用**同一个定义**:既不重复符号,也不产生多份机器码; -3. 头部末尾 `using namespace pvdetail;` ⇒ 各 .cpp 函数体里原有的非限定调用(`IntersectRect` / `NiceStep` …) - **一行都不用改**; -4. 成员定义按“方法名 → 分片”归类后整块搬移,即**纯代码搬移**,不改任何一行函数体。 - -**验证**:`make release` / `make debug` 零警告;四套探针全绿(`verify` 49/49 含逐像素断言)。 -**代价(诚实记录)**:产物 **754 176 → 764 416 B(+10 240 B,+1.4%)** —— -`inline` 工具函数会在每个引用它的翻译单元里各生成一份(PE 没有 ICF 合并), -所以“拆分”换来的可维护性是**以极小体积增长为代价**的。这再次印证: -**拆分解决的是可维护性,不是体积**(体积那 −24.6% 全部来自图标)。 - -### 7.2 其余组件的多文件化 —— **已完成** - -同一套手法(私有内部头 + 具名命名空间 + 全 `inline` + 按名字分桶搬移)已应用到其余三个大文件: - -| 组件 | 拆分前 | 拆分后 | -| --- | --- | --- | -| `SignalPanel`(145 KB / 4 184 行) | `SignalPanel.cpp` | `SignalPanel.cpp`(核心 1 141 行)+ `SignalPanelPaint.cpp`(697)+ `SignalPanelInteraction.cpp`(2 095)+ `SignalPanelInternal.hpp` | -| `Application`(104 KB / 3 135 行) | `Application.cpp` | `Application.cpp`(核心 1 427 行)+ `ApplicationCommands.cpp`(1 184)+ `ApplicationLoading.cpp`(270)+ `ApplicationInternal.hpp` | -| `DataModel`(88 KB / 2 615 行) | `DataModel.cpp` | `DataModel.cpp`(核心 1 583 行,含自由函数区)+ `DataModelTab.cpp`(732,全部 `TabConfig`)+ `DataModelInternal.hpp` | - -⚠️ 拆分过程中对两个**易错点**做了专门处理(均已固化到 `.preflight/perf/split_component.ps1`): - -1. **首个成员定义之前的行不能丢**:`DataModel.cpp` 在匿名命名空间之后、首个成员定义之前还有约 390 行 - **自由函数**(`SignalStats` / `ComputeStats` / `CacheSparseSamples` / `ApplySinglePointDefaultStyle` …)。 - 分桶时把“第一个片段”的起点设为 0,这些行就会跟着进核心文件。 -2. **包含的依赖不能漏**:内部头必须带上原文件真正用到的头(如 `DataModel` 需要 `Theme.hpp`、``、 - ``、``、``、``、``)。 - -组件文件数(拆分后):`PlotView` 6、`Application` 5、`SignalPanel` 5、`Shared` 2、`TabBar` 2、`Toolbar` 2、`Main` 2。 - -### 7.3 `Lane` 的内聚性 —— **已完成(本轮补齐,见 §九)** - -`Lane` 内含 4 类实验性功能的状态 + 各自的面板坐标(12 个平铺字段);已抽出 -`LanePanelState { bool open; double x; double y; }`,`Lane` 只保留「开关 + 每信号参数 + 面板状态」 -四项成员(`edgePanel` / `edgeCenterPanel` / `meanFilterPanel` / `weakPeakPanel`)。 - -### 7.4 错误提示与模态阻塞解耦 —— **已完成(本轮补齐,见 §九)** - -`SetStatusMessage(..., isError=true)` 不再是「无条件 `MessageBoxW`」:模态与否由宿主决定 -(`Application::SetModalErrorsEnabled`),隐藏窗口启动时**自动**降级为绘图区非模态提示。 - -### 7.5 MF4 压缩块 / 数据列表(上一轮建议 2–3) - -按用户要求**继续不做**。 - ---- - -## 八、变更清单(本轮) - -``` -新增 Components/Shared/Inc/UiControls.hpp 通用 UI 控件(自 PaintUtil 迁出) -新增 Components/Shared/Src/UiControls.cpp -新增 Components/PlotView/Src/PlotViewInternal.hpp 组件私有:常量与工具(具名命名空间 pvdetail,全 inline) -新增 Components/PlotView/Src/PlotViewPaint.cpp 渲染与像素缓存 -新增 Components/PlotView/Src/PlotViewInteraction.cpp 交互、命中与编辑 -新增 Components/PlotView/Src/PlotViewFeatures.cpp 视图特性与派生数据 -新增 Components/SignalPanel/Src/SignalPanelInternal.hpp -新增 Components/SignalPanel/Src/SignalPanelPaint.cpp -新增 Components/SignalPanel/Src/SignalPanelInteraction.cpp -新增 Components/Application/Src/ApplicationInternal.hpp -新增 Components/Application/Src/ApplicationCommands.cpp -新增 Components/Application/Src/ApplicationLoading.cpp -新增 Utilities/Src/DataModelInternal.hpp -新增 Utilities/Src/DataModelTab.cpp -修改 Utilities/Inc/PaintUtil.hpp 移除 16 个控件声明 -修改 Utilities/Src/PaintUtil.cpp 移除控件实现(953 → 571 行) -修改 Utilities/Inc/DataModel.hpp SignalData/SignalGroup 单一名称字段;注释/命名修正 -修改 Utilities/Src/DataModel.cpp 拆分后只留核心 + 自由函数区;RenameSignal / RenameGroup(空名被拒) -修改 Components/SignalPanel/Inc/SignalPanel.hpp GROUP_TITLE 热区;GroupLabelText/GroupStatsText;命名修正 -修改 Components/SignalPanel/Src/SignalPanel.cpp 拆分后只留核心;折起热区、改名输入框边界、过时注释/命名 -修改 Components/PlotView/Inc/PlotView.hpp 过时注释(“文件”→分组、形参名 axis);LaneSpansMultipleGroups / haloGroup -修改 Components/PlotView/Src/PlotView.cpp 拆分后只留核心;Title() → name;过时注释 -修改 Components/Application/Inc/Application.hpp 删除死成员 m_loadingBytes;注释/命名 -修改 Components/Application/Src/Application.cpp 拆分后只留核心;过时注释/命名 -修改 Components/TabBar/Src/TabBar.cpp 引入 UiControls.hpp -修改 Components/Toolbar/Inc/Toolbar.hpp 注释修正 -修改 Utilities/Src/PopupMenu.cpp | ColorPickerPopup.cpp 引入 UiControls.hpp -修改 Utilities/Inc/CsvLoader.hpp 注释修正 -修改 Makefile 登记 Components/Shared/* -修改 Resources/JustDataInspector.ico 256×256 条目改 PNG 存储(302 KB → 43 KB) -``` - -* `Documents/agent_mem.md` 同步更新(§9.37 + §11.2 约束 + 变更日志)。 -* 上一轮报告 `Documents/refactor_report_2026-10-15.md` **已删除**(其有效内容已并入 - `agent_mem.md` 与本文)。 - ---- - -## 九、收尾补记(同一轮次的第二遍:把「仍未完成」清空) - -上一版报告在 §7.3 / §7.4 留下两项「本轮未做」。这一遍把它们做完,并在过程中又发现并修掉 -一个**真实缺陷**。所有改动都是「结构内聚 + 反馈通道」类,**不改任何功能性行为**。 - -### 9.1 `LanePanelState`:`Lane` 的 12 个平铺面板字段 → 4 个聚合成员 - -```cpp -// Utilities/Inc/DataModel.hpp -struct LanePanelState -{ - bool open = true; /**< 浮动面板是否展开。 */ - double x = -1.0; /**< 面板左上角 X(-1 表示尚未定位,首次绘制时自动摆放)。 */ - double y = -1.0; /**< 面板左上角 Y。 */ -}; -``` - -* `Lane` 的 `edgePanelOpen/edgePanelX/edgePanelY` 等 **12 个字段**被 - `LanePanelState edgePanel / edgeCenterPanel / meanFilterPanel / weakPeakPanel` 取代; - 访问点全部改为 `lane.edgePanel.open` 这样的**结构内聚**形式(`DataModelTab.cpp` 的快照拷贝、 - `PlotViewInteraction.cpp` 的浮动面板拖拽、`PlotViewPaint.cpp` 的面板层序绘制)。 -* `RebuildLanes` 内部的「网格自身状态携带」结构 `LaneCarry` 同步收敛:原来 12 个平铺字段 - 改为 4 个 `LanePanelState`,逐字段赋值也简化为整体赋值(少 8 行、少 8 个易漏字段)。 -* **收益**:参数的归属一眼可辨(哪个坐标属于哪个功能)、新增一类实验性功能时只需加 1 个成员; - 同时把「3 个字段必须一起改」的隐式约束变成类型约束。 -* ⚠️ 教训(已写进 `agent_mem.md`):批量改名脚本**不能**在「声明」与「使用」上跑同一组替换对 —— - 对 `Lane` 使用的 `edgePanelOpen → edgePanel.open` 用在 `LaneCarry` 的**声明行**上会得到 - `bool edgePanel.open = true;` 这种非法声明。修正方式是**声明处单独重写**(4 个聚合成员), - 只让「使用处」走替换对。 - -### 9.2 错误反馈:模态对话框 → 可关闭(隐藏窗口自动降级为非模态提示) - -* `Application` 新增 `SetModalErrorsEnabled(bool)` / `ModalErrorsEnabled()`, - 成员 `m_modalErrorsEnabled` **默认 `true`**(保持交互场景的既有行为); - `Initialize(instance, showCommand)` 在**最先**执行 `SetModalErrorsEnabled(showCommand != SW_HIDE)`。 - 理由:隐藏窗口(自动化/离屏)下弹模态框**没有人能点掉**,等于把进程钉死。 -* `SetStatusMessage(text, /*isError*/true)` 分支化: - 允许模态 → 原样 `MessageBoxW`;不允许 → `m_plot.ShowPersistentHint(text)`(绘图区底部浮层)。 - 普通提示(`isError == false`)与空文本依旧被忽略 —— **交互体验与此前完全一致**。 -* `PlotView` 新增公开接口 `ShowPersistentHint(text)`(内部转 `SetHintSticky`), - 把「私有提示机制」变成「宿主可用的反馈通道」,且不暴露粘滞/淡出等内部细节。 -* **顺带修掉一个真实缺陷**:`PlotView::Paint` 在 `m_lanes.empty()` 时**提前 return**, - 而提示浮层在 return 之后的叠加层里绘制 ⇒ **「一个信号都没勾选」时任何错误都显示不出来**, - 恰恰是「文件加载失败」这一最需要提示的场景。修法是在该分支补一次 `PaintHint(g)`。 - 这一条是**探针先失败、才回头发现的**(见 9.3),说明「非模态通道」本身也需要像素级验证。 - -### 9.3 新增离屏探针 `.preflight/perf/errcheck`(19/19) - -模态/非模态无法用「截图看着对」来判断,因此写成可自动判定的断言: - -| 断言组 | 内容 | -| --- | --- | -| 默认与降级 | 默认 `ModalErrorsEnabled() == true`;`Initialize(..., SW_HIDE)` 后为 `false`;宿主可显式开关 | -| 不被打断 | 从**工作线程**调用 `SetStatusMessage(err,true)`,5 s 内必须返回(旧行为会永久阻塞 ⇒ 直接判失败并 `ExitProcess`,探针**不会挂住**) | -| 不产生对话框 | 用 `EnumWindows` 统计**本进程**中类名 `#32770` 的顶层窗口,调用前后数量不变 | -| 走到提示通道 | `m_hintText` 等于错误文本、`m_hintSticky == true`、`HintNeedsAnimation() == true`、`m_hintShownAt != 0` | -| **真的画出来** | 「有提示」帧与「无提示」帧在绘图区矩形内的差异像素 **3781**,且全帧差异 3781(说明差异**只**来自提示) | -| 提示不吞消息 | 成功提示 / 空文本错误不进入提示通道;第二条错误顶掉第一条 | - -### 9.4 本轮验证(全部离屏,`SW_HIDE` + 无输入注入 + `BELOW_NORMAL`) - -| 项目 | 结果 | -| --- | --- | -| `make release` / `make debug` | 通过,**零警告** | -| 产物体积 | `Build/Release/Target.exe` **764 416 B**(与拆分后一致;本轮是纯结构改动,未引入新代码膨胀) | -| `.preflight/perf/verify`(界面与像素) | **49/49** | -| `.preflight/perf/datacheck`(数据层) | **69/69** | -| `.preflight/perf/edgecheck`(边沿统计) | **46/46** | -| `.preflight/perf/panelanim`(分组折起动画 + 两处 UI 修复) | **156/156** | -| `.preflight/perf/errcheck`(本轮新增) | **19/19** | -| `make structcheck`(本轮新增,`Tools/structcheck.ps1`) | **93/93**(结构约束 + 本轮行为/注释不变量,见 §9.6 / §9.7) | -| 上一遍的 A/B(回退两处 UI 修复) | 如实失败 **9** 项(失败文案逐条点名「点组名不应折起」「改名框不应包住条目计数」),恢复后 156/156 | - -### 9.5 本轮变更清单 - -``` -修改 Utilities/Inc/DataModel.hpp 新增 LanePanelState;Lane 的 12 个平铺面板字段 → 4 个聚合成员 -修改 Utilities/Src/DataModelInternal.hpp LaneCarry 同步收敛为 4 个 LanePanelState(含整体赋值简化) -修改 Utilities/Src/DataModelTab.cpp 面板状态快照拷贝改用 .open/.x/.y -修改 Components/PlotView/Src/PlotViewInteraction.cpp 浮动面板拖拽改用 .open/.x/.y -修改 Components/PlotView/Src/PlotViewPaint.cpp 面板层序绘制同改;空状态分支补 PaintHint(缺陷修复) -修改 Components/PlotView/Inc/PlotView.hpp 新增公开 ShowPersistentHint -修改 Components/PlotView/Src/PlotViewFeatures.cpp 实现 ShowPersistentHint -修改 Components/Application/Inc/Application.hpp SetModalErrorsEnabled / ModalErrorsEnabled / m_modalErrorsEnabled -修改 Components/Application/Src/Application.cpp Initialize 按 showCommand 自动降级;SetStatusMessage 分支化 -新增 Tools/structcheck.ps1 结构检查(拆分 + 共享控件 + 分层 + 本轮行为/注释不变量),93 条断言 -新增 Tools/structcheck_phrases.txt 注释核验用中文短语表(UTF-8,供 .ps1 读取) -修改 Makefile 新增 structcheck 目标 -新增 .preflight/perf/errcheck_main.cpp 模态/非模态解耦探针(gitignored,不随产物提交) -``` - -### 9.6 结构证据:把「拆分」与「共享控件」变成可复跑的检查(`make structcheck`) - -上一版报告对 §7.1 / §7.2 / §7.3 的完成情况只有散文式描述。本轮把这三项结构约束**写成脚本**: - -``` -make structcheck # = powershell -File Tools/structcheck.ps1 -``` - -`Tools/structcheck.ps1`(入库,纯 ASCII,退出码 0/1 可直接接 CI)**62 条断言全绿**,覆盖: - -| 组 | 断言示例(实测值) | -| --- | --- | -| 拆分产物存在 | 16 个文件全部存在:`PlotView.cpp` **705** 行(原 13 695)、`PlotViewPaint.cpp` **5 950**、`PlotViewInteraction.cpp` **2 537**、`PlotViewFeatures.cpp` **2 821**、`PlotViewInternal.hpp` **884**;`SignalPanel.cpp` **1 019**(原 4 184)/`Paint` **614**/`Interaction` **1 925**/`Internal` **288**;`Application.cpp` **1 326**(原 3 135)/`Commands` **1 071**/`Loading` **237**/`Internal` **286**;`DataModel.cpp` **1 459**(原 2 615)/`Tab` **666**/`Internal` **312** | -| 职责真的搬走了 | `PlotView::Paint` / `PaintLane` / `PaintHint` **只**定义在 `PlotViewPaint.cpp`;`LaneAtY` / `CancelDrag` **只**在 `PlotViewInteraction.cpp`;`FitAll` / `SetHint` **只**在 `PlotViewFeatures.cpp`;`Layout` / `BuildLaneVisuals` 留在 `PlotView.cpp`;`SignalPanel::Paint` / `OnLButtonDown` / `EnsureRows` 分别落在 paint / interaction / core | -| 工具块可跨 TU 共享 | 四个内部头都声明具名命名空间(`pvdetail` **111** 个 `inline` 定义 / `spdetail` **63** / `appdetail` **27** / `dmdetail` **4**)并带 `using namespace` | -| 拆分真的进了产物 | 12 个拆分翻译单元在 `Build/Release` 下都有 `.o`;`nm` 断言**绘制符号在 `PlotViewPaint.o`、`PlotView.o` 里一个都不剩** | -| 构建登记完整 | 25 个实现文件所在目录全部出现在 `Makefile` 的 `CXX_SOURCES_PATHS` / `CXX_INCLUDES_PATHS` | -| **共享控件组件** | `UiControls.hpp` 声明 **16** 个控件;`nm` 断言这 16 个**全部由 `UiControls.o` 定义**(其他任何 `.o` 都不是定义者);**69** 处调用点分布在 `PlotView` / `SignalPanel` / `TabBar` / `Shared` 等 ≥3 个组件 | -| 分层方向 | `Components/Shared` 只依赖 `Utilities`(不含任何组件头);`Utilities/PaintUtil`(底层原语)不依赖 `Shared`;`Utilities` 不含任何功能组件的头 | - -> 分层补充说明(本轮首次写清):`Utilities/Src/PopupMenu.cpp` 与 `ColorPickerPopup.cpp` 是历史上放在 -> `Utilities/` 下的自绘 UI 小部件,它们与各组件一样**消费** `Shared` 控件(方向同组件)。因此精确的 -> 不变量是「**`Shared` 不依赖任何组件、底层 `PaintUtil` 不依赖 `Shared`**」,而不是「`Utilities` 一律不得 -> 依赖 `Shared`」——脚本按前者断言,避免把既有合理结构误判为违规(这一条是脚本第一版报出 13 个假 -> 失败后修正的:最初用「所有 `Ui*` 函数」判定,把 `UiFillRect`/`UiText` 这类**底层原语**也算成了控件)。 - -### 9.7 三处功能要求也变成可复跑的检查(`structcheck` 第 [3] 组,总计 93 条) - -本轮另外三处用户要求(不折起热区、改名框不吞项数、`SignalData` 去冗余 + 注释核验)此前只有 -散文描述。现在它们同时有**源码级断言**与 **A/B 反证**: - -**(a) 源码级断言(`Tools/structcheck.ps1` 第 [3] 组,31 条)** - -| 断言 | 实测 | -| --- | --- | -| `RowHitPart` 产出专用热区 `RowPart::GROUP_TITLE`,且其矩形**正好**是「组N」标签矩形 | OK | -| 单击折起门限只接受 `DISCLOSURE` / `GROUP_TITLE`(全文件**恰好 1 处**) | OK | -| `ToggleGroupFromRow` 只有 **2 个入口**(单击门限 + 双击折叠三角) | OK | -| 双击**三角**仍折起;双击**非名称本体**一律 `return false`(不进入改名) | OK | -| 改名框右边界 = 项数左缘(`statsRight - statsWidth - 6*scale`),且**不存在** `right = statsRight;` | OK | -| `SignalData` / `SignalGroup` **各只声明 1 个字符串字段且名为 `name`** | 各 1 个 | -| `RenameSignal` / `RenameGroup` 存在;全仓 `displayName` / `Title()` / `Set*DisplayName` 残留 **0 处** | 0 | -| 过时措辞(`文件下标` / `所属文件下标` / `原信号所属文件下标` / `文件分组` / `原文件下标` / `文件序号`)**全仓 0 处** | 0 | -| 已修正注释**确实存在**,例如 `DerivedSignalRequest::groupIndex` → 「原信号所属分组下标」 | OK | - -> ⚠️ 中文短语不能写在 `.ps1` 里(PowerShell 5.1 按 ANSI 读脚本),因此它们放在 UTF-8 数据文件 -> `Tools/structcheck_phrases.txt` 中,由脚本以 `UTF8Encoding($false)` 读入后逐条断言(`forbid` / `require`)。 - -**(b) A/B 反证(判别力,两处 UI 修复)** - -把两处修复回退(`if (part != DISCLOSURE && part != GROUP_TITLE)` → `if (false)`; -`right = statsRight - statsWidth - 6*scale;` → `right = statsRight;`)后: - -| 验证手段 | 回退后 | 恢复后 | -| --- | --- | --- | -| `panelanim.exe`(真实 `Application` + 消息注入 + 逐像素断言) | **147/156,9 项如实失败** | **156/156** | -| `make structcheck` | **90/93,3 项如实失败** | **93/93** | - -回退后 `panelanim` 报出的前两条失败文案**直接点名需求**: -`FAIL: the group rename box stops before the item counter`、 -`FAIL: clicking the group name does not start collapsing` / `does not collapse the group`; -`structcheck` 报出的三条:`single-click toggle gate accepts only DISCLOSURE / GROUP_TITLE (found 0)`、 -`rename box right edge excludes the item counter`、`rename box never extends over the counter`。 - -**(c) 运行时行为断言(数据层,`datacheck` 69/69)** - -``` -Check(set.RenameSignal(groupIndex, 0, L"改名") && set.Resolve(SignalRef{0, 0})->name == L"改名", - "signal rename writes the single name field"); -Check(set.RenameGroup(groupIndex, L"我的组") && set.GroupAt(0)->name == L"我的组", - "group rename writes the single name field"); -Check(!set.RenameSignal(groupIndex, 0, L"") && set.Resolve(SignalRef{0, 0})->name == L"改名", - "empty rename is rejected (name is never blanked)"); -``` - -即:重命名**只写一个 `name` 字段**(编译期已无 `displayName` 可写),空名被拒。