分组折起热区与改名输入框修正 + 数据模型去冗余(单一名称)+ 新增 Components/Shared 组件 + 过时注释/命名大清理 + 二进制瘦身 25.6%

## 一、UI 交互修复(用户报告)

① 分组折起热区(用户要求):点分组**名称**不再折起,只有**折叠三角**或**「组X」标签**
   才折起。RowPart 新增 GROUP_TITLE;RowHitPart() 为分组行判出三角 / 复选框 / 关闭 ✕ /
   「组X」标签矩形 / 其余 BODY;OnLButtonDown 只在这两个热区调用 ToggleGroupFromRow()。
   附带:双击「组X」不再进入改名(该区域现为折起热区),双击分组名仍可改名。
② 分组改名输入框不再吞掉「N 项」:RowNameRect() 对分组行的右边界改为「项数左边缘 − 6*scale」;
   新增 GroupLabelText()/GroupStatsText() 作为文本与宽度的唯一来源(PaintGroupRow 与
   RowNameRect 共用),避免名称可用宽度与实际绘制文本漂移。

## 二、数据模型去冗余(用户指出的重点)

- SignalData / SignalGroup **只保留一个名称字段** name:删除 displayName、删除 Title()
  (31 处调用点改为直接用 name);DataSet::SetSignalDisplayName / SetGroupDisplayName
  → RenameSignal / RenameGroup,且**空串 / 纯空白一律拒绝**(名称不再有回退值)。
  依据:信号靠 SignalRef 下标定位、从不按名字匹配 ⇒ 两个字段是同一语义的两份副本。
  收益:每条信号 / 每个分组少一个 std::wstring(24B + 堆分配)与一条 Title() 分支。
- 删除 Application::m_loadingBytes(全仓既不读也不写的死成员,注释却称“用于展示体积”)。

## 三、新增 Components/Shared 组件(通用 UI 控件)

- 从 Utilities/PaintUtil.* 迁出全部「控件类」绘制/命中:图标(+ / ✕ / 三角)、复选框、
  滑块(UiSliderRatioFromX / UiDrawSlider)、内联输入框(UiDrawEditField /
  UiDrawEditFieldContent / UiEditTextLeft / UiEditOffsetAt / UiEditFieldMouseDown /
  UiEditFieldMouseMove)、颜色样块(UiDrawSwatch / UiSwatchNeedsOutline)、
  取值胶囊配色(UiInk / UiColorLuminance / UiContrastInk)。
