信号面板分组折起动画 + 筛选态折起(项数按筛选计数)+ 悬浮组名可交互与“先滚动再折起” + 适应视图与 Y 轴自适应同源

① 分组折起/展开改为动画过渡(用户要求:被折起的信号向上收起、下方组随之上升、
   离组名越远的成员收起越快、末段减速、总时长合理):
   - 新增 SignalPanel::m_groupAnims + GroupRowScale():每行速度倍率
     1 + 1.2×ordinal/(行数−1)(越靠下越快收尽),叠加 ease-out cubic;
     时长 = clamp(115 + 14×行数, 140, 300) ms。
   - BuildRows 按比例输出非整行高;低于 0.75 逻辑 px 的行不再产生(不绘制/不命中)。
   - 宿主 WM_TIMER(kScrollTimerId) 同时驱动滚动与折起动画;Render 的定时器判定改为
     ScrollAnimationActive() || GroupAnimationActive()。
   - 新增 SetRowClip/ClearRowClip:动画期间行内容裁到本行矩形(否则 DT_VCENTER
     垂直居中的文本会溢出到相邻行)。

② 筛选状态下的分组折起 + 项数按筛选结果计数:
   - 筛选模式改用独立的 m_filterCollapsed(默认展开),不再“筛选时忽略折叠状态”,
     也不再把筛选里的临时折起污染到普通列表的布局选择。
   - ListRow::displayCount 记录当前模式下列出的信号数;组名后方「N 项」只读它
     (筛选时 = 该组内符合筛选条件的信号个数)。
   - 筛选文本变化 / 清空筛选 / 数据变化 ⇒ 清空临时折起状态 + 进行中的动画 + 挂起请求。

③ 悬浮组名可正常交互 + 点击时“先滚动就位再折起”:
   - 根因:HitRow 只按滚动偏移把指针映射到内容位置,而悬浮组名是画在列表顶部的覆盖层
     (其自然位置已滚出列表)⇒ 点击穿透到它下面那一行(折叠三角 / 复选框 / ✕ /
     名称区域全都点不动)。
   - 修法:HitRow 先用 StickyGroupRowIndex() / StickyGroupRowTop() 判定是否落在悬浮行
     矩形内,命中则临时打开 m_rowYOverrideActive 并调用新的 RowHitPart() 算行内区域;
     同时跳过高度为 0 的行(折起动画中已收尽,否则空行会白白吸走一次点击)。
   - 新增 ToggleGroupFromRow():若该行正悬浮(滚动偏移大于该行的内容偏移),先
     StartScrollAnimationTo(该行内容偏移, groupIndex) 并把挂起请求记在
     m_pendingGroupToggle,滚动到位后才启动折起动画;分组行的单击与双击折叠三角都走
     这个入口。滚轮 / 拖滚动条 / 筛选变化 / 切标签页一律清掉挂起请求。

④ 「适应视图」与「Y 轴自适应」彻底同源(单数据点信号):
   - 根因:FitSinglePointCluster 给每个网格另算一套幅值区间(“散点半径留白 + 跨度 1”),
     而 AutoRangeLaneToView 走 PadValueRange ⇒ 同一视界下两者结果不同;且它显式写
     lane.fitYToSelection = false ⇒ 点「适应视图」会把「Y 轴自适应」悄悄关掉。
   - 修法:单点簇只负责时间轴(以该时刻居中 + 一大格 1s),逐网格幅值轴与常规路径 /
     Y 轴自适应完全同源(fitYToSelection → AutoRangeLaneToView,否则 autoRange = true,
     两者之后都接 ApplySinglePointValueMargins),并且绝不改写 fitYToSelection。
   - 有意的行为变化:单点信号「适应视图」的幅值跨度由旧的 1 变为 2×max(1,|值|×0.1)
     (即与 PadValueRange 一致);§9.33 用例 [A] 里“幅值跨度 1”的期望已作废,
     散点仍在网格正中且完整可见(ApplySinglePointValueMargins 兜底)。

