预览曲线按面板高亮 + 统计数字不再卡在“统计中…” + 中心轴面板改表格 + 默认主题 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
This commit is contained in:
DeepSeek V4.1 Flash committed 2026-11-02 15:22:26 +08:00
1 parent 7a6fcd68e2
commit 8de8a8fb85
14 files changed
+934 -141

No files matched your search

+365 -43
View File
@@ -745,6 +745,13 @@ namespace jdi
boxes.Add(layout.edit, 3);
boxes.Add(layout.button, 3);
boxes.Add(layout.content, 3);
boxes.Add(layout.tableHeader, 3);
for (const EdgePanelLayout::TableColumn& column : layout.tableColumns)
{
boxes.Add(gd::RectF(column.left, layout.tableHeader.Y, column.width,
layout.tableHeader.Height),
4);
}
boxes.Add(layout.scrollTrack, 3);
boxes.Add(layout.scrollThumb, 4);
boxes.Add(layout.opacityTrack, 3);
@@ -783,11 +790,24 @@ namespace jdi
}
// 收尾:解除面板裁剪并**复位层号**(宿主后续组件依赖它)。
// 浮动面板之上的叠加层(滚动条 / 回到最上方 / 内联输入框)必须比**最上面板**再高一层。
// ⚠️ **只在它们真的会被绘制时上报**(用户 2026-11-02 报告:关闭分组后仍能看到大量
// 残留的布局边界框):这些矩形在 `Layout()` 里就无条件算好,但绘制时会按可见性 /
// 编辑态门控。旧实现照样上报 ⇒ 空绘图区里同时出现“不存在的滚动条 / 回到最上方 /
// 输入框”的框。
boxes.SetClip(gd::RectF());
boxes.SetLayer(topOverlayLayer);
boxes.Add(m_scrollBarRect, 5);
boxes.Add(BackToTopRect(), 5);
boxes.Add(EditFieldRect(), 5);
if (m_scrollBarVisible)
{
boxes.Add(m_scrollBarRect, 5);
}
if (BackToTopVisible())
{
boxes.Add(BackToTopRect(), 5);
}
if (m_editTarget != PlotEditTarget::NONE)
{
boxes.Add(EditFieldRect(), 5);
}
boxes.SetLayer(baseLayer);
}
@@ -855,6 +875,19 @@ namespace jdi
if (m_lanes.empty())
{
InvalidateSceneCache();
// ⚠️⚠️ **必须清掉上一帧登记的叠加层/命中几何**(用户 2026-11-02 报告:关闭整个分组后
// 仍能看到大量残留的布局边界框):本分支直接 return,`PaintLaneLegend` /
// `PaintCursor` / `PaintFreeMarker` 都不会跑,于是 `m_legendHits` /
// `m_cursorLabelRects` / `m_cursorSpanRect` / `m_freeMarkerHits` 里的**旧矩形**
// 会一直保留(布局边界会继续把它们画出来,甚至命中测试也会命中已不存在的元素)。
m_legendHits.clear();
m_cursorLabelRects.clear();
m_cursorSpanRect = gd::RectF();
m_freeMarkerHits.clear();
m_decorRects.clear();
m_panelOrder.clear();
m_panelSlots.clear();
m_panelValid = false;
PaintEmptyState(g);
// ⚠️ 空状态(尚未勾选任何信号)也必须绘制提示浮层:加载失败 / 解析异常正是
// **最需要**非模态提示的场景,若在此提前返回,宿主的错误信息将永远不可见。
@@ -2074,7 +2107,12 @@ namespace jdi
const int saveState = g.Save();
g.SetClip(lane.rect, gd::CombineModeIntersect);
// 面板条目栏的悬停高亮(用户 2026-10-30 要求):鼠标停在哪一行,就把那条**预览曲线**
// 画一圈光晕 + 加粗。`m_meanRowHoverLane/Ref` 由 `OnMouseMove` 维护。
// 画一圈光晕 + 加粗。`m_meanRowHoverLane/Ref/Kind` 由 `OnMouseMove` 维护。
//
// ⚠️⚠️ **必须按面板种类分别判定**(用户 2026-11-02 报告):三个面板(均值滤波 /
// 中心轴边沿计数 / 弱峰调整)都有“信号条目栏”,同一条信号可能同时在三个面板里各有一行。
// 旧实现只给 MEAN 分支传 `highlight` ⇒ 只开弱峰 + 中心轴时悬停毫无反应;三个全开时
// 悬停任何一个面板都只高亮均值滤波那条。现在只高亮“鼠标所在面板”对应的曲线。
const bool hoverActive = (m_meanRowHoverLane == laneIndex) && m_meanRowHoverRef.IsValid();
for (const SignalRef& ref : tabLane.signals)
{
@@ -2083,13 +2121,16 @@ namespace jdi
{
continue;
}
const bool hovered = hoverActive && (ref == m_meanRowHoverRef);
const bool hoveredThisRow = hoverActive && (ref == m_meanRowHoverRef);
if (centerOn)
{
const Lane::EdgeCenterItem* centerItem = tabLane.FindEdgeCenterItem(ref);
if (centerItem != nullptr)
{
PaintMeanFilterCurve(g, resolved, lane, centerItem->points, centerItem->iterations);
const bool highlight =
hoveredThisRow && (m_meanRowHoverKind == EdgePanelKind::CENTER);
PaintMeanFilterCurve(g, resolved, lane, centerItem->points, centerItem->iterations,
highlight);
}
}
if (meanFilterOn)
@@ -2097,7 +2138,9 @@ namespace jdi
const Lane::MeanFilterItem* item = tabLane.FindMeanFilterItem(ref);
if (item != nullptr)
{
PaintMeanFilterCurve(g, resolved, lane, item->points, item->iterations, hovered);
const bool highlight =
hoveredThisRow && (m_meanRowHoverKind == EdgePanelKind::MEAN);
PaintMeanFilterCurve(g, resolved, lane, item->points, item->iterations, highlight);
}
}
if (weakPeakOn)
@@ -2111,7 +2154,9 @@ namespace jdi
const int boostIterations = std::max(
kWeakPeakIterationsMin,
std::min(kWeakPeakIterationsMaxLimit, item->iterations));
PaintWeakPeakCurve(g, resolved, lane, amountPercent, boostIterations, hovered);
const bool highlight =
hoveredThisRow && (m_meanRowHoverKind == EdgePanelKind::PEAK);
PaintWeakPeakCurve(g, resolved, lane, amountPercent, boostIterations, highlight);
}
}
}
@@ -3500,6 +3545,9 @@ namespace jdi
// 这些叠加层都画在**面板之下**、面板本身不透明且贴回时只贴圆角形状之内,因此交互期间
// 直接复用面板像素是安全的;鼠标停下后键自动恢复,面板重画一次即可。
const bool interactivePanelKey = InputIsInteractive();
// 供宿主判断“冻结过后需要补一帧”(冻结期间面板显示的是旧像素,鼠标停下后
// 不会再有重绘请求 ⇒ 统计数字会永远停在“统计中…”)。
m_panelKeysFrozen = interactivePanelKey;
if (interactivePanelKey)
{
mix(0x9E3779B9ull);
@@ -3534,6 +3582,9 @@ namespace jdi
// “鼠标处于条目栏上方时对应曲线/条目需要高亮提示”)。
mix(static_cast<std::uint64_t>(m_meanRowHoverLane + 2));
mixRef(m_meanRowHoverRef);
// 悬停所在的**面板种类**同样改变面板像素:同一条信号可能在多个面板里各有一行,
// 只有“这一行属于本面板”时该面板才叠强调色(否则悬停一个面板会把其它面板的同名行一起点亮)。
mix(static_cast<std::uint64_t>(m_meanRowHoverKind) + 2);
// 时间光标与统计区间:面板标题的时间后缀、以及「中心轴」行的上升/下降沿数字都依赖它们。
// ⚠️⚠️ **交互期间必须用固定占位值**:拖动光标时时间每帧都在变,键一变就要重画全部面板。
@@ -3665,6 +3716,12 @@ namespace jdi
mixRect(layout.showDelta);
mixRect(layout.showEnum);
mixRect(layout.content);
mixRect(layout.tableHeader);
for (const EdgePanelLayout::TableColumn& column : layout.tableColumns)
{
mixFloat(column.left);
mixFloat(column.width);
}
mixRect(layout.scrollTrack);
mixRect(layout.scrollThumb);
mixRect(layout.opacityTrack);
@@ -3817,12 +3874,12 @@ namespace jdi
}
else if (kind == EdgePanelKind::CENTER && !interactiveKey)
{
// 「中心轴边沿计数」是**表格**:每个“已勾选统计”的行都会显示两列数字,
// 因此 `rowRefs` 里凡是 `FindEdgeCenterItem != nullptr` 的都要把数字混进键
//(⚠️ 表格没有 `rowStats` 了,不能再按它门控 —— 否则数字变化不会让键变化,
// 面板会永远停在“统计中…”)。
for (std::size_t rowIndex = 0; rowIndex < layout.rowRefs.size(); ++rowIndex)
{
if (rowIndex >= layout.rowStats.size() || layout.rowStats[rowIndex].Width <= 0.0f)
{
continue; // 与绘制时的条件一致:只有这一排才显示统计数字
}
const SignalRef& ref = layout.rowRefs[rowIndex];
const ResolvedSignal resolved = ResolveSignal(ref);
const Lane::EdgeCenterItem* item = lane.FindEdgeCenterItem(ref);
@@ -4484,12 +4541,12 @@ namespace jdi
const bool meanFilter = (kind == EdgePanelKind::MEAN);
const bool weakPeak = (kind == EdgePanelKind::PEAK);
const bool freeMarker = (kind == EdgePanelKind::MARK);
// ⚠️ `itemized` 只表示**旧的逐条目排布**(现在只剩「弱峰调整」)。
// 「均值滤波」自 2026-10-30 起、**「中心轴边沿计数」自 2026-11-02 起**都有自己的
// **可滚动分支**(`if (listStyle)` 内)。若它们仍被算作 `itemized`,旧分支会
// **再画一遍**每行的名称 + 复选框 —— 且旧分支**没有任何裁切**,滚动时这些元素就会
// 画到标题栏上(用户 2026-10-30 报告“元素超出浮动面板”的根因)。
const bool listStyle = (meanFilter || centerAxis || weakPeak);
// ⚠️ 「均值滤波」「弱峰调整」= 可滚动的分组列表(条目栏 + 展开后的两行滑块);
// **「中心轴边沿计数」= 表格**(表头:信号名 / 上升沿计数 / 下降沿计数),单独一支。
// 三者都**不能**再走旧的逐条目分支:那个分支没有任何裁切,滚动时会画到标题栏上
//(用户 2026-10-30 报告“元素超出浮动面板”的根因)。
const bool listStyle = (meanFilter || weakPeak);
const bool centerTable = centerAxis;
const bool hasButton = (meanFilter || weakPeak);
{
if (index >= count)
@@ -4721,6 +4778,284 @@ namespace jdi
if (!layout.collapsed)
{
// 「中心轴边沿计数」= **表格**:表头(信号名 / 上升沿计数 / 下降沿计数)**固定不滚动**,
// 下面每条可见信号一行,行底色 = 该信号的配色,三列与表头严格对齐。
// 单击行(非复选框)展开 / 收起该信号自己的两行参数滑块。
if (centerTable)
{
// 复选框自绘(在信号色叠底上用**自适应对比色**)。
const auto paintTableCheck = [this, &g](const gd::RectF& rect, bool checked,
const UiInk& ink,
std::uint32_t background) {
if (rect.Width <= 0.0f || rect.Height <= 0.0f)
{
return;
}
const float radius = 2.5f * m_scale;
if (!checked)
{
UiDrawRoundRect(g, rect, radius, ink.text, 1.2f * m_scale);
return;
}
UiFillRoundRect(g, rect, radius, ink.text);
const float left = rect.X + rect.Width * 0.22f;
const float middleX = rect.X + rect.Width * 0.44f;
const float right = rect.X + rect.Width * 0.78f;
const float highY = rect.Y + rect.Height * 0.30f;
const float lowY = rect.Y + rect.Height * 0.72f;
UiDrawLine(g, left, rect.Y + rect.Height * 0.52f, middleX, lowY, background,
1.6f * m_scale);
UiDrawLine(g, middleX, lowY, right, highY, background, 1.6f * m_scale);
};
// ---- 表头(固定;不参与滚动)----
{
UiFillRoundRect(g, layout.tableHeader, 3.0f * m_scale, m_theme->pressed);
UiDrawRoundRect(g, layout.tableHeader, 3.0f * m_scale, m_theme->border, 1.0f);
for (int column = 0; column < 3; ++column)
{
const EdgePanelLayout::TableColumn& cell = layout.tableColumns[column];
if (cell.width <= 0.0f)
{
continue;
}
const float textLeft =
(column == 0) ? (cell.left + 4.0f * m_scale) : cell.left;
UiTextClipped(g, EdgeCenterTableHeader(column), m_fonts->uiSmall,
gd::RectF(textLeft, layout.tableHeader.Y, cell.width,
layout.tableHeader.Height),
m_theme->textMuted,
(column == 0) ? TextAlign::LEFT : TextAlign::CENTER,
layout.tableHeader);
}
}
const int tableSave = g.Save();
// 行内容必须裁到内容区:滚动时行会越出可视范围。
g.SetClip(layout.content, gd::CombineModeIntersect);
const float valueColumn =
std::max(UiMeasureTextWidth(m_fonts->monoSmall, L"100000"),
UiMeasureTextWidth(m_fonts->monoSmall, L"1000"));
const float sliderLabelColumn =
std::max(UiMeasureTextWidth(m_fonts->uiSmall, kMeanFilterPointsSliderLabel),
UiMeasureTextWidth(m_fonts->uiSmall, kMeanFilterIterationsSliderLabel));
for (std::size_t rowIndex = 0; rowIndex < layout.rowRefs.size(); ++rowIndex)
{
if (rowIndex >= layout.rowHeaders.size() ||
rowIndex >= layout.rowChecks.size() ||
rowIndex >= layout.rowTriangles.size())
{
break;
}
const SignalRef& ref = layout.rowRefs[rowIndex];
const ResolvedSignal resolved = ResolveSignal(ref);
if (resolved.data == nullptr || resolved.style == nullptr)
{
continue;
}
const gd::RectF& row = layout.rowHeaders[rowIndex];
const Lane::EdgeCenterItem* item = tabLane.FindEdgeCenterItem(ref);
const bool checked = (item != nullptr);
// 整行(含展开的滑块)的纵向范围决定是否还需要绘制。
float rowBottom = row.Y + row.Height;
if (rowIndex < layout.rowIterTracks.size() &&
layout.rowIterTracks[rowIndex].Height > 0.0f)
{
rowBottom = std::max(rowBottom, layout.rowIterTracks[rowIndex].Y +
layout.rowIterTracks[rowIndex].Height);
}
if (rowIndex < layout.rowLevelTracks.size() &&
layout.rowLevelTracks[rowIndex].Height > 0.0f)
{
rowBottom = std::max(rowBottom, layout.rowLevelTracks[rowIndex].Y +
layout.rowLevelTracks[rowIndex].Height);
}
if (rowBottom <= layout.content.Y ||
row.Y >= layout.content.Y + layout.content.Height)
{
continue;
}
const std::uint32_t signalColor = StyleColor(*resolved.style);
const UiInk ink = UiContrastInk(signalColor);
// 行底色 = 该信号的配色(用户 2026-11-02 要求);未勾选的行用普通底
// + 弱文本,表示“尚未参与统计”。
if (checked)
{
UiFillRoundRect(g, row, 3.0f * m_scale, signalColor);
UiFillRoundRect(g, row, 3.0f * m_scale, WithAlpha(signalColor, 0xE8));
}
else
{
UiFillRoundRect(g, row, 3.0f * m_scale, m_theme->cardBg);
UiDrawRoundRect(g, row, 3.0f * m_scale, m_theme->border, 1.0f);
}
if (m_meanRowHoverLane == static_cast<int>(index) &&
m_meanRowHoverRef == ref && m_meanRowHoverKind == kind)
{
UiFillRoundRect(g, row, 3.0f * m_scale,
WithAlpha(m_theme->accent, 0x2E));
}
const std::uint32_t rowInk = checked ? ink.text : m_theme->text;
// 展开 / 收起三角(仅已勾选时存在)。
const gd::RectF& triangle = layout.rowTriangles[rowIndex];
if (triangle.Width > 0.0f && checked)
{
UiDrawTriangleIcon(g, triangle, rowInk,
item->collapsed ? IconDirection::RIGHT
: IconDirection::DOWN);
}
// 信号名(第一列):超出列宽时省略。
const gd::RectF& nameRect = layout.rowNameRects[rowIndex];
UiTextClipped(g, UiEllipsize(resolved.data->name, m_fonts->uiSmall,
std::max(8.0f, nameRect.Width)),
m_fonts->uiSmall, nameRect, rowInk, TextAlign::LEFT,
layout.content);
// 复选框:位于名称单元的右端。
paintTableCheck(layout.rowChecks[rowIndex], checked, ink,
checked ? signalColor : m_theme->cardBg);
// 两列计数:与表头列**同一左边界 / 同一宽度**,纵向严格对齐。
std::size_t rising = 0;
std::size_t falling = 0;
bool complete = true;
if (checked)
{
EdgeCenterCountsFor(*resolved.axis, *resolved.data, *resolved.style,
item->points, item->iterations, EdgeRangeActive(),
EdgeRangeMin(), EdgeRangeMax(), rising, falling, complete);
}
for (int column = 1; column < 3; ++column)
{
const EdgePanelLayout::TableColumn& cell = layout.tableColumns[column];
if (cell.width <= 0.0f)
{
continue;
}
std::wstring text;
if (checked)
{
text = EdgeCountCellText((column == 1) ? rising : falling, complete);
}
UiTextClipped(g, text, m_fonts->monoSmall,
gd::RectF(cell.left, row.Y, cell.width, row.Height), rowInk,
TextAlign::CENTER, layout.content);
}
// 展开后的两行参数滑块(均值点数 / 迭代次数)。
if (!checked || item->collapsed)
{
continue;
}
const auto paintTableSlider = [&](const gd::RectF& track,
const gd::RectF& valueRect,
const gd::RectF& resetRect,
const wchar_t* label, int current,
int minimum, int maximum, bool active) {
if (track.Width <= 0.0f)
{
return;
}
const float labelLeft =
track.X - 6.0f * m_scale - sliderLabelColumn;
UiTextClipped(g, label, m_fonts->uiSmall,
gd::RectF(labelLeft, track.Y - 8.0f * m_scale,
sliderLabelColumn, track.Height + 16.0f * m_scale),
m_theme->textMuted, TextAlign::LEFT, layout.content);
const double ratio =
(maximum > minimum)
? (static_cast<double>(current - minimum) /
static_cast<double>(maximum - minimum))
: 0.0;
UiDrawSlider(g, track, ratio, m_theme->accent, active, m_scale);
UiTextClipped(g, std::to_wstring(current), m_fonts->monoSmall,
gd::RectF(valueRect.X, valueRect.Y, valueColumn,
valueRect.Height),
m_theme->textStrong, TextAlign::LEFT, layout.content);
const bool hot =
(m_dragMode != PlotDragMode::EDGE_PANEL_MOVE) &&
UiPointInRect(resetRect, static_cast<float>(m_mouseX),
static_cast<float>(m_mouseY));
if (hot)
{
UiFillRoundRect(
g, gd::RectF(resetRect.X - 1.0f * m_scale,
resetRect.Y - 1.0f * m_scale,
resetRect.Width + 2.0f * m_scale,
resetRect.Height + 2.0f * m_scale),
3.0f * m_scale, WithAlpha(m_theme->accent, 0x33));
}
PaintPanelResetIcon(g, resetRect,
hot ? m_theme->accent : m_theme->textMuted, m_scale);
};
const int pointsFloor = 0; // 中心轴:0 = 不平滑(保持旧语义)
const int pointsMax = std::max(
pointsFloor,
std::min(kMeanFilterPointsMaxLimit, item->maxPoints));
const int pointsValue =
std::max(pointsFloor, std::min(pointsMax, item->points));
const int iterationsMax =
std::max(kMeanFilterIterationsMin,
std::min(kMeanFilterIterationsMaxLimit, item->maxIterations));
const int iterationsValue =
std::max(kMeanFilterIterationsMin,
std::min(iterationsMax, item->iterations));
const bool pointsActive = (m_dragMode == PlotDragMode::MEAN_POINTS) &&
(m_meanDragLane == static_cast<int>(index)) &&
(m_meanDragRef == ref);
const bool iterationsActive =
(m_dragMode == PlotDragMode::MEAN_ITERATIONS) &&
(m_meanDragLane == static_cast<int>(index)) && (m_meanDragRef == ref);
if (rowIndex < layout.rowLevelTracks.size() &&
layout.rowLevelTracks[rowIndex].Width > 0.0f)
{
paintTableSlider(
layout.rowLevelTracks[rowIndex],
rowIndex < layout.rowLevelValues.size()
? layout.rowLevelValues[rowIndex]
: gd::RectF(),
rowIndex < layout.rowLevelResets.size()
? layout.rowLevelResets[rowIndex]
: gd::RectF(),
kMeanFilterPointsSliderLabel, pointsValue, pointsFloor, pointsMax,
pointsActive);
}
if (rowIndex < layout.rowIterTracks.size() &&
layout.rowIterTracks[rowIndex].Width > 0.0f)
{
paintTableSlider(
layout.rowIterTracks[rowIndex],
rowIndex < layout.rowIterValues.size()
? layout.rowIterValues[rowIndex]
: gd::RectF(),
rowIndex < layout.rowIterResets.size()
? layout.rowIterResets[rowIndex]
: gd::RectF(),
kMeanFilterIterationsSliderLabel, iterationsValue,
kMeanFilterIterationsMin, iterationsMax, iterationsActive);
}
}
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);
}
}
// 「均值滤波」面板:逐信号的**信号色叠底标题栏**(左侧展开三角 + 信号名 +
// 右端复选框)。复选框勾选后才出现三角、才能展开下面的两行参数滑块
// (「均值点数」/「迭代次数」);内容超高时可滚动(滚动条在内容区右侧)。
@@ -4821,14 +5156,12 @@ namespace jdi
}
const std::uint32_t signalColor = StyleColor(*resolved.style);
const UiInk ink = UiContrastInk(signalColor);
// 「均值滤波」看 `meanFilterItems`、「中心轴边沿计数」看 `edgeCenterItems`
//(两者都是 `Lane::MeanFilterItem`,字段完全一致);「弱峰调整」看 `weakPeakItems`。
// 「均值滤波」看 `meanFilterItems`、「弱峰调整」看 `weakPeakItems`
//(中心轴边沿计数已改为独立的表格分支)。
const Lane::WeakPeakItem* peakItem =
weakPeak ? tabLane.FindWeakPeakItem(ref) : nullptr;
const Lane::MeanFilterItem* item =
weakPeak ? nullptr
: (meanFilter ? tabLane.FindMeanFilterItem(ref)
: tabLane.FindEdgeCenterItem(ref));
weakPeak ? nullptr : tabLane.FindMeanFilterItem(ref);
const bool checked = weakPeak ? (peakItem != nullptr) : (item != nullptr);
const bool rowCollapsed =
weakPeak ? (peakItem != nullptr && peakItem->collapsed)
@@ -4840,7 +5173,10 @@ namespace jdi
UiFillRoundRect(g, header, 3.0f * m_scale, WithAlpha(signalColor, 0xE8));
// 条目栏悬停:在信号色叠底上再压一层强调色,给出"这一行对应哪条曲线"的提示
//(曲线本身的高亮见 `PaintLaneDerivedOverlays`)。
if (m_meanRowHoverLane == static_cast<int>(index) && m_meanRowHoverRef == ref)
// ⚠️ 必须同时比**面板种类**:同一条信号在其它面板里也有一行,
// 只比 (lane, ref) 会把别的面板的同名行一起点亮。
if (m_meanRowHoverLane == static_cast<int>(index) &&
m_meanRowHoverRef == ref && m_meanRowHoverKind == kind)
{
UiFillRoundRect(g, header, 3.0f * m_scale,
WithAlpha(m_theme->accent, 0x2E));
@@ -4873,21 +5209,6 @@ namespace jdi
continue;
}
// 「中心轴边沿计数」:参数行最上面是**同一行**的「上升沿 / 下降沿」统计。
if (centerAxis && rowIndex < layout.rowStats.size() &&
layout.rowStats[rowIndex].Width > 0.0f)
{
std::size_t rising = 0;
std::size_t falling = 0;
bool complete = true;
EdgeCenterCountsFor(*resolved.axis, *resolved.data, *resolved.style, item->points,
item->iterations, EdgeRangeActive(), EdgeRangeMin(),
EdgeRangeMax(), rising, falling, complete);
paintCounts(layout.rowStats[rowIndex], rising, falling, complete,
layout.content);
}
// 两行参数滑块(标签 / 轨道 / 数值 / 复位按钮)。
const auto paintMeanSlider = [&](const gd::RectF& track,
const gd::RectF& valueRect,
@@ -5034,7 +5355,8 @@ namespace jdi
}
else
{
const int pointsFloor = centerAxis ? 0 : kMeanFilterPointsMin;
// 只可能是「均值滤波」:中心轴边沿计数已改为独立的表格分支。
const int pointsFloor = kMeanFilterPointsMin;
const int pointsMax = std::max(pointsFloor,
std::min(kMeanFilterPointsMaxLimit,
item->maxPoints));
@@ -5448,9 +5770,9 @@ namespace jdi
{
continue;
}
if (freeMarker || listStyle)
if (freeMarker || listStyle || centerTable)
{
// 这两种排布已在上面画完(各自有自己的可滚动分支)。
// 这些排布已在上面画完(各自有自己的可滚动 / 表格分支,含裁切)。
continue;
}
if (rowY + rowHeight * 2.0f <= layout.content.Y ||
@@ -5927,4 +6249,4 @@ namespace jdi
UiDrawLine(g, cx, cy - depth, cx + halfWidth, cy - depth * 0.15f, ink, stroke);
}
} // namespace jdi
} // namespace jdi