Commit Graph
76 Commits
Author SHA1 Message Date
DeepSeek V4.1 Flash affb9cc340 目录规范化(Inc/Src)+「自由标记」第六轮交互调整
1) 目录规范化:所有 .hpp 只放 Inc,Src 只放 .cpp
   - 4 个 *Internal.hpp 从 Src 迁到 Inc(DataModel/Application/PlotView/SignalPanel)
   - Makefile 固化该约定(注释必须单独成行:写在同一行会把 CXX_INCLUDES_PATHS 整条注释掉)
   - include 全是扁平形式,搬家无需改任何 include

2) 常规模式 = 普通鼠标(未开启「自由标记点选取」)
   - ProbeFreeMarker 早退:不预览、不画纵向光标、单击不固定
   - 补 if (!hitCurve) continue:否则整列光标下 probe.ref 恒有效,任意位置都能固定
   - 固定点仍恒吸附到真实采样时刻;「已吸附」圆环只在稀疏判据成立时画

3) 两个显示开关:按钮改名「自由标记点选取」,其下新增「显示增量 | 显示枚举文本」
   - 默认关闭,只影响已固定点的胶囊文本;预览胶囊恒为「值 = 枚举」
   - 开关行计入面板高度与内容区高度

4) 面板六列:新增恒存在的「序号」列(组内 1 起)
   - 枚举列存在时,无枚举/对不上任何枚举项显示 N/A
   - 列与列之间画竖线;悬停某行时其它固定点封顶 50% 不透明度,移出恢复
   - 删除固定点 ✕ 的悬停浮动提示(PaintFreeMarkerTooltip 实现与声明全部移除)
   - 开启选取模式的网格上,信号悬停提示只显示信号名

5) 缓存键补齐
   - FreeMarkerContents 混入枚举表指纹(旧键会让加上枚举表后仍显示 N/A)
   - PanelSlotKey 混入两个新按钮矩形与开关状态

6) 注释/文档
   - 修正 FreeMarkerProbe、SetLaneFreeMarkerCursor、标记透明度等处的过期说明
   - 空态提示按是否开启选取模式区分文案

验证(全离屏,未占用鼠标/键盘/屏幕)
   - 新增 .preflight/mark5check 52/52
   - 回归:markcheck 67/67、mark4check 40/40、fixcheck 37/37、round3check 31/31、
     round4check 34/34、round5check 5/5、editcheck 20/20、convcheck 18/18、diag 19/19
   - check 136/137(唯一失败项为既有不稳定项,drift=15818 与改动前逐位相同)
   - make release / make debug 零警告;Release 产物 TargetCheck.exe 857,600 字节
2026-10-23 15:50:55 +08:00
DeepSeek V4.1 Flash 79eebc281e 自由标记:面板表头/吸顶/行选中与悬停联动 + 原位显示 + 探测容差与阶跃吸附
- 面板:每个展开分组下方新增一行列表头(标记值/枚举文本/增量/时间点/时间间隔),
  与数据行共用列边界;列宽同时容纳列标题;表头行计入内容总高与面板高度;
  全部无枚举文本时仍整列缺省
- 吸顶:分组栏与表头各自按 max(自然, min(吸顶, 组下边界-自身高)) 钉住,
  组末尾按"表头先走 -> 条目 -> 分组栏最后离场";绘制顺序改为条目->表头->吸顶分组栏,
  被盖住的条目不绘制也不可命中
- 行交互:Lane::freeMarkerSelected(单击选中/再点取消)+ 独立的整行悬停状态
  (整行悬停不会把该行 ✕ 变成 danger 色);悬停时网格内对应固定点整体高亮
  (数据点圆环 + 胶囊强制不透明 + accent 描边,仅视界内)
- 固定点恢复原位显示(删除纵向避让,允许遮挡,回到初版方案)
- 探测判据重写:鼠标到画出来的折线的垂直距离 <= kSignalHitTolerance(与曲线高亮同源),
  命中后吸附到最近采样时刻 => 修复阶跃竖直跳变线无法探测/固定的问题;
  删除已无用的 kFreeMarkerHoverTolerance
- 修复滚动时条目文字串到面板标题栏(GDI 文本不认 GDI+ 裁剪,改用 UiTextClipped)
- 顺带清理过期注释(四列->五列、折起指示器已无横线、探测判据说明),
  RemoveFreeMarker 改为复用 RemoveFreeMarkerAt(选中下标校正只有一处)
- 验证:新增离屏 .preflight/mark4check 38/38(三组 A/B 均如实失败);
  回归 markcheck 61/61、fixcheck 37/37、round3check 31/31、round4check 34/34、
  round5check 5/5、editcheck 20/20、convcheck 18/18;make release/debug 零警告
2026-10-22 21:04:03 +08:00
DeepSeek V4.1 Flash 8f735d51be 修复拆分网格误继承实验性功能 / 阶跃线宽 / 面板拖拽残影 / 自由标记卡顿与交互
- 拆分网格:实验性功能只由原网格的首个信号继承(LaneCarry::owner);
  SplitLane 只让第一份继承;新增 PruneLaneSignalItems 按 lane.signals 过滤四类
  逐信号条目(修复"新网格出现别人的固定点、面板里却查不到")
- 阶跃信号:包络带横向膨胀半径 ceil -> floor(1px 线宽不再把竖直沿撑成 3px)
- 浮动面板:面板位置移出 PanelGlobalKey(拖动单个面板不再重画全部面板),
  新增 vacatedRects 让"上一帧被移动/缩小/关闭的面板腾出的区域"也强制重画
- 自由标记:新增 FreeMarkerContents 内容缓存(键不含视界),大量固定点时拖光标/平移不再逐帧采样;
  透明度滑块与光标模式解耦(始终显示);悬停胶囊恢复不透明(瞄着被遮住的数据点时除外);
  胶囊与面板五列按实测文本撑开,不再省略
- 面板交互:折起指示器与信号列表组名同款三角;GX 徽标/标题栏悬停不改光标、移除徽标气泡;
  标题栏双击滚动到该网格(单击不触发,双击不再折起)
- 验证:新增离屏 .preflight/fixcheck 37/37(三处 A/B 均能如实失败);
  回归 round3check 31/31、round4check 34/34、round5check 5/5、editcheck 20/20、convcheck 18/18;
  make release/debug 零警告
2026-10-21 17:21:09 +08:00
DeepSeek-V4.1-Flash 3400ac7c29 自由标记体验重做 + 全部实验性面板可折起
- 网格内固定点:取值胶囊按已放置矩形做纵向避让(不再互相遮挡);行尾 ✕ 改为与
  胶囊同高的正方形;移除固定点的纵向半透明竖线
- 新增「标记透明度」(0–100%),作用于本网格全部固定点的圆点/胶囊/描边/文字/✕;
  半透明时文字改走 GDI+(GDI DrawTextW 忽略 alpha),100% 时像素零变化;
  ≤2% 时不绘制且不登记 ✕ 命中矩形(看不见的按钮不可点)
- 面板按信号分组:信号色托底分组栏(展开三角 + 信号名 + N 标记点 + ✕,文字/三角/✕
  一律用自适应对比色),点击分组栏收起/展开;条目五列(值/枚举/增量/时刻/间隔),
  首项增列与 Δt 显示 N/A,整个面板没有枚举文本时枚举列整列缺省;行尾仍有 ✕
- 面板内容超高时右侧出现滚动条(分组栏与条目一起滚动),滚轮在内容区翻页;
  底部按钮与「标记透明度」滑块固定在底部、不参与滚动
- 底部按钮改为「光标选取模式」切换类按钮(外观与工具栏切换按钮一致,激活高亮),
  激活时其上方出现「标记透明度」滑动条(与信号样式区的透明度滑块同款)
- 所有实验性功能浮动面板支持展开/折起:标题栏 GX 徽标左侧新增「三角 + 横线」指示器,
  单击切换、双击标题栏切换;折起的面板只有标题栏
- 数据层:LanePanelState.collapsed、Lane.freeMarkerOpacity/freeMarkerScroll/
  freeMarkerCollapsedGroups,LaneCarry 与 SplitLane 一并继承
- 验证:新增离屏 .preflight/perf/mark2check 69/69(含两组 A/B);回归 datacheck 69/69、
  verify 49/49、panelanim 158/158、errcheck 19/19;make release/debug 零警告
2026-10-21 02:59:05 +08:00
DeepSeek V4.1 Flash 067128f46c 自由标记改进:默认不启用光标模式 + 面板四列对齐 + 光标模式切换按钮
用户需求(逐条落地):
1) 默认不启用网格私有光标,只有鼠标处于信号上方时才显示数据点数值;命中
   「自动点线」稀疏判据的信号,只有靠近数据点时才有数值显示;
2) 有数值预览时左键单击固定该数据点(固定点样式不变:半透明纵向细线);鼠标
   下方没有信号曲线时,按下/拖动就是普通的平移视界(选区放大模式下照旧框选);
3) 浮动面板最下方新增切换按钮「启用光标模式」↔「关闭光标模式」;启用后与最初
   实现一致(跟随鼠标的纵向自由光标),并修掉「光标模式下拖不动浮动面板」;
   鼠标处于浮动面板上时网格不画自由光标,面板上的操作不穿透到网格;
4) 面板只列出确实有固定点的信号(否则该信号名条目不出现);
5) 每个信息元素纵向对齐:取值 / 增量 / 时刻 / 间隔各自一列。

数据层
- Lane 新增 bool freeMarkerCursor(默认 false,功能总开关仍是 freeMarker);
  LaneCarry 同步携带。
- PlotView 新增 SetLaneFreeMarkerCursor / LaneFreeMarkerCursor /
  ToggleLaneFreeMarkerCursor,切换时立即作废属于该网格的探针。

探针与绘制
- FreeMarkerProbe 新增 cursorMode。
- 非光标模式:稀疏判据成立但吸附失败 ⇒ 跳过该信号;其余要求鼠标与曲线纵向距离
  在 kFreeMarkerHoverTolerance(8 逻辑像素)以内;都不满足 ⇒ 探针无效 ⇒ 按下即
  普通平移/框选,不会误固定数据点。
- 光标模式:与之前实现一致(网格内始终有效)。
- PaintFreeMarker:纵向线 + 上下把手只在光标模式绘制;取值胶囊/数据点/吸附圆环
  两种模式都画。

浮动面板
- 底部新增「启用光标模式 / 关闭光标模式」切换按钮(layout.button),手型光标 +
  悬停高亮,PanelSlotKey 混入 freeMarkerCursor。
- 只给有固定点的信号发名称行(高度分支与矩形分支同步跳过空信号)。
- 四列固定横坐标(EdgePanelLayout::markerColumns:取值 / 增量 / 时刻 / 间隔),
  面板宽度 340 → 415(= 4 列 + 间隙 + 行 ✕ + 两侧内边距);绘制一律左对齐 +
  省略号截断,双击分行改按列边界判断(不再用 xxxRight - 40*scale 反推)。

修复的缺陷(用户报告)
- 「光标模式下拖不动浮动面板」的真因不是命中判定,而是 OnMouseMove 的
  EDGE_PANEL_MOVE 分支里 switch (m_edgePanelDragKind) 漏了 MARK:拖动坐标被写进
  lane.edgePanel,而布局从 lane.freeMarkerPanel 读 ⇒ 面板纹丝不动。已补 case。
- 鼠标压在浮动面板上时:OnMouseMove 先 TopPanelAtPoint,命中即清空探针并跳过固定
  点 ✕ 的悬停判定;OnLButtonDown 的自由标记分支增加 freeOverPanel 守卫,
  确保面板操作不穿透到网格(也不再绘制穿过面板的自由光标)。

验证(全程离屏,不占用鼠标/键盘/屏幕)
- .preflight/markcheck 重写并扩到 61/61:默认无光标、贴近曲线才有效、远离曲线单击
  不固定且拖动仍是平移、四列几何对齐、按钮切换光标模式、光标模式下拖动面板确实
  移动、面板上不画光标、选区放大共存、Δt 编辑重排、两处 ✕、空信号不出行、深度
  放大吸附、视界外固定点不绘制。
- 回归:editcheck 20/20、round3check 31/31、round4check 34/34、round5check 5/5、
  convcheck 18/18、check 136/137(唯一失败项是此前就存在的不稳定断言)、真实
  MF4-Record.MF4 仍 283 条信号。
- make release / make debug 零警告;Build/Release/Target.exe 834,560 字节。
- 同步更新 Documents/agent_mem.md(§11.2 第 189–193 条 + 文末本轮章节)。
2026-10-20 20:39:42 +08:00
DeepSeek V4.1 Flash dd8f63aa0e 新增实验性功能「自由标记」:网格内跟随鼠标的纵向光标 + 单击固定数据点 + 浮动面板
需求(用户):网格右键菜单「实验性功能」里新增带 √ 的「自由标记」。开启后网格内跟随鼠标
出现一条仅限本网格的纵向光标(上下各一个把手、不显示时间值),穿过信号时只标注离鼠标
最近的数据点;左键单击把该点固定(带 ✕ 可移除,鼠标移开仍显示、超出视界直接不显示),
后续固定点显示为相对前一个的增量;面板按信号分组列出「值(±增量) = 枚举」+「t:… Δt:…」
(两列右对齐、可双击按秒编辑并自动换位);命中自动点线判据的信号要有吸附提示;
两级 ✕ 都要悬停提示;与选区放大共存时原地单击 = 固定、按下拖动 = 框选。

一、数据层
- Lane 新增 freeMarker 开关、freeMarkerItems(FreeMarkerItem{ref, time})、freeMarkerPanel。
  time 是「显示时刻」(画在 TimeToX(time) 处),与光标同一口径;取值必须走
  SampleDisplayedSignalAt(内部处理时间偏移与水平镜像)。
- 新增 AddFreeMarker / RemoveFreeMarker / RemoveFreeMarkerAt / RemoveFreeMarkersOf /
  FindFreeMarker / HasFreeMarkers。同一信号同一时刻不重复加入(相对容差 1e-12);
  freeMarkerItems 保持插入顺序,显示顺序与「前一个固定点」一律由 SortedFreeMarkerIndices
  按时间排序得出(删除某项不打乱其它项下标)。
- LaneCarry 同步携带,勾选/取消勾选信号导致网格重建时不会丢掉已固定的点。

二、网格内交互
- ProbeFreeMarker:仅当鼠标所在网格开启该功能时有效;在网格内全部可见信号里挑屏幕纵向
  距离鼠标最近的数据点。MinorCellSparse 成立时经 SnapTimeToSample 吸附到真实采样时刻,
  绘制侧画一圈圆环作为吸附提示。吸附只看判据,不看当前绘制方式。
- PaintFreeMarker(接在 paintOverlays 里、浮动面板之前):纵向线只在本网格内,上把手尖端
  朝下(与常规光标顶部把手一致)、下把手尖端朝上,不画时间读数;已固定点画低透明度细线 +
  圆点 + 「值(±增量) = 枚举」胶囊 + 行尾 ✕(悬停高亮 + 提示气泡);
  超出视界直接不显示(不复用常规数据点的贴边定格 + 方向三角)。
- 网格内固定点 ✕ 的命中矩形由绘制时登记(m_freeMarkerHits),因为它的位置取决于取值文本宽度。
- OnMouseLeave 清跟随光标与三处悬停态(已固定的点保留)。

三、单击 vs 框选
- OnLButtonDown 在开启该功能的网格上只登记 m_freeMarkerPendingFix,并把拖拽模式设为
  「选区放大 ? RUBBER_SELECT : PAN」;OnLButtonUp 里只有「整段操作没有拖动且选区轴向未定」
  才 AddFreeMarker,否则照旧框选/平移。

四、浮动面板(EdgePanelKind::MARK / PLOT_CMD_LANE_FREE_MARKER = 12)
- 每个可见信号一行名称(右侧带清除该信号全部固定点的 ✕),名称行下方按时间升序列出该信号的
  固定点;没有任何固定点时第一行给灰色提示。
- 行 = 左侧「值(±增量) = 枚举文本」(无尖头括号、首项无增量)+ 右侧两列各自右对齐的
  「t:<时刻>s」与「Δt:+<复合单位>」(自动单位匹配)+ 行尾 ✕。
  两列右边界存进 EdgePanelLayout(markerTimeRight / markerDeltaRight),保证所有条目纵向对齐。
- 双击行:左半编辑自身时刻(FREE_MARKER_TIME),右半编辑相对前一固定点的间隔
  (FREE_MARKER_DELTA,最左边的点没有前一项所以不进入编辑)。提交后只改 time,
  顺序由排序函数自动重排(越过后续点即交换条目位置)。
- PanelSlotKey 把每个固定点的信号 + 时刻、全部 marker 行矩形与两列边界一起 mix。

五、一并修掉的两个自伤
1. EdgePanelWidth 的 MARK 分支原本逐行取采样值算文本宽度 —— 它被 EdgePanelLayoutFor 调用,
   而后者在 TopPanelAtPoint / SyncPanelOrder 里每次鼠标移动调用数次,等于「鼠标一动就做几十次
   采样查询」。改为固定宽度 kFreeMarkerPanelWidth(340 逻辑像素)+ 绘制侧 UiEllipsize。
2. PaintFreeMarker 与跟随光标的胶囊绘制原本是两份重复代码,抽 PaintFreeMarkerCapsule 共用;
   FreeMarkerLabelRects 增加 text 出参,避免绘制侧重复求解一次取值。

验证(全离屏,不占用鼠标/键盘/屏幕)
- 新增 .preflight/markcheck 31/31:探测有效与就近选择、原地单击恰好新增 1 个固定点、
  选区放大共存(单击固定不缩放 / 拖动缩放不固定)、面板行数与时间升序、首项无增量、
  后续项增量与 Δt 正确、两列右对齐、行尾 ✕ 只删 1 项、信号名 ✕ 清空该信号、
  编辑 Δt 后移动并重排、深放大时吸附且时刻精确等于采样时刻、视界外固定点不绘制。
- 回归:editcheck 20/20、round3check 31/31、round4check 34/34、round5check 5/5、
  convcheck 18/18;check 136/137(唯一失败项是本轮之前就存在的不稳定项,上一轮已用
  git stash 在基线复现同一数值);真实 MF4 仍 283/8/18/24 个信号。
- make release / make debug 零警告。
- TargetCheck.exe 833,536 字节(814.0 KiB)—— 比上一轮 +24 KB,首次超出旧的 800 KiB
  红线约 14 KB。已做的瘦身见上;剩余为新功能的真实代码量,若要回到 800 KiB 以内需要
  改变取舍(动态 CRT / UPX / 砍其它功能),本轮按「实现功能优先」未动。

文档:Documents/agent_mem.md 同步 §10 变更日志、§11.2 第 181-188 条硬约束
与文末本轮小节。
2026-10-20 19:47:55 +08:00
DeepSeek V4.1 Flash 53b981a2a9 全项目大检查:修复数据层/UI 真 BUG、清理死代码与过期注释、并评估二进制体积
按「性能 / BUG / 错误 / 过期注释 / 命名与实际含义不符」四项对全仓做了一次审查,
四个只读子审查交叉核对后逐条回到源码确认再改。

一、数据层真 BUG
- MatH5:U8/U16/U32/U64 用逐字节 Cached() 拼装,而 Cached 只服务「完全落在同一个
  64KB 块内」的请求 => 跨块时静默读成 0(U32 整段为 0、U64 高 32 位为 0),分块 B-tree
  的 chunk 偏移/子节点地址被破坏且无任何报错。新增 ScalarLE():命中缓存用缓存,
  否则退回一次 ReadAt;U64 顺带从 8 次缓存查找降到 1 次。
- Mf4Loader::IsBlock 未校验链接表是否落在块内 => DataOffset 越界,随后
  block_len - (data - offset) 在 uint64 上下溢,得到一个约 2^64 的"可用字节数",
  下游 vector(usable+1) 直接 bad_alloc。同时把 offset + bytes > size 改成不产生回绕的写法。
- DecodeRawValue 的整数路径:bit_offset%8 != 0 且位宽 64 时需 9 个字节,先拼成 64 位
  再整体右移会把最高字节静默丢弃 => 改为逐字节带位偏移累加。
- cg_cycle_count 加「数据段总字节 / 记录字节」上限(压缩段按 deflate 最大膨胀比估算)。
- CsvLoader 表头分支同样跳过 #/% 注释行(否则整份文件按 1 列解析并被丢弃)。
- MatLoader:double -> int 的越界转换加了范围检查;普通对象递归加深度上限与成环检测。
- Utf8Util::BuildTicks:double -> long long 越界转换前先判商是否落在 ±9e18 内。

二、界面 BUG
- 工具栏按钮永远不显示悬停高亮:Paint 读的 Button::hover 全仓从未赋值,改为按 m_hoverIndex 判断。
- 菜单打开时"取消底层拖拽"对每个消息都执行,把菜单自己的 Target::EDIT_FIELD 也清掉
  => 菜单内联输入框无法拖拽框选。排除 EDIT_FIELD。
- GdiplusShutdown 之前未释放 PlotView 的 GDI+ 位图(Application 是进程单例,成员析构在
  WinMain 之后)=> 新增 PlotView::ReleaseGraphicsResources() 并在 Shutdown 里调用。
- WM_KILLFOCUS 现在会提交标签页/绘图区编辑并复位捕获与拖拽状态。
- WM_LBUTTONUP 不再抢走中键平移的鼠标捕获。
- TextEdit:被过滤器拒绝的字符返回 false;无选区时 Ctrl+X 只复制、不再清空整段文本。
- SignalPanel::InvalidateRows 作废编辑/待确认/改名计时等行下标状态。
- EndFilterEdit 改走 ApplyFilterText(否则输入纯空格后回车,列表仍停在筛选结果)。
- 新增 SignalBadgeVisible 作为唯一判据(原来命中只看"可见",绘制还要求"已分配网格")。
- 折起动画结束后夹取滚动量;双击分组名改名时清掉该组正在播放的折起动画。
- ScrollToSignal 复用 SignalScrollTarget(含悬浮组名避让)。
- RowNameRect 与绘制使用同一最小宽度(原来退化时返回空矩形)。
- 分隔条拖拽上限改用与 Relayout 同一公式;UpdateDragCursor 补 SPLITTER/EDIT_FIELD。
- SignalPanel 里几个守卫改为判断真正被解引用的指针;分组行 ✕ 也返回手型光标。

三、PlotView
- FindStyleOnlyLanes 改用完整的 SceneStyleEntry::operator==(原来只比 4 个字段,
  另一个网格同时改"调整"时带着旧像素被当成最新)。
- 宽基底图层不再烘焙拖拽光晕(LaneLayerHash 不含它),改由 PaintHoverHighlight 每帧叠加。
- PaintLaneLegend 提前返回前清掉本网格的命中矩形(否则能点中看不见的图例条目)。
- 面板主体改为完全不透明(消除平移后 4% 透出的旧波形,零额外代价)。
- PAN / Shift+滚轮改走 SetTimeView,使"自动点线"判据随平移重算。
- 内联输入框命中字体与绘制统一为 monoSmall。
- 派生曲线取值与窗口按 SourceIndexFor 映射(水平镜像时不再读到镜像另一端)。
- EdgePanelWidth 补 style/axis 空指针守卫;二分结果夹到 count。

四、清理
- 删除 Toolbar::ActionLabel、Theme::statusBg、PopupMenu 的 editResult 死机制、
  Toolbar::Layout 的重复 clear()、HitRow 的恒假分支、OnDoubleClick 的重复判定、
  PaintUtil 里孤立的注释块。
- 修正一批过期/错误注释(包络带"不做加宽"、LaneLayerHash 的图例说法、
  均值滤波曲线的窗口说法、样式区行数与「调整」按钮顺序、FIT_ALL 退出缩放模式、
  RemoveSignal 的 ✕ 位置、.mat/.dat 格式文档、MatH5 的"映射视图"、DataModel 的
  "点线/被移除文件"、SignalPanel 的"行高连续缩小"、TextEdit 的 PaintUtil 引用等)。

五、二进制体积评估(按要求只评估,未做压缩修改)
- release 809,472 字节;.text 652KB、.xdata+.pdata 约 75KB、.rdata 42KB、
  .rsrc 20KB、.idata 11.5KB。
- 去掉 -ld3d11 -ldwmapi -ld3dcompiler(仓内无任何调用)实测 0 字节收益
  (静态库只拉取被引用的对象)=> 删链接库不是压缩手段,只是配置整洁。
- 仓内已无 <iostream>/<sstream>/<filesystem>/<regex>/<locale> 等体积炸弹。
- 结论:已无明显的浪费可捡;要显著变小只能改变取舍(动态 CRT / UPX / 砍功能),
  需要用户决策,本轮未动。

验证
- editcheck 20/20、round3check 31/31、round4check 34/34、round5check 5/5、convcheck 18/18。
- check 136/137:唯一失败项经 git stash 回到改动前复现同一数值,属该探针自身的既有
  不稳定项(取决于前置用例留下的缓存/时序),与本轮改动无关。
- 真实数据:4 个 MF4 => 283/8/18/24 个信号;.mat 样例 8 信号、
  带无障碍对照-防夹力曲线数据 8 信号、BIG_MAT.mat 5 信号(私有内存 +741MB)。
- make release / make debug 零警告。

