64 Commits
Author SHA1 Message Date
DeepSeek V4.1 Flash 328a3b3b59 fix(ui): 选区截取面板空白真正根因、拖拽指示线层序、新建分组修复、避让动画更顺
一、选区截取面板「条目区与按钮之间的空白」(连续三轮未修好的那个)
根因不是内容区几何,而是**底部固定区高度算了两行**:footerHeight 用
`buttonHeight * 2 + 6 + pad` 预留,而布局把「截取为新信号 / 原始信号截取」
两个按钮并排画在同一行 ⇒ 内容区下边界与按钮上边界之间空出一整行。
改为按一行算(两处:高度算式分支 + 布局几何分支同步修改)。
数值核对(scale=1.25、1 条可见信号):GAP 53.8px → 8.8px(= 一个 pad 的预期留白)。

二、拖拽插入指示线不再盖住被拖条目
PaintSignalDragGhost 的绘制顺序改为「插入指示线 → 阴影 → 幽灵行 → 描边」,
指示线始终位于跟随鼠标的那部分之下。

三、「移动到…」二级菜单
* 条目文案改为「组号 | 组名」(不再是括号形式)。
* 修复「新建分组」点了没反应:DataSet::AddGroup 拒收「无信号且无时间轴」的空分组,
  旧实现「先建空组再搬信号」必然在建组那一步失败。新增
  DataSet::MoveSignalToNewGroup 把建组 + 搬运时间轴 + 搬运信号做成原子操作(失败回滚)。
* TabConfig::RemapAfterSignalMove 在目标分组是新建时自行扩展 m_visible
  (调用方不再先 SyncToDataSet —— 那次同步会按旧下标清理引用)。
* Application::MoveSignalCore 增加 newGroupName 参数并返回 bool,避免失败仍报"已移动"。

四、信号拖拽避让动画更流畅
* 插值系数按真实帧间隔计算:1 - exp(-dt/22ms),dt 夹在 1~60ms,帧间隔抖动时不再忽快忽慢。
* 鼠标移动事件里也推进一次插值,快速拖动时让位效果跟得上指针。

验证:删 .o 全量重编译零 error / 零 warning,产物 875,008 B(< 1MB 预警阈值)。
本轮未占用鼠标/键盘/屏幕,未运行任何 GUI 探针;面板空白改为纯数值算式核对。
规则已同步到 Documents/agent_mem.md(含"改面板几何必须数值验证"的教训条目)。
2026-11-03 20:09:01 +08:00
DeepSeek V4.1 Flash 97b361dedd fix(ui): 采样统计显示一致化、GX 拖拽在布局边界下可用、信号「移动到…」与拖拽排序
一、采样间隔/频率显示不一致(用户报告 1ms vs 1000us、100us 0ns)
* FormatDurationCompound 改为「先量化到整数皮秒、再整数分解」,
  彻底消除同一数值因浮点噪声落到不同单位/留下零尾数的问题;
  秒级分支与亚秒分支共用 totalPs 方案,次级进位/单位升级集中在一处。
* FormatFrequency 的单位选择改为按「舍入后的显示值」判定(999.9999999Hz
  与 1000Hz 均为 1.000kHz),并做向下校正避免 0.001kHz。

二、选区截取面板
* 提示文本改为用户指定原文「启用双光标模式以进行选区截取」。
* EdgePanelWidth 计入该提示的实际文本宽度;绘制改用 UiTextClipped 不再省略。
* viewHeight 补回漏减的 bottomPad,修掉条目区与按钮之间的空白。

三、未展开的信号条目栏悬停不再变成手型光标。

四、开启「显示布局边界」后 GX 标签可正常拖拽排序
* 抽出 PaintLaneReorderOverlay(吸附槽位 + 被拖轮廓),贴图与整幅重绘共用。
* 整幅重绘在 LANE_REORDER 时按 LaneDragOffset 逐格平移重画(被拖网格最后画),
  修掉调试模式下不跟随鼠标、无吸附提示、只能盲拖的问题。

五、信号右键新增「移动到…」二级菜单(位于「仅显示该信号」之上)
* 条目为「分组 N(组名)」,当前组打勾;无标题行;底部「新建分组」。
* MenuItem::submenuMaxRows + PopupMenu 独立子菜单滚动(滚动条、滚轮、视口命中过滤),
  IsScrollable 计入子菜单可滚状态。
* 数据层 DataSet::MoveSignalToGroup/MoveSignal 让时间轴随信号搬运
  (独占则移动、共享则复制);TabConfig::RemapAfterSignalMove 改写引用而非先删后插。
* Application::MoveSignalCore 统一入口,保证派生计算暂停、引用修正、选中迁移与同步一致。

六、信号列表支持按住拖拽排序(组内重排 / 跨组移动)
* 4px 阈值进入拖拽;条目脱离原位跟随鼠标(阴影 + 强调色描边 + 插入指示线)。
* 其它信号行/分组行按插入位置做避让位移动画(RowRect 统一叠加插值位移,
  宿主 kScrollTimerId 驱动 AdvanceSignalDragAnimation)。
* 数据层 DataSet::MoveSignal 支持任意位置;TabConfig::RemapAfterSignalReorder 线性重映射。
* 拖拽中禁用组名悬浮;双击改名与 InvalidateRows 均正确终止拖拽态。

验证:删 .o 全量重编译零 error / 零 warning;产物 873,984 B(< 1MB 预警阈值)。
本轮未使用鼠标/键盘/屏幕,未运行任何 GUI 探针,仅编译验证 + 源码级核对。
2026-11-03 19:27:43 +08:00
DeepSeek V4.1 Flash 62220477c0 feat(clip): 新增「选区截取」实验性功能 + 筛选态组复选框只作用于筛选结果 + 重采样统计不被省略
一、重采样面板统计信息不再被省略为 `…`
   - 根因:EdgePanelWidth 里数值列按"典型样例"(1h 1min 2s(1.23456 GHz))估宽,
     真实文本(16ms 182.614us(61.795 Hz))更长 => 绘制矩形不够宽 => 尾部被裁。
   - 新增 FormatIntervalWithFrequency() 作为「间隔(频率)」文本的唯一实现,
     宽度计算与绘制共用它;EdgePanelWidth 改为逐信号实测真实文本宽度取最大值。

二、筛选状态下组复选框只作用于「筛选出来的信号」
   - TabConfig 新增 SetGroupVisibleSubset / GroupAllVisibleSubset / GroupHasVisibleSubset,
     只读写掩码内元素,未通过筛选的信号可见性保持不变。
   - SignalPanel::BuildFilterMask 提供逐信号掩码(未筛选时返回 false,走整组语义)。
   - 组复选框的点击、绘制、行位图缓存键三处统一改用子集判定
     (只改绘制不改缓存键会导致勾选后复选框不刷新)。

三、新增实验性功能「选区截取」(EdgePanelKind::CLIP = 6,PLOT_CMD_LANE_CLIP = 14)
   - 菜单位置:实验性功能二级菜单,「重采样」之下、「自由标记」上方,带勾选。
   - 面板:逐信号一行(信号色叠底条目栏 + 右侧复选框,不可展开),内容可滚动;
     底部两个不滚动的按钮「截取为新信号」与「原始信号截取」;
     未启用双光标时两个按钮为空矩形并给出提示。
   - 截取为新信号:区间内原始数据(不烘焙)+ 继承源信号整套调整,
     名称为 <源信号名> (AAAA - BBBB)(显示时刻、保留三位小数、带 s),插入源信号下方,
     使用 AppendGroupAxis 新建的独立时间轴。
   - 原始信号截取:DataSet::ClipSignalRange 就地裁剪时间轴与采样,
     用新建+swap+shrink_to_fit 真正归还内存,并重算统计与轴的 minTime/maxTime/monotonic。
     调用前先 PauseDerivedAsync() 避免后台派生读到已释放采样。
   - 时间轴偏移参与区间比较:双光标是显示时刻(time+offsetTime),
     BuildClippedSignal 比较时加偏移,ClipSignalInPlace 换算回采样时刻(low-offsetTime)。
   - 同步点已全部补齐:LaneCarry(三处)/SplitLane/PruneLaneSignalItems/RemapAfterSignalInsert/
     purgeRefs/PanelSlotKey/CollectDebugBoxes/SceneContentHash/kEdgePanelKinds/kOrder/
     EdgePanelLayoutFor/EDGE_PANEL_MOVE/m_pendingPanelClose/MEAN_SCROLL/滚轮/悬停/
     SetLanePanelCollapsed/LanePanelCollapsed/QueryCursor。

make release 零 warning,产物 860,160 B。
2026-11-03 16:04:48 +08:00
DeepSeek V4.1 Flash 9fdbf49841 fix(resample,formula,clone): 重采样面板拖动/关闭失效 + 统计频率与输入框不一致 + 派生烘焙接入公式映射 + 克隆拆分
一、「重采样」浮动面板拖不动 / ✕ 关不掉(两个独立缺陷)
1. 拖不动:OnLButtonDown 的 resampleMode 分支处理完条目与按钮后无条件 return true,
   而「标题栏 -> EDGE_PANEL_MOVE」的判定在面板分支最末尾 => 标题栏按下被吞掉。
   改为只消费 insideContent 之内的点击。
2. 拖不动(第二处):OnMouseMove 的 EDGE_PANEL_MOVE -> switch (m_edgePanelDragKind)
   漏了 case RESAMPLE => 位置写进 edgePanel、又从 resamplePanel 读 => 纹丝不动。
3. ✕ 关不掉:OnLButtonUp 的 m_pendingPanelClose -> switch (kind) 漏了 RESAMPLE
   => 落进 default 关掉的是「边沿计数」。补 SetLaneResample(lane,false)。

二、统计「最小数据间隔」与「重采样频率」输入框不一致(用户报告)
   实例 MF4-Record.MF4 / m_MOT_Current_DCom[0]:最小间隔 0.003696631s
   - 统计行先舍入到 3 位有效数字(3.70ms)再换算 => 270.27 Hz;
   - 输入框用未舍入原值换算 => 270.517 Hz。
   真值 270.5166 Hz,两条路径基准不同。修法:频率一律由 RoundIntervalForDisplay
   之后的同一份显示值换算 => 输入框与统计行同为 270.27 Hz、间隔文本同为 3ms 700us。
   未改动面板行数与任何 UI 布局。

三、派生烘焙接入「公式映射」
   BuildDerivedSignal 与 CloneSignal 各写了一份
   gain!=1 || absolute || mirrorVertical || offsetY!=0,漏掉 HasFormula()
   => 带公式的信号派生/克隆后公式被静默丢弃。改为含 HasFormula() 的统一判定。
   BuildResampledSignal 本就走 ApplyDrawTransform 全程烘值,天然包含公式。

四、信号右键菜单:克隆拆成两个语义(均不烘焙采样值)
   - 「克隆该信号」:原始数据 + 外观 + 仅保留时间轴偏移,其余调整复位;
   - 「克隆该信号(带调整)」:原始数据 + 外观 + 整套调整参数。
   新增 Application::CloneSignalAdjusted、CMD_SIGNAL_CLONE_ADJUSTED=6、
   kMessageCloneSignalAdjusted=WM_APP+6。

五、教训:离屏探针崩溃会经 MemProbe::OnTerminate 弹模态「内存不足」框,
   反复重跑即反复弹窗(用户看到十几次报警,真实进程并无内存问题)。
   探针应避免 Initialize+Render,或只做数据层验证;必须无缓冲输出阶段标记。

验证:.preflight/r26 三个探针(真实 MF4 数据一致性 SAME;逻辑 7/7;端到端 12/12);
      make release 零 warning。
2026-11-03 11:09:25 +08:00
DeepSeek-V4.1-Flash 74456b9305 fix(formula,resample): 公式映射卡顿根治 + 语法/输入框完善 + 重采样面板三处修正
一、「公式映射」卡顿(用户报告:只是点进输入框就卡,编辑过公式后彻底没法用)
1. 真因:菜单内联编辑提交时无条件回写初值 ⇒ 点进输入框再点别处就把
   formula = "f(t)" 写进样式;而 HasFormula() 只判 !empty(),于是
   DrawTransformOrderPreserving() 变 false、ComputeLaneRange() 每帧逐点扫描
   整条信号(2580 万采样 ≈ 960ms/帧)。
   修法:新增 FormulaIsIdentity()(空串与 f(t) 等价);提交时归一化后恒等即清空;
   BuildLaneVisuals() 为「网格幅值范围」加指纹缓存(数据指针/长度/统计值 +
   影响数值的样式字段),键不变直接复用(实测 0.0005ms)。
