refactor(panels): 浮动面板抽出唯一"面板特征层",消除 3 处分类/滚动几何/滚动设置的重复

背景(用户观察):7 个实验性功能都有自己的浮动面板,观感与骨架高度一致
(标题栏 = 折起三角 + GX 徽标 + 标题 + ✕,内容区可滚动 + 滚动条,底部按钮行),
但每新增一种面板或改动布局,总会出"意料之外"的缺陷。

审计结论:骨架绘制本来只有一份,真正的问题是**同一套"按种类分支"的代码被抄了多份**:

| 重复项 | 位置 | 份数 |
| --- | --- | --- |
| 分类布尔量(listStyle / countTable / hasButton…) | EdgePanelWidth / EdgePanelLayoutFor / PaintEdgeCursor | 3 |
| 滚动条几何 + 滚动量夹取 | 布局里表格 / 列表 / 自由标记三个分支 | 3 |
| 滚动条绘制(6 行) | 绘制里同样三个分支 | 3 |
| SetLane*Scroll 实现 | PlotViewFeatures.cpp | 7 |
| 按种类转发滚动写入 | 滚轮 / 拖滚动条 | 2 |
| 标题文本 | EdgePanelWidth 的三元式链 / 绘制里的三元式链 | 2(内容还不一致!) |

一、新增"面板特征层"(PlotViewInternal.hpp,pvdetail)
* `PanelStyle PanelStyleOf(EdgePanelKind)`:唯一分类来源,`listRows` / `countTable` /
  `markerGroups` + `buildButton` / `twoButtons` / `markerFooter`,`switch` 穷举不写 default
  (新增面板漏登记 ⇒ `-Wswitch` 编译报警)。
  其中专门区分 `footerButtons()`(底部有按钮行)与 `buildButton`(是"构建为新信号")——
  混为一谈会让「选区截取」底部少预留一行高度(本轮实测 124 → 87 px,被像素基线当场抓住)。
* `PanelScrollMetricsFor(...)`:滚动条轨道/滑块几何 + 滚动量夹取 + 内容宽度扣减的唯一实现。
* `PlotView::PanelTitle(EdgePanelKind)`:标题唯一来源。
  ⚠️ 修掉一处**潜伏**错误:`EdgePanelWidth` 的标题链漏了 MARK/CLIP(两者按"边沿计数"量宽),
  目前两种标题恰好等宽所以看不出来,但任何文案改动都会让宽度与标题不匹配。

二、收敛调用点
* `EdgePanelWidth` / `EdgePanelLayoutFor`(表格 / 列表 / 自由标记三分支)/ `PaintEdgeCursor`
  全部改用 `PanelStyleOf` 与 `PanelTitle`;滚动几何改用 `PanelScrollMetricsFor`。
* `EdgePanelLayoutFor` 的 5 层滚动量三元式 → `LanePanelScrollOf(lane, kind)`(该派发点第 33 轮
  就已存在,但布局这里一直没用它)。
* `PaintEdgeCursor` 的滚动条绘制抽成 `paintPanelScrollBar(layout)`,三处调用。
* 7 个 `SetLane*Scroll` 改为一行转发到新的 `SetLanePanelScroll(laneIndex, kind, scroll)`;
  滚轮与"拖滚动条"两条 `switch`/`if` 链改为直接调它(读与写从此同一个入口)。

三、新增离屏检查 `panelscroll`(护栏)
* 对 7 种面板逐一:可滚动时滚到底 / 滚回顶,断言 `scrollOffset` 夹取到 `maxScroll`、
  滑块到达/返回轨道端点、内容区几何不随滚动改变;不可滚动时断言不出现滚动条。
* 用 `MF4-Record.MF4`(566 信号合并到同一网格)实测 6/7 面板可滚(自由标记无固定点,正确)。
  没有这条检查,本次收敛就等于"改完只能靠肉眼"。

验证(本机 GCC 15.2.0 UCRT / 125% DPI)
* `panels -mode compare`:7 个面板的**像素哈希与几何逐项不变**(零差异)。
  这条检查当场抓到了上面 `footerButtons` 的回归,证明基线有效。
* `OffscreenChecks -all`:6 个检查全 PASS,`checks=87 failed=0`。
* `UnitTests`:`checks=219 failed=0`。
* release 全量零 error / 零 warning;产物 788,480 → **787,968 B**(−512)。
This commit is contained in:
DeepSeek V4.1 Flash committed 2026-11-04 14:20:57 +08:00
1 parent 5c48bf922e
commit abcf3e6c44
7 files changed
+416 -250

No files matched your search

