适应视图:撤回旧策略并重做边界规则(单值散点让出半径 + 单点簇视图)

用户 2026-10-15 第五轮报告的两个问题与要求(详见 Documents/agent_mem.md §9.33):

1. 加载文件后不勾选任何信号、连点几次「适应视图」,再勾选一个单值信号时,
   初次自适应没有发生、数据点既不居中也不在网格内。
   根因:FitAll() 在没有任何可见信号时也把 Tab().ViewInitialized() 置为 true,
        随后勾选第一个信号时 Application::SynchronizeView 认为"已经初始化",
        于是跳过自适应,视界停在"无信号"那一次的整段范围上(实测 MF4 为 42.386s,
        数据点被推到绘图区右边缘之外)。
   修法:FitAll() 只有在存在可见信号时才置位 ViewInitialized();
        并让"初次自适应"与工具栏「适应视图」共用 FitAll() 这一条实现
        (不再先试 FitSinglePointSignal)。

2. 仅勾选 WIN_Motor_StallCurrentVolBrk(单值信号)与 m_MOT_Current_DCom[0](连续信号)
   时点「适应视图」,单值信号的点被挤到显示区域外只剩半个;上一轮"为左右空出一小格"
   的需求效果不佳。
   修法(按用户要求先撤回旧策略、再重做规则):
   * 撤回并删除 FitSinglePointLanes()(左右各留一小格)、TimeGridCellSeconds()、
     LaneValueGridCell()、FitSinglePointSignal()(首个单值信号的时间跨度 1s 视图)。
   * 新规则:常规全局适应后,任何一条边界(时间轴左/右、每个网格幅值上/下)
     若被一个单值信号的数据点顶住,就把该边界向外让出该散点的半径(+1 像素),
     使其完整显示在网格内;只在散点会被裁切时才扩展(内部散点使视界一点不动);
     边界不是单值信号的数据点时就落在该信号点上(时间轴不留额外余量)。
     判断依据用信号自身的数据点,而不是时间轴范围(MF4 里单值信号与连续信号
     常共用同一条时间轴,用轴的范围会误判)。
   * 特例:全部可见数据点都是单值信号且时刻完全相同时,时间轴以该时刻居中、
     缩放到一大格 = 1s(视界 = kTimeTickTarget × 1s = 10s,此时 NiceStep(10,10) 恰为 1.0),
     每个网格的幅值区间以"能完整画出该网格内的全部数据点"为准
     (退化成单值时以跨度 1 居中)。
   * 新增 FitContribution / CollectFitContributions / FitLaneHeight /
     ApplySinglePointValueMargins / FitSinglePointCluster 五个成员承载上述规则。

验证(全部离屏,不占用鼠标 / 键盘 / 屏幕):
- .preflight/check(SW_HIDE + 只注入合成消息 + 进程/线程 BELOW_NORMAL)**62/62 通过**,
  连跑 3 次稳定:[A] 初次自适应、[B] 时间边界(外扩量恰为半径 + 1px)、
  [C] 幅值边界、[D] 单点簇特例(跨度 10、一大格 1s、每个点都在网格内)、
  [E] 回归(普通信号视界 = 数据范围、网格保持自动自适应、§9.31 的
  "适应视图 ↔ Y 轴自适应互不干扰"仍逐位成立)。
- [F] 真实文件 TestData/MF4-Record.MF4:WIN_Motor_StallCurrentVolBrk 确实只有 1 个有效点
  (t = 0.005492),与 m_MOT_Current_DCom[0](8475 样点)复现用户场景 —— 点「适应视图」后
  散点圆心离绘图区左边界 16.5px、半径 15px,完整可见(修复前它正好压在边界上、只剩半个)。
- A/B 判别:回退四处规则后 **20 项如实失败**,失败信息即用户症状
  ("the dot is horizontally centred (got 1697, want 1084.75)"、
   "[F] initial view = one big cell per second (got 42.386313, want 10.000000)")。
- make release:全部源文件重新编译 **零警告**;链接用临时 `make release TARGET_FILE_NAME=Target_check` 通过。

