全局 Y 轴自适应跟随网格结构 + 组名悬浮 + 稀疏信号默认点线 + 单点光标吸附 + 放大自动点线

一、全局「Y 轴自适应」跟随网格结构
 * 新勾选信号产生的新网格现在会**继承**该开关:RebuildLanes 重建前先算
   previousAllAutoFit(非空且每个网格都开着 = 全局开启的正式定义),LaneCarry 加
   matched 标记,新出现的信号用它作为 fitYToSelection 的默认值。
 * 新增 PlotView::ReapplyGlobalAutoFitIfEnabled(),在 Layout()(网格数量变化 →
   Relayout)、RefreshLanes()(信号数量变化)、SplitLane() 之后立即重算一次。
   ⇒ 新勾选(按当前视界自适应)、多信号网格内取消显示某信号(剩余信号重新收敛)、
     拆分网格(每份各自自适应)三项都覆盖。

二、信号列表「组名悬浮」
 * 分组行被滚出列表上边界、而该组仍有信号留在列表里时,把分组行钉在列表顶部
   (不透明底 + 底部边界线);整组翻过去后本组组名消失,改由下一组悬浮。
 * 新增 SignalPanel::StickyGroupRowIndex() / PaintStickyGroupRow();RowRect 增加
   m_rowYOverrideActive/m_rowYOverrideY 覆盖行顶部坐标(RowRect 是行内名称框 / ✕ /
   徽标的唯一几何来源)。常规行循环跳过被钉住的那一行,避免同一行被画两次。

三、「少数数据点」信号的默认样式
 * 有效采样点 1 个 → 「点线」+ 最大线宽(10px);2 … 16 个 → 「点线」+ 2px;
   其余保持「线段」+ 1px。只在新建信号时生效(BuildSignalGroup 与
   InsertDerivedSignal),不覆盖用户设置。
 * 线宽范围常量统一到 DataModel.hpp(kSignalLineWidthMin/Max),SignalPanel 的线宽
   滑块改为引用它们(原本两处各自硬编码,容易漂移)。

四、单点信号的光标吸附(吸附而非卡住)
 * 旧逻辑要求时间轴至少 2 个采样点才能算「采样间距」,单点信号因此永远不吸附。
 * 新增 SignalData::singlePointTime(加载时与 ComputeStats 同趟扫描缓存该点时刻),
   SnapTimeToSample 新增半径分支(kCursorSnapPointRadiusPixels = 8 逻辑像素):
   半径内吸附到该点,半径外自由移动(移开即自由,不是把光标吸住)。

五、放大到「采样点稀疏」时自动启用点线
 * SignalStyle 新增运行态三字段:autoScatterActive / autoScatterSavedMode /
   autoScatterUserOverride。
 * AxisSparseInView()(采样点间距 > 半个绘图区宽度,与光标吸附上门限同源;单点时间轴
   不算稀疏)+ UpdateAutoScatterStyles():进入阈值保存原绘制方式并切「点线」,
   退出阈值(如适应视图)恢复;平移不改变缩放等级 ⇒ 结果稳定,不会样式抖动。
 * 自动启用期间用户手动改了绘制方式则退出时**不恢复**(尊重用户选择);该判定必须在
   恢复判断之前完成 —— 用户改完样式可能紧接着就退出阈值,否则会被覆盖。
 * 钩子:SetTimeView(视界唯一写入口)与 Layout / RefreshLanes / SplitLane。

六、验证(离屏 .preflight/check:SW_HIDE 窗口、不注入真实鼠标键盘、进程/线程 BELOW_NORMAL)
 65/65 通过(连跑 3 次稳定):[A1]/[A2]/[A3] 全局自适应、[B] 组名悬浮(含钉住行与自然
 位置逐像素一致、整组翻过后组名消失)、[C] 稀疏默认样式边界(1/2/5/16/17/1000 点)、
 [D] 单点吸附(半径内吸附、半径外自由、无残留粘滞)、[E] 自动点线(进入/退出/平移稳定/
 用户手动改过后不覆盖/再次进入再启用)。
 A/B:临时禁用六个机制后重跑 ⇒ 21 项如实失败(覆盖全部六项),证明断言有判别力。
 make release 零警告(用户正运行 Target.exe,链接用临时 -o 名字验证通过)。

