组名平滑悬浮 + 单点信号初始视图 + 光标吸附统一判定(自动点线同源)

一、信号列表「组名悬浮」平滑化、且不再遮住下一个组名
 * 悬浮位置改为 min(列表上边界, 本组最后一行下边界 − 组名高度):
   - 本组还有行在列表里时钉在列表上边界;
   - 本组最后一行开始滑出上边界时,组名跟着一起上移、随滚动条自然离界
     (不再像旧实现那样突然消失);
   - 「本组最后一行的下边界」恰好就是下一个分组行的自然位置,因此悬浮组名的下边界
     永远不超过下一组名 —— 旧实现固定钉在列表上边界会把下一组名盖住,让别人以为
     后面的信号属于上一组。
 * 折叠的分组不再悬浮(折叠时本组“最后一行”就是它自己,悬浮位置 = 自然位置,
   于是随滚动自然划过,不会停在最上方)。
 * 底部边界线只在真的“浮”在其它行之上时才画,避免与正常绘制出现像素差。

二、单点信号的初始视图
 * 新增 PlotView::FitSinglePointSignal():当前没有任何信号被勾选、而勾选的第一个信号
   只有一个有效数据点时,把该数据点居中放置,时间跨度 = 1s、Y 轴跨度 = 1
   (各为一个大格),并关闭该网格的整段自适应以免被覆盖。
   Application::SynchronizeView 在首次勾选时先尝试它,失败才走 FitAll()。
 * AutoRangeLaneToView 的退化区间(可见区间内只有一个取值)改用与 PadRange 一致的留白
   max(1, |v|×0.1),不再产生“几乎为 0”的区间(那会让幅值网格线挤成一团、看不出数据点)。
 * 需求作废:上一轮“2…16 个有效采样点默认改为点线 + 2px”已取消(放大自动点线已覆盖该场景);
   只保留“只有一个有效采样点 → 点线 + 最大线宽”。
 * SignalData::singlePointTime 升级为 sparseTimes/sparseValues(有效点 ≤ 1024 时在加载/插入
   时缓存全部有效点),同时服务单点初始视图与光标吸附。

三、光标吸附的统一判定
 * N = GridMinorCellCount():网格底衬一横排的小格数,与网格绘制用同一套
   NiceStep + 五等分算出(因此不随缩放等级大幅变化)。
 * SignalSnapEnabled():当且仅当某信号在当前一屏内的数据点个数 < N×4 时为它开启吸附;
   判据随视界实时重算 ⇒ 疏密变化的信号在稀疏区自动吸附、移回稠密区自动自由移动。
   额外要求视界内至少有一个可见数据点(否则“0 < N×4”会把数据全在视界之外的信号
   误判为稀疏)。统计只在可见下标区间内进行且攒够阈值即早退,代价有界。
 * SnapTimeToSample():判定只针对鼠标所在网格(同一个时间轴上不同网格结果可以不同);
   同一网格内所有符合判定的信号的数据点都参与吸附比较;
   吸附半径 = 一个网格小格的宽度,且不超过半个局部采样间距 ⇒ 数据点很稀疏
   (一屏只有一两个点)时光标在点之间仍可自由移动(吸附是提示,不是卡住)。
 * 删除旧的 SampleSpacingPixels(6px 下门限 + 半屏宽上门限)。

四、放大自动切点线改为与吸附判定完全同源
 * UpdateAutoScatterStyles() 改用 SignalSnapEnabled:进入判定自动切「点线」,
   不再满足(适应视图、或视界从稀疏区移到稠密区)自动恢复原样式;
   自动期间用户手动改过则不恢复(尊重用户选择,判定放在恢复之前)。
 * 删除旧的 AxisSparseInView(“采样间距 > 半屏宽”)。