2. 真实公式仍然贵:新增 PlotView::AcquireFormulaEnvelope()(变换后的逐分组
   包络,65536 组,按需算一次并缓存),BuildPolyline 的包络带改用它。
   实测:静止 215→21ms、中键拖动 8ms、缩放最坏 843→81ms;极值仍逐点统计
   (不会漏尖峰)。
3. AcquireProgram 只比 data() 指针 + 长度 ⇒ std::wstring 短串走 SSO 时,
   等长改公式会一直复用旧程序(f(t)^2→f(t)^3 波形不更新)。改为按内容判定。

二、公式语法(Utilities/FormulaEval)
- 新增 ^ 幂(右结合,2^3^2=512、2^-1=0.5、-2^2=-4),支持 2^f(t) 与 f(t)^2;
- 常量 e / pi(大小写均接受,保存前统一小写);
- 数据点只允许 X / x / f(t):单独的 f 不再接受(且不归一化,直接报错);
- 数字扫描修正:2e 里的 e 不再被当成指数吞掉。

三、「公式映射」输入框(SignalPanelMenu)
- Y= 画在输入框内部(非编辑态),进入编辑态只显示公式本身;
- label 留空 ⇒ 输入框占满整行,与下方「系数调整」同宽。

四、「重采样」面板
- 修复点进间隔/频率输入框后输入框整块消失(paintOverlays 的 PaintEditField
  名单漏了 RESAMPLE_INTERVAL / RESAMPLE_FREQUENCY);
- 最大/最小数据间隔:3 位有效数字 + Δt 复合单位,末尾追加(频率)尾缀;
- 间隔显示自适应单位、编辑态用秒;频率显示自适应单位、编辑态用 Hz(均不带单位);
- 面板宽度按含(频率)尾缀的文本量宽。

五、十进制格式化(Utf8Util)
- 新增 FormatDecimal(绝不产生科学计数法);FormatDurationCompound /
  FormatDurationSeconds / FormatFrequency 改用它,Δt/间隔/频率读数不再出现 1e-05。

验证:离屏探针 .preflight/perf/probe/formulacheck_main.cpp 75/75;
make release / make debug 零 warning;Release 856,064 B。
2026-11-03 02:55:22 +08:00
DeepSeek V4.1 Flash 955b233e63 feat(style): 「系数/偏移」改为「映射/偏移」并新增「公式映射」
- 按钮文案改为「映射/偏移」;激活态 = 公式非空 或 系数≠1 或 任一偏移≠0
- 二级面板顶部新增「公式映射」(标题栏带还原图标):Y = + 表达式输入框,默认 f(t)
- 结算顺序固定:公式映射 -> 系数 -> 绝对值 -> 垂直镜像 -> Y 偏移 -> 时间轴偏移
- 表达式规则:支持 + - * / ( ) 一元正负、常量 pi、自变量 X/x/f/f(t);保存前把 X 归一化为 f(t)
- 非法表达式不写入样式,警告走 SignalPanel::TakePendingHint -> PlotView::ShowPersistentHint(自绘浮层,不用 MessageBox)
- 公式参与折线缓存键 / SceneContentHash / SceneStyleEntry / HaloLayerCache;
  DrawTransformOrderPreserving 返回 false(非保序),自适应幅值在 HasFormula() 时逐点扫描
- 派生信号与克隆的烘焙自动包含公式(时间轴偏移除外)
- Documents/agent_mem.md 追加「第二十四轮」硬约束
2026-11-02 20:42:58 +08:00
DeepSeek V4.1 Flash 1e1c2432ae feat(resample): 实验性功能新增「重采样」(位于「自由标记」上方)
- Lane::ResampleItem + resample/resampleItems/resamplePanel/resampleScroll,
  并在 LaneCarry / SplitLane / PruneLaneSignalItems / RemapAfterSignalInsert|Removal 同步
- 面板:可滚动分组列表(信号色条目栏 + 复选框 + 展开三角),展开后 4 行:
  最大/最小数据间隔(只读,勾选时一次性缓存,绘制不扫描采样)+ 重采样间隔/频率(输入)
- 输入框联动:改间隔同步频率,改频率同步间隔(输入单位分别为秒 / Hz)
- 「构建为新信号」:DataSet::AppendGroupAxis 追加新时间轴 + InsertDerivedSignal,
  采样值先按公式/系数/绝对值/镜像/Y偏移变换再等间隔线性插值,时间轴偏移不烘值
- 新信号名 <原信号> (重采样 <自适应单位频率>);Utf8Util::FormatFrequency 为唯一实现
- 非单调轴 / 点太少 / 间隔过大 / 点数超 2000 万 ⇒ 拒绝构建并给出自绘提示
- 顺带加入 FormulaEval(公式编译/求值,供 ApplyDrawTransform 与后续 UI 使用)
2026-11-02 20:41:26 +08:00
DeepSeek V4.1 Flash 8de8a8fb85 预览曲线按面板高亮 + 统计数字不再卡在“统计中…” + 中心轴面板改表格 + 默认主题 Light + 标签页 DPI 修复 + 布局边界幽灵元素
一、预览曲线悬停高亮(只开弱峰+中心轴时无反应;三个全开时总亮均值曲线)
   根因:PaintLaneDerivedOverlays 里只有 MEAN 分支传 highlight,且悬停状态没有记录面板种类。
   修法:新增 m_meanRowHoverKind,高亮条件 = 悬停行 == 本行 && 面板种类 == 本曲线来源;
         条目栏强调色同样按种类门控;悬停种类进入面板缓存键。

二、统计数字松手后停在“统计中…”
   根因:交互期间面板缓存键被冻结(为拖动跟手),冻结期满后没有重绘请求(鼠标已停、
         统计也已完成),最后一次绘制仍是冻结帧。
   修法:记录 m_panelKeysFrozen 并公开 PanelRefreshPending();Render 末尾把它并入
         滚动动画定时器的武装条件 ⇒ 空闲后自动补一帧真实数字,随后停表。

三、「中心轴边沿计数」面板改为表格
   表头固定:信号名 / 上升沿计数 / 下降沿计数;每行底色 = 该信号配色(未勾选的行普通底色),
   三列与表头严格对齐,内容可滚动,单击行展开两行参数滑块。
   - EdgePanelLayout 新增 tableHeader / tableColumns[3];EdgePanelLayoutFor 新增 centerTable 分支;
   - 新增 EdgeCenterTableHeader() / EdgeCountCellText();EnsureEdgeCenterItem 默认 collapsed;
   - PanelSlotKey 的 CENTER 分支改为按 item 判断并混合计数(表格已没有 rowStats)。

四、默认主题改为 Light(Application、ThemeFor/ThemeName 默认分支、CurrentTheme 兜底)。

五、标签页文本在 DPI 切换后被省略
   根因:标签宽度恰好等于“内边距+文本+间距+关闭按钮”,无任何余量;TabItemWidget 缓存了
         Configure 时的 scale 并用它反推可用文本宽度,而 TabBar::SetScale 不重建控件树
         ⇒ 125%→100% 后可用宽度少约 4.5px ⇒ 吃掉末字。
   修法:SetScale 变化时重建控件树;并把文本可用矩形改由 ArrangeWidgets 统一下发
         (SetState 增加 textRect 参数),绘制侧不再自行反推。

六、布局边界里的幽灵元素
   - 新增 SignalPanel::HasStyleTarget();BuildBoxes 与 QueryCursor 在无样式目标时
     不再上报/响应色块、样式、调整、线宽、透明度等控件;
   - Paint 的“无网格”分支显式清空 m_legendHits / m_cursorLabelRects / m_cursorSpanRect /
     m_freeMarkerHits / m_panelSlots / m_panelOrder(关闭整个分组后不再残留边界框);
   - 浮动面板之上的滚动条 / 回到最上方 / 内联输入框改为按可见性上报几何。

验证(离屏探针,SW_HIDE + 合成消息,不占用鼠标键盘屏幕)
  .preflight/p21.exe 9/9   .preflight/p22.exe 14/14   .preflight/p23.exe 7/7
  .preflight/p20 系列(9/11/5)全部回归通过
  make release / make debug 零 warning;Release 813,568 B
2026-11-02 15:22:26 +08:00
DeepSeek V4.1 Flash 7a6fcd68e2 面板限高与滚动改造 + 边沿统计不再卡在“统计中…” + 组名折起真正收起
一、浮动面板(用户报告:下方空白、滚动时画到面板外、未改造的面板让一切交互卡顿)
1. ⭐ 根因:旧「逐条目」排布没有高度上限 —— 288 条信号时面板 13010px(绘图区 846px)
   ⇒ PanelRectInsideLayer 恒假 ⇒ 面板像素缓存一次都存不进去 ⇒ 每帧整块重画。
   现在 5 个面板全部限高 + 可滚动,旧的 itemized 布局/绘制/交互整体删除。
2. 无按钮面板(中心轴边沿计数)不再预留「按钮行 + pad」⇒ 底部空白 55px → 8.8px(1 个 pad)。
3. ⚠️ 统计文字改用 UiTextClipped:GDI 文本不受 GDI+ SetClip 约束,滚动时画到标题栏之外。
4. 「弱峰调整」改造为与均值滤波同款的分组列表(条目栏 + 右侧复选框 + 展开后的两行滑块
   + 复位按钮 + 底部「构建为新信号」);新增 Lane::weakPeakScroll / SetLaneWeakPeakScroll,
   WeakPeakItem 增加 collapsed。
5. 「边沿计数」保留两行排版,但内容区同样限高、可滚动(新增 Lane::edgeCountScroll)。
6. 鼠标指针:列表式面板的条目栏**只有已勾选**才显示手型(未勾选点击无效果,保持箭头);
   参数行控件只在参数行展开时给手型 / 左右箭头。
7. 内部标志 m_meanDragCenter / m_editMeanCenter(bool)改为 m_meanDragKind / m_editMeanKind
   (EdgePanelKind),以便 MEAN / CENTER / PEAK / COUNT 共用同一套滚动与滑块拖拽。

二、边沿统计“全部统计中…且永不更新”(288 信号合并到同一网格)
1. ⭐ 根因:累积状态上限固定 64 < 信号数 288 ⇒ 新建状态总淘汰未完成的 ⇒ 没有任何状态能扫完。
   新增 EdgeScanStateLimit() = max(64, 全部信号数 × 2 + 16)。
2. 去掉每状态 60000 的固定配额下限(状态一多就把整帧预算耗光),改为 budget / 未完成状态数;
   分母只数未完成状态,短信号扫完后立刻把预算让给长信号。

三、组名折起后“三角已收起但信号行仍在、仍可点选”
1. ⭐ 根因:RequestEdgeScanFrame() 在每个未完成状态里各 Post 一次(单帧最多 288 次),
   把优先级最低的 WM_TIMER 饿死 ⇒ AdvanceGroupAnimations 永不执行 ⇒ 折起动画永不结束。
   修法:每帧最多 Post 一次(m_edgeScanRequestFrame)。
2. 另一道保险:SignalPanel::EnsureRows() 开头推进一次动画 —— 动画进度只依赖挂钟时间,
   不依赖消息能否按时到达。

四、验证(离屏探针 .preflight/p20,全部 SW_HIDE + 合成消息,不占用鼠标/键盘/屏幕)
  p20.exe  9/9:面板限高且像素缓存真的写进去了、288 个统计全部 complete、中心轴面板底部留白、
               组名折起后行集合无该组信号、两组都折起后 signalRows == 0(真实消息 + WM_TIMER)。
  p20c.exe 11/11:弱峰面板限高/可滚动/缓存命中、展开与收起、滚动生效、条目栏手型规则、
               中心轴滚动时标题栏 0 像素变化(A/B 关掉裁剪 ⇒ 805px)、288 信号空闲帧 2.6ms。
  make release / make debug 零 warning。
2026-11-02 10:00:24 +08:00
DeepSeek-V4.1-Flash 944c074ab3 288 信号卡顿根治 + 中心轴面板重做 + 标签页默认状态
一、卡顿真因(实测 29.8ms/帧 → 6.6ms/帧)
1. 中心轴面板高度没有上限:288 个信号时面板高 9996px(绘图区只有 1141px)
   ⇒ PanelRectInsideLayer 恒为假 ⇒ 面板像素缓存一次都存不进去 ⇒ 每帧全量重画。