七、规则同步
 Documents/agent_mem.md 新增 §9.29、§11.2 第 55–58 条硬约束与变更日志。
This commit is contained in:
DeepSeek V4.1 Flash committed 2026-10-15 14:10:13 +08:00
1 parent 3bca3b1201
commit 9f6de3bba4
7 files changed
+559 -5

No files matched your search

+101
View File
@@ -1502,6 +1502,83 @@ GDI+ 抗锯齿逐段扫描这些**近竖直、满幅**的边,代价爆炸(
---
### 9.29 全局 Y 轴自适应跟随网格结构 + 组名悬浮 + 稀疏信号默认点线 + 单点光标吸附 + 放大自动点线(本轮,起源设备)
> 本轮六项(用户 2026-10-15 报告):① 全局「Y 轴自适应」开启时,新勾选信号产生的新网格没有按当前视界自适应;
> ② 全局开启时,多信号网格内取消显示某信号 / 拆分网格后需要重新做一次全局自适应;
> ③ 信号列表滚动时组名要**悬浮**在最上方,直到该组信号全部翻过去;
> ④ 「少数数据点」信号默认用「点线」+ 2px,只有一个点时用最大线宽(10px);
> ⑤ 单点信号的光标吸附(吸附而非卡住);⑥ 放大到采样点稀疏时自动启用点线、退出阈值恢复原样式。
> 离屏验证 **65/65 通过**(连跑 3 次稳定),并做了 A/B(临时禁用六个机制后 **21 项如实失败**)。
#### 一、全局 Y 轴自适应要跟住网格结构
- **根因**:`RebuildLanes`(`DataModel.cpp`)对**新出现的**信号返回默认 `LaneCarry()` ⇒
`fitYToSelection = false`,于是全局开关看着是开的、新网格却不自适应。
- **修法 1(继承)**:重建前先算 `previousAllAutoFit = 非空且每个网格都开着 fitYToSelection`
(这就是「全局 Y 轴自适应」的正式定义),`LaneCarry` 加 `matched` 标记,
`applyCarry` 对新信号用 `previousAllAutoFit` 作为默认值。
- **修法 2(立即算一次)**:新增 `PlotView::ReapplyGlobalAutoFitIfEnabled()`
(判据 = `AllLanesAutoFitToSelection()`,与工具栏按钮同源),在 **`Layout()`**
(网格数量变化走 `Relayout`)、**`RefreshLanes()`**(信号数量变化)、**`SplitLane()`** 之后调用。
⇒ 新勾选信号(按当前视界自适应)、取消显示信号(剩余信号重新收敛)、拆分(每份各自自适应)三项都覆盖。
- ⚠️ **新硬约束**:凡「与网格集合相关」的自动行为,必须同时覆盖 `Layout` 与 `RefreshLanes`
两条路径 —— `Application::SynchronizeView` 是**按“网格数量是否变化”**二选一的
(变化 → `Relayout`→`Layout`;不变 → `RefreshLanes`),只挂一处必然漏掉一半场景。
⚠️ 这两个钩子都**不能**放进 `BuildLaneVisuals()`:它在 `Paint` 里也会被调用(配合 `m_renderMutex`),
在那里自动改样式/自动算 Y 会污染渲染路径。
#### 二、组名悬浮(`SignalPanel`)
- 新增 `StickyGroupRowIndex(rowCount)`(纯计算)+ `PaintStickyGroupRow(g, row)`(钉在列表顶部绘制:
不透明 `panelBg` 底 + 底部一条 `border` 线),`RowRect` 增加 `m_rowYOverrideActive/m_rowYOverrideY`
覆盖行顶部坐标 —— `RowRect` 是行内所有几何(名称框 / ✕ / 徽标)的唯一来源,因此只需覆盖它。
- 判据:分组行 `RowRect(row).Y < m_listRect.Y` **且**本组最后一行的下边界 `> m_listRect.Y`。
整组滚过去后本组组名消失;若下一组有信号可见,则改由下一组悬浮。
- ⚠️ 常规行循环必须**跳过**被钉住的那一行(否则同一行会被画两次:一次在滚动位置、一次在顶部)。
#### 三、稀疏信号的默认样式(`DataModel`)
- 新增常量(`DataModel.hpp`):`kSignalLineWidthMin/Max = 0.5/10`(**SignalPanel 的线宽滑块改为引用它们**,
避免“最大线宽”两处漂移)、`kSparseSignalSampleLimit = 16`、`kSparseSignalLineWidth = 2`、
`kSinglePointSignalLineWidth = kSignalLineWidthMax`。
- 新增 `ApplySparseDefaultStyle(finiteCount, style)`:1 个有效点 → 点线 + 10px;2..16 → 点线 + 2px。
在 `BuildSignalGroup` 与 `InsertDerivedSignal` **两处**调用(只在新建信号时生效,不覆盖用户设置)。
- `ComputeStats` 新增输出 `firstFiniteIndex`;`SignalData` 新增 **`singlePointTime`**
(单点信号的那个时刻,加载时填好)—— 供光标吸附使用,避免在鼠标移动时线性扫描采样。
#### 四、单点信号的光标吸附(`PlotView::SnapTimeToSample`)
- 旧逻辑对「只有一个采样点」的信号无能为力:`SampleSpacingPixels` 要求 ≥2 个采样 ⇒ 永远不吸附
(用户报告“无法准确指向数据点”)。
- 新增半径分支:`signal.singlePointTime` 在视界内且与光标的距离 ≤ `kCursorSnapPointRadiusPixels`
(8 逻辑像素)时吸附过去;半径外**不吸附** ⇒ 自由移动。
⚠️ 这是「吸附」而不是「卡住」:移开半径后光标立刻自由(用户明确要求只是提示性响应)。
#### 五、放大到稀疏时自动启用点线(`SignalStyle` + `PlotView`)
- `SignalStyle` 新增运行态三字段:`autoScatterActive` / `autoScatterSavedMode` / `autoScatterUserOverride`。
- `PlotView::AxisSparseInView(axis)`:采样点间距 > 半个绘图区宽度(与光标吸附上门限**同源**)。
⚠️ **单点时间轴不算稀疏**(它一直最稀疏,默认样式已是点线;若算进去,用户手动改成线段会被立刻改回)。
- `UpdateAutoScatterStyles()`:进入阈值 → 保存原 mode + 切「点线」;退出阈值 → 恢复。
⚠️⚠️ **必须在恢复判断之前先做“用户是否手动改过”的判定**(本机制只写点线,所以自动期间
`mode != LINE_SCATTER` 只能是用户改的):用户改完样式可能**紧接着就缩出去**(例如直接点适应视图),
那次调用的视界已经不稀疏;若等到那时才判定,恢复逻辑会先把用户的修改覆盖掉。
- 钩子:**`SetTimeView`**(视界唯一写入口 ⇒ 缩放/平移/适应视图全覆盖;
平移不改变缩放等级 ⇒ 结果稳定、不会样式抖动)以及 `Layout` / `RefreshLanes` / `SplitLane`。
#### 六、验证(离屏 `.preflight/check`,`SW_HIDE` + 不注入真实输入 + 进程/线程 `BELOW_NORMAL`)
- **65/65 通过**(连跑 3 次稳定):`[A1]` 新网格继承开关且按**当前视界**自适应、`[A2]` 取消勾选后重算、
`[A3]` 拆分后每份各自重算、`[B]` 组名悬浮(含“钉住行与自然位置逐像素一致”与“整组翻过后组名消失”)、
`[C]` 稀疏默认样式(1/2/5/16/17/1000 点边界)、`[D]` 单点吸附(半径内吸附、半径外自由、无残留粘滞)、
`[E]` 自动点线(进入/退出/平移稳定/用户手动改过后不覆盖/再次进入再启用)。
- **A/B**:临时禁用六个机制后重跑 ⇒ **21 项如实失败**(覆盖全部六项),证明断言确有判别力。
- ⚠️ 两个测试手法坑:① 用例之间必须**清空分组**(否则行号/网格号不确定);
② 抓取「列表顶部像素带」时必须**排除右侧滚动条**(滑块位置随滚动变化,实测会混入 54px 假差异)。
---
## 10. 变更日志(本文件维护记录)
| 日期 | 设备/模型 | 摘要 |
@@ -1543,6 +1620,7 @@ GDI+ 抗锯齿逐段扫描这些**近竖直、满幅**的边,代价爆炸(
| 2026-10-15 | DeepSeek-V4.1-Flash(**另一台设备**) | **「调整」数值符号规则 + 边沿统计按帧推进(含首帧公平分摊)+ 浮动面板排版与弱峰复位控件(详见 §9.26)**。① **符号**:Y 轴偏移 0 不再显示 `+0.00`;系数非 0 正值带 `+`(编辑态初始文本仍无符号);`Δt:` 后按正负带符号(`Δt:+5s 60ms` / `Δt:-5s 60ms` / `Δt:0s`)。② **边沿统计卡住**根因:预算按 `GetTickCount64()` 分桶(系统粒度 15.6ms)而 `PostMessage` 自持重绘把帧率拉高 ⇒ 同一 tick 内多帧完全不推进 → 改为 **`Paint` 开头按帧重置**;并给**每个状态独立每帧配额**(`share = max(6, m_edgeScans.size())`,避免同帧新建状态被第一个独占整额)→ 实测首帧 5 个状态全部推进、**9 帧内全部扫完且不注入任何鼠标消息**、与参考实现逐位一致。③ **面板排版**:信号名宽度上限 `kEdgePanelNameMaxWidth=150`(300 字符名字只多 44px,旧实现多 5100px);统计信息**两列等宽**(`2×max(列宽)+间距`)⇒ 多信号“上升沿/下降沿”列起点固定、垂直对齐;未完成时**两个数字后都带 `…`**(旧实现只跟在“下降沿”后)。④ **弱峰调整**:每行滑块右侧新增**复位控件**(复用还原图标,悬停高亮 + `IDC_HAND` + 提示;点击恢复幅度 0 / 迭代 1 且上限回 10);上限输入框提示改为“输入上限(当前N)”;滑块标签去掉多余的 `%` / `N`。验证:新增离屏探针 `panelcheck` **46/46 通过**(全部走真实菜单/点击路径),既有 `verify` 47/47(另 2 条为本机未同步的旧规则断言),`make release` 零警告。 |
| 2026-10-15 | DeepSeek-V4.1-Flash(**另一台设备**) | **数据层大重构(文件→分组 / 时间轴一等公民 / 加载器抽象)+ CSV 多时间轴 + MF4 支持(详见 §9.27 与 `Documents/refactor_report_2026-10-15.md`)**。① **解耦**:`DataFile`→**`SignalGroup`**(删除 `filePath`/`fileName`/单一 `time`/`timeColumnName`/`timeMonotonic`/`minTime`/`maxTime`),新增 **`TimeAxis`**(1..N 条/组)与 `SignalData::axisIndex`,`SignalData` 删除冗余的 `rawHeader` 与**全仓无人使用的 `unit`**;`SignalRef::fileIndex`→`groupIndex`;`DataSet`/`TabConfig` 全套改名并新增 `Axis(ref)`;采样/统计 API(`SampleSignalAt`/`FindNearestSample`/`VisibleIndexRange`/`EdgeIndexRange`/`EdgeCountsFor`/`EdgeCenterCountsFor`/`EdgeScanFor`/`MeanFilterValueAt`/`WeakPeakValueAt`/`SampleSpacingPixels`)**首参由 `const DataFile&` 改为 `const TimeAxis&`**;`ResolvedSignal` 携带 `{group, axis}`。② **加载器抽象**:新增 `DataTable.hpp`(`DataTable`/`LoadProgress`/`TableBlock`/`LoadedSource`)、`DataLoader.*`(唯一入口 `LoadSignalSource` 按扩展名分发)、`CsvLoader.*`、`Mf4Loader.*`,删除 `CsvTable.*`;唯一装配点 `BuildSignalGroup` 与格式无关(新增格式只写加载器)。③ **CSV 多时间轴**:每个 `time` 列开启新块、各块按自己的时间列裁剪 ⇒ 支持各轴不等长(实测 3 条轴 7/4/7),**不再把其它 `time` 列当成信号**;时间轴判据收紧。④ **MF4 支持**:加载后新建分组、组名=文件名,全文件信号归入该组;每个通道组(CG)一条时间轴并自动去重(实测 27 CG → 3 轴 / 124 信号);实现 MDF v4 块解析(DG/CG/CN/CC/DT)、位级整数与 32/64 位浮点解码、线性换算 `phys = cc_val[0] + cc_val[1]*raw`、无效位处理;**`##TX` 兼容规范的「长度+UTF-16」与 CANape 的「裸 NUL 结尾文本」**(否则通道名全空)。⑤ 顺带修复:`Application::Initialize` 在窗口类已注册时不再弹模态框(旧实现导致同进程二次构造永久阻塞),`Shutdown` 注销窗口类;UI 文案统一到「分组」,对话框支持 `*.mf4/*.mdf`。验证:新增 `datacheck` **69/69**、`edgecheck` **46/46**(替代被编码破坏的 `panelcheck`)、既有 `verify` **47/47**(另 2 条本机旧规则断言)、`make release` 零警告。 |
| 2026-10-15 | DeepSeek-V4.1-Flash(**起源设备**) | **信号列表滚动越界绘制 + 边沿统计面板「显示冻结」(详见 §9.28)**。① **列表把组名/信号名画到筛选框上**(MF4 多信号场景):根因是 `UiText` 走 GDI `DrawTextW`,**GDI 文本不遵循 GDI+ 的 `SetClip`** —— 被滚出视口的「部分可见」行会把文字画到列表上方的筛选框上(`UiFillRect` 的大面积不透明矩形走 GDI `FillRect` 同理)。修法:`PaintUtil` 新增 **`UiTextClipped`**(同一 HDC `SaveDC` + `IntersectClipRect` + `RestoreDC`;`UiText` 转发 `clip=nullptr`,行为不变),`SignalPanel` 新增 `PaintRowText`(裁到 `m_listRect`)/ `FillRowRect`(夹到 `m_listRect`),分组行与信号行的全部文本与行底色均改走它们。② **边沿统计面板在不移动鼠标时显示冻结**:面板像素图层缓存键(`PanelSlotKey`)**不含**面板上显示的统计数字,而统计是**分帧推进**的中间结果 ⇒ 推进不会让键变化 ⇒ 面板一直贴旧像素 ⇒ 显示停在「统计中…」/中间值,**只有移动鼠标**(改变面板层序/悬停/鼠标坐标等其它键)才重绘并显示早已算完的正确结果(扫描推进本身没停)。修法:`PanelSlotKey` 内按面板实际显示的条目回调 `EdgeCountsFor`/`EdgeCenterCountsFor` 并把 `(rising, falling, complete)` mix 进键。验证:新增离屏 `.preflight/check`(`SW_HIDE` + 不注入真实输入 + `BELOW_NORMAL`)**39/39 通过**(连跑两次稳定):`[A]` 40 信号滚轮下滚后「列表上方区域逐像素 0 变化」且「列表内部仍有变化」(修复前 **1571 像素被污染**)、`[B]` 3 信号 × 300 万采样下两种统计面板的**缓存键持续变化**且**无鼠标输入即完成**(修复前键不变化)、`[C]` 真实 `MF4-Record.MF4`(**124 信号 / 3 条时间轴**)同样 0 像素变化;**并做了 A/B**(`git stash` 回退修复后 `[A]/[B]` 如实失败);`make release` 零警告。 |
| 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` 零警告。 |
---
@@ -1745,6 +1823,29 @@ GDI+ 抗锯齿逐段扫描这些**近竖直、满幅**的边,代价爆炸(
⚠️ 推论(写显示类测试时必须注意):**任何“内容随时间变化”的面板都要把该内容计入键**,
否则既会显示陈旧像素,也会掩盖“自持帧推进”的真实行为;反之,**为使显示类用例真正有判别力**,
测试数据要避开“其它变量恰好也在改变缓存键”的情形(如数字位数变化改变面板宽度)。
55. ⚠️ **与「网格集合」相关的自动行为必须同时挂在 `Layout()` 与 `RefreshLanes()` 两处**:
`Application::SynchronizeView` 是**按“网格数量是否变化”二选一**的 ——
变化 → `Relayout()` → `PlotView::Layout()`;不变 → `PlotView::RefreshLanes()`。
只挂一处必然漏掉一半场景(本轮:新勾选信号走 `Layout`、取消勾选走 `RefreshLanes`)。
⚠️ 这两处逻辑**绝不能放进 `BuildLaneVisuals()`** —— 它在 `Paint` 里也会被调用
(配合 `m_renderMutex`),在里面自动改样式 / 自动算 Y 会污染渲染路径。
56. ⚠️ **「自动行为」一旦要尊重用户的手动覆盖,其判定必须在恢复动作之前**:
本轮「自动点线」用**状态自证**(本机制只写 `LINE_SCATTER` ⇒ 自动期间 `mode != LINE_SCATTER`
就是用户改的)。⚠️ 用户改完样式可能**紧接着就退出阈值**(例如直接点适应视图),
而那次调用看到的**已经是新视界**;若把判定写在“退出分支”里,恢复动作会先覆盖用户的修改。
✓ 先无条件判定覆盖 → 再决定是否恢复。
57. ⚠️ **滚动容器的行几何只能有一个来源**:`SignalPanel::RowRect()` 是行内所有派生矩形
(名称框 / ✕ / 徽标)的唯一来源,因此「组名悬浮」只需给它加一个顶部坐标覆盖
(`m_rowYOverrideActive/m_rowYOverrideY`)即可整体跟随;并且**常规行循环必须跳过被钉住的那一行**,
否则同一行会被画两次(一次在滚动位置、一次在顶部)。
58. ⚠️ **「少数数据点」信号的默认样式**(用户明确要求):有效采样点 **1 个** → 「点线」+
**最大线宽(10px)**(单点就是一个小圆点,半径与线宽成正比);**2 … 16 个** → 「点线」+ 2px;
其余保持「线段」+ 1px。只作用于**新建**信号(不覆盖用户设置)。
⚠️ 线宽范围常量(`kSignalLineWidthMin/Max`)必须**单一来源**(`DataModel.hpp`),
信号面板滑块要引用它,否则「最大线宽」会在两处漂移。
⚠️ 单点信号的光标吸附是**半径判据**(`kCursorSnapPointRadiusPixels = 8` 逻辑像素),
半径外必须自由移动 —— 是「吸附」不是「卡住」;其时刻由 `SignalData::singlePointTime`
在加载时缓存(不要改成鼠标移动时线性扫描采样)。
### 11.3 离屏验证方法学(本机偏好,强烈建议沿用)