+21 -28
View File
@@ -4892,19 +4892,30 @@ namespace jdi
//(`baseLayer + 1 + 面板序数`,面板序数按 `m_panelOrder` 底 → 顶)。
// 少了这一步,下层面板的文字框会记在比上层面板更低的层里被反复挖洞/穿透。
UiSetTextLayer(panelTextLayer + static_cast<int>(panelOrdinal));
// ⚠️ 排版类别只从唯一来源取(`PanelStyleOf`):过去宽度 / 布局 / 本函数各写一份
// 6 个 `kind == XXX` + 3 条组合判断,新增面板漏改一处就错位(见该函数注释)。
const PanelStyle panelStyle = PanelStyleOf(kind);
const bool centerAxis = (kind == EdgePanelKind::CENTER);
const bool meanFilter = (kind == EdgePanelKind::MEAN);
const bool weakPeak = (kind == EdgePanelKind::PEAK);
const bool freeMarker = (kind == EdgePanelKind::MARK);
const bool resamplePanel = (kind == EdgePanelKind::RESAMPLE);
const bool clipPanel = (kind == EdgePanelKind::CLIP);
// ⚠️ 「均值滤波」「中心轴边沿计数」「弱峰调整」「重采样」「选区截取」= 可滚动的分组列表
//(条目栏 + 展开后的参数行);
// **「边沿计数」= 表格**(表头:信号名 / 上升沿计数 / 下降沿计数),单独一支。
// 它们都**不能**再走旧的逐条目分支:那个分支没有任何裁切,滚动时会画到标题栏上。
const bool listStyle = (meanFilter || centerAxis || weakPeak || resamplePanel || clipPanel);
const bool countTable = (kind == EdgePanelKind::COUNT);
const bool hasButton = (meanFilter || weakPeak || resamplePanel || clipPanel);
const bool listStyle = panelStyle.listRows;
const bool countTable = panelStyle.countTable;
const bool hasButton = panelStyle.buildButton;
// 内容区滚动条:三个排版分支(表格 / 列表 / 自由标记)共用同一段绘制。
// ⚠️ 过去这段 6 行代码在三个分支里各抄一份,任何外观微调(颜色 / 圆角)都要改三处。
const auto paintPanelScrollBar = [this, &g](const EdgePanelLayout& panelLayout) {
if (panelLayout.scrollTrack.Width <= 0.0f)
{
return;
}
UiFillRoundRect(g, panelLayout.scrollTrack, panelLayout.scrollTrack.Width * 0.5f,
WithAlpha(m_theme->border, 0x88));
UiFillRoundRect(g, panelLayout.scrollThumb, panelLayout.scrollThumb.Width * 0.5f,
m_theme->borderStrong);
};
{
if (index >= count)
{
@@ -5230,13 +5241,7 @@ namespace jdi
g.Restore(tableSave);
// 内容滚动条(只在内容超高时出现;表头不参与滚动)。
if (layout.scrollTrack.Width > 0.0f)
{
UiFillRoundRect(g, layout.scrollTrack, layout.scrollTrack.Width * 0.5f,
WithAlpha(m_theme->border, 0x88));
UiFillRoundRect(g, layout.scrollThumb, layout.scrollThumb.Width * 0.5f,
m_theme->borderStrong);
}
paintPanelScrollBar(layout);
}
// 「均值滤波」面板:逐信号的**信号色叠底标题栏**(左侧展开三角 + 信号名 +
@@ -5757,13 +5762,7 @@ namespace jdi
// ⚠️ 必须在 `g.Restore(meanSave)` **之后**画:滚动条位于内容区右侧、在
// 内容裁剪区之外,若在裁剪内画会被整条裁掉(用户 2026-10-30 报告
// "全部启用后没有出现滚动条"的根因)。
if (layout.scrollTrack.Width > 0.0f)
{
UiFillRoundRect(g, layout.scrollTrack, layout.scrollTrack.Width * 0.5f,
WithAlpha(m_theme->border, 0x88));
UiFillRoundRect(g, layout.scrollThumb, layout.scrollThumb.Width * 0.5f,
m_theme->borderStrong);
}
paintPanelScrollBar(layout);
}
// 「自由标记」面板:按信号分组的**分组栏**(信号色托底 + 三角 + 名称 +
// 「N 标记点」+ ✕),下属固定点条目六列对齐(序号 / 取值 / 枚举 / 增量 / 时刻 / 间隔),
@@ -6073,13 +6072,7 @@ namespace jdi
g.Restore(contentState);
// ---- 内容滚动条(只在内容超高时出现;按钮与滑块不参与滚动)----
if (layout.scrollTrack.Width > 0.0f)
{
UiFillRoundRect(g, layout.scrollTrack, layout.scrollTrack.Width * 0.5f,
WithAlpha(m_theme->border, 0x88));
UiFillRoundRect(g, layout.scrollThumb, layout.scrollThumb.Width * 0.5f,
m_theme->borderStrong);
}
paintPanelScrollBar(layout);
// ---- 「标记透明度」滑动条(固定在「自由标记点选取」按钮上方)----
if (layout.opacityTrack.Width > 0.0f)