均值面板越界根因 + 网格线进入主题配色 + 条目栏悬停/整行点击 + 均值点数下限 1

1. 「元素超出浮动面板」真根因(用户 2026-10-30 报告的截图)
   - 绘制侧 PaintEdgeCursor 的 itemized 仍是 (centerAxis || meanFilter || weakPeak),
     而布局侧 EdgePanelLayoutFor 早已是 (centerAxis || weakPeak)。
     于是均值滤波面板被旧的「逐条目」分支**再画一遍**(每行名称 + 复选框,且没有任何裁切),
     滚动后那些行的坐标是负值/越界 ⇒ 直接画到标题栏上。
   - 修法:绘制侧与布局侧统一;旧分支的兜底循环跳过条件补 meanFilter。
     底部「构建为新信号」由共享的 if (hasButton) 绘制,不受影响。
   - 顺带删除「只画完全落在内容区里的行」的 fullyInside 门控:真因修掉后
     g.SetClip(layout.content) 已足够,半截行正常显示,滚动观感连续。

2. 条目栏交互(与「自由标记」分组栏一致)
   - 整行可点:复选框区 = 切换是否预览;已启用 = 展开/收起参数行;未启用 = 直接开始预览。
     命中只要求点落在 layout.content 内(不再要求整行在内容区内),因此半截行也能操作。
   - 悬停行 → 对应预览曲线高亮:新增 m_meanRowHoverLane / m_meanRowHoverRef,
     由 OnMouseMove 维护、OnMouseLeave 与 InvalidateDerivedCaches 清除;
     PaintDerivedCurve/PaintMeanFilterCurve/PaintWeakPeakCurve 新增 bool highlight,
     先画 +5px 半透明同色光晕再把线加粗,行头叠一层 accent;悬停态变化会 InvalidateSceneCache。

3. 均值点数下限 1(用户要求:不可以为 0)
   - kMeanFilterPointsMin 0 → 1;输入提交用 centerTarget ? 0 : kMeanFilterPointsMin
     (中心轴边沿计数沿用旧语义 0 = 不平滑);预览与构建请求都再夹一次下限,
     即使条目被外部写成 0,渲染/构建结果与 1 点逐位一致。

4. 网格线颜色成为主题配色的一部分
   - struct Theme 新增 laneBg(网格叠底色);NormalizePlotContrast → FinalizeTheme,
     先解 laneBg = EnsureContrast(plotBg, cardBg, 1.03),再让 gridWeak/gridStrong/
     axisLine/borderStrong 统一对 laneBg 归一(1.30 / 1.70 / 2.10 / 1.70)。
   - 四个主题各自给出显式取值:Dark 的网格线对叠底色原本只有 1.071(几乎不可见)⇒ 现 ≥1.30;
     其它三个主题同步微调,网格可见性一致。ThemeLaneBackground 返回 laneBg。

5. 行三角尺寸统一
   - 新增 kEdgePanelRowTriangleSize = 12 逻辑像素,均值滤波行与自由标记分组栏共用;
     标题栏的 kEdgePanelDisclosureWidth = 20 保持不变。

6. 均值面板滚动条与文本
   - 滚动条必须在 g.Restore(meanSave) 之后绘制(它在内容裁剪区之外,裁剪内画会被整条裁掉)。
   - 滑块标签/数值改用 UiTextClipped(..., layout.content)(GDI 文本不受 GDI+ 裁剪约束)。

验证(全部离屏)
   uicheck 69/69、lbcheck 21/21、lbresid 5/5、lbzorder 4/4、lbcovr 7/7、lbpanel 10/10、
   derivecheck 16/16、round15check 28/28、themecheck 17/17、.preflight/meancheck 17/17;
   .preflight/meanpix:滚到顶/中/底时标题栏与面板外均为 0 像素差异,内容区正常滚动;
   rendercheck:A–H 八个场景哈希与基线完全一致;
   make release 零 warning(805,376 B);make debug 零 warning。

同步文档:Documents/agent_mem.md 追加「2026-10-30 第十七轮」,
并把第十六轮里「均值点数(0…上限)」「命中要求行完全落在内容区里」两条改成现行做法。
This commit is contained in:
DeepSeek V4.1 Flash committed 2026-10-30 10:51:39 +08:00
1 parent bdd703b88b
commit 2d83b494a3
9 files changed
+396 -100

No files matched your search