2. PanelGlobalKey 混入鼠标驱动状态(m_hoverSignal / m_hoverLane / m_hoverCursor /
   CursorTime / EdgeRangeMin/Max)⇒ 鼠标一动键就变 ⇒ 所有面板每帧重画;
   PanelSlotKey 里还会为每个信号调一次 EdgeCenterCountsFor(单帧 288 次查询)。
3. 缓存命中路径的投影(与面板同大的半透明抗锯齿圆角矩形)每帧重画,GDI+ 实测 > 20ms。

修法:
* 新增 PlotView::InputIsInteractive()(拖拽中 / 鼠标 140ms 内移动过)。交互期间冻结
  全部鼠标驱动项与统计数字、跳过投影、统计预算降到 8 万(静止时 40 万)。
  m_meanRowHoverLane/Ref 例外 —— 它直接改面板像素且只在进出某行时变化。
* kEdgeScanPairsPerFrame 由 1,200,000 降为 400,000。
* 中心轴面板重做为可滚动列表 ⇒ 面板高度被绘图区夹住、缓存可用、只画可见行。

二、「中心轴边沿计数」面板重做(与「均值滤波」同风格)
条目栏为信号色叠底的可折叠栏 + 右端复选框;栏下同一行显示「上升沿 / 下降沿」;
再下面两行滑块(均值点数 / 迭代次数,标签不带 X/N),数值可点击输入上限、
右侧复位按钮;整个面板可滚动;未勾选时条目栏不可展开。
新增 Lane::edgeCenterScroll + SetLaneEdgeCenterScroll;交互复用 MEAN_* 拖拽模式 +
m_meanDragCenter / m_editMeanCenter 标志分派;光标形状与均值滤波一致。

三、标签页
* 唯一标签页点 ✕ 不再弹警告:直接 erase 后 NewTab(true),名字必然为「视图 1」。
* 新建标签页(含手动新建)默认:组全部折起、无选中信号(清空全局 Selected)、
  样式区无目标。

四、验证
.preflight/bugcheck.exe 59/59(A1 空闲 0.7ms、A2 平移 2.5ms、A3 双光标 2.4ms、
A4 自由标记 5.2ms、A5 中心轴拖动 6.6ms;B3 真实 WM_TIMER+WM_PAINT 路径;
B5 快速连点与动画反向;C 中心轴面板交互;D 标签页默认状态)
panelcheck 71/71、markercheck 13/13、ringcheck 6/6
make release / make debug 零 warning
2026-11-02 03:33:28 +08:00
DeepSeek V4.1 Flash 8271879ede 面板动画帧不再重绘绘图区(组名展开/收起卡顿根治)+ 均值面板两处修正 + 信号行集合自愈
1. 组名展开/收起卡顿的真因(用户报告:288 个信号同一网格、双光标/实验性功能开启时,
   点组名展开收起的动画非常卡顿,而网格里没有任何需要重绘的变化)
   - 滚动定时器给面板动画帧做的是整窗失效,WM_PAINT 里"更新区域不在绘图区内 ⇒
     m_chromeDirty = true"这条启发式把整帧判成脏 ⇒ Render 走 fullChrome 分支 ⇒
     标签栏/工具栏/绘图区全部重绘。绘图区走场景缓存很快,但**光标叠加层每帧都要重画**:
     288 个信号的取值胶囊(圆角 + GDI 文本 + 重叠避让)实测 24~46ms/帧。
   - 新增 Application::m_panelAnimationFrame:滚动定时器在只有面板动画时置位,
     WM_PAINT 命中它且没有任何其它消息要求装饰区重绘时不再把整帧判脏 ⇒ 走 panelOnly。
   - 新增 Application::m_plotPaintValid:panelOnly 帧且该标志为真时跳过 PlotView::Paint
     (面板不覆盖绘图区,上一帧像素原样留在后备缓冲区,整窗回送也不会错)。
     作废点:InvalidatePlotRect(签名多收一个 bool&,5 个调用点统一传入)、Relayout、
     EnsureBackBuffer、WM_PAINT 中"更新区域就在绘图区内"、以及"面板动画帧里绘图区的
     滚动/回到最上方动画同时也在跑"(否则会把绘图区动画冻住)。
   - panelOnly 分支画分隔条时把右边界夹到 floor(m_plotRect.X):分隔条是浮点矩形
     (457.5…462.5),GDI+ 会把最后一列按 50% 混进去;整帧重绘时绘图区随后会盖掉它,
     但跳过绘图区的帧会留下**一整列 1px 接缝**(实测 847 像素)。
   - 实测(283 信号同网格 + 双光标):动画帧 34~43ms → 1.6~2.1ms。

2. 均值滤波面板
   - **未勾选的条目栏点击完全无响应**:未勾选时点名称区域不再自动勾选、也不展开/收起,
     只能点右端复选框(点击仍被消费,不穿透)。
   - **滚动时参数滑块不再消失**:原实现按条目栏矩形判断整行可见性,条目栏滚出内容区后
     整行(含两行参数滑块)被跳过。现在按整行纵向范围(条目栏 ∪ 两行轨道/数值/复位)判定。

3. 信号列表:行集合与折叠状态自愈(用户报告:开自由标记并标记点后,点 MF4 组名,
   三角显示已展开但下属 283 条信号行一条都不出现)
   - 新增 SignalPanel::RowsSignature()(FNV 混合 分组集合/每组信号数/是否折起/是否有动画),
     BuildRows 记录它,EnsureRows 每帧 O(分组数) 校验,不一致就地重建。
     这样任何"改了折叠状态却忘记置 m_rowsDirty"的路径都不会再让行集合与折叠状态脱节。

4. 验证(全部离屏,不占鼠标/键盘/屏幕)
   uicheck 69 / lbcheck 21 / lbresid 5 / lbzorder 4 / lbcovr 7 / lbpanel 10 /
   derivecheck 16 / round15check 28 / themecheck 17 / meancheck 21 全绿;
   新增 .preflight/panelbug 14/14(外部改折叠状态→行集合自愈、动画中途改状态→位移清零、
   未勾选行点击无响应、条目栏滚出后滑块仍绘制);
   新增 .preflight/panelperf 12/12(动画帧 <16ms、且跳过绘图区后该区域像素与紧接的
   整幅重绘逐像素完全相同);
   rendercheck A–H 8 个场景哈希与基线一致;make release / make debug 零 warning
   (805,888 B)。

同步文档:Documents/agent_mem.md 追加「2026-10-30 第十八轮」,
并把第十七轮里"尚未启用 → 直接开始预览"改标注为已推翻。
2026-10-30 15:06:01 +08:00
DeepSeek-V4.1-Flash bdd703b88b 主题自适应配色 + 预览曲线连续配色 + 样式区复位按钮 + 均值滤波面板重做 + 面板缓存环带残留 + 调试层序 + 自由标记选取与性能
① 主题自适应(`Theme.cpp`):新增 `SrgbLuminance` / `ContrastRatio` / `EnsureContrast` /
   `MixToLuminance` / `ThemeLaneBackground`;四套主题的结构色(gridWeak 1.30 / gridStrong 1.65 /
   axisLine 2.10 / borderStrong 1.70)按底色自动校正,Solarised Light 逐位不变;网格叠底改用
   `ThemeLaneBackground()`(4 处),深色主题下网格层与绘图区底色可辨。
② 派生预览曲线(`HighContrastDerived`)重写为**连续**方案:方向沿信号所在侧远离底色,
   目标亮度 = max(与底色对比度 3.2, 信号亮度 ± 0.18),再按亮度平滑混色;调用点传入真实网格底色
   (旧实现默认写死 Solarised Light 底色 ⇒ 深色主题永远选到更暗的那一侧、几乎看不见)。
③ 信号样式区「线宽 / 透明度」两行右侧新增**复位按钮**(`UiPaintResetIcon` 成为唯一实现,
   菜单/弱峰面板/样式区共用同一套笔画);默认线宽统一到 `DefaultSignalLineWidth(finiteCount)`
   (单点信号取 10.0)。按钮右边界与内容列对齐,内容列过窄时整列不显示。
④ 「均值滤波」面板重做:信号色叠底标题栏 + 复选框 + 展开三角 + 「均值点数 / 迭代次数」两行滑块
   (点击数值可改滑块上限、右侧复位)+ 内容区滚动;新增 `MeanFilterItem::{maxPoints,maxIterations,
   collapsed}` 与 `Lane::meanFilterScroll`,缓存键同步补全。
⑤ 浮动面板命中像素缓存时只贴**面板圆角形状之内**的像素(`CreateRoundRectRgn` 裁剪)并按本帧重画投影
   ⇒ 中键移视界后面板周围不再出现一圈旧波形。
⑥ 显示布局边界:每个浮动面板**独占一个绘制层**(按 `m_panelOrder` 递增),绘制侧
   `UiSetTextLayer` 在同一循环内同步推进 ⇒ 下层面板的边框/文字不再画到上层面板上。
⑦ 自由标记:采样候选距离改为**二维欧氏距离**(单点信号不再在整幅宽度内吸住光标),
   纵向光标画在鼠标位置、圆点落在真实采样点(新增 `FreeMarkerProbe::pointX`);
   `MinorCellSparse` 只对胜出信号算一次;折线缓存容量改为 `max(64, 信号数×2)` 且查找改为
   单键哈希 ⇒ MF4 的 285 信号合并网格中 `ProbeFreeMarker` 平均 0.11ms。

验证:`.preflight` 探针重建(`panelcheck` 71/71、`markercheck` 13/13、`ringcheck` 6/6,合计 90 条,
LTO 目标文件上复跑同样全绿);`make release` / `make debug` 零警告;产物 815,104 B。
文档:`agent_mem.md` 第十六轮(含补丁脚本与编码/行尾踩坑)。
2026-10-30 03:10:51 +08:00
DeepSeek V4.1 Flash 71195c4c48 新增主题配色(4 套)+ 信号右键菜单精简与「克隆该信号」+ 布局边界层级根治 + 单点信号吸附修复
## 为什么

用户 2026-10-29 报告/要求五件事:

1. 派生信号的「时间轴偏移保留在样式上、其它调整烘值」维持现状(确认无需改动);
2. 信号名右键菜单里很多功能已被下方样式自定义区覆盖 ⇒ 只保留「仅显示该信号」,
   并在它**上方**新增「克隆该信号」;
3. 设置菜单新增「主题配色」二级菜单(带 √ 状态),顺序 Light / Solarised Light /
   Dark / Solarised Dark,默认主题维持现状;
4. 布局边界:中键平移视界时浮动面板上的边界**闪烁**;光标取值胶囊的**文字**框会**穿透**
   到浮动面板上;并请核对网格内固定构件(图例 / Y 轴标尺 / GX 标签 / 回到最上方 /
   浮动面板)的层级关系;
5. 单数据点信号在拖动光标时的**吸附**功能消失。

## 根因

### 布局边界层级(两个都很隐蔽)

- **文本层号写死**:文本是在绘制过程中由 `UiTextRectSink` 收集的,而宿主把所有文本都记在
  固定层 2;组件几何层却是"组件表下标"(`PlotView` = 0)。面板层 = PlotView 层 + 1 = 1,
  **低于**文本层 2 ⇒ 面板的遮挡矩形挖不到文本 ⇒ 胶囊文字画在面板表面。
- **面板像素缓存**:面板"先画进图层、命中缓存就贴图",贴图那一帧不画字 ⇒ 文本框凭空消失
  ⇒ 面板上的边界闪烁。
- 顺带发现 `PlotView::BuildBoxes` 用 `PushLayer()` 却**从不复位**,会把宿主后续所有组件的
  层号整体推高。

### 单点信号吸附

`MinorCellSparse` 在"把判定范围收窄到数据实际时间跨度"的分支里有
`if (!(dataMax > dataMin)) return false;`。单个有效采样点时 `dataMax == dataMin`,
于是被判定成"不稀疏" ⇒ 不吸附。

## 改了什么

### 1. 布局边界层级

- `UiTextRectSink` 新增 `OnTextLayer(int)` / `TextLayer()`;新增 `UiSetTextLayer` /
  `UiGetTextLayer`;`UiBoxTable` 实现为改 `m_layer`,并暴露 `Layer()`。
