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:
1 parent
5c48bf922e
commit
abcf3e6c44
7 files changed
+416
-250
No files matched your search
@@ -17,6 +17,29 @@
|
||||
|
||||
namespace jdi
|
||||
{
|
||||
std::wstring PlotView::PanelTitle(EdgePanelKind kind) const
|
||||
{
|
||||
// ⚠️ 穷举且不写 `default:`(`-Wswitch` 会在新增面板时报警)。
|
||||
switch (kind)
|
||||
{
|
||||
case EdgePanelKind::COUNT:
|
||||
return std::wstring(L"边沿计数") + EdgeRangeSuffix();
|
||||
case EdgePanelKind::CENTER:
|
||||
return std::wstring(L"中心轴边沿计数") + EdgeRangeSuffix();
|
||||
case EdgePanelKind::MEAN:
|
||||
return std::wstring(L"均值滤波");
|
||||
case EdgePanelKind::PEAK:
|
||||
return std::wstring(kWeakPeakTitle);
|
||||
case EdgePanelKind::MARK:
|
||||
return std::wstring(L"自由标记");
|
||||
case EdgePanelKind::RESAMPLE:
|
||||
return std::wstring(kResampleTitle);
|
||||
case EdgePanelKind::CLIP:
|
||||
return std::wstring(kClipTitle);
|
||||
}
|
||||
return std::wstring();
|
||||
}
|
||||
|
||||
float PlotView::EdgePanelWidth(int laneIndex, EdgePanelKind kind) const
|
||||
{
|
||||
if (m_tab == nullptr || m_fonts == nullptr || laneIndex < 0 ||
|
||||
@@ -25,28 +48,18 @@ namespace jdi
|
||||
return kEdgePanelWidth * m_scale;
|
||||
}
|
||||
const Lane& lane = m_tab->Lanes()[static_cast<std::size_t>(laneIndex)];
|
||||
// ⚠️ 排版类别与标题都只从唯一来源取(见 `PanelStyleOf` / `PanelTitle`):
|
||||
// 过去这三份分类布尔量在宽度 / 布局 / 绘制里各写一遍,新增面板漏改一处就错位。
|
||||
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 resamplePanel = (kind == EdgePanelKind::RESAMPLE);
|
||||
const bool clipPanel = (kind == EdgePanelKind::CLIP);
|
||||
// 「均值滤波」「中心轴边沿计数」「弱峰调整」「重采样」「选区截取」= 可滚动的分组列表
|
||||
//(信号色叠底条目栏 + 右侧复选框 + 展开后的参数行);
|
||||
// **「边沿计数」= 表格**(表头:信号名 / 上升沿计数 / 下降沿计数)。
|
||||
// ⚠️ 旧式「逐条目」排版(无滚动的名称+参数行)已彻底删除:上百个信号时它会算出
|
||||
// 上万像素高的面板,超出绘图区后**面板像素缓存一次都存不进去** ⇒ 每帧整块重画。
|
||||
const bool listStyle = (centerAxis || meanFilter || weakPeak || resamplePanel || clipPanel);
|
||||
const bool countTable = (kind == EdgePanelKind::COUNT);
|
||||
const bool listStyle = panelStyle.listRows;
|
||||
const bool countTable = panelStyle.countTable;
|
||||
|
||||
// 标题行 = “GX” 徽标 + 标题(“边沿计数” / “中心轴边沿计数” / “均值滤波” / “弱峰调整”
|
||||
// + 光标区间尾缀)。
|
||||
const std::wstring title =
|
||||
meanFilter ? std::wstring(L"均值滤波")
|
||||
: (weakPeak ? std::wstring(kWeakPeakTitle)
|
||||
: (resamplePanel
|
||||
? std::wstring(kResampleTitle)
|
||||
: ((centerAxis ? L"中心轴边沿计数" : L"边沿计数") +
|
||||
EdgeRangeSuffix())));
|
||||
// 标题行 = “GX” 徽标 + 标题(+ 双光标区间尾缀)。
|
||||
const std::wstring title = PanelTitle(kind);
|
||||
float width = kEdgePanelPad * 2.0f * m_scale + kLaneBadgeWidth * m_scale +
|
||||
8.0f * m_scale + UiMeasureTextWidth(m_fonts->uiSmall, title) +
|
||||
kEdgePanelTitleHeight * m_scale;
|
||||
@@ -214,7 +227,7 @@ namespace jdi
|
||||
width = std::max(width, kEdgePanelPad * 2.0f * m_scale + rowWidth);
|
||||
}
|
||||
|
||||
if (meanFilter || weakPeak || resamplePanel)
|
||||
if (panelStyle.buildButton)
|
||||
{
|
||||
// 底部按钮行 = 「构建为新信号」(均值滤波 / 弱峰调整 / 重采样有),按按钮文字实测宽度确定下限。
|
||||
const float buttonWidth =
|
||||
@@ -330,19 +343,16 @@ namespace jdi
|
||||
return layout;
|
||||
}
|
||||
const Lane& lane = m_tab->Lanes()[static_cast<std::size_t>(laneIndex)];
|
||||
// ⚠️ 排版类别只从唯一来源取(`PanelStyleOf`)—— 过去宽度 / 布局 / 绘制各写一份。
|
||||
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);
|
||||
// 「均值滤波」「中心轴边沿计数」「弱峰调整」「重采样」「选区截取」共用同一套
|
||||
//**可滚动分组列表**排布(信号色叠底条目栏 + 展开后的参数行);
|
||||
//「边沿计数」自 2026-11-02 起走**表格**分支。
|
||||
//⚠️ 「选区截取」的条目**不可展开**(只需右侧复选框),因此只占一行。
|
||||
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;
|
||||
// ⚠️ 面板的「启用 / 展开 / 折起」状态一律通过 `LanePanelEnabled` /
|
||||
// `LanePanelStateOf` 取值(见 `PlotViewInternal.hpp` 的说明):
|
||||
// 旧实现是三条 7 层嵌套三元式,新增面板时极容易漏改或写错括号。
|
||||
@@ -377,7 +387,8 @@ namespace jdi
|
||||
const float pad = kEdgePanelPad * m_scale;
|
||||
// 「中心轴边沿计数」「均值滤波」「弱峰调整」共用同一套**可滚动列表**排布,
|
||||
// 因此不再有独立的底部编辑行;「边沿计数」走独立的**表格**分支。
|
||||
const float buttonHeight = hasButton ? (kEdgePanelButtonHeight * m_scale) : 0.0f;
|
||||
const float buttonHeight =
|
||||
panelStyle.footerButtons() ? (kEdgePanelButtonHeight * m_scale) : 0.0f;
|
||||
// 面板底部(内容区之下)占的纵向空间:有按钮时 = pad + 按钮 + pad,无按钮时只有 pad。
|
||||
// ⚠️⚠️ 「选区截取」的两个按钮(截取为新信号 / 原始信号截取)是**并排在同一行**的
|
||||
// (见下方的几何赋值),因此 footer 只能按**一行**算。旧实现按 `2 * 按钮高 + 6`
|
||||
@@ -660,30 +671,21 @@ namespace jdi
|
||||
const float scrollW = kFreeMarkerScrollWidth * m_scale;
|
||||
const float viewHeight =
|
||||
std::max(0.0f, height - titleHeight - pad - tableHeaderHeight - pad);
|
||||
const bool scrollable = meanContentHeight > viewHeight + 0.5f;
|
||||
const float maxScroll = std::max(0.0f, meanContentHeight - viewHeight);
|
||||
const float scroll = std::max(0.0f, std::min(lane.edgeCountScroll, maxScroll));
|
||||
const float innerWidth =
|
||||
std::max(0.0f, width - pad * 2.0f - (scrollable ? (scrollW + 4.0f * m_scale) : 0.0f));
|
||||
// 滚动几何走唯一实现(见 `PanelScrollMetricsFor`)。
|
||||
const PanelScrollMetrics scrollMetrics =
|
||||
PanelScrollMetricsFor(left, top + titleHeight + pad + tableHeaderHeight, viewHeight,
|
||||
meanContentHeight, LanePanelScrollOf(lane, kind), width - pad * 2.0f,
|
||||
pad, scrollW, m_scale);
|
||||
const float innerWidth = scrollMetrics.innerWidth;
|
||||
layout.tableHeader =
|
||||
gd::RectF(left + pad, top + titleHeight + pad, innerWidth, tableHeaderHeight);
|
||||
layout.content = gd::RectF(left + pad, layout.tableHeader.Y + tableHeaderHeight, innerWidth,
|
||||
viewHeight);
|
||||
layout.markerContentHeight = meanContentHeight;
|
||||
layout.scrollOffset = scroll;
|
||||
if (scrollable)
|
||||
{
|
||||
// 滚动条只占**内容区**(不覆盖固定的表头,表头永远干净可读)。
|
||||
layout.scrollTrack =
|
||||
gd::RectF(left + pad + innerWidth + 4.0f * m_scale, layout.content.Y, scrollW,
|
||||
viewHeight);
|
||||
const float thumbHeight =
|
||||
std::max(18.0f * m_scale, viewHeight * (viewHeight / meanContentHeight));
|
||||
const float thumbTop =
|
||||
layout.content.Y +
|
||||
(viewHeight - thumbHeight) * ((maxScroll > 0.0f) ? (scroll / maxScroll) : 0.0f);
|
||||
layout.scrollThumb = gd::RectF(layout.scrollTrack.X, thumbTop, scrollW, thumbHeight);
|
||||
}
|
||||
layout.scrollOffset = scrollMetrics.scroll;
|
||||
// 滚动条只占**内容区**(不覆盖固定的表头,表头永远干净可读)。
|
||||
layout.scrollTrack = scrollMetrics.track;
|
||||
layout.scrollThumb = scrollMetrics.thumb;
|
||||
// 三列:名称列吃掉剩余宽度,两列计数按内容宽度固定 —— 表头与数据行严格对齐。
|
||||
const float gap = kEdgePanelStatColumnGap * m_scale;
|
||||
const float contentLeft = layout.content.X;
|
||||
@@ -723,7 +725,7 @@ namespace jdi
|
||||
layout.tableColumns[1].width = risingCol;
|
||||
layout.tableColumns[2].left = layout.tableColumns[1].left + risingCol + gap;
|
||||
layout.tableColumns[2].width = fallingCol;
|
||||
float rowY = layout.content.Y - scroll;
|
||||
float rowY = layout.content.Y - scrollMetrics.scroll;
|
||||
for (const SignalRef& ref : lane.signals)
|
||||
{
|
||||
const ResolvedSignal resolved = ResolveSignal(ref);
|
||||
@@ -792,31 +794,20 @@ namespace jdi
|
||||
// 条目区下方与按钮之间留出一片空白(用户 2026-11-03 两次报告)。
|
||||
const float viewHeight =
|
||||
std::max(0.0f, height - titleHeight - pad - bottomPad - footerHeight);
|
||||
const bool scrollable = meanContentHeight > viewHeight + 0.5f;
|
||||
const float maxScroll = std::max(0.0f, meanContentHeight - viewHeight);
|
||||
const float storedScroll = meanFilter ? lane.meanFilterScroll
|
||||
: (weakPeak ? lane.weakPeakScroll
|
||||
: (resamplePanel ? lane.resampleScroll
|
||||
: (clipPanel ? lane.clipScroll
|
||||
: lane.edgeCenterScroll)));
|
||||
const float scroll = std::max(0.0f, std::min(storedScroll, maxScroll));
|
||||
const float innerWidth =
|
||||
std::max(0.0f, width - pad * 2.0f - (scrollable ? (scrollW + 4.0f * m_scale) : 0.0f));
|
||||
// ⚠️ 滚动量一律走 `LanePanelScrollOf(lane, kind)`(唯一派发点):
|
||||
// 过去这里是一条 5 层三元式(均值/弱峰/重采样/截取/中心轴),
|
||||
// 而「边沿计数」不在其中 —— 它与 `LanePanelScrollOf` 各记一份映射,
|
||||
// 新增面板时极易只改一处(滚轮能滚、拖动滚动条滚不动之类的错位)。
|
||||
const PanelScrollMetrics scrollMetrics =
|
||||
PanelScrollMetricsFor(left, top + titleHeight + pad, viewHeight, meanContentHeight,
|
||||
LanePanelScrollOf(lane, kind), width - pad * 2.0f, pad, scrollW,
|
||||
m_scale);
|
||||
const float innerWidth = scrollMetrics.innerWidth;
|
||||
layout.content = gd::RectF(left + pad, top + titleHeight + pad, innerWidth, viewHeight);
|
||||
layout.markerContentHeight = meanContentHeight;
|
||||
layout.scrollOffset = scroll;
|
||||
if (scrollable)
|
||||
{
|
||||
layout.scrollTrack =
|
||||
gd::RectF(left + pad + innerWidth + 4.0f * m_scale, layout.content.Y, scrollW,
|
||||
viewHeight);
|
||||
const float thumbHeight =
|
||||
std::max(18.0f * m_scale, viewHeight * (viewHeight / meanContentHeight));
|
||||
const float thumbTop =
|
||||
layout.content.Y +
|
||||
(viewHeight - thumbHeight) * ((maxScroll > 0.0f) ? (scroll / maxScroll) : 0.0f);
|
||||
layout.scrollThumb = gd::RectF(layout.scrollTrack.X, thumbTop, scrollW, thumbHeight);
|
||||
}
|
||||
layout.scrollOffset = scrollMetrics.scroll;
|
||||
layout.scrollTrack = scrollMetrics.track;
|
||||
layout.scrollThumb = scrollMetrics.thumb;
|
||||
|
||||
// 两行滑块的列宽(与 `EdgePanelWidth` 同一组算式 ⇒ 布局与宽度不会漂移)。
|
||||
const float labelColumn =
|
||||
@@ -846,7 +837,7 @@ namespace jdi
|
||||
std::max(10.0f * m_scale,
|
||||
(resetLeft - resetGap - valueColumn - 6.0f * m_scale) - trackLeft);
|
||||
|
||||
float rowY = layout.content.Y - scroll;
|
||||
float rowY = layout.content.Y - scrollMetrics.scroll;
|
||||
// ⚠️ 「选区截取」在**未启用双光标**时不生成任何信号行:面板只显示一句居中提示
|
||||
// (用户 2026-11-03 明确要求:不应显示信号名条目、更不应能滚动)。
|
||||
const bool clipRowsVisible = !clipPanel || clipReadyHere;
|
||||
@@ -1065,26 +1056,18 @@ namespace jdi
|
||||
{
|
||||
totalContent += groupRow;
|
||||
}
|
||||
const bool scrollable = totalContent > viewHeight + 0.5f;
|
||||
const float maxScroll = std::max(0.0f, totalContent - viewHeight);
|
||||
const float scroll = std::max(0.0f, std::min(lane.freeMarkerScroll, maxScroll));
|
||||
const float innerWidth =
|
||||
std::max(0.0f, width - pad * 2.0f - (scrollable ? (scrollW + 4.0f * m_scale) : 0.0f));
|
||||
// 滚动几何走唯一实现(与表格 / 列表分支同一套算式)。
|
||||
const PanelScrollMetrics scrollMetrics =
|
||||
PanelScrollMetricsFor(left, top + titleHeight + pad, viewHeight, totalContent,
|
||||
LanePanelScrollOf(lane, kind), width - pad * 2.0f, pad, scrollW,
|
||||
m_scale);
|
||||
const float innerWidth = scrollMetrics.innerWidth;
|
||||
layout.content =
|
||||
gd::RectF(left + pad, top + titleHeight + pad, innerWidth, viewHeight);
|
||||
layout.markerContentHeight = totalContent;
|
||||
if (scrollable)
|
||||
{
|
||||
layout.scrollTrack =
|
||||
gd::RectF(left + pad + innerWidth + 4.0f * m_scale, layout.content.Y, scrollW,
|
||||
viewHeight);
|
||||
const float thumbHeight =
|
||||
std::max(18.0f * m_scale, viewHeight * (viewHeight / totalContent));
|
||||
const float thumbTop =
|
||||
layout.content.Y +
|
||||
(viewHeight - thumbHeight) * ((maxScroll > 0.0f) ? (scroll / maxScroll) : 0.0f);
|
||||
layout.scrollThumb = gd::RectF(layout.scrollTrack.X, thumbTop, scrollW, thumbHeight);
|
||||
}
|
||||
layout.scrollTrack = scrollMetrics.track;
|
||||
layout.scrollThumb = scrollMetrics.thumb;
|
||||
const float scroll = scrollMetrics.scroll;
|
||||
|
||||
// 六列(① 序号 ② 标记值 ③ 枚举文本 ④ 增量 ⑤ 时间点 ⑥ 时间间隔)
|
||||
// 从左往右排布:每一行的同类信息都从同一横坐标开始 ⇒ 纵向上严格对齐(用户要求)。
|
||||
|
||||
Reference in new issue
Block a user