+92 -2
View File
@@ -8687,13 +8687,15 @@ Solarised Light 的 `0xFFFDF6E3`,这是"换深色主题后预览曲线永远
* 条目 = **信号色叠底标题栏**(左侧展开三角 + 名称 + 右端**复选框**)。勾选前只有标题栏;
勾选后出现三角与两行滑块,三角可收起参数行。复选框自绘(对比色描边/填充),
主题色在信号色叠底上不可辨。
* 两行滑块:`均值点数`(0…上限)与 `迭代次数`(1…上限),**标签不带 X / N**;
* 两行滑块:`均值点数`(1…上限,**2026-10-30 第十七轮把下限从 0 改为 1**)与
`迭代次数`(1…上限),**标签不带 X / N**;
右侧数值可点击输入**滑块上限**(沿用"输入上限(当前N)"提示与 `EditFilter::BYTE`),
最右侧是复位按钮(点数 → 5 且上限 → 50;迭代 → 1 且上限 → 10)。
新增 `Lane::MeanFilterItem::{maxPoints, maxIterations, collapsed}`、`Lane::meanFilterScroll`。
* 内容区超高时**滚动**(`layout.content` + 滚动条 + 滚轮),底部「构建为新信号」按钮固定。
面板高度上限 = 绘图区可用高度 ⇒ 上百信号合并时不再顶出屏幕。
* 命中要求"行**完全落在内容区**里",避免滚动出的行抢底部按钮的点击。
* 命中:只要**点击/悬停点落在内容区内**即算命中(行可以是"半截"显示的,与绘制一致);
第十七轮已删掉旧的"行必须完全落在内容区内"限制(它造成"看得见却点不到")。
* 缓存键:`PanelSlotKey` 增补 `rowHeaders / rowTriangles / scrollOffset / MarkerGroup::check /
meanItem->maxPoints / maxIterations / collapsed`;`meanFilterScroll` **只进槽键**(不进全局键,
否则滚动一个面板会让所有面板重画)。
@@ -8764,3 +8766,91 @@ make debug -> 零 error 零 warning
`PlotView::` 前缀。
* 探针的工程根目录推导:探针若放在 `.preflight\xxx.exe`,从 exe 路径**上溯 2 级**即根目录
(上溯 3 级会跑到桌面上,`TestData` 找不到)。
---
## 2026-10-30 第十七轮(本机):均值面板越界根因 + 网格线进入主题配色 + 悬停/整行点击 + 点数下限 1
### 1. 「元素超出浮动面板」的真正根因(**最重要,别再误判**)
**症状**:均值滤波面板内容超高、滚到底时,**标题栏上**出现一条信号名 + 一个蓝色复选框(无信号色底)。
**曾经的错误判断**:以为是「GDI+ `SetClip` 在滚动面板里不可靠」,于是加了「只画完全落在
`layout.content` 里的行」的双保险 —— 完全无效(像素差 789/1808 **一位都没变**)。
**真因**:`PaintEdgeCursor` 里绘制侧的 `itemized` 判定仍为
`(centerAxis || meanFilter || weakPeak)`,而布局侧的 `EdgePanelLayoutFor` 早已改为
`(centerAxis || weakPeak)`(均值滤波有自己的可滚动分支)。于是**均值面板被旧「逐条目」分支
又画了一遍**:它给每行画 `名称 + 复选框`,**没有任何裁切**,行按 `layout.rowHeaders` 的坐标画
(滚动后就是负的/越界的 Y)⇒ 直接画到标题栏上。
**修法**:
* 绘制侧 `itemized = (centerAxis || weakPeak)`(与布局侧统一);
* 旧「逐条目」兜底循环的跳过条件补上 `meanFilter`(否则均值面板会被当成普通面板画两行文字);
* 底部「构建为新信号」按钮由**共享的 `if (hasButton)`** 绘制,不受影响。
**教训**:同一语义的标志位在「布局」和「绘制」两处各有一份时,改一处必须同步另一处;
出现"画出来的东西比布局多"时,先怀疑**有第二个绘制分支**,而不是怀疑裁剪 API。
### 2. 顺带恢复:半截行正常显示
真因修掉后 `g.SetClip(layout.content)` 是够用的,于是删除了「`fullyInside` 门控」
(行必须整行落在内容区)。现在滚动时行可以**半截**显示,滚动观感连续、不再跳空档。
探针 `meanpix`:`titleDiff(top vs mid)=0`、`titleDiff(mid vs max)=0`、`outsideDiff=0`(面板外 0 差异),
而 `contentDiff` 仍然很大(内容确实在滚动)。
### 3. 条目栏交互:整行点击 + 悬停高亮预览曲线
* **整行可点**(不再只能点 12px 的三角):点击落在右端复选框上 → 切换"是否预览";
已启用 → 切换参数行的收起/展开;尚未启用 → 直接开始预览。
* **悬停行 → 对应预览曲线高亮**:`m_meanRowHoverLane / m_meanRowHoverRef` 由 `OnMouseMove` 维护,
`OnMouseLeave` / `InvalidateDerivedCaches` 清除;绘制侧
`PaintDerivedCurve(..., bool highlight)` 先画 `+5px` 半透明同色光晕、再把线加粗 `+1.6px`,
并在行头叠一层 `accent 0x2E`。
* 悬停状态变化会 `InvalidateSceneCache()`(派生曲线属于"静态场景",否则高亮要等下一次重绘)。
* 命中不再要求行"完全在内容区内"(见上),但**命中点本身必须落在 `layout.content` 里**,
所以不会抢底部按钮的点击,也不会出现"看不见却能点到"的热区。
### 4. 均值点数下限 1(用户明确要求:不可以为 0)
* `kMeanFilterPointsMin` 由 **0 → 1**(`PlotViewInternal.hpp`);
* 输入提交路径:`pointsMin = centerTarget ? 0 : kMeanFilterPointsMin`
(中心轴边沿计数沿用旧语义 0 = 不平滑,不跟着改);
* 预览:`PaintMeanFilterCurve` 内 `points/iterations` 再夹一次下限 ⇒ 即使条目被外部写成 0,
画出来也与 1 点完全一致(探针断言"0 与 1 的整帧像素 0 差异");
* 构建请求 `MeanFilterBuild.points` 同样夹到下限 ⇒ 预览与「构建为新信号」仍逐位一致。
### 5. 网格线颜色成为主题配色的一部分
* `struct Theme` 新增 `laneBg`(网格叠底色,= 绘图区在该主题下的真实底色)。
* 全部结构性颜色(`gridWeak / gridStrong / axisLine / borderStrong`)统一**对 `laneBg`**
做 WCAG 对比度归一(`FinalizeTheme`,契约:gridWeak ≥1.30、gridStrong ≥1.70、
axisLine ≥2.10、borderStrong ≥1.70),并给出每个主题的显式取值。
* Dark 主题原本 gridWeak 对 lane 只有 **1.071**(几乎不可见)⇒ 现改为 ≥1.30;
其它主题同步微调,四个主题的网格都清晰可辨。
### 6. 验证
```
uicheck 69/69 lbcheck 21/21 lbresid 5/5 lbzorder 4/4
lbcovr 7/7 lbpanel 10/10 derivecheck 16/16
round15check 28/28 themecheck 17/17
meancheck 17/17 (含:滚动条确实被绘制、三角 12 逻辑像素、整行点击可反复切换、
悬停记录/清除、点数 0 与 1 等价、同状态两帧逐像素一致)
meanpix 标题栏 / 面板外:滚动前后 0 像素差异
rendercheck 8 个场景哈希与基线完全一致(A…H 未变)
make release -> 零 warning(805,376 B);make debug -> 零 warning
```
### 7. 踩坑(本轮新增)
* ⚠️⚠️ **探针 `build.ps1` 只编译 `main.cpp`,链接的是 `Build/Release/*.o`**:
改完工程源码后只重建探针 ⇒ 探针仍在跑**旧目标文件**,诊断打印"凭空消失"、
修复"看起来无效"。**先 `make release` 再重建探针**,并且**不要用
`Select-String error` 过滤 `make` 输出后忽略非零退出码**(本轮就因为一次
`const gd::RectF& header` 重复声明导致 `make` 失败而被过滤掉,白查了半小时)。
* ⚠️ 探针里断言"某行已启用"前要确认条目真的存在:`SetLaneMeanFilter(lane,true)` 会**自动启用
第一个信号**,再"全量切换"会把它关掉 ⇒ 出现 39/40 这种偏差,断言就会落在"未启用的行"上,
而“未启用的行单击 = 开始预览”本来就不切换 `collapsed`。
* ⚠️ 异步派生曲线(大信号走后台线程)让"改参数 → 立刻截图比对"变得不稳定:
每个状态**连渲染两帧**再取像素,才是稳定态。