- PaintUtil.* 只保留低层绘制原语(矩形 / 圆角 / 直线 / 批量实线 / 颜色混合 / 文本与测量)。
- 依赖方向固定:Components/* → Components/Shared → Utilities(不得反向,Utilities 不再含控件)。
- Makefile 与 .preflight/perf/build.ps1 已登记新目录;调用方改 include "UiControls.hpp"。

## 四、过时注释 / 命名批量修正(承接上一轮未做完的部分)

- 注释:文件下标→分组下标、文件内信号下标→组内信号下标、所属文件下标→所属分组下标;
  措辞统一 文件分组→分组、移除该文件→移除该分组、打开 CSV 文件→打开数据文件 等一批。
- 文不对题的形参名: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 两处被压扁的注释块(/**<TAB><TAB>* @brief …)与 Application::NewTab
  上方重复的文档块。
- ⚠️ 踩坑:GROUP_NAME 撞上 nb30.h(windows.h 引入)的 #define GROUP_NAME 0x80,
  导致枚举编译失败,改名 GROUP_TITLE 解决。

## 五、二进制瘦身:1 013 248 → 754 176 B(−25.6%,省 259 KB)

- 真因不是 PlotView.cpp,而是 Resources/JustDataInspector.ico 里 **256×256@32bpp 以未压缩
  BMP 存储的条目(270 376 B,占整包 27%)**。改为 PNG 存储(Vista+ 标准做法,观感零变化)
  ⇒ 该条目 270 376 → 11 280 B、整个 ico 302 430 → 43 334 B(小尺寸仍保留 BMP)。
- 实测结论:“拆分大文件 ⇒ 瘦身”不成立(同一批机器码仍要进二进制);
  -ffunction-sections/-fdata-sections/-Wl,--gc-sections 在本工程反而 +9 216 B(已回退)。
- ⚠️ .o 体积受节对齐/重定位表主导,判断“代码搬去哪了”要用 nm,不要看 .o 字节数。

## 六、验证(全部离屏:SW_HIDE + 不注入真实输入 + 进程/线程 BELOW_NORMAL)

- panelanim 156/156(连跑 5 次稳定;新增/更新:点分组名不折起也不滚动、扫掠定位「组X」热区、
  筛选态与普通态折叠三角逐像素一致、改名输入框右边界早于项数文本);verify 49/49;
  datacheck 69/69(含“重命名写入唯一名称字段且空名被拒”);edgecheck 46/46。
- A/B:回退两处 UI 修复 ⇒ 10 项如实失败;恢复后全绿。
- make release / make debug 零警告。

## 七、本轮未完成(已在报告与 agent_mem.md 中如实记录)

- PlotView.cpp(13 695 行 / 544 KB)按功能拆分为多个协作文件:未执行。
  前置改造是把第 13–888 行的匿名命名空间(~870 行常量与工具函数)改成具名命名空间并逐个
  inline,再逐个文件搬移并反复验证;时间不足以安全完成,故主动不做(附可执行方案)。
- SignalPanel.cpp / Application.cpp / DataModel.cpp 的多文件化、Lane 抽出 LanePanelState、
  SetStatusMessage 模态阻塞解耦:均未执行。

## 八、文档

- 新增 Documents/refactor_report_2026-10-16.md(本轮详细报告);
  删除 Documents/refactor_report_2026-10-15.md(有效内容已并入 agent_mem.md 与本报告)。
- Documents/agent_mem.md 新增 §9.37 与 §11.2 第 85–90 条;更新数据层架构摘要(单一名称字段);
  §12 历史附录加“时效声明”。
This commit is contained in:
DeepSeek V4.1 Flash committed 2026-10-16 03:25:48 +08:00
1 parent 5831a0ea0d
commit 0871eb0cba
22 files changed
+1268 -1010

No files matched your search

+130 -6
View File
@@ -1367,12 +1367,12 @@ GDI+ 抗锯齿逐段扫描这些**近竖直、满幅**的边,代价爆炸(
* **新增 `TimeAxis`**:`{ name, time, minTime, maxTime, monotonic, finiteCount }`。
* **`SignalData`**:删除 `rawHeader`(`name` 的副本)与 `unit`(由列名猜出、全仓无人使用);
新增 **`axisIndex`** 指向所属时间轴 ⇒ 采样值与时间的对应关系**自包含**,不再需要「文件」。
名字只剩两个且职责明确:`name`(源名称,永不改写)/ `displayName`(用户重命名)。
* **`SignalGroup`(原 `DataFile`)**:`{ name(默认=源文件名), displayName, axes[], signals[] }`;
⚠️ 名称字段**只有一个 `name`**(2026-10-16 起去掉 `displayName` 与 `Title()`,见 §9.37 / 第 87 条)。
* **`SignalGroup`(原 `DataFile`)**:`{ name(默认=源文件名), axes[], signals[] }`;
**删除** `filePath` / `fileName` / `time` / `timeColumnName` / `timeMonotonic` / `minTime` / `maxTime`。
新增 `AxisOf(signal)` / `AxisAt(i)` / `TimeRange(low, high)`(多轴并集)。
* **`SignalRef::fileIndex` → `groupIndex`**;`DataSet`:`AddGroup` / `RemoveGroup` / `GroupAt` / `GroupCount` /
`Groups()` / `SetGroupDisplayName` / `RemapSelectionAfterGroupRemoval` + 新增 **`Axis(ref)`**。
`Groups()` / **`RenameSignal` / `RenameGroup`** / `RemapSelectionAfterGroupRemoval` + 新增 **`Axis(ref)`**。
* `TabConfig`:`SetGroupVisible` / `GroupHasVisible` / `GroupAllVisible` / `RemapAfterGroupRemoval`;
`RowKind::GROUP`、`CMD_GROUP_*`。
* **采样/统计 API 首参由 `const DataFile&` 改为 `const TimeAxis&`**:
@@ -2091,7 +2091,7 @@ GDI+ 抗锯齿逐段扫描这些**近竖直、满幅**的边,代价爆炸(
#### 三、筛选态折叠三角状态修正
- **用户报告**:筛选时分组行的小三角维持在了筛选前的状态,展开/折起都不变化。
- **根因**:`PaintFileRow` 直接读 `m_tab->IsCollapsed(...)`(普通模式的折叠状态),
- **根因**:`PaintGroupRow` 直接读 `m_tab->IsCollapsed(...)`(普通模式的折叠状态),
而筛选模式用的是**独立**的 `m_filterCollapsed`(§9.35 二)⇒ 筛选里折叠时三角形不变。
- **修法**:改为统一的 `IsGroupCollapsed(groupIndex)`(按当前模式分派)。
- **验证**:离屏像素对比 —— 「筛选态折叠」与「普通态折叠」的组名行(含三角)**逐像素 0 差异**;
@@ -2131,6 +2131,95 @@ GDI+ 抗锯齿逐段扫描这些**近竖直、满幅**的边,代价爆炸(
---
### 9.37 分组折起热区与改名输入框修正 + 数据模型去冗余(单一名称)+ `Components/Shared` 组件 + 二进制瘦身(本轮,本机)
> 本轮(用户 2026-10-16 第三轮,本机 = 另一台设备)。报告:
> `Documents/refactor_report_2026-10-16.md`(已删除上一轮的 `refactor_report_2026-10-15.md`)。
> 离屏验证:`panelanim` **156/156**、`verify` **49/49**、`datacheck` **69/69**、`edgecheck` **46/46**;
> A/B **10 项如实失败**;`make release` / `make debug` 零警告。
#### 一、分组折起热区:只有折叠三角与「组X」标签可折起
- **需求**:点分组**名称**不折起;只有折叠三角或「组X」标签才折起。
- **实现**:`RowPart` 新增 **`GROUP_TITLE`**;`RowHitPart()` 为分组行判
三角 → 复选框 → 关闭 ✕ → **「组X」标签矩形(`checkRect.Right + 8*scale` 起,宽 `GroupLabelWidth`)**
→ 否则 `BODY`;`OnLButtonDown` 仅在 `DISCLOSURE` / `GROUP_TITLE` 时调用 `ToggleGroupFromRow()`。
- 附带:双击「组X」不再进入改名(该区域现为折起热区);双击分组名仍改名。
#### 二、分组改名输入框不再吞掉「N 项」
- **根因**:`RowNameRect()`(分组行)右边界原先到关闭 ✕ 左侧,把右侧项数文本也框了进去。
- **实现**:右边界改为 `项数左边缘 − 6*scale`;新增 **`GroupLabelText(groupIndex)`** 与
**`GroupStatsText(row)`** 作为“绘制 / 命中 / 宽度”的唯一文本来源(`PaintGroupRow` 与
`RowNameRect` 共用),避免名称宽度与实际绘制文本漂移。
#### 三、`SignalData` / `SignalGroup` 只保留**一个**名称字段
- **依据**(用户):信号靠 `SignalRef` 下标定位、从不按名字匹配 ⇒ `name` + `displayName`
是同一语义的两份副本。
- **改动**:删除 `displayName`;`name` 成为唯一名称字段(重命名即写入它);
**删除 `Title()`**(31 处调用点改为 `.name` / `->name`);
`DataSet::SetSignalDisplayName` → **`RenameSignal`**、`SetGroupDisplayName` → **`RenameGroup`**。
- ⚠️ **语义收紧**:`RenameSignal/RenameGroup` 对**空串 / 纯空白一律拒绝**(不再表示“恢复源名称”)
—— 名称不再有回退值。
- 另删除 `Application::m_loadingBytes`(**全仓既不读也不写**的死成员)。
#### 四、新增 `Components/Shared` 组件(通用 UI 控件)
```
Components/Shared/Inc/UiControls.hpp
Components/Shared/Src/UiControls.cpp
```
- 从 `Utilities/PaintUtil.*` **迁出**全部“控件类”绘制/命中:图标(+ / ✕ / 三角)、复选框、
滑块(`UiSliderRatioFromX` / `UiDrawSlider`)、内联输入框(`UiDrawEditField` /
`UiDrawEditFieldContent` / `UiEditTextLeft` / `UiEditOffsetAt` / `UiEditFieldMouseDown` /
`UiEditFieldMouseMove`)、颜色样块(`UiDrawSwatch` / `UiSwatchNeedsOutline`)、
取值胶囊配色(`UiInk` / `UiColorLuminance` / `UiContrastInk`)。
- `PaintUtil.*` 只保留**低层绘制原语**(矩形 / 圆角 / 直线 / 批量实线 / 颜色混合 / 文本与测量)。
- ⚠️ **依赖方向固定**:`Components/* → Components/Shared → Utilities`;
`Utilities` **不得**再包含任何控件代码(也不要 include `UiControls.hpp`)。
- ⚠️ 新增组件目录必须同时登记进 `Makefile` 的 `CXX_INCLUDES_PATHS` / `CXX_SOURCES_PATHS`
以及 `.preflight/perf/build.ps1` 的 `$includes` / `$srcDirs`。
#### 五、二进制瘦身:1 013 248 → **754 176 B(−25.6%)**
- **真因**是 **`Resources/JustDataInspector.ico` 的 256×256@32bpp 条目以未压缩 BMP 存储
(270 376 B,占整包 27%)**,而不是 `PlotView.cpp`。改为 **PNG 存储**(Vista+ 标准做法,
观感零变化)⇒ 该条目 270 376 → **11 280 B**、整个 ico 302 430 → **43 334 B**。
- ⚠️ **拆分大文件不会减小机器码**(“拆分 ⇒ 瘦身”不成立);实测
`-ffunction-sections -fdata-sections -Wl,--gc-sections` 在本工程**反而 +9 216 B**(已回退)。
- ⚠️ `.o` 体积受节对齐/重定位表主导,**判断“代码搬去哪了”要看 `nm`,不要看 `.o` 字节数**。
#### 六、过时注释 / 命名批量修正(承接上一轮未做完的部分)
- 参数与成员注释:`文件下标`→`分组下标`、`文件内信号下标`→`组内信号下标`、`所属文件下标`→`所属分组下标`
等一批;措辞统一 `文件分组`→`分组`、`移除该文件`→`移除该分组`、`打开 CSV 文件`→`打开数据文件` …;
- **文不对题的形参名**:`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`
上方重复的文档块;
- ⚠️⚠️ **新增标识符前必须确认不是 Windows 头里的宏**:本轮 `GROUP_NAME` 撞上
`nb30.h`(由 `windows.h` 引入)的 `#define GROUP_NAME 0x80`,导致枚举编译失败,改名
`GROUP_TITLE` 解决。同类高风险名:`GROUP_NAME`、`TIME`、`SMALL`、`NEAR`、`FAR`、`IGNORE`。
#### 七、本轮**未**完成(诚实记录,勿误以为已完成)
1. **`PlotView.cpp`(13 695 行 / 544 KB)按功能拆分**:未执行。前置改造是把第 13–888 行的
匿名命名空间(~870 行常量与工具函数)改成具名命名空间并**逐个 `inline`**,
再做 5 个协作文件的搬移;必须“搬一类 → 编译 → 跑四套探针”反复迭代,本轮时间不足以安全完成。
⚠️ 切勿草率动手 —— 它会直接影响 `Paint()` 分支顺序与图层/装饰缓存不变量(见 §11.2)。
2. `SignalPanel.cpp`(145 KB)/ `Application.cpp`(104 KB)/ `DataModel.cpp`(88 KB)的多文件化:
未执行。三者中 **`SignalPanel.cpp` 的辅助区只有 ~60 行,是性价比最高的下一个拆分入口**。
3. `Lane` 抽出 `LanePanelState`(内聚性重构):未执行。
4. `SetStatusMessage(..., isError)` 的模态阻塞解耦:未执行(需要用户拍板提示样式)。
---
## 10. 变更日志(本文件维护记录)
| 日期 | 设备/模型 | 摘要 |
@@ -2180,6 +2269,7 @@ GDI+ 抗锯齿逐段扫描这些**近竖直、满幅**的边,代价爆炸(
| 2026-10-15 | DeepSeek-V4.1-Flash(**起源设备**) | **信号面板滚动条跟随 / 工具栏连点丢状态 / 图层网格线步长 / 多网格单点自适应 / 单点信号拖拽(详见 §9.32)**。① **滚动条"卡住"**:`Application::WM_MOUSEMOVE` 的 `plotOnlyMove` 只看 `inPlot && m_mouseOverPlot`,拖滚动条进绘图区后判定"只影响绘图区"⇒ 面板滚动值变了却不重绘;修法:新增 `draggingNonPlot`(正在拖非绘图区组件时一律完整失效)。② **工具栏丢点击**:窗口类带 `CS_DBLCLKS` ⇒ 快速第二次按下被合成 `WM_LBUTTONDBLCLK` 而工具栏没处理(**按下丢失**);且 `HandleMouseMove` 只要不在工具栏上就 `OnMouseLeave()` ⇒ 清掉 `m_pressedIndex`(按住划出再回按钮松手无效);两处都已修。③ **网格线错位/丢失/闪烁**:`PaintLane` 的竖向主刻步长取自 `Tab().ViewMin/ViewMax`,而渲染信号图层时该区间已被改成**宽窗口**(≈2×)⇒ 图层辅助线步长 2×、位置与前台不同、判重判据也错位;修法:新增 `m_renderTimeStep`,由 `RenderLaneLayerRegion` 用**前台跨度**算好传入。④ **多网格单点「适应视图」**:新增 `FitSinglePointLanes()`(左右边界 = 最左/最右数据点 ± **一个网格小格**;逐网格 Y 自适应、各留**一个 Y 小格**;全部点同刻 ⇒ 跨度 1s 居中;单值网格 ⇒ Y 跨度 1 居中)。⑤ **单点信号**:`HitTestSignal` 新增单点分支 + 环状光晕 + 可拖拽换网格(悬停**不改光标**,按下后才改)。验证:离屏 `.preflight/check` **67/67 通过**(连跑 3 次稳定);**A/B** 回退六处修复 ⇒ **17 项如实失败**(含 [C] 辅助线 49px vs 25px、20 vs 40 条、最大偏移 539px);`make release` 零警告、链接通过。 |
| 2026-10-16 | DeepSeek-V4.1-Flash(**另一台设备**) | **分组折起/展开动画 + 筛选态可折起(项数按筛选计数)+ 悬浮组名可交互与"先滚动再折起" + 「适应视图」与「Y 轴自适应」彻底同源(详见 §9.35)**。① **折起动画**:`SignalPanel::m_groupAnims` + `GroupRowScale()` —— 每行速度倍率 `1 + 1.2×ordinal/(行数−1)`(**离组名越远越快收尽**)叠加 ease-out cubic(末段减速),总时长 `clamp(115+14×行数, 140, 300)` ms;`BuildRows` 输出**非整行高**,低于 0.75 逻辑 px 的行不再产生;宿主 `WM_TIMER` 同时驱动滚动与折起动画(`Render` 判定改为二者取或);新增 `SetRowClip/ClearRowClip` 把行内容裁到本行(否则垂直居中文本溢出到相邻行)。② **筛选态折起**:筛选模式改用**独立**的 `m_filterCollapsed`(默认展开,不污染普通列表),`ListRow::displayCount` 承载"该组内符合筛选条件的个数",筛选文本/清空/数据变化即清空。③ **悬浮组名可交互**:根因是 `HitRow` 只按滚动偏移映射行 ⇒ 点击穿透到下层;修法:`HitRow` 先用 `StickyGroupRowIndex()`+`StickyGroupRowTop()` 判定悬浮行矩形并临时用 `m_rowYOverrideActive` + 新的 `RowHitPart()` 算行内区域,并跳过高度为 0 的行;新增 `ToggleGroupFromRow()`:正悬浮时 `StartScrollAnimationTo(行内容偏移, groupIndex)`(挂起请求 `m_pendingGroupToggle`),滚动到位后才启动折起动画。④ **单点信号上两种自适应不一致**:`FitSinglePointCluster` 曾另算"半径留白 + 跨度 1"且把 `fitYToSelection` 置回 false ⇒ 点「适应视图」既改区间又**关掉 Y 轴自适应**;修法:单点簇只负责时间轴(居中 + 一大格 1s),**逐网格幅值轴与常规路径/Y 轴自适应完全同源**(`fitYToSelection` → `AutoRangeLaneToView`,否则 `autoRange=true`,再接 `ApplySinglePointValueMargins`),且**绝不改写** `fitYToSelection`。验证:新增离屏探针 `.preflight/perf/panelanim_main.cpp`(真实 `Application` + `SW_HIDE`)**94/94 通过**(连跑 3 次稳定;覆盖折起/展开的分帧过渡、单调收敛、越靠下越矮、末态精确无残留、筛选计数与独立折起状态、悬浮四个可交互区域全部命中且下层行仍可点、点击后"先滚动到位再折起"、真实 MF4 单点信号两种自适应逐位一致/往返稳定);**A/B** 三组(回退绘图区修复 / 禁用悬浮命中优先 / 动画时长置 0)分别 **4 / 13 / 8 项如实失败**;既有 `verify` **49/49**(顺带修正 4 条与远端新规则不符的过期断言)、`datacheck` **69/69**、`edgecheck` **46/46**;`make release` 零警告。 |
| 2026-10-16 | DeepSeek-V4.1-Flash(**另一台设备**) | **动画改「抽拉卷尺」式刚性平移 + 筛选态折叠三角修正 + 样式区「目标」行绘制/布局修正(详见 §9.36)**。① **动画重做**(推翻上一轮的“按行缩放”):整组信号作为**刚性整体**平移,**行高永不变**、只维护一个逐行平移量 `m_rowShift` ⇒ 动画期间**无需重建行集合**(流畅关键);收起时逐条被组名栏吞掉、展开时逐条冒出;中途反向点击**从当前位移续接**不跳变;时长 `clamp(115+6×行数, 150, 320)` ms。② **藏进组名栏的裁剪**:信号行越过组名的**绘制**下边界即裁掉,且除 GDI 裁剪外**必须用 `g.SetClip(..., CombineModeIntersect)` 同时收紧 GDI+ 裁剪**(否则行内色块/复选框/徽标画到组名栏上)。③ **悬浮组名折起改为“一边滚动一边收起”**(推翻“先滚动到位再折起”):`scrollSynced` 让滚动与平移按同一进度推进 ⇒ 组名屏幕位置恒定;结束时滚动偏移恰好落在“组名正好在最上方”,并且**无跳变**;旧 `m_pendingGroupToggle` 机制删除。④ **筛选态折叠三角**:`PaintFileRow` 改读 `IsGroupCollapsed()`(筛选模式用的是独立的 `m_filterCollapsed`),修复“三角维持筛选前状态”。⑤ **「目标」行**:`PaintTargetRow` 误用列表裁剪版 `PaintRowText` ⇒ 名称与 GX 文本被完全裁掉(改用 `UiText`);GX 标签改为**右对齐到内容列右边缘**、名称无标签时占满内容列;`EditFieldRect()` 对目标行只返回名称矩形(否则改名输入框把 GX 标签包进去)。验证:离屏探针 `.preflight/perf/panelanim_main.cpp` **147/147 通过**(连跑 3 次稳定;含逐帧断言组名栏像素零变化、`m_rowsDirty` 恒 false、滚动单调且恰好落位、筛选/普通态折叠三角逐像素 0 差异、目标行文本像素存在与两端对齐);**A/B** 五组(回退筛选三角+非刚性平移 / 回退目标行绘制与布局 / 关闭同步滚动 / 去掉 GDI+ 收紧裁剪 / 不续接位移)分别 **4 / 6 / 6 / 2 / 1 项如实失败**;`verify` **49/49**、`datacheck` **69/69**、`edgecheck` **46/46**;`make clean && make all` 零警告。 |
| 2026-10-16 | DeepSeek-V4.1-Flash(**另一台设备**) | **分组折起热区与改名输入框修正 + 数据模型去冗余(单一名称)+ 新增 `Components/Shared` 组件 + 过时注释/命名大清理 + 二进制瘦身 25.6%(详见 §9.37 与本轮报告 `Documents/refactor_report_2026-10-16.md`)**。① **折起热区**(用户要求):点分组**名称**不再折起,只有**折叠三角**或**「组X」标签**(新增 `RowPart::GROUP_TITLE`)才折起。② **分组改名输入框不再吞掉「N 项」**:`RowNameRect` 右边界改为项数左缘;新增 `GroupLabelText`/`GroupStatsText` 作为文本与宽度的唯一来源。③ **`SignalData`/`SignalGroup` 只留一个名称字段**:删除 `displayName` 与 `Title()`(31 处改用 `name`),`SetSignalDisplayName`/`SetGroupDisplayName` → **`RenameSignal`/`RenameGroup`**(空名一律拒绝);删除死成员 `Application::m_loadingBytes`。④ **新增 `Components/Shared`**(用户要求):从 `PaintUtil` 迁出全部**控件**(图标/复选框/滑块/内联输入框/颜色样块/取值胶囊配色)到 `UiControls.hpp/.cpp`,`PaintUtil` 只留低层原语;依赖方向固定 `Components/* → Components/Shared → Utilities`。⑤ **过时注释/命名**:`文件下标`→`分组下标`、`文件分组`→`分组` 等一批;形参名 `file`→`axis`/`group`;`PaintFileRow`→`PaintGroupRow`、`FileCloseRect`→`GroupCloseRect`、`OpenFileMenu`→`OpenGroupMenu`、`FileGroupLabelWidth`→`GroupLabelWidth`、`StyleEditField::FILE_NAME`→`GROUP_TITLE`;修复 `DataModel.hpp` 两处被压扁的注释块与 `NewTab` 重复文档块。⑥ **二进制瘦身**:真因是 **ICO 里 256×256 未压缩 BMP 条目(270 KB,占整包 27%)**,改为 **PNG 存储**(观感零变化)⇒ `Target.exe` **1 013 248 → 754 176 B(−25.6%)**;实测“拆分大文件不减小机器码”、`--gc-sections` 反而 +9 KB(已回退)。验证:`panelanim` **156/156**、`verify` **49/49**、`datacheck` **69/69**、`edgecheck` **46/46**;**A/B** 回退两处 UI 修复 **10 项如实失败**;`make release`/`make debug` 零警告。⚠️ **未完成(已如实记录)**:`PlotView.cpp`(13.7 K 行)与其它大文件的多文件化拆分、`LanePanelState` 抽取、模态提示解耦。 |
---
@@ -2505,7 +2595,7 @@ GDI+ 抗锯齿逐段扫描这些**近竖直、满幅**的边,代价爆炸(
筛选只是临时子集视图,不该污染普通列表的布局选择;筛选文本变化 / 清空筛选 / 数据变化时
一律清空它以及进行中的折起动画(行数与分组可见性都变了)。
组名后方的项数必须按当前模式显示:普通 = 信号总数,筛选 = 该组内**符合筛选条件**的个数
(`ListRow::displayCount`,在 `BuildRows` 里填好;`PaintFileRow` 只读它)。
(`ListRow::displayCount`,在 `BuildRows` 里填好;`PaintGroupRow` 只读它)。
⚠️ 推论:**所有“读取折起状态”的地方都必须用 `IsGroupCollapsed()`(按模式分派)**,
不能直接读 `m_tab->IsCollapsed()` —— 否则筛选态折叠时**折叠三角不会变**
(用户 2026-10-16 报告;已用“筛选态 vs 普通态折叠后组名行逐像素 0 差异”锁定)。
@@ -2530,8 +2620,36 @@ GDI+ 抗锯齿逐段扫描这些**近竖直、满幅**的边,代价爆炸(
**无 GX 标签时名称可用宽度 = 整个内容列**(因此更宽,仍左对齐)。
⚠️ `EditFieldRect()` 对 `m_editRow < 0` 必须返回 `m_targetNameRect`(**只管名称那一块**),
不能返回内容列全宽 —— 否则改名输入框会把 GX 标签包进去(用户报告)。
84. ⚠️ **筛选态的折叠三角必须用 `IsGroupCollapsed()`**(见第 79 条推论):`PaintFileRow` 直接读
84. ⚠️ **筛选态的折叠三角必须用 `IsGroupCollapsed()`**(见第 79 条推论):`PaintGroupRow` 直接读
`m_tab->IsCollapsed()` 时,筛选模式里折叠/展开三角**不会变化**(用户 2026-10-16 报告)。
85. ⚠️ **分组折起的热区只有两处**(用户 2026-10-16 第三轮要求):**折叠三角**(`RowPart::DISCLOSURE`)
与**「组X」标签**(`RowPart::GROUP_TITLE`)。点击分组**名称本体**(`BODY`)**不得折起**
(也不要触发滚动)。`RowHitPart()` 必须为分组行判出 `GROUP_TITLE`(标签矩形从
`checkRect.Right + 8*scale` 起、宽 `GroupLabelWidth(row)`);`OnLButtonDown` 只在
这两个区域调用 `ToggleGroupFromRow()`。
86. ⚠️ **分组改名输入框不得覆盖右侧的「N 项」计数**(用户 2026-10-16 报告):
`RowNameRect()` 对分组行的右边界 = `项数左边缘 − 6*scale`。
✓ 项数文本与它的宽度必须走**同一个来源** `GroupStatsText(row)`(`GroupLabelText(groupIndex)`
同理),否则“名称可用宽度”与“实际绘制文本”会漂移。
87. ⚠️ **`SignalData` / `SignalGroup` 只有一个名称字段 `name`**(用户 2026-10-16 明确要求):
它既是加载时的列名 / 通道名,也是用户重命名写入的地方;**不要再引入 `displayName`
或 `Title()` 之类的第二份副本**(信号靠 `SignalRef` 下标定位,从不按名字匹配)。
`RenameSignal(group, signal, name)` / `RenameGroup(group, name)` 对**空串 / 纯空白一律拒绝**
(名称不再有“回退值”)。
88. ⚠️ **通用 UI 控件属于 `Components/Shared`**(用户 2026-10-16 要求建此组件):
图标 / 复选框 / 滑块 / 内联输入框 / 颜色样块 / 取值胶囊配色都在
`Components/Shared/Inc/UiControls.hpp`;`Utilities/PaintUtil.*` **只保留低层绘制原语**。
依赖方向固定 `Components/* → Components/Shared → Utilities`,**不得反向**。
新增组件目录要同时登记 `Makefile`(`CXX_INCLUDES_PATHS` / `CXX_SOURCES_PATHS`)与
`.preflight/perf/build.ps1`。
89. ⚠️⚠️ **新增标识符前必须确认不是 Windows 头里的宏**:`windows.h` 会引入 `nb30.h` 的
`#define GROUP_NAME 0x80` ⇒ 用它做枚举成员会编译失败(“expected identifier before numeric
constant”)。同类高风险名:`GROUP_NAME`、`TIME`、`SMALL`、`NEAR`、`FAR`、`IGNORE`、`DELETE`。
90. ⚠️ **二进制体积的真正大头是 ICO 里未压缩的 256×256 条目**(本轮实测占整包 27%):
应把它按 **PNG** 存储(Vista+ 标准,观感零变化),小尺寸保留 BMP。
⚠️ **拆分大文件不会减小机器码**;“拆分 ⇒ 瘦身”不成立。
⚠️ `-ffunction-sections/-fdata-sections/--gc-sections` 在本工程**反而增大 9 KB**(已回退)。
⚠️ `.o` 字节数受节对齐/重定位表主导,**判断“代码搬去哪了”要用 `nm`,不要看 `.o` 大小**。
71. ⚠️ **`FitAll()` 在没有任何可见信号时不得置位 `Tab().ViewInitialized()`**:
否则用户先连点几次「适应视图」(此时无信号被勾选)再勾选第一个信号,
`Application::SynchronizeView` 会**跳过初次自适应**,视界停在“无信号”那次的整段范围上,
@@ -2579,6 +2697,12 @@ GDI+ 抗锯齿逐段扫描这些**近竖直、满幅**的边,代价爆炸(
## 12. 起源设备详细工程记忆(原文附录)
> ⚠️⚠️ **时效声明**:本节是**历史原文**,成文于数据层大重构与后续多轮修改**之前**。
> 其中出现的 `DataFile` / `fileName` / `SignalData::displayName` / `Title()` /
> `SetSignalDisplayName` / `SetFileDisplayName` / `PaintFileRow` / `FileCloseRect` /
> `FileGroupLabelWidth` 等**符号已全部更名或删除**,请勿照抄。
> 现行约定一律以 **§1–§11** 为准(§11.2 是硬约束权威清单)。
> 以下为起源设备(本机)Agent 的**详细工程记忆原文**,与 §1–§10 同处**一个记忆文件**(不再另建文件)。
> ⚠️ 其它设备更新本文件时**请保留本节**;可继续在本节之后追加。
-245
View File
@@ -1,245 +0,0 @@
# 数据层审计与重构报告(2026-10-15)
> 本轮任务:① 审计代码库的命名/参数/注释/数据结构;② 在审计基础上做一次解耦重构(大刀阔斧,
> 但**保持全部交互与显示不变**);③ 完善 CSV(多时间轴 / 各轴不等长)并新增 MF4 支持。
>
> 结论:**数据层与加载层被重写为「分组 / 时间轴 / 信号」三段式结构**,文件格式与数据模型彻底解耦;
> CSV 多时间轴与 MF4 均已支持;三套离屏测试(69 + 46 + 47 条断言)通过;`make release` 零警告。
---
## 一、审计发现的问题(按严重度)
### 1.1 「文件」概念渗进了数据模型的核心(最严重)
| 位置 | 问题 |
| --- | --- |
| `DataFile`(结构体名) | 名字把「一次加载产生的信号集合」和「磁盘上的文件」绑死;实际应用语义是**信号分组**。 |
| `DataFile::filePath` / `fileName` | 加载完成后应用与文件再无关系(文件可移动/删除/改名),这两个字段在运行期**没有任何用途**(`filePath` 只在加载消息里出现一次)。 |
| `DataFile::time` / `timeColumnName` / `timeMonotonic` / `minTime` / `maxTime` | 把「时间轴」焊死在分组上 ⇒ **一个分组只能有一条时间轴**,与 CSV 多 `time` 列、MF4 多通道组的现实不符。 |
| `SignalRef::fileIndex` | 引用语义被写成「文件下标」,全仓 223 处;后来者会以为信号与文件有绑定关系。 |
| `EdgeCountsFor(const DataFile& file, const SignalData& signal, …)` | **典型文不对题**:函数只需要「时间 + 采样值」,却要求调用方提供整个「文件」;这也是用户指出的重点。 |
| `SampleSignalAt(const DataFile& file, …)`、`FindNearestSample(const DataFile& file, …)`、`VisibleIndexRange(const DataFile& file, …)`、`EdgeIndexRange(const DataFile& file, …)`、`MeanFilterValueAt`、`WeakPeakValueAt` | 同上一类:参数语义过宽,时间轴与信号被迫通过「文件」间接耦合。 |
| `ResolvedSignal::file` | 视图层的解析结果也携带「文件」指针,导致采样/命中/包络/边沿统计处处 `resolved.file->time`。 |
### 1.2 `SignalData` 的冗余字段(用户指出的重点)
| 字段 | 问题 | 结论 |
| --- | --- | --- |
| `name` | 源名称(列名/通道名)。 | **保留**,语义明确:加载后原样不变。 |
| `displayName` | 用户重命名。 | **保留**(`Title()` 优先返回它)。 |
| `rawHeader` | `name` 的副本(构建时 `rawHeader = headers[i]; name = Trim(rawHeader)`),派生信号里又 `rawHeader = derived.name`。 | **删除**(纯冗余,两个字段表达同一件事)。 |
| `unit` | 由列名括号里猜出来的单位,全仓只有 2 处写、0 处读(从未显示、从未参与计算)。 | **删除**(属于「未实现的元数据」残留;用户明确说单位元数据暂不支持)。 |
| 时间信息 | 缺失 ⇒ 只能靠「文件」找时间轴。 | **新增 `axisIndex`**:信号指向所属时间轴,采样值与时间的对应关系自包含。 |
### 1.3 加载层与格式耦合
| 位置 | 问题 |
| --- | --- |
| `CsvTable`(结构体) | 「从文件读出来的原始表格」这个名字把通用中转结构钉死在 CSV 上;后续每加一种格式都要再抄一遍类似结构。 |
| `CsvProgress` / `CsvProgressInfo` | 名字里的 `Csv` 让「加载进度」这类通用概念只服务于 CSV。 |
| `BuildDataFile(CsvTable&&, path, …)` | 建组函数直接吃 CSV 解析结果;MF4(或任何其它格式)都无法复用,只能复制一份逻辑。 |
| `bool BuildDataFile(...)` 里的「识别时间轴列 / 回退第一列」逻辑 | 与「多时间轴」需求正面冲突:只认**第一个**时间列,且要求所有列等长。 |
| `.preflight` 之外无任何「格式 → 数据」的装配点 | 新增格式要在 `Application::RunAsyncLoad` 里改流程。 |
### 1.4 过期 / 文不对题的注释
| 位置 | 问题 |
| --- | --- |
| `LooksLikeTimeHeader` | 判据是「列名含 time/时间」⇒ 会把 `timeout`、`TimeStampExtra` 之类误判成时间轴(多时间轴下会**切出空块**)。 |
| `ExtractUnit` | 解析出来的单位无人使用(见 1.2)。 |
| 大量 `@param [IN] file 信号所属文件` | 参数已改语义后注释未跟(本轮全部改为 `axis 该信号使用的时间轴`)。 |
| UI 文案 | `选中该文件全部信号` / `取消该文件全部信号` / `关闭该文件(从工作区移除)` / `请关闭部分文件分组或信号` 等,把「分组」说成「文件」。 |
| 空状态文案 | `尚未加载数据,请打开 CSV 文件` / `打开 CSV 文件以查看仿真数据波形`:应用已不只支持 CSV。 |
### 1.5 健壮性缺陷(审计中顺带发现)
| 位置 | 问题 |
| --- | --- |
| `Application::Initialize` | `RegisterClassExW` 失败即弹**模态**错误框。同一进程内第二次构造 `Application` 必然命中 `ERROR_CLASS_ALREADY_EXISTS` ⇒ 调用方**永久阻塞**(本轮离屏测试就被它卡住)。`Shutdown` 也从不注销窗口类。 |
| `Application::LoadFile` 失败路径 | 会弹模态对话框(离屏/自动化场景是雷区)。已在测试中绕开;建议后续把「错误提示」与「模态阻塞」解耦。 |
| MDF 文本块解析 | `##TX` 在 MDF 规范里是「长度 + UTF-16/UTF-8」,但 CANape 16 会写**无长度前缀的裸 ASCII**(见 §三),只按规范解析会得到空名称。 |
---
## 二、重构内容(改动清单)
### 2.1 数据模型(`Utilities/Inc/DataModel.hpp`、`Utilities/Src/DataModel.cpp`)
**新增 `TimeAxis`(时间轴成为一等公民)**
```cpp
struct TimeAxis {
std::wstring name; // 时间列名(源文件原样)
std::vector<double> time;
double minTime, maxTime;
bool monotonic;
std::size_t finiteCount;
std::size_t Count() const;
};
```
**`SignalData` 精简 + 指向时间轴**
```cpp
struct SignalData {
std::wstring name; // 源名称(原样保留、永不修改)
std::wstring displayName; // 用户重命名(空 = 用 name)
std::size_t axisIndex = 0; // 所属时间轴(分组内下标) ← 新增
std::vector<double> values;
double minValue, maxValue; std::size_t finiteCount;
std::vector<double> coarseMin, coarseMax; std::size_t coarseStride;
SignalStyle style;
const std::wstring& Title() const;
};
// 删除:rawHeader、unit
```
**`DataFile` → `SignalGroup`(分组 = 若干时间轴 + 若干信号)**
```cpp
struct SignalGroup {
std::wstring name; // 默认取源文件名(便于辨认来源)
std::wstring displayName; // 用户重命名
std::vector<TimeAxis> axes; // 1..N 条时间轴 ← 取代单一 time
std::vector<SignalData> signals;
const std::wstring& Title() const;
const TimeAxis& AxisOf(const SignalData&) const;
const TimeAxis& AxisAt(std::size_t) const;
bool TimeRange(double& low, double& high) const; // 全部轴的并集
};
// 删除:filePath、fileName、time、timeColumnName、timeMonotonic、minTime、maxTime
```
**引用与集合**
* `SignalRef::fileIndex` → **`groupIndex`**(含注释:「分组是应用内的逻辑容器,与磁盘文件没有任何关系」)。
* `DataSet`:`AddDataFile→AddGroup`、`RemoveFile→RemoveGroup`、`FileAt→GroupAt`、`FileCount→GroupCount`、
`Files()→Groups()`、`SetFileDisplayName→SetGroupDisplayName`、
`RemapSelectionAfterFileRemoval→RemapSelectionAfterGroupRemoval`;新增 **`Axis(ref)`**(解析信号→时间轴)。
* `TabConfig`:`SetFileVisible→SetGroupVisible`、`FileHasVisible→GroupHasVisible`、`FileAllVisible→GroupAllVisible`、
`RemapAfterFileRemoval→RemapAfterGroupRemoval`、`RowKind::FILE→RowKind::GROUP`、`CMD_FILE_*→CMD_GROUP_*`。
**采样与统计 API 去掉「文件」**
```cpp
bool SampleSignalAt(const TimeAxis&, const SignalData&, double time, bool interpolate, double&);
bool SampleDisplayedSignalAt(const TimeAxis&, const SignalData&, const SignalStyle&, …);
int FindNearestSample(const TimeAxis&, double time);
// PlotView:SampleSpacingPixels / VisibleIndexRange / EdgeIndexRange /
// MeanFilterValueAt / WeakPeakValueAt / EdgeCountsFor /
// EdgeCenterCountsFor / EdgeScanFor —— 首参全部由 const DataFile& 改为 const TimeAxis&
```
**视图层**
* `ResolvedSignal`:`const DataFile* file` → **`const SignalGroup* group` + `const TimeAxis* axis`**;
所有 `resolved.file->time` 改为 `resolved.axis->time`,`resolved.file->Title()` 保留走 `group`。
* `PolylineCacheEntry::file` → `group`;`EdgeScanState::file` → **`axis`**(统计身份 = 时间轴 + 信号)。
* `TabConfig::ComputeVisibleTimeBounds`:只统计**可见信号真正用到的时间轴**(多轴分组下避免拉出过宽范围)。
* 时间轴刻度列名取「首个可见信号所属分组的第一条时间轴」。
### 2.2 加载层(新文件,格式与模型解耦)
```
Utilities/Inc/DataTable.hpp DataTable(通用列优先表格)+ LoadProgress/Info
+ TableBlock(一个时间轴块)+ LoadedSource(源文件的全部块)
Utilities/Inc/DataLoader.hpp LoadSignalSource(path,…) 唯一入口 + IsSupportedSignalFile + 对话框过滤器
Utilities/Src/DataLoader.cpp 按扩展名分发(.csv/.txt → CSV;.mf4/.mdf → MDF4;其它按文本尝试)
Utilities/Inc/CsvLoader.hpp LoadCsvSource
Utilities/Src/CsvLoader.cpp CSV 解析 + 多时间轴切分 + 各轴按自己的时间列裁剪
Utilities/Inc/Mf4Loader.hpp LoadMf4Source
Utilities/Src/Mf4Loader.cpp ASAM MDF v4 解析(见 §三)
删除:Utilities/Inc/CsvTable.hpp、Utilities/Src/CsvTable.cpp
```
**唯一装配点**:`BuildSignalGroup(LoadedSource&&, paletteBase, SignalGroup&, error, progress)`
—— 与格式无关,负责:时间轴**去重合并**(内容逐位相同只保留一条)、信号命名、调色板取色、统计与粗粒度包络。
新增格式时**只需写一个加载器**,数据模型与全部界面代码不用改。
### 2.3 CSV:多时间轴 + 各轴不等长
* 规则(与 MATLAB/Simulink 说明一致):**只有名为 `time` 的列是时间轴**;每个时间列开启一个新块,
它之后直到下一个时间列之前的列都属于该块。
* 各块按**自己的时间列**裁剪(较短的时间轴在数据区提前结束,其余位置为空)⇒ 支持
`TestData/Multi_Timeline.CSV`(`time,data1,time,data2,data3,time,data4`,实测切出 3 条轴,长度 7/4/7)。
* 修复用户报告的缺陷:**不再把其它 `time` 列当成信号显示**。
* 保留旧行为:完全没有 `time` 列时退化为「第一列是时间轴」的单轴文件。
* 判据收紧:`LooksLikeTimeHeader` 只接受 `t` / `time` / `timestamp` / `simulation time` / `时间` / `时间轴`
(不再用「包含 time」这种会误判 `timeout` 的规则)。
### 2.4 MF4:新增支持
* 加载后**新建一个分组、组名 = 文件名**,文件内的全部信号都归入该分组(用户要求的行为)。
* 每个**通道组(CG)**是独立时间轴:内容相同的时间轴自动合并
(实测该文件 27 个 CG → **3 条时间轴**、124 条信号,避免 25 条重复的单样本轴)。
* 只解码绘制需要的部分:字符串/字节数组通道、压缩块(DZ/HZ)、MIME 通道一律跳过;
非数值通道不产生信号。
### 2.5 UI 文案与健壮性
* 文案统一到「分组」:`选中该分组全部信号`、`取消该分组全部信号`、`关闭该分组(从工作区移除)`、
`请关闭部分分组或信号后重试。`、`尚未加载数据,请打开数据文件`、`打开数据文件以查看波形`、
`(可同时加载多个文件,各自成为一个分组)`;打开对话框标题改为「打开测量数据文件」并支持 `*.mf4/*.mdf`。
* `Application::Initialize`:`RegisterClassExW` 返回 `ERROR_CLASS_ALREADY_EXISTS` 时**复用**已注册的类
(不再弹模态错误框);`Shutdown` 注销窗口类并销毁窗口。⇒ 同一进程可多次构造/销毁 `Application`。
---
## 三、MDF v4 解析要点(本轮实测得到,供后续维护)
文件实测为 **MDF 4.10 / CANape 16.0.50**(`##HD … ##DG … ##CG … ##CN … ##CC … ##DT`)。
* **块头(24 字节)**:`id[4]` + `reserved[4]` + `block_len(U64)` + `link_count(U64)`;之后是
`link_count` 个 **8 字节绝对文件偏移**(0 = 空);数据段紧随链接区。DT/HL 等块可以 0 链接。
* **ID 块**:偏移 0 起 `"MDF "`,偏移 8 起版本串(如 `"4.10 "`)。
* **HD**:链接 `[0] = 第一个 DG`。
* **DG**:链接 `[1] = 第一个 CG`、`[2] = 该组的数据块(DT)`。
* **CG**:链接 `[1] = 第一个 CN`、`[2] = tx_name`;数据段 `+8 = cycle_count`。
**记录长度**取 `DT 数据段字节数 / cycle_count`(实测 114/110 字节,与声明一致),拿不到时按通道位宽向上取整。
* **CN**:链接 `[2] = 名称 TX`、`[4] = 换算 CC`;数据段
`+0 type`、`+1 sync_type`(**1 = 时间主通道**)、`+2 data_type`、`+3 bit_offset`、
`+4 byte_offset(U32,单位**字节**)`、`+8 bit_count`、`+12 flags`(bit0 整列无效 / bit1 有无效位)、
`+16 invalid_bit_pos`、`+32 val_range_min`、`+40 val_range_max`。
**绝对位偏移 = byte_offset × 8 + bit_offset**。
* **data_type**:0/1 无符号(LE/BE)、2/3 有符号、4/5 IEEE 浮点、≥6 字符串/字节数组(跳过)。
浮点只接受字节对齐的 32/64 位;整数支持 1–64 位任意位偏移(含符号扩展)。
* **CC**:`+0 type`、`+1 precision`、`+2 flags`、`+4 ref_param_nr`、`+6 val_count`、
`+8 phy_range_min`、`+16 phy_range_max`、`+24 cc_val[]`(每个 8 字节)。
**换算公式(实测确认)= `phys = cc_val[0] + cc_val[1] × raw`**。
本文件里时间主通道是「double 纳秒 + 线性换算 1e-9」⇒ 解码后时间范围 `[0.024320125, 42.3918] s`
与 CC 里的物理范围逐位吻合,可据此自检。
* **文本块(TX)**:规范是「U32 长度 + UTF-16 代码单元」(MDF 4.1 起也可 UTF-8 字节数),
但 **CANape 会写无长度前缀的 NUL 结尾裸文本**。解析顺序:先按长度前缀(UTF-16 → UTF-8),
失败则按「裸文本(自动判断 UTF-16/UTF-8)」回退。
---
## 四、验证(全部离屏;进程/线程 `BELOW_NORMAL`、窗口 `SW_HIDE`、不注入真实鼠标键盘)
| 套件 | 覆盖 | 结果 |
| --- | --- | --- |
| `.preflight/perf/datacheck_main.cpp` | 单轴 CSV 回归;多时间轴 CSV(3 轴、7/4/7、不再出现 `time` 信号);MF4(组名、124 信号、3 条轴、主时间轴 ns→s、`setSpeed` 首样本与原始字节人工核对);`DataSet` 不变量(轴共享/解析/时间范围/重命名);派生信号沿用源时间轴;**MF4 集成**(124 信号并入一个网格渲染 67.5ms/帧) | **69/69 通过** |
| `.preflight/perf/edgecheck_main.cpp` | 「调整」符号规则(真实菜单路径);边沿统计按帧推进 / 不饿死 / 与参考实现逐位一致 / 无鼠标输入也完成;面板两列等宽与名称限宽;弱峰复位控件 | **46/46 通过** |
| `.preflight/perf/verify_main.cpp`(既有) | PlotView 渲染/交互:Y 自适应、派生信号、图例命中、GX 徽标悬停、装饰层无残影、平移逐像素 | **47/47**(另有 2 条**本机未同步的旧规则断言**:`derived offsetTime kept`、`mouse in lane1 lower half`,与本轮无关) |
| 构建 | `make release` | **零警告** |
补充:`Target.exe` 手工运行确认加载 `Multi_Timeline.CSV` 与 `MF4-Record.MF4` 正常(真实交互由用户后续验收)。
---
## 五、遗留与建议(未在本轮实施)
1. **错误提示与模态阻塞解耦**:`SetStatusMessage(..., isError=true)` 直接 `MessageBoxW`,
在自动化/离屏场景会永久阻塞。建议改为宿主级非模态提示(或至少让调用方可选择不弹)。
2. **MF4 压缩块(`##DZ`/`##HL`/`##SD`)与 `##DL` 数据列表**未实现:遇到这类文件会跳过相应通道组。
如后续需要,解析 `DL` 的偏移数组即可(数据布局不变)。
3. **MF4 单元/注释(`##CC` 的 unit TX、`##MD`)未使用**:用户明确暂不需要元数据。
4. **`Component` 层仍在 `TabConfig` 里保存逐信号的边沿统计/预览参数**:属于「视图配置」与「数据」的
第二次耦合(当前可用,但若要支持「同一信号在不同标签页用不同统计参数」需再拆一层)。
5. **`Lane` 内含 4 类实验性功能的状态与面板坐标**(`edgeCount/meanFilter/weakPeak` + 各自面板 X/Y),
已经把 `Lane` 撑到 200+ 行;建议后续抽出 `LanePanelState`。
6. **`PlotView.cpp` 仍有 1.1 万行**:渲染分支(静态缓存/宽基底/样式补丁/整幅)与交互命中混在一个类里,
建议按「渲染器 / 命中测试 / 派生曲线调度」拆成 3 个协作类(风险较高,宜单独一轮做)。
7. **`SignalRef` 仍是「分组下标 + 组内下标」**:删除/重排分组时依赖调用方按顺序调用
`Remap*` 系列函数维护;可考虑改为稳定 ID(后续需要撤销/重做时收益更大)。
+283
View File
@@ -0,0 +1,283 @@
# 代码审计与解耦报告(2026-10-16)
> 本轮任务:① 修两个 UI 交互缺陷;② 继续上一轮(`6eedbe3c5d`)之后的审计与解耦 —— 修正过时注释 /
> 函数与参数命名、消除数据结构冗余成员、抽出 `Components/Shared` 共享组件、按组件目录结构拆分
> 巨型实现文件;③ 为二进制产物瘦身;④ 全部改动不得影响功能与性能。
>
> 结论:**UI 缺陷已修复并 A/B 验证;数据模型去冗余(信号 / 分组只保留唯一名称)落地;全仓过时注释与
> 命名批量修正;新增 `Components/Shared` 组件并迁入通用 UI 控件;产物体积 −25.6%(1013 KB → 737 KB)**。
> 四套离屏测试(49 + 69 + 46 + **156** 条断言)全绿、`make release` / `make debug` 零警告。
> **`PlotView.cpp` 的整体拆分本轮未执行**(见 §五,附可执行的落地方案与原因)。
---
## 一、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 项如实失败**,恢复后全绿。
---
## 二、数据模型去冗余(用户指出的重点)
### 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` 的“体量过大”属**内聚性**问题而非冗余,
> 见 §五 的 `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 结论:**1013 KB → 737 KB(−25.6%,省 ≈276 KB)**
| 项 | 大小 |
| --- | --- |
| `Target.exe`(Release `-Os -s`,本轮前) | 1 013 248 B |
| `Target.exe`(Release `-Os -s`,本轮后) | **754 176 B** |
| 差值 | **−259 072 B(−25.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`)。
---
## 七、本轮未完成 / 后续建议(诚实记录)
### 7.1 `PlotView.cpp`(13 695 行 / 544 KB)按功能拆分 —— **本轮未执行**
* **为什么不硬做**:拆分本身是**纯代码搬移**(不改变行为),真正的成本在**前置条件**:
该文件的辅助区(第 13–888 行的匿名命名空间,含 ~870 行常量与工具函数)被**全部**成员函数
广泛引用。要让多个 .cpp 共享它们,必须先把匿名命名空间改成具名命名空间并**逐个函数标注
`inline`**(否则多 TU 重复定义或产生 N 份机器码),再逐个把成员定义搬走。
这是一次需要“搬一类 → 编译 → 跑四套探针”反复迭代的工程(预计 5 个 .cpp:基底/图层缓存、
绘制、命中与交互、派生曲线调度、浮动面板),**在没有足够时间逐步验证时贸然动手会破坏
性能敏感的渲染路径**,因此本轮**主动不做**,而不是草率地做一半。
* **推荐的落地方案(可直接照着做)**:
1. 新建 `Components/PlotView/Src/PlotViewInternal.hpp`:包含现有 `#include` 段 +
`namespace jdi { namespace pvdetail { <常量与工具函数,逐个加 inline> } using namespace pvdetail; }`;
2. `PlotView.cpp` 只留核心(构造/析构、`Layout`/`RefreshLanes`/`BuildLaneVisuals`、缓存失效);
3. 按**粘连度**由低到高搬移:`PlotViewDerived.cpp`(曲线线程池与 `CurveSpanFor` / `ScheduleCurveJob`)、
`PlotViewPanels.cpp`(4 类实验性浮动面板的几何/命中/绘制)、`PlotViewInteraction.cpp`
(`On*` 与命中测试)、`PlotViewPaint.cpp`(`Paint*` 与图层/装饰缓存);
4. 每搬一个文件:`make release` 零警告 + `verify`/`panelanim` 全绿,再继续。
5. ⚠️ 千万别动 `Paint()` 的**分支顺序**与图层/装饰缓存的既有不变量(见 `agent_mem.md` §11.2)。
### 7.2 其余组件的多文件化(同样未执行)
`SignalPanel.cpp`(145 KB / 3.7 K 行)、`Application.cpp`(104 KB)、`DataModel.cpp`(88 KB)
同样适合按“列表/绘制/交互/编辑”“窗口与消息/命令与动作/加载”“数据集合/统计与采样/标签页配置”
拆成 3–4 个文件。`SignalPanel.cpp` 的辅助区仅约 60 行(十几个常量 + 少量格式化函数),
**改造成本远低于 `PlotView.cpp`**,是下一步性价比最高的拆分入口。
### 7.3 `Lane` 的内聚性(上一轮建议 5)
`Lane` 内含 4 类实验性功能的状态 + 各自的面板坐标,已超 200 行;建议抽出
`LanePanelState`(开关 + 每信号参数 + 面板位置),`Lane` 只保留结构与归属。
属**结构内聚**重构而非去冗余,需同步改 `DataModel` 与 `PlotView` 的多处访问点,本轮未做。
### 7.4 错误提示与模态阻塞解耦(上一轮建议 1)
`SetStatusMessage(..., isError=true)` 仍直接 `MessageBoxW`;自动化 / 离屏场景与其耦合,
建议改为宿主级非模态提示。本轮未做(与“不影响功能”的约束存在交互设计决策,需用户拍板样式)。
### 7.5 MF4 压缩块 / 数据列表(上一轮建议 2–3)
按用户要求**继续不做**。
---
## 八、变更清单(本轮)
```
新增 Components/Shared/Inc/UiControls.hpp 通用 UI 控件(自 PaintUtil 迁出)
新增 Components/Shared/Src/UiControls.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)
修改 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` 与本文)。