- `Application::Render` 在绘制每个组件前下发 `UiSetTextLayer(ComponentLayer(&component))`,
  浮层用 `OverlayLayer(1/2)`;新增 `ComponentLayer()` / `OverlayLayer()` /
  `kLayersPerComponent = 3`,与 `PaintLayoutBounds` 的层号**完全同源**。
- `PaintLayoutBounds` 改为**显式** `SetLayer(下标 × 3)`(不再依赖 `PushLayer` 累加)。
- `PlotView`:`BuildBoxes` 用 `baseLayer + 1` 放浮动面板(并登记遮挡)、
  `baseLayer + 2` 放绘制在面板之上的滚动条 / 回到最上方 / 输入框,结束**复位**层号;
  `paintOverlays` 同步分两次抬高文本层。
- 面板像素图层新增 `PanelSlot::textRects` + 转发型收集器 `PanelTextCollector`:
  渲染时存文本框,命中缓存贴图时回放 ⇒ 几何与缓存命中无关(消除闪烁)。

### 2. 信号右键菜单 + 克隆

- 菜单只剩:标题、**克隆该信号**、**仅显示该信号**;删除显示/隐藏、显示全部/隐藏全部、
  预设色块行、样式三项,以及对应的 `CMD_SIGNAL_TOGGLE/SHOW_ALL/HIDE_ALL`、
  `CMD_COLOR_BASE`、`CMD_MODE_BASE` 及其分支(死代码)。
- 新增 `CMD_SIGNAL_CLONE`(post `WM_APP + 5`)→ `Application::CloneSignal`:
  把「调整」(系数 / 绝对值 / 镜像 / Y 偏移)烘进采样值、**时间轴偏移保留在样式上**,
  名称前置 `*`,插入到源信号**正下方**,并**保留原外观**(颜色/透明度/线宽/绘制样式)。
- 抽出 `Application::InsertSignalBelowSource(...)` 供派生与克隆共用。

### 3. 主题配色

- `ThemeKind{LIGHT, SOLARIZED_LIGHT, DARK, SOLARIZED_DARK}` + `ThemeFor()` / `ThemeName()`;
  Light 取 VS Code 2026 默认浅色、Dark 取 Dark+、Solarized Dark 取 VS Code solarized dark。
  **默认仍是 Solarised Light**。
- 新增 `CurrentTheme()` / `SetCurrentTheme()`:宿主交出自己的 `m_theme` 地址,
  切换主题只改内容;`UiControls` 里没有 `Theme` 形参的原语改用它(原来硬编码
  `SolarizedLight()`,换主题不生效)。
- `Application::ApplyTheme()`:改配色 → `PlotView::OnThemeChanged()`(作废场景/宽基底/
  装饰/光晕/面板图层)+ `SignalPanel::OnThemeChanged()`(作废行位图)→ 整窗重绘。
- 菜单用 `SUBMENU` + 4 个 `keepOpen` 子项;新增 `PopupMenu::UpdateSubmenuItemLabel()`
  回写勾选,并让子菜单分支尊重 `keepOpen`(原为无条件关闭)。

### 4. 单点信号吸附

`MinorCellSparse` 改为只在 `dataMax < dataMin` 时放弃;相等时按"那一个小格"判定
(`need = 5 > finiteCount = 1` ⇒ 稀疏 ⇒ 吸附恢复)。

### 5. 审计

`Makefile` 的 `LAYOUT_BOUNDS` / `JDI_LAYOUT_BOUNDS` **从未被任何 `#if` 引用**
(`LAYOUT_BOUNDS=0` 产物字节数完全相同),属名实不符的死开关 ⇒ 删除并注明原因。

## 怎么验证

新增两支离屏探针(不占鼠标 / 键盘 / 屏幕):

```
.preflight/round15check  28 passed, 0 failed
  单点吸附 7 条(含"远处不吸附")、面板层级 5 条、面板文本框跨帧与中键平移稳定 2 条、
  克隆 14 条(烘值逐点比对 / 名称前缀 / 偏移与外观保留 / 调整复位)
.preflight/themecheck    17 passed, 0 failed
  4 套主题可读性与可区分、默认 = Solarised Light、逐主题换色且无旧底色残留、
  切回默认逐像素还原(diff = 0)、子菜单 4 项 / 顺序 / 唯一勾选 / 命令切换 / 菜单保持打开
```

回归全绿:

```
uicheck 69/69   lbcheck 21/21  lbresid 5/5   lbzorder 4/4
lbcovr   7/7    lbpanel 10/10  derivecheck 16/16
rendercheck: 8 个场景哈希与上一轮逐一相同 + FUNC 7/7, exit=0
  A FAED59DF284153DB  B E6443BFE4E3955F8  C 96A0AA8C402F1729  D E3D5DBD998E43720
  E DA0583CB13C26A6F  F FFB822159F604D1D  G 19F8DE8FE3A8DF18  H DBBDDE4CB2C97C92
```

`make release` / `make debug` 零 error / 零 warning;产物 **791,552 B**
(单一 exe,未拆 DLL,未改 `-Oz`;离 1MB 预警线约 250 KB)。
2026-10-29 20:38:55 +08:00
DeepSeek V4.1 Flash 044b87383b 体积 L3(-fno-exceptions + MemProbe + LTO)与 L1 几何产物层;产物 909,824 -> 788,992 B
## 为什么

用户 2026-10-29 要求:按既定计划完整实现改进,但
① **不得把编译产物拆成 DLL**(项目定位是单文件轻量工具,最终只能有一个 .exe);
② 异常处理改用**预检验**而不是 try/catch(-fno-exceptions 省体积,且 try/catch 本身开销大);
③ 先删除 Documents 内三份 UI 重构文档再按"计划 / 当前进度 / 最终报告"重建;
④ 顺便审计代码,删除无用/过时注释,修复名实不符;
⑤ 提交到本地仓库,期间不要占用用户的鼠标/键盘/屏幕。

## 改了什么

### 1. 体积(909,824 B -> 788,992 B,-120,832 B,-13.3%)

Makefile 新增三个可覆盖开关(默认全为 1):

- `LAYOUT_BOUNDS`  编入/剔除「显示布局边界」叠加层
- `NO_EXCEPTIONS`  追加 `-fno-exceptions`
- `LTO`            追加 `-flto`(编译 + 链接)

派生:`FUNC_SECTIONS`(LTO 下为空)、`GC_SECTIONS`(`-Wl,--gc-sections`)、
`ASM_LISTING`/`ASM_LISTING_C`(LTO 下为空,因为 lto-wrapper 会丢弃 `-Wa` 清单)。

两个实测复现的坑(已写进 Makefile 注释):

- **切换 LTO 必须 `make clean`**:LTO 与非 LTO 目标文件混链会让 GCC 15.2.0 在
  `binds_to_current_def_p (symtab.cc:2598)` 处 ICE。
- **`-flto` 与 `-ffunction-sections -fdata-sections` 同时打开也 ICE**(干净重编也必崩,
  报错落点是 `DataModel.hpp:393`,但那是 COMDAT 解析落点、不是原因)⇒
  LTO 下 `FUNC_SECTIONS` 置空,并加 `-flto-partition=one`:默认 11 个分区的 LTRANS
  稳定复现,单分区从未复现。**这是稳定性选项,不是性能选项。**

### 2. 关异常必须成对交付:预检验 + 兜底

新增 `Utilities/Inc/MemProbe.hpp` + `Utilities/Src/MemProbe.cpp`:

- `MemAvailablePhysical()`:`GlobalMemoryStatusEx` + 200ms 缓存
- `MemCanAllocate(bytes)`:预留后仍须保有 1/8 可用内存
- `MemReserveIfRoom(container, n)`:含 `capacity()` 早退与乘法溢出保护
- `MemInstallTerminateHandler()`:OOM 时弹一次带建议的模态提示后 `_Exit`(而不是静默消失)

接入:

- `Utilities/Src/DataModel.cpp`:全仓**唯一**的 `try/catch`(`extremes.reserve` 捕
  `std::bad_alloc`)改为 `MemReserveIfRoom(extremes, count / 8)`
- `Components/PlotView/Src/PlotView.cpp`:`AvailablePhysicalBytes()` 原有一份**完整重复**
  的 `GlobalMemoryStatusEx` + 缓存实现 → 转发 `MemAvailablePhysical()`
- `Components/Application/Src/Application.cpp`:`Initialize()` 开头安装 terminate handler

### 3. L1 几何产物层

新增 `Components/Shared/Inc/UiBox.hpp` + `Src/UiBox.cpp`:

- `struct UiBox { rect, clip, depth, layer }` —— 纯值类型,不继承任何东西
  (继承会和 PlotView 4 层位图缓存 / SignalPanel 行位图缓存的失效体系打架)
- `class UiBoxTable : public UiTextRectSink` —— 只累计几何 + 裁剪/层/遮挡,**不含绘制代码**

`UiComponent::CollectDebugBoxes` → **`virtual void BuildBoxes(UiBoxTable&)`**:
组件从"收集布局边界(调试用)"改为"上报几何产物",不再需要知道"调试"这个概念存在。
`UiDebugBounds.hpp/.cpp` 退化为唯一的消费者(配色 + 包角描边 + 遮挡挖洞)。

除宿主 `ApplicationCommands.cpp` 外,所有组件源码现在只包含 `UiBox.hpp`。

### 4. 审计

- `MemProbe` 里"省 40~60KB"的估算 → 改为实测 27,136 B
- `MemProbe.hpp` 的 `<new>`、`MemProbe.cpp` 的 `<cstdio>`:未使用,删除
- `UiDebugPaintBounds(g, rect, depth)`:零调用者,删除(只保留带 clip 的版本)
- `TabBar`/`Toolbar`/`PlotView`/`SignalPanel` 的 `BuildBoxes` 文档"(调试用)"→"几何 box"
- `PlotView::SetForceFullRepaint` 补写"为什么保留而不优化"

### 5. 文档

`Documents/UI_Refactor_Plan.md` / `_Progress.md` / `_Report.md` 先 `git rm` 再按
「计划 / 当前进度 / 最终报告」重建;`Documents/agent_mem.md` 追加第十三轮记录。

## 怎么验证

```
make clean
make release TARGET_FILE_NAME=JustDataInspector -j16   # 零 error / 零 warning,788,992 B
make debug   TARGET_FILE_NAME=JustDataInspectorD -j16  # 零 warning

uicheck 69/69   lbcheck 21/21  lbresid 5/5  lbzorder 4/4  lbcovr 7/7  lbpanel 10/10
rendercheck: 8 个场景哈希与改造前逐一相同 + FUNC 7/7 PASS, exit=0
  A FAED59DF284153DB  B E6443BFE4E3955F8  C 96A0AA8C402F1729  D E3D5DBD998E43720
  E DA0583CB13C26A6F  F FFB822159F604D1D  G 19F8DE8FE3A8DF18  H DBBDDE4CB2C97C92
```

体积溯源(`objdump -h`,基线 → 最终):
`.text` 735,600 → 657,168;`.rdata` 49,184 → 35,016;
`.xdata` 48,132 → 27,512;`.pdata` 35,424 → 28,524;`.rsrc` 20,656 不变。

## 没做什么(有意为之)

- **没有拆 DLL**(用户明确要求单文件)
- **没有改优化等级**(`-Oz` 保持)
- **L2(`UiDamage` 脏矩形 + 独立叠加层)评估后缓做**:受益方只有"调试开关打开时的帧率",
  而正确实现必须把叠加层旧矩形注入底层组件重绘范围,穿透 PlotView 四条快路径 + 两层缓存;
  在用户反复报过"拖拽错位 / 贴图残留"的背景下,风险与收益不成比例。理由写在
  `Documents/UI_Refactor_Plan.md` §5 与 `PlotView::SetForceFullRepaint` 的文档里。
2026-10-29 11:53:54 +08:00
DeepSeek V4.1 Flash aa4925a2b7 fix(UI): 边界框不再内缩 + 文本获得几何身份 + 补全漏项 + 描边提速 32%
## 1. 取消内缩(这是"边框不清晰"的主因)

第九轮为"父子矩形共边不叠粗"给 depth > 0 的 box 加了 1px 内缩。结果 GX 徽标
(30x17 逻辑像素的胶囊,圆角 = 高的一半,外接矩形正好等于 LaneBadgeRect)内缩 1px 后
框比形状小一圈 ⇒ 左右尖端与上下圆弧全部落在框外。

