diff --git a/Documents/agent_mem.md b/Documents/agent_mem.md index 55e030f..5f7f162 100644 --- a/Documents/agent_mem.md +++ b/Documents/agent_mem.md @@ -2209,6 +2209,13 @@ Components/Shared/Src/UiControls.cpp #### 七、多文件拆分(本轮**已完成**,不再是遗留项) +✅ **可复跑证据:`make structcheck`(= `Tools/structcheck.ps1`,入库脚本,62 条断言全绿,退出码 0/1)** +—— 脚本从磁盘与 `nm` 两路核验「文件是否真被拆开 / 职责是否真搬走 / 新 TU 是否真进产物 / 构建是否登记完整」, +不再依赖散文描述。实测行数:`PlotView.cpp` 705(原 13 695)、`PlotViewPaint.cpp` 5 950、 +`PlotViewInteraction.cpp` 2 537、`PlotViewFeatures.cpp` 2 821、`PlotViewInternal.hpp` 884; +`SignalPanel` 1 019 / 614 / 1 925 / 288;`Application` 1 326 / 1 071 / 237 / 286; +`DataModel` 1 459 / 666 / 312(25 个实现文件、每个目录都登记在 `Makefile`)。 + 四个大文件都按同一手法拆成多个协作文件(私有内部头 + 具名命名空间 + 全 `inline` + 按名字分桶搬移): | 原文件 | 拆分后 | @@ -2320,8 +2327,8 @@ Components/Shared/Src/UiControls.cpp | 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` 组件 + 过时注释/命名大清理 + 二进制瘦身 24.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` 重复文档块。⑥ **多文件拆分(本轮完成)**:`PlotView.cpp`(13 695 行)+ `SignalPanel.cpp` + `Application.cpp` + `DataModel.cpp` 四个大文件全部按功能拆成多个协作文件(私有内部头 + 具名命名空间 + 全 `inline` + 按名字分桶),最大单文件 544 KB → 276 KB。⑦ **二进制瘦身**:真因是 **ICO 里 256×256 未压缩 BMP 条目(270 KB,占整包 27%)**,改为 **PNG 存储**(观感零变化)⇒ `Target.exe` **1 013 248 → 764 416 B(−24.6%)**;实测“拆分不会减小机器码,反而 +1.4%”、`--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` 抽取、模态提示解耦。 | -| 2026-10-16 | DeepSeek-V4.1-Flash(**另一台设备**) | **收尾:`LanePanelState` 抽取 + 错误提示非模态化(把上一行的「未完成」清零,详见 §9.37 第八节与报告 `Documents/refactor_report_2026-10-16.md` §九)**。① **`Lane` 的 12 个平铺面板字段**(`edgePanelOpen/X/Y` × 4 类实验性功能)→ **4 个 `LanePanelState`**(`struct LanePanelState { bool open=true; double x=-1; double y=-1; }`),访问点全部改为 `lane.edgePanel.open` 形式;`RebuildLanes` 的 `LaneCarry` 同步收敛为 4 个 `LanePanelState`(逐字段赋值 → 整体赋值)。⚠️ 教训:**批量改名脚本不能对「声明」与「使用」跑同一组替换对**(会生成 `bool edgePanel.open = true;` 非法声明),声明处须单独重写。② **错误提示与模态阻塞解耦**:新增 `Application::SetModalErrorsEnabled(bool)`/`ModalErrorsEnabled()`(成员默认 `true`),`Initialize` **最先**执行 `SetModalErrorsEnabled(showCommand != SW_HIDE)`(隐藏窗口弹模态框 = 进程被钉死);`SetStatusMessage(err,true)` 允许模态时原样 `MessageBoxW`,否则走 `m_plot.ShowPersistentHint(text)`(绘图区底部非模态浮层);`PlotView` 新增公开 `ShowPersistentHint`。普通提示/空文本仍被忽略 ⇒ **交互行为不变**。③ **顺带修掉真实缺陷**:`PlotView::Paint` 在 `m_lanes.empty()` 时提前 `return`,提示浮层在其后绘制 ⇒ **未勾选任何信号时错误永远不可见**(正是"加载失败"场景),该分支补一次 `PaintHint(g)`。④ **新增探针 `.preflight/perf/errcheck` 19/19**:默认值/隐藏降级/宿主开关、**工作线程 + 5s 超时**判"不阻塞"(旧行为会永久阻塞 ⇒ 判失败并 `ExitProcess`,探针不挂)、`EnumWindows` 数本进程 `#32770` 顶层窗口数不变、提示文本/粘滞/需重绘/起始时刻、**像素级**"有/无提示"差异 3781 且全帧差异同为 3781。验证:`make release`/`make debug` 零警告,`Target.exe` **764 416 B**;`verify` 49/49、`datacheck` 69/69、`edgecheck` 46/46、`panelanim` 156/156。 | +| 2026-10-16 | DeepSeek-V4.1-Flash(**另一台设备**) | **分组折起热区与改名输入框修正 + 数据模型去冗余(单一名称)+ 新增 `Components/Shared` 组件 + 过时注释/命名大清理 + 二进制瘦身 24.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` 重复文档块。⑥ **多文件拆分(本轮完成)**:`PlotView.cpp`(13 695 行)+ `SignalPanel.cpp` + `Application.cpp` + `DataModel.cpp` 四个大文件全部按功能拆成多个协作文件(私有内部头 + 具名命名空间 + 全 `inline` + 按名字分桶),最大单文件 544 KB → 276 KB。⑦ **二进制瘦身**:真因是 **ICO 里 256×256 未压缩 BMP 条目(270 KB,占整包 27%)**,改为 **PNG 存储**(观感零变化)⇒ `Target.exe` **1 013 248 → 764 416 B(−24.6%)**;实测“拆分不会减小机器码,反而 +1.4%”、`--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` 等大文件多文件化拆分、`LanePanelState` 抽取、模态提示解耦)**已全部在紧随其后的两个提交里完成**:拆分 → 提交 **`e6b7bf6`**(详见 §9.37 第七节),`LanePanelState` + 模态解耦 → 提交 **`d2e6fdd`**(详见 §9.37 第八节)。**本行不再表示任何未完成项。** | +| 2026-10-16 | DeepSeek-V4.1-Flash(**另一台设备**) | **收尾:`LanePanelState` 抽取 + 错误提示非模态化(把上一行的「未完成」清零,详见 §9.37 第八节与报告 `Documents/refactor_report_2026-10-16.md` §九)**。① **`Lane` 的 12 个平铺面板字段**(`edgePanelOpen/X/Y` × 4 类实验性功能)→ **4 个 `LanePanelState`**(`struct LanePanelState { bool open=true; double x=-1; double y=-1; }`),访问点全部改为 `lane.edgePanel.open` 形式;`RebuildLanes` 的 `LaneCarry` 同步收敛为 4 个 `LanePanelState`(逐字段赋值 → 整体赋值)。⚠️ 教训:**批量改名脚本不能对「声明」与「使用」跑同一组替换对**(会生成 `bool edgePanel.open = true;` 非法声明),声明处须单独重写。② **错误提示与模态阻塞解耦**:新增 `Application::SetModalErrorsEnabled(bool)`/`ModalErrorsEnabled()`(成员默认 `true`),`Initialize` **最先**执行 `SetModalErrorsEnabled(showCommand != SW_HIDE)`(隐藏窗口弹模态框 = 进程被钉死);`SetStatusMessage(err,true)` 允许模态时原样 `MessageBoxW`,否则走 `m_plot.ShowPersistentHint(text)`(绘图区底部非模态浮层);`PlotView` 新增公开 `ShowPersistentHint`。普通提示/空文本仍被忽略 ⇒ **交互行为不变**。③ **顺带修掉真实缺陷**:`PlotView::Paint` 在 `m_lanes.empty()` 时提前 `return`,提示浮层在其后绘制 ⇒ **未勾选任何信号时错误永远不可见**(正是"加载失败"场景),该分支补一次 `PaintHint(g)`。④ **新增探针 `.preflight/perf/errcheck` 19/19**:默认值/隐藏降级/宿主开关、**工作线程 + 5s 超时**判"不阻塞"(旧行为会永久阻塞 ⇒ 判失败并 `ExitProcess`,探针不挂)、`EnumWindows` 数本进程 `#32770` 顶层窗口数不变、提示文本/粘滞/需重绘/起始时刻、**像素级**"有/无提示"差异 3781 且全帧差异同为 3781。验证:`make release`/`make debug` 零警告,`Target.exe` **764 416 B**;`verify` 49/49、`datacheck` 69/69、`edgecheck` 46/46、`panelanim` 156/156。⑤ **新增结构检查** `Tools/structcheck.ps1` + `make structcheck`(**62/62**):从磁盘与 `nm` 核验「四个巨型文件已按职责拆分(`PlotView.cpp` 13 695 行 → 705 行,绘制符号只在 `PlotViewPaint.o`)」与「`Components/Shared` 16 个控件全部由 `UiControls.o` 定义、69 处调用跨 ≥3 个组件」以及分层方向。 | --- @@ -2738,6 +2745,16 @@ Components/Shared/Src/UiControls.cpp 95. ⚠️ **模态反馈必须是可关闭的**:`Application::SetModalErrorsEnabled` 默认 `true`, `Initialize(instance, showCommand)` 按 `showCommand != SW_HIDE` **自动降级**为 `m_plot.ShowPersistentHint(text)`;隐藏窗口下弹模态框没人能点掉 = 进程被钉死。 +96. **结构约束有脚本可验**:`make structcheck`(脚本 `Tools/structcheck.ps1`,入库、纯 ASCII、 + 退出码 0/1)核验「巨型文件按职责拆分」与「`Components/Shared` 共享控件」两类不变量: + 文件存在与行数、职责函数是否**只**定义在预期文件、内部头的具名命名空间 + `inline` 计数、 + `Build/Release/*.o` 是否齐全、**`nm` 断言绘制符号只在 `PlotViewPaint.o`**、 + 每个实现目录都在 `Makefile` 里、`UiControls.hpp` 声明的每个控件**是否只由 `UiControls.o` 定义**、 + 调用点是否真的跨 ≥3 个组件。⚠️ 判定控件要用「`UiControls.hpp` **声明**的名字」而不是「所有 `Ui*` 函数」 + (`UiFillRect`/`UiText`/`UiMeasureTextWidth` 是底层原语,本就住在 `Utilities/PaintUtil`)。 + ⚠️ 精确分层不变量:**`Components/Shared` 不依赖任何组件;底层 `PaintUtil` 不依赖 `Shared`; + `Utilities` 不包含任何功能组件头**;`Utilities/{PopupMenu,ColorPickerPopup}` 是历史上放在 `Utilities/` + 下的自绘 UI 小部件,**允许**消费 `Shared`(方向同组件),不要把它们判成违规。 71. ⚠️ **`FitAll()` 在没有任何可见信号时不得置位 `Tab().ViewInitialized()`**: 否则用户先连点几次「适应视图」(此时无信号被勾选)再勾选第一个信号, `Application::SynchronizeView` 会**跳过初次自适应**,视界停在“无信号”那次的整段范围上, diff --git a/Documents/refactor_report_2026-10-16.md b/Documents/refactor_report_2026-10-16.md index 76c6ea7..274ddf3 100644 --- a/Documents/refactor_report_2026-10-16.md +++ b/Documents/refactor_report_2026-10-16.md @@ -5,10 +5,15 @@ > 巨型实现文件;③ 为二进制产物瘦身;④ 全部改动不得影响功能与性能。 > > 结论:**UI 缺陷已修复并 A/B 验证;数据模型去冗余(信号 / 分组只保留唯一名称)落地;全仓过时注释与 -> 命名批量修正;新增 `Components/Shared` 组件并迁入通用 UI 控件;三个大文件(PlotView / SignalPanel / +> 命名批量修正;新增 `Components/Shared` 组件并迁入通用 UI 控件;四个大文件(PlotView / SignalPanel / > Application / DataModel)按功能拆分为多个协作文件;产物体积 **−24.6%**(1 013 248 → 764 416 B)**。 -> 四套离屏测试(49 + 69 + 46 + **156** 条断言)全绿、`make release` / `make debug` 零警告。 -> **`PlotView.cpp` 的整体拆分本轮未执行**(见 §五,附可执行的落地方案与原因)。 +> 离屏测试链(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` 推翻,此处已更正。 --- @@ -211,7 +216,11 @@ Components/*(功能组件) ──▶ Components/Shared(UI 控件) ─ --- -## 七、本轮未完成 / 后续建议(诚实记录) +## 七、遗留项与后续建议(逐项的最终处理状态) + +> 2026-10-16 更正:本行曾写“整体拆分本轮未执行”,那是提交 `0871eb0` 时刻的状态; +> 紧随其后的提交 **`e6b7bf6`(按组件目录结构把四个巨型实现文件拆分为多个协作文件)** +> 已把该项做完(详见 §7.1 / §7.2 与 §九)。 ### 7.1 `PlotView.cpp`(13 695 行 / 544 KB)按功能拆分 —— **已完成** @@ -391,6 +400,7 @@ struct LanePanelState | `.preflight/perf/edgecheck`(边沿统计) | **46/46** | | `.preflight/perf/panelanim`(分组折起动画 + 两处 UI 修复) | **156/156** | | `.preflight/perf/errcheck`(本轮新增) | **19/19** | +| `make structcheck`(本轮新增,`Tools/structcheck.ps1`) | **62/62**(结构约束:拆分 + 共享控件 + 分层,见 §9.6) | | 上一遍的 A/B(回退两处 UI 修复) | 如实失败 **9** 项(失败文案逐条点名「点组名不应折起」「改名框不应包住条目计数」),恢复后 156/156 | ### 9.5 本轮变更清单 @@ -405,5 +415,33 @@ struct LanePanelState 修改 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 结构检查(拆分 + 共享控件 + 分层),62 条断言 +修改 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` 这类**底层原语**也算成了控件)。 diff --git a/Makefile b/Makefile index 01d79d3..d7e043f 100644 --- a/Makefile +++ b/Makefile @@ -188,6 +188,11 @@ debug: $(BUILD_DIR)/$(SUB_DIR_DEBUG)/$(TARGET_FILE_NAME).exe clean: $(BUILD_DIR) powershell rm -r $(BUILD_DIR) +# 结构检查任务:校验「巨型文件按职责拆分」与「Components/Shared 共享控件」两项结构约束 +# (需要先完成一次 release 构建,脚本会读取 Build/Release 下的目标文件) +structcheck: + powershell -ExecutionPolicy Bypass -File Tools/structcheck.ps1 + # 构建目录生成 $(BUILD_DIR): powershell mkdir $@ diff --git a/Tools/structcheck.ps1 b/Tools/structcheck.ps1 new file mode 100644 index 0000000..6b449c6 --- /dev/null +++ b/Tools/structcheck.ps1 @@ -0,0 +1,202 @@ +# Structural verification (NOT part of the shipped app; under .preflight/, gitignored). +# +# Proves on disk -- not by prose -- that the two structural requirements are satisfied: +# [1] The giant implementation files were split by responsibility into sibling files +# under each component's own Src/ (and each new translation unit is compiled). +# [2] The common UI controls live in a real `Components/Shared` component, are compiled +# into the binary, and are consumed by the other components. +# +# Run: powershell -File .preflight\perf\structcheck.ps1 +# ASCII only (PowerShell 5.1 reads .ps1 as ANSI). + +$ErrorActionPreference = "Stop" +$root = Split-Path -Parent $PSScriptRoot +Set-Location $root + +$pass = 0 +$fail = 0 + +function Check([bool]$ok, [string]$what) { + if ($ok) { $script:pass++ ; Write-Host (" OK " + $what) } + else { $script:fail++ ; Write-Host (" FAIL " + $what) } +} + +function Lines([string]$path) { + if (-not (Test-Path $path)) { return -1 } + return (Get-Content $path | Measure-Object -Line).Lines +} + +Write-Host "== [1] split of the giant implementation files ==" + +# 1a. every expected file of the split exists and is a sibling under the component's Src/ +$split = @( + "Components/PlotView/Src/PlotView.cpp", + "Components/PlotView/Src/PlotViewPaint.cpp", + "Components/PlotView/Src/PlotViewInteraction.cpp", + "Components/PlotView/Src/PlotViewFeatures.cpp", + "Components/PlotView/Src/PlotViewInternal.hpp", + "Components/SignalPanel/Src/SignalPanel.cpp", + "Components/SignalPanel/Src/SignalPanelPaint.cpp", + "Components/SignalPanel/Src/SignalPanelInteraction.cpp", + "Components/SignalPanel/Src/SignalPanelInternal.hpp", + "Components/Application/Src/Application.cpp", + "Components/Application/Src/ApplicationCommands.cpp", + "Components/Application/Src/ApplicationLoading.cpp", + "Components/Application/Src/ApplicationInternal.hpp", + "Utilities/Src/DataModel.cpp", + "Utilities/Src/DataModelTab.cpp", + "Utilities/Src/DataModelInternal.hpp" +) +foreach ($f in $split) { + $n = Lines $f + Check ($n -gt 0) ("exists + non-empty: " + $f + " (" + $n + " lines)") +} + +# 1b. the core file of each split is a small fraction of the original monster file +Check ((Lines "Components/PlotView/Src/PlotView.cpp") -lt 900) "PlotView.cpp shrunk to < 900 lines (was 13 695)" +Check ((Lines "Components/PlotView/Src/PlotViewPaint.cpp") -gt 4000) "PlotViewPaint.cpp carries the rendering (> 4000 lines)" +Check ((Lines "Components/SignalPanel/Src/SignalPanel.cpp") -lt 1200) "SignalPanel.cpp shrunk to < 1200 lines (was 4 184)" +Check ((Lines "Components/Application/Src/Application.cpp") -lt 1500) "Application.cpp shrunk to < 1500 lines (was 3 135)" +Check ((Lines "Utilities/Src/DataModel.cpp") -lt 1600) "DataModel.cpp shrunk to < 1600 lines (was 2 615)" + +# 1c. responsibility really moved: each concern is defined in exactly one file +$concerns = @( + @{ symbol = "PlotView::Paint"; file = "PlotViewPaint.cpp" }, + @{ symbol = "PlotView::PaintLane"; file = "PlotViewPaint.cpp" }, + @{ symbol = "PlotView::PaintHint"; file = "PlotViewPaint.cpp" }, + @{ symbol = "PlotView::LaneAtY"; file = "PlotViewInteraction.cpp" }, + @{ symbol = "PlotView::CancelDrag"; file = "PlotViewInteraction.cpp" }, + @{ symbol = "PlotView::FitAll"; file = "PlotViewFeatures.cpp" }, + @{ symbol = "PlotView::SetHint"; file = "PlotViewFeatures.cpp" }, + @{ symbol = "PlotView::Layout"; file = "PlotView.cpp" }, + @{ symbol = "PlotView::BuildLaneVisuals"; file = "PlotView.cpp" }, + @{ symbol = "SignalPanel::Paint"; file = "SignalPanelPaint.cpp" }, + @{ symbol = "SignalPanel::OnLButtonDown"; file = "SignalPanelInteraction.cpp" }, + @{ symbol = "SignalPanel::EnsureRows"; file = "SignalPanel.cpp" } +) +foreach ($c in $concerns) { + $hits = @(Select-String -Path "Components/*/Src/*.cpp", "Utilities/Src/*.cpp" -Pattern ("^\s*\w[\w:<>, ]* " + [regex]::Escape($c.symbol) + "\(") | + ForEach-Object { Split-Path $_.Path -Leaf }) + $ok = ($hits.Count -eq 1) -and ($hits[0] -eq $c.file) + Check $ok ($c.symbol + " defined only in " + $c.file + " (found: " + ($hits -join ",") + ")") +} + +# 1d. the constant/tool block of each big file became a named internal header + using-declaration +$internal = @( + @{ file = "Components/PlotView/Src/PlotViewInternal.hpp"; ns = "pvdetail" }, + @{ file = "Components/SignalPanel/Src/SignalPanelInternal.hpp"; ns = "spdetail" }, + @{ file = "Components/Application/Src/ApplicationInternal.hpp"; ns = "appdetail" }, + @{ file = "Utilities/Src/DataModelInternal.hpp"; ns = "dmdetail" } +) +foreach ($i in $internal) { + $text = Get-Content $i.file -Raw + $hasNs = $text -match ("namespace\s+" + $i.ns) + $hasUsing = $text -match ("using namespace\s+" + $i.ns + "\s*;") + $inlineCount = ([regex]::Matches($text, "(?m)^\s*(?:\[\[[^\]]*\]\]\s*)?inline\s")).Count + Check $hasNs ($i.file + " declares namespace " + $i.ns) + Check $hasUsing ($i.file + " injects it via the using-declaration for " + $i.ns) + Check ($inlineCount -ge 4) ($i.file + " marks the shared helpers inline (" + $inlineCount + " definitions)") +} + +# 1e. the split files are compiled, and their objects hold the moved code +$objs = @("PlotView.o","PlotViewPaint.o","PlotViewInteraction.o","PlotViewFeatures.o", + "SignalPanel.o","SignalPanelPaint.o","SignalPanelInteraction.o", + "Application.o","ApplicationCommands.o","ApplicationLoading.o", + "DataModel.o","DataModelTab.o") +$missing = @() +foreach ($o in $objs) { if (-not (Test-Path ("Build/Release/" + $o))) { $missing += $o } } +Check ($missing.Count -eq 0) ("all split translation units were compiled (missing: " + ($missing -join ",") + ")") + +$nm = Get-Command nm -ErrorAction SilentlyContinue +if ($nm -ne $null) { + $paintInPaint = @(nm --defined-only -C "Build/Release/PlotViewPaint.o" 2>$null | Select-String "jdi::PlotView::Paint") + $paintInCore = @(nm --defined-only -C "Build/Release/PlotView.o" 2>$null | Select-String "jdi::PlotView::Paint") + Check ($paintInPaint.Count -gt 0) "nm: paint symbols live in PlotViewPaint.o" + Check ($paintInCore.Count -eq 0) "nm: no paint symbol left in PlotView.o" +} else { + Check $false "nm not available (cannot verify symbol placement)" +} + +# 1f. every implementation file of every component is registered in the Makefile +$srcFiles = @(Get-ChildItem "Components/*/Src/*.cpp", "Utilities/Src/*.cpp" | ForEach-Object { + $_.FullName.Substring($root.Length + 1).Replace("\", "/") +}) +$makefile = Get-Content "Makefile" -Raw +$unregistered = @() +foreach ($s in $srcFiles) { + $dir = $s.Substring(0, $s.LastIndexOf("/")) + if ($makefile -notmatch [regex]::Escape($dir)) { $unregistered += $s } +} +Check ($unregistered.Count -eq 0) ("every implementation directory is registered in the Makefile (missing: " + ($unregistered -join ",") + ")") +Check ($srcFiles.Count -ge 24) ("implementation files in the tree: " + $srcFiles.Count) + +Write-Host "" +Write-Host "== [2] Components/Shared ==" + +Check (Test-Path "Components/Shared/Inc/UiControls.hpp") "Components/Shared/Inc/UiControls.hpp exists" +Check (Test-Path "Components/Shared/Src/UiControls.cpp") "Components/Shared/Src/UiControls.cpp exists" +Check ((Lines "Components/Shared/Inc/UiControls.hpp") -gt 100) ("UiControls.hpp declares the control API (" + (Lines "Components/Shared/Inc/UiControls.hpp") + " lines)") + +# 2a. the shared header really owns the enumerated control families +$families = @("UiDrawPlusIcon","UiDrawCrossIcon","UiDrawTriangleIcon","UiDrawCheckBox", + "UiDrawSlider","UiDrawEditField","UiEditFieldMouseDown","UiDrawSwatch", + "UiSwatchNeedsOutline","UiContrastInk","UiSliderRatioFromX") +$header = Get-Content "Components/Shared/Inc/UiControls.hpp" -Raw +$missingFamilies = @() +foreach ($f in $families) { if ($header -notmatch ("\b" + $f + "\b")) { $missingFamilies += $f } } +Check ($missingFamilies.Count -eq 0) ("shared header exposes all control families (missing: " + ($missingFamilies -join ",") + ")") + +# 2b. symbol-level proof: every declared control is DEFINED by UiControls.o and by no other object. +# (Text matching cannot tell a definition from a call, so ask the linker's view instead.) +$declared = @([regex]::Matches($header, "(?m)^\s*[\w:<>, ]+\s+(Ui\w+)\s*\(") | ForEach-Object { $_.Groups[1].Value } | Sort-Object -Unique) +Check ($declared.Count -ge 15) ("UiControls.hpp declares " + $declared.Count + " control functions") + +$definitionOwner = @{} +$objectFiles = @(Get-ChildItem "Build/Release/*.o" -ErrorAction SilentlyContinue) +if ($nm -eq $null) { + Check $false "nm not available (cannot verify control definitions)" +} else { + foreach ($o in $objectFiles) { + $syms = nm --defined-only -C $o.FullName 2>$null + foreach ($name in $declared) { + if ($syms | Select-String (" T jdi::" + [regex]::Escape($name) + "\(")) { + $definitionOwner[$name] = $o.Name + } + } + } + $wrongOwner = @($declared | Where-Object { -not $definitionOwner.ContainsKey($_) -or $definitionOwner[$_] -ne "UiControls.o" }) + Check ($wrongOwner.Count -eq 0) ("nm: all " + $declared.Count + " declared controls are defined in UiControls.o (wrong/undefined: " + ($wrongOwner -join ",") + ")") + $uiControlsSymbols = @(nm --defined-only -C "Build/Release/UiControls.o" 2>$null | Select-String " T jdi::Ui").Count + Check ($uiControlsSymbols -ge 15) ("nm: UiControls.o defines " + $uiControlsSymbols + " Ui* symbols") +} + +# 2c. other components actually consume them (>= 3 components, >= 30 call sites) +$consumers = @(Select-String -Path "Components/*/Src/*.cpp", "Components/*/Inc/*.hpp" -Pattern "UiDrawPlusIcon|UiDrawCrossIcon|UiDrawTriangleIcon|UiDrawCheckBox|UiDrawSlider|UiDrawEditField|UiEditFieldMouseDown|UiEditFieldMouseMove|UiDrawSwatch|UiSwatchNeedsOutline|UiContrastInk|UiSliderRatioFromX|UiEditTextLeft|UiEditOffsetAt" | + Where-Object { (Split-Path $_.Path -Leaf) -ne "UiControls.hpp" } | + Group-Object { ($_.Path.Substring($root.Length + 1) -split "[\\/]")[1] }) +$componentNames = @($consumers | ForEach-Object { $_.Name }) +$totalCalls = 0 +foreach ($g in $consumers) { $totalCalls += $g.Count } +Check ($componentNames.Count -ge 3) ("shared controls consumed by >= 3 components (" + ($componentNames -join ",") + ")") +Check ($totalCalls -ge 30) ("shared control call sites across components: " + $totalCalls) + +# 2d. layering, stated exactly as the code obeys it: +# Components/Shared -> Utilities only (it must not include any component header) +# Utilities/PaintUtil = base layer (must not include UiControls.hpp) +# Utilities must not include feature components (PlotView / SignalPanel / Toolbar / TabBar) +# NOTE: Utilities/Src/PopupMenu.cpp and Utilities/Src/ColorPickerPopup.cpp are hand-drawn UI +# widgets that live in the Utilities folder historically; they consume the shared controls +# (same direction as the components do) and are deliberately excluded from the ban below. +$sharedIncludes = @(Select-String -Path "Components/Shared/Inc/*.hpp", "Components/Shared/Src/*.cpp" -Pattern '^#include "' | + Where-Object { $_.Line -match '"(Application|PlotView|SignalPanel|Toolbar|TabBar|PopupMenu|ColorPickerPopup)\.hpp"' }) +Check ($sharedIncludes.Count -eq 0) "Components/Shared depends on Utilities only (no component header)" +$paintUtilIncludes = @(Select-String -Path "Utilities/Inc/PaintUtil.hpp", "Utilities/Src/PaintUtil.cpp" -Pattern '#include "UiControls.hpp"' -ErrorAction SilentlyContinue) +Check ($paintUtilIncludes.Count -eq 0) "Utilities/PaintUtil (base layer) does not depend on Components/Shared" +$badDir = @(Select-String -Path "Utilities/Src/*.cpp", "Utilities/Inc/*.hpp" -Pattern '#include "(PlotView|SignalPanel|Toolbar|TabBar)' -ErrorAction SilentlyContinue) +Check ($badDir.Count -eq 0) "Utilities never includes a feature component" + +Write-Host "" +Write-Host ("structcheck: " + $pass + " passed, " + $fail + " failed") +if ($fail -ne 0) { exit 1 } +exit 0 +