验证(离屏 SW_HIDE,不占用鼠标 / 键盘 / 屏幕;进程与线程均 BELOW_NORMAL):
   - 新增 .preflight/perf/panelanim_main.cpp(panelanim.exe)94/94 通过(连跑 3 次稳定):
     折起/展开的分帧过渡与内容高度单调收敛、越靠下的行越矮、末态精确无残留、
     筛选计数与独立折起状态、悬浮组名四个可交互区域全部命中且下层行仍可点击、
     点击后“先滚动到位再折起”、真实 MF4-Record.MF4 单点信号下两种自适应逐位一致且往返稳定。
   - A/B 三组回退(回退绘图区修复 / 禁用悬浮命中优先 / 动画时长置 0)分别 4 / 13 / 8 项如实失败。
   - 既有 verify 49/49、datacheck 69/69、edgecheck 46/46;make clean && make all 零警告。
     (顺带修正本机 verify 探针 4 条与远端新规则不符的过期断言:幅值留白改为调用唯一的
     PadValueRange、派生信号 offsetTime 断言 0(§9.21 已烘入数据)、拖拽换位按“被拖网格
     自身边界”判据的新目标值。)

记忆:Documents/agent_mem.md 新增 §9.35 与 §11.2 第 76–81 条,并更新第 70 条与 §9.33 备注。
This commit is contained in:
DeepSeek V4.1 Flash committed 2026-10-16 01:25:54 +08:00
1 parent cb9d3e4b8f
commit 1d314ddc24
5 files changed
+801 -161

No files matched your search