五、验证(离屏 .preflight/check:SW_HIDE 窗口、不注入真实鼠标键盘、进程/线程 BELOW_NORMAL)
 55/55 通过(连跑 3 次稳定):[A] 组名悬浮(钉住/平滑上移/整组滚过后换下一组/折叠组不悬浮/
 悬浮下边界不超过下一组名)、[B] 单点初始视图(跨度 1s 与 1、居中、点线 + 最大线宽、
 两信号时不再走单点路径)、[C] 吸附判定(满视图都关闭/稀疏窗口开启/无可见点不开启/
 同一时间轴不同网格结果不同/同网格多信号的点都参与/稠密↔稀疏双向动态切换/
 单点靠近吸附远离自由)、[D] 自动点线(同源判定/恢复/用户手动改过不覆盖)。
 A/B:临时禁用「悬浮位置公式 / 单点初始视图 / 吸附判定」后重跑 ⇒ 14 项如实失败。
 make release 零警告(用户正运行 Target.exe,链接用临时 -o 名字验证通过)。

六、规则同步
 Documents/agent_mem.md 新增 §9.30、§11.2 第 58–61 条硬约束与变更日志。
This commit is contained in:
DeepSeek V4.1 Flash committed 2026-10-15 15:10:36 +08:00
1 parent 9f6de3bba4
commit 57cc714c1d
8 files changed
+808 -230

No files matched your search