现行规则:**框 = 可见形状的外接矩形,绝不内缩**。父子共边时让"后画的覆盖"即可,
这本来就是正确的图层语义。层级 > 0 只影响配色,不再影响几何。

## 2. 文本第一次有了几何身份(唯一出口回报,不逐处登记)

文本是本项目唯一"没有几何身份"的元素:65 处绘制点各自现算 gd::RectF、画完即弃。
逐处补登记必漏,因此在唯一出口回报一次:

  PaintUtil 新增
    class UiTextRectSink { virtual void OnTextRect(const gd::RectF&) = 0; };
    void UiSetTextRectSink(UiTextRectSink*);       // 开关关闭时为 nullptr
    void UiSetTextRectOffset(float dx, float dy);  // 离屏位图渲染的坐标换算
    UiTextRectSink* UiGetTextRectSink();

DrawUiText() / UiTextAtClipped() 画完后按对齐方式 + 实测宽高把布局矩形**收窄成墨迹矩形**
再回报。UiDebugBoxList 直接继承该接口,宿主只需 UiSetTextRectSink(&m_debugBoxes) 一句。
开关关闭时只是一次空指针判断,正常绘制零影响。

## 3. 缓存命中的行也要有文本框

面板的行是"先画进位图再贴回",命中缓存的帧**根本不调用 UiText** ⇒ 文本框第二帧起消失。
做法:行渲染时用本地 RowTextCollector 收集(位图坐标)→ 连同位图存进
RowBitmapCache::textRects → 贴图时按 (originX, originY) 回放。
⚠️ **必须无条件收集**:只在"外层有收集器"时收集会导致"关调试时滚过列表 → 打开调试 →
命中缓存 → 永远没有文本框"。

## 4. 补齐 PlotView 漏项(全部读现成成员)

m_timeAxisRect / m_scrollBarRect / BackToTopRect() / m_cursorSpanRect /
m_cursorLabelRects / EditFieldRect()

## 5. 描边提速:图元 12 → 8

微基准结论:图元数量是唯一瓶颈(每次 FillRectangle ≈1µs)。
旧实现把每条边拆成"中段 1px + 两端各一段加粗"= 12 次;新实现 = 4 条 1px 边 + 4 个自适应角块。
实测 paintBoxes 5.892ms → 4.028ms(-32%),观感几乎一致(角仍是两条粗边的交点)。
⚠️ 不要改写成 GraphicsPath/FillPath(单次约 20µs,比 12 段还慢);
GDI PolyPolygon 一次画 5688 个矩形要 114ms,更不能用。

## 6. 关闭开关时清空容器(释放内存,避免误读)

## 7. 验证

- make release / make debug 零 error 零 warning;TargetCheck.exe 909,824 B(888.5 KiB < 900 KiB)。
- 新增探针 .preflight/lbcovr 7/7:box-time-axis / box-plot-scrollbar / box-back-to-top /
  box-gx-badge-matches-pill(容差 0.5px)/ text-boxes-collected(89 个)/
  row-text-boxes-from-cached-rows / boxes-cleared-when-off。
- 回归:lbcheck 21/21、lbresid 5/5、lbzorder 4/4、uicheck 69/69。
- rendercheck:8 个场景的整帧与区域哈希与上一轮**逐一相同** + FUNC 7/7
  (证明调试功能对正常绘制零影响)。
- 帧耗时分解(MF4 / 283 网格):base 9.06ms、仅强制整帧 6.66ms、完整调试 11.35ms;
  collect 0.082ms、paintBoxes 4.028ms(上一轮 5.892ms)。

## 8. 文档

Documents/UI_Refactor_Plan.md / UI_Refactor_Progress.md / UI_Refactor_Report.md 追加第九阶段;
Documents/agent_mem.md 同步变更日志与「2026-10-28 第十一轮」硬约束与踩坑。
2026-10-28 17:38:55 +08:00
DeepSeek V4.1 Flash 57e12cfb27 fix(UI): 布局边界按尺寸自适应 + 修复拖滚动条的绘制残留与浮层层级
## 1. 边界描边按元素尺寸自适应(修复"小元素变成粗描边")

旧规则四角固定 3-4px:对 15px 的复选框来说包角占短边 20%,看起来就是一整圈粗边框。
新规则全部以**短边**为基准:
- 四边固定 1 设备像素;
- 包角粗细 = min(基础 4/3, 短边/10),再夹到 [1, 短边/4];
- 包角长度 = min(14, 短边/3),且不小于包角粗细;
- 短边 <= 12 只画四个 L 形角标记、**不画边**(一圈连续描边会让小方框显得"被框住");
- 短边 <= 5 完全不画(再画就盖住元素本身);
- 内缩只用于「层级 > 0 且短边 >= 10」,否则小元素会被缩没。

同时**删除文字标签**(含 UiDebugPaintStyle):黑底白字在小元素上必糊成一片,体积也省约 1.5KB。

## 2. 拖网格滚动条时的"大量绘制残留":三个独立成因

R1. PlotView::CollectDebugBoxes 读了 m_decorRects —— 那是**装饰层位图缓存**的登记矩形,
    坐标处在位图/图层坐标系(带负 Y、超出绘图区),于是出现 (472,-27 1213x101) 这类
    **横跨工具栏的幽灵巨框**。改为只用 LaneBadgeRect 与 m_legendHits(屏幕坐标)。

R2. Paint 的三条快路径(静态场景贴图 / 宽基底贴图 / 样式补丁)只重画**变化的部分**,
    而调试边框是画在整个窗口上的叠加内容 ⇒ 上一帧的边框残留在未被重画的像素上,
    直到松开鼠标走整幅重绘才消失。
    新增 PlotView::SetForceFullRepaint(bool):开启调试叠加层时禁用全部快路径。

R3. 组件内元素可能落在自己地盘之外(滚出可视区的 lane.rect 的 Y 为负),不裁剪就画到
    工具栏/标签栏上。box 增加 clip:宿主按组件 Area() 设置,Add() 跳过无交集、
    绘制时与 clip 求交。

另外把 Render 的 fullChrome 并入 m_showLayoutBounds,装饰区也保证完整一帧。

## 3. 层级:排序 + 挖洞(修复二级菜单被下层边界覆盖)

- UiDebugBoxList::PushLayer() + 按 (layer, depth) 稳定排序:后收集的内容压住先收集的。
- 但仅靠排序不够:调试线画在所有内容之后,下层组件的线仍会显示在不透明菜单上面。
  新增 AddOccluder():菜单/拾色器把本体登记为遮挡矩形,绘制**更低层**的 box 时用
  Region::Exclude 挖洞。实测菜单区与子菜单区的琥珀/绿色像素均为 0。
- 浮层**不设** clip = 本体矩形:二级子菜单会展开到本体之外,设裁剪会把它整个裁掉。

## 4. 验证

- make release / make debug 零 error 零 warning;TargetCheck.exe 906,752 B(885.5 KiB < 900 KiB)。
- 新增 .preflight/lbresid(MF4-Record.MF4 / 283 网格 / 拖滚动条)**5/5**:
  toolbar-unchanged-while-scrolling、repaint-same-state-is-identical、
  toolbar-restored-by-full-repaint、residual-never-above-plot-area、later-layer-wins-over-earlier。
- 新增 .preflight/lbzorder **4/4**:菜单与二级子菜单区内下层颜色像素 = 0。
- 回归:.preflight/lbcheck 21/21、Tools/uicheck 69/69、
  Tools/rendercheck「工具栏带以下」区域哈希 8/8 与上一轮逐一相同 + FUNC 7/7。

注:判断"是否有绘制残留"不能用"拖拽帧 vs 松开后一帧"——两者图层预热进度不同会产生
几十万像素的**合法**差异;正确判据是**同一状态连续两帧必须逐像素相同**。

## 5. 文档

Documents/UI_Refactor_Plan.md / UI_Refactor_Progress.md / UI_Refactor_Report.md 追加第八阶段;
Documents/agent_mem.md 同步变更日志与「2026-10-28 第十轮」硬约束与踩坑。
2026-10-28 17:01:26 +08:00
DeepSeek V4.1 Flash 24706c906a feat(UI): 布局边界改为四角粗/四边细包角 + 全元素覆盖 + 几何同源(RowRect O(1))
## 1. 边界观感:四角粗、四边细

用户反馈"边框就是实心矩形、粗细不一,效果不佳"。旧实现是四条等宽实心矩形
(层级 0 用 2px、其余 1px),同一元素的角与边粗细不一致,父子矩形共边时还会叠成更粗的一条。

新规则:
- 四条边固定 1 设备像素(四条独立实心矩形;不用 DrawRectangle,笔宽奇偶会偏半像素);
- 四个角额外加粗:层级 0 = 4px、其余 3px,沿水平/垂直各延伸 cornerLen(≤14px 且 ≤ 短边一半);
- 层级 > 0 整体内缩 1px,父子矩形共边不再叠粗;
- 绝不填充(几十层嵌套填充会把内容糊住);
- 整像素对齐 + PixelOffsetModeNone + 关抗锯齿。

四角粗不只是观感:多条边重合时靠角的形状分辨层次(父=青蓝大包角、子=同位置品红小包角)。

## 2. 协议:组件"收集 box",宿主统一绘制

UiComponent::CollectDebugBoxes(UiDebugBoxList&)  默认只登记 Area()
UiWidget::CollectDebugBoxes(boxes, depth)        递归整棵控件树
Application::PaintLayoutBounds                   收集后按 depth 升序一次性描边

刻意保持 box 为**值类型**而不是让它继承控件基类:PlotView 的 4 层位图缓存、
SignalPanel 的 192 槽行位图缓存都要求"绘制由组件自己负责",几何进继承体系会与
缓存键/失效体系打架。box 不参与绘制,因此能让绘制/命中/调试共用同一份几何。

## 3. 覆盖补齐(此前大量元素被裹在外层单位里)

- Toolbar:区域 / 每个按钮 / **按钮内容(文字或齿轮图标,新增 UiButtonWidget::ContentRect())**
- TabBar:区域 / 标签条 / 新建按钮 / 每个 ✕
- SignalPanel:4 分区 + 筛选输入框/清空✕/全部显示 + 滚动轨道与滑块
  + 样式区 5 行及其 12 色块/自定义/3 模式/3 调整/线宽与透明度轨道和数值
  + 目标行/名称/GX 徽标 + **每个可见行**的折起三角/复选框/色块/名称/GX 徽标/✕
  (组行另有组名/计数)+ 内联编辑框
- PlotView:区域 / 每个网格 / GX 徽标 / 装饰层矩形 / 图例托底+色块+文本
  / **5 类浮动面板全部 20+ 字段**(标题/✕/折起三角/徽标/编辑行/按钮/内容区/滚动轨道与滑块
  /透明度轨道与数值/两个开关/各行复选框与数值与滑轨与复位与名称与✕/分组栏/表头/固定点行)
- PopupMenu:浮层 / 每个条目 / 可滚动可视区 / 轨道 / 滑块 / 二级子菜单与条目
- ColorPickerPopup:面板 / 标题 / 预览 / 色号 / 色域 / 色相条 / 3 条滑轨与数值 / 输入框

标签增加**尺寸门槛**(< 56×16 逻辑像素不画):复选框/色块/✕ 紧邻,逐个小框贴字会糊成一片。

## 4. 设置按钮配色 + 齿轮图标

- ToolbarAction::SETTINGS 改为 primary = true,与「导入文件」同为强调色主按钮。
- 齿轮几何重做:齿占最外一圈(0.66R→1.0R)、外环中心线 0.54R/笔宽 0.24R、轴孔 0.17R。
  旧版让齿从环中心线伸出,齿被环的描边吃掉 ⇒ 看起来只剩一个"甜甜圈"。

## 5. 几何同源与性能

- RowRect(row) 原本每次调用都从头累加行高(O(row)),而画一行要调它 **5 次**
  (✕/徽标/名称/组计数/折起三角各一次)⇒ 200 行只见 30 行时每帧上万次加法,
  命中测试(每次鼠标移动)同样走这条路。
  新增 m_rowTop 前缀和(由 UpdateAnimGeometry() 统一重建)⇒ **O(1)**;
  长度与行集合不一致时自动退回逐行累加(正确性优先)。