+141 -2
View File
@@ -1848,6 +1848,8 @@ GDI+ 抗锯齿逐段扫描这些**近竖直、满幅**的边,代价爆炸(
- `[A]` 无信号时连点 3 次"适应视图" ⇒ `ViewInitialized()` 仍为 false;再勾选单值信号 ⇒
初次自适应发生(时间跨度 **10**、以该点居中、幅值跨度 **1** 居中、散点落在绘图区正中)。
⚠️ 其中"幅值跨度 **1**"的期望**已被 §9.35 取代**:单点簇的幅值轴现在与常规路径/「Y 轴自适应」
完全同源(只由 `PadValueRange` 提供留白,退化时跨度 = `2×max(1,|值|×0.1)`)。
- `[B]` 时间边界:只有普通信号 ⇒ 视界**正好**等于数据范围;内部单值点 ⇒ 视界不变;
左/右边界处的单值点 ⇒ 边界外扩,且**外扩量 = 半径 + 1px**(实测 0.013408s),散点完整可见。
- `[C]` 幅值边界:单值点顶住某网格上边界 ⇒ 该网格上边界外扩、散点完整可见;
@@ -1942,6 +1944,103 @@ GDI+ 抗锯齿逐段扫描这些**近竖直、满幅**的边,代价爆炸(
---
### 9.35 分组折起/展开动画 + 筛选态可折起 + 悬浮组名可交互(含"先滚动再折起")+ 适应视图与 Y 轴自适应彻底同源(本轮,本机)
> 本轮四项(用户 2026-10-16,本机 = 另一台设备)。离屏验证 `.preflight/perf/panelanim_main.cpp`
> **94/94 通过**(连跑 3 次稳定);**A/B** 三组回退分别如实失败(4 / 13 / 8 项);
> 既有 `verify.exe` **49/49**(顺带修正了 4 条与远端新规则不符的过期断言)、
> `datacheck` **69/69**、`edgecheck` **46/46**;`make release` 零警告。
#### 一、分组折起 / 展开改为动画过渡(`SignalPanel::GroupAnim`)
- **需求**(用户):折起时被折起的信号**向上收起**、下方分组随之上升;
**被折起的组中最下方成员离得越远收起速度越快**,随后**慢慢减速**直到完成;
总时长要在合理范围。
- **实现**:新增 `m_groupAnims`(每分组一条,可并存)+ `GroupRowScale(group, ordinal, rowCount)`:
* 每行速度倍率 = `1 + kGroupAnimDepthBoost(1.2) × ordinal/(行数−1)`,`ordinal` = 该行在分组内
参与列出的序号(0 = 紧邻组名)⇒ **越靠下越快收尽**;
* 每行进度再过 **ease-out cubic** ⇒ 起步快、末段显著减速;
* 总时长 = `clamp(115 + 14×行数, 140, 300)` ms(3 行 ≈157ms;40 行 = 300ms)。
- **接入点**:`BuildRows()` 把每个信号行的高度乘上比例(不再是非 0 即满高);低于
`kGroupAnimMinRowHeight`(0.75 逻辑 px) 的行**不产生**;宿主 `WM_TIMER(kScrollTimerId)`
同时调用 `AdvanceScrollAnimation()` 与 `AdvanceGroupAnimations()`,
`Render` 的定时器判定改为 `ScrollAnimationActive() || GroupAnimationActive()`。
- **行内容裁剪**:新增 `SetRowClip/ClearRowClip`,`PaintRowText`/`FillRowRect` 在动画期间
裁到**本行矩形**(否则 `DT_VCENTER` 的文字会溢出到相邻行)。
- **实测**(`panelanim.exe`):折起 3 行 = 12 帧 / 169ms,内容高度**单调下降**、末态 = 只剩组行;
展开回到**逐像素级精确**的原始行高(无残留);中间帧可见"越靠下的行越矮"。
- **已知取舍**:同一分组在动画中途被再次切换(快速连点折叠三角)会从"干净的一端"重新开始
⇒ 会有一次视觉跳变(未做跨方向插值)。
#### 二、筛选态下分组也能折起 + 项数按筛选结果计数
- **需求**(用户):筛选状态下筛选结果要**能以组为单位折起**,且组名后方的项数显示为
该组内**符合筛选条件**的信号个数;动画与上一条相同。
- **实现**:
* 折起状态分模式保存:普通模式 = `TabConfig::IsCollapsed`(原有),筛选模式 = 新增
`m_filterCollapsed`(默认**展开**)。`IsGroupCollapsed()/SetGroupCollapsed()` 按当前模式分派。
⇒ 筛选里折起不会污染普通列表的布局,清空筛选即恢复。
* `BuildRows()` 不再"筛选时忽略折叠状态";`ListRow::displayCount` 记录当前模式下列出的
信号数,`PaintFileRow` 的「N 项」只读它。
* 筛选文本变化 / 清空筛选 / 数据变化 ⇒ 清空 `m_filterCollapsed` + `m_groupAnims` + 挂起请求。
- **实测**:筛选 `sig_b*` ⇒ 只列匹配组、项数 = 2、无匹配的组不出现;筛选里折起 A 组后
`TabConfig::IsCollapsed(A)` 保持 false;清空筛选后 A 组仍为展开且项数回到 3。
#### 三、悬浮组名可正常交互 + 点击时"先滚动就位再折起"
- **需求①②**(用户):组名悬浮在列表顶部时,它上面的所有元素都应能交互(此前点击会
**穿透到下层行**);且悬浮状态下点击组名折起时,滚动条要**先向上滚动到组名真正完整显示
的位置**,再播放折起动画(筛选状态下同样适用)。
- **根因**:`HitRow` 只按"滚动偏移把指针映射到内容位置"找行,而悬浮组名是**画在列表顶部**的
覆盖层(其自然位置已滚出列表)⇒ 点击它实际命中的是**它下面那一行**。
- **修法**:
* `HitRow` 先用 `StickyGroupRowIndex()` + `StickyGroupRowTop()` 判定是否落在悬浮行矩形内,
命中就临时打开 `m_rowYOverrideActive` 并通过新的 `RowHitPart()` 算行内区域(折叠三角 /
复选框 / ✕ / 名称全部正确);同时跳过**高度为 0** 的行。
* 新增 `ToggleGroupFromRow(group, row)`:若 `m_scrollOffset > 该行内容偏移`(= 正悬浮)⇒
`StartScrollAnimationTo(该行内容偏移, groupIndex)`(把挂起请求记在 `m_pendingGroupToggle`),
滚动到位后由 `AdvanceScrollAnimation` 的结束分支启动折起动画;否则直接切换并播放动画。
分组行的单击与"双击折叠三角"都走这个入口。
- **实测**:悬浮组名的四个可交互区域全部命中该组行(A/B 回退后 13 项如实失败);
点击组名后 `ScrollAnimationActive()==true` 且 `GroupAnimationActive()==false`(**先滚动**),
滚动结束后 `m_scrollOffset == 该行的内容偏移`(组名恰好落在列表顶部)、随后才开始折起动画与
折叠生效。
#### 四、「适应视图」与「Y 轴自适应」彻底同源(单数据点信号)
- **需求**(用户):以 `TestData/MF4-Record.MF4` 为例加载并只显示**一个单数据点信号**时,
点「适应视图」与「Y 轴自适应」得到的**范围不一致**;且若原本开着 Y 轴自适应,
点「适应视图」后 Y 视界会变、并且**「Y 轴自适应」被解除**。
- **根因**(都在 `PlotView::FitSinglePointCluster`,即"全部可见数据点都是单值且同刻"的特例路径):
* 它给每个网格另算了一套幅值区间("散点半径留白 + **跨度 1**"),而 `AutoRangeLaneToView`
走 `PadValueRange`(退化时跨度 = `2×max(1,|v|×0.1)`)⇒ 两者对**同一视界**结果不同;
* 它还显式写了 `lane.fitYToSelection = false` ⇒ 点「适应视图」就把 Y 轴自适应**关掉**了。
- **修法**:时间轴部分仍是"以该时刻居中 + 一大格 1s",但**逐网格幅值轴改为与常规路径、
与 Y 轴自适应完全同源**:`fitYToSelection` 的网格 → `AutoRangeLaneToView()`;否则 →
`autoRange = true`;两者之后都接 `ApplySinglePointValueMargins()`。**不再改写
`fitYToSelection`**。
- **实测**(真实 MF4,`WIN_Motor_StallCurrentVolBrk` 只有 1 个有效点):
适应视图的区间 == `PadValueRange(单点绘制值)`(逐位相同);打开 Y 轴自适应后区间**完全不变**;
再点适应视图后区间不变、`fitYToSelection` 仍为 true、时间视界逐位不变;
「适应视图 ↔ Y 轴自适应」反复往返**逐位稳定**。单点 + 连续信号同网格(常规路径)同样成立。
- **行为变化(有意)**:单点信号的「适应视图」幅值跨度从旧的 **1** 变为
`2×max(1,|值|×0.1)`(即与 `PadValueRange` 一致)。§9.33 用例 [A] 里"幅值跨度 1"的期望
**已作废**;散点仍在网格正中且完整可见(`ApplySinglePointValueMargins` 兜底)。
#### 五、本机探针修正(`.preflight`,不入库)
- 新增 `.preflight/perf/panelanim_main.cpp`(`panelanim.exe`):用真实 `Application`
(`SW_HIDE`)驱动 `SignalPanel` 做上述 [A]–[D],并用真实 `MF4-Record.MF4` 做 [E]。
- ⚠️ **教训**:直接调 `Application::Render()` **不会**重绘面板(`m_chromeDirty` 已在上一次
`Render` 里被清掉)⇒ 探针里必须 `app.m_chromeDirty = true` 再 Render(本轮的"动画模型"辅助函数
`RenderFrame()`),否则行高不会随动画刷新,"看起来动画没生效"。
- 顺带修正本机 `verify_main.cpp` 里 4 条与远端新规则不符的过期断言:
两处幅值留白改为调用 **`PadValueRange`**(唯一来源);派生信号 `offsetTime` 改为断言 **0**
(§9.21 已烘入数据);拖拽换位改为断言**被拖网格自身边界**判据的新目标值。
⇒ `verify.exe` 现为 **49/49 全绿**。
---
## 10. 变更日志(本文件维护记录)
| 日期 | 设备/模型 | 摘要 |
@@ -1989,6 +2088,7 @@ GDI+ 抗锯齿逐段扫描这些**近竖直、满幅**的边,代价爆炸(
| 2026-10-15 | DeepSeek-V4.1-Flash(**起源设备**) | **浮动面板不穿透 / 网格高亮改由信号列表触发 / 样式区「目标」行 / 联动平滑滚动(详见 §9.34)**。① **浮动面板上双击穿透到网格**:`PlotView::OnDoubleClick` 在所有下层命中都不成立时无条件 `FitAll()` ⇒ 在面板上双击会让网格莫名其妙自适应一次;修法:**最前面**用 `TopPanelAtPoint()` 判定并消费。② **网格变色高亮的触发来源重新定义**(旧行为"鼠标在网格里移动就高亮"**作废**):改为**信号列表 / 样式区「目标」行悬停到某条已绘制信号** ⇒ 该信号所在网格变色高亮**且该信号曲线本身也高亮**(新增 `m_externalHoverSignal`,与 `m_hoverSignal` 分开保存,由 `Render()` 每帧同步;新增 `EffectiveHoverLane()`)。③ **样式区「目标」行**(样式自定义区最上方):显示当前编辑的目标信号名称 + (已绘制时)名称**后方**的 GX 标签,悬停高亮并联动网格/曲线高亮,**单击不编辑、双击改名**。④ **联动平滑滚动**:绘图区选中信号(点图例 / 点曲线 / 双击图例)与样式区任何交互都触发 `ScrollToSignalAnimated()`(ease-out cubic、时长 `clamp(距离×0.35ms,130,300)`、**上行让出组行高度避免被悬浮组名遮挡**、滚轮/拖滚动条即取消)。验证:离屏 `.preflight/check` **73/73 通过**(连跑 3 次稳定;含浮动面板双击不移视界 vs 空白处双击必自适应的对照组、未勾选信号悬停不高亮、目标行 GX 标签位置与单击/双击区分、长列表 122 行平滑滚动与遮挡检查);**A/B** 回退六处实现 ⇒ **15 项如实失败**;`make release` 零警告、链接通过。 |
| 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` 零警告。 |
---
@@ -2281,8 +2381,47 @@ GDI+ 抗锯齿逐段扫描这些**近竖直、满幅**的边,代价爆炸(
常共用同一通道组(同一条轴),用轴判断会误判。
70. ⚠️ **特例:全部可见数据点都是单值信号且时刻完全相同**(`FitSinglePointCluster`):
时间轴**以该时刻居中**、缩放到 **一大格 = 1s**(视界 = `kTimeTickTarget × 1s` = 10s,
此时 `NiceStep(10,10) == 1.0`);每个网格的幅值区间以“能完整画出该网格内的全部数据点”
为准(退化成单值时以**跨度 1 居中**)。旧规则“跨度 1s 居中”“各留一小格”**已作废**。
此时 `NiceStep(10,10) == 1.0`);**幅值轴与其余场景完全同源**(见第 76 条):
`fitYToSelection` 的网格走 `AutoRangeLaneToView`、其余走整段自适应(`ComputeLaneRange`),
两者都只由 `PadValueRange` 提供留白 —— 退化(单值)时的区间就是
`[v − max(1,|v|×0.1), v + max(1,|v|×0.1)]`(跨度 = 2×留白)。
⚠️ 旧版曾在这里用"半径留白 + **跨度 1**"另算一套,已作废(会导致
「适应视图」与「Y 轴自适应」对同一视界算出**不同区间**)。
旧规则"跨度 1s 居中""各留一小格"同样已作废。
76. ⚠️ **`FitAll()` 的两条路径(常规 / 单点簇)必须共用同一段逐网格幅值逻辑**:
`fitYToSelection` 的网格 → `AutoRangeLaneToView()`;否则 → `autoRange = true`;
两者之后都接 `ApplySinglePointValueMargins()`。**不得**任何一条路径里另写一套区间
(否则「适应视图」与「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` 垂直居中,
不裁剪就会把文字溢出到相邻行上。
79. ⚠️ **筛选模式的分组折起状态与普通模式**分开**保存**(`m_filterCollapsed`,默认展开):
筛选只是临时子集视图,不该污染普通列表的布局选择;筛选文本变化 / 清空筛选 / 数据变化时
一律清空它以及进行中的折起动画(行数与分组可见性都变了)。
组名后方的项数必须按当前模式显示:普通 = 信号总数,筛选 = 该组内**符合筛选条件**的个数
(`ListRow::displayCount`,在 `BuildRows` 里填好;`PaintFileRow` 只读它)。
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`)。
71. ⚠️ **`FitAll()` 在没有任何可见信号时不得置位 `Tab().ViewInitialized()`**:
否则用户先连点几次「适应视图」(此时无信号被勾选)再勾选第一个信号,
`Application::SynchronizeView` 会**跳过初次自适应**,视界停在“无信号”那次的整段范围上,