+83 -26
View File
@@ -310,11 +310,12 @@ namespace jdi
void ReapplyGlobalAutoFitIfEnabled();
/**
* @brief 依据「采样点是否稀疏」自动启用/恢复「点线」绘制方式。
* @brief 依据「光标吸附判定」自动启用/恢复「点线」绘制方式。
*
* 用户要求(2026-10-15):把波形放大到一定程度(此时光标会吸附数据点)后,
* 线段模式已经看不出数据点在哪 → 自动为该信号启用「点线」提示;视界回退到阈值
* 之外(例如适应视图)时自动恢复**原来的**绘制方式。
* 用户要求(2026-10-15):**仅在满足光标吸附判定的信号上**生效 ——
* 即“一屏内数据点少于 N×4”(见 `SignalSnapEnabled`)时自动切到「点线」提示
* 用户数据点的位置;一旦不再满足(适应视图、或把视界从稀疏区移到稠密区)
* 自动恢复**原来的**绘制方式。
*
* ⚠️ 自动启用期间若用户**手动**改了绘制方式(`SignalStyle::autoScatterUserOverride`
* 由本函数自动识别:自动状态下 mode 不等于 `LINE_SCATTER` 即说明是用户改的),
@@ -325,24 +326,76 @@ namespace jdi
bool UpdateAutoScatterStyles();
/**
* @brief 判断某个时间轴在当前视界下是否已经“稀疏到需要点线提示”。
* @param [IN] axis 目标时间轴。
* @return 采样点间距超过半个绘图区宽度返回 true。
* @brief 网格底衬**一横排的小格数量 N**(与 `PaintLane` 画出来的网格同源)。
*
* 时间刻度用 `NiceStep(span, kTimeTickTarget)` 定主刻度、再五等分 ⇒
* 不管缩放等级如何,一横排总是差不多同样多的小格。
*
* @return 小格数量(无法判定时返回 0)。
*/
bool AxisSparseInView(const TimeAxis& axis) const;
int GridMinorCellCount() const;
/**
* @brief 判断某信号在当前视界下是否**开启光标吸附**。
*
* 用户 2026-10-15 的定义:**当且仅当**该信号在“当前一屏”(当前视界)内的数据点
* 个数 `< N × 4`(N = `GridMinorCellCount()`)时开启吸附。
* 判据随视界实时重算 ⇒ 疏密变化的信号在稀疏区自动吸附、稠密区自动自由移动。
*
* ⚠️ 额外要求视界内**至少有一个可见数据点**:数据点全在视界之外的信号既没有可吸附
* 的对象、也没有可提示的内容(否则它会被判成“0 < N×4 稀疏”而白切一次「点线」)。
*
* @param [IN] axis 该信号使用的时间轴。
* @param [IN] signal 目标信号。
* @return 开启吸附返回 true。
*/
bool SignalSnapEnabled(const TimeAxis& axis, const SignalData& signal) const;
/**
* @brief 判断某信号在给定可见下标区间内是否存在**落在视界内**的有效采样点。
* @param [IN] axis 该信号使用的时间轴。
* @param [IN] signal 目标信号。
* @param [IN] begin 可见下标区间起点。 @param [IN] end 终点(不含)。
* @return 存在返回 true。
*/
bool HasVisibleSample(const TimeAxis& axis, const SignalData& signal, std::size_t begin,
std::size_t end) const;
/**
* @brief 找某信号**可见区间内**离给定时刻最近的有效采样点。
*
* 稀疏信号(有效点已缓存)用二分;一般信号从最近下标向两侧**有界**探测
*(NaN 只出现在缺少数据的区段)。视界之外的采样点不是合法落点,一律排除。
*
* @param [IN] axis 该信号使用的时间轴。
* @param [IN] signal 目标信号。
* @param [IN] time 目标时刻。
* @param [OUT] candidate 最近的可见有效采样点时刻。
* @return 找到返回 true。
*/
bool NearestSnapSample(const TimeAxis& axis, const SignalData& signal, double time,
double& candidate) const;
/**
* @brief 取某时刻附近的**局部采样间距**(时间单位,用于限制吸附半径)。
* @param [IN] axis 该信号使用的时间轴。
* @param [IN] signal 目标信号。
* @param [IN] time 目标时刻。
* @param [OUT] spacing 相邻采样点的时间间隔(无法判定时为 0)。
*/
void LocalSampleSpacing(const TimeAxis& axis, const SignalData& signal, double time,
double& spacing) const;
/**
* @brief 把时间吸附到距今最近的数据采样点。
*
* 仅在横向放大到采样点间距足够大(可精确分辨)时生效,
* 避免缩小时光标出现“抖动”难以微调。
*
* ⚠️ 吸附有**上下两个门限**:
* * 下门限:采样点间距 ≥ `kCursorSnapMinPixels`(缩小时不吸附,否则难以微调);
* * 上门限:视界内至少能容纳 **2 个**采样点(`pixels <= plotWidth / 2`)。
* 视界比采样间隔还窄时“最近采样点”在视界之外,吸附会把光标一把甩到视界边缘
* 并钉住(表现为“一拖光标就飞到视界边缘、跟不动鼠标”)。
* * 并且只接受**落在当前可见区间内**的采样点。
* 判定与半径(用户 2026-10-15 统一后的定义):
* * **判定只针对鼠标所在的网格**:该网格内的信号各自按 `SignalSnapEnabled` 判定,
* 符合条件的信号的数据点全部参与吸附;网格内没有符合条件的信号时完全自由移动;
* * 同一网格内有多条信号时,所有符合条件的数据点都参与比较;
* * 吸附半径 = **一个网格小格的宽度**(与判定同源),且**不超过半个局部采样间距**
* ⇒ 数据点很稀疏(一屏只有一两个点)时光标在点与点之间仍可自由移动,
* 而不是被“死死吸住”(吸附只是提示性响应,不是卡住光标)。
*
* @param [IN] time 原始时间。
* @param [OUT] snapped 吸附后的时间。
@@ -350,15 +403,6 @@ namespace jdi
*/
bool SnapTimeToSample(double time, double& snapped) const;
/**
* @brief 计算指定文件中某一时刻附近采样点的时间间隔。
* @param [IN] file 目标文件。
* @param [IN] time 目标时刻。
* @param [OUT] pixels 间隔换算到屏幕上的像素宽度。
* @return 计算成功返回 true。
*/
bool SampleSpacingPixels(const TimeAxis& axis, double time, double& pixels) const;
/**
* @brief 判断坐标是否落在绘图区范围内。
* @param [IN] x 客户区横坐标。
@@ -380,6 +424,19 @@ namespace jdi
*/
void FitAll();
/**
* @brief 「单点信号」的初始视图:把唯一的数据点居中,时间跨度 1s、幅值跨度 1。
*
* 用户要求(2026-10-15):当前**没有任何信号被勾选**、而勾选的第一个信号
* **只有一个有效数据点**时,整段自适应并不能把它的状态显示出来
*(时间轴会覆盖整条时间列、幅值区间退化成几乎为 0)。
* 因此这种情况改为:数据点居中放置,**时间轴跨度 = 一个大格 = 1s**、
* **Y 轴跨度 = 一个大格 = 1**。
*
* @return 已按“单点信号”初始化返回 true(调用方无需再做 `FitAll`)。
*/
bool FitSinglePointSignal();
/**
* @brief 以给定倍率缩放时间轴(以视图中心为锚点)。
* @param [IN] factor 缩放倍率,大于 1 表示放大。