分组动画改「抽拉卷尺」式刚性平移 + 悬浮组名折起一边滚动一边收起 + 筛选态折叠三角修正 + 样式区「目标」行绘制/布局修正

① 分组折起/展开动画重做为「抽拉卷尺」式刚性平移(推翻上一轮的“按行缩放”):
   - 用户要求:折起时所有信号一起向上平移、逐条消失在组名栏里;展开时逐条从组名栏
     向下冒出;卷尺上的元素相对位置不变。
   - 旧实现按行高缩放 ⇒ 元素被压扁(相对位置改变),且每帧行高都在变、每帧都要重建
     行集合。
   - 新实现:整组作为一个刚性整体平移,只维护一个逐行平移量 m_rowShift;**行高永不改变**,
     平移只作用于“组名行之后”的行(组名是卷尺外壳,始终不动)。
     收起 = shift 从 fromShift 推到 blockHeight(全部藏进组名栏);展开 = 推到 0。
     ease-out cubic;时长 = clamp(115 + 6×行数, 150, 320) ms。
   - 动画期间**不重建行集合**(AdvanceGroupAnimations 不再设 m_rowsDirty)——这是流畅的关键。
   - 中途反向点击(折叠没播完就点展开)从**当前位移续接**(GroupAnim::fromShift),不再跳变。
   - m_contentHeight = 自然高度 − Σ位移;RowRect() 减去本行位移;
     HitRow() 改为逐行比较实际绘制位置(动画中行位置与内容坐标不再一一对应)。

② “藏进组名栏”的裁剪(新增 AnimationHideTopForRow / ClampRowClipTop):
   - 信号行越过组名**绘制位置**的下边界即被裁掉;已经完全越过的行整行跳过。
   - 除 GDI 裁剪(PaintRowText / FillRowRect 用的 m_rowClipRect)外,还必须用
     g.SetClip(clip, CombineModeIntersect) 收紧 GDI+ 裁剪 —— 行内的色块 / 复选框 /
     网格徽标走 GDI+、只受 SetClip 约束,不收紧就会画到组名栏上面去。

③ 悬浮组名折起改为“一边向上滚动一边收起”(推翻上一轮的“先滚动到位再折起”):
   - 用户要求:悬浮组名既然在最上方,就一边向上滚动一边收起,保证它始终在最上方;
     动画完成时滚动偏移恰好落在“组名正好在最上方”且信号已经收起。
   - 实现:GroupAnim::scrollSynced —— 滚动偏移与平移量按同一进度推进
     (m_scrollOffset = scrollFrom + (scrollTo − scrollFrom) × eased),二者合成让组名的
     屏幕位置恒定;目标 scrollTo = min(组名的内容偏移, 折起后可达的最大滚动量)
     (后者用于“折起后内容比列表还短”的情形,避免结束时被强行夹取 ⇒ 无跳变)。
   - 旧的 m_pendingGroupToggle 两段式机制与 StartScrollAnimationTo 的挂起参数已删除;
     用户滚轮 / 拖滚动条 ⇒ CancelScrollAnimation() 只解除同步,折叠/展开继续播放。

④ 筛选状态下折叠三角状态修正:
   - 根因:PaintFileRow 直接读 m_tab->IsCollapsed()(普通模式的折叠状态),而筛选模式用的是
     独立的 m_filterCollapsed ⇒ 筛选里折叠/展开时三角不变化。
   - 修法:改读统一的 IsGroupCollapsed()(按当前模式分派)。

⑤ 样式区「目标」行的绘制与布局修正:
   - 绘制:PaintTargetRow 误用列表裁剪版 PaintRowText(文本被裁到 m_listRect),而目标行在
     样式区、根本不在列表矩形内 ⇒ 信号名与 GX 标签文本被完全裁掉。改用不裁剪的 UiText,
     并在 PaintRowText 注释里写明“样式区不得使用”。
   - 布局:名称从内容列左边缘开始、GX 标签**右对齐到内容列右边缘**(与线宽滑轨/数值框同列宽
     同两端),两者不重叠;无 GX 标签时名称可用宽度 = 整个内容列(因此更宽,仍左对齐)。
   - 双击改名:EditFieldRect() 对目标行只返回 m_targetNameRect,不再返回内容列全宽
     (旧实现会把 GX 标签包进输入框)。