- ContentOffsetBeforeRow 同样改用前缀和。
- PanelBounds() 收敛 6 处重复的 (m_clientBounds.Width > 1.0f) ? m_clientBounds : m_area。

## 6. 验证

- make release / make debug 零 error 零 warning;TargetCheck.exe 910,336 B(889 KiB < 900 KiB)。
- .preflight/lbcheck:21 / 21 PASS(含 bounds-off-restores-pixels:关闭后逐像素回到开启前)。
- Tools/rendercheck:「工具栏带以下」区域哈希 8/8 与上一轮**逐一相同**
  (证明本轮改动只落在工具栏带内);FUNC 断言 7/7 PASS。
- Tools/uicheck:69 passed / 0 failed。

## 7. 文档

Documents/UI_Refactor_Plan.md / UI_Refactor_Progress.md / UI_Refactor_Report.md 追加第七阶段;
Documents/agent_mem.md 同步变更日志与「2026-10-28 第九轮」硬规则与踩坑记录。
2026-10-28 16:28:57 +08:00
DeepSeek V4.1 Flash ab19a6e72e feat(UI): 显示布局边界(Android 风格)+ 工具栏设置菜单(齿轮)+ 三处可见收益优化
## 1. 新功能:齿轮按钮 -> 应用设置菜单 -> 显示布局边界

- ToolbarAction::SETTINGS 位于「导入文件」左侧;工具栏布局改为
  「左侧固定簇(kLeftClusterCount = 2:齿轮 + 导入文件)+ 右侧按钮组」。
- 齿轮是矢量化绘制(8 齿 + 外环 + 轴孔,全部按 half 的比例推导),
  不用字体字符「⚙」——该字符在不同回退字体里大小与基线不一致,会忽大忽小、垂直不居中。
  UiButtonWidget 新增 UiButtonIcon(NONE / GEAR)与 SetIcon。
- MenuScope::APP_SETTINGS(新增)+ 设置菜单:HEADER「调试」+ 勾选切换项「显示布局边界」
  (keepOpen:点击后菜单保持打开,勾选态由 UpdateItemLabel 回写)。
  锚点取 Toolbar::ButtonRect(SETTINGS) 的控件实测矩形,宿主不再自己推算按钮几何。

## 2. 布局边界:基类给协议,组件自己上报

- UiComponent 新增 virtual PaintDebugBounds(g, scale),默认只画 Area()。
- Toolbar / TabBar 覆盖:跳过与区域重合的根控件,遍历 m_root->Children()(层级 1);
  TabBar 另上报每个 ✕(层级 2)。
- SignalPanel 覆盖:表头 / 筛选框 / 列表 / 样式区(1)+ 滚动条、样式行、当前可见列表行(2)。
- PlotView 覆盖:每个 LaneVisual::rect(1);不上报随数据变化的瞬态几何(曲线/胶囊/提示)。
- UiWidget 新增 PaintDebugBounds(g, scale, depth) 递归整棵控件树 —— 已上树的控件零成本获得边界。
- 宿主 PaintLayoutBounds 补客户区外框、分隔条、打开的菜单/拾色器。

视觉与实现约束(Android「Show layout bounds」风):
- 只描边、不填充(几十个嵌套矩形叠加填充会把内容糊住);
- 层级配色 青蓝(2px) / 品红 / 琥珀 / 绿(1px);
- 线宽按设备像素固定(不乘 scale),临时 PixelOffsetModeNone + 关抗锯齿 + 整像素对齐,
  保证 1px 就是 1px;
- ⚠️ 只在 Render 最后一步叠加,绝不进入任何位图缓存(PlotView 4 层缓存 / SignalPanel 行位图缓存),
  否则关掉开关后线条会残留;
- ⚠️ fullFrame 并入 m_showLayoutBounds:开启时必须整帧回送,否则「只回送绘图区」的快路径
  会让装饰区边界停在旧帧上。

## 3. 可见收益优化(行为不变)

- Toolbar::Layout:控件树改为按需重建(m_treeDirty),只在规格/字体/主题/缩放/动态文本
  变化时重建。拖动面板分隔条时 Relayout 每帧调用一次 -> 不再每帧 10 次堆分配 + 8 次测量缓存失效。
- Toolbar::SetActionLabel:只写 m_labelOverrides(唯一权威)+ 置树脏,消除「覆盖表与控件文本
  双写」可能不同步的问题。
- UiWidget::DispatchRedraw(event):新增;收敛 Toolbar / TabBar 里重复的
  Dispatch(); return UiRectHasArea(TakeDirty());(TakeDirty 会清零,漏一次就把下一次真实变化
  误判成「无变化」)。

## 4. 验证

- make release / make debug:零 error 零 warning;TargetCheck.exe 897,024 B(< 900 KB)。
- 新增离屏探针 .preflight/lbcheck:21 / 21 PASS
  (齿轮在导入文件左侧/同高/更窄;开启后青蓝 24,930 px、品红 19,734 px;关闭后逐像素还原;
   菜单 scope、锚定贴合按钮、宽度未被撑大、条目初始未勾选、点击翻转开关、菜单保持打开)。
- Tools/rendercheck:8 个场景的「工具栏带以下区域」哈希与改造前**逐一相同**
  (证明改动像素被完全限制在工具栏带内);整帧哈希因新增按钮整体变化,属预期。
  FUNC 断言 7 / 7 PASS。
- Tools/uicheck:69 passed / 0 failed。

## 5. 文档

Documents/UI_Refactor_Plan.md / UI_Refactor_Progress.md / UI_Refactor_Report.md 追加第六阶段;
Documents/agent_mem.md 同步变更日志与「2026-10-28 第八轮」硬约束与验证坑。
2026-10-28 15:46:45 +08:00
DeepSeek V4.1 Flash 0ec8055e95 重构(UI): 统一组件协议 + 去手写链路 + 大文件按职责拆分 + 代码审查 + 剥离 Tools
## 1. 新增 UiComponent 顶层组件协议(不是负优化)

Components/Shared/Inc/UiComponent.hpp:纯接口、无状态,8 个方法
SetScale / Layout / Area / Paint / HitTest / OnMouseLeave / IsEditing / QueryCursor。

Toolbar / TabBar / SignalPanel / PlotView 本来就各自拥有完全同名同签名的成员,
因此改造 = 加 override + 补 Area()(PlotView 另加一个非 const 的 QueryCursor 转发,
因为它自己的查询会惰性建立行缓存),实现代码零改动、静态派发、体积无增长。

刻意不把 UiWidget 当作它的基类:UiWidget 是"树节点"(父给尺寸、父下探脏区、
命中沿树链派发),而 PlotView(4 层位图缓存)/ SignalPanel(行位图缓存)是
"地盘宿主"。强行上树会推翻"父脏则子全脏"的前提,历史上正是这类耦合导致过
"后台渲染覆盖平移结果""命中矩形错位"的事故。头文件里已写明这条理由。
Toolbar/TabBar 是两者的交集:对外实现 UiComponent,对内持有一棵 UiWidget 树。

## 2. 宿主侧:四条手写链 -> 组件表

Application 新增 ComponentEntry{ component, target, chrome } 与 m_components
(底->顶:plot -> panel -> toolbar -> tabBar),BuildComponentTable() 装配一次。

- ApplyDpi        6 行逐个 SetScale  ->  1 个循环
- Relayout        手算三列 x 坐标    ->  UiStackRects(内容区, HORIZONTAL, {panelW, splitW, AUTO})
- WM_MOUSELEAVE   逐个 OnMouseLeave   ->  循环累积 chromeChanged / plotChanged
- WM_SETCURSOR    if/else if 链      ->  从表尾向前找首个命中者,取其 QueryCursor

顺带兑现历史缺口:UiLayout::UiStackRects 自引入以来第一次有了生产调用点。

有意保留显式的三条链(WM_LBUTTONDOWN / WM_LBUTTONUP / WM_MOUSEMOVE):它们各自带
组件专属后置动作(提交重命名、构建派生信号、面板联动滚动、更新拖拽光标、
plotOnlyMove 决定局部失效还是整帧失效)。塞进表里只能靠 lambda 字段,
可读性反而下降 —— 属于用户明确禁止的"过度抽象",因此不做。

## 3. 大文件按职责拆分(纯整函数搬移,逐像素 0 差异)

SignalPanelInteraction.cpp 2329 -> 831 行,拆出:
  SignalPanelEdit.cpp   616  内联编辑(筛选框、组名/信号名、数值、颜色与透明度)
  SignalPanelScroll.cpp 205  动画与滚动(折起、联动滚动、滚动到目标信号)
  SignalPanelMenu.cpp   527  弹出菜单(系数/偏移、镜像、右键菜单与命令回执)

Shared/PopupMenu.cpp 1901 -> 706 行,拆出:
  PopupMenuLayout.cpp 753  尺寸测量、项矩形排版、滚动可见性
  PopupMenuPaint.cpp  397  背景/阴影/项绘制
  Inc/PopupMenuInternal.hpp 103   pmdetail 常量 + inline 辅助(跨 TU 安全)

拆分判据是"职责"而非行数;共享常量一律收敛到 *Internal.hpp。

## 4. 代码审查(修 5 处)

- SignalPanelInteraction.cpp 头注释仍写"4 片",实际 6 片 -> 改为完整清单
- PopupMenuLayout.cpp 机械搬移时把构造函数一起搬走 -> 重复定义,只保留在交互分片
- PopupMenu.cpp 的 #include "PopupMenuInternal.hpp" 落进 namespace jdi 内 -> 移到文件头
- PopupMenuPaint.cpp 缺 UiControls.hpp(UiDrawEditFieldContent 未声明)-> 补 include
- 全仓复查历史删除符号残留引用(m_paintPlot / ClampCursorsAfterPan /
  PaintFreeMarkerTooltip / CaptureDecorCache / FitSinglePointSignal / CsvTable /
  DataFile / .preflight / structcheck)-> 零命中

## 5. Tools/ 剥离

git rm -r --cached Tools + .gitignore 新增 Tools/(本地文件保留,可随时复跑)。
ui_refactor 分支为全本地分支,另做 filter-branch 清除历史中的该目录。

## 6. 验证

- make release / make debug:零 error 零 warning
- rendercheck:A-H 八个场景哈希与改造前逐一相同
  (A 2166933039626F66 B C444ECFEB18FD73D C 7984446D8E7A4418 D BF855879CA12F289
   E 1C663E4118616016 F 0535292D8E07AE18 G 7A58172C46577729 H 23F390DD76141113)
  FUNC 7/7 PASS
- uicheck:69 passed, 0 failed
- TargetCheck.exe 891,392 B(< 900 KB 阈值)

## 7. 文档

Documents/UI_Refactor_Plan.md / UI_Refactor_Progress.md / UI_Refactor_Report.md
新增第五阶段章节;Documents/agent_mem.md 同步变更日志与「第七轮」硬约束。
2026-10-28 14:35:32 +08:00
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 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 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 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 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 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 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 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 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 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 fc77b0262f 「调整」行:系数/偏移 按钮 + 偏移调整(Y 轴/时间轴)二级面板;结算顺序加入偏移;派生信号继承调整变换
按钮与面板:
- 「系数」按钮改名为「系数/偏移」;点击展开的二级面板在原「系数调整」输入框下方新增「偏移调整」:
  与「系数调整」同一套绘制(标题 + 标题栏右侧的还原图标),下面两行分别是「Y轴」「时间轴」,
  每行左侧说明文字、右侧内联输入框(整行数值框)。
- 「Y轴」默认 0.00,非编辑态**始终带正负号**(+0.00 / -12.50),小数不足两位时补两位、更高精度保留更多位。
- 「时间轴」默认 0.00,非编辑态显示与双光标差值同一套复合单位文本(`Δt:500ms`、`Δt:1h 1min 2s`);
  进入编辑态时显示以**秒**为单位的数值,并在输入框最右侧提示单位 `s`。
- 「偏移调整」的还原图标把两个偏移恢复为默认值 0;「系数调整」的还原图标仍只重置系数。
- 面板/输入框的编辑初值与显示文本分离(`MenuItem::editValue` / `editSuffix`),
  非编辑态(`Δt:…`)与编辑态(`3662`)不一致时也能正确回显。

