面板限高与滚动改造 + 边沿统计不再卡在“统计中…” + 组名折起真正收起
一、浮动面板(用户报告:下方空白、滚动时画到面板外、未改造的面板让一切交互卡顿) 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:
1 parent
334015c1a9
commit
7a6fcd68e2
9 files changed
+914
-795
No files matched your search
@@ -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
|
||||
|
||||
Reference in new issue
Block a user