验证(全程离屏 SW_HIDE,不占用鼠标 / 键盘 / 屏幕;进程与线程均 BELOW_NORMAL):
   - .preflight/perf/panelanim_main.cpp 147/147 通过(连跑 3 次稳定):
     刚性平移(行高不变 / 间距恒定 / 组名行不动 / 可见行数单调减少 / 逐帧断言组名栏像素零变化 /
     中间帧无行集合重建 / 帧耗时)、中途反向切换续接不跳变、筛选态折起与项数、
     筛选态与普通态折叠三角逐像素 0 差异、悬浮组名命中(不穿透)、
     同步滚动(逐帧钉住 + 恰好落位 + 无跳变)、真实 MF4 单点信号两种自适应逐位一致、
     目标行文本像素存在 + 两端对齐 + 输入框不覆盖 GX 标签 + 无标签时名称更宽。
   - A/B 五组回退分别 4 / 6 / 6 / 2 / 1 项如实失败。
   - 既有 verify 49/49、datacheck 69/69、edgecheck 46/46;make clean && make all 零警告。

记忆:Documents/agent_mem.md 新增 §9.36、改写 §11.2 第 77–81 条并新增 82–84 条,
标注 §9.35 的“按行缩放动画”与“先滚动再折起”已被推翻。
This commit is contained in:
DeepSeek V4.1 Flash committed 2026-10-16 02:40:31 +08:00
1 parent 1d314ddc24
commit 5831a0ea0d
3 files changed
+547 -243

No files matched your search