语义与结算顺序:
- 结算顺序变为 **系数 → 绝对值 → 镜像 → 偏移**(前三步不变)。
- Y 偏移:给绘制值加常数(波形上下移动);时间偏移:采样绘制在 `t + offsetTime` 处
  (例如 t=1s、偏移 -0.5s → 画在 0.5s)。
- ⚠️ Y 偏移以**未镜像空间**为准:垂直镜像开关变化时同步取反偏移量,
  使绘制等价于「先加上偏移再镜像」(开关来回切换可逆)。
- 绘制链路全部跟随偏移:`BuildPolyline`(逐点描边 / 像素列包络 / 跨列边界插值 / 粗粒度分组层,
  保序的 Y 偏移直接加在分组极值上)、派生预览曲线、光标取值读数(含 Δt)、
  可见区间 Y 轴自适应、整段自适应的区间映射;折线缓存键、场景指纹、样式快照与图层指纹都纳入两个偏移。

派生信号(实验性功能「构建为新信号」):
- 修复「预览是调整后的波形、构建出来却按原始状态绘制」:新信号**继承源信号的调整变换**
  (系数/绝对值/两个镜像/两个偏移)——网格预览绘制的是 `变换(滤波结果)`,
  而构建出的信号只存了滤波结果,因此必须把变换一并继承;
  时间轴偏移**只能**靠继承(派生信号与源信号共用同一文件时间轴,无法把时间平移烘进数据)。

离屏验证(不干扰用户):
- ⚠️ harness 现在在每次注入鼠标消息后**立即释放本窗口的鼠标捕获**
  (宿主 WM_LBUTTONDOWN 会 SetCapture,隐藏窗口同样会把用户的真实鼠标输入吸走);
  后续消息本来就是直接投递,无需捕获。
- 新增 [AP]:按钮标签不裁切、面板 5 行顺序与「偏移调整」位于系数输入框下方、
  Y轴/时间轴行的标签与输入框同行且默认值/单位提示正确、时间轴编辑态显示秒值并在最右侧提示 `s`、
  Y 偏移提交/显示正负号/波形整体平移量、时间偏移提交/Δt 复合单位(含 3662 → Δt:1h 1min 2s)/
  编辑态为秒、**按"偏移若干个采样点"构造的精确几何验证**(采样被画到移位后的像素位置、且不再画在原处)、
  读数跟随、两个还原图标、垂直镜像对 Y 偏移取反(可逆)、
  以及派生信号继承变换;[AL] 补充「系数→绝对值→镜像→偏移」的顺序与区间映射(含 Y 偏移)。
- 合计 645/645 通过;make clean && make all 零警告。
2026-10-13 17:56:00 +08:00
DeepSeek V4.1 Flash 2b02518f85 样式区「调整」行改为 系数/镜像/绝对值:系数二级面板(还原图标 + 整行输入框)、镜像二级面板(水平/垂直复选框);Y 轴自适应按变换后曲线
- 「调整」行三个按钮改为「系数」「镜像」「绝对值」(顺序即结算顺序:系数 → 绝对值 → 镜像)。
  「系数」「镜像」点击后在按钮下沿展开二级面板(按下只登记、抬起命中同一按钮才展开,
  与自定义颜色入口一致),「绝对值」仍是直接切换的按钮。
  打开面板时主动清除样式区悬停态 —— 面板会接管鼠标事件,否则该按钮会一直保持"鼠标按住"般的高亮。

- 数据层:`SignalStyle::inverted` 拆成 `mirrorVertical`(Y=0 上下翻转)与
  `mirrorHorizontal`(水平镜像 = 数据反序);新增 `Mirrored()`、
  `SourceIndexFor`(水平镜像的遍历下标 → 源下标映射)、`TransformValueRange`(区间映射)。
  `ApplyDrawTransform` 顺序固定为 系数 → 绝对值 → 垂直镜像;水平镜像在采样遍历层处理。

- 绘制链路:`BuildPolyline`(逐点描边与像素列包络两条路径、跨列边界插值、粗粒度分组层)、
  派生预览曲线、光标取值读数(含 Δt)全部按同一变换取值;水平镜像时按源下标区间反序读取,
  包络层仍复用粗粒度分组;折线缓存键、`SceneContentHash`、样式快照都带上两个镜像位。

- 系数二级面板:标题「系数调整」,标题栏最右端是一个可点击的「还原」自绘图标按钮
  (悬停有高亮反馈,点击后系数重置为 1,面板保持打开);下面是一整行输入框
  (不再显示「绘制系数」说明文字),数值至少显示两位小数、精度更高时按实际精度显示
  (1 → 1.00、2.5 → 2.50、1.2345678 原样)。

- 镜像二级面板:两行带复选框的切换项「水平镜像」「垂直镜像」,点击后**面板保持打开**
  (`MenuItem::keepOpen`),勾选状态就地回写;任一项勾选时「镜像」按钮显示为激活态。

- Y 轴自适应修复:整段自适应(`TabConfig::ComputeLaneRange`)按区间映射(`TransformValueRange`)
  计算,不再直接用原始 min/max;可见区间自适应(`AutoRangeLaneToView`)逐点套用绘制变换
  (含水平镜像的源下标映射)。用户报告的"调整系数/镜像后自适应区域错误、仍按原始数据"由此修复。

- `PopupMenu`:新增 `MenuItemKind::HEADER_ACTION`(标题 + 右侧可点击图标,图标有悬停高亮)、
  `MenuItem::keepOpen`(切换项点击后不关闭菜单)、`HeaderIconRect()`,
  以及无说明文字的整行输入框(占满内容区)。

验证(离屏 harness,SW_HIDE、不动鼠标键盘屏幕):
- [AL] 重写/扩充:变换顺序与区间映射(含水平镜像的源下标反序)、调整行三按钮的顺序/等宽/标签不裁切、
  「系数」按住不弹出 / 移开取消 / 单击展开(标题 HEADER_ACTION、无标题输入框占满整行、初始 1.00、
  两位小数与更高精度回显、提交后面板保持打开、折线缓存键携带系数)、
  「还原」图标位置/悬停像素变化/点击后系数回到 1.00 且面板保持打开、
  「镜像」面板两行复选框(初始未勾选、keepOpen、勾选后按钮激活、取消其一保留另一)、
  打开面板后按钮不再停留高亮、系数/镜像按钮的激活填充与其它启用态切换按钮逐像素同色;
  `BuildPolyline` 顶点范围与变换后数据范围逐项对齐(默认/系数 2/绝对值/垂直镜像/三者叠加),
  水平镜像用窄视窗逐顶点核对"取的是对面端点的采样"(8/8)+ 读数跟随;
  以及 Y 轴自适应:整段/可见区间自适应在系数、绝对值、垂直镜像下均跟随变换后的曲线。
- 合计 594/594 通过;make clean && make all 零警告。
2026-10-13 14:50:38 +08:00
DeepSeek V4.1 Flash 2f243dd557 样式区「调整」行:系数调整(二级面板输入)+ 取绝对值 + 中心反转
- 移除上一轮新增的「系数增益」输入行与「取绝对值 / 中心反转」按钮行。
- 在「样式」下方、「线宽」上方新增「调整」行,一行三个等宽按钮:
  「系数调整」「取绝对值」「中心反转」(后两者即原按钮迁移至此)。
  样式区固定行数回到五行(颜色 / 样式 / 调整 / 线宽 / 透明度),标签列宽回到 44(只需容纳「调整」)。
- 「系数调整」交互与"自定义颜色色块入口"一致:按下只登记,**抬起**且指针仍在同一按钮上才展开
  二级面板;面板锚定在按钮下沿,内含一个数值输入框(默认 1,EditFilter::NUMBER)。
  提交后面板保持打开(与网格 Y 轴上下限的菜单内联编辑一致),并把夹取后的真实值回写输入框。
- 按钮启用态规则:绘制系数 != 1 时显示为"已启用"(与其它切换按钮启用样式完全一致:
  accentSoft 底 + accent 描边),等于默认值 1 时为"未启用"样式。
- 系数为 1 / 非保序变换的回退逻辑、SceneContentHash / 样式快照 / 折线缓存键等绘制链路不变。

验证(离屏 harness,SW_HIDE、不动鼠标键盘屏幕):
- [AL] 重构为:五行高度与行序(调整行在样式行与线宽行之间)、三按钮按序铺满且不重叠、
  三个标签都完整放得下(无裁切);
  「系数调整」按住不弹出 / 移开再松开取消 / 单击展开(SIGNAL_PANEL 范围、标题「系数调整」、
  初始值 1、锚定在按钮下沿)→ 点击输入框进入编辑 → 键入 2.5 回车提交(面板保持打开、
  输入框回显 2.5、折线缓存键携带新系数)→ 点别处关闭面板;
  像素级核对按钮样式:gain=1 时填充为 cardBg,gain=2.5 时填充与"取绝对值已启用"的按钮**逐像素同色**
  (0xFFCFE4F3)且与未启用态不同。
- 其余用例(样式变换顺序与顶点范围、拾色器两列等高、选区放大下 GX/图例命中、标题栏内存读数)
  保持不变。合计 549/549 通过;make clean && make all 零警告。
2026-10-13 13:51:31 +08:00
DeepSeek V4.1 Flash c3343907ec 标题栏内存占用 / 样式区增益与取绝对值·中心反转 / 拾色器排版 / 图例去托底与命中优先级
标题栏:
- 应用名称后追加「  |  RAM : <值>」:竖线两侧各留两个空格(与分组标题「组X | 组名」的间距风格一致)。
- 数值按 KB/MB/GB 自适应、保留两位小数(复用 FormatByteSize);用 K32GetProcessMemoryInfo 取本进程
  工作集(与任务管理器「内存」一致,无需新增链接依赖);1 秒定时器刷新,数值未变时不调用 SetWindowTextW。

信号样式自定义栏:
- 线宽与透明度交换上下位置(线宽在上、透明度在下)。
- 新增「系数增益」:输入框(默认 1,只接受数值,范围 ±10000),点击进入编辑、回车/点击别处提交。
- 新增「取绝对值」「中心反转」两个同行的切换按钮。
- 三者作用顺序固定为「系数增益 → 取绝对值 → 中心反转」(中心反转最后结算),实现为
  ApplyDrawTransform(style, value),在 BuildPolyline(逐点描边与像素列包络两条路径,含粗粒度分组层)、
  派生预览曲线与光标取值读数统一调用;样式属于信号本身(跨视图共享)并进入 SceneContentHash /
  SceneStyleEntry / 折线缓存键,改动会立即重绘。
- 非保序变换(取绝对值 / 反转 / 负增益)会回退到逐采样点变换、不再使用粗粒度包络层,保证包络精确。

自定义颜色面板:
- 把底部的「色号 + 预览色块」移到 RGB 滑块上方;右侧一列(预览行 + 三行滑块)与左侧「色域方块 +
  色相条」高度严格相等,两列上下边对齐,面板不再为它单独保留一行。

网格内:
- 去掉图例与 GX 编号下方的整块不透明托底(它会把网格线整片擦掉,看起来像一个"可点击控件",
  而其右侧空白实际并不响应点击)。
- 图例与 GX 徽标改为优先命中:先于时间光标线 / 光标读数 / 边沿计数横向光标 / 选区放大判定,
  因此选区放大模式下也能正常拖拽 GX 标签与点选图例(浮动面板仍然优先,与绘制层序一致)。

验证:离屏 harness(.preflight/uitest,SW_HIDE、不动鼠标键盘屏幕)新增
[AK] 标题栏内存读数(格式与竖线间隙、单位阈值、两位小数、随 256MB 分配增长、定时器刷新);
[AL] 样式区行序与几何、增益输入提交、两个切换按钮的真实点击、ApplyDrawTransform 顺序,以及
BuildPolyline 顶点范围与变换后数据范围逐项对齐(默认 / 增益 2 / 取绝对值 / 中心反转 /
负增益+取绝对值+中心反转叠加);
[AM] 拾色器两列严格等高(140.00 == 140.00);
[AN] 选区放大模式下 GX 拖拽重排 + 图例点选 + 网格线穿过图例带(托底已移除)。
另修正旧用例 [AI](远端已删除"光标跟随视界",改为断言光标时间位置不变、允许滑出视窗)。
合计 530/530 通过;make clean && make all 零警告。
2026-10-13 12:06:38 +08:00
DeepSeek V4.1 Flash d3c83237f1 信号 X 按钮移至行尾并统一抬起响应,光标读数补秒单位,图例悬停高亮信号,去除筛选框内圈
1. 信号条目的「移除」X 按钮移到最右端
   - 与分组行关闭 X 同尺寸(18)、同右边距(同一列)、同行内垂直居中,描边宽度也统一。
   - 网格编号 GX 徽标让位到 X 左侧,二者恒在列表最右侧;名称可用宽度改按实际徽标是否显示计算,
     不再白留一段空间。

