面板限高与滚动改造 + 边沿统计不再卡在“统计中…” + 组名折起真正收起

一、浮动面板(用户报告:下方空白、滚动时画到面板外、未改造的面板让一切交互卡顿)
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。
This commit is contained in:
DeepSeek V4.1 Flash committed 2026-11-02 10:00:24 +08:00
1 parent 334015c1a9
commit 7a6fcd68e2
9 files changed
+914 -795

No files matched your search

+191 -270
View File
@@ -3774,6 +3774,8 @@ namespace jdi
mix(static_cast<std::uint64_t>(peakItem != nullptr ? peakItem->amountPercent + 2 : 0));
mix(static_cast<std::uint64_t>(peakItem != nullptr ? peakItem->iterations + 2 : 0));
mix(static_cast<std::uint64_t>(peakItem != nullptr ? peakItem->maxIterations + 2 : 0));
// 「弱峰调整」的条目栏三角(展开 / 收起)直接改变面板像素 ⇒ 必须进键。
mix((peakItem != nullptr && peakItem->collapsed) ? 1ull : 0ull);
}
// ⚠️⚠️ **边沿统计的数字本身就是面板内容,必须进缓存键**(用户报告的缺陷根因):
@@ -4381,21 +4383,25 @@ namespace jdi
// ⚠️ 未完成时**两个数字后面都带 `…`**(旧实现只在整行末尾追加一次,
// 视觉上只跟在“下降沿”的数字后面;用户明确要求为“上升沿”也补上这个尾缀)。
const auto paintCounts = [this, &g](const gd::RectF& rect, std::size_t rising,
std::size_t falling, bool complete)
std::size_t falling, bool complete, const gd::RectF& clip)
{
// ⚠️ **必须用 `UiTextClipped`**:GDI 文本不受 GDI+ `SetClip` 约束,
// 内容区滚动时统计文字会画到面板标题栏 / 面板之外
//(用户 2026-11-02 报告“滚动时绘制超出了浮动面板”的直接原因)。
if (EdgeCountsPending(rising, falling, complete))
{
UiText(g, L"统计中…", m_fonts->monoSmall, rect, m_theme->textMuted, TextAlign::LEFT);
UiTextClipped(g, L"统计中…", m_fonts->monoSmall, rect, m_theme->textMuted,
TextAlign::LEFT, clip);
return;
}
const float gap = kEdgePanelStatColumnGap * m_scale;
const float columnWidth = std::max(10.0f, (rect.Width - gap) * 0.5f);
UiText(g, EdgeRisingText(rising, complete), m_fonts->monoSmall,
gd::RectF(rect.X, rect.Y, columnWidth, rect.Height), m_theme->textMuted,
TextAlign::LEFT);
UiText(g, EdgeFallingText(falling, complete), m_fonts->monoSmall,
gd::RectF(rect.X + columnWidth + gap, rect.Y, columnWidth, rect.Height),
m_theme->textMuted, TextAlign::LEFT);
UiTextClipped(g, EdgeRisingText(rising, complete), m_fonts->monoSmall,
gd::RectF(rect.X, rect.Y, columnWidth, rect.Height), m_theme->textMuted,
TextAlign::LEFT, clip);
UiTextClipped(g, EdgeFallingText(falling, complete), m_fonts->monoSmall,
gd::RectF(rect.X + columnWidth + gap, rect.Y, columnWidth, rect.Height),
m_theme->textMuted, TextAlign::LEFT, clip);
};
std::vector<PanelSlot> nextSlots;
@@ -4483,8 +4489,7 @@ namespace jdi
// **可滚动分支**(`if (listStyle)` 内)。若它们仍被算作 `itemized`,旧分支会
// **再画一遍**每行的名称 + 复选框 —— 且旧分支**没有任何裁切**,滚动时这些元素就会
// 画到标题栏上(用户 2026-10-30 报告“元素超出浮动面板”的根因)。
const bool listStyle = (meanFilter || centerAxis);
const bool itemized = weakPeak;
const bool listStyle = (meanFilter || centerAxis || weakPeak);
const bool hasButton = (meanFilter || weakPeak);
{
if (index >= count)
@@ -4715,225 +4720,6 @@ namespace jdi
// 折起的面板只保留标题栏:内容区(条目 / 分组栏 / 按钮 / 滚动条)全部省略。
if (!layout.collapsed)
{
// 「中心轴边沿计数」/「均值滤波」/「弱峰调整」面板都采用「逐条目」排布:
// 第一排 = 名称 + 复选框,第二排 = 两个参数输入框(仅勾选的条目,
// 弱峰调整没有参数输入框),中心轴再多第三排 = 对应的统计结果。
if (itemized)
{
for (std::size_t rowIndex = 0; rowIndex < layout.rowRefs.size(); ++rowIndex)
{
const SignalRef& ref = layout.rowRefs[rowIndex];
const ResolvedSignal resolved = ResolveSignal(ref);
if (resolved.data == nullptr || resolved.style == nullptr)
{
continue;
}
const std::uint32_t signalColor = StyleColor(*resolved.style);
const gd::RectF& checkRect = layout.rowChecks[rowIndex];
const bool checked = meanFilter ? tabLane.MeanFilterEnabled(ref)
: (weakPeak ? tabLane.WeakPeakEnabled(ref)
: tabLane.EdgeCenterEnabled(ref));
// 名称:第一排左侧,避开右侧复选框(超长时省略)。
const float nameWidth =
std::min(std::max(20.0f, checkRect.X - (panel.X + pad) - 6.0f * m_scale),
kEdgePanelNameMaxWidth * m_scale);
UiText(g, UiEllipsize(resolved.data->name, m_fonts->uiSmall, nameWidth),
m_fonts->uiSmall,
gd::RectF(panel.X + pad, checkRect.Y - 3.0f * m_scale, nameWidth,
checkRect.Height + 6.0f * m_scale),
signalColor, TextAlign::LEFT);
UiDrawCheckBox(g, checkRect, checked ? 1 : 0, 3.0f * m_scale);
if (!checked)
{
continue;
}
// MEAN / CENTER 的平滑参数条目(弱峰调整用自己的 `WeakPeakItem`)。
const Lane::MeanFilterItem* item =
(meanFilter || centerAxis)
? (meanFilter ? tabLane.FindMeanFilterItem(ref)
: tabLane.FindEdgeCenterItem(ref))
: nullptr;
// 「弱峰调整」:每个被勾选的信号各有两行**滑块**
//(调整幅度 -200% – +200% / 迭代次数 1 … 该信号自定义上限)。
// 迭代次数行的右侧数值可点击 → 整行变为输入框以修改滑块上限。
if (weakPeak)
{
const Lane::WeakPeakItem* peakItem = tabLane.FindWeakPeakItem(ref);
if (peakItem != nullptr && rowIndex < layout.rowLevelTracks.size() &&
layout.rowLevelTracks[rowIndex].Width > 0.0f)
{
// 两行滑块的「标签」始终绘制(编辑上限时也要能看出在改哪一项);
// 「轨道 + 数值」在编辑上限时改由 `PaintEditField` 画成输入框。
const auto paintSliderLabel = [&](const gd::RectF& track,
const wchar_t* label)
{
const float labelColumn =
WeakPeakLabelColumnWidth(m_fonts->uiSmall);
const float labelLeft =
track.X - 6.0f * m_scale - labelColumn;
UiText(g, label, m_fonts->uiSmall,
gd::RectF(labelLeft, track.Y - 8.0f * m_scale,
labelColumn, track.Height + 16.0f * m_scale),
m_theme->textMuted, TextAlign::LEFT);
};
const auto paintSliderBody = [&](const gd::RectF& track, double ratio,
const std::wstring& valueText,
bool active)
{
UiDrawSlider(g, track, ratio, m_theme->accent, active, m_scale);
UiText(g, valueText, m_fonts->monoSmall,
gd::RectF(track.X + track.Width + 6.0f * m_scale,
track.Y - 8.0f * m_scale,
WeakPeakValueColumnWidth(m_fonts->monoSmall),
track.Height + 16.0f * m_scale),
m_theme->textStrong, TextAlign::LEFT);
};
// 每行滑块右侧的**复位控件**(与「系数/偏移」二级面板里的还原图标同一套绘制):
// 悬停时高亮 + 手型光标,点击把该行参数恢复为默认值(幅度 0 / 迭代 1 且上限回初始值)。
const auto paintSliderReset = [&](const gd::RectF& rect)
{
if (rect.Width <= 0.0f)
{
return;
}
const bool hot =
(m_dragMode != PlotDragMode::EDGE_PANEL_MOVE) &&
UiPointInRect(rect, static_cast<float>(m_mouseX),
static_cast<float>(m_mouseY));
if (hot)
{
UiFillRoundRect(
g, gd::RectF(rect.X - 1.0f * m_scale, rect.Y - 1.0f * m_scale,
rect.Width + 2.0f * m_scale,
rect.Height + 2.0f * m_scale),
3.0f * m_scale, WithAlpha(m_theme->accent, 0x33));
}
PaintPanelResetIcon(g, rect,
hot ? m_theme->accent : m_theme->textMuted, m_scale);
};
const gd::RectF& levelTrack = layout.rowLevelTracks[rowIndex];
const gd::RectF& iterTrack = layout.rowIterTracks[rowIndex];
paintSliderLabel(levelTrack, kWeakPeakLevelLabel);
paintSliderLabel(iterTrack, kWeakPeakIterationsSliderLabel);
const bool levelActive = (m_dragMode == PlotDragMode::PEAK_LEVEL) &&
(m_peakDragLane == static_cast<int>(index)) &&
(m_peakDragRef == ref);
const double levelRatio =
(static_cast<double>(std::max(kWeakPeakAmountMin,
std::min(kWeakPeakAmountMax,
peakItem->amountPercent))) /
(100.0 * kWeakPeakSliderScale) +
1.0) *
0.5;
paintSliderBody(levelTrack, levelRatio,
(peakItem->amountPercent > 0 ? L"+" : L"") +
std::to_wstring(peakItem->amountPercent) + L"%",
levelActive);
paintSliderReset(layout.rowLevelResets[rowIndex]);
// 迭代次数滑块:量程 = 1 … 该信号自己的上限。
const int peakMax = std::max(kWeakPeakIterationsMin,
std::min(kWeakPeakIterationsMaxLimit,
peakItem->maxIterations));
const int peakIter = std::max(
kWeakPeakIterationsMin, std::min(peakMax, peakItem->iterations));
const double iterRatio =
(peakMax > kWeakPeakIterationsMin)
? (static_cast<double>(peakIter - kWeakPeakIterationsMin) /
static_cast<double>(peakMax - kWeakPeakIterationsMin))
: 0.0;
const bool iterActive = (m_dragMode == PlotDragMode::PEAK_ITERATIONS) &&
(m_peakDragLane == static_cast<int>(index)) &&
(m_peakDragRef == ref);
const bool editingMax =
(m_editTarget == PlotEditTarget::PEAK_ITERATIONS_MAX) &&
(m_editLane == static_cast<int>(index)) && (m_editMeanRef == ref);
if (!editingMax)
{
paintSliderBody(iterTrack, iterRatio, std::to_wstring(peakIter),
iterActive);
paintSliderReset(layout.rowIterResets[rowIndex]);
}
}
}
// MEAN / CENTER:第二排的两个参数输入框。
if (!weakPeak)
{
const int shownPoints = (item != nullptr) ? item->points : 5;
const int shownOrder = (item != nullptr) ? item->iterations : 1;
const PlotEditTarget pointsTarget =
meanFilter ? PlotEditTarget::MEAN_FILTER_POINTS
: PlotEditTarget::EDGE_CENTER_POINTS;
const PlotEditTarget orderTarget =
meanFilter ? PlotEditTarget::MEAN_FILTER_ITERATIONS
: PlotEditTarget::EDGE_CENTER_ITERATIONS;
const auto paintField = [&](const gd::RectF& rect, const wchar_t* label,
int value, PlotEditTarget target)
{
if (rect.Width <= 0.0f)
{
return;
}
const float labelWidth = UiMeasureTextWidth(m_fonts->uiSmall, label);
UiText(g, label, m_fonts->uiSmall,
gd::RectF(rect.X, rect.Y, labelWidth, rect.Height), m_theme->textMuted,
TextAlign::LEFT);
const float fieldWidth =
std::max(kEdgePanelEditFieldWidth * m_scale * 0.75f,
UiMeasureTextWidth(m_fonts->monoSmall, std::to_wstring(value)) +
18.0f * m_scale);
const float fieldLeft = rect.X + rect.Width - fieldWidth;
const float fieldHeight = rect.Height - 4.0f * m_scale;
const gd::RectF fieldRect(fieldLeft, rect.Y + 2.0f * m_scale, fieldWidth,
fieldHeight);
const bool editingThis = (m_editTarget == target) &&
(m_editLane == static_cast<int>(index)) &&
(m_editMeanRef == ref);
if (editingThis)
{
UiDrawEditField(g, fieldRect, m_edit, m_fonts->monoSmall, m_scale);
}
else
{
UiFillRoundRect(g, fieldRect, 3.0f * m_scale, m_theme->pressed);
UiDrawRoundRect(g, fieldRect, 3.0f * m_scale, m_theme->border, 1.0f);
UiText(g, std::to_wstring(value), m_fonts->monoSmall, fieldRect,
m_theme->textStrong, TextAlign::CENTER);
}
};
paintField(layout.rowPoints[rowIndex], L"均值点数 X", shownPoints, pointsTarget);
paintField(layout.rowIterations[rowIndex], kWeakPeakIterationsLabel, shownOrder,
orderTarget);
}
// 第三排(中心轴):该信号在当前参数下的上升沿/下降沿统计结果。
if (centerAxis && rowIndex < layout.rowStats.size() &&
layout.rowStats[rowIndex].Width > 0.0f)
{
const gd::RectF& statsRect = layout.rowStats[rowIndex];
const int statsPoints = (item != nullptr) ? item->points : 5;
const int statsOrder = (item != nullptr) ? item->iterations : 1;
std::size_t rising = 0;
std::size_t falling = 0;
bool statsComplete = false;
EdgeCenterCountsFor(*resolved.axis, *resolved.data, *resolved.style,
statsPoints, statsOrder, EdgeRangeActive(),
EdgeRangeMin(), EdgeRangeMax(), rising, falling,
statsComplete);
// 两列排版(列起点固定)⇒ 多条信号的“上升沿 / 下降沿”垂直对齐。
paintCounts(statsRect, rising, falling, statsComplete);
}
}
}
// 「均值滤波」面板:逐信号的**信号色叠底标题栏**(左侧展开三角 + 信号名 +
// 右端复选框)。复选框勾选后才出现三角、才能展开下面的两行参数滑块
@@ -5036,10 +4822,17 @@ namespace jdi
const std::uint32_t signalColor = StyleColor(*resolved.style);
const UiInk ink = UiContrastInk(signalColor);
// 「均值滤波」看 `meanFilterItems`、「中心轴边沿计数」看 `edgeCenterItems`
// (两者都是 `Lane::MeanFilterItem`,字段完全一致)。
const Lane::MeanFilterItem* item = meanFilter ? tabLane.FindMeanFilterItem(ref)
: tabLane.FindEdgeCenterItem(ref);
const bool checked = (item != nullptr);
//(两者都是 `Lane::MeanFilterItem`,字段完全一致);「弱峰调整」看 `weakPeakItems`。
const Lane::WeakPeakItem* peakItem =
weakPeak ? tabLane.FindWeakPeakItem(ref) : nullptr;
const Lane::MeanFilterItem* item =
weakPeak ? nullptr
: (meanFilter ? tabLane.FindMeanFilterItem(ref)
: tabLane.FindEdgeCenterItem(ref));
const bool checked = weakPeak ? (peakItem != nullptr) : (item != nullptr);
const bool rowCollapsed =
weakPeak ? (peakItem != nullptr && peakItem->collapsed)
: (item != nullptr && item->collapsed);
// 内容区已由 `g.SetClip(layout.content)` 裁切(行可以“半截”显示,滚动更顺滑);
// 已经完整滚出可视范围的行在上面就 `continue` 了,这里不再额外门控。
@@ -5055,11 +4848,11 @@ namespace jdi
// 左侧三角(仅勾选时存在):朝下 = 展开、朝右 = 收起。
const gd::RectF& triangle = layout.rowTriangles[rowIndex];
if (triangle.Width > 0.0f && item != nullptr)
if (triangle.Width > 0.0f && checked)
{
UiDrawTriangleIcon(g, triangle, ink.text,
item->collapsed ? IconDirection::RIGHT
: IconDirection::DOWN);
rowCollapsed ? IconDirection::RIGHT
: IconDirection::DOWN);
}
// 名称:从三角右侧到复选框左侧。
@@ -5075,7 +4868,7 @@ namespace jdi
TextAlign::LEFT, layout.content);
paintMeanCheck(check, checked, ink, signalColor);
if (!checked || item->collapsed)
if (!checked || rowCollapsed)
{
continue;
}
@@ -5090,9 +4883,11 @@ namespace jdi
EdgeCenterCountsFor(*resolved.axis, *resolved.data, *resolved.style, item->points,
item->iterations, EdgeRangeActive(), EdgeRangeMin(),
EdgeRangeMax(), rising, falling, complete);
paintCounts(layout.rowStats[rowIndex], rising, falling, complete);
paintCounts(layout.rowStats[rowIndex], rising, falling, complete,
layout.content);
}
// 两行参数滑块(标签 / 轨道 / 数值 / 复位按钮)。
const auto paintMeanSlider = [&](const gd::RectF& track,
const gd::RectF& valueRect,
@@ -5140,6 +4935,105 @@ namespace jdi
// ⚠️ 「均值点数」的最小值:**均值滤波 = 1**(用户 2026-10-30 要求不能为 0,
// 0 与不做平滑完全等价,看起来像功能坏了);**中心轴边沿计数 = 0**
// (0 = 不平滑,直接对原始采样值统计边沿 —— 这是有意义的选项,保持旧语义)。
if (weakPeak)
{
// 「弱峰调整」两行滑块:调整幅度(0 在正中间 ⇒ ±400%)与迭代次数(1 … 自定上限)。
const int peakAmount = std::max(
kWeakPeakAmountMin, std::min(kWeakPeakAmountMax, peakItem->amountPercent));
const int peakMax = std::max(
kWeakPeakIterationsMin,
std::min(kWeakPeakIterationsMaxLimit, peakItem->maxIterations));
const int peakIter =
std::max(kWeakPeakIterationsMin, std::min(peakMax, peakItem->iterations));
const bool levelActive = (m_dragMode == PlotDragMode::PEAK_LEVEL) &&
(m_peakDragLane == static_cast<int>(index)) &&
(m_peakDragRef == ref);
const bool iterActive = (m_dragMode == PlotDragMode::PEAK_ITERATIONS) &&
(m_peakDragLane == static_cast<int>(index)) &&
(m_peakDragRef == ref);
const bool editingMax =
(m_editTarget == PlotEditTarget::PEAK_ITERATIONS_MAX) &&
(m_editLane == static_cast<int>(index)) && (m_editMeanRef == ref);
// 幅度滑块:比例 = (幅度 / (100*4) + 1) / 2(0% 在正中间)。
const auto paintPeakSlider = [&](const gd::RectF& track,
const gd::RectF& valueRect,
const gd::RectF& resetRect,
const wchar_t* label, double ratio,
const std::wstring& valueText, bool active,
bool hideBody)
{
const float labelLeft = track.X - 6.0f * m_scale -
UiMeasureTextWidth(m_fonts->uiSmall, label);
UiTextClipped(g, label, m_fonts->uiSmall,
gd::RectF(labelLeft, track.Y - 8.0f * m_scale,
UiMeasureTextWidth(m_fonts->uiSmall, label),
track.Height + 16.0f * m_scale),
m_theme->textMuted, TextAlign::LEFT, layout.content);
if (hideBody)
{
return; // 正在编辑上限:轨道与数值改由 `PaintEditField` 画
}
UiDrawSlider(g, track, ratio, m_theme->accent, active, m_scale);
UiTextClipped(g, valueText, 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 double levelRatio =
(static_cast<double>(peakAmount) / (100.0 * kWeakPeakSliderScale) + 1.0) * 0.5;
const double iterRatio =
(peakMax > kWeakPeakIterationsMin)
? (static_cast<double>(peakIter - kWeakPeakIterationsMin) /
static_cast<double>(peakMax - kWeakPeakIterationsMin))
: 0.0;
if (rowIndex < layout.rowLevelTracks.size() &&
layout.rowLevelTracks[rowIndex].Width > 0.0f)
{
paintPeakSlider(
layout.rowLevelTracks[rowIndex],
rowIndex < layout.rowLevelValues.size()
? layout.rowLevelValues[rowIndex]
: gd::RectF(),
rowIndex < layout.rowLevelResets.size()
? layout.rowLevelResets[rowIndex]
: gd::RectF(),
kWeakPeakLevelLabel, levelRatio,
(peakAmount > 0 ? L"+" : L"") + std::to_wstring(peakAmount) + L"%",
levelActive, false);
}
if (rowIndex < layout.rowIterTracks.size() &&
layout.rowIterTracks[rowIndex].Width > 0.0f)
{
paintPeakSlider(
layout.rowIterTracks[rowIndex],
rowIndex < layout.rowIterValues.size()
? layout.rowIterValues[rowIndex]
: gd::RectF(),
rowIndex < layout.rowIterResets.size()
? layout.rowIterResets[rowIndex]
: gd::RectF(),
kWeakPeakIterationsSliderLabel, iterRatio,
std::to_wstring(peakIter), iterActive, editingMax);
}
}
else
{
const int pointsFloor = centerAxis ? 0 : kMeanFilterPointsMin;
const int pointsMax = std::max(pointsFloor,
std::min(kMeanFilterPointsMaxLimit,
@@ -5153,11 +5047,12 @@ namespace jdi
kMeanFilterIterationsMin, std::min(iterationsMax, item->iterations));
const bool pointsActive = (m_dragMode == PlotDragMode::MEAN_POINTS) &&
(m_meanDragLane == static_cast<int>(index)) &&
(m_meanDragRef == ref) && (m_meanDragCenter == centerAxis);
(m_meanDragRef == ref) &&
(m_meanDragKind == kind);
const bool iterationsActive =
(m_dragMode == PlotDragMode::MEAN_ITERATIONS) &&
(m_meanDragLane == static_cast<int>(index)) && (m_meanDragRef == ref) &&
(m_meanDragCenter == centerAxis);
(m_meanDragKind == kind);
if (rowIndex < layout.rowLevelTracks.size())
{
paintMeanSlider(layout.rowLevelTracks[rowIndex],
@@ -5182,6 +5077,8 @@ namespace jdi
kMeanFilterIterationsSliderLabel, iterations,
kMeanFilterIterationsMin, iterationsMax, iterationsActive);
}
}
}
g.Restore(meanSave);
@@ -5537,38 +5434,63 @@ namespace jdi
}
}
float rowY = panel.Y + titleHeight + pad;
for (const SignalRef& ref : tabLane.signals)
// 「边沿计数」:逐信号两行(名称 / 上升沿·下降沿)。内容区**可滚动且按内容区裁剪**
//(上百个信号时内容高达上万像素:限高后出现滚动条,面板高度被绘图区夹住,
// 面板像素缓存才存得进去 —— 否则每帧整块重画,所有交互都会卡顿)。
{
const ResolvedSignal resolved = ResolveSignal(ref);
if (resolved.data == nullptr || resolved.style == nullptr || !resolved.visible)
const int contentState = g.Save();
g.SetClip(layout.content, gd::CombineModeIntersect);
float rowY = layout.content.Y - layout.scrollOffset;
for (const SignalRef& ref : tabLane.signals)
{
continue;
}
if (itemized || freeMarker || listStyle)
{
// 逐条目绘制已在上面完成(自由标记同样是逐条目排布,
// 均值滤波走自己的可滚动分支)。
continue;
}
const std::uint32_t signalColor = StyleColor(*resolved.style);
const float textWidth = panel.Width - pad * 2.0f;
// 名称:超长时省略(面板宽度被 `kEdgePanelNameMaxWidth` 限制,不会把面板顶得很长)。
UiText(g,
UiEllipsize(resolved.data->name, m_fonts->uiSmall,
std::min(textWidth, kEdgePanelNameMaxWidth * m_scale)),
m_fonts->uiSmall, gd::RectF(panel.X + pad, rowY, textWidth, rowHeight),
signalColor, TextAlign::LEFT);
const ResolvedSignal resolved = ResolveSignal(ref);
if (resolved.data == nullptr || resolved.style == nullptr || !resolved.visible)
{
continue;
}
if (freeMarker || listStyle)
{
// 这两种排布已在上面画完(各自有自己的可滚动分支)。
continue;
}
if (rowY + rowHeight * 2.0f <= layout.content.Y ||
rowY >= layout.content.Y + layout.content.Height)
{
rowY += rowHeight * 2.0f; // 整行滚出可视范围
continue;
}
const std::uint32_t signalColor = StyleColor(*resolved.style);
const float textWidth = layout.content.Width;
// 名称:超长时省略(面板宽度被 `kEdgePanelNameMaxWidth` 限制,不会把面板顶得很长)。
// ⚠️ GDI 文本必须走 `UiTextClipped`,否则滚动时会画到标题栏 / 面板之外。
UiTextClipped(g,
UiEllipsize(resolved.data->name, m_fonts->uiSmall,
std::min(textWidth, kEdgePanelNameMaxWidth * m_scale)),
m_fonts->uiSmall,
gd::RectF(layout.content.X, rowY, textWidth, rowHeight),
signalColor, TextAlign::LEFT, layout.content);
std::size_t rising = 0;
std::size_t falling = 0;
bool complete = true;
EdgeCountsFor(*resolved.axis, *resolved.data, *resolved.style, tabLane.edgeValue,
EdgeRangeActive(), EdgeRangeMin(), EdgeRangeMax(), rising, falling,
complete);
paintCounts(gd::RectF(panel.X + pad, rowY + rowHeight, textWidth, rowHeight), rising,
falling, complete);
rowY += rowHeight * 2.0f;
std::size_t rising = 0;
std::size_t falling = 0;
bool complete = true;
EdgeCountsFor(*resolved.axis, *resolved.data, *resolved.style,
tabLane.edgeValue, EdgeRangeActive(), EdgeRangeMin(),
EdgeRangeMax(), rising, falling, complete);
paintCounts(
gd::RectF(layout.content.X, rowY + rowHeight, textWidth, rowHeight), rising,
falling, complete, layout.content);
rowY += rowHeight * 2.0f;
}
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);
}
}
// 逐条目面板底部按钮行:「构建为新信号」(悬停高亮);
@@ -6006,4 +5928,3 @@ namespace jdi
}
} // namespace jdi