文档:Documents/agent_mem.md 同步 §10 变更日志、§11.2 第 171-180 条硬约束
与文末本轮小节(含体积评估数据)。
2026-10-20 16:10:29 +08:00
DeepSeek V4.1 Flash 30f853a361 MF4 ##CA 全字段/三维 + cc_type=8 区间枚举 + 稠密包络带线宽双向加粗
一、##CA 数组通道:从「按魔数猜」改成「按记录容量校验」
- 头字段全部按块长度做边界检查后读取:+0 ca_type / +1 ca_storage /
  +2 ndim(U16) / +4 flags(U32) / +8 ca_byte_offset_base(U16) /
  +10 ca_inval_bit_pos(U16) / +16 dim_size[](U64,步长 8),
  其后才是**可选**的 cycle_count[](U32)。
  ⚠️ 实测 CANape 写出的文件没有 cycle_count(块长度恰好到 dim_size 就结束),
  盲读会把下一个块头当数字(实测得到 1481909027)。
- 展开条件(任何一条不满足就原样保留,只显示元素 0,绝不猜):
  ca_storage == 0;ca_type <= 2(0 数组 / 1 缩放轴 / 2 区间轴,三者物理布局相同);
  cn_bit_count ∈ [1, 64];cycle_count 每维 <= 1(元素不被拆到多条记录);
  bit_offset + 元素数 × 位宽 <= cg_data_bytes × 8(真正的完整性校验)。
  规模护栏:元素数 <= 16384 且 元素数 × 记录数 <= 32M 单元格
  (取代原来的 count > 1024 魔数)。
- 元素名按 dim_size 从末维向前取余,1/2/3 维分别得到 [i] / [i][j] / [i][j][k]。
- MF4-Record.MF4 仍是 283 个信号(25 个 ##CA 全部通过新的容量校验,最大 7x9=63)。

二、cc_type = 8(值 -> 文本,区间表)补齐
- cc_val[] 交错存放 (下限, 上限),cc_ref[4 + i] 是第 i 段文本(最后一个引用是默认文本,不使用);
  cc_val_count 必须是偶数(奇数说明表残缺,整表丢弃);下限 > 上限时交换。
- SignalEnumText 新增 rangeMin / rangeMax / rangeTexts,并拆出 HasExact() / HasRanges();
  Find() 先查精确表(cc_type = 7)再查区间表(cc_type = 8,闭区间)。
- 界面没有改动:两种枚举共用同一出口,只在**光标取值标注**追加 " = <文本>",
  常规状态零变化。

三、修复:稠密包络带下改变线宽时竖直沿不变粗
- 根因:包络带每列只有 1 个 x 坐标,旧实现只在 top/bottom 各外扩半个线宽
  => 水平段会变粗,但方波竖直沿(上升/下降段)的横向宽度恒为 1 个像素列。
- 修法:EmitEnvelopeBand 新增 halfWidth 形参,先对上下包络做横向窗口膨胀
  (top 取窗口最小、bottom 取窗口最大,半径 ceil(halfWidth / 列距)),再纵向各外扩;
  膨胀只在同一段内生效,不会跨 NaN 断点连线。

验证(全离屏,不占用鼠标/键盘/屏幕)
- .preflight/convcheck 18/18:新增 3 维命名与取值、元素放不进记录时必须拒绝展开、
  cc_type = 8 的命中/未命中。
- 新增 .preflight/round5check 5/5:合成 20 万采样数字信号(必然走包络带),
  在网格中间行测量竖直沿横向像素宽度:线宽 1px -> 6px,10px -> 30px。
- A/B:临时把横向膨胀半径置 0(等价旧行为)=> 1px -> 2px、10px -> 2px 且如实 FAIL。
- 回归:check 137/137、editcheck 20/20、round3check 31/31、round4check 34/34;
  真实 MF4 信号数 283/8/18/24 不变;make release / make debug 零警告;
  TargetCheck.exe 808,448 字节(789.5 KiB)。

文档:Documents/agent_mem.md 同步 §10 变更日志、§11.2 第 165-170 条硬约束与文末本轮小节
(含「6 位有效数字」的口径说明)。
2026-10-20 14:29:49 +08:00
DeepSeek V4.1 Flash b315201598 MF4 补齐数值换算 2/3/4/5/6 + ##CA 数组通道 + 数值型 VLSD
标定量(特性曲线 / 公式)与「一个通道装整条曲线」的数组通道是 CANape /
Simulink 导出 MF4 时最常见的两种表达,之前只支持线性换算、数组通道整列丢失。

一、数值换算(cc_type 2/3/4/5/6)
- ChannelSpec 把 linearConversion/offset/factor 换成 ConversionType(0-8)+
  conversionValues[],ParseChannel 按 cc_type 校验参数个数后填充。
- 新增 ApplyConversion:
  1 线性  v0 + v1*raw
  2 有理式 (v0 + v1*raw) / (v2 + v3*raw)
  3 代数式 (v0*raw^2 + v1*raw + v2) / (v3*raw^2 + v4*raw + v5)
  4 表插值(交错 (键,值) 对,区间内线性插值、区间外夹紧)
  5 表阶梯(同一张表,取键 <= raw 的最后一段)
  6 区间映射((下限,上限,物理值) 三元组)
- 参数个数不符 / 表为空 / 分母为 0 一律返回原始值(宁可显示未标定值,
  也不显示编造值);线性换算 v0 == v1 == 0 视为未配置而非恒为 0。

二、##CA 数组通道(一个通道装整条曲线)
- 解析 CN 链接[1] 的 ##CA 块:+0 ca_type / +1 ca_storage / +2 ndim(U16) /
  +4 flags(U32) / +16 dim_size[U64](步长 8,Simulink 与 CANape 实测一致)。
- 新增 ExpandArrayChannel:仅当 ca_storage == 0(元素在记录内首尾相连)、
  1 < 元素数 <= 1024、bit_count != 0 时展开成多列,bit_offset += i*bit_count,
  名字 name[i] / name[i][j],换算与枚举复制给每个元素。
- MF4-Record.MF4 信号数 124 -> 283(9 点特性曲线读到 6000..14000,
  二维 map 读到 175/120/180...)。

三、VLSD 数值型
- 数值型不再要求 cn_bit_count > 0;新增 InferVlsdElementBits(按样本长度
  优先整除 64,再 32/16/8),DecodeVlsdValue 只按一个元素做越界检查并取
  样本首个元素;主通道候选排除 VLSD(其记录里存的是 ##SD 偏移)。
- 实测 4 个真实文件里 VLSD 通道只有 Comment(字符串、cycles = 0),
  数值型 VLSD 数量为 0,故本次不改变现有文件的输出。

验证(全离屏,不占用鼠标/键盘/屏幕)
- 新增 .preflight/convcheck:现场合成一个最小 MDF4(1 DG / 1 CG / 2 记录 /
  recordBytes = 72),一次覆盖 cc_type 1-6、##CA、数值 VLSD => 11/11。
- A/B:强置 conversion = NONE 且关闭数组展开 => 8 项如实失败,还原后 11/11。
- 回归:check 137/137、editcheck 20/20、round3check 31/31、round4check 34/34;
  真实文件信号数 283 / 8 / 18 / 24 与预期一致。
- make release / make debug 零警告;TargetCheck.exe 804,352 字节(785.5 KiB)。

文档:Documents/agent_mem.md 同步 §10 变更日志、§11.2 第 160-164 条硬约束
与文末本轮小节(含 VLSD / 单位在文件中的实际情况报告)。
2026-10-20 11:25:03 +08:00
DeepSeek-V4.1-Flash 181f95cc3a MF4 补 ##SD/##SI/VLSD + 信号枚举文本 enumText + .mat 改随机读(流式进度)
1) 修复上轮记录的两个已知问题
   - MatLoader: LoadPlainObject 不再为拿 dims 调 ReadDataset(整块解码第二次),
     改用 ReadDatasetInfo;普通 v7.3 大矩阵的时间/内存不再翻倍。
   - MdfFile::TextBlock 注释与实现对齐:真的支持 ##MD(UTF-8 XML,无长度前缀)。

2) 信号枚举信息(以 TestData/NEW-MF4.mf4 为目标)
   - 新增 SignalEnumText(DataTable.hpp):values[] + texts[] + Empty()/Find(),
     命名用 enumText —— 它描述"取值代表哪个状态",不是单位。
   - DataTable::columnEnumText 与 columns 同下标;SignalData::enumText 由
     BuildSignalGroup 移动赋值;其它格式(CSV/.mat)保持为空。
   - MF4 cc_type = 7:cc_val[i](F64,数据段 +24)↔ cc_ref[4 + i](##TX/##MD 文本),
     最后一个引用是默认文本,不使用。
   - 界面只在**光标取值标注**追加 " = <枚举文本>"(等号两侧各一个空格、无尖括号),
     查表用采样原值(ApplyDrawTransform 之前);常规状态显示一个像素都不变。
   - 实测:NEW-MF4.mf4 8 信号 / 4 条带枚举(32+5+32+5),MF4-Record.MF4 另有 2 种(12/32)。

3) MF4 支持范围
   - ##SI 采样索引:只有索引 ≥2 条且全部 < 周期数才生效(逐记录有效性位图)。
     实测部分工具给每个通道挂只含常量 513/4 的 ##SI,必须忽略,否则整列变 NaN。
   - ##SD + VLSD(cn_type == 1):##SD 数据段是「UINT32 长度 + 字节」样本序列,
     记录里放 64 位偏移;AppendSignalData 递归展开 SD/DZ/DL/HL(单通道上限 512MB),
     DecodeVlsdValue 先按直接偏移、失败再按 >>1 回退,取样本第一个元素。
   - CN 的 cn_data / md_unit / md_comment 下标按链接总数反推:8 链接(MDF 4.0 风格)
     = 换算[4]/数据[5]/单位[6]/注释[7],9 链接(4.1)= [5]/[6]/[7]/[8]。
     实测 Simulink 导出的“4.10”仍写 8 链接,不能写死 Link(cn,4/5)。
   - 主通道:cn_sync_type 1/2/3(时间/角度/距离)→ 名字 t/time/timestamp/zeit/时间
     (含 t[...])→ 第一个数值通道兜底;cg_flags 第 2 位(远程主通道)跳过 sync 判定。
   - 记录长度优先 cg_data_bytes(CG 数据段 +24);旧实现把 U8(cgData) 当
     cg_record_id_len,其实是 cg_flags 低字节(总线事件位)。
   - 进度:先走 DG 链得到真实组数(不再写死 64),hasBytes = true,bytesRead 取
     MdfFile::ReadAt 累计(夹到文件大小);4MB 分块读取检查取消。
   - ##DZ:数据段前有 28 字节头(original_type/zip_type/param/original_size/zip_size),
     旧实现整段当 zlib 流是错的;只支持纯 deflate(zip_type = 0)。
   - 一个信号都没有时,错误信息附带「布局无法解析 / 缺少时间通道 / 空数据组」计数。

4) .mat:内存映射 → 随机读 + 元数据块缓存(消除 SEH 风险)
   - MatHdf5::Open 改为 CreateFileW(FILE_FLAG_SEQUENTIAL_SCAN) + ReadFile;
     删除 MappedBytes,改为 ReadAt / Cached(16 × 64KB LRU + 上次命中槽)/ Tag /
     ReadData / ReadCString。I/O 失败只会变成 false,不再触发页访问异常。
   - 数据读取:连续布局按 4MB 切片;分块布局按需 + 64KB 对齐。
     固定 4MB 预读在交错存放的分块数据集上会把读盘量放大到 2.2 倍(2874MB/1293MB),
     改后 1301MB。
   - 关键提速:dims=[N]、chunk=[32768,1] 归一化后 rank=2、每行 1 字节,逐行算坐标
     是 12.9 亿次迭代;新增「rank == 2 且 chunkDims[1] == dims[1]」快路径(整块一次
     sink)。BIG_MAT.mat(1.29GB / 5 × 16,142,471):21.5s → 7.9s,私有内存 +741MB。
   - 进度:Open 立刻上报「0 / 总大小」,之后每读一块回调 ReadProgress(bytesRead,total)
     → MatLoader 映射到 0.02–0.98 并单调夹紧(字节进度与逐流解码取较大值),收尾 1.0。
     遮罩层百分比不再一上来就 100%;取消会中止(StreamDataset 返回 false),不留 NaN 空洞。
   - 连续布局数据不足(文件被截断)现在明确失败,不再静默截短。

验证(全部离屏:无可见窗口、不注入鼠标键盘屏幕)
- 新增 .preflight/perf/loadercheck(数据层)32/32:
  NEW-MF4 8 信号 / 4 条枚举 / 枚举文本逐条核对 / 9396 与 94 行;
  MF4-Record 124 信号 / 3 轴 / 4 条枚举;data-sample.mat 4 组 8 信号 / 行数;
  BIG_MAT.mat 5 信号 × 16,142,471 + 进度(total 已知、ratio 单调、bytes 递增、
  起点 ≤0.05、终值 1.0、更新 ≥10 次)。
- 新增 .preflight/perf/enumui(真实 Application + SW_HIDE)12/12:
  只显示 gVR_WinOpenReq[0],光标落在枚举采样点上 —— 标签宽度 265px;
  临时清空 enumText 后 25px;差值 240px 与 " = WinOpenReqAtDrvr_Idle" 的
  UiMeasureTextWidth 完全一致;恢复后宽度回到 265px。
- 回归:datacheck 69/69、matcheck 21/21、matui 15/15(loading bytes 达到总大小)、
  verify 49/49。
- make release / make debug 零警告。Build/Release/TargetCheck.exe = 820,224 字节
  (上轮 785,920,+34KB)。(Target.exe 正在被用户运行的程序占用,链接步骤未覆盖它。)
2026-10-20 01:56:47 +08:00
DeepSeek V4.1 Flash 75f0fafd41 .mat 改为流式解码(内存映射 + 逐 chunk 填充);MF4 支持 ##DL/##DZ/##HL;光标数据点被时间面板遮住时改三角
1) .mat(BIG_MAT.mat 1.29GB / 5 信号 x 16,142,471 采样)
   旧链路同时驻留三份内存:整文件 fread 进 vector、整个数据集解压成字节数组、再 memcpy 成
   double;外加 streams 缓存与每条信号各一份时间列拷贝。实测私有内存 +1233.9MB、117s。
   现改为:
   - MatHdf5::Open 用 CreateFileW + CreateFileMappingW + MapViewOfFile(MappedBytes 只读视图,
     解析代码零改动;虚拟地址不等于物理内存,未访问页不驻留);
   - 新增 ReadDatasetInfo(只读元信息)+ StreamDataset(ByteSink)(连续布局 4MB 切片、
     分块布局逐 chunk inflate)+ ReadDoubleStream(SDI #data# 字节流按 8 字节一组直接写 double,
     7 字节尾巴处理非 8 对齐 chunk);ReadNumbers 改为流式转换;
   - MatLoader 先收集信号清单,再按(分组,时间流)合并为一个 TableBlock:时间列按内容去重、
     数据列直接 move;进度改成 0-0.02 映射 / 0.02-0.98 逐流解码 / 1.0。
   结果:117.3s -> 12.5s,私有内存 +1233.9MB -> +740.4MB(最终列数据 738.9MB)。
   关键性能修正:SDI 的 #data# 是 dims=[N]、chunk=[N,1],归一化后每行只有 1 字节;旧的逐行回调
   (且每行分配 coordinate 向量)只有 11MB/s。把目标连续的行合并成一次回调后约 110MB/s。
   顺带把 inflate 的 Huffman 解码改成 9 位快查表,并抽到新的 Deflate.hpp/.cpp 供 MF4 复用。

2) MDF4 数据块
   Problem-MF4.mf4 的第二个数据组把记录写在 ##DL(数据列表,两个 ##DT:131040+504 字节)
   里,旧实现只认 ##DT,整组被跳过,于是 8 个信号只读出 4 个。
   新增 CollectDataSegments(递归展开 ##DL / ##HL,叶子识别 ##DT / ##DV / ##DZ)与
   ReadRecordSegments(跨段拼接、压缩段先 inflate、非压缩段仍按 4MB 分块读),
   压缩段的记录长度改用 max(cg_record_id_len*8, 各通道 bitOffset+bitCount) 推算(载荷是压缩长度)。
   实测 8 个信号:gVP_WindowLearnSt[0] 94 行、gVR_WinOpenReq[0] 9396 行,时间列单调。
   ##SD(已排序数据)仍未实现,这类数据组会被跳过。

   另按要求只做检查、不改代码:该文件中 gVP_WindowLearnSt[0] 与 gVR_WinOpenReq[0] 都存在
   值枚举文本(##CC,cc_type=7;links[4+i] 对应 cc_val[i]),清单已写入 Documents/agent_mem.md。

3) 光标数据点标记
   光标的时间读数与差值胶囊绘制在绘图区底部、且在取值标注之后绘制,靠近底部的数据点圆点
   会被完全盖住。新增 MarkerCoveredByCursorLabel(圆心落入面板或重叠面积达到半个圆)
   与 CoverHiddenMarker(改成向下的三角、锚点放到面板上沿上方),在原始信号与派生曲线胶囊
   两处调用,语义与“数据点在视界下方”一致。

验证(全部离屏:SW_HIDE 窗口 + 只注入合成消息 + BELOW_NORMAL,不占用鼠标键盘屏幕):
- 新增 .preflight/round4check 34/34;A/B:临时禁用 ##DL 支持 => 2 项如实失败;
  临时禁用 CoverHiddenMarker => 3 项如实失败(含像素断言为 0);
- 回归:.preflight/round3check 31/31、editcheck 20/20、check 137/137;
- make release / make debug 零警告;Build/Release/Target.exe 785,920 字节(< 800KB)。
2026-10-19 20:13:17 +08:00
DeepSeek V4.1 Flash be76f523f3 修复组名编辑态点击穿透;浮动面板 GX 徽标悬停提示 + 点击滚动联动;最小网格高度提升到 81 逻辑像素
1) 组名/信号名处于编辑态时,在编辑框内双击、快速连点或框选编辑文本会让分组展开/收起。
   两处根因:
   - 行高大于输入框高度(上下各留 3 逻辑像素),起点落在内边距上时 OnEditMouseDown 返回
     false,宿主把按下交给 OnLButtonDown,命中分组行的 BODY 分支 ⇒ 折叠状态被切换;
   - 窗口带 CS_DBLCLKS,快速第二次按下被合成为 WM_LBUTTONDBLCLK,而 OnDoubleClick 没有
     编辑态保护,直接进入分组行双击逻辑 ⇒ 再次折叠 / 再次改名。
   修法:新增 NameEditAbsorbRect(输入框横向范围 × 整行高度)作为编辑态的“文本吸收区”:
   OnLButtonDown 命中即按文本操作处理(落点夹回输入框再算插入符),OnDoubleClick 命中即
   直接返回。横向仍限定在名称区域,同一行的复选框 / 「N 项」/ GX 徽标 / ✕ 继续可点。

2) 所有实验性功能浮动面板标题栏左侧的 GX 徽标:
   - EdgePanelLayout 新增 badge 矩形,成为绘制 / 悬停 / 手型光标 / 点击联动的唯一几何来源
     (命中顺序 close → badge → title,徽标在标题栏内部,放到 title 之后会被判成拖面板);
   - 悬停时徽标高亮 + 气泡提示(“G12 · 点击滚动到该网格” / “G12 · 已在视界内”),气泡在
     面板像素图层存进缓存之后绘制,避免被烘进位图后在鼠标移开后残留;
   - 点击调用 ScrollLaneIntoView:目标网格不可见时启动与“点选信号联动滚动”同源的平滑动画,
     已完整可见时不做任何改动。

3) 网格被压缩时的最小高度由 54 提升到 81 逻辑像素(1.5 倍)。同时修掉 m_minLaneHeight 被
   二次乘 m_scale 的隐患:Layout 里不再预先乘缩放,滚动条判定改用 laneCount × 81 × scale,
   与实际网格高度使用同一换算(125% 缩放下 128 个网格实测 101.25px = 81×1.25)。

验证(全部离屏:SW_HIDE 窗口 + 只注入合成消息 + BELOW_NORMAL,不占用鼠标键盘屏幕):
- 新增 .preflight/round3check(真实 TestData/MF4-Record.MF4,128 网格)31/31;
  A/B 临时禁用双击保护 ⇒ 1 项如实失败(证明断言有判别力);
- 回归:.preflight/editcheck 20/20、.preflight/check 137/137;
- make release / make debug 零警告。
2026-10-19 14:01:19 +08:00
DeepSeek V4.1 Flash 1e50494c41 修复列表内联编辑(组名/信号名)看不到插入符与选区;把 UI 基础层迁入 Components/Shared
1) 行位图缓存键漏了插入符与选区:信号列表整行走 PaintRowCached,RowCacheKey
   在编辑态只混合“是否编辑 + 文本” ⇒ 左右方向键与鼠标框选虽然改变了 TextEdit
   状态(caret/anchor),键却不变 ⇒ 贴回旧位图。表现就是“无法部分选择、
   鼠标框选无反应、光标不动”,而绘图区的光标时间输入(无行缓存)一直正常。
   修法:GROUP_TITLE / SIGNAL_NAME 两个分支编辑态把 Caret / SelectionBegin /
   SelectionEnd 一并 mix 进键;新增 SignalPanel::EditFieldFont() 统一
   绘制(uiSmall / ui)与鼠标命中、插入符换算所用字体。