同步记忆:Documents/agent_mem.md 新增 §9.33、§11.2 第 69–71 条硬约束(并把第 68 条与
§9.32 的第四小节显式标注为"已作废/被取代")、变更日志新行。
This commit is contained in:
DeepSeek V4.1 Flash committed 2026-10-15 18:35:11 +08:00
1 parent aee3a388b5
commit 73389f424c
4 files changed
+470 -282

No files matched your search

+96 -2
View File
@@ -1749,6 +1749,11 @@ GDI+ 抗锯齿逐段扫描这些**近竖直、满幅**的边,代价爆炸(
#### 四、多个网格、每个网格只有一个数据点时「适应视图」
> ⚠️⚠️ **本节规则(“左右各留一小格” + `FitSinglePointLanes()`)已被 §9.33 作废/取代**
> (用户 2026-10-15 第五轮:“首先应该对之前的策略做撤回,然后重新设计新的规则”)。
> 保留本节仅为历史脉络:现行规则见 **§9.33**(边界处的单值散点让出**半径**,
> 而不是“留一小格”)。
用户给出的规则(逐条落地在 `PlotView::FitSinglePointLanes()`):
* **时间轴**:左边界 = 所有网格中**最左**的数据点时间,再向左偏**一个网格小格**对应的时间跨度
@@ -1789,6 +1794,75 @@ GDI+ 抗锯齿逐段扫描这些**近竖直、满幅**的边,代价爆炸(
---
### 9.33 「适应视图」边界规则**撤回重做**:单值散点让出半径 + 单点簇视图(本轮,起源设备)
> 本轮(用户 2026-10-15 第五轮)。用户原话要点:
> ① 加载文件后**不勾选任何信号**时点几次「适应视图」,再勾选一个单值信号 —— 初次自适应**没有发生**
> (数据点既不居中也不在网格内);② 只勾选 `WIN_Motor_StallCurrentVolBrk`(单值)+ `m_MOT_Current_DCom[0]`
> (连续)时点「适应视图」,单值信号的点被挤到显示区域外**只剩半个**;
> ③ **撤撤回上一轮的策略**(§9.32 的"左右各留一小格"),重新设计边界规则。
> 离屏验证 `.preflight/check` **62/62 通过**(连跑 3 次稳定);**A/B** 回退四处规则后 **20 项如实失败**
> (含 `the dot is horizontally centred (got 1697, want 1084.75)` = 散点正好压在绘图区**右边缘**、
> `[F] initial view (got 42.386313, want 10)` = 初次自适应压根没发生)。
#### 一、撤回的旧策略(勿再用)
- 删除 `PlotView::FitSinglePointLanes()`("左右边界 = 最左/最右数据点 ± **一个网格小格**")。
- 删除 `PlotView::TimeGridCellSeconds()` / `PlotView::LaneValueGridCell()`(只为它服务)。
- 删除 `PlotView::FitSinglePointSignal()`("首个单值信号:时间跨度 1s + Y 跨度 1")——
初次自适应现在与工具栏「适应视图」**走同一条路径**(`FitAll()`),规则不会再出现两套。
#### 二、新的边界规则(`PlotView::FitAll()`)
1. 先做一次**常规**的全局适应:
* 时间 = `TimeBounds()`(可见信号的**时间轴**范围并集,与 `ComputeVisibleTimeBounds` 同源);
* 幅值 = `ComputeLaneRange()`(含唯一的留白实现 `PadValueRange`,即 **6%**)。
2. **单值散点让位**(上下左右四条边界统一规则):
* 若某条边界被一个**单值信号的数据点**(`finiteCount == 1`,画出来是一个圆点)顶住,
则把该边界再向外让出**该散点的半径**(`SinglePointDotRadius()`,再 +1 像素;
两个方向都迭代两遍,因为"半径换算成多少时间/数值"依赖当前跨度);
* **只在散点会被裁切时**才扩展(散点本来就在内部 ⇒ 视界**一点不动**);
* 边界不是单值散点时 ⇒ 边界就落在这个信号点上,**不留额外余量**
(时间轴本来就不留;幅值仍沿用 `PadValueRange` 的 6%,这是 §11.2 第 62 条确立的
"幅值留白只能有一个来源",本次**没有**改动它)。
* 判断依据是**信号自身的数据点**(不是轴),因此"单值信号与连续信号共用一条时间轴"
(MF4 同一通道组)时不会误判。
3. **特例:全部可见数据点都是单值信号、且时刻完全相同**(`FitSinglePointCluster()`):
* 时间轴以该时刻**居中**、缩放到 **一大格 = 1s**
(视界 = `kTimeTickTarget × 1s` = **10s**,此时 `NiceStep(10, 10)` 正好是 1.0
——用户要求的"一大格 1s 的级别");
* 每个网格的幅值区间以**能完整画出该网格内的全部数据点**为准
(取值不同时 + 两侧半径余量并迭代;退化成单值时以**跨度 1 居中**,
与"单值信号居中显示"的观感一致),并置 `autoRange = false`。
* 该分支不再复位 `ZoomMode()`(用户要求"点适应视图后仍保持已启用的缩放模式")。
#### 三、初次自适应"没有发生"的根因(用户报告 ①)
- 没有任何信号被勾选时 `FitAll()` 也把 `Tab().ViewInitialized()` 置成了 **true**,
于是随后勾选第一个信号时 `Application::SynchronizeView()` 认为"已经初始化过"⇒ **跳过自适应**,
视界停在"无信号"那次的整段范围上(实测 MF4:42.386s 的视界,数据点被推到右边缘外)。
- 修法:`FitAll()` **只有存在可见信号时**才置位 `ViewInitialized()`;
`Application::SynchronizeView` 也不再分两步(不再先试 `FitSinglePointSignal`),直接 `FitAll()`。
#### 四、验证(离屏 `.preflight/check`,`SW_HIDE` + 只注入合成消息 + `BELOW_NORMAL`)
- `[A]` 无信号时连点 3 次"适应视图" ⇒ `ViewInitialized()` 仍为 false;再勾选单值信号 ⇒
初次自适应发生(时间跨度 **10**、以该点居中、幅值跨度 **1** 居中、散点落在绘图区正中)。
- `[B]` 时间边界:只有普通信号 ⇒ 视界**正好**等于数据范围;内部单值点 ⇒ 视界不变;
左/右边界处的单值点 ⇒ 边界外扩,且**外扩量 = 半径 + 1px**(实测 0.013408s),散点完整可见。
- `[C]` 幅值边界:单值点顶住某网格上边界 ⇒ 该网格上边界外扩、散点完整可见;
反之"该网格只有一个取值"⇒ 保持自动自适应(区间 = 退化留白 `max(1,|v|×0.1)` 两侧展开)。
- `[D]` 特例:两个网格、全部为同一时刻的单值点(一个网格内有 3 和 7 两个取值)⇒
时间跨度 **10**、以该时刻居中、`NiceStep(10,10) == 1.0`、每个散点都在自己的网格内。
- `[E]` 回归:普通信号的视界 = 数据范围、各网格保持 `autoRange`;
§9.31 的"适应视图 ↔ Y 轴自适应互不干扰"(区间逐位相同)仍然成立。
- `[F]` **真实文件** `TestData/MF4-Record.MF4`:`signals=124`,`WIN_Motor_StallCurrentVolBrk`
确实只有 **1** 个有效点(t = 0.005492),用它 + `m_MOT_Current_DCom[0]`(**8475** 样点)复现用户场景:
点「适应视图」后散点圆心离绘图区左边界 **16.5px**、半径 15px ⇒ 完整可见
(修复前它正好落在边界上、只剩半个)。
---
## 10. 变更日志(本文件维护记录)
| 日期 | 设备/模型 | 摘要 |
@@ -1833,6 +1907,7 @@ GDI+ 抗锯齿逐段扫描这些**近竖直、满幅**的边,代价爆炸(
| 2026-10-15 | DeepSeek-V4.1-Flash(**起源设备**) | **全局 Y 轴自适应跟随网格结构 + 组名悬浮 + 稀疏信号默认点线 + 单点光标吸附 + 放大自动点线(详见 §9.29)**。① **新网格不继承全局 Y 轴自适应**:`RebuildLanes` 对新出现的信号返回默认 `LaneCarry()` ⇒ `fitYToSelection=false`;修法:重建前先算 `previousAllAutoFit`(非空且**每个**网格都开着 = 全局开启的正式定义),`LaneCarry` 加 `matched` 标记,新信号用它当默认;并新增 `ReapplyGlobalAutoFitIfEnabled()` 在 **`Layout()`(网格数变化)/`RefreshLanes()`(信号数变化)/`SplitLane()`** 之后立即重算 ⇒ 新勾选(按**当前视界**自适应)、取消显示(剩余信号重新收敛)、拆分(每份各自自适应)三项全覆盖。② **组名悬浮**:新增 `SignalPanel::StickyGroupRowIndex()` + `PaintStickyGroupRow()`(不透明底 + 底部边界线),`RowRect` 加 `m_rowYOverrideActive/Y` 覆盖行顶部(行内所有几何的唯一来源);判据 = 分组行已在列表上边界之上且本组最后一行仍在列表内;常规循环**跳过**被钉住的行(否则画两次)。③ **稀疏信号默认样式**:新增 `ApplySparseDefaultStyle`(1 个有效点 → 点线 + **最大线宽 10px**;2..16 → 点线 + 2px),在 `BuildSignalGroup`/`InsertDerivedSignal` 两处调用;线宽常量统一到 `DataModel.hpp`(`kSignalLineWidthMin/Max`)并被 SignalPanel 滑块引用;`ComputeStats` 输出 `firstFiniteIndex`、`SignalData` 新增 `singlePointTime`。④ **单点光标吸附**:`SnapTimeToSample` 新增半径分支(`kCursorSnapPointRadiusPixels=8` 逻辑像素)吸附 `singlePointTime`,半径外自由移动(**吸附而非卡住**)。⑤ **放大自动点线**:`SignalStyle` 新增 `autoScatterActive/autoScatterSavedMode/autoScatterUserOverride`,`AxisSparseInView`(间距 > 半个绘图区宽度,与吸附上门限同源,**单点轴不算稀疏**)+ `UpdateAutoScatterStyles()`(在 `SetTimeView` 等四处调用)⇒ 进入阈值自动切点线、退出阈值恢复原样式,**用户手动改过则不恢复**(判定必须放在恢复判断**之前**,否则用户改完紧接着缩出去会被覆盖)。验证:新增离屏 `.preflight/check` **65/65 通过**(连跑 3 次稳定,覆盖 7 组断言;含“钉住行与自然位置逐像素一致”“单点吸附半径内外”“用户手动改样式后退出不覆盖”),**A/B**:临时禁用六个机制后 **21 项如实失败**;`make release` 零警告。 |
| 2026-10-15 | DeepSeek-V4.1-Flash(**起源设备**) | **组名平滑悬浮 + 单点信号初始视图 + 光标吸附统一判定(自动点线同源)(详见 §9.30)**。① **组名悬浮平滑化**:位置改为 `StickyGroupRowTop = min(列表上边界, 本组最后一行下边界 − 组名高度)` ⇒ 组名随最后一行**自然滚出**(不再突然消失),且下边界永不越过下一个分组行的自然位置 ⇒ **不再遮住下一组名**(旧实现固定钉在列表上边界会把下一组名盖住,让别人以为后面的信号属于上一组);**折叠的分组不悬浮**(随滚动自然划过);底部边界线只在真的“浮”起来时才画。② **单点信号初始视图**:新增 `PlotView::FitSinglePointSignal()`(无任何信号被勾选、勾选的第一个信号只有 1 个有效点时生效)—— 数据点居中、**时间跨度 1s**、**Y 跨度 1**,并置 `autoRange=false`/`fitYToSelection=false`;`Application::SynchronizeView` 首次勾选时先试它、失败才 `FitAll()`;`AutoRangeLaneToView` 的退化区间改为 `PadRange` 同款留白 `max(1,|v|×0.1)`。③ **吸附判定统一**(用户给出的唯一判据):新增 `GridMinorCellCount()`(网格一横排小格数 N,与网格绘制同源)+ `SignalSnapEnabled()`(**当且仅当**某信号在当前一屏内的数据点 `< N×4` 才开启吸附,随视界实时重算 ⇒ 疏密信号自适应),`SnapTimeToSample` **只按鼠标所在网格**判定、同网格内所有符合判定的信号数据点都参与、吸附半径 = 一个网格小格宽且不超过半个局部采样间距(吸附≠卡住);旧的 `SampleSpacingPixels`(6px 下门限+半屏宽上门限)**已删除**。④ **自动点线改为与吸附判定完全同源**(`UpdateAutoScatterStyles` 调 `SignalSnapEnabled`),旧 `AxisSparseInView` **已删除**;进入阈值自动切点线、不再满足自动恢复、用户手动改过则不恢复。⑤ **需求作废**:“2…16 个有效采样点默认点线 + 2px”**已取消**(只保留“1 个有效点 → 点线 + 最大线宽”);`SignalData::singlePointTime` 升级为 **`sparseTimes`/`sparseValues`**(有效点 ≤ 1024 时全部缓存,供吸附与单点视图使用)。验证:新增离屏 `.preflight/check` **55/55 通过**(连跑 3 次稳定;覆盖组名悬浮四种场景、单点初始视图、吸附判定/网格作用域/多信号/动态切换/单点半径、自动点线同源与用户覆盖);**A/B** 禁用三项机制 ⇒ **14 项如实失败**;`make release` 零警告。 |
| 2026-10-15 | DeepSeek-V4.1-Flash(**起源设备**) | **自适应范围/时机澄清 + 适应视图与 Y 轴自适应互不干扰 + 吸附跟随时间偏移(详见 §9.31)**。① **澄清“自适应范围/时机”**(修正记忆里“额外要求视界内至少有一个可见数据点”这句不完善的表述):**首个网格**显示内容时 **Y 轴与时间轴都**自适应;只有一个数据点的信号要**居中显示在网格里**(`FitSinglePointSignal`:居中 + 时间跨度 1s + Y 跨度 1);**后续新增网格只做 Y 轴自适应、不再调整时间轴**。② **「适应视图」与「Y 轴自适应」互相干扰**:根因是两条自适应路径**留白不同**(`PadRange` 6% vs `AutoRangeLaneToView` 5%)⇒ 视界未变时也会算出不同区间、网格被无意义重绘(实测切一次 **143513** 像素、反向 **137326** 像素)。修法:新增 `DataModel.hpp` 的 **`PadValueRange()`** 作为唯一留白实现(含退化区间 `max(1,|v|×0.1)`),`PadRange` 与 `AutoRangeLaneToView` 共用 ⇒ 四个步骤(适应视图→开自适应→关自适应→再适应视图)**区间逐位相同且绘图区逐像素 0 差异**。③ **「适应视图」在单点信号上让数据点“飞了”**:`FitAll()` 开头先试 `FitSinglePointSignal()`(仅一个网格 + 唯一可见信号为单点),与首次加载效果一致且幂等。④ **吸附跟随「调整 → 时间轴偏移」**:采样点画在 `time[k] + offsetTime` 处而光标存的是显示时间,旧实现按源时刻吸附 ⇒ 落到历史位置;修法:`SignalSnapEnabled`/`HasVisibleSample`/`NearestSnapSample`/`LocalSampleSpacing` 全部加 `timeShift`,查找时 `time - timeShift` 回源空间、返回时加回去,`UpdateAutoScatterStyles` 同步传 `offsetTime`。验证:离屏 `.preflight/check` **39/39 通过**(连跑 3 次稳定;含像素级零变化断言与“真缩放会重绘”对照组);**A/B** 回退三项修复 ⇒ **7 项如实失败**(含 143513/137326 像素重绘症状);`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` 零警告、链接通过。 |
---
@@ -2109,11 +2184,30 @@ GDI+ 抗锯齿逐段扫描这些**近竖直、满幅**的边,代价爆炸(
67. ⚠️ **单点信号(`finiteCount == 1`)必须在命中测试 / 光晕 / 拖拽虚影里单独处理**:
它没有折线可测距,`HitTestSignal` 要用 `SinglePointScreenPos()` + 容差 `max(常规容差, 点绘制半径)`;
悬停时**不改鼠标形状**(`QueryCursor` 不查 `HitTestSignal`),只在按下拖拽后改(`IDC_SIZEALL`)。
68. ⚠️ **多网格单点信号的「适应视图」规则**(用户 2026-10-15 明确给出):左/右边界 = 所有网格中
68. ~~⚠️ **多网格单点信号的「适应视图」规则**(用户 2026-10-15 明确给出):左/右边界 = 所有网格中
最左/最右数据点时间再向外偏**一个网格小格**的时间跨度;每个网格的 Y 按自身数据点自适应,
若该网格内存在"时间一致但取值不一致"的多个单点 ⇒ 纵向上下各留**一个 Y 小格**;
全部点在同一时刻 ⇒ 时间跨度 **1s** 居中于该时刻(与"首个单点信号初始视图"对齐);
某网格只有一个取值 ⇒ 该网格 Y 跨度 **1** 居中。仅在**所有可见信号都是单点**时启用。
某网格只有一个取值 ⇒ 该网格 Y 跨度 **1** 居中。仅在**所有可见信号都是单点**时启用。~~
⚠️ **本规则已作废(§9.33,用户 2026-10-15 第五轮明确要求撤回重做)**:
改为“边界处的单值散点让出**该散点的半径**”(而不是一小格),且不再要求“全部都是单点”
才生效 —— 任何一条边界被单值散点顶住都要让位。现行规则见 69–71。
69. ⚠️ **「适应视图」的边界规则(§9.33,用户 2026-10-15 第五轮重新定义)**:
常规全局适应后,**若某条边界(时间轴左 / 右、每个网格幅值上 / 下)被一个单值信号的
数据点顶住,则把该边界向外让出该散点的半径(+1px)**,使其完整显示在网格内;
**只在散点会被裁切时才扩展**(内部散点不动视界);边界不是单值散点时边界就落在该
信号点上(时间轴不留余量;幅值仍走 `PadValueRange` 的 6%,**没有**改动它)。
判断依据必须是**信号自身的数据点**,不能用“轴的范围”——MF4 里单值信号与连续信号
常共用同一通道组(同一条轴),用轴判断会误判。
70. ⚠️ **特例:全部可见数据点都是单值信号且时刻完全相同**(`FitSinglePointCluster`):
时间轴**以该时刻居中**、缩放到 **一大格 = 1s**(视界 = `kTimeTickTarget × 1s` = 10s,
此时 `NiceStep(10,10) == 1.0`);每个网格的幅值区间以“能完整画出该网格内的全部数据点”
为准(退化成单值时以**跨度 1 居中**)。旧规则“跨度 1s 居中”“各留一小格”**已作废**。
71. ⚠️ **`FitAll()` 在没有任何可见信号时不得置位 `Tab().ViewInitialized()`**:
否则用户先连点几次「适应视图」(此时无信号被勾选)再勾选第一个信号,
`Application::SynchronizeView` 会**跳过初次自适应**,视界停在“无信号”那次的整段范围上,
单值信号的数据点既不居中、还会有一半被裁在网格外(用户 2026-10-15 报告)。
另外「适应视图」两条路径(初次自适应 / 工具栏按钮)必须**只有一条实现**(都走 `FitAll`)。
### 11.3 离屏验证方法学(本机偏好,强烈建议沿用)