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

用户 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

+54 -41
View File
@@ -430,40 +430,68 @@ namespace jdi
HCURSOR QueryCursor(int x, int y) const;
/**
* @brief 时间轴与幅值轴同时自适应到可见信号范围。
* @brief 「适应视图」:时间轴与幅值轴同时自适应到可见信号范围。
*
* 边界规则(用户 2026-10-15 第五轮**重新定义**,取代此前"左右各留一小格"的旧做法):
* * 常规的全局适应后,若某条边界(时间轴的左 / 右、每个网格幅值的上 / 下)被一个
* **单值信号的数据点**顶住,则该边界再向外让出**该散点的半径**,使它**完整地
* 显示在网格内**;边界若不是单值信号的数据点,则边界就落在这个信号点上
* (时间轴不留额外余量,幅值仍走 `PadValueRange` 的统一留白)。
* * 唯一特例:**所有可见信号都是单值数据点、且时刻完全相同**时,时间轴以该时刻
* 居中并缩放到"一大格 = 1s",每个网格的幅值区间保证其数据点都能完整画出。
*
* ⚠️ 没有任何可见信号时**不置位** `Tab().ViewInitialized()`:用户先连点几次
* 「适应视图」后再勾选第一个信号,初次自适应仍必须发生。
*/
void FitAll();
/**
* @brief 「单点信号」的初始视图:把唯一的数据点居中,时间跨度 1s、幅值跨度 1。
*
* 用户要求(2026-10-15):当前**没有任何信号被勾选**、而勾选的第一个信号
* **只有一个有效数据点**时,整段自适应并不能把它的状态显示出来
*(时间轴会覆盖整条时间列、幅值区间退化成几乎为 0)。
* 因此这种情况改为:数据点居中放置,**时间轴跨度 = 一个大格 = 1s**、
* **Y 轴跨度 = 一个大格 = 1**。
*
* @return 已按“单点信号”初始化返回 true(调用方无需再做 `FitAll`)。
* @brief 一个**可见信号**在「适应视图」里提供的边界贡献(见 `FitAll` 的边界规则)。
*/
bool FitSinglePointSignal();
struct FitContribution
{
SignalRef ref; /**< 信号引用。 */
double minTime = 0.0; /**< 时间轴下限(源时刻,与 `ComputeVisibleTimeBounds` 同源)。 */
double maxTime = 0.0; /**< 时间轴上限。 */
bool singlePoint = false; /**< 是否只有一个有效数据点(画出来就是一个圆点)。 */
double pointTime = 0.0; /**< 单值时:数据点的源时刻。 */
double pointValue = 0.0; /**< 单值时:**绘制变换后**的取值。 */
float dotRadius = 0.0f; /**< 单值时:散点半径(设备像素)。 */
};
/**
* @brief 「网格内只显示单点信号」时的自适应(含多个网格各只有一个数据点)。
*
* 用户要求(2026-10-15):多网格、每个网格只有一个数据点时,适应视图必须保证
* **所有散点都在屏幕内**:
* * 时间轴左边界 = 所有点中最早的时刻 **再向左偏移一小格**(一小格 = 该视界下
* 主刻度 / 五等分,与网格叠底同源);右边界同理向右偏移一小格;
* * 若**所有点都在同一时刻**(最极端情况),按“首个网格加载为单点信号”的方式对齐
* (时间跨度 1s、居中);
* * 逐网格的 Y 轴:网格内若有**取值不同**的多个单点信号,纵向同样在各边界留出一小格
* (`LaneValueGridCell`);只有一个取值时跨度 1、居中。
*
* 任一网格里存在非单点的可见信号时**不适用**(返回 false,交回常规整段自适应)。
*
* @return 已按单点规则完成自适应返回 true。
* @brief 汇总全部可见信号的 `FitContribution`(按网格分组)。
* @param [OUT] perLane 与 `Tab().Lanes()` 同序的分组结果。
* @return 可见信号总数(0 表示当前没有任何可见信号)。
*/
bool FitSinglePointLanes();
std::size_t CollectFitContributions(std::vector<std::vector<FitContribution>>& perLane) const;
/**
* @brief 当前布局下**单个网格的高度**(设备像素)。
*
* 与 `BuildLaneVisuals()` 用同一公式(而不是读 `m_lanes[i].rect.Height`):
* `FitAll` 可能在网格数量刚变化、布局尚未重建时被调用,公式给出"将要生效"的值。
* @return 网格高度;无网格时返回 0。
*/
float FitLaneHeight() const;
/**
* @brief 若该网格的**幅值上/下边界**被单值散点顶住,把区间向外让出散点半径。
*
* 只在散点**会被裁切**时才改动区间("散点本来就完整可见"不动它,保持自动自适应的
* 语义);改动时把 `autoRange` 置 false 并写入显式区间。
*/
void ApplySinglePointValueMargins(int laneIndex, const std::vector<FitContribution>& items,
float laneHeight);
/**
* @brief 「所有可见数据点都是单值信号、且时刻完全相同」时的自适应。
*
* 时间轴以 `sharedTime` 居中、**一大格 = 1s**;每个网格的幅值区间保证其数据点
* 完整显示(网格内只有一个取值时以跨度 1 居中)。
*/
void FitSinglePointCluster(const std::vector<std::vector<FitContribution>>& perLane,
double sharedTime);
/**
* @brief 取某信号**唯一的有效数据点**(时刻 + 原始取值)。
@@ -478,21 +506,6 @@ namespace jdi
bool SinglePointOfSignal(const SignalData& signal, const TimeAxis& axis, double& time,
double& value) const;
/**
* @brief 时间轴**一小格**对应的时间跨度(= 主刻 / 五等分,与网格叠底同源)。
* @param [IN] span 参考跨度。
* @return 一小格的时间跨度(无法判定时返回 0)。
*/
double TimeGridCellSeconds(double span) const;
/**
* @brief 某网格**一小格**对应的幅值跨度(与网格叠底的横向刻度同一套算法)。
* @param [IN] span 参考幅值跨度。
* @param [IN] laneHeight 网格高度(像素,决定纵向刻度密度)。
* @return 一小格的幅值跨度(无法判定时返回 0)。
*/
double LaneValueGridCell(double span, float laneHeight) const;
/**
* @brief 以给定倍率缩放时间轴(以视图中心为锚点)。
* @param [IN] factor 缩放倍率,大于 1 表示放大。