2) Theme / PaintUtil / TextEdit / PopupMenu / ColorPickerPopup 由
   Utilities/{Inc,Src} git mv 到 Components/Shared/{Inc,Src}:
   Utilities 只保留纯数据/逻辑模块(DataModel、DataTable、各 *Loader、MatH5、
   Utf8Util),Shared 成为自洽的 UI 基础层(Theme → PaintUtil → UiControls →
   PopupMenu / ColorPickerPopup)。依赖方向仍是 Components → Shared → Utilities;
   PaintUtil 一并迁移是因为它依赖 Theme(否则构成 Utilities → Shared 反向依赖)。
   Makefile 无需改动(include 平铺,两个目录都已在搜索路径),但需清理 Build/*
   里对应的旧 .o/.d。

验证(全部离屏,不占用鼠标/键盘/屏幕):
- 新增 .preflight/editcheck 20/20;A/B 修复前 6 项如实失败(键不变 + 屏幕不重绘);
- 既有 .preflight/check 137/137;
- make release / make debug 零警告。
2026-10-19 11:11:31 +08:00
DeepSeek-V4.1-Flash f44f8021e3 Release 产物 1.74MB → 796KB(去掉 <filesystem> 与 zlib)+ .mat 加载进度不再停住
一、体积膨胀的真因(用 -Wl,-Map 逐对象/逐节对比上一提交定位,只有两个)
1) <filesystem>(致命,约 +900KB):MatH5::Open 用 std::filesystem::file_size 取文件大小,
   MinGW 的 libstdc++ 因此链入 fs_path/fs_dir/fs_ops/c++locale/codecvt/collate_members/
   cow-wstring-inst/cxx11-wlocale-inst/fstream-inst/cp-demangle 整套 locale/iostream 机器
   (归档成员 415→538);还把 64KB 静态表从 .bss 挤成 .data(1.8KB→73.8KB)。
   修法:改用 _wfopen_s + _fseeki64/_ftelli64 取大小。
2) -lz(约 46KB):只需要"解开 HDF5 分块"。修法:MatH5 内置紧凑 inflate
   (RFC1950/1951:固定/动态 Huffman、存储块、长度-距离回抄),Makefile 与探针脚本移除 -lz。
顺带把 UploadHaloLayer 里 64KB 的预乘 alpha 查表换成 (v*a+128)*257>>16(无除法、无表)。

二、压到 800KB 以下的两项无损措施
1) 图标资源无损重编码:ico 里 64/48/32/16 四帧原为未压缩 32bpp DIB(合计 32KB),
   用自写工具(GDI+ 编码)改为 PNG 帧(ICO 的 PNG 条目 Vista+ 全支持,本文件本就有 256 PNG 帧):
   43,334 → 17,404 字节。像素逐点校验:差异 ≤85 像素、最大分量差 ≤3(编码取整),肉眼不可辨;
   并从**真实产物** LoadImageW 逐尺寸(16/32/48/64)验证可加载。
2) RELEASE_OPT 由 -Os 改为 -Oz(省约 6KB);实测动画帧 p50 7.73ms(-Os) vs 7.77ms(-Oz),
   无可辨识差异。
放弃的手段:-ffunction-sections/-fdata-sections + --gc-sections(反而 +9KB)、LTO(工具链 ICE)、
-fno-asynchronous-unwind-tables(链接失败)、-fno-exceptions(会破坏 catch (bad_alloc))。
结果:Build/Release/Target.exe = 795,648 字节。

三、.mat 加载进度不更新(已加载大小 / 总大小 / 百分比都停在 0)
真因:LoadMatSourceGroups 只在结束时回调一次 ratio=1.0,且从不设置 hasBytes;
读 42MB 文件与解码 19 个样本流(解压+拷贝,占大头)期间进度恒为 0。
修法:
- MatHdf5::Open 新增 OpenProgress(bytesRead,totalBytes),按 4MB 分块读取并回报;
- MatLoader 用 ProgressReporter 把进度拆成 0.00–0.55 读文件(hasBytes=true)与
  0.55–0.98 解码样本流(每解完一个流推进 done/total),末段 1.0;
- 取消与出错分开:取消时清空 error,不弹错误框。
实测:回调 29 次、字节数变化 11 次、比例单调、终值 1.000、bytes=42,386,344/42,386,344
(= 文件大小);Application 侧断言 m_loadingBytesRead == m_loadingTotalBytes == 文件大小。

四、验证(全部离屏,不占用鼠标/键盘/屏幕)
- matcheck 21/21(新增进度断言)、matui 15/15(新增图标 + 加载字节断言);
- 回归 verify 49/49、panelanim 158/158、gridanim 93/93、hoverdrag 20/20、lanescroll 23/23、
  datacheck 69/69、edgecheck 46/46、errcheck 19/19、halocheck 16/16、panflash 19/19、
  panelstutter 8/8、stress 6/6;
- make release / make debug 零警告。

文档:Documents/agent_mem.md 追加本轮小节(含 5 条新规则:MinGW 下 <filesystem> 是体积炸弹、
链一整条库前先算账、膨胀要看节表而非 .o 大小、图标可无损重编码并用产物校验、
长解析必须按阶段回报进度且 hasBytes 只在有字节时置真)。
2026-10-19 02:37:52 +08:00
DeepSeek-V4.1-Flash 5b24dacee0 悬停高亮恢复旧版外延观感 + 支持 MATLAB .mat(v7.3 / HDF5,嵌套分组)
一、悬停高亮「几乎看不到、只是曲线换了颜色」(对比提交 f845616 的旧版观感)
根因:调色板第 1 色就是主题强调色(0xFF268BD2),而上一轮为提速把稠密信号的光晕改成
「按包络带原始几何 FillPath」——与曲线**逐像素同形**,于是曲线上的那层半透明强调色
完全看不出变化;旧版观感来自「lineWidth+4 的描边笔向两侧各外扩 2~3px」。
修法(PlotViewPaint.cpp::PaintSignalHaloFor):
1) 逐列成对填四边形,上下左右都外扩 grow = (lineWidth + 4) * scale / 2
   (竖向跃变处旧版 5px 笔涂出的正是那条整高色带);
2) 不能把整条包络拼成一个多边形:满幅方波相邻列区间互相交叉,缠绕填充规则会在交叠处
   相互抵消 ⇒ 填充变成一条条竖缝(实测只覆盖约一半带宽);
3) alpha 从 0x55 调回 0x66(与旧版描边笔一致)。
代价仍只在「换信号/换视界/改样式」时付一次,此后每帧只 AlphaBlend。
三维对照(新增 haloshot 探针 + f845616 工作树构建):变化像素 旧 73726 / 上一轮 13529 /
本轮 74916,目视与旧版一致(跃变处的整高色带恢复)。

二、MATLAB .mat(v7.3 = HDF5)支持,含嵌套分组
新增只读 HDF5 子集(不引第三方库):Utilities/Inc/MatH5.hpp + Utilities/Src/MatH5.cpp
- 超级块 v0/v2;对象头 v1(含续块,主块长度以 headerSize 为准);符号表(B-tree v1 +
  本地堆)与 link 消息;数据空间 v1/v2;数据类型 v1;数据布局 v3(紧凑/连续/分块);
  分块数据集支持 deflate(zlib);地址一律相对基地址解析。
新增 MATLAB 语义层:Utilities/Inc/MatLoader.hpp + Utilities/Src/MatLoader.cpp
1) Simulink SDI / SimulationData.Dataset(识别 #refs# + #sigstream#):
   · #refs# 元素有 ElementType(signal/dataset)、Name、Values;
   · timeseries 的 Values/DataR2、Values/TimeR2 是 #sigstream#/<n> 的流编号;
     #data# 是 uint8 字节流,按 8 字节一组解释为 double(与 #length#/#sampleSize# 一致);
   · 子数据集的 Values 是对象引用数组(子元素)⇒ 还原嵌套树;
   · 分组名 = 从根到信号的各级数据集名用 - 连接(如 A/A1/XYZ → 组 A-A1、信号 XYZ),
     信号名取元素 Name(空则退回 timeseries 的 Name,再退回 信号N)。
2) 普通 v7.3 变量:顶层数值矩阵按「列 = 信号、行 = 采样」读取(时间轴 = 采样序号),
   结构体按字段递归。
加载管线扩成多分组:新增 LoadSignalSourceGroups()(DataLoader)→
AsyncLoadState::groups → Application::LoadFile 逐个 AddGroup;其它格式仍只有一个分组。
文件对话框与 IsSupportedSignalFile 增加 .mat。

三、验证(全部离屏,不占用鼠标/键盘/屏幕)
- 新增 haloshot(截图对照 f845616)、matcheck(15/15:4 组 8 信号、时间轴单调、采样数正确)、
  matui(11/11:Application::LoadFile 端到端把 4 个分组加进数据层与信号列表);
- 回归 verify 49/49、panelanim 158/158、gridanim 93/93、hoverdrag 20/20、lanescroll 23/23、
  datacheck 69/69、edgecheck 46/46、errcheck 19/19、halocheck 16/16、panflash 19/19、
  panelstutter 8/8、stress 6/6;
- make release / make debug 零警告;Makefile 与探针构建脚本补 -lz。

文档:Documents/agent_mem.md 追加本轮小节(含 4 条新规则:叠加层必须自带外扩、
缠绕填充不可用于自交包络带、HDF5 主消息块长度看 headerSize、
MATLAB 分块维度可能比数据空间多一维、按右侧补 1 对齐)。
2026-10-19 00:52:41 +08:00
DeepSeek-V4.1-Flash 24f0bf849d 修复悬停高亮变长方形色块/联动失效 + 信号列表动画卡顿真因(行位图缓存)
一、悬停高亮恢复"整条曲线发光"(上一轮裁成鼠标附近一段 ⇒ 满幅方波上就是个长方形色块)
光晕的语义是"整条曲线一起发光",对编码器这种满幅方波,一列包络本来就跨满网格高,
裁成 144px 宽 × 满高的窗口在屏幕上就是一个像选区框的色块(用户 2026-10-18 第二轮报告)。
修法(PlotViewPaint.cpp):
- PaintSignalHaloFor 恢复**整条**几何(包络带 FillPath / 折线 DrawLines);
- 新增 PaintSignalHaloCached:光晕几何只取决于「信号 + 视界 + 网格几何 + 样式」,**与鼠标无关**
  ⇒ 渲染一次进离屏位图,之后每帧只做一次 AlphaBlend(源位图预乘 alpha,用 256×256 查表);
- 缓存键与 AcquirePolyline 同源,**不含网格屏幕位置**(滚动网格只按新位置贴同一张位图)。
  210ms/帧(整条描边)→ 一次性 15ms 渲染 + 每帧 ~2ms 贴图。
halocheck 校验:悬停必命中(60/60 位置)、光晕贴合带宽(outsideBand=0)、不越出网格、
bbox 覆盖整条曲线(不是中段方块)、列表行悬停联动仍生效。

二、信号列表动画卡顿的三个真因(探针 panelstutter:129 个可见网格 + 反复"展开→切组复选框→收起")
1) 面板每帧重画 ~36 行 × ~0.35ms(十来个 GDI+ 原语)⇒ panel.Paint 24~35ms;
2) 动画帧里仍在预热网格图层:每帧重建 1 个网格图层,2500 万采样信号 10~30ms ⇒ 帧被拉长;
3) 勾选分组会把上百个**离屏**网格排进待重画队列,随后每帧补 1 个 ⇒ 数秒持续掉帧。
修法:
1) 行位图缓存(SignalPanel::PaintRowCached):按"行的可见内容"做键,整行渲染进离屏 DIB,
   命中时一次 BitBlt。panel.Paint 24~35ms → 5~6ms。
   ⚠️ 必须 DIB + 内存 DC(gd::Bitmap 上 GDI 会关闭 ClearType,整列文字退化);
   ⚠️ 行内几何在 RowRect 里平移(GDI 文本不认 GDI+ 世界变换,否则文字画到小位图之外);
   ⚠️ 渲染到行位图时不按列表矩形裁剪,裁剪放到贴图时的 DC(GDI 不认 GDI+ 裁剪,
      必须自己 SaveDC + IntersectClipRect,折起动画时行不许画到组名上);
   ⚠️ 缓存上限 192 行(≈10MB)。静态画面逐像素一致(面板区域 54/275200 不同)。
2) 新增 PlotView::SetAnimationBusy:UI 动画期间不预热网格图层(队列保留,动画结束后继续)。
3) 待重画队列只保留可视 + 预取带内的网格;RollWideBuffer 不再登记离屏网格。
实测:展开 p50 35ms → 13ms、收起 33ms → 13ms、动画帧内图层重建 56 次 → 0 次。

三、试过并放弃(避免以后重走)
- ExtTextOutW 替代 DrawTextW:快 2~4 倍但光栅化不同(面板 10% 像素差异)⇒ 观感变化,放弃;
- 用 gd::Bitmap 做行位图:文字丢失 ClearType,放弃。

四、验证(全部离屏,不占用鼠标/键盘/屏幕)
- 新增 .preflight/perf/panelstutter_main.cpp(8/8)、halocheck_main.cpp(16/16);
- 回归 verify 49/49、panelanim 158/158、hoverdrag 20/20、gridanim 93/93、lanescroll 23/23、
  datacheck 69/69、edgecheck 46/46、errcheck 19/19、panflash 19/19、stress 6/6;
- make release / make debug 零警告(TARGET_FILE_NAME=TargetCheck/TargetCheckD)。

文档:Documents/agent_mem.md 追加本轮小节(含 4 条新规则:高亮语义不得为性能让步、
GDI 文本的三个隐形陷阱、面板卡顿先量"每行成本×可见行数"与"本帧是否顺带做绘图区重活"、
动画帧内绝不做可延迟的整帧级工作)。
2026-10-18 21:40:56 +08:00
DeepSeek-V4.1-Flash 0efaa8c0ca 悬停光晕 210ms/帧 → 4ms(包络带填充 + 光标窗口裁剪)+ 平移丢波形判据修正 + 随机组合压力测试
一、悬停巨型信号时整帧卡死(用户报:VERY_BIG_DATA 的「编码器A相」,浮动提示不再跟随鼠标)
探针复现:hoverMean = 210.31ms / baseline = 0.90ms。
根因:PaintSignalHalo 用「加粗 GDI+ 折线」描边整条曲线,而包络带信号的顶点是
「每像素列上/下包络交替」的满幅竖锯齿 —— GDI+ 描边代价 ≈ 线段数 × 覆盖行数
(2503 顶点 × 1081 行)。同一几何改用 FillPath 只要 ~1ms。
修法(PlotViewPaint.cpp / PlotViewInternal.hpp):
1) 包络带信号(cached.bandMode)的光晕改为**填充与曲线本身同一几何**的包络带
   (bandBreaks 段 → 上包络正向 + 下包络反向,alpha 0x55);
2) 只画鼠标附近一段(kHaloWindowHalfWidth=72 逻辑像素、kHaloWindowMinPoints=360):
   · 包络带:窗口换算成列区间再填充(下包络反向存储 ⇒ 列 c 在 start+half+(half-1-c),
     列区间恰对应一段正向可读的连续区间);
   · 折线描边:O(n) 扫一遍取窗口内的连续顶点段,段首尾各多含一个顶点(跨窗口的线段完整画出);
   · 散点高亮同样按窗口过滤(步长仍与 PaintLane 同源)。
3) 窗口中心 = 鼠标 x;鼠标不在绘图区时取绘图区中心(列表/样式区悬停代价同样恒定)。
实测 210ms → 4.0ms(提示恢复跟手)。几何正确性用「透明位图上两次绘制」探针确认:
窗口内与整条光晕逐像素相同(mismatched=0)、窗口外 0 像素(far=0)、全部落在包络带内(outsideBand=0)。

二、组折叠/展开动画卡顿甚至中断(用户用 MF4→BIG→MF4 + 只勾「编码器A相」稳定复现)
这是上面那条的连带症状:列表悬停时 Application::Render 把 panel.HoveredSignal() 推给
SetExternalHoverSignal ⇒ 鼠标每动一次就重画一次光晕;悬停在 2576 万采样的编码器行上
= 210ms/帧 ⇒ 动画定时器被饿死,看起来「卡顿甚至中断」。
同一修复后列表悬停帧 0.72ms(≈ 基线 0.81ms)。探针 [3b] 固化该判据。

三、快速中键拖拽「所有网格信号一起消失」——主修复已正确,之前的失败是度量错了
上一轮把「位移超出 kLayerRollMaxShiftPixels 就作废图层」改成
「driftLimit + 每帧最多滚 kLayerRollMaxStepPixels=240px」,剩余位移由 BlitWideBuffer 的
srcX 承担 ⇒ |shift| 可长期停在 ±(margin-24) 附近而**不会空白**。
之前探针拿「适应视图时的墨迹」当基准,而平移本身会移出数据范围(时间轴外没有采样),
墨迹自然下降 ⇒ 误报。正确判据(已固化进 panflash / stress):
同一次拖拽中把当前帧与**同一视界的整幅强制重绘**(m_sceneValid=false + 清空 m_layerFront)比墨迹,
比值 < 0.9 才算丢波形。三条拖拽模式(120px/帧、90×7 斜向、260px/帧)全部 badFrames=0,
worstRatio 0.966~0.999;MF4 8 个单点信号网格 framesWithMissingPoints=0。

四、新增随机组合压力测试(用户第 4 项要求:给出评估方法与压力验证)
.preflight/perf/stress_main.cpp:1 个 VERY_BIG_DATA(5×2576 万采样)+ 6 个 MF4 分组
(~1500 信号、41 个可见网格),固定种子的随机序列覆盖:中键拖拽(抽样做空白判据)、
滚轮缩放/分布缩放、悬停扫掠、组折叠展开(**每帧都把鼠标扫过正在出现/消失的行**,并校验
m_contentHeight 单调)、适应视图、面板滚动条拖拽、勾选/取消信号、标签页新建/切换/关闭。
判据全部为**同一次运行内的相对量**(基线帧 vs 交互帧),避免绝对 ms 阈值的漂移。
实测:ops=26 frames=209 mean=8.53ms worst=44.87ms(toggleSignal) overBudget=0;
pan blank check 90 个车道样本 0 个低于整幅重绘的 80%;group animation heightRegressions=0。
最慢帧是勾选信号(41 个网格全部重画曲线像素),属于用户明确排除的「需要重绘信号曲线」场景。

五、验证(全部离屏,不占用鼠标/键盘/屏幕)
- 新增 .preflight/perf/panflash_main.cpp(panflash.exe)19/19:三条拖拽模式的丢波形判据、
  MF4 单点网格、悬停耗时(4.0ms)、列表悬停联动(0.72ms)、光晕窗口化几何精确性;
- 新增 stress 6/6;既有回归 verify 49/49、panelanim 158/158、hoverdrag 20/20、
  gridanim 93/93、lanescroll 23/23、datacheck 69/69;
- make release TARGET_FILE_NAME=TargetCheck 通过(Target.exe 被运行中的实例占用)。

文档:Documents/agent_mem.md 新增本轮小节(含 4 条新规则:GDI+ 加粗描边不得用于包络带几何、
随鼠标变化的绘制必须 O(光标附近)、判据必须是同一次运行内的相对量、平移正确性只能与整幅重绘对比)。
2026-10-18 03:51:53 +08:00
DeepSeek-V4.1-Flash fd606a6f80 多层加载拖拽卡顿根治 + 高亮命中线段中段 / 散点高亮 + 选区放大也高亮 + 自动点线判定与规则修正
一、三明治加载后拖拽视界卡顿(39.2ms → 8.2ms 均值,47.9ms → 10.3ms 最坏)—— 四个叠加根因
1) RenderLaneLayerRegion 每次「横向补条」都调用两次 BuildLaneVisuals()(对全部网格算
   ComputeLaneRange,O(网格数))⇒ 253 网格时单帧 RollWideBuffer = 26.8ms。
   修法:进入前快照 m_lanes / m_laneContentHeight,结束时直接拷回(它只渲染一个网格)。
2) 横向「重新居中」本来就不是正确性要求:BlitWideBuffer 按 (ViewMin - layer.wideMin) * scale
   算源偏移,不滚动也画得对 ⇒ 新增 kLayerRollQuantumX=32(不足一个量子不滚)+
   kLayerRollsPerFrame=3(每帧最多重居中 3 个网格,分帧摊销)。
3) 位移过大时旧实现「立刻整块重画」:巨型信号(25M 采样)一次 20~130ms,实测拖拽最坏帧 28ms。
   修法:kLayerRollMaxShiftPixels=160,超出就作废图层 + 登记待重画,本帧由 BlitWideBuffer 的
   网格底衬兜底(正是用户要求的“没来得及画信号就按只有网格叠底继续拖”);
   并且 RollWideBuffer 只处理可视/预取带内的网格(离屏网格不做像素工作)。
4) 包络带路径也支持横向补条裁剪:包络是逐像素列独立计算的,只构建条带覆盖的列即可。
   旧实现对包络带总是构建整幅可见范围 ⇒ 巨型信号一条 32px 竖带要 5ms。

二、鼠标悬停「两点之间的连线」无法高亮(放大到自动点线后)
根因:HitTestSignal 的横向窗口用 upper_bound(fx+tol) 取顶点区间,只扫 [first, last) 的线段;
放大后相邻点相距几十像素,鼠标落在两点中间时两端都在窗口外 ⇒ windowFirst >= windowLast ⇒ 跳过。
修法:线段扫描上界改为 min(stop, windowLast + 1)(必须包含“一端在窗口内、另一端在窗口外”的线段);
散点命中的顶点范围也向两侧各扩一个。实测 11 段长线段中点命中 0/11 → 11/11。

三、散点模式补充高亮(用户要求:原线段/点线高亮“去掉数据点之间连线的部分”)
PaintSignalHalo 原来对 DrawMode::SCATTER 直接 return ⇒ 只有拖拽提示、没有高亮。
现在给每个参与绘制的散点套一圈半透明强调色圆环(步长与绘制同源 kMaxScatterPoints)。
线段 / 点线保持原有的折线光晕。列表 / 图例 / 样式区「目标」行联动高亮走同一条路径,自动生效。

四、「选区放大」模式恢复悬停高亮(仍不允许拖信号)
OnMouseMove 去掉 ZoomMode() == NONE 的门控;OnLButtonDown 在 SELECT 模式下仍直接进入 RUBBER_SELECT。
该模式下的提示文案只显示信号名(去掉 “· 按住拖拽可移入其它网格” 尾缀)。

五、自动点线误判(MF4 + VERY_BIG_DATA 同时加载时 MF4 信号默认就被切点线)
根因:MinorCellSparse 把整个视界切成小格。VERY_BIG_DATA 跨度 ~130s、MF4 数据只覆盖 ~11s ⇒
MF4 信号在 90% 的格子里本来就没有任何采样点 ⇒ “存在一个格子内 < 5 个点”恒成立。
(用户猜测是“用了抽稀点”,实际是“空格子”,但现象一致。)
修法:先用可见数据的显示时间跨度 [dataMin, dataMax] 收窄参与的格子(firstCell..lastCell),
鸽巢判定的分母改用参与格子数;端点 NaN 时做 64 步有界探测并回退到轴区间。
实测:248 个 MF4/中等信号被误判的数量 248 → 0。

六、自动点线规则收窄(用户本次要求)
只有 DrawMode::LINE 的信号在放大时自动切 LINE_SCATTER;SCATTER 不切换(散点已足够提示数据点位置)、
LINE_SCATTER 不动;用户手动改过样式(autoScatterUserOverride)时不切换,避免“改完下一帧又被改回去”。
退出时(用户未手动改过)恢复原来的 LINE。
实测:LINE → 放大后 mode=2 auto=1 → 适应视图后 mode=0 auto=0;SCATTER → 放大后 mode=1 auto=0。

验证(全部离屏,不占用鼠标/键盘/屏幕)
- 新增 .preflight/perf/hoverdrag_main.cpp(hoverdrag.exe)20/20:三明治加载后的点选跳转 + 拖拽逐帧耗时、
  线段中点命中、散点高亮像素、选区放大悬停命中、自动点线判定与规则。
- 既有回归:verify 49/49、panelanim 158/158、datacheck 69/69、edgecheck 46/46、errcheck 19/19、
  gridanim 93/93、lanescroll 23/23。
- 探针的绝对毫秒阈值改为相对基准(同一基线代码在不同时段可差 2~3 倍,避免假失败)。
- make release / make debug 零错误零警告(用 TARGET_FILE_NAME=TargetCheck/TargetCheckD 避免与运行中实例冲突)。

文档:Documents/agent_mem.md 新增 §9.45、§11.2 第 141–150 条与变更日志一行。
2026-10-18 01:08:11 +08:00
DeepSeek-V4.1-Flash 6b15575829 滚动动画根治(巨型信号不再影响滚动)+「N 项」计数点击语义 + 浮动面板首现右上角 + 开关/关闭面板卡顿
一、滚动过渡动画在巨型信号处卡顿 —— 真因是「退化成整幅重绘」
滚动到**从未构建过图层的区域**时,可见网格一个都贴不出图层像素 ⇒ BlitWideBuffer() 返回 false
⇒ 落到 Paint() 末尾的整幅重绘 ⇒ 把本帧全部可见网格的波形(含千万级采样的巨型信号)每帧重画一遍。
实测(MF4 124 网格 + VERY_BIG_DATA 5 网格,点选最后一个信号):单帧 20~35ms,且「可见的巨型信号
网格数」越多越慢(0 个 ≈20ms、5 个 ≈34ms);对照手动拖滚动条只要 ~6ms。
修法:BlitWideBuffer 只要还能画网格底衬(backing 非空)就返回 true,
只有 blits 与 backing 都为空时才交给整幅重绘 —— 这正是「没来得及画信号时就按只有网格叠底的状态继续滚动」。
实测:动画均值 23.0→11.2ms、最坏 35.6→13.1ms;含 5 个巨型信号网格的帧 33→10.7~12.3ms。

二、面板动画帧只重绘面板(额外 −2.7ms/帧)
新增 Application::m_panelDirty:滚动定时器在只有面板动画在跑时置位,Render 走 else if (panelOnly)
只重绘面板 + 分隔条(SignalPanel::Paint 会铺满自己的区域)。m_chromeDirty(真内容变化)仍走完整装饰重绘。
⚠️ 回送仍是 if (fullFrame) {整窗} else {绘图区} 的无条件分支(遵守 §9.41)。
实测帧耗时拆解:plot 4.8ms / plot+panel 11.4ms / plot+全装饰 13.9ms。

三、分组行「N 项」计数:只折叠展开,绝不进入改名
新增 RowPart::GROUP_COUNT 与 SignalPanel::GroupStatsRect(row)(绘制与命中的唯一几何来源,命中矩形在
文本左右各外扩 3 逻辑像素)。RowHitPart 在名称区域之前判定计数区:
- OnLButtonDown:折叠/展开;OnDoubleClick:只折叠/展开,任何点击频率都不进入改名;
- DoubleClickIsPlainPress 返回 true ⇒ 快速连点每一击都生效。
⚠️「快速双击进改名」的计时只对 RowPart::BODY 记录(否则“点计数、紧接着点名称”会被误判成双击)。

四、浮动面板首次出现的位置:右上角
旧默认位置 top = plotTop + 16 + laneIndex*28:上百个网格时 laneIndex 很大 ⇒ 被夹到右下角
(实测 plotH=1081 时 topMargin=964)。改为按「当前真正显示出来的面板中排在本面板之前的个数」错开
(固定遍历顺序:网格下标 → 面板种类),并把错开量夹到“面板下沿不超过绘图区 45% 高度”以内:
第一个面板错开 0 ⇒ 正好贴在上右角(实测 topMargin=24、rightMargin=24),6 个面板时最坏 topMargin=234。
⚠️ 判定「面板是否显示」不能只看 *Panel.open —— LanePanelState::open 默认就是 true,必须同时看功能启用。

五、开关实验性功能 / 关闭面板卡顿 —— RebuildWideBuffer() 整批重建
四个 SetLane* 末尾都调 RebuildWideBuffer()(注释说是“预建宽缓冲基底”),而它同步重渲染全部网格的图层:
129 个网格(含 5 个千万级采样的巨型信号)整批重建要几百毫秒 ⇒ 点一下功能开关 / 关闭浮动面板就会卡住。
修法:新增 PlotView::EnsureLaneLayersPrefetched() —— 只有「图层数组与网格数量不符」(首次绘制 / 增删网格)
才同步整批建立;其余只 RollWideBuffer() 登记缺失/过期网格,由既有预算(每帧最多一个)逐帧补齐。
实测:几百毫秒 → ≤22ms;巨型信号网格上开关均值滤波同样 15ms 量级。

验证(全部离屏,不占用鼠标/键盘/屏幕)
- 新增 .preflight/perf/lanescroll_main.cpp(lanescroll.exe)23/23:逐帧耗时 + 「可见巨型信号网格数」关联、
  「N 项」计数热区与双击不进改名、面板首现位置与多面板错开、开着巨型信号的开关/关闭面板耗时。
- 既有回归:verify 49/49、panelanim 158/158、datacheck 69/69、edgecheck 46/46、errcheck 19/19、
  gridanim 93/93。
- make release / make debug 零错误零警告(用 TARGET_FILE_NAME=TargetCheck/TargetCheckD 避免与运行中实例冲突)。

文档:Documents/agent_mem.md 新增 §9.44、§11.2 第 134–140 条与变更日志一行。
2026-10-17 09:56:45 +08:00
DeepSeek-V4.1-Flash 436dcbfd52 滚动过渡动画根治 + 快速连点跟手 + 分组名单击展开 + 光标吸附同源 + 按钮淡出对齐 + 滚动位置复位
一、滚动过渡动画卡顿(用户报告:甚至不如手动拖滚动条)—— 三处独立根因
1) WM_TIMER(kScrollTimerId) 无条件整窗失效 ⇒ WM_PAINT 看到更新区域不在绘图区内 ⇒ 把
   m_chromeDirty 置真 ⇒ 每一帧都重绘标签栏/工具栏/面板。
   修法:用「推进之前」的面板动画状态决定失效范围 —— 面板动画(滚动到目标信号 / 分组折起展开)
   确实要改面板像素 ⇒ 整窗失效;只有绘图区动画(网格滚动到可视区 / 回到最上方)⇒ 只失效绘图区。
   实测:绘图区动画帧 13.6ms → 3.5ms;手动拖滚动条参照 4.2ms。
   ⚠️ 未改变 §9.41 的回送结构(仍是 if (fullFrame) {整窗} else {绘图区})。
2) 图层预热不足:上百个离屏网格长期不建图层,长距离滚动动画扫进去时每一帧都有十几个网格
   没有图层像素,只能靠「网格底衬兜底」重画(约 0.5ms/网格 ⇒ +8ms/帧)。
   修法:LaneVisuallyRelevant(laneIndex, prefetchScreens) 增加 ±1 屏预取带(AnyVisibleLanePending 用),
   并新增 PrefetchStepAllowed()(kPrefetchMinIntervalMs=32 ⇒ 约 31 次/秒)限速地继续请求重绘,
   让整批图层在静置期间逐步建好。
   实测:lane-only + VERY_BIG_DATA 帧 13.9ms → 5.3ms;signal-click + VERY_BIG_DATA 18.3ms → 10.3ms。
3) 网格滚动量原本参与「宽基底可用性」判据(上一轮已修:SceneContentHash(..., includeLaneScroll=false))。

二、光标吸附改用「放大自动点线」的同一判定(用户 2026-10-17 修订)
AutoPointLineNeeded 改名 MinorCellSparse(中性名,同一判定两处用途):任一小格内有效数据点 < 5 即为稀疏。
- 「放大自动切点线」与「光标吸附」共用它;判定完全不看绘制方式 ⇒ 即使用户手动改过显示样式也照样吸附。
- 鼠标下方网格内既有符合又有不符合的信号时,只有符合的那条的数据点参与吸附;没有符合的则自由移动。
- 删除旧的整屏判据 SignalSnapEnabled 与 kSnapVisibleCellsPerPoint(死代码)。

三、「回到最上方」按钮淡出与「浮动提示」一致
kBackToTopFadeOutMs = kHintFadeMs(500ms)+ 线性 alpha,到达顶部立即开始淡出(不停留)。
淡入保留 170ms ease-out。实测 50% 时刻 alpha = 0.500(线性),旧 ease-out 会是 0.875。

四、快速连点跟手(根因:CS_DBLCLKS 合成 WM_LBUTTONDBLCLK 导致按下丢失)
组件各自实现 DoubleClickIsPlainPress(x, y)(该落点有没有双击语义),宿主 WM_LBUTTONDBLCLK 据此
选择走 OnLButtonDown(当作按下)还是 OnDoubleClick:
- PlotView:浮动面板(含复选框/滑块/复位/✕/标题栏)、「回到最上方」按钮、绘图区滚动条、GX 徽标 ⇒ 按下;
  时间光标线/读数标签/图例条目名/绘图区空白 ⇒ 保留双击。
- SignalPanel:样式区数值框、「目标」行名称与 GX 标签、行名称区域 ⇒ 保留双击;其余全部 ⇒ 按下。
- 弹出菜单 / 拾色器打开时,WM_LBUTTONDBLCLK 也按各自 OnLButtonDown 走。

五、分组行:名称恢复单击展开收起 + 只有快速双击进入改名
- OnLButtonDown 分组分支不再排除 RowPart::BODY:三角 /「组X」/ 名称本体统一折叠展开。
- OnDoubleClick 名称区域:只有快速双击(kGroupRenameDoubleClickMs=300,比系统 500ms 紧)才进入改名;
  超阈值的「慢双击」只当又一次单击;快速双击前先撤销按下那一次的折叠切换,保证进编辑时状态与点击前一致。

六、信号重现时网格滚动回到最上方
“有过需滚动条的网格 → 没有任何信号显示 → 又出现需滚动条数量的网格”这一类情形必须从最上方开始看。
修法:Application::SynchronizeView 的「首次出现可见信号」分支里,在 FitAll() 之后 tab.LaneScroll() = 0。

验证(全部离屏,不占用鼠标/键盘/屏幕)
- grabdanim(.preflight/perf/gridanim_main.cpp)93/93:真实消息循环帧距 + 逐帧渲染耗时(与手动拖滚动条
  对比)、光标吸附新判定、按钮淡出线性、快速连点路由、分组名单击/双击/慢双击、滚动位置复位。
- 既有回归:verify 49/49、panelanim 158/158(按新分组名规则更新 8 条断言)、datacheck 69/69、
  edgecheck 46/46、errcheck 19/19。
- make release / make debug 零错误零警告(用 TARGET_FILE_NAME=TargetCheck/TargetCheckD 避免与正在运行的实例冲突)。

文档:Documents/agent_mem.md 新增 §9.43、§11.2 第 125–133 条与变更日志一行。
2026-10-17 03:17:42 +08:00
DeepSeek-V4.1-Flash 34375481de 网格底衬永不空白 + 动画流畅性根治 + 光标随首个信号重现 + 「放大自动点线」新判据
一、网格底衬消失(半屏/局部/全屏,且要等一会才刷新好)—— 两个机制叠加
1) BlitWideBuffer 对「跨度已过期」的图层原用 StretchBlt 放大占位:深度放大时把一小段像素放大
   几十倍 ⇒ 整块网格变成一片无网格的模糊色块(用户看到的就是「网格叠底消失、没有任何内容」)。
   修法:删除占位拉伸;对拿不到(完整)图层像素的网格【就地补画网格底衬】
   (新增 PaintLaneGridBacking + m_renderGridOnly,复用 PaintLane 的网格部分,跳过波形/散点/
   派生曲线/Y 轴刻度/图例)。底衬代价极低,可每帧执行 ⇒ 网格永不空白/闪烁。
   ⚠️ 顺序:先画底衬(GDI+)再 g.GetHDC() 做 BitBlt;只有全部网格都贴不出像素时才交给整幅重绘。
2) 待重画队列按下标顺序消费(一次 1 个)⇒ 上百个网格时可视网格要等上百帧。
   修法:RenderPendingLaneRebuilds 里做稳定划分,【可视网格优先】(LaneVisuallyRelevant);
   「结算」判据由 LanesAllReadyForView() 改为 LanesVisiblyReadyForView()。

二、过渡动画卡顿/不平顺/直接闪到终态 —— 三处根因
1) 「m_layerRebuildPending 非空就每帧请求再送一帧」:上百个网格时 700ms 内挤出 124 次重绘,
   而 WM_TIMER 优先级低于投递消息 ⇒ 动画定时器被饿死(实测 frames=124 / timerTicks=1)。
   修法:新增 AnyVisibleLanePending(),只用「还有没有可视网格待补」决定是否请求再送一帧;
   离屏网格改为机会式预热(实测 frames=25 / timerTicks=16)。
2) SetTimer 的间隔会被量化到系统 tick(默认 15.6ms):请求 16ms 实际等到下一个 tick
   ⇒ 实测帧距 ~26ms(只有约 38fps)。修法:kScrollTimerPeriodMs 16 → 10(实测 ~15.5ms ≈ 64fps;
   真实消息循环帧距 ~25ms → ~16ms)。实测映射:8→15.8 / 10→15.5 / 12→16.5 / 16→26.4 / 31→37.9。
   ⚠️ timeBeginPeriod(1) 在本机无效(已 A/B 验证)。
3) 网格滚动联动 / 「回到最上方」每帧都改 LaneScroll(),而它原本参与「宽基底可用性」判据
   (SceneGeometryHash)⇒ 每一帧都退化成整幅重绘。修法:SceneContentHash 新增 includeLaneScroll 形参,
   宽基底可用性判据不计网格滚动量/各网格屏幕纵向位置(它们只是图层的贴图落点);
   静态场景缓存的指纹仍然计入。实测网格滚动动画单帧 18.36ms → 1.19ms。

三、光标工具激活且没有任何信号绘制时,首个信号出现后按规则重新出现(用户要求)
新增 PlotView::ResetCursorPlacementOnFreshContent()(规则与 CycleCursorMode() 的「首次出现」同源:
单光标置于视图中心;双光标第二条紧邻第一条且落在可见范围内),由 Application::SynchronizeView
的「首次出现可见信号」分支在 FitAll() 之后调用。
实测:单光标 stale=4.60 → 新的视图中心 20.92;双光标 stale=(2.89,7.18) → (20.92,26.07)。

四、「放大自动点线」规则改为:任一小格内数据点少于 5 个即自动切换(用户重新定义)
新增 PlotView::AutoPointLineNeeded()(推翻此前「一屏内数据点 < N×4」):按 GridMinorCellCount()
切成横向小格,只要存在任何一个小格内有效数据点 < kAutoPointLineMinPointsPerCell(=5) 就切「点线」。
实现:先「总有效点 < N×5」鸽巢 O(1) 充分判定;单调时间轴逐小格两次二分取区间(单格累计到 5 个即早退,
另给 kAutoPointLineCellScanCap=512 封顶);非单调时间轴一次有界扫描分桶。保留「视界内至少一个可见点」保护。
⚠️ 光标吸附仍用旧的整屏判据 SignalSnapEnabled(用户明确要求「其它规则不变」)。

验证(全部离屏,不占用鼠标/键盘/屏幕)
- 新增 .preflight/perf/gridanim_main.cpp(gridanim.exe)55/55:
  [A] 缩放/适应视图/「预热未完成再滚动」等每个用例逐帧断言「每个可视网格都有网格底衬
      (有竖直锐边、无模糊占位、无未绘制带)」;[B] 直接驱动 + 真实消息循环两条路径统计每帧耗时、
      帧距均值/最大与进度单调性;[C] 光标重现;[D] 自动点线新判据(稠密不切、稀疏切换、恢复原样式)。
- 既有回归:verify 49/49、panelanim 156/156、datacheck 69/69、edgecheck 46/46、errcheck 19/19。
- make release 零错误零警告(使用 TARGET_FILE_NAME=TargetCheck 以避免与用户正在运行的实例冲突)。

文档:Documents/agent_mem.md 新增 §9.42、§11.2 第 117–124 条与变更日志一行。
2026-10-17 00:56:19 +08:00
DeepSeek V4.1 Flash f845616938 回退 5b52eef 并修复「屏幕不刷新」根因:拖动滚动条卡顿 / 悬停高亮卡住 / 信号拖拽无效果
根因(代码级确认)
- Render() 把界面画进后备缓冲区后用 GetDC + BitBlt 回送窗口,5b52eef 把回送写成
  `if (fullFrame) { BitBlt(整窗) } else if (panelOnly) { BitBlt(本帧重绘区域并集) }`,没有兜底 else。
- 「鼠标只在绘图区内移动」的帧(WM_MOUSEMOVE 的 plotOnlyMove 路径只 InvalidatePlotRect)恰好
  fullFrame == false && panelOnly == false ⇒ 后备缓冲区被正确重绘,窗口 DC 一个像素都不回送。
- 这一个根因同时造成用户报告的三个症状:悬停光晕卡住(移开不消失)、拖动滚动条不跟手/非常卡顿、
  拖拽信号换网格看不到任何结果。

修法
- git reset --hard HEAD~1 撤销 5b52eef(保留 backup-bad-commit 标签供 A/B)。
- 只挑回该提交里与更新区域无关的功能改动(用户明确要求过、本身正确):
  * SignalPanel::ExpandGroupForSignal():联动滚动前立刻展开折起的分组(不播展开动画);
  * FitAll() / FitSinglePointCluster() 不再复位 Tab().LaneScroll()(适应视图不影响滚动条);
  * 右下角「回到最上方」浮动按钮 + 网格滚动到可视区的平滑动画。
- Application.cpp 只保留 3 处最小改动:Render 里把 panelAnimating 计入整帧脏、WM_TIMER 推进
  AdvanceBackToTopAnimation() 且保持整窗失效、定时器存活判定带 BackToTopAnimationActive()。
  Render 的绘制与 BitBlt 路径逐字节等同于 82b5ebe。
- 彻底删除 m_paintPlot / m_panelDirty / panelOnly / InvalidatePanelRect / 并集回送。

验证(全部离屏执行,不移动真实光标、不抢焦点、不截屏)
- .preflight/diag(新增离屏探针,19/19):窗口移到屏幕外 + WS_EX_TOOLWINDOW + SW_SHOWNOACTIVATE,
  保证有真实更新区域与 WM_PAINT 但不影响用户。断言滚动条拖动(滑块与绘图区都在后备缓冲区被重绘)、
  悬停光晕出现与消退(像素完全回到基线)、在曲线上按下进入 MOVE_SIGNAL 且拖到另一网格后归属正确。
- .preflight/check 137/137。
- .preflight/diag/verify_blit.ps1(新增源码级护栏,10/10):断言 Render() 的回送块必须有无条件 else、
  不得出现 else if、m_paintPlot/m_panelDirty/panelOnly 必须不存在、失效只允许整窗或仅绘图区。
  A/B:换回 backup-bad-commit ⇒ 4 项如实失败;换回修复版 ⇒ 10/10。
- make release 零错误零警告。

文档:Documents/agent_mem.md 新增 §9.41、§11.2 第 108–116 条(含 §9.40 与旧 108–114 条的作废标注)
与变更日志一行。

教训(写进记忆):① 离屏探针无法验证「窗口回送 / 合成」(屏幕外 DWM 不合成、窗口 DC 恒为空白)⇒
回送类约束必须有源码级护栏;② 离屏窗口 + TrackMouseEvent 会立刻收到 WM_MOUSELEAVE,探针须丢弃该消息;
③ 网格回收会让下标左移,断言要用锚信号。
2026-10-16 17:33:08 +08:00
DeepSeek V4.1 Flash 82b5ebe4f9 「所属网格」菜单:标题固定 + 10 行上限 + 滚动条;网格滚动联动改为动态过渡
1) 菜单滚动时最上方的「所属网格」标题不再跟着滚走
   - 根因:上一轮只实现了“尾部固定区”(最后一个分隔线 + 「新建网格」钉在底部),
     而滚动区是从条目 0 开始的,于是标题也被一起滚掉。
   - 修法:BuildLayout 新增 m_scrollFirst(= 前导 HEADER / HEADER_ACTION 的个数),
     滚动区收敛为半开区间 [m_scrollFirst, m_pinnedIndex):
     · 前导标题从菜单顶向下固定排(不参与滚动);
     · 滚动区条目受 m_scrollOffset 影响;
     · 尾部固定区从菜单底向上对齐。
   - 配套:Paint 的裁剪与“跳过不可见条目”范围、ItemAtPoint 的“必须落在滚动视口内”判定、
     EnsureItemVisible 的下界全部改用该区间;新增 ScrollRegion(first, last) 供查询/测试。

2) 菜单长度上限改为「最多 10 条网格」并加滚动条
   - MenuLayoutConstraint 新增 maxScrollRows(滚动区最多显示的标准行数;与 maxHeight
     同时给出时取更严格者);OpenLaneMenu 传 10(kLaneMenuVisibleRows),
     另保留 maxHeight = max(180, 客户区高 × 0.8) 作为矮窗口的硬上限。
   - 新增滚动条:轨道贴滚动条目区右内侧(不盖住标题与底部固定项),滑块高度按视口/内容比
     (下限 22 逻辑像素);滚动区条目宽度让出 kScrollBarWidth + kScrollBarGap,
     文本与对勾不会压在滑块上。
   - 滚动条可拖拽:按下滑块 → 拖动(只改偏移、不更新悬停);点击轨道空白 → 滑块中心移过去;
     松开结束拖拽且不下发命令、菜单保持打开。新增 ScrollOffsetForThumbTop 反算偏移。
   - ⚠️ BuildLayout 绝不能复位 m_scrollBarDragging:拖拽时每帧都会 RebuildLayout(),
     在那里清零会让拖拽“拖一下就脱手”(该状态只在 Open / Close 里复位)。

3) 网格滚动联动改为动态过渡(旧实现直接把视界瞬移过去,过于僵硬)
   - PlotView::ScrollLaneIntoView 只负责启动平滑动画:与信号列表的“滚动到目标信号”同源
     (ease-out cubic;时长 clamp(距离×0.35ms, 130, 300)),新增
     m_laneScrollAnimActive/From/To/StartMs/DurationMs。
   - 宿主 16ms 定时器(kScrollTimerId)调用 AdvanceLaneScrollAnimation() 逐帧推进,
     与信号面板动画共用同一条定时器;Render 的“需要保持定时重绘”判定加入
     LaneScrollAnimationActive()。
   - 每帧 ClampLaneScroll() + BuildLaneVisuals()(网格滚动量参与场景/图层指纹与各网格 rect);
     结束时精确落到目标。已完整可见的网格仍然不启动动画。
   - ⚠️ 用户自己滚动网格(滚轮 / 拖滚动条)会 CancelLaneScrollAnimation(),避免与动画抢滚动位置。

验证(全部离屏执行:窗口 SW_HIDE + 只注入合成消息 + 进程与线程 BELOW_NORMAL,
不移动真实光标、不抢焦点、不截屏):
- .preflight/check 扩到 95 项,95/95 通过;新增断言:
  · 菜单顶部标题在滚动时不动(Y 坐标不变)、滚动视口高度恰为 10 × 行高;
  · 滚动条轨道/滑块存在、随滚动移动、按住滑块拖到底(菜单不关、不下发命令、末条目入视口)、
    滚轮回到顶部;
  · ScrollLaneIntoView 不瞬移(刚触发时几乎未动)、中间帧单调递进、收尾精确到位、
    已可见时不启动动画、滚轮/手动滚动会取消动画。
- A/B:回退三处(标题固定 / 10 行上限 / 滚动动画)⇒ 10 项如实失败;恢复后 95/95。
- make release / make debug 零警告。

文档:Documents/agent_mem.md 新增 §9.39、§11.2 第 104–107 条与变更日志一行。
2026-10-16 14:12:42 +08:00
DeepSeek V4.1 Flash 2904699eae 修复五项交互缺陷:折起后滚动条 / 单点信号样式 / 线宽实时生效 / 网格菜单限高滚动 / 点选信号滚动绘图区
1) 分组折起后再展开,信号列表的滚动条消失(筛选态与常规态均可复现)
   - 根因:AdvanceGroupAnimations 只在“折起完成”时重建行集合;而“展开完成”那一帧
     m_contentHeight 从收起态跳回展开态,却没有任何路径重算滚动条可见性 ⇒ 滚动条停在隐藏。
   - 修法:任一方向动画结束都 m_rowsDirty = true + BuildRows(),并每帧 RefreshScrollBar(false)。
     ⚠️ 动画期间不夹取滚动偏移,避免打断与滚动同步的折起动画(scrollSynced)。

2) 单点信号(finiteCount == 1)默认绘制方式由「点线」改为「散点」
   - ApplySinglePointDefaultStyle → DrawMode::SCATTER(仍取最大线宽)。
   - 新增 SignalPanel::ModeOptionEnabled:单点信号的「线段」「点线」置灰、不可点、
     且不参与悬停高亮(避免“看着能点其实点不动”);样式区的绘制与命中都用它。
   - PlotView::UpdateAutoScatterStyles 跳过单点信号,否则“放大自动点线”会把它又改回「点线」。

3) 拖动线宽滑块完全不生效(拖动时线宽不变、松手瞬间变了、再移动鼠标又变回去)
   - 根因一:AcquirePolyline 的折线缓存键没有 lineWidth,而 BuildPolyline 把线宽烘进
     包络带几何(每个像素列的上下包络各向外扩 max(1,线宽)*scale*0.5,「梳状」判定也依赖它)
     ⇒ 稠密信号(采样数 > 绘图区宽度)永远复用旧线宽的顶点与包络带。
   - 根因二:样式补丁只更新“静态场景缓存 + 后备缓冲区”,而每网格图层(宽基底)仍是旧样式像素,
     下一帧命中“宽基底快路径”时把新样式覆盖回旧像素。
   - 修法:PolylineCacheEntry 增加 lineWidth 并进比较;
     样式补丁之后 QueueLaneRebuild(受影响网格) + RenderPendingLaneRebuilds(1)(沿用每帧至多 1 个的预算)。

4) 「目标」行的 GX 标签复用「所属网格」菜单,并先给 PopupMenu 增加「限高 + 条目区滚动 + 尾部固定项」
   - MenuLayoutConstraint::maxHeight(OpenLaneMenu 传客户区高度的 62%);
   - 固定区 = 最后一个分隔线及其之后的条目(即「新建网格」)始终钉在菜单底部,其余条目可滚动;
   - 滚轮滚动条目区(Application 在菜单内滚动时不再关闭菜单,其余情形保持既有行为);
   - 键盘移动悬停项自动滚入可视区(EnsureItemVisible);ItemAtPoint 要求条目同时命中滚动视口;
   - 滚动区文本用 UiTextClipped(GDI 文本不遵循 GDI+ SetClip),并跳过完全不可见的条目(性能);
   - BuildLayout 不再无条件清零 m_scrollOffset(否则 RebuildLayout 会把滚轮位置吃掉);
   - 「目标」行 GX 标签点击**不参与**联动滚动(悬停联动保留)。

5) 点选信号名 ⇒ 绘图区自动把目标网格滚动到可视区内(上百个独立网格时旧有高亮无法定位)
   - SignalPanel::RequestPlotLaneScroll / TakePlotLaneScrollRequest;
     Application::ApplyPendingPlotLaneScroll(WM_LBUTTONDOWN 的面板分支);
     PlotView::ScrollLaneIntoView(在可视区上方就上滚、在下方就下滚、已完整可见则不动)。
   - 只有“点击信号名”触发;悬停绝不改变视图;勾选框 / GX 徽标 / 移除 ✕ 也不触发。

验证(全部离屏执行,窗口 SW_HIDE + 只注入合成消息 + 进程与线程 BELOW_NORMAL,
不移动真实光标、不抢焦点、不截屏):
- 新增 .preflight/check(不入库)68/68 通过;
- A/B:五处修复全部回退 ⇒ 17 项如实失败(B 4 / C 4 / D 7 / E 2);“折起后滚动条”需要
  逐帧重绘驱动动画才能复现,单独回退时 ⇒ 5 项如实失败;恢复后 68/68;
- make release / make debug 零警告。

其它:
- 按要求删除工作区 Tools/(structcheck 脚本)并移除 Makefile 的 structcheck 目标;
- Documents/agent_mem.md 同步:新增 §9.38、§11.2 第 97–103 条与变更日志一行。
2026-10-16 11:06:31 +08:00
DeepSeek V4.1 Flash 9cf6b2fedc 把三处功能要求也变成可复跑的检查(structcheck 93 条 + A/B 反证)
背景
----
「点组名不折起」「组名改名框不吞项数」「SignalData 去掉 displayName」
三处用户要求此前只有散文描述与已压缩的历史记录。本提交给它们补上
(a) 源码级断言、(b) A/B 反证、(c) 运行时断言三类证据。

新增 / 修改
-----------
- Tools/structcheck.ps1 新增第 [3] 组「行为与注释不变量」(31 条断言):
  * 折起热区:RowHitPart 产出专用 RowPart::GROUP_TITLE,其矩形正好是「组N」标签矩形;
    单击门限 `if (part != RowPart::DISCLOSURE && part != RowPart::GROUP_TITLE)` 全文件恰好 1 处;
    ToggleGroupFromRow 恰好 2 个入口(单击门限 + 双击折叠三角);
    双击非名称本体(`if (part != RowPart::BODY) return false;`)不进入改名。
  * 改名框:右边界必须是 `statsRight - statsWidth - 6*scale`,且全文件不得出现 `right = statsRight;`;
    项数宽度必须来自 GroupStatsText。
  * 数据模型:SignalData / SignalGroup 各只声明 1 个字符串字段且名为 name;
    RenameSignal / RenameGroup 存在;全仓 displayName / Title() / Set*DisplayName 残留 0 处。
  * 注释核验:过时措辞(文件下标 / 所属文件下标 / 原信号所属文件下标 / 文件分组 / 原文件下标 /
    文件序号)全仓 0 处;已修正注释确实存在(例如 DerivedSignalRequest::groupIndex →
    「原信号所属分组下标」)。
- Tools/structcheck_phrases.txt(新增,UTF-8):中文短语表,供 .ps1 以 UTF8Encoding($false)
  读入后逐条断言(forbid / require)。⚠️ 中文不能直接写进 .ps1 —— PowerShell 5.1 按 ANSI 读脚本。
- 断言总数 62 → 93;退出码仍为 0/1,可直接接 CI。

A/B 反证(判别力)
------------------
回退两处 UI 修复(`if (part != DISCLOSURE && part != GROUP_TITLE)` → `if (false)`;
`right = statsRight - statsWidth - 6*scale;` → `right = statsRight;`):
  * panelanim.exe   147/156(9 项如实失败)→ 恢复后 156/156
    失败文案直接点名需求:the group rename box stops before the item counter /
    clicking the group name does not start collapsing / does not collapse the group ...
  * structcheck     90/93(3 项如实失败)→ 恢复后 93/93
    single-click toggle gate accepts only DISCLOSURE / GROUP_TITLE (found 0) /
    rename box right edge excludes the item counter / rename box never extends over the counter

运行时断言(数据层,datacheck 69/69)
-------------------------------------
  Check(set.RenameSignal(groupIndex, 0, L"改名") && set.Resolve(SignalRef{0,0})->name == L"改名",
        "signal rename writes the single name field");
  Check(set.RenameGroup(groupIndex, L"我的组") && set.GroupAt(0)->name == L"我的组",
        "group rename writes the single name field");
  Check(!set.RenameSignal(groupIndex, 0, L"") && ..., "empty rename is rejected (name is never blanked)");
即重命名只写一个 name 字段(编译期已无 displayName 可写),空名被拒。

文档
----
- Documents/refactor_report_2026-10-16.md:新增 §9.7(三处功能要求的源码断言 + A/B + 运行时断言);
  §1.3 补完整 A/B 实测;§9.4 验证表与 §9.5 变更清单同步(62 → 93)。
- Documents/agent_mem.md:§9.37 开头补可复跑证据块;§11.2 第 96 条扩写为三组 + 中文短语数据文件的做法;
  变更日志 d2e6fdd 行的结构检查描述更新为 93 条与 A/B 数据。

验证
----
make release 零警告,Target.exe 764 416 B;
structcheck 93/93;verify 49/49;datacheck 69/69;edgecheck 46/46;
panelanim 156/156;errcheck 19/19。
2026-10-16 04:56:35 +08:00
DeepSeek V4.1 Flash 695b73a366 结构证据落库:新增 Tools/structcheck.ps1 + make structcheck(62/62)
背景
----
上一轮报告对「巨型实现文件按职责拆分」与「Components/Shared 共享控件」两件事只有散文描述,
缺少可复跑的证据。本提交把这两项结构约束写成入库脚本,任何人(或 CI)都能一键核验。

新增
----
- Tools/structcheck.ps1(入库,纯 ASCII,退出码 0/1):
  [1] 拆分
      * 16 个拆分产物必须存在且非空,并断言核心文件已显著变小
        (PlotView.cpp 13 695 → 705 行;SignalPanel.cpp 4 184 → 1 019;
          Application.cpp 3 135 → 1 326;DataModel.cpp 2 615 → 1 459);
      * 12 组职责函数必须「只」定义在预期文件里
        (PlotView::Paint/PaintLane/PaintHint → PlotViewPaint.cpp;
          LaneAtY/CancelDrag → PlotViewInteraction.cpp;FitAll/SetHint → PlotViewFeatures.cpp;
          Layout/BuildLaneVisuals → PlotView.cpp;SignalPanel::Paint/OnLButtonDown/EnsureRows → paint/interaction/core);
      * 四个内部头必须声明具名命名空间并带 using-declaration,且把共享工具标 inline
        (pvdetail 111 / spdetail 63 / appdetail 27 / dmdetail 4 个 inline 定义);
      * 12 个拆分翻译单元必须真的被编译(Build/Release/*.o 齐全),并用 nm 断言
        「绘制符号在 PlotViewPaint.o、PlotView.o 里一个都不剩」;
      * 25 个实现文件所在目录必须全部登记在 Makefile;
  [2] Components/Shared
      * UiControls.hpp 声明 16 个控件,nm 断言这 16 个全部由 UiControls.o 定义(其他 .o 不是定义者);
      * 69 处调用点必须跨 ≥3 个组件(实测 PlotView / SignalPanel / TabBar / Shared);
      * 分层:Components/Shared 不含任何组件头;底层 Utilities/PaintUtil 不依赖 Shared;
        Utilities 不含任何功能组件头。
- Makefile:新增 structcheck 目标(make structcheck)。

说明(脚本第一版的两个假失败,已修正判定)
------------------------------------------
1. 判定「控件定义」必须用 UiControls.hpp 里**声明**的名字,而不是「所有 Ui* 函数」:
   UiFillRect / UiText / UiMeasureTextWidth 等是底层原语,本就住在 Utilities/PaintUtil。
2. 精确分层不变量是「Shared 不依赖任何组件;底层 PaintUtil 不依赖 Shared;
   Utilities 不含功能组件头」,而不是「Utilities 一律不得依赖 Shared」:
   Utilities/Src/{PopupMenu,ColorPickerPopup}.cpp 是历史上放在 Utilities 下的自绘 UI 小部件,
   允许消费 Shared 控件(方向与各组件一致)。

文档
----
- Documents/refactor_report_2026-10-16.md:
  * TL;DR 处更正首版「PlotView.cpp 整体拆分本轮未执行」的错误结论(那是提交 0871eb0 时刻的状态,
    已被 e6b7bf6 推翻);
  * §七 标题改为「遗留项与后续建议(逐项的最终处理状态)」;
  * §9.4 验证表新增 make structcheck 62/62;
  * 新增 §9.6「结构证据:把拆分与共享控件变成可复跑的检查」;
  * §9.5 变更清单补充 Tools/structcheck.ps1 与 Makefile。
- Documents/agent_mem.md:
  * §9.37 第七节补上可复跑证据与实测行数;
  * §11.2 新增第 96 条(结构检查脚本的用法、两个易错判定、精确分层不变量);
  * 变更日志 d2e6fdd 行补充 structcheck;
  * 0871eb0 行的「未完成(已如实记录)」改为明确的「已由后续两个提交完成」。

验证
----
make release 零警告,Target.exe 764 416 B;
make structcheck 62/62;verify 49/49;datacheck 69/69;edgecheck 46/46;
panelanim 156/156;errcheck 19/19。
2026-10-16 04:48:11 +08:00
DeepSeek V4.1 Flash d2e6fdd085 收尾:LanePanelState 抽取 + 错误提示非模态化(清空上一轮的“未完成”)
一、Lane 的内聚性(上一轮建议 5,本轮完成)
- 新增 struct LanePanelState { bool open = true; double x = -1.0; double y = -1.0; };
- Lane 的 12 个平铺面板字段(edgePanelOpen/X/Y 等 × 4 类实验性功能)
  → 4 个聚合成员 edgePanel / edgeCenterPanel / meanFilterPanel / weakPeakPanel;
- 访问点全部改为 lane.edgePanel.open 形式(DataModelTab.cpp 快照拷贝、
  PlotViewInteraction.cpp 面板拖拽、PlotViewPaint.cpp 面板层序绘制);
- RebuildLanes 内部的 LaneCarry 同步收敛为 4 个 LanePanelState,
  12 行逐字段赋值简化为 4 行整体赋值。
- 教训:批量改名不能对“声明”和“使用”跑同一组替换对,
  否则声明行会变成 `bool edgePanel.open = true;` 这类非法代码。

二、错误提示与模态阻塞解耦(上一轮建议 1,本轮完成)
- Application 新增 SetModalErrorsEnabled(bool) / ModalErrorsEnabled(),
  成员 m_modalErrorsEnabled 默认 true(交互场景行为不变);
- Initialize 最先执行 SetModalErrorsEnabled(showCommand != SW_HIDE):
  隐藏窗口(自动化/离屏)下没人能点掉模态框,等于把进程钉死;
- SetStatusMessage(text, isError=true):允许模态时原样 MessageBoxW,
  否则改走非模态通道 m_plot.ShowPersistentHint(text)(绘图区底部浮层);
  普通提示与空文本依旧被忽略;
- PlotView 新增公开接口 ShowPersistentHint(text)(内部转 SetHintSticky),
  不向宿主暴露粘滞/淡出等内部细节。

三、顺带修掉一个真实缺陷
- PlotView::Paint 在 m_lanes.empty() 时提前 return,而提示浮层在其后的
  叠加层绘制 ⇒ 一个信号都没勾选时任何错误都显示不出来,恰恰是
  “文件加载失败”最需要提示的场景;该分支补一次 PaintHint(g)。

四、验证(全部离屏:SW_HIDE + 不注入输入 + BELOW_NORMAL)
- make release / make debug:通过,零警告;Build/Release/Target.exe = 764416 B;
- verify 49/49、datacheck 69/69、edgecheck 46/46、panelanim 156/156;
- 新增探针 .preflight/perf/errcheck 19/19:默认值 / 隐藏窗口自动降级 / 宿主显式开关;
  工作线程 + 5s 超时判定“不阻塞”(旧行为会永久阻塞,判失败并 ExitProcess,
  探针不会挂住);EnumWindows 统计本进程 #32770 顶层窗口数量前后不变;
  错误文本进入 m_hintText / m_hintSticky / HintNeedsAnimation / m_hintShownAt;
  像素级:有提示帧与无提示帧在绘图区内的差异像素 3781,且全帧差异同为 3781
  (证明差异只来自提示)。

五、文档
- Documents/agent_mem.md:§9.37 新增第八节(本轮补齐)、第九节(仅剩 MF4 项)、
  §11.2 新增第 93–95 条硬约束、变更日志新增一行;
- Documents/refactor_report_2026-10-16.md:§7.3 / §7.4 标记完成,新增 §九 收尾补记
  (含 9.1–9.5:聚合化、解耦、探针、验证结果、变更清单)。
2026-10-16 04:38:12 +08:00
DeepSeek V4.1 Flash e6b7bf66ec 按组件目录结构把四个巨型实现文件拆分为多个协作文件(PlotView / SignalPanel / Application / DataModel)
承接上一条提交(0871eb0)—— 该提交只完成了 Components/Shared 与注释/命名清理,并**主动未做**
大文件拆分;本提交把大文件拆分补齐,至此“按功能拆分逻辑块到各组件自己的 Inc/Src”已落地。

## 一、拆分结果

| 原文件 | 拆分后 |
| --- | --- |
| Components/PlotView/Src/PlotView.cpp(13 695 行 / 544 KB) | PlotView.cpp(787) + PlotViewPaint.cpp(6 367) + PlotViewInteraction.cpp(2 715) + PlotViewFeatures.cpp(3 005) + PlotViewInternal.hpp(936) |
| Components/SignalPanel/Src/SignalPanel.cpp(4 184 行) | SignalPanel.cpp(1 141) + SignalPanelPaint.cpp(697) + SignalPanelInteraction.cpp(2 095) + SignalPanelInternal.hpp |
| Components/Application/Src/Application.cpp(3 135 行) | Application.cpp(1 427) + ApplicationCommands.cpp(1 184) + ApplicationLoading.cpp(270) + ApplicationInternal.hpp |
| Utilities/Src/DataModel.cpp(2 615 行) | DataModel.cpp(1 583,含自由函数区) + DataModelTab.cpp(732) + DataModelInternal.hpp |

分片职责(以 PlotView 为例):
  core        视图核心:构造/析构、上下文、布局、坐标映射、视界与平移、网格结构与归属
  paint       渲染与像素缓存:全部绘制、场景/装饰缓存、每网格图层、宽基底与滚动补条
  interaction 交互与编辑:命中测试、鼠标/键盘、光标形状、内联输入框、浮动面板层序
  features    视图特性与派生数据:光标读数/提示/缩放、自适应、吸附、单点、派生曲线线程池、
              边沿统计、实验性功能参数
(SignalPanel 为 core/paint/interaction;Application 为 core/commands/loading;
  DataModel 为 core/tab。)

## 二、关键手法(这是能安全拆开的前提)

原文件的辅助区都是**匿名命名空间**(PlotView 约 870 行常量与工具函数),被所有成员函数广泛引用。
直接拆文件会让各 .cpp 拿不到这些工具,因此:

1. 把该块整块搬进 `<组件>Internal.hpp`,从 `namespace { }` 改为**具名**命名空间
   (pvdetail / spdetail / appdetail / dmdetail);
2. 块内**每个函数与常量都标 inline** ⇒ 多个翻译单元共用同一个定义:既不重复符号,
   也不产生 N 份函数定义;
3. 头部末尾 `using namespace <ns>;` ⇒ 各 .cpp 函数体里原有的非限定调用(IntersectRect /
   NiceStep / ClampDouble …)**一行都不用改**;
4. 成员定义按「方法名 → 分片」归类后**整块搬移** —— 全程**纯代码搬移**,没有改动任何一行函数体。

## 三、拆分过程中踩到并修正的四个坑(已固化到 .preflight/perf/split_component.ps1)

1. `[[maybe_unused]]` 等属性**必须排在 inline 之前**(`inline [[x]] T` 非法);
   单独的 `{`/`}` 行、注释行、`struct`/`enum`/`using`/`template` 行不能加 inline。
2. 匿名命名空间的**结束行可能带注释**(`} // namespace`),只认 `^\t}$` 会一路匹配到后面
   某个成员函数的结束括号,把大段成员定义误当成“辅助区”搬走。
3. **首个成员定义之前的所有行**必须跟着进核心文件:DataModel.cpp 在匿名命名空间之后、
   首个成员定义之前还有约 390 行命名空间外的自由函数(SignalStats / ComputeStats /
   CacheSparseSamples / ApplySinglePointDefaultStyle …),漏掉会被静默丢弃。
4. 内部头必须带上原文件真正用到的全部头:DataModel 需要 Theme.hpp / <cstring> / <cstdio> /
   <deque> / <atomic> / <cwctype> / <new>(原文件靠这些头才编译得过)。

## 四、附带完成的命名审计

PlotView 中残留的 File 语义命名一并修正:LaneSpansMultipleFiles → LaneSpansMultipleGroups、
firstFile → firstGroup、multiFile → multiGroup、SceneCacheKey::haloFile → haloGroup。

## 五、验证(全部离屏:SW_HIDE + 不注入真实输入 + 进程/线程 BELOW_NORMAL)

- .preflight/perf/verify.exe(PlotView 渲染/交互,含逐像素断言)49/49
- .preflight/perf/panelanim.exe(信号面板:卷尺式折起/展开、筛选态、悬浮组名、目标行)156/156
- .preflight/perf/datacheck.exe(数据层:单轴/多时间轴 CSV、MF4、重命名语义)69/69
- .preflight/perf/edgecheck.exe(边沿统计与面板排版)46/46
- make release / make debug 零警告。
即:拆分后**行为与拆分前完全一致**(纯代码搬移,四套探针全绿)。

## 六、体积(诚实记录)

  Target.exe: 754 176 → 764 416 B(+10 240 B,+1.4%)
因为 inline 工具函数会在每个引用它的翻译单元里各生成一份(PE/COFF 链接器没有 ICF 合并)。
这再次印证:拆分解决的是**可维护性**,不是体积(本轮的 −24.6% 全部来自 ICO 256×256 条目改 PNG)。

## 七、文档

Documents/agent_mem.md:§9.37 新增“七、多文件拆分(已完成)”与“八、仍未完成”(只剩
Lane → LanePanelState、模态提示解耦、MF4 压缩块三项);§11.2 新增第 91、92 条(多文件化的
唯一安全手法 + 体积代价)。Documents/refactor_report_2026-10-16.md 同步更新 §五/§七/§八。
2026-10-16 04:02:36 +08:00
DeepSeek V4.1 Flash 0871eb0cba 分组折起热区与改名输入框修正 + 数据模型去冗余(单一名称)+ 新增 Components/Shared 组件 + 过时注释/命名大清理 + 二进制瘦身 25.6%
## 一、UI 交互修复(用户报告)

① 分组折起热区(用户要求):点分组**名称**不再折起,只有**折叠三角**或**「组X」标签**
   才折起。RowPart 新增 GROUP_TITLE;RowHitPart() 为分组行判出三角 / 复选框 / 关闭 ✕ /
   「组X」标签矩形 / 其余 BODY;OnLButtonDown 只在这两个热区调用 ToggleGroupFromRow()。
   附带:双击「组X」不再进入改名(该区域现为折起热区),双击分组名仍可改名。
② 分组改名输入框不再吞掉「N 项」:RowNameRect() 对分组行的右边界改为「项数左边缘 − 6*scale」;
   新增 GroupLabelText()/GroupStatsText() 作为文本与宽度的唯一来源(PaintGroupRow 与
   RowNameRect 共用),避免名称可用宽度与实际绘制文本漂移。

## 二、数据模型去冗余(用户指出的重点)

- SignalData / SignalGroup **只保留一个名称字段** name:删除 displayName、删除 Title()
  (31 处调用点改为直接用 name);DataSet::SetSignalDisplayName / SetGroupDisplayName
  → RenameSignal / RenameGroup,且**空串 / 纯空白一律拒绝**(名称不再有回退值)。
  依据:信号靠 SignalRef 下标定位、从不按名字匹配 ⇒ 两个字段是同一语义的两份副本。
  收益:每条信号 / 每个分组少一个 std::wstring(24B + 堆分配)与一条 Title() 分支。
- 删除 Application::m_loadingBytes(全仓既不读也不写的死成员,注释却称“用于展示体积”)。

## 三、新增 Components/Shared 组件(通用 UI 控件)

- 从 Utilities/PaintUtil.* 迁出全部「控件类」绘制/命中:图标(+ / ✕ / 三角)、复选框、
  滑块(UiSliderRatioFromX / UiDrawSlider)、内联输入框(UiDrawEditField /
  UiDrawEditFieldContent / UiEditTextLeft / UiEditOffsetAt / UiEditFieldMouseDown /
  UiEditFieldMouseMove)、颜色样块(UiDrawSwatch / UiSwatchNeedsOutline)、
  取值胶囊配色(UiInk / UiColorLuminance / UiContrastInk)。
- PaintUtil.* 只保留低层绘制原语(矩形 / 圆角 / 直线 / 批量实线 / 颜色混合 / 文本与测量)。
- 依赖方向固定:Components/* → Components/Shared → Utilities(不得反向,Utilities 不再含控件)。
- Makefile 与 .preflight/perf/build.ps1 已登记新目录;调用方改 include "UiControls.hpp"。

## 四、过时注释 / 命名批量修正(承接上一轮未做完的部分)

- 注释:文件下标→分组下标、文件内信号下标→组内信号下标、所属文件下标→所属分组下标;
  措辞统一 文件分组→分组、移除该文件→移除该分组、打开 CSV 文件→打开数据文件 等一批。
- 文不对题的形参名:VisibleIndexRange(const TimeAxis& file,…) → axis;
  Application::LoadFileAsync(…, SignalGroup& file,…) → group。
- 函数名里的 File 语义(上一轮只改了 RowKind::FILE):PaintFileRow→PaintGroupRow、
  FileCloseRect→GroupCloseRect、OpenFileMenu→OpenGroupMenu、FileGroupLabelWidth→GroupLabelWidth、
  StyleEditField::FILE_NAME→GROUP_TITLE;局部变量 fileRow/fileHeight/isFile →
  groupRow/groupHeight/isGroup。
- 修复 DataModel.hpp 两处被压扁的注释块(/**<TAB><TAB>* @brief …)与 Application::NewTab
  上方重复的文档块。
- ⚠️ 踩坑:GROUP_NAME 撞上 nb30.h(windows.h 引入)的 #define GROUP_NAME 0x80,
  导致枚举编译失败,改名 GROUP_TITLE 解决。

## 五、二进制瘦身:1 013 248 → 754 176 B(−25.6%,省 259 KB)

- 真因不是 PlotView.cpp,而是 Resources/JustDataInspector.ico 里 **256×256@32bpp 以未压缩
  BMP 存储的条目(270 376 B,占整包 27%)**。改为 PNG 存储(Vista+ 标准做法,观感零变化)
  ⇒ 该条目 270 376 → 11 280 B、整个 ico 302 430 → 43 334 B(小尺寸仍保留 BMP)。
- 实测结论:“拆分大文件 ⇒ 瘦身”不成立(同一批机器码仍要进二进制);
  -ffunction-sections/-fdata-sections/-Wl,--gc-sections 在本工程反而 +9 216 B(已回退)。
- ⚠️ .o 体积受节对齐/重定位表主导,判断“代码搬去哪了”要用 nm,不要看 .o 字节数。

## 六、验证(全部离屏:SW_HIDE + 不注入真实输入 + 进程/线程 BELOW_NORMAL)

- panelanim 156/156(连跑 5 次稳定;新增/更新:点分组名不折起也不滚动、扫掠定位「组X」热区、
  筛选态与普通态折叠三角逐像素一致、改名输入框右边界早于项数文本);verify 49/49;
  datacheck 69/69(含“重命名写入唯一名称字段且空名被拒”);edgecheck 46/46。
- A/B:回退两处 UI 修复 ⇒ 10 项如实失败;恢复后全绿。
- make release / make debug 零警告。

## 七、本轮未完成(已在报告与 agent_mem.md 中如实记录)

- PlotView.cpp(13 695 行 / 544 KB)按功能拆分为多个协作文件:未执行。
  前置改造是把第 13–888 行的匿名命名空间(~870 行常量与工具函数)改成具名命名空间并逐个
  inline,再逐个文件搬移并反复验证;时间不足以安全完成,故主动不做(附可执行方案)。
- SignalPanel.cpp / Application.cpp / DataModel.cpp 的多文件化、Lane 抽出 LanePanelState、
  SetStatusMessage 模态阻塞解耦:均未执行。

## 八、文档

- 新增 Documents/refactor_report_2026-10-16.md(本轮详细报告);
  删除 Documents/refactor_report_2026-10-15.md(有效内容已并入 agent_mem.md 与本报告)。
- Documents/agent_mem.md 新增 §9.37 与 §11.2 第 85–90 条;更新数据层架构摘要(单一名称字段);
  §12 历史附录加“时效声明”。
2026-10-16 03:25:48 +08:00
DeepSeek V4.1 Flash 5831a0ea0d 分组动画改「抽拉卷尺」式刚性平移 + 悬浮组名折起一边滚动一边收起 + 筛选态折叠三角修正 + 样式区「目标」行绘制/布局修正
① 分组折起/展开动画重做为「抽拉卷尺」式刚性平移(推翻上一轮的“按行缩放”):
   - 用户要求:折起时所有信号一起向上平移、逐条消失在组名栏里;展开时逐条从组名栏
     向下冒出;卷尺上的元素相对位置不变。
   - 旧实现按行高缩放 ⇒ 元素被压扁(相对位置改变),且每帧行高都在变、每帧都要重建
     行集合。
   - 新实现:整组作为一个刚性整体平移,只维护一个逐行平移量 m_rowShift;**行高永不改变**,
     平移只作用于“组名行之后”的行(组名是卷尺外壳,始终不动)。
     收起 = shift 从 fromShift 推到 blockHeight(全部藏进组名栏);展开 = 推到 0。
     ease-out cubic;时长 = clamp(115 + 6×行数, 150, 320) ms。
   - 动画期间**不重建行集合**(AdvanceGroupAnimations 不再设 m_rowsDirty)——这是流畅的关键。
   - 中途反向点击(折叠没播完就点展开)从**当前位移续接**(GroupAnim::fromShift),不再跳变。
   - m_contentHeight = 自然高度 − Σ位移;RowRect() 减去本行位移;
     HitRow() 改为逐行比较实际绘制位置(动画中行位置与内容坐标不再一一对应)。

② “藏进组名栏”的裁剪(新增 AnimationHideTopForRow / ClampRowClipTop):
   - 信号行越过组名**绘制位置**的下边界即被裁掉;已经完全越过的行整行跳过。
   - 除 GDI 裁剪(PaintRowText / FillRowRect 用的 m_rowClipRect)外,还必须用
     g.SetClip(clip, CombineModeIntersect) 收紧 GDI+ 裁剪 —— 行内的色块 / 复选框 /
     网格徽标走 GDI+、只受 SetClip 约束,不收紧就会画到组名栏上面去。

③ 悬浮组名折起改为“一边向上滚动一边收起”(推翻上一轮的“先滚动到位再折起”):
   - 用户要求:悬浮组名既然在最上方,就一边向上滚动一边收起,保证它始终在最上方;
     动画完成时滚动偏移恰好落在“组名正好在最上方”且信号已经收起。
   - 实现:GroupAnim::scrollSynced —— 滚动偏移与平移量按同一进度推进
     (m_scrollOffset = scrollFrom + (scrollTo − scrollFrom) × eased),二者合成让组名的
     屏幕位置恒定;目标 scrollTo = min(组名的内容偏移, 折起后可达的最大滚动量)
     (后者用于“折起后内容比列表还短”的情形,避免结束时被强行夹取 ⇒ 无跳变)。
   - 旧的 m_pendingGroupToggle 两段式机制与 StartScrollAnimationTo 的挂起参数已删除;
     用户滚轮 / 拖滚动条 ⇒ CancelScrollAnimation() 只解除同步,折叠/展开继续播放。

④ 筛选状态下折叠三角状态修正:
   - 根因:PaintFileRow 直接读 m_tab->IsCollapsed()(普通模式的折叠状态),而筛选模式用的是
     独立的 m_filterCollapsed ⇒ 筛选里折叠/展开时三角不变化。
   - 修法:改读统一的 IsGroupCollapsed()(按当前模式分派)。

⑤ 样式区「目标」行的绘制与布局修正:
   - 绘制:PaintTargetRow 误用列表裁剪版 PaintRowText(文本被裁到 m_listRect),而目标行在
     样式区、根本不在列表矩形内 ⇒ 信号名与 GX 标签文本被完全裁掉。改用不裁剪的 UiText,
     并在 PaintRowText 注释里写明“样式区不得使用”。
   - 布局:名称从内容列左边缘开始、GX 标签**右对齐到内容列右边缘**(与线宽滑轨/数值框同列宽
     同两端),两者不重叠;无 GX 标签时名称可用宽度 = 整个内容列(因此更宽,仍左对齐)。
   - 双击改名:EditFieldRect() 对目标行只返回 m_targetNameRect,不再返回内容列全宽
     (旧实现会把 GX 标签包进输入框)。

验证(全程离屏 SW_HIDE,不占用鼠标 / 键盘 / 屏幕;进程与线程均 BELOW_NORMAL):
   - .preflight/perf/panelanim_main.cpp 147/147 通过(连跑 3 次稳定):
     刚性平移(行高不变 / 间距恒定 / 组名行不动 / 可见行数单调减少 / 逐帧断言组名栏像素零变化 /
     中间帧无行集合重建 / 帧耗时)、中途反向切换续接不跳变、筛选态折起与项数、
     筛选态与普通态折叠三角逐像素 0 差异、悬浮组名命中(不穿透)、
     同步滚动(逐帧钉住 + 恰好落位 + 无跳变)、真实 MF4 单点信号两种自适应逐位一致、
     目标行文本像素存在 + 两端对齐 + 输入框不覆盖 GX 标签 + 无标签时名称更宽。
   - A/B 五组回退分别 4 / 6 / 6 / 2 / 1 项如实失败。
   - 既有 verify 49/49、datacheck 69/69、edgecheck 46/46;make clean && make all 零警告。

记忆:Documents/agent_mem.md 新增 §9.36、改写 §11.2 第 77–81 条并新增 82–84 条,
标注 §9.35 的“按行缩放动画”与“先滚动再折起”已被推翻。
2026-10-16 02:40:31 +08:00
DeepSeek V4.1 Flash 1d314ddc24 信号面板分组折起动画 + 筛选态折起(项数按筛选计数)+ 悬浮组名可交互与“先滚动再折起” + 适应视图与 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 备注。
2026-10-16 01:25:54 +08:00
DeepSeek V4.1 Flash cb9d3e4b8f 修复浮动面板双击穿透、网格高亮触发来源、新增样式区「目标」行与联动平滑滚动
用户 2026-10-15 第六轮报告的四个问题与要求(详见 Documents/agent_mem.md §9.34):

1. 在网格内的实验性功能浮动面板上双击任意位置会穿透到下层网格,导致网格触发自适应。
   根因:PlotView::OnDoubleClick 在“差值标签 / 光标标签 / 光标线 / 图例”都不命中时
        无条件调用 FitAll()。
   修法:在该函数最前面(高于一切下层命中)用 TopPanelAtPoint() 判定,命中浮动面板
        则直接消费(面板自身双击无额外语义)。确立“上层元素的交互不得影响下层”的规则。

2. 未开启任何工具栏功能时鼠标移到网格上会让整个网格变色高亮(旧需求已不适用)。
   修法:取消“鼠标在网格里移动就高亮”的行为;改为在信号选择区域悬停某条信号时,
        若该信号已被勾选绘制到某个网格,则该网格变色高亮,并且该信号曲线本身也高亮;
        鼠标移开即恢复。悬停未勾选的信号不产生任何高亮。
   实现:PlotView 新增 m_externalHoverSignal + SetExternalHoverSignal()(与 m_hoverSignal
        分开保存,因为绘图区 OnMouseLeave 会清掉后者,而列表悬停时鼠标不在绘图区内)、
        EffectiveHoverLane();SignalPanel 新增 HoveredSignal(),并在 InvalidateRows()
        里一并作废;Application::Render() 每帧同步(覆盖 WM_MOUSELEAVE / 数据变化 /
        切换标签页等全部路径)。GX 徽标的悬停高亮仍按鼠标位置独立判定。

3. 在信号样式自定义区域最上方(预设颜色色块之上)新增「目标」行。
   内容:当前正在被编辑的目标信号名称;若它已绘制到网格,则在名称后方显示所在 GX 标签。
   交互:与信号选择区域的信号行一致 —— 名称/GX 标签悬停有高亮提示,且悬停时同样触发
        该信号所在网格高亮 + 曲线高亮;单击不进入编辑(只触发联动滚动),双击才进入改名
        (复用 StyleEditField::SIGNAL_NAME,m_editRow < 0 表示目标是选中信号)。
   实现:SignalPanel 新增 m_targetRowRect / PaintTargetRow / UpdateTargetRowGeometry
        (名称宽度与“是否有 GX 标签”都可能在不重排时变化,因此按需重算并带缓存键)、
        StyleHit::TARGET_NAME / TARGET_BADGE;DesiredStyleHeight() 与 LayoutStyleControls()
        相应多算一行。

4. 联动机制更新:从文件导入大量信号(如 MF4 的 124 条)时,被选中的信号可能被滚动到
   可见范围之外而找不到。
   修法:新增 SignalPanel::ScrollToSignalAnimated()(+ AdvanceScrollAnimation),
        触发联动时把信号列表平滑滚动到目标信号完整可见:
        * 目标在视界上方 ⇒ 向上滚动,且顶部让出一个组行高度,避免被悬浮组名遮挡;
        * 目标在视界下方 ⇒ 向下滚动到完整露出;
        * 已经完整可见则不动;
        * ease-out cubic(越远越快、靠近变慢),时长 = clamp(距离×0.35ms, 130, 300),
          总时长有上限;用户滚轮 / 拖滚动条立即取消动画。
   触发点:绘图区内把某条信号选为当前目标(点击图例、点击信号曲线、双击图例改名)
        ⇒ PlotView::SelectSignal() 发布请求(TakePanelScrollRequest()),宿主在鼠标消息
        处理后调用 ApplyPendingPanelScroll();样式自定义区的任何交互(预设色块、自定义
        颜色入口、样式按钮、线宽/透明度滑块与数值框、「系数/偏移」「镜像」二级菜单、
        绝对值)以及「目标」行的单击也都会触发同一次滚动。
   宿主:Application 新增 16ms 定时器(kScrollTimerId)驱动动画,并由 Render 自管开关
        (与提示浮层淡出同一套做法)。

验证(全部离屏,不占用鼠标 / 键盘 / 屏幕):
- .preflight/check(SW_HIDE + 只注入合成消息 + 进程/线程 BELOW_NORMAL)**73/73 通过**,
  连跑 3 次稳定:[A] 浮动面板双击不移视界,且对照组(空白处双击)必须触发自适应;
  [B] 鼠标在网格里移动不高亮;[C] 列表悬停 → 网格高亮 + 移开恢复 + 未勾选信号不高亮;
  [D] 目标行的布局/占位/GX 标签位置与显隐/悬停联动/单击不编辑 + 双击改名(初始文本正确);
  [E] 长列表 122 行:动画分多帧、单调、时长上限 300ms、目标完整可见且不被悬浮组名遮挡、
      已可见时不动、滚轮取消动画;[F] 样式区交互与绘图区联动都会滚动到目标并完整显示;
  [G] 布局回归(目标行在列表下方、样式区仍完整、默认无悬停与高亮)。
- A/B 判别:回退六处实现后 **15 项如实失败**(含"面板双击仍触发自适应"、"鼠标移到网格即高亮"、
  "目标行不联动"、"联动瞬移而非动画")。
- make release:全部源文件重新编译 **零警告**;链接用临时 `make release TARGET_FILE_NAME=Target_check` 通过。

同步记忆:Documents/agent_mem.md 新增 §9.34、§11.2 第 72–75 条硬约束、变更日志新行。
2026-10-15 20:32:16 +08:00
DeepSeek V4.1 Flash 73389f424c 适应视图:撤回旧策略并重做边界规则(单值散点让出半径 + 单点簇视图)
用户 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 的第四小节显式标注为"已作废/被取代")、变更日志新行。
2026-10-15 18:35:11 +08:00
DeepSeek V4.1 Flash aee3a388b5 修复信号面板滚动条跟随、工具栏连点丢状态、图层竖向网格线步长、多网格单点自适应与单点信号拖拽
用户 2026-10-15 报告的五个问题,逐一给出根因与修法(详见 Documents/agent_mem.md §9.32):

1. 拖动信号面板滚动条、鼠标移到绘图区后“滑块卡在原地不动”
   根因:Application::WM_MOUSEMOVE 的 plotOnlyMove 只看 inPlot && m_mouseOverPlot,
        拖滚动条进入绘图区后判定“本帧只影响绘图区” ⇒ 面板滚动偏移已变却从不重绘。
   修法:新增 draggingNonPlot(正在拖非绘图区组件时一律走完整失效)。

2. 工具栏快速连点 / 双击丢状态
   根因:(a) 窗口类带 CS_DBLCLKS,快速第二次按下被合成 WM_LBUTTONDBLCLK 而工具栏未处理
             (那一次按下完全丢失);
        (b) HandleMouseMove 只要鼠标不在工具栏上就调 OnMouseLeave(),清掉 m_pressedIndex
             ⇒ 按住期间从按钮边缘划出再回到按钮松手不会触发任何动作。
   修法:WM_LBUTTONDBLCLK 命中工具栏时按“按下”处理;m_activeTarget == TOOLBAR 期间不调 OnMouseLeave()。

3. 网格叠底:拖拽期间竖向网格线错位 / 丢失,操作信号样式时“闪烁”
   根因:PaintLane 的竖向主刻步长由 Tab().ViewMin/ViewMax 推出,而渲染信号图层时该区间已被
        RenderLaneLayerRegion 改成“宽窗口”(≈2× 前台)⇒ 图层辅助线步长 2×、位置与前台不同、
        “与主刻重合则跳过”的判据也随之错位 ⇒ 贴图后网格线错位 / 整条丢失,
        并在“图层帧 ↔ 结算帧”之间来回切换时闪烁。
   修法:新增 m_renderTimeStep,由 RenderLaneLayerRegion 用前台视界跨度算好传入
        (与纵向 m_wideTickSpan/m_wideTickHeight 同一思路),PaintLane 优先使用它。

4. 多个网格、每个网格只有一个数据点时「适应视图」把数据点甩飞
   修法:新增 FitSinglePointLanes(),按用户给出的规则:
        * 左右边界 = 所有网格中最左 / 最右数据点的时间,再向外各偏“一个网格小格”的时间跨度;
        * 逐网格 Y 自适应,并在纵向上下各留“一个 Y 小格”的高度;
        * 全部数据点在同一时刻时 ⇒ 时间跨度 1s 且以该时刻居中(与首个单点信号的初始视图对齐);
        * 某网格只有一个取值 ⇒ 该网格 Y 跨度 1 且居中。
        仅在全部可见信号都是单点(finiteCount == 1)时启用,否则交回常规 FitAll。

5. 单点信号的悬停高亮与拖拽换网格
   修法:HitTestSignal 新增单点分支(SinglePointScreenPos + 容差 = max(常规容差, 该点绘制半径));
        悬停用环状光晕且不改鼠标形状(QueryCursor 不查 HitTestSignal),按下拖拽后才改 IDC_SIZEALL;
        拖拽虚影对单点信号回退为实心点 + 环。

验证(全部离屏,不占用鼠标 / 键盘 / 屏幕):
- .preflight/check(SW_HIDE + 只注入合成消息 + 进程/线程 BELOW_NORMAL)**67/67 通过**,连跑 3 次稳定。
  其中 [C] 直接量“宽基底图层位图自身”的网格线间距(图层主刻 123px vs 前台 122px、辅助线 25px vs 25px),
  并断言拖拽帧确实来自宽基底贴图路径(m_wideLastValid)。
- A/B 判别:回退上述六处修复后 **17 项如实失败**([C] 辅助线 49px vs 25px、20 vs 40 条、最大偏移 539px)。
- make release:三个改动文件重新编译 **零警告**;链接用临时 `make release TARGET_FILE_NAME=Target_check` 通过。

同步记忆:Documents/agent_mem.md 新增 §9.32、§11.2 第 64–68 条硬约束、变更日志末行。
2026-10-15 17:58:33 +08:00
DeepSeek V4.1 Flash 68d8b7ede6 自适应范围/时机澄清 + 适应视图与 Y 轴自适应互不干扰 + 吸附跟随时间偏移
一、澄清「自适应范围与时机」(并修正记忆里不完善的表述)
 * 准确说法:「额外要求视界内至少有一个可见数据点」这句不完善,完整规则是:
   - 首个网格显示内容时对它做完整自适应 —— **Y 轴与时间轴都**匹配自己的数据范围;
   - 只有一个数据点的信号必须显示在网格**中间**,保证其可见
     (FitSinglePointSignal:数据点居中、时间跨度 1s、Y 轴跨度 1);
   - **后续新增网格只做 Y 轴自适应,不再调整时间轴**。
 * Documents/agent_mem.md 相应章节(§9.30 正文、§11.2 第 58/60 条)已按此改写。

二、修复「适应视图」与「Y 轴自适应」互相干扰
 * 根因:两条自适应路径的留白不同 —— 整段自适应(ComputeLaneRange → PadRange)用 6%,
   「选区 Y 轴自适应」(AutoRangeLaneToView)用 5%。视界未变时两者本应完全相等,
   于是「点过适应视图 → 打开 Y 轴自适应」会算出不同区间、网格被无意义重绘
   (实测切一次 143513 像素),反向操作同样(137326 像素)。
 * 修法:新增 DataModel.hpp 的 PadValueRange() 作为**唯一**留白实现(含退化区间规则
   max(1, |值|×0.1)),PadRange 与 AutoRangeLaneToView 共用它。
 * 结果:「适应视图 → 开自适应 → 关自适应 → 再适应视图」四个步骤上生效区间逐位相同,
   绘图区逐像素 0 差异;而真正改变视界(缩放)时仍然正常重绘。

三、修复「适应视图」在单点信号上让数据点“飞了”
 * FitAll() 开头先尝试 FitSinglePointSignal():仅一个网格、且唯一可见信号只有一个有效
   数据点时,走与首次加载完全相同的单点初始视图(数据点居中、时间跨度 1s、Y 跨度 1),
   并保持幂等(重复点击像素不变)。

四、修复光标吸附未考虑「调整 → 时间轴偏移」
 * 根因:采样点 k 画在 time[k] + offsetTime 处,而光标存的也是显示时间,
   但吸附一直在源时间空间查找 —— 设过时间轴偏移后吸附会落到历史位置。
 * 修法:SignalSnapEnabled / HasVisibleSample / NearestSnapSample / LocalSampleSpacing
   全部增加 timeShift 形参:可见性与点数统计比较 time[k] + timeShift;
   NearestSnapSample 内部用 time - timeShift 回源空间查找、返回时再加回去;
   UpdateAutoScatterStyles(自动点线)同步传入 offsetTime,保持与吸附判定同源。

五、验证(离屏 .preflight/check:SW_HIDE 窗口、不注入真实鼠标键盘、进程/线程 BELOW_NORMAL)
 39/39 通过(连跑 3 次稳定):
 [A] 四个步骤的生效区间逐位相同 + 绘图区逐像素 0 差异(对照组:真缩放重绘 79581 像素);
 [B] 单点信号的适应视图与首次加载一致(1s/1、居中、幂等);
 [C] 首个网格自适应时间轴、后续新增/隐藏信号都不再改动时间轴;
 [D] 偏移 +0.3 时吸到“画面上的点”,偏移 0 行为不变,偏移把点移出视界时不吸附;
 [E] 回归:单点默认样式(点线 + 最大线宽)与“2…16 点不再改点线”均保持。
 A/B:回退三项修复 ⇒ 7 项如实失败(含 143513 / 137326 像素的重绘症状)。
 make release 零警告(用户正运行 Target.exe,链接用临时 -o 名字验证通过)。

六、规则同步
 Documents/agent_mem.md 新增 §9.31、§11.2 第 58–63 条(含修正后的自适应范围/时机规则)
 与变更日志。
2026-10-15 15:46:14 +08:00
DeepSeek V4.1 Flash 57cc714c1d 组名平滑悬浮 + 单点信号初始视图 + 光标吸附统一判定(自动点线同源)
一、信号列表「组名悬浮」平滑化、且不再遮住下一个组名
 * 悬浮位置改为 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 条硬约束与变更日志。
2026-10-15 15:10:36 +08:00
DeepSeek V4.1 Flash 9f6de3bba4 全局 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 条硬约束与变更日志。
2026-10-15 14:10:13 +08:00
DeepSeek V4.1 Flash 3bca3b1201 修复「信号列表滚动把组名/信号名画到筛选框上」+「边沿统计面板在不移动鼠标时显示冻结」
一、列表滚动越界绘制(SignalPanel + PaintUtil)
根因:UiText 走 GDI DrawTextW,而 GDI 文本不遵循 GDI+ 的 Graphics::SetClip。
列表虽然用 g.SetClip(m_listRect) 裁剪,但被滚出视口的「部分可见」行(分组行、信号名)
仍会把文字画到列表上方的筛选框区域(用户报告 MF4 多信号场景);UiFillRect 对大面积的
不透明矩形会改走 GDI FillRect,同样越界。
修法:
 * PaintUtil 新增 UiTextClipped(g, text, font, rect, argb, align, clipRect)
   ——在同一个 HDC 上 SaveDC + IntersectClipRect + RestoreDC;UiText 重构为转发
   clip = nullptr,行为与旧实现完全一致。
 * SignalPanel 新增 PaintRowText()(裁剪到 m_listRect)与 FillRowRect()(夹取到
   m_listRect),分组行与信号行的全部文本、行底色/选中底色都改走它们。

二、两个边沿计数浮动面板「鼠标不动就不刷新」(PlotView::PanelSlotKey)
根因:浮动面板使用像素图层缓存(m_panelDc + m_panelSlots + PanelSlotKey),而界面上显示的
「上升沿 / 下降沿」是分帧推进出来的中间结果,PanelSlotKey 里没有这些数字 ⇒ 统计推进不会
让键变化 ⇒ 面板一直贴旧像素 ⇒ 显示停在「统计中…」或某个中间值,只有移动鼠标(改变面板
层序 / 悬停 / 鼠标坐标等其它键)才会重绘并显示早已算完的正确结果;扫描推进本身并没有停。
修法:PanelSlotKey 内按面板实际显示的条目回调 EdgeCountsFor(COUNT 面板)/
EdgeCenterCountsFor(CENTER 面板),把 (rising, falling, complete) 一并 mix 进键。

三、验证(离屏 .preflight/check:SW_HIDE 窗口、不注入真实鼠标键盘、进程/线程 BELOW_NORMAL)
39/39 通过(连跑两次稳定):
 * [A] 40 信号同一网格、滚轮下滚:列表上边界以上区域逐像素 0 变化,列表内部仍有变化
   (证明是裁剪而不是整块不画);
 * [B] 3 信号 × 300 万采样、两种统计同时开启:面板像素图层缓存键在扫描期间持续变化,
   且不注入任何鼠标消息即可 complete;
 * [C] 真实 TestData/MF4-Record.MF4(124 信号 / 3 条时间轴):同样 0 像素变化。
 A/B:git stash 回退本轮修复后重跑 ⇒ [A] 1571 像素被污染、[B] 两个面板的缓存键均不推进,
 如实失败。
 make release 零警告(用户正在运行 Target.exe,链接用临时 -o 名字验证通过)。

四、规则同步
 Documents/agent_mem.md 新增 §9.28、§11.2 第 53/54 条硬约束与变更日志。
2026-10-15 12:13:40 +08:00
DeepSeek V4.1 Flash 6eedbe3c5d 数据层大重构:文件→分组 / 时间轴一等公民 / 加载器抽象 + CSV 多时间轴 + MF4 支持
一、数据模型解耦(详见 Documents/refactor_report_2026-10-15.md 与 agent_mem §9.27)
1. DataFile → SignalGroup:删除 filePath / fileName / time / timeColumnName / timeMonotonic /
   minTime / maxTime —— 「分组」是应用内的逻辑容器,加载完成后与磁盘文件再无关系。
2. 新增 TimeAxis(name / time / minTime / maxTime / monotonic / finiteCount),一个分组可含 1..N 条;
   SignalData 新增 axisIndex 指向所属时间轴,采样值与时间的对应关系自包含。
3. SignalData 删除冗余的 rawHeader(name 的副本)与 unit(由列名猜出、全仓无人使用);
   名字只保留 name(源名称)+ displayName(用户重命名),Title() 统一展示。
4. SignalRef::fileIndex → groupIndex;DataSet:AddGroup/RemoveGroup/GroupAt/GroupCount/Groups()/
   SetGroupDisplayName/RemapSelectionAfterGroupRemoval + 新增 Axis(ref);
   TabConfig:SetGroupVisible/GroupHasVisible/GroupAllVisible/RemapAfterGroupRemoval +
   RowKind::GROUP / CMD_GROUP_*。
5. 采样与统计 API 首参由 const DataFile& 改为 const TimeAxis&:
   SampleSignalAt / SampleDisplayedSignalAt / FindNearestSample(Utilities)与
   SampleSpacingPixels / VisibleIndexRange / EdgeIndexRange / MeanFilterValueAt / WeakPeakValueAt /
   EdgeCountsFor / EdgeCenterCountsFor / EdgeScanFor(PlotView)。
6. 视图层:ResolvedSignal = { data, style, group, axis, visible };PolylineCacheEntry::file→group;
   EdgeScanState::file→axis(统计身份 = 时间轴 + 信号);
   ComputeVisibleTimeBounds 只统计可见信号真正用到的时间轴。

二、加载层抽象(格式与模型彻底解耦)
- 新增 Utilities/Inc/DataTable.hpp:DataTable(通用列优先表格)+ LoadProgress/Info +
  TableBlock(一个时间轴块)+ LoadedSource(源文件全部块)。
- 新增 Utilities/Inc|Src/DataLoader.*:唯一入口 LoadSignalSource 按扩展名分发 +
  IsSupportedSignalFile + SignalFileDialogFilter。
- 新增 Utilities/Inc|Src/CsvLoader.*:CSV 解析 + 多时间轴切分 + 各轴按自己的时间列裁剪。
- 新增 Utilities/Inc|Src/Mf4Loader.*:ASAM MDF v4 解析。
- 删除 Utilities/Inc/CsvTable.hpp 与 Utilities/Src/CsvTable.cpp。
- 唯一装配点 BuildSignalGroup(LoadedSource&&, paletteBase, group, error, progress):
  时间轴去重合并 + 信号命名 + 调色板取色 + 统计与粗粒度包络;新增格式只需写一个加载器。

三、CSV 多时间轴(修正用户报告的缺陷)
- 只有名为 t/time/timestamp/simulation time/时间/时间轴 的列是时间轴(判据收紧,
  不再用「包含 time」以免把 timeout 误判成时间轴)。
- 每个时间列开启一个新块,其后直到下一个时间列之前的列都属于该块;各块按自己的时间列裁剪
  ⇒ 支持各轴长度不同(Multi_Timeline.CSV 实测切出 3 条轴,长度 7/4/7);
  **不再把其它 time 列当成信号显示**。
- 完全没有时间列时保留旧行为(第一列当时间轴,单轴)。

四、MF4 支持(新功能)
- 加载后新建一个分组、组名 = 文件名,文件内全部信号归入该分组。
- 每个通道组(CG)是一条时间轴,内容相同的时间轴自动合并(实测 27 个 CG → 3 条轴 / 124 条信号)。
- 实现 MDF v4 块解析(HD/DG/CG/CN/CC/DT)、1–64 位整数(任意位偏移、含符号扩展)与
  32/64 位 IEEE 浮点解码、无效位(inval bit)处理、线性换算 phys = cc_val[0] + cc_val[1]*raw。
- TX 文本块同时兼容规范写法(U32 长度 + UTF-16/UTF-8)与 CANape 的「无长度前缀 NUL 结尾裸文本」。
- 未实现(跳过该组):压缩块 ##DZ/##HL/##SD、##DL 数据列表、字符串/MIME 通道。

五、顺带修复
- Application::Initialize:窗口类已注册(ERROR_CLASS_ALREADY_EXISTS)时复用而不是弹模态错误框
  (旧实现让同一进程第二次构造 Application 永久阻塞);Shutdown 注销窗口类并销毁窗口。
- UI 文案统一到「分组」:选中该分组全部信号 / 取消该分组全部信号 / 关闭该分组(从工作区移除)/
  请关闭部分分组或信号后重试。/ 尚未加载数据,请打开数据文件 / 打开数据文件以查看波形 /
  (可同时加载多个文件,各自成为一个分组);对话框过滤器增加 *.mf4/*.mdf。

六、验证(全部离屏:BELOW_NORMAL 优先级 + SW_HIDE 窗口 + 只注入合成消息)
- 新增 .preflight/perf/datacheck_main.cpp:69/69 通过(单轴 CSV 回归、多时间轴 CSV、MF4 组名/信号数/
  时间轴/主时间轴 ns→s/setSpeed 首样本与原始字节人工核对、DataSet 不变量、派生信号沿用源轴、
  MF4 集成 124 信号并一格 60~68ms/帧)。
- 新增 .preflight/perf/edgecheck_main.cpp(ASCII-only,替代被编码破坏的 panelcheck):46/46 通过
  (「调整」符号规则走真实菜单路径、边沿统计按帧推进/不饿死/逐位一致/无鼠标输入也完成、
  面板两列等宽与信号名限宽、弱峰复位控件)。
- 既有 .preflight/perf/verify_main.cpp(已适配新 API):47/47,另有 2 条本机未同步的旧规则断言
  (derived offsetTime kept / mouse in lane1 lower half),与本轮无关。
- make clean && make all 零警告。
2026-10-15 03:48:58 +08:00
DeepSeek V4.1 Flash fba40ee1a2 「调整」数值符号规则 + 边沿统计按帧推进(含首帧公平分摊)+ 浮动面板排版与弱峰复位控件
一、「调整」类数值的符号(SignalPanel)
1. FormatSignedNumber:0 不再显示 `+`(旧实现恒带符号 → `+0.00`),Y 轴偏移默认值显示 0.00。
2. FormatGainText:非 0 正值带 `+`、负值带 `-`、0 不带符号;仅用于非编辑态显示,
   进入编辑的初始文本仍为无符号纯数值。
3. 新增 FormatOffsetTimeText:`Δt:` 后按正负带符号(Δt:+5s 60ms / Δt:-5s 60ms / Δt:0s),
   打开面板 / CMD_OFFSET_TIME 提交 / CMD_OFFSET_RESET 三处统一走它。

二、边沿统计:按帧预算 + 每状态公平配额
(用户报告:按住横向光标不动时统计停在“统计中…”不更新;改中心轴参数回车后不动鼠标时停在部分值)
根因:预算按 GetTickCount64() 分桶,而系统时钟粒度约 15.6ms;RequestEdgeScanFrame 的
PostMessage 自持重绘把帧率拉高 ⇒ 同一 tick 内连画多帧却完全不推进。
1. Paint 开头 ++m_edgeScanFrame; m_edgeScanBudget = kEdgeScanPairsPerFrame;(按帧重置),
   删除 m_edgeScanTick。
2. 每个状态各自持有每帧配额(pumpedFrame/pumpedPairs):perState = max(60000, 1.2M/share),
   share = max(kEdgeScanFairShare=6, m_edgeScans.size())。
   注意:不能直接用 size() 分摊 —— 状态在同一帧内逐个创建,第一个那一刻 size()==1 会独占整额,
   同帧后面新建的状态全部被截断为 0 配额(即“只有第一条信号推进”)。
实测(合成 5 信号 × 200 万采样、同一网格):首帧 5 个状态全部推进;9 帧内全部扫完(期间不注入
任何鼠标消息);数值与独立参考实现逐位一致;阈值变化后就地重置、随后“停住不动”仍精确扫完。

三、浮动面板排版
1. 信号名宽度上限 kEdgePanelNameMaxWidth=150 逻辑像素:宽度计算与绘制都用 min(实测, 上限) +
   UiEllipsize(300 字符名字只比单字符宽 44px,旧实现要多 5100px)。
2. 统计信息两列等宽(新增 EdgeRisingText/EdgeFallingText/EdgeCountsPending):面板宽度按
   2×max(两列文本宽)+kEdgePanelStatColumnGap 预留,绘制时两列各占 (可用宽−间距)/2 ⇒ 多条信号的
   “上升沿/下降沿”列起点固定、严格垂直对齐。
3. 未完成时 rising 与 falling 两个数字后面都带 `…`(旧实现只在整行末尾追加一次,
   视觉上只跟在“下降沿”的数字后面)。

四、弱峰调整面板
1. 每行滑块右侧新增复位控件(复用还原图标绘制 PaintPanelResetIcon):悬停高亮 + IDC_HAND +
   一次性提示;点击恢复幅度 0 / 迭代 1 且 maxIterations 回到初始上限 10。
   EdgePanelLayout 新增 rowLevelResets/rowIterResets(并进 PanelSlotKey),两行按钮共用同一 X。
2. 迭代次数上限输入框的空内容提示改为“输入上限(当前N)”。
3. 滑块标签去掉多余的 %/N(kWeakPeakLevelLabel=调整幅度;新增 kWeakPeakIterationsSliderLabel=
   迭代次数;MEAN / CENTER 输入框的“迭代次数 N”保持不变)。

五、验证(全部离屏;进程/线程 BELOW_NORMAL、窗口 SW_HIDE、不注入真实鼠标键盘)
- 新增 .preflight/perf/panelcheck_main.cpp:46/46 通过(符号规则走真实菜单路径;按帧推进 /
  不饿死 / 精确性 / 自持驱动;两列等宽预留(含未完成时两处 …);名字上限;
  弱峰复位控件位置 / 对齐 / 点击恢复默认值与上限)。
- 既有 verify_main.cpp:47/47(另 2 条为本机 .preflight 未随远端同步的旧规则断言)。
- make release 零警告。

规则同步:Documents/agent_mem.md 新增 §9.26,§11.2 新增第 43–48 条硬约束,变更日志补一行。
2026-10-15 01:35:47 +08:00
DeepSeek V4.1 Flash 65f7625dce 修复「偶发的改一个信号的中心轴参数、其它信号也重新统计」
一、现象(用户报告)
加载 VERY_BIG_DATA.CSV、所有信号显示在同一网格、同时启用普通边沿计数与中心轴边沿计数
并勾选全部信号;改变某一个信号的中心轴参数时,**偶发**地让其它几个信号的统计也重新计算
(不是必现,多测几次才出现)。

二、根因(用间歇性探针实测定位,不是推测)
EdgeScanFor 的中心轴分支原先靠 AcquireCoveringCurve + ScheduleCurveJob 拿**整条**均值滤波曲线:
- 千万级信号(2580 万采样)一条整段曲线 ≈ 103MB;每多试一个参数值就多一条(参数变化产生新键),
  绘制侧按可见窗口还会再发布一份;
- 这些条目共享同一个曲线缓存,预算为 kCurveSpanCacheMaxBytes = 768MB;
- 一旦超限,PruneCurveEntriesLocked() 从**队首**淘汰 —— 最早发布的往往正是**其它信号**的整段曲线;
- 于是那些信号被判为「曲线未就绪」→ 界面显示「统计中…」并在后台重新排队计算 →
  表现出来就是「其它几个信号重新统计」,而且**偶发**(取决于当时缓存里积了多少条目)。

⇒ 统计数值本身是正确的,错的是「几条信号的统计通过一块共享缓存互相牵连」,
而这块缓存的压力取决于用户试过多少个参数值。这才是要根除的问题。

三、修法
1. EdgeScanState 新增 localCurve / localBegin / localEnd / localValid:
   中心轴扫描时**就地计算**自己需要的一小段均值滤波曲线(ensureLocalCurve):
   - 半径 = points * iterations(迭代 N 次 X 点均值滤波最多向两侧传播 X*N 个采样),
     再夹到 kDerivedCurveGuardMax;
   - 窗口至少覆盖 8 个扫描块(kEdgeBlockSamples * 8),把「窗口 × 迭代次数」的重复计算摊薄;
   - 窗口只有几十 KB~几百 KB,与曲线缓存、与其它信号**彻底解耦**;
   - 计算成本按「采样数 × 迭代次数」计入扫描预算(不会一帧算太多);
   - **扫完后立即释放** localCurve(区间查询需要时按需重算)→ 多信号网格下常驻内存很小。
2. 删除原「中心轴曲线未就绪就把整 tick 扫描预算清零」的门控:
   它会把**同一帧里排在后面的其它信号的统计一起饿死**(测试里表现为全部卡在「统计中」)。
   要「让路」只应跳过自己。
3. 加固状态上限:kEdgeScanStateLimit 16 → 64,且超限时**优先淘汰「已扫完」的最旧状态**,
   绝不淘汰正在推进的。上限太小会淘汰其它信号的已扫完状态 → 它们从头重扫(同一类症状);
   实测 10 个信号 × (普通 + 中心轴)= 20 个状态,原来的 16 就会踩到。
4. 附带收益:统计不再等后台算整条曲线,超大文件下第一个统计值出现得更快。

四、验证(全部离屏,harness 与探针均为 BELOW_NORMAL 优先级,不占用鼠标/键盘/屏幕)
1. uitest 新增 [AU](间歇性复现探针):5 个信号合并在同一网格、普通 + 中心轴全部启用,
   用真实编辑路径**连续改 25 次**某个信号的中心轴参数,每轮 14 帧逐帧检查**未被改动的**统计:
   CASE A = 状态被删除;CASE B = 状态还在但其曲线消失(显示 0 / "统计中");
   CASE C = 状态被重置或截断。
   - 修复前:baseline **0/10 完成**(全部卡在「统计中」,此时 curve entries 0);
   - 修复后:baseline **10/10 完成**,25 次编辑 **CASE A/B/C 全 0**,且 curve entries 0
     (证明统计已完全不需要后台曲线缓存)。
2. 新增「本地窗口 vs 整条曲线」正确性断言:points ∈ {5,17,64,300} × iterations ∈ {1,2,4}
   共 12 组,中心轴结果与「整条信号均值滤波后逐点判定」的参考实现**逐位一致**
   (证明 X*N 半径的保护带足够)。
3. 合计 789/789 通过(连跑 3 次:2 次全绿,1 次命中既有的菜单点击类 harness 抖动——
   [AP] reset 图标与面板菜单,重跑即好,与本轮改动无关);make release 零警告。

规则同步:Documents/agent_mem.md 新增 §9.25,§11.2 新增第 40–42 条硬约束
(派生曲线不得依赖共享整条曲线缓存 / 预算不得因等待而清零 / 状态上限必须大于 种类×信号数),
变更日志补一行。
2026-10-14 17:19:09 +08:00
DeepSeek V4.1 Flash 48bc534bc1 边沿统计改为「定向失效」:改一个信号的参数不再重算其它信号的统计
一、问题
加载 VERY_BIG_DATA.CSV 并把所有信号显示在同一个网格,同时启用「边沿计数」与
「中心轴边沿计数」(两个面板都作用于网格内全部信号)。此时改变**一个**信号的
中心轴边沿计数参数,两个浮动面板里的**所有**统计值都从头重新计算。

二、根因
参数提交路径(PlotView::EndEdit)与勾选切换路径里用的是 m_edgeScans.clear():
它把两个面板、所有信号的累积统计状态全部丢掉。于是:
- 其它信号(参数没变、键没变、块和依然有效)被迫从头重扫;
- 连普通「边沿计数」(centre == false)的状态也被一起丢掉 → 另一个面板也全部重算。

三、修法
1. 新增 PlotView::InvalidateEdgeScansFor(const SignalData* signal, bool centreOnly):
   按键(文件/信号/种类/样式指纹/参数/区间)只擦除**该信号**的累积状态;
   centreOnly = true 时只擦除它的「中心轴」状态。
2. EndEdit 的参数提交分支:
   - 中心轴参数(EDGE_CENTER_POINTS / EDGE_CENTER_ITERATIONS)变化 →
     InvalidateEdgeScansFor(该信号, true);该信号的普通边沿计数、以及其它信号完全不受影响;
   - 均值滤波参数变化 → 与统计无关(中心轴用**自己的**条目列表与自己的曲线键)→ 不作废任何统计。
3. 勾选/取消勾选信号:该信号的统计(与参数相关)依然有效,只是"是否显示"变化 →
   删除原来的三个 clear()(m_meanCurveCache/m_edgeScans/m_weakPeakCurveCache),
   不再作废任何缓存(改为依赖 m_sceneDerived 快照做局部重画)。

四、验证(全部离屏,不占用鼠标/键盘/屏幕;harness 与探针均降到 BELOW_NORMAL 优先级)
- .preflight/uitest 新增 [AT]:3 个信号合并到同一网格,中心轴统计全部扫完后,通过**真实编辑路径**
  (plot.BeginEdit(EDGE_CENTER_POINTS, 0, L"7") + plot.EndEdit(true))修改第 2 个信号的参数,
  断言:
  * 被改信号的中心轴状态已作废,且其条目参数确实变成 7;
  * 另两个信号的 scannedPair / rising **逐位不变**(累计块和没有被丢弃);
  * 被改信号的**普通**边沿计数状态 scannedPair / rising **逐位不变**(另一个面板完全未失效);
  * 未被改动的信号下一次查询仍然 complete(瞬时返回,不需要重扫)。
- 合计 774/774 通过(连跑 3 次稳定);make release 零警告。

规则同步:Documents/agent_mem.md 新增 §9.24,§11.2 新增第 39 条硬约束(参数变化必须定向失效、
禁止对累积缓存整体 clear()),变更日志补一行。
2026-10-14 16:30:12 +08:00
DeepSeek V4.1 Flash 375a15c8c2 派生曲线内存治理(弱峰调整等)+ 边沿统计改为统一预算状态机(含中心轴)+ 离屏测试零打扰
一、派生曲线内存治理(按上一轮报告的方案实施;绘制性能优先,同时为小内存设备留出空间)
1. PeakExtremum 字段重排(double value + uint32 index + int8 type):24 → 16 字节。
   编码器方波这类极值密集信号省约 1/3 的极值表内存,且**不改变任何数值结果**
   (value 保持 double,改成 float 会让结果与历史版本不一致):
   实测 3M 采样单次计算的工作缓冲峰值 33.23 → 26.73 MiB。
2. ComputeDerivedWindow 对「整段窗口」不再复制采样(直接引用原向量):
   8 B/采样 × 2580 万 ≈ 206 MB/条,而“预览曲线一律按整条信号计算”正是最常见情形。
3. 大窗口任务的并发上限由内存压力决定(CurveJob::hugeLimit:内存充足 2、紧张 1):
   单条整段曲线工作缓冲约 620 MB(2580 万采样),原先最多 4 条并发 → 峰值 2.5GB+;
   实测 4 并发 3M 采样 412 → 324 MiB,且外推值不再随线程数放大。
4. 内存严重不足时**直接拒绝计算**(DerivedMemoryAllows:可用内存 < 输出曲线本体 + 工作缓冲的
   最小需求)并只告警一次(新增 kMessagePlotLowMemory;宿主弹一次明确的提示框)。
   界面保持可用,绝不会因内存不足抛 bad_alloc 逸出线程导致进程终止。
5. 低内存模式“少量多次”:throttle = true 时并发降为 1、且每阶之间 Sleep(1),慢慢算完。
   内存充足时不做任何节流,尽可能快。
6. 取消标志贯通到算法内部(ComputeWeakPeakAdjust / ComputeMovingAverageIterated 新增
   cancel 参数,每阶 / 每 1M 采样检查一次)⇒ **过时任务立即退出并归还工作缓冲**;
   PauseDerivedAsync / StopCurveWorker 先 m_curveCancelAll = true 再 join。
7. AvailablePhysicalBytes() 结果按 200ms 缓存(绘制路径上不再每帧多次系统调用)。
8. 极值表按 count/8 预留容量(try/catch(bad_alloc) 保护),避免 push_back 反复扩容的 1.5x 峰值。
9. MeanFilterCurveFor / WeakPeakCurveFor(整条同步计算)现在只有测试使用;
   产品代码一律走 CurveSpanFor(窗口化 + 异步)。

二、边沿统计:统一「逐块累积 + 每 tick 预算」状态机(普通边沿计数与中心轴共用)
1. EdgeScanState + EdgeScanFor 成为唯一实现,EdgeCountsFor / EdgeCenterCountsFor 是薄包装;
   删除 EdgeCenterCountEntry / m_edgeCenterCountCache。
2. **未完成时主动请求下一帧**(RequestEdgeScanFrame → PostMessage(kMessagePlotDerivedReady)):
   修掉“鼠标不动就一直显示统计中,只有把鼠标移到面板上才更新”(根因就是没有重绘 → 预算不推进)。
3. 阈值/参数变化就地把状态重置(不新建条目,避免拖动时状态爆炸);重置当 tick 不展示部分值
   (修掉“除了最后一个信号显示了一个错误的且不更新的数字,其它变成统计中”);
   稳定后展示递增的部分值 + “…”;扫完展示精确值(统一走 EdgeCountsText)。
4. 中心轴判定所需曲线**只在后台计算**:只在 AcquireCoveringCurve 命中(键完全相同)时扫块,
   否则 ScheduleCurveJob 后本帧不推进。原先该调用没传 allowAsyncCompute(默认 false)⇒
   首帧 / 参数变化后同步算整条均值滤波曲线 ⇒ 界面停顿数秒(用户报告“一改参数整个程序卡住很久”)。
5. MeanFilterValueAt(光标读数)不再调整条同步的 MeanFilterCurveFor,改为窗口化 + 异步。
6. ScheduleCurveJob 改为幂等(m_curveRunningKeys 集合 + 队列去重):统计侧会每帧请求同一份
   整条曲线,若每次都当作新请求,正在算的那份立刻被判过时 → 请求→作废→重请求 死循环。
7. 扫描热路径加“无变换/无镜像/非中心轴”的紧凑循环:单次预算耗时 18ms → 1.3ms。
8. 实测(合成方波):6M 采样首帧 1.4ms(部分值)、单次最坏 2.1ms;
   拖动横向光标 10 次查询 1.07ms;**同一帧内 5 条信号合计 1.3ms**(预算跨信号共享);
   保持不动无需移动鼠标即收敛到与参考实现逐位一致。

三、离屏测试绝不打扰用户的前台工作
- .preflight/uitest 与 .preflight/probe 的 main 开头设置
  SetPriorityClass(GetCurrentProcess(), BELOW_NORMAL_PRIORITY_CLASS) +
  SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_BELOW_NORMAL),
  再长的测试也不会与用户的编辑/浏览抢 CPU(此前“打字不顺畅”的根因)。
- 继续保持:窗口 SW_HIDE、只注入合成消息(不动真实鼠标键盘)、每次注入后立即 ReleaseCapture
  (SetCapture 是系统级的,不释放会把用户的鼠标输入吸进测试进程)。

四、验证(全部离屏)
- .preflight/uitest 新增/强化:预算推进首帧不显示部分值 → 第二帧显示递增部分值 →
  保持不动收敛到精确值;未完成时会 PostMessage 请求下一帧(直接断言);
  拖动阈值期间 complete == false、不显示部分数字、总耗时 < 60ms;
  5 条信号同帧合计 < 120ms(实测 1.3ms);中心轴与参考实现逐位一致且无需鼠标移动即可完成。
- 合计 751/751 通过(连跑 3 次稳定);make release 零警告。

规则同步:Documents/agent_mem.md 新增 §9.23(含内存治理表与实测数据)、§11.2 新增第 32–38 条硬约束、
变更日志补一行。
2026-10-14 15:24:19 +08:00
DeepSeek V4.1 Flash d6d3fcd6c1 新标签页分组默认折起 + GX 徽标悬停不改光标 + 标题栏 RAM 改私有工作集 + 边沿计数改为预算推进
一、新建标签页的信号分组默认折起
- Application::NewTab(bool collapseGroups):启动时的首个标签页传 false(加载文件后可直接勾选
  信号),用户新建的标签页(Ctrl+T / 标签栏 ⊕ / 标签栏右键)传 true。
- TabConfig::SetGroupsCollapsedByDefault(bool) + SetAllCollapsed(bool)(新增);
  SyncToDataSet 里 m_collapsed.resize(fileCount, m_groupsCollapsedByDefault)(原来固定 false)——
  只影响之后新出现的分组,已存在的分组保持用户当前选择;ToggleCollapsed 补建状态时同样用它。

二、网格 GX 徽标:悬停不改变鼠标形状
- PlotView::QueryCursor 里 GX 徽标分支由 IDC_HAND 改为 return nullptr(= 保持箭头),
  仍然保持"提前 return"的位置以免被光标线/图例判定抢走优先级;
  只有拖拽中(LANE_REORDER)才是 IDC_SIZENS。

三、标题栏 RAM 口径:总工作集 → 私有工作集(任务管理器"内存"列)
- 症状:加载大文件再关闭分组后,标题栏仍显示几十/上百 MB,任务管理器只有几 MB。
- 根因:原实现取 PROCESS_MEMORY_COUNTERS::WorkingSetSize(总工作集),它把映射进来的 DLL /
  共享页面以及已释放但尚未被系统回收的驻留页面都算进去;任务管理器"内存"列的口径是私有工作集。
- 修法:ProcessWorkingSetBytes() → ProcessPrivateWorkingSetBytes(),用 PROCESS_MEMORY_COUNTERS_EX2
  (MinGW 的 psapi.h 没有该类型 → 在 .cpp 里就地声明,布局与文档一致),失败时依次退回
  PrivateUsage(私有提交)→ WorkingSetSize。
- 实测(离屏探针):加载后 total/priv/commit = 117.55/109.96/138.28 MiB;
  关闭分组后 = 15.44/7.62/8.27 MiB ⇒ 内存没有泄漏、完整归还,用户的差异来自口径。

四、边沿计数(EdgeCountsFor)改为「逐块累积 + 每 tick 预算推进」
- 量化(探针):改前 6M 采样 = 26.8ms / 次阈值变化 → 25.8M 采样 ≈115ms/条;
  横向光标拖动时阈值每帧都变 ⇒ 单条信号每帧 115ms,5 条信号同一网格 = 575ms/帧
  (与用户"开启即卡顿、之后光标几乎拖不动"完全吻合)。
- 新模型:
  * 采样对 k(采样 k-1 → k)按 kEdgeBlockSamples = 8192 分块,每块各存上升/下降沿数;
    scannedPair = 下一个待扫描的采样对([1, scannedPair) 已扫完)。
  * 查询区间:内部整块用块和相加,两端边界块就地精确扫描(≤2 块,代价固定)⇒ 拖动时间光标的
    单帧代价与区间长度无关(单调时间轴下区间不进键);非单调时间轴无法用下标区间表达 ⇒ 区间进键。
  * 每 tick 预算 kEdgeScanPairsPerTick = 1200000(GetTickCount64 分桶,同帧多次调用共享);
    预算用完返回当前部分值,complete 出参告诉界面"还没算完"。
  * 拖动横向光标(EDGE_MOVE)期间不推进(阈值每帧都变,推进纯属白做功)→ 返回"最新的完整条目"
    的结果(数字稳定)+ complete = false。
  * 快路径:单调 && 无镜像 && 无调整变换时用紧凑无分支循环直接比较 values[k] 与阈值
    (NaN 与任何数比较都是 false ⇒ 天然满足"NaN 不参与且打断连续",无需 isfinite 分支)。
- 界面:新增 EdgeCountsText(rising, falling, complete) 统一生成文本(完全未算出显示"统计中…"、
  部分结果追加" …"),EdgePanelWidth 与绘制都改用它,宽度按实际文本测量避免数字被截断。
- 实测(改后):6M 采样首次调用 1.7ms(部分值)、5 帧内扫完且与参考实现逐位一致;
  单次调用最坏 1.9ms;拖动中连查 10 个阈值合计 0.11ms;区间查询在扫描完成后一次调用即精确。
- EdgeCenterCountsFor 未改(它的曲线窗口本来就被 CurveSpanFor 限制,不是本次报告的场景)。

五、内存与性能排查报告(第 4 项按用户要求只排查、未改动)
- 泄漏审计:RemoveFile 路径会 PauseDerivedAsync(清 m_curveEntries/m_curveJobs/m_curvePins)
  → InvalidateDerivedCaches(清折线/边沿/均值/弱峰/场景缓存),DataSet::RemoveFile 用
  "前移 + pop_back + shrink_to_fit" 立即归还采样缓冲;实测三种内存口径全部回落到十几 MB 以下。
- 弱峰调整内存暴涨(峰值 4.4GB):实测工作缓冲 ≈34 B/采样(主导项 = 极值表 PeakExtremum 24 B/极值,
  编码器方波极值密集 ⇒ 接近 24 B/采样),3M 采样单次 ≈103 MiB、4 条工作线程并发 +412 MiB;
  按 25.8M 采样外推 ⇒ 单次 ≈0.9GB、峰值 ≈3.5GB,再加数据本体 1.03GB 与曲线缓存预算 ≤768MB
  ⇒ 4~5GB,与用户观测吻合。
- 现有机制只解决"算完/关掉之后归还"(ReuseBufferScope / TrimDerivedCurveDataIfIdle /
  kHugeSignalPreviewSamples / 曲线缓存字节预算),没有任何"峰值上限"与"优雅降级":
  全仓没有 try/catch,重分配失败抛 std::bad_alloc 而重计算都在 std::thread 里
  (CurveWorkerLoop / RunAsyncLoad),异常逸出线程入口 ⇒ std::terminate() ⇒ 进程终止。
  1GB 剩余内存时:先大量换页(系统卡顿),提交失败则直接崩溃。
- 建议(未实施):①超大信号派生计算串行化 ②极值表字段瘦身/分块流式 ③废弃任务尽早退出
  ④GlobalMemoryStatusEx 自适应降档 ⑤线程入口加 try/catch。

六、验证(全部离屏,不占用鼠标/键盘/屏幕)
- 新增 .preflight/probe(诊断探针):内存口径与泄漏审计、边沿计数扫描耗时、弱峰工作缓冲峰值
  (带 1ms 峰值采样线程)。
- .preflight/uitest 新增 [AR](边沿计数预算/精确性/区间查询/拖动期 O(1)、GX 徽标悬停光标)与
  [AS](标题栏 RAM 口径 = 私有工作集);并给菜单类用例加了关闭残留菜单 + 允许补点的卫生逻辑。
- 合计 738/738 通过(连跑 5 次约 1 次出现既有 harness 抖动:菜单点击类用例,重跑即好);
  make release 零警告(Target.exe 被占用时链接报 Permission denied,用临时 -o 名字验证链接)。

规则同步:Documents/agent_mem.md 新增 §9.22(含内存与性能排查报告),§11.2 新增第 26–31 条硬约束,
变更日志补一行。
2026-10-14 14:12:54 +08:00
DeepSeek V4.1 Flash a2a38236fe 边沿计数改用「绘制值」+ 派生信号烘入时间偏移 + 图例托底统一为 GX 徽标款 + 调整二级面板向上展开
一、两个「边沿计数」按「画出来的曲线」判定(原先按原始采样值 → 应用过「调整」的信号恒为 0)
- 用户复述的权威定义:设定一个横向(Y 轴)光标,信号绘制出来的曲线从下往上穿过该幅值 = 上升沿,
  从上往下穿过 = 下降沿。
- 症状:VERY_BIG_DATA 的「编码器B相」设系数 50 + 时间轴偏移 -20s,把光标拖到画面中间,
  很规整的方波却统计出 0/0。
- 根因:EdgeCountsFor / EdgeCenterCountsFor 只比较 signal.values 的原始采样值,而横向光标读的是
  屏幕上的值(已乘系数、已加偏移)→ 原始值域与阈值完全不相交。
- 修法:判定值一律取 ApplyDrawTransform(style, values[SourceIndexFor(style, count, index)]);
  * 中心轴两侧必须套用同一套变换(drawn - drawnCenter);
  * 水平镜像时曲线数组按源下标存储、而统计区间是显示下标 → 请求曲线窗口前必须把
    [needFirst, needEnd) 映射为 [count-needEnd, count-needFirst);
  * 缓存必须带样式指纹:EdgeCountEntry / EdgeCenterCountEntry 新增 styleKey
    (EdgeStyleKey():gain/offsetY 按 1e6 定点 + absolute/mirrorVertical/mirrorHorizontal 位),
    否则改完「调整」仍会命中变换前的旧计数。

二、时间区间之外的采样不得计入(区间边界点)
- 症状:A 在阈值以下、B 超过阈值;双光标下一个光标正好在 B 点,旧实现把 B 算成一个上升沿。
- 根因:EdgeIndexRange 在左端多取了一个采样点(if (begin > 0) --begin;)。
- 修法:删掉该外扩,收紧为「只有两个端点都落在 [begin, end) 内的采样对才计入」
  (begin = lower_bound(rangeMin)、end = upper_bound(rangeMax),调用方从 begin+1 枚举到 end-1)。
- 结果:把区间起点从 times[crossing-1] 移到 times[crossing] 恰好少一个上升沿。

三、派生信号把 offsetTime 也烘进采样值(推翻 §9.19 的「唯一例外」)
- 用户要求:新信号默认不启用任何「调整」时就必须与网格里的预览逐点重合。
- 修法(Application::BuildDerivedSignal,在「值变换 + 水平镜像反序」之后追加一步):
  新信号第 i 个采样 = 已变换的曲线在 time[i] - offsetTime 处的取值(取最近的采样点);
  单调时间轴用只前进的游标(整段 O(n));平移后越界的采样置 NaN(与预览一致);
  新信号样式全部保持默认(gain=1 / absolute=false / 两个镜像=false / offsetY=0 / offsetTime=0)。
- 退化路径:时间轴非单调或时间列缺失时无法按下标推进取样,退回把 offsetTime 带在样式上
  (两条路径的显示效果相同)。

四、图例托底统一为 GX 编号徽标同款 + 新增更小字体 uiTiny
- 托底高度 = kLaneBadgeHeight、上下边界与 LaneBadgeRect() 完全一致、圆角 = 高度的一半(胶囊形)、
  配色与描边与徽标相同(选中态 accentSoft/accent)、水平内边距 = 胶囊半径。
- 文本改用新增字体 UiFonts::uiTiny(10 逻辑 px):原 uiSmall(12px) 几乎顶满 17px 高的胶囊。
- LegendItem 拆出 plateWidth(托底宽 = 内容宽 + 2×内边距),条目步进 = plateWidth + kLegendItemGap;
  kLegendPlateRadius 已删除(半径一律由徽标高度推出)。

五、「系数/偏移」「镜像」二级面板改为向上展开
- PopupMenu::Open(..., bool expandUp = false):true 时 (x, y) 是菜单的左下角、菜单向上展开;
  BuildLayout 增加同参数并记录 m_buildExpandUp(RebuildLayout 必须复用,否则数值回写触发的重排
  会让面板跳回向下展开)。
- SignalPanel::OpenGainMenu / OpenMirrorMenu:锚点 = (button.X, button.Y - kAdjustMenuGap),
  即「面板左下角对准按钮左上角、再略微上去一点」(kAdjustMenuGap = 4 逻辑像素)。

六、验证(全部离屏,不占用用户的鼠标 / 键盘 / 屏幕)
- .preflight/uitest 新增 [AR]:
  * 边沿计数 plain / gain=50 / offsetY / mirrorVertical / mirrorHorizontal 逐个与「独立参考实现」
    逐位一致;offsetY 用例给出 A/B:按原始采样值算会得到 7/6 个边沿(正是用户报告的缺陷);
  * 区间边界:起点从 crossing-1 移到 crossing 恰好少一个上升沿;
  * 中心轴:absolute + offsetY 的样式下与参考实现逐位一致(32234 个边沿量级);
  * 派生信号:无偏移时全部 128 万采样逐点等于 transform(滤波);带 +10 个平均采样周期的偏移时,
    抽样点等于 transform(滤波) 在 time[i] - offsetTime(最近采样)处的取值;样式全默认;
  * 图例托底:height == 徽标高度、Y == 徽标 Y、radius == height/2、色块与文本都在托底之内;
  * 调整面板:菜单左边界 == 按钮左边界、菜单底 == 按钮顶 - 4*scale、且不覆盖按钮(两个按钮各测一次);
  * GX 拖拽让位:±0.6H 各让位 1 格(目标 0 / 3),±0.4H 与恰好 ±0.5H 都不让位(严格「超过」)。
- 同步修正 harness 里两处「旧规则」断言(否则必然 FAIL 并误导后来者):
  [AP] 「派生信号继承调整变换」→ 「样式默认 + 采样值按 time[i]-offsetTime 烘焙」;
  [AN] 「图例带无托底(网格线可透出)」→ 「装饰矩形逐块、无一块跨越整条带、未被托底覆盖的列
  仍能看到底层内容」。
- 合计 722/722 通过;make release 零警告(本机 GCC 15.2.0 UCRT;Target.exe 被占用时链接会报
  Permission denied,已用临时 -o 名字验证链接正确性)。

规则同步:Documents/agent_mem.md 新增 §9.21,§9.19 的「offsetTime 仍需继承」标注为已作废,
新增 §11.2 第 21–25 条硬约束,并补变更日志一行。
2026-10-14 11:40:49 +08:00
DeepSeek V4.1 Flash 1b3ab4ac09 装饰层改用带 alpha 的独立图层(消除 GX/图例"直角白边")+ 图例圆角托底 + 预览曲线内存释放
一、GX 编号 / 图例「圆角矩形外多出一圈直角白边」的根因与修复
- 根因:装饰层原本是"从后备缓冲区**抓取一块不透明矩形**"(CaptureDecorCache + BitBlt),
  圆角之外(属于波形)的像素被一起烘进缓存;为了让这块矩形像素能跨视界复用,
  又在圆角矩形外铺了一层**直角**的不透明底色(OpaqueBaseRect)。两者叠加 ⇒ 圆角之外
  显示的是网格底色而不是波形 ⇒ 在密集波形上就是一圈直角白边(Y 轴刻度胶囊同理)。
- 修法:把装饰层改成**带真 alpha 的独立图层**:
  * m_decorDc 改为 32bpp CreateDIBSection(自上而下),像素可直接读写 alpha;
  * 新增 RenderDecorCache():把装饰**单独绘制**进一张全透明的位图(不再从后备缓冲区抓取),
    圆角之外保持全透明;位图覆盖范围为「客户区原点 → 绘图区右下角」,绘制坐标与正常绘制
    完全一致(GDI 文本不认 GDI+ 变换,绝不能做坐标平移);
  * BlitDecorCache() 改用 AlphaBlend(AC_SRC_ALPHA,GDI+ 写入 DIB 段即预乘 alpha)逐块合成
    ⇒ 圆角之外透出的是**当前**波形,既没有白边也不会残留旧波形像素;
  * AlphaBlend 经 GetProcAddress(gdi32!GdiAlphaBlend)(回退 msimg32!AlphaBlend)解析,
    不新增链接依赖(两台协作设备工具链不同,避免改 Makefile);
  * alpha 修补:GDI 文本(DrawTextW/ExtTextOutW)会把写入像素的 alpha 清成 0,
    若不修补文字处会变成"透明洞",因此在装饰矩形内把「A==0 且有颜色」的像素还原为不透明;
  * 删除全部外扩的直角不透明底色:Y 轴胶囊与图例托底都只画圆角形状本身;
  * 补齐「+N」(图例空间不足)与「(无可见信号)」提示的装饰矩形(否则画进缓存却不被贴回)。
- 装饰缓存的每帧开销因此更低:BlitDecorCache 约 0.19ms/帧(原 BitBlt 方案约 0.3ms),
  RenderDecorCache 仅在装饰内容变化时执行(约 2.4ms / 5 网格)。

二、图例样式:与 GX 编号徽标同款圆角托底 + 悬停/选中配色统一
- 每个图例条目 = 一个圆角矩形托底(kLegendPlateRadius=4,cardBg 底 + border 描边),
  内容为色块 + 信号名 —— "近似于一个 GX 标签,只是内容换成了图例信息"。
- 选中态与悬停态同配色(accentSoft 底 + accent 描边 + textStrong 文字),与 GX 徽标悬停一致;
  悬停高亮画在贴图之后,并**在不透明托底上原样重绘色块与文字**(否则内容会被盖住),
  同时保留对应信号的曲线光晕;LegendHit 新增 radius 字段。
- 选中态与左侧信号列表同源(DataSet::Selected()):面板选中 → 图例高亮;图例点选 → 面板行选中。

三、预览曲线内存:thread_local 工作缓冲封顶 + 关闭功能即释放
- 根因:DataModel 里三处 thread_local 工作缓冲(弱峰调整的 current/next/极值表、
  ApplyWeakPeakStage 的位移表、迭代均值滤波的中转表)。整段信号(2580 万采样)计算时
  每条后台线程常驻数百 MB,而后台曲线线程池最多 4 条线程 ⇒ 2GB 以上;thread_local
  只在线程退出时才析构 ⇒ 关掉预览不释放,只有 PauseDerivedAsync()(关闭文件分组会调用)
  join 线程后才归还(用户实测 1.42GB → 4.21GB 且关不掉)。
- 修法:① 保留容量封顶(kReuseBufferMaxElements=4M + TrimReuseBuffer + ReuseBufferScope
  作用域守卫,所有退出路径都会裁剪):超大计算改为一次性分配、用完即释放,内存不再随
  线程数放大;中/小规模仍保留容量以维持"多阶弱峰调整 991ms → 577ms"的收益。
  ② 新增 AnyDerivedPreviewEnabled() / TrimDerivedCurveDataIfIdle(),在
  SetLaneWeakPeak / SetLaneMeanFilter / SetLaneEdgeCenterCount(..., false) 里调用 ——
  关闭最后一个预览功能时立即 PauseDerivedAsync()(join 线程 ⇒ thread_local 析构 ⇒ 归还内存)
  并清空均值/弱峰曲线缓存。
- 实测(.preflight/perf/stylecheck mode 2,VERY_BIG_DATA):
  baseline 1202MB → 弱峰(10 迭代,1 条) 1399MB → 5 条曲线 1792MB → **关闭面板后 1203MB**。
  ⚠️ 计算期间的瞬时峰值仍可达约 2.2GB(整段 O(n) 计算的固有代价),但计算结束立刻归还。

四、验证(全部离屏,不占用鼠标/键盘/屏幕)
- .preflight/perf/verify_main.cpp(离屏 Application harness)**49/49 通过**,新增/强化:
  装饰位图圆角外 alpha≈0、中心 alpha=255(直接读 m_decorBits,证明没有直角白边);
  选中图例=悬停配色(accentSoft 像素 未选中 0 / 选中 1525);平移(宽基底)帧的装饰与整幅重绘
  逐像素一致(只比较装饰不透明像素:worst=1 / 29554px);图例溢出的「+N」提示已登记且可见。
- .preflight/perf/stylecheck_main.cpp:mode 0(装饰重建/贴回耗时)、mode 1(密集波形下导出
  GX+图例角落 PNG 目视)、mode 2(预览曲线内存增长/释放)、mode 4(装饰位图 alpha 形状检查)。
- zoomcheck mode 13:收敛帧 vs 整幅重绘 **逐像素 0 差异**(此前为 3503)。
- make clean && make all 零警告。
2026-10-14 03:25:10 +08:00
DeepSeek V4.1 Flash 7c32eca004 修复自适应/首帧卡顿回归 + 派生信号烘值 + 图例交互与托底重做
一、性能(详见 agent_mem §9.19)
- AutoRangeLaneToView:把 DataSet::Style(ref) 从逐采样循环里提出来(远端新增的「调整」
  功能让它每个采样点解析两次,实测 147ms 劣化到 1200ms),并在「视界覆盖整条信号且
  无任何数值变换」时直接取信号已缓存的 min/max(O(1))。工具栏「Y 轴自适应」在全视图下
  1200ms → 0.02ms,结果与逐点扫描逐位一致;有变换时仍逐点套用与绘制相同的变换。
- 新增 WarmLaneLayersStep():加载后宽基底图层从未分配,用户第一次平移/选区放大时
  Paint 会在那一帧同步重建全部网格图层(首帧 64.3ms)。现在改为在「命中静态场景缓存」
  (视界未变、非拖拽)时用既有预算机制每帧至多补 1 个网格,把整批重建摊到静止帧;
  选区放大首帧 64.3ms → 13.0ms。

二、派生信号
- 「构建为新信号」把源信号的「调整」变换(系数 → 绝对值 → 垂直镜像 → Y 偏移,以及
  水平镜像=数据反序)烘进派生采样值,新信号样式一律保持默认(不再是"继承的指纹");
  仅时间轴偏移 offsetTime 因派生信号与源信号共用同一个文件时间轴仍需继承。

三、交互
- 网格 GX 拖拽让位判据改为「鼠标指针」:指针越过相邻网格的中心水平线即让位并出现吸附
  提示(原先按"被拖网格的中心",要求把整个网格拖过去一格才吸附)。
- GX 徽标悬停高亮:徽标本体在装饰缓存里被冻结,高亮改为贴图之后的叠加层,并新增
  m_hoverBadge 状态驱动重绘/取消;QueryCursor 在徽标上返回 IDC_HAND —— 与左侧信号列表
  里同一个 GX 徽标一致。
- 修复「中键平移过视界之后图例悬停/点击/双击改名全部失效」:整幅重绘路径会清空
  m_legendHits,而装饰缓存命中时 PaintLaneLegend 提前 return 导致永不重建。现在把该函数
  拆成「几何 → 命中/装饰矩形 → 绘制」三段,任何路径都先重建命中矩形。

四、绘制
- 每个信号图例单独加圆角托底(配色/圆角风格与 GX 徽标一致);装饰矩形改为逐块
  (徽标一块 + 每个条目一块 + 每个 Y 轴胶囊一块),不再覆盖整条图例带;每块托底先用
  不透明底色铺满「外接整数像素再外扩 1px」的矩形再画圆角 —— 消除「GX/图例周边旧波形
  像素残留」(逐像素验证:装饰矩形内 0 差异)。
- 图例悬停高亮改为不透明托底并在其上重绘色块与文字(否则不透明托底会把图例内容盖住),
  同时保留对应信号的曲线光晕;Y 轴刻度胶囊补不透明外接底色,消除同类残留。

验证:离屏 Application harness(窗口 SW_HIDE + 直接注入鼠标消息 + 从后备缓冲区截图,
不占用鼠标/键盘/屏幕)40/40 通过;zoomcheck 新增 mode 20-26 量化;make release 零警告。
2026-10-14 01:55:34 +08:00
DeepSeek V4.1 Flash 20b90848fe 深放大中键拖拽卡顿根因修复(GDI+ 抗锯齿"满幅锯齿")+ 横向补条裁剪 + 并行网格图层(B/C)
用户报告:VERY_BIG_DATA.CSV 只显示编码器A相、选区放大到 [10.2719,10.2831]s(≈1.12ms 一大格)
后,中键拖拽视界非常卡顿。要求做 B(拆分宽基底路径定位瓶颈)与 C(解除"不允许多线程"约束
并实现并行网格渲染)。

B —— 宽基底路径耗时拆解(.preflight/perf/dragperf mode 1):
  该视界下 RollWideBuffer(补条重绘)13.08ms,BlitWideBuffer 0.50 / PaintTimeAxis 0.78 /
  PaintLaneDividers 0.04 / PaintLaneDerivedOverlays 0.00 / CaptureDecorCache 0.03ms
  —— 瓶颈全在"补条重绘"。

横向补条裁剪(RollWideBuffer 加速):
  补条只覆盖图层位图一条窄竖带,但折线原本按整条宽窗口(2x 视界)构建/描边整段顶点。
  新增 m_stripRenderActive/m_stripX0/m_stripX1:横向补条时把折线采样区间裁剪到条带对应 x 范围
  (两侧留 max(4, lineWidth+3) 像素余量),并旁路折线缓存(补条只构建片段,写缓存会污染整段复用)。
  裁剪只对"逐点描边"路径生效,且必须在"包络/描边"判定之后(否则 visibleCount 变化会改路径判定)。
  实测 RollWideBuffer 13.08ms -> 1.73ms;拖拽整帧 13~24ms -> ~3ms;补条 vs 整块逐像素 0 差异。

真正的病态根因:GDI+ 抗锯齿描边"满幅锯齿":
  拖拽超出预渲染余量时走整块重画,整宽单网格渲染实测 655ms。逐层定位:BuildPolyline 仅 0.03ms;
  同样 4481 点交给 Graphics::DrawLines —— AA round 694ms / 无 AA 29ms(23 倍)。即编码器方波在
  1~2 采样/像素时形成每像素列一次整幅跳变的锯齿,GDI+ 抗锯齿逐段扫描近竖直满幅边,代价爆炸
  (原实现只在 >2 采样/像素或"梳状"时关 AA,这一区间两个条件都不满足)。
  修法(几何完全不变,只改"何时走包络/何时关 AA"):
   1) 包络带阈值从"每像素 2 个采样(budget)"降到"每像素 1 个采样"(visibleCount > plotWidth);
   2) EmitEnvelopeBand 新增 laneHeight 形参,扩展"梳状(关 AA)"判据:多数列列内跨幅 > 0.5x网格高,
      或相邻列上下包络整幅跳变(> 0.25x网格高)-> 本段关闭抗锯齿。
  实测:整宽单网格渲染 655ms -> 47.7ms;补条 6.9 -> 2.4ms;中键拖拽 200 帧 avg 4.5ms /
  max 5.8ms / 0 帧 >8ms(修复前 13~24ms 且含 700ms 尖峰)。目视核对(mode 9 导出 PNG)正常。

C —— 并行网格图层渲染(用户明确解除多线程限制):
  文档中"图层渲染一律在 UI 线程内同步完成 / 绝不要搬到后台线程"的约束已按用户要求解除,但根因
  (共享可变渲染状态)依然成立,故并行的唯一安全姿势是"每线程持有隔离渲染上下文"。
  实现 PlotView::RebuildLaneLayersParallel:每线程一个隔离 PlotView(m_tab 指向自有 m_workerTab
  副本,m_area/m_plotRect/m_lanes 都是它自己的),各自渲染不同 laneIndex 到不同的 m_layerFront 元素;
  主线程 join 后按真实网格下标重算 LaneLayerKey。
  踩坑:BuildLaneVisuals() 在 m_tab==nullptr 时会清空 m_lanes -> 不能靠置空 m_tab 隔离,必须给它一个
  非空的、自有的 TabConfig(m_workerTab)。
  正确性:并行 vs 顺序逐网格比较 0 差异(5 网格全 0)。
  诚实结论:并行仅 1.03x —— 同机自检证明 4 线程 CPU 真并行(3.98x,hw=20),故瓶颈是 GDI+ 光栅化
  本身的串行化,不是调度。因此并行对光栅化无收益,仍保持"视界停后每帧补 1 个网格"(一次补齐只会把
  一帧从 ~10ms 抬到 ~31ms,更易掉帧);并行只用于本来就要一次画完的 RebuildWideBuffer()。

验证:dragperf mode 0/1/3/5/6/8/9;pancheck 7 场景全 dy=0;zoomcheck mode 0/13/14 无回归
(f1 8~9ms、缩放收敛 3649 差异=预存在残差);make clean && make all 零警告。
文档同步:§9.18 新增;§11.2 第 5、14 条改为"解禁但必须隔离状态"的新表述。
2026-10-13 03:16:47 +08:00
DeepSeek V4.1 Flash 065b948297 交互流畅性三项修复:光标不再跟随视界 / 缩放首帧不再卡顿 / 超大信号拖预览不再拖慢前台
一、光标不再跟随视界(用户要求)
- 删除 ClampCursorsAfterPan() 及其在平移路径的三处调用(中键/左键拖拽平移、
  PanTimeByFraction 方向键、OnMouseWheel(shift=true))。ClampCursorToView() 保留,
  只在真正拖拽光标时使用。
- 现行为:平移/缩放视界时光标的时间位置完全不变(可滑出可视区,读数贴在网格边缘显示)。
- 验证:平移 20 帧把视界移到 [257.6, 386.4]s(光标时间 38.643036 远在视口外)后
  CursorTime 完全不变;Shift+滚轮同样不变。

二、缩放首帧不再卡顿(证实"必要像素量级飙升导致首帧卡住"的猜测)
- 量化:每次缩放变化后首帧 70~80ms(宽跨度)/ 26~40ms(第二帧),第三帧起 0.5ms。
- 根因:跨度变化时 Paint 里 if (!LayersStructurallyReady()) RebuildWideBuffer();
  会同步重建全部 5 个网格图层(70~80ms),随后一帧再做一次整幅"结算"(40ms),
  单次缩放合计约 110ms 卡顿。
- 修法(用户方案:先画了再说,不卡前台):
  1) BlitWideBuffer 对"跨度过期"的图层不再跳过(跳过会让网格在重画完成前空白),
     改为把旧图层覆盖的时间段重新映射到当前视界,用一次 StretchBlt(HALFTONE)
     拉出低分辨率但立即可用的占位画面;
  2) Paint 不再同步整批重建:只有"图层数量与网格数量不符"(首次绘制/增删网格,
     新增 LayersAllocated())才 RebuildWideBuffer();跨度/内容过期一律入队,由
     RenderPendingLaneRebuilds 逐帧补齐(原本遇到跨度不符会跳过该网格,现改为直接
     按新跨度重建);
  3) 只在视界稳定后(viewStopped)才花预算做精细重绘:视界仍在变化的每一帧只贴拉伸
     占位图,避免缩放序列每帧夹带一个网格的整块重建(宽跨度约 20ms)。
- 实测:缩放后首帧 70~80ms → ~10ms;精细重绘分散到后续帧(宽跨度 ~20ms/帧、
  深跨度 ~13ms/帧),收敛后由"结算"整幅帧保证像素精确。
- 保留"结算"帧:实测宽基底路径与从零整幅重绘并非像素同源(图层 2448px 按 1px 列聚合
  vs 整幅 1224px 列,列相位差对稠密脉冲信号放大成 17 万+ 像素差异),必须保留这次
  全质量整幅重绘来收敛。
- 正确性 A/B:缩放收敛后的画面 vs 从零整幅渲染,改动前后同为 3774 像素差异
  (预存在的列相位/装饰残差),非本次引入。

三、超大信号拖预览滑块不再拖慢前台(部分改善)
- 量化:无预览基线 0.38ms/帧;预览 + 参数不变 3.4ms/帧;预览 + 参数每帧变化
  11.8~15ms/帧;而直接调用 PaintLaneDerivedOverlays 仅 0.375ms —— 开销不在绘制,
  而在"参数每帧变化"触发的整条信号(2576 万采样)预览曲线重算调度(预览曲线按整条
  信号计算以保证与「构建为新信号」逐位一致)。
- 缓解:新增 kHugeSignalPreviewSamples = 4000000 —— 信号超过该规模且正在拖拽时,
  不再每帧排队整条信号的重算,沿用旧曲线、松手后再算一次最新的。实测 15.0→13.7ms。
- 未完全解决:参数变化带来的 ~8ms/帧开销尚未完全归因(已排除绘制;"暂停后台线程"的
  对照无效,因为 EnsureCurveWorker() 会在暂停后被重新拉起)。用户提议的"网格内按信号
  并行绘制 + 后台缓冲合成"仍需先把渲染器改成无共享可变状态,留待后续评估。

验证:pancheck(平移/纵向 7 场景)全部 dy=0;zoomcheck mode 0/13/14;
make clean && make all 零警告。文档同步更新 §9.17。
2026-10-13 01:25:39 +08:00
DeepSeek V4.1 Flash de27d9b94f 修复垂直中键拖拽后再平移的网格纵向跳变/绘制错误
症状:对某网格做一次纵向中键拖拽(幅值窗口上下平移)后,不松开中键、只做一点点
水平(或垂直)位移,该网格波形整体纵向错位(最多 15px),看起来像"纵向跳帧/抽搐"。
松开后再按下中键做同样的微小水平移动同样复现,且与缩放等级无关。

根因:RenderLaneLayerRegion 结束时无条件把 layer.anchorYMax 重置为"当前 yMax"。
该赋值只对"整块重画 / 纵向补条(整宽)"成立(那两种情况整块像素都对齐到当前映射),
对"横向补细条(整高)"不成立 —— 横向滚动只改一条竖带,其余像素仍在原锚点上。
纵向拖动以 kLayerRollQuantum = 16px 量化:|vOffset| < 16px 时不滚动、交给贴图偏移,
因此拖动停下时图层会残留一个 <16px 的纵向偏移且 anchorYMax != 当前 yMax。第一次水平
微移(哪怕只有 3px)就把该残余偏移静默清零(vOffset 变成 0),而像素并未上移 →
整个网格的波形相对正确位置纵向错位,错位量恰好等于那个残余量。

修法:RenderLaneLayerRegion 新增形参 double anchorYMax(本次渲染像素所对应的纵向锚点),
内部以它作为幅值映射基准(yMin = anchorYMax - laneSpan)渲染,并写回
layer.anchorYMax = anchorYMax。各调用点:
  * 整块重画 / 纵向补条 / 待重画补齐:传当前 yMax(纵向补条传滚动后的 layer.anchorYMax,
    与已滚动像素同一映射);
  * 横向补细条:传 layer.anchorYMax(保持不变),细条因此与其余像素纵向同源,
    残余偏移继续由贴图偏移承担。
vOffset == 0 时新逻辑与旧逻辑完全等价,故既有"仅水平/仅纵向平移"的像素结果不变。

验证:新增离屏探针 .preflight/perf/pancheck(窗口 SW_HIDE + 直接注入消息 + 读图层内部
anchorYMax/当前 yMax/vOffset + 把持按钮帧与释放后的整幅全质量帧做纵向互相关)。
修复前 A 场景 H step 1 起 vOff 从 -15.00 被清零为 +0.00、lane0 best dy=+15px(差异 39245);
修复后全程保持 -15.00、best dy=0(差异 1220,仅已知 <=0.5px 列相位残差)。
对照/中间网格/长距水平/斜向/松开重按共 7 个场景在缩放 0/8/26 步下全部 dy=0
(中间网格 +1px 为整像素量化残差)。make clean && make all 零警告。
2026-10-12 22:58:27 +08:00