2. 所有 X 关闭按钮由"按下即响应"改为"左键抬起才响应"
   - 与常见的可取消按钮一致:按下只记录目标,按住后移开鼠标再松开视为放弃。
   - 覆盖文件分组 X、信号 X、筛选框清空 X、标签页 X 与实验性功能面板 X。
   - 筛选框的清空 X 在编辑态下走的是宿主的 EDIT_FIELD 分支(OnEditMouseDown/OnEditMouseUp),
     非编辑态才走面板的按下/抬起分支,两条路径都已改造。
   - 标签页 X 原先只要求"抬起时仍在同一标签内",现收紧为"仍在 X 上"。
   - PlotView::OnLButtonUp 增加横坐标参数以便判定抬起位置。

3. 时间光标读数补上 s 后缀
   - 新增 FormatCursorTime,标签宽度测量与文字绘制同时使用,避免文字被裁掉;
     内联编辑仍使用不带单位的秒值。

4. 鼠标悬停网格图例时高亮对应信号
   - 除图例条目的半高亮外,对命中信号绘制与"鼠标贴近曲线"相同的光晕,
     便于确认该条目对应哪条曲线。

5. 筛选信号输入框编辑态不再出现内圈
   - 把输入框绘制拆分为"底板+边框"与"仅内容"两部分;筛选框外框已自绘,
     编辑态改用仅内容版本,空内容时不再套一层多余的圆角内框;
     有输入时的清空 X 不受影响。
2026-10-10 11:30:07 +08:00
DeepSeek V4.1 Flash a1e30985b8 修复输入框全选高亮溢出、标签页重命名无法失焦、新增信号行删除按钮
1. 内联输入框全选高亮越界(组名/标签页名/信号名编辑)
   UiFillRect 在不透明且面积 >= 20000 时走 GDI FillRect 快路径,而 GDI 填充不受
   GDI+ SetClip 约束;长文本全选时高亮宽度可达上千像素,于是画到输入框之外。
   UiDrawEditField 现在先把高亮矩形与裁剪区求交再填充。

2. 标签页重命名处于编辑态时,点击输入框之外的任何位置都不退出编辑
   Application 只在点击落在标签栏内时才把点击交给 TabBar。现在 WM_LBUTTONDOWN
   会先检查 m_tabBar.IsEditing(),是则提交(OnEditKey + OnTabBarResult)并退出编辑态,
   覆盖绘图区、信号面板、工具栏、分隔条等位置,且不会丢失新名字。

3. 信号选择区域每个信号新增 ✕ 删除按钮
   - 位置与所在组复选框同一列(垂直对齐),悬停显示手型光标并高亮。
   - 点击后面板向宿主发送 WM_APP+2;Application::RemoveSignal 会重映射所有标签页的
     可见性与网格引用、修正当前选中项,再真正从 DataSet 中删除该信号并释放采样内存;
     删除组内最后一个信号时退化为移除整个文件分组。
   - DataSet::RemoveSignal 采用移动填充 + shrink_to_fit,保证缓冲区内即刻析构。
2026-10-10 10:54:50 +08:00
DeepSeek V4.1 Flash 350538c634 命名审查与垃圾代码清理(纯重命名 / 死代码删除,无行为变更)
前提:加载完成后全部采样都已驻留内存,程序此后与源文件再无联系。
据此修正了一批会让人误以为"仍在读写文件"的命名:

- SampleFileAt → SampleSignalAt(取的是内存信号在给定时刻的采样值)。
- DataSet::AddPreparedFile → AddDataFile。
- PlotView::InvalidatePolylineCache → InvalidateDerivedCaches
  (它同时清空折线、边沿统计、中心平均值曲线、静态场景缓存与悬停引用)。
- PlotView::CursorColor(int which) → CursorColor()(两条光标同色,参数从未使用)。
- UiEllipsize(g,…) → UiEllipsize(…):纯测量不绘制,删掉被忽略的绘图上下文参数;
  同时删除与 UiMeasureTextWidth 完全重复的 UiTextWidth,调用点统一到 UiMeasureTextWidth。

清理残留参数((void)xxx;):
- TabConfig 的可见性/网格族(SetVisible / ToggleVisible / SetFileVisible / SetAllVisible /
  SoloSignal / SetMode / SyncLanes / FileHasVisible / FileAllVisible)去掉从未使用的
  const DataSet& 参数(仅 SyncToDataSet 真正需要数据层,保留)。
- PlotView::OnLButtonUp(int y)、OnMouseWheel(int delta, bool shift);PainterSignalHalo 去掉未用的 ref。
- BuildDataFile 中未使用的局部 rowCount。

删除无人调用的死函数:
  DataSet::Clear / AddFromTable;SignalColorCount;WideToUtf8 / StartsWith;
  PlotView::ViewMinTime / ViewMaxTime / EdgePanelRect / EdgePanelCloseRect /
  EdgeCenterPanelRect / ScaleLaneRange / BeginLaneValueEdit / LaneContentHeight;
  SignalPanel::NeedsScrollBar / IsFilterEditing / FilterText / ContentHeight;
  PopupMenu::EditingItemId;TabBar::IsDragging;TextEdit::PlaceCaret。

删除只写不读的死字段:CsvTable::sourceRowCount(与 rowCount 等义)与 skippedRowCount。

以 VERY_BIG_DATA.CSV 回归验证:加载与勾选、筛选框(含"无匹配的筛选信号")、
样式区线宽文案省略、选区放大(横向与纵向)、双光标读数与 Δt、右键菜单(Y 轴数值与
实验性功能子菜单)、合并/拆分网格、标签与信号重命名省略号,行为均与改动前一致;
make clean && make all 无任何警告。
2026-10-09 21:52:46 +08:00
DeepSeek V4.1 Flash 8ae8d7af4f 中心轴曲线配色 + 筛选框交互完善 + 双光标拖拽性能修复
- 中心轴边沿计数的平均值曲线改为「由信号色派生的高对比度颜色」:保持色相、
  大幅拉开明度(与原始波形高对比),透明度与原信号一致;原信号改色时曲线
  颜色随之变化,而光标显示该曲线数值时仍沿用原始信号的配色样式。
- 信号筛选框:内容区左右内缩改为与上下一致(不再「左右空隙明显更大」);
  有输入时右侧显示清空 ✕(悬停高亮 + 手型光标,点击清空且保持编辑态);
  筛选无结果时在列表区提示「无匹配的筛选信号」。
- 光标数值超出上下视界时,三角箭头标识下移 kCursorHandleReserve,
  不再遮挡光标自身的三角把手(含双光标模式)。
- 性能:新增「装饰区域脏标记 + 绘图区局部回送」快路径——绘图区内的悬停/拖拽
  期间跳过标签栏/工具栏/面板/分隔条的重绘,且只 BitBlt 绘图区矩形;
  WM_PAINT 依据更新区域是否落在绘图区内决定快慢路径,窗口缩放/遮挡露出/
  菜单与拾色器弹出等场景仍整帧重绘。
  以 VERY_BIG_DATA.CSV 全信号双光标拖拽实测:单帧耗时由约 28ms 降至约 12ms。
  同时修复拖拽期间残留的信号悬停提示气泡。
2026-10-09 19:10:43 +08:00
DeepSeek V4.1 Flash fe72afc83c 绘图交互完善 + 稠密区域拖拽性能修复(VERY_BIG_DATA 2ms/大格不再卡顿)
一、功能修正
- 修复「光标数据点在上下视界外不绘制」的问题,并让上下越界与左右越界一样用三角表示:
  新增 `DataMarker` / `PaintDataMarker()`,统一处理「可视区内画圆点、越界则钉在网格边上
  画三角指示方向」的四向情形(左/右/上/下)。光标取值与中心轴取值都改走这一套,
  因此纵向被裁掉的数据点不再消失。
- 「边沿计数(中心轴)」的中心平均值曲线改为**跟随原信号的样式**:
  与原始信号同色但更透明,线宽同样跟随(lineWidth + 0.6,下限 1.4),
  原信号改颜色/线宽/透明度后曲线同步变化(曲线参与场景指纹,样式变化即失效重绘)。
  光标显示中心值时的胶囊也改用**原始信号颜色**(一一对应),并**始终按线段模式**着色,
  不再使用光标色。
- 「拆分当前网格」在可点击时追加尾缀「(N个信号)」(N = 该网格内信号数),
  只有 1 条信号(不可点击)时不加尾缀。

二、新增:左侧信号筛选框
- 信号列表上方新增「筛选信号」输入框:空内容时显示灰色占位提示,单击进入编辑,
  点击别处或回车结束编辑;列表内容随输入**实时**变化。
- 支持 `*` 通配符(`*` 等价于"任意字符",因此 `*` 单独出现等价于不过滤),忽略大小写。
- 筛选结果**仍以组为单位**,组默认展开,只显示匹配的信号;组内无匹配信号时整组不显示。
- 复用统一的 `TextEdit` 编辑模型,因此光标/选区/Ctrl+A/C/X/V 等行为与其它输入框一致。

三、性能:修复 VERY_BIG_DATA.CSV 在 2ms/大格密集区域拖拽卡顿
以「显示全部 5 条信号 + 时间轴缩放到 2ms 一大格」为场景逐段打点定位后修复:
- **主因:稠密包络的锯齿描边光栅化**。抽稀时每列产生 min/max 两个顶点交替的锯齿,
  2000+ 条近乎满幅的斜线段在 GDI+ 抗锯齿下要光栅化几十万像素(实测每网格 14ms,
  5 网格约 70ms/帧)。
  改为**包络带一次填充**:逐列记录上/下包络,收段时拼成「上包络正向 + 下包络反向」的
  闭合多边形,用一条 `GraphicsPath` + `FillPath` 绘制(显式 `FillModeWinding`,
  自交包络不会挖洞)。基准测试(1224 列):14.26ms → **1.69ms(约 8.4 倍)**,
  且**保留抗锯齿**、静态帧与拖拽帧完全一致(不是"交互降级")。
  逐像素对比确认观感等价(墨量覆盖率 90.8% vs 91.3%)。
  包络带同时用于静态绘制与 MOVE_SIGNAL 的拖拽虚影。
- **大面积不透明填充改走 GDI**:`UiFillRect` 在面积 ≥ 20000px² 且颜色不透明时使用
  `GetHDC + FillRect`(GDI+ FillRectangle 约 2.2ns/像素,整窗背景 1.7M 像素要 0.9ms、
  面板背景 1.9ms)。半透明填充仍走 GDI+(GDI 不做 alpha 混色);小面积仍走 GDI+
  (避免 GetHDC 固定开销)。顺序安全:GetHDC 会先刷入已排队的 GDI+ 内容。
- **`UiTextWidth` 不再触发 GDI+ 批次刷新**:改为复用 `UiMeasureTextWidth`
  (`GetDC(nullptr)`),`UiEllipsize` 逐字符回退时的十几次刷新被完全消除。

实测(125% 缩放、1725x996、5 信号、2ms 一大格):
  绘图区绘制 47~87ms → **3.6~4.5ms**;整帧最大 196.8ms → **约 17~21ms(≈50~60fps)**,
  与空闲帧基本持平,即"拖拽与不拖拽一样流畅"。

验证(VERY_BIG_DATA.CSV;面板行坐标因新增筛选框整体下移,复选框客户区 (61,250) 起、行距 33):
- 筛选框:输入 `B` 后只剩「编码器B相」「电流钳采集(ABSx10)」,组仍展开;清空即恢复。
- 拆分:合并 3 条信号后右键显示「拆分当前网格(3个信号)」;单信号网格该项置灰无尾缀。
- 光标/中心轴:竖线光标取值与中心值胶囊均按四向越界正确显示三角/圆点。
- 稠密区域:5 信号全选 + 缩放到 2ms 一大格,左键拖拽信号、左键平移、中键平移均流畅;
  渲染观感与修改前等价(叠图对比)。
- make clean && make all 无警告无错误。
2026-10-09 17:07:52 +08:00