+131 -19
View File
@@ -1946,6 +1946,10 @@ GDI+ 抗锯齿逐段扫描这些**近竖直、满幅**的边,代价爆炸(
### 9.35 分组折起/展开动画 + 筛选态可折起 + 悬浮组名可交互(含"先滚动再折起")+ 适应视图与 Y 轴自适应彻底同源(本轮,本机)
> ⚠️ **本节的「一」(按行缩放动画)与「三」里的“先滚动到位再折起”已被 §9.36 推翻/取代**
> (用户 2026-10-16 第二轮明确要求“抽拉卷尺”式整体平移,且悬浮组名折起改为**一边滚动一边收起**)。
> 仍成立的部分:二(筛选态折起 + 项数按筛选计数)、三(悬浮组名命中不得穿透)、四(自适应同源)。
> 本轮四项(用户 2026-10-16,本机 = 另一台设备)。离屏验证 `.preflight/perf/panelanim_main.cpp`
> **94/94 通过**(连跑 3 次稳定);**A/B** 三组回退分别如实失败(4 / 13 / 8 项);
> 既有 `verify.exe` **49/49**(顺带修正了 4 条与远端新规则不符的过期断言)、
@@ -2039,6 +2043,92 @@ GDI+ 抗锯齿逐段扫描这些**近竖直、满幅**的边,代价爆炸(
(§9.21 已烘入数据);拖拽换位改为断言**被拖网格自身边界**判据的新目标值。
⇒ `verify.exe` 现为 **49/49 全绿**。
### 9.36 动画改为「抽拉卷尺」式刚性平移 + 筛选态折叠三角修正 + 样式区「目标」行绘制/布局修正(本轮,本机)
> 本轮四项(用户 2026-10-16 第二轮,本机 = 另一台设备)。
> 离屏验证 `.preflight/perf/panelanim_main.cpp` **147/147 通过**(连跑 3 次稳定);
> **A/B** 五组回退分别如实失败(4 / 6 / 6 / 2 / 1 项);
> 既有 `verify` **49/49**、`datacheck` **69/69**、`edgecheck` **46/46**;`make clean && make all` 零警告。
#### 一、分组动画重做为「抽拉卷尺」式**刚性平移**(推翻 §9.35 一)
- **用户原话要点**:折起时所有信号一起向上平移、逐条消失在组名栏里;展开时逐条从组名栏向下冒出;
**卷尺上的元素相对位置不会变**;收起/展开要流畅(性能)。
- **旧实现错在哪**:按「行高×比例」逐行缩放 —— 元素被压扁(相对位置改变),
与“卷尺”完全不同;而且每帧行高都在变 ⇒ 每帧都要重建行集合(本可用性能)。
- **新实现**(`SignalPanel::UpdateAnimGeometry` + `m_rowShift`):
* 动画只产生**一个逐行平移量** `m_rowShift[row]`(内容坐标),**行高永不变**;
平移只作用于**组名行之后**的行 ⇒ 组名是“卷尺外壳”(始终不动),信号行从它下面滑过。
* 收起:`shift = fromShift + (blockHeight − fromShift) × eased(p)`(推到全部藏进组名栏);
展开:推到 0(逐条冒出)。`eased` = ease-out cubic;时长 `clamp(115 + 6×行数, 150, 320)` ms。
* **中途反向切换从当前位移续接**(`GroupAnim::fromShift`)⇒ 连点折叠三角不会跳变
(旧实现会跳回“干净的一端”)。
* `m_contentHeight = 自然高度 − Σ shift`;`RowRect()` 减去本行位移;
`HitRow()` 改为**逐行比较实际绘制位置**(动画中行位置与“内容坐标”不再一一对应)。
* ⚠️ **不得再改成“逐行缩放”**:方案的关键收益是**行高不变 ⇒ 动画期间无需重建行集合**
(探针直接断言中间帧 `m_rowsDirty == false`)。
- **“藏进组名栏”的裁剪**:信号行越过**组名绘制位置的下边界**即被裁掉(`AnimationHideTopForRow`)。
已经完全越过的行整行跳过(省绘制)。
⚠️⚠️ **除了 GDI 裁剪(`PaintRowText`/`FillRowRect` 用的 `m_rowClipRect`)外,必须同时用
`g.SetClip(clip, CombineModeIntersect)` 收紧 GDI+ 裁剪** —— 行内的色块/复选框/网格徽标走 GDI+,
只受 `SetClip` 约束;不收紧就会画到组名栏上面去(A/B 实测失败)。
- **实测**:3 行折叠 ≈ 4–5 帧、平均 **~10.8ms/帧**(离屏 harness 含整帧重绘)、最坏 ~13ms。
#### 二、悬浮组名折起:一遗滚动一遗收起(推翻 §9.35 三 的“先滚动到位再折起”)
- **用户要求**:悬浮组名折起时不再先上滚再播放动画;而是**一遗向上滚动一遗收起**,
保证悬浮组名始终在最上方;动画完成时滚动偏移恰好落在“组名正好在最上方”,且信号已经收起。
- **实现**:`GroupAnim::scrollSynced` —— 滚动偏移与平移量按**同一进度**推进
(`m_scrollOffset = scrollFrom + (scrollTo − scrollFrom) × eased`)。
数学上这让组名的屏幕位置**恒定**(只要起始越界量 Δ0 ≤ blockHeight,即“组名确实悬浮”的常态)。
目标 `scrollTo = min(组名的内容偏移, 折起后可达的最大滚动量)` —— 后者用于“折起后内容比列表还短”
的情况(此时组名在物理上无法停到最上方,取可达值以避免结束时被强行夹取 ⇒ **无跳变**)。
- 旧的 `m_pendingGroupToggle` / `PumpScroll` 两段式机制**已删除**;
用户滚轮 / 拖滚动条 ⇒ `CancelScrollAnimation()` 仅解除同步(折叠/展开继续播放)。
- **实测**:逐帧断言组名的**绘制位置**始终在列表上沿(<1.5px)、滚动单调上移、
结束时 `m_scrollOffset == 组名的内容偏移`(<1px),且随后 `ClampScroll()` 不再移动(无跳变)。
#### 三、筛选态折叠三角状态修正
- **用户报告**:筛选时分组行的小三角维持在了筛选前的状态,展开/折起都不变化。
- **根因**:`PaintFileRow` 直接读 `m_tab->IsCollapsed(...)`(普通模式的折叠状态),
而筛选模式用的是**独立**的 `m_filterCollapsed`(§9.35 二)⇒ 筛选里折叠时三角形不变。
- **修法**:改为统一的 `IsGroupCollapsed(groupIndex)`(按当前模式分派)。
- **验证**:离屏像素对比 —— 「筛选态折叠」与「普通态折叠」的组名行(含三角)**逐像素 0 差异**;
且筛选态折叠前后三角区域确实有变化(有判别力)。A/B 失败 2 项。
#### 四、样式区「目标」行:绘制与布局修正
- **用户报告**:目标行显示的信号名与 GX 标签都没有正确绘制;双击进入编辑时输入框把 GX 标签包了进去。
- **根因①②**:
* **没画出来**:`PaintTargetRow` 用了 `PaintRowText`,而它把文本裁到 `m_listRect`(列表矩形)——
目标行在**样式区**、根本不在列表矩形内 ⇒ 名称与 GX 文本被**完全裁掉**。
✓ 修法:目标行(以及样式区其它文本)一律用**不裁剪**的 `UiText`;
并在 `PaintRowText` 的注释里写明“**样式区不得使用**”。
* **输入框包住 GX 标签**:`EditFieldRect()` 对 `m_editRow < 0` 返回“内容列全宽”,
而 GX 标签在列内(旧实现是紧跟名称)⇒ 输入框盖住标签。
✓ 修法:返回 `m_targetNameRect`(只管名称那一块),并断言“输入框与名称矩形逐值相等”。
- **布局对齐**(用户要求:“同时出现时左右宽度与其它控件对齐;不显示 GX 标签时名称可以更宽”):
* 名称从**内容列左边缘**开始(与线宽滑轨/数值框同一左边界);
* GX 标签**右对齐到内容列右边缘**(而不是紧跟在名称后面);
* 名称可用宽度 = 到 GX 标签左侧;**无标签时 = 整个内容列**(因此更宽,但仍左对齐)。
- **验证**:断言名称左缘 == `m_widthTrackRect.X`、GX 右缘 == `m_widthValueRect` 右缘、
两者不重叠;并用 `PixelsNear` 断言名称与 GX 的**文本像素确实存在**(旧实现下名称为 0 像素);
无 GX 标签时名称更宽且仍两端对齐。A/B 失败 6 项。
- ⚠️ 注意:`PaintTargetRow` 里的**悬停/点击/双击**三种交互仍要在绘制前重建几何
(`m_targetRowRect` 只依赖布局,而 `m_targetNameRect/m_targetBadgeRect` 依赖名称与网格归属)。
#### 五、本轮探针(`.preflight/perf/panelanim_main.cpp`,不入库)
- 新增/重写的断言组:`[A]` 刚性平移(行高不变、间距恒定、组名行不动、可见行数单调减少、
**逐帧断言组名栏未被覆盖**、无行集合重建、帧耗时)、`[A3]` 中途反向切换续接不跳变、
`[B]` 筛选态折起与计数、`[B2]` 筛选/普通态折叠三角逐像素一致、`[C]` 悬浮组名命中、
`[D]/[D2]` 同步滚动(逐帧钉住 + 恰好落位 + 无跳变)、`[E]` 自适应同源、`[F]` 目标行绘制/布局/输入框。
- ⚠️ **教训(已写入 §11.3)**:探针里直接 `app.Render()` 不会重绘面板(`m_chromeDirty` 已被上次清掉)
⇒ 必须先 `app.m_chromeDirty = true`(`RenderFrame()`)。
⚠️ 动画循环里**最后一帧**会移除行集合并把位移归零,断言必须在
`if (!GroupAnimationActive()) break;` **之后**再继续,否则会把“正常收尾”误判为“位移倒退”。
---
## 10. 变更日志(本文件维护记录)
@@ -2089,6 +2179,7 @@ GDI+ 抗锯齿逐段扫描这些**近竖直、满幅**的边,代价爆炸(
| 2026-10-15 | DeepSeek-V4.1-Flash(**起源设备**) | **「适应视图」边界规则撤回重做:单值散点让出半径 + 单点簇视图(详见 §9.33)**。① **初次自适应没有发生**(用户报告):`FitAll()` 在**没有任何可见信号**时也把 `ViewInitialized()` 置 true ⇒ 随后勾选第一个信号时 `SynchronizeView` 认为已初始化、**跳过自适应**(实测 MF4 视界停在 42.386s,数据点被推到右边缘外);修法:只有存在可见信号才置位,并让初次自适应与工具栏「适应视图」**共用 `FitAll()` 一条路径**。② **单值散点被裁掉半个**(用户报告):旧规则"左右各留一小格"在"单值 + 连续信号"下既不准也不够(`WIN_Motor_StallCurrentVolBrk` 只有 1 个有效点,正好落在边界上);按用户要求**撤回旧策略**(删除 `FitSinglePointLanes`/`TimeGridCellSeconds`/`LaneValueGridCell`/`FitSinglePointSignal`),改为:**任何一条边界被单值散点顶住就让出该散点的半径(+1px)**,只在散点**会被裁切**时才扩展(内部散点不动视界),边界不是单值散点时边界就落在信号点上。③ **特例**:全部可见数据点都是单值信号且时刻相同 ⇒ 时间轴以该时刻居中、缩放到**一大格 = 1s**(视界 10s,`NiceStep(10,10)==1.0`),每个网格的幅值区间保证其数据点都能完整画出(退化成单值时跨度 1 居中)。验证:离屏 `.preflight/check` **62/62 通过**(连跑 3 次稳定;含**真实文件** `TestData/MF4-Record.MF4`:`WIN_Motor_StallCurrentVolBrk`(1 点) + `m_MOT_Current_DCom[0]`(8475 点) 复现用户场景,散点圆心离绘图区左边界 16.5px、半径 15px ⇒ 完整可见);**A/B** 回退四处规则后 **20 项如实失败**(含"散点正好压在绘图区右边缘"、"初次视界 42.386s");`make release` 零警告、链接通过。 |
| 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` 零警告。 |
---
@@ -2394,34 +2485,53 @@ GDI+ 抗锯齿逐段扫描这些**近竖直、满幅**的边,代价爆炸(
(否则「适应视图」与「Y 轴自适应」会互相改变对方的结果)。
⚠️ 且 **`FitAll` 绝不可写 `lane.fitYToSelection`**:用户开着「Y 轴自适应」时点一下
「适应视图」会把它悄悄关掉(用户 2026-10-16 报告)。
77. ⚠️ **信号面板「分组折起/展开」必须是动画过渡**(用户 2026-10-16 要求):
被折起的信号**向上收起**、下方分组连续上浮;**离组名越远的成员收起越快**
(`GroupRowScale`:第 ordinal 行速度倍率 = `1 + kGroupAnimDepthBoost × ordinal/(行数−1)`,
配合 ease-out cubic)⇒ 最深一行最先收尽;总时长 = `clamp(115 + 14×行数, 140, 300)` ms。
实现要点:`m_groupAnims`(可按分组并存)+ `AdvanceGroupAnimations()`(宿主 16ms 定时器
与滚动动画共用 `kScrollTimerId`,⚠️ `Application::Render` 的判定要同时看
`ScrollAnimationActive() || GroupAnimationActive()`);`BuildRows` 按比例给出
**非整行高**;低于 `kGroupAnimMinRowHeight`(0.75 逻辑 px)的行不再产生(不绘制也不命中)。
⚠️ 同一分组在动画中途被再次切换会从"干净的一端"重新开始(会有一次视觉跳变)—— 属已知取舍。
78. ⚠️ **折起动画期间行内容必须裁剪到本行矩形**(`SetRowClip`/`ClearRowClip`,用于
`PaintRowText` 与 `FillRowRect`):行高连续缩小时,`UiText` 是 `DT_VCENTER` 垂直居中,
不裁剪就会把文字溢出到相邻行上。
77. ⚠️ **信号面板「分组折起/展开」必须是「**抽拉卷尺**」式动画**(用户 2026-10-16 第二轮,
推翻第一轮的“按行缩放”):整组信号作为一个**刚性整体**平移 —— 收起时所有信号一起向上移动、
逐条消失(被组名栏吞掉),展开时逐条从组名栏下方冒出;**卷尺上的元素相对位置不变**
⇒ **行高绝不改变**,只维护一个逐行平移量 `m_rowShift`(`UpdateAnimGeometry`)。
收起:`shift = fromShift + (blockHeight − fromShift) × easeOutCubic(p)`;展开:推到 0;
时长 = `clamp(115 + 6×行数, 150, 320)` ms。
⚠️ **动画期间不得重建行集合**(`AdvanceGroupAnimations` 不能设 `m_rowsDirty`)——
这正是该方案流畅的关键(探针直接断言中间帧 `m_rowsDirty == false`)。
⚠️ 中途反向点击(折叠没播完就点展开)必须**从当前位移续接**(`GroupAnim::fromShift`),不许跳变。
78. ⚠️ **平移只作用于“组名行之后”的行,并且必须双向裁剪**:组名是“卷尺外壳”始终不动。
信号行越过**组名绘制位置的下边界**就要被裁掉(`AnimationHideTopForRow` + `ClampRowClipTop`),
已经完全越过的行整行跳过。
⚠️⚠️ 除了 GDI 裁剪(`PaintRowText`/`FillRowRect` 用的 `m_rowClipRect`),**必须同时用
`g.SetClip(clip, CombineModeIntersect)` 收紧 GDI+ 裁剪** —— 行内的色块 / 复选框 / 网格徽标
走 GDI+,只受 `SetClip` 约束,不收紧就会画到组名栏上面(A/B 实测会失败)。
(探针逐帧断言“组名栏区域像素零变化”。)
79. ⚠️ **筛选模式的分组折起状态与普通模式**分开**保存**(`m_filterCollapsed`,默认展开):
筛选只是临时子集视图,不该污染普通列表的布局选择;筛选文本变化 / 清空筛选 / 数据变化时
一律清空它以及进行中的折起动画(行数与分组可见性都变了)。
组名后方的项数必须按当前模式显示:普通 = 信号总数,筛选 = 该组内**符合筛选条件**的个数
(`ListRow::displayCount`,在 `BuildRows` 里填好;`PaintFileRow` 只读它)。
⚠️ 推论:**所有“读取折起状态”的地方都必须用 `IsGroupCollapsed()`(按模式分派)**,
不能直接读 `m_tab->IsCollapsed()` —— 否则筛选态折叠时**折叠三角不会变**
(用户 2026-10-16 报告;已用“筛选态 vs 普通态折叠后组名行逐像素 0 差异”锁定)。
80. ⚠️ **悬浮(sticky)组名的命中判定必须最优先**:`HitRow` 要先用
`StickyGroupRowIndex()` + `StickyGroupRowTop()` 判是否落在悬浮行矩形内,命中则临时打开
`m_rowYOverrideActive` 并用 `RowHitPart()` 算行内区域 —— 否则点击会**穿透到它下面的那一行**
(用户 2026-10-16 报告:悬浮组名的折叠三角/复选框/✕/名称全都点不动)。
⚠️ `HitRow` 同时要跳过**高度为 0** 的行(折起动画中已收尽),否则空行会白白吸走一次点击。
81. ⚠️ **点击悬浮组名折起时,必须先平滑滚动到"组名完整显示在列表顶部"再播放折起动画**
(用户 2026-10-16 要求):`ToggleGroupFromRow()` 判 `m_scrollOffset > 组行内容偏移`
(= 正悬浮)时调 `StartScrollAnimationTo(组行内容偏移, groupIndex)`,滚动完成后由
`AdvanceScrollAnimation` 的结束分支取出 `m_pendingGroupToggle` 启动折起动画。
⚠️ 用户滚轮 / 拖滚动条 / 筛选变化 / 切标签页都必须**清掉挂起请求**(`CancelScrollAnimation` /
`ApplyFilterText` / `ClearFilter` / `InvalidateRows` / `SetContext`)。
⚠️ `HitRow` 必须**逐行比较实际绘制位置**(`m_rowShift` 参与),不能用“滚动偏移→内容坐标”
的一刀切映射;并且要跳过完全藏进组名栏的行(否则空行会白白吸走一次点击)。
81. ⚠️ **悬浮组名折起:一边向上滚动一边收起**(用户 2026-10-16 第二轮,**推翻** §9.35 三的
“先滚动到位再折起”):`GroupAnim::scrollSynced` 让滚动偏移与平移量按**同一进度**推进
(`m_scrollOffset = scrollFrom + (scrollTo − scrollFrom) × eased`),
从而**组名的屏幕位置恒定**(悬浮组名始终在最上方)。
目标 `scrollTo = min(组名的内容偏移, 折起后可达的最大滚动量)`(后者处理“折起后内容比列表短”)。
⚠️ `m_pendingGroupToggle` 两段式机制**已删除,不要再恢复**;
用户滚轮 / 拖滚动条 ⇒ `CancelScrollAnimation()` **只解除同步**,折叠/展开继续播放。
82. ⚠️ **`PaintRowText` 只能用于“列表行”**(它把文本裁到 `m_listRect`,见第 53 条)。
样式区(如「目标」行)里的文本**必须用不裁剪的 `UiText`** —— 否则文本会被完全裁掉
(用户 2026-10-16 报告:“目标”行的信号名与 GX 标签都没有正确绘制;A/B 实测该区域文本像素为 0)。
83. ⚠️ **样式区「目标」行的布局**(用户 2026-10-16 要求):名称从**内容列左边缘**开始、
GX 标签**右对齐到内容列右边缘**(与线宽滑轨/数值框同列宽同两端),两者不得重叠;
**无 GX 标签时名称可用宽度 = 整个内容列**(因此更宽,仍左对齐)。
⚠️ `EditFieldRect()` 对 `m_editRow < 0` 必须返回 `m_targetNameRect`(**只管名称那一块**),
不能返回内容列全宽 —— 否则改名输入框会把 GX 标签包进去(用户报告)。
84. ⚠️ **筛选态的折叠三角必须用 `IsGroupCollapsed()`**(见第 79 条推论):`PaintFileRow` 直接读
`m_tab->IsCollapsed()` 时,筛选模式里折叠/展开三角**不会变化**(用户 2026-10-16 报告)。
71. ⚠️ **`FitAll()` 在没有任何可见信号时不得置位 `Tab().ViewInitialized()`**:
否则用户先连点几次「适应视图」(此时无信号被勾选)再勾选第一个信号,
`Application::SynchronizeView` 会**跳过初次自适应**,视界停在“无信号”那次的整段范围上,
@@ -2452,6 +2562,8 @@ GDI+ 抗锯齿逐段扫描这些**近竖直、满幅**的边,代价爆炸(
(ease-out cubic;时长 = `clamp(距离×0.35ms, 130, 300)`;宿主 16ms 定时器驱动)。
向上滚动时顶部要**让出一个组行高度**(悬浮组名只可能是目标自己那个分组的组名,
因为分组在列表里连续排列)。用户滚轮 / 拖滚动条必须**立即取消**动画。
⚠️ 宿主定时器同时驱动「折起/展开动画」(见第 77 条):`Render` 的判定必须是
`ScrollAnimationActive() || GroupAnimationActive()`。
### 11.3 离屏验证方法学(本机偏好,强烈建议沿用)