fix(plotview): 边沿计数表格归位、统计面板悬停时持续更新、加载不再被统计阻塞、统计提速
1) 表格改造归位(上一轮误做在中心轴上) - 「边沿计数」改为表格:表头(信号名/上升沿计数/下降沿计数)、每信号一行、 行底色取信号配色、三列对齐、内容可滚动(Lane::edgeCountScroll)。 - 「中心轴边沿计数」恢复为可滚动分组列表(条目栏 + 复选框 + 展开后统计行 + 两行滑块), Lane::EnsureEdgeCenterItem 恢复默认展开。 - EdgePanelWidth / EdgePanelLayoutFor / PaintEdgeCursor 统一 listStyle / countTable 判定; EdgeCenterTableHeader 更名为 EdgeCountTableHeader。 2) 鼠标持续移动时统计面板不更新 - 根因:交互态(拖拽或 140ms 内移动)把统计数字整段从面板缓存键中跳过, 只要鼠标不停移动就永远命中旧缓存。 - 修法:扫描推进/完成时自增 m_edgeScanRevision,Paint 开头转为 m_panelStatsVersion (非交互帧立即同步,交互帧按 kEdgeScanStatsRefreshFrames=4 节流), PanelSlotKey 对 COUNT/CENTER 仅混入该版本号(O(1),不再逐信号查询)。 3) 统计进行中无法加载文件 - 新增 PlotView::SetLoadingBusy:加载期间统计时间预算归零、跳过浮动面板绘制、 不再自请求扫描帧;Application::LoadFileAsync 进出各设置一次。实测加载 22~31ms。 4) 统计提速 - 扫描预算由固定采样对改为 QPC 时间片(非交互 6ms / 交互 1.5ms,每块检查超时), 采样对配额改为按未完成状态数公平分摊并把上限提到 8M(交互 2M)。 - 真实 MF4-Record.MF4(283 信号)全部统计状态在 1 帧内完成;悬停+移动帧中位 2~3ms。 验证:离屏探针 p20/p20c/p20d/p21/p22/p23/p25 全绿(新增 p25 12 项); make release / make debug 零 warning;Release 808,960 B。共享规则已同步到 Documents/agent_mem.md。
This commit is contained in:
1 parent
8de8a8fb85
commit
ea704b03ea
9 files changed
+424
-553
No files matched your search
@@ -835,18 +835,40 @@ namespace jdi
|
||||
// 就会在同一系统 tick 内连画多帧而**完全不推进**统计 —— 用户报告
|
||||
// “按住横向光标不动时统计卡住不更新”“回车确认参数后不移动鼠标就停在部分值”。
|
||||
++m_edgeScanFrame;
|
||||
// ⚠️⚠️ **交互帧必须用小预算**(用户 2026-11-02 报告):288 个信号合并到同一网格、
|
||||
// 开启中心轴边沿计数后,拖动横向光标每帧要 30ms,界面完全跟不上手。中心轴每个采样对
|
||||
// 还要按「均值点数 × 迭代次数」算一次均值窗口 ⇒ 120 万对 ≈ 3 千万次浮点运算/帧。
|
||||
// 现在:正在拖拽、或鼠标刚刚移动过(`kEdgeScanInteractiveIdleMs` 内)⇒ 用交互预算
|
||||
// (1/5),优先保证界面跟手;鼠标停下约 0.14s 后自动恢复高预算、把统计快速补齐。
|
||||
// ⚠️⚠️ **交互帧必须用小预算**(用户 2026-11-02 报告):288 个信号合并到同一网格、
|
||||
// 开启中心轴边沿计数后,拖动横向光标每帧要 30ms,界面完全跟不上手。中心轴每个采样对
|
||||
// 还要按「均值点数 × 迭代次数」算一次均值窗口 ⇒ 120 万对 ≈ 3 千万次浮点运算/帧。
|
||||
// 现在:正在拖拽、或鼠标刚刚移动过(`kEdgeScanInteractiveIdleMs` 内)⇒ 用交互预算
|
||||
// (1/5),优先保证界面跟手;鼠标停下约 0.14s 后自动恢复高预算、把统计快速补齐。
|
||||
m_edgeScanBudget =
|
||||
InputIsInteractive() ? kEdgeScanPairsPerFrameInteractive : kEdgeScanPairsPerFrame;
|
||||
// 统计数据版本号 → 面板缓存键(见 `m_edgeScanRevision`):
|
||||
// * 非交互帧:**立刻**同步(统计一推进就重画面板,用户看到数字实时变化);
|
||||
// * 交互帧:按 `kEdgeScanStatsRefreshFrames` 节流 —— 否则鼠标一直移动时
|
||||
// 面板每帧重画上百行(正是“悬停在信号上持续移动、面板一直不更新”的反面:
|
||||
// 以前是冻结永不更新,现在是有节制地刷新)。
|
||||
if (m_edgeScanRevision != m_panelStatsSeen &&
|
||||
(!InputIsInteractive() ||
|
||||
(m_edgeScanFrame % kEdgeScanStatsRefreshFrames) == 0u))
|
||||
{
|
||||
m_panelStatsVersion = m_edgeScanRevision;
|
||||
m_panelStatsSeen = m_edgeScanRevision;
|
||||
}
|
||||
// ⚠️⚠️ **交互帧用小预算 + 时间上限**:288 个信号合并到同一网格、开启中心轴边沿计数时,
|
||||
// 拖动横向光标每帧要 30ms(实测),界面完全跟不上手。现在:
|
||||
// * 采样对配额只做“公平分摊”(不让第一个状态吃光全帧);
|
||||
// * 真正的总开销由 `m_edgeScanDeadline`(时间)限制 —— 普通计数因此能一次推进得很快,
|
||||
// 中心轴也不会卡住一帧;
|
||||
// * 加载中(`m_loadingBusy`)完全不推进,避免把文件加载拖慢。
|
||||
const bool interactive = InputIsInteractive();
|
||||
m_edgeScanBudget = m_loadingBusy ? 0
|
||||
: (interactive ? kEdgeScanPairsPerFrameInteractive
|
||||
: kEdgeScanPairsPerFrame);
|
||||
{
|
||||
LARGE_INTEGER frequency = {};
|
||||
LARGE_INTEGER now = {};
|
||||
QueryPerformanceFrequency(&frequency);
|
||||
QueryPerformanceCounter(&now);
|
||||
const double budgetMs = m_loadingBusy ? 0.0
|
||||
: (interactive ? kEdgeScanTimeBudgetInteractiveMs
|
||||
: kEdgeScanTimeBudgetMs);
|
||||
m_edgeScanDeadline =
|
||||
now.QuadPart + static_cast<std::int64_t>(static_cast<double>(frequency.QuadPart) *
|
||||
budgetMs / 1000.0);
|
||||
}
|
||||
// 双缓冲:清掉上一帧钉住的曲线条目(本帧会按需重新钉住),并清理已被更新条目覆盖 /
|
||||
// 超出内存预算的旧条目。此刻没有任何条目被钉住,回收是安全的。
|
||||
m_curvePins.clear();
|
||||
@@ -3835,71 +3857,25 @@ namespace jdi
|
||||
mix((peakItem != nullptr && peakItem->collapsed) ? 1ull : 0ull);
|
||||
}
|
||||
|
||||
// ⚠️⚠️ **边沿统计的数字本身就是面板内容,必须进缓存键**(用户报告的缺陷根因):
|
||||
// 面板像素图层按本键缓存,而界面上的「上升沿 / 下降沿」是**分帧推进**出来的中间结果 ——
|
||||
// 若这些数字不进键,则「统计推进 → 数字变化」不会让键变化 ⇒ 面板一直贴旧像素
|
||||
// ⇒ 用户不动鼠标时统计永远停在“统计中…”或某个中间值,**只有移动鼠标**(改变别的键)
|
||||
// 才会显示出早已算完的正确结果。
|
||||
// ⇒ 把该面板当前要显示的 (上升沿, 下降沿, 是否扫完) 一并 mix 进去:推进会让键变化 ⇒
|
||||
// 面板重绘 ⇒ 显示随进度更新,直到扫完(扫完后数字不再变,重新命中缓存)。
|
||||
// ⚠️⚠️ **边沿统计的数字本身就是面板内容,数字变化必须能让面板重绘**:
|
||||
// 界面上的「上升沿 / 下降沿」是**分帧推进**出来的中间结果,若数字变化不改变键,
|
||||
// 面板就会一直贴旧像素 ⇒ 用户看到"永远统计中…"(只有移动鼠标/悬停面板才更新)。
|
||||
//
|
||||
// ⚠️⚠️ **但交互期间必须跳过这一整段**(用户 2026-11-02 报告“拖动横向光标卡顿”):
|
||||
// * 计算这一段本身要为**每个信号**调一次 `EdgeCountsFor` / `EdgeCenterCountsFor`
|
||||
// (含区间边界块扫描)—— 288 个信号时单帧 288 次查询;
|
||||
// * 而且中间数字每帧都在变 ⇒ 键每帧都变 ⇒ 面板像素缓存**永远不命中**,
|
||||
// 于是每帧都要把整块面板(288 行 × 十来个 GDI+ 原语)重画一遍 ⇒ 实测 30ms/帧。
|
||||
// 拖动/鼠标移动期间数字本来就在飞速重算,**显示中间值毫无意义**;此时让键保持稳定
|
||||
// (用一个固定常量占位)⇒ 面板直接贴上一帧像素 ⇒ 真正跟手。鼠标停下
|
||||
// `kEdgeScanInteractiveIdleMs` 之后恢复完整键 ⇒ 面板重画并显示最终数字。
|
||||
const bool interactiveKey = InputIsInteractive();
|
||||
if (kind == EdgePanelKind::COUNT && !interactiveKey)
|
||||
// 但**不能**在这里为每个信号调一次 `EdgeCountsFor` / `EdgeCenterCountsFor`:
|
||||
// * 288 个信号时单帧 288 次查询(每次还要线性查找累积状态);
|
||||
// * 中间数字每帧都在变 ⇒ 键每帧都变 ⇒ 面板像素缓存**永远不命中**,
|
||||
// 于是每帧都要把整块面板(上百行 × 十来个 GDI+ 原语)重画一遍 ⇒ 实测 30ms/帧。
|
||||
//
|
||||
// ⇒ 改为混合一个**统计数据版本号**(O(1)、零查询):
|
||||
// `EdgeScanFor` 每推进/完成一次就自增 `m_edgeScanRevision`;`Paint` 开头把它
|
||||
// 转成 `m_panelStatsVersion`(非交互帧立刻同步;交互帧节流,避免每帧重画)。
|
||||
// 于是"统计推进 ⇒ 版本号变化 ⇒ 面板重绘",而"没推进 ⇒ 键稳定 ⇒ 贴回缓存"。
|
||||
if (kind == EdgePanelKind::COUNT || kind == EdgePanelKind::CENTER)
|
||||
{
|
||||
for (const SignalRef& ref : lane.signals)
|
||||
{
|
||||
const ResolvedSignal resolved = ResolveSignal(ref);
|
||||
if (resolved.data == nullptr || resolved.style == nullptr || resolved.axis == nullptr ||
|
||||
!resolved.visible)
|
||||
{
|
||||
continue; // 与绘制时的可见性判定保持一致
|
||||
}
|
||||
std::size_t rising = 0;
|
||||
std::size_t falling = 0;
|
||||
bool complete = true;
|
||||
EdgeCountsFor(*resolved.axis, *resolved.data, *resolved.style, lane.edgeValue,
|
||||
EdgeRangeActive(), EdgeRangeMin(), EdgeRangeMax(), rising, falling, complete);
|
||||
mix(static_cast<std::uint64_t>(rising));
|
||||
mix(static_cast<std::uint64_t>(falling));
|
||||
mix(complete ? 1ull : 0ull);
|
||||
}
|
||||
}
|
||||
else if (kind == EdgePanelKind::CENTER && !interactiveKey)
|
||||
{
|
||||
// 「中心轴边沿计数」是**表格**:每个“已勾选统计”的行都会显示两列数字,
|
||||
// 因此 `rowRefs` 里凡是 `FindEdgeCenterItem != nullptr` 的都要把数字混进键
|
||||
//(⚠️ 表格没有 `rowStats` 了,不能再按它门控 —— 否则数字变化不会让键变化,
|
||||
// 面板会永远停在“统计中…”)。
|
||||
for (std::size_t rowIndex = 0; rowIndex < layout.rowRefs.size(); ++rowIndex)
|
||||
{
|
||||
const SignalRef& ref = layout.rowRefs[rowIndex];
|
||||
const ResolvedSignal resolved = ResolveSignal(ref);
|
||||
const Lane::EdgeCenterItem* item = lane.FindEdgeCenterItem(ref);
|
||||
if (resolved.data == nullptr || resolved.style == nullptr || resolved.axis == nullptr ||
|
||||
item == nullptr)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
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);
|
||||
mix(static_cast<std::uint64_t>(rising));
|
||||
mix(static_cast<std::uint64_t>(falling));
|
||||
mix(complete ? 1ull : 0ull);
|
||||
}
|
||||
mix(m_panelStatsVersion);
|
||||
}
|
||||
|
||||
|
||||
// 弱峰调整的滑块行矩形(每信号两行)。
|
||||
for (const gd::RectF& rect : layout.rowLevelTracks)
|
||||
{
|
||||
@@ -4329,6 +4305,15 @@ namespace jdi
|
||||
{
|
||||
return;
|
||||
}
|
||||
// ⚠️ **加载中跳过全部统计面板绘制**:加载线程主循环不断 `UpdateWindow` 触发整帧绘制,
|
||||
// 而这里含统计推进与上百行 GDI+ 原语 ⇒ 加载进度被拖慢(用户报告“统计没算完就无法
|
||||
// 加载文件”)。加载遮罩会盖住整个界面,面板本来也看不见。
|
||||
if (m_loadingBusy)
|
||||
{
|
||||
m_panelSlots.clear();
|
||||
m_panelValid = false;
|
||||
return;
|
||||
}
|
||||
|
||||
std::vector<Lane>& tabLanes = m_tab->Lanes();
|
||||
const std::size_t count = std::min(lanes.size(), tabLanes.size());
|
||||
@@ -4541,12 +4526,11 @@ namespace jdi
|
||||
const bool meanFilter = (kind == EdgePanelKind::MEAN);
|
||||
const bool weakPeak = (kind == EdgePanelKind::PEAK);
|
||||
const bool freeMarker = (kind == EdgePanelKind::MARK);
|
||||
// ⚠️ 「均值滤波」「弱峰调整」= 可滚动的分组列表(条目栏 + 展开后的两行滑块);
|
||||
// **「中心轴边沿计数」= 表格**(表头:信号名 / 上升沿计数 / 下降沿计数),单独一支。
|
||||
// 三者都**不能**再走旧的逐条目分支:那个分支没有任何裁切,滚动时会画到标题栏上
|
||||
//(用户 2026-10-30 报告“元素超出浮动面板”的根因)。
|
||||
const bool listStyle = (meanFilter || weakPeak);
|
||||
const bool centerTable = centerAxis;
|
||||
// ⚠️ 「均值滤波」「中心轴边沿计数」「弱峰调整」= 可滚动的分组列表(条目栏 + 展开后的参数行);
|
||||
// **「边沿计数」= 表格**(表头:信号名 / 上升沿计数 / 下降沿计数),单独一支。
|
||||
// 它们都**不能**再走旧的逐条目分支:那个分支没有任何裁切,滚动时会画到标题栏上。
|
||||
const bool listStyle = (meanFilter || centerAxis || weakPeak);
|
||||
const bool countTable = (kind == EdgePanelKind::COUNT);
|
||||
const bool hasButton = (meanFilter || weakPeak);
|
||||
{
|
||||
if (index >= count)
|
||||
@@ -4678,7 +4662,6 @@ namespace jdi
|
||||
PanelTextCollector panelTextBoxes(UiGetTextRectSink());
|
||||
|
||||
const float titleHeight = kEdgePanelTitleHeight * m_scale;
|
||||
const float rowHeight = kEdgePanelRowHeight * m_scale;
|
||||
const float pad = kEdgePanelPad * m_scale;
|
||||
|
||||
// 以下整段(面板底色 → 标题栏 → 各控件 → 行内容)的画字都要走本地收集器。
|
||||
@@ -4778,37 +4761,12 @@ namespace jdi
|
||||
if (!layout.collapsed)
|
||||
{
|
||||
|
||||
// 「中心轴边沿计数」= **表格**:表头(信号名 / 上升沿计数 / 下降沿计数)**固定不滚动**,
|
||||
// 「边沿计数」= **表格**:表头(信号名 / 上升沿计数 / 下降沿计数)**固定不滚动**,
|
||||
// 下面每条可见信号一行,行底色 = 该信号的配色,三列与表头严格对齐。
|
||||
// 单击行(非复选框)展开 / 收起该信号自己的两行参数滑块。
|
||||
if (centerTable)
|
||||
// 内容超高时右侧出现滚动条(只占内容区,不覆盖表头)。
|
||||
if (countTable)
|
||||
{
|
||||
// 复选框自绘(在信号色叠底上用**自适应对比色**)。
|
||||
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);
|
||||
@@ -4821,7 +4779,7 @@ namespace jdi
|
||||
}
|
||||
const float textLeft =
|
||||
(column == 0) ? (cell.left + 4.0f * m_scale) : cell.left;
|
||||
UiTextClipped(g, EdgeCenterTableHeader(column), m_fonts->uiSmall,
|
||||
UiTextClipped(g, EdgeCountTableHeader(column), m_fonts->uiSmall,
|
||||
gd::RectF(textLeft, layout.tableHeader.Y, cell.width,
|
||||
layout.tableHeader.Height),
|
||||
m_theme->textMuted,
|
||||
@@ -4834,18 +4792,10 @@ namespace jdi
|
||||
// 行内容必须裁到内容区:滚动时行会越出可视范围。
|
||||
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())
|
||||
rowIndex >= layout.rowNameRects.size())
|
||||
{
|
||||
break;
|
||||
}
|
||||
@@ -4856,80 +4806,33 @@ namespace jdi
|
||||
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 ||
|
||||
if (row.Y + row.Height <= 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);
|
||||
}
|
||||
// 行底色 = 该信号的配色(用户 2026-11-02 要求)。
|
||||
UiFillRoundRect(g, row, 3.0f * m_scale, signalColor);
|
||||
UiFillRoundRect(g, row, 3.0f * m_scale, WithAlpha(signalColor, 0xE8));
|
||||
|
||||
// 信号名(第一列):超出列宽时省略。
|
||||
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,
|
||||
m_fonts->uiSmall, nameRect, ink.text, 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)
|
||||
if (resolved.axis != nullptr)
|
||||
{
|
||||
EdgeCenterCountsFor(*resolved.axis, *resolved.data, *resolved.style,
|
||||
item->points, item->iterations, EdgeRangeActive(),
|
||||
EdgeRangeMin(), EdgeRangeMax(), rising, falling, complete);
|
||||
EdgeCountsFor(*resolved.axis, *resolved.data, *resolved.style,
|
||||
tabLane.edgeValue, EdgeRangeActive(), EdgeRangeMin(),
|
||||
EdgeRangeMax(), rising, falling, complete);
|
||||
}
|
||||
for (int column = 1; column < 3; ++column)
|
||||
{
|
||||
@@ -4938,109 +4841,12 @@ namespace jdi
|
||||
{
|
||||
continue;
|
||||
}
|
||||
std::wstring text;
|
||||
if (checked)
|
||||
{
|
||||
text = EdgeCountCellText((column == 1) ? rising : falling, complete);
|
||||
}
|
||||
const std::wstring text =
|
||||
EdgeCountCellText((column == 1) ? rising : falling, complete);
|
||||
UiTextClipped(g, text, m_fonts->monoSmall,
|
||||
gd::RectF(cell.left, row.Y, cell.width, row.Height), rowInk,
|
||||
gd::RectF(cell.left, row.Y, cell.width, row.Height), ink.text,
|
||||
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);
|
||||
@@ -5055,7 +4861,6 @@ namespace jdi
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 「均值滤波」面板:逐信号的**信号色叠底标题栏**(左侧展开三角 + 信号名 +
|
||||
// 右端复选框)。复选框勾选后才出现三角、才能展开下面的两行参数滑块
|
||||
// (「均值点数」/「迭代次数」);内容超高时可滚动(滚动条在内容区右侧)。
|
||||
@@ -5156,12 +4961,14 @@ namespace jdi
|
||||
}
|
||||
const std::uint32_t signalColor = StyleColor(*resolved.style);
|
||||
const UiInk ink = UiContrastInk(signalColor);
|
||||
// 「均值滤波」看 `meanFilterItems`、「弱峰调整」看 `weakPeakItems`
|
||||
//(中心轴边沿计数已改为独立的表格分支)。
|
||||
// 「均值滤波」看 `meanFilterItems`、「中心轴边沿计数」看 `edgeCenterItems`、
|
||||
//「弱峰调整」看 `weakPeakItems`(前两者同为 `Lane::MeanFilterItem`)。
|
||||
const Lane::WeakPeakItem* peakItem =
|
||||
weakPeak ? tabLane.FindWeakPeakItem(ref) : nullptr;
|
||||
const Lane::MeanFilterItem* item =
|
||||
weakPeak ? nullptr : tabLane.FindMeanFilterItem(ref);
|
||||
weakPeak ? nullptr
|
||||
: (centerAxis ? tabLane.FindEdgeCenterItem(ref)
|
||||
: tabLane.FindMeanFilterItem(ref));
|
||||
const bool checked = weakPeak ? (peakItem != nullptr) : (item != nullptr);
|
||||
const bool rowCollapsed =
|
||||
weakPeak ? (peakItem != nullptr && peakItem->collapsed)
|
||||
@@ -5209,6 +5016,23 @@ 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;
|
||||
if (resolved.axis != nullptr)
|
||||
{
|
||||
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,
|
||||
@@ -5756,64 +5580,6 @@ namespace jdi
|
||||
}
|
||||
}
|
||||
|
||||
// 「边沿计数」:逐信号两行(名称 / 上升沿·下降沿)。内容区**可滚动且按内容区裁剪**
|
||||
//(上百个信号时内容高达上万像素:限高后出现滚动条,面板高度被绘图区夹住,
|
||||
// 面板像素缓存才存得进去 —— 否则每帧整块重画,所有交互都会卡顿)。
|
||||
{
|
||||
const int contentState = g.Save();
|
||||
g.SetClip(layout.content, gd::CombineModeIntersect);
|
||||
float rowY = layout.content.Y - layout.scrollOffset;
|
||||
for (const SignalRef& ref : tabLane.signals)
|
||||
{
|
||||
const ResolvedSignal resolved = ResolveSignal(ref);
|
||||
if (resolved.data == nullptr || resolved.style == nullptr || !resolved.visible)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (freeMarker || listStyle || centerTable)
|
||||
{
|
||||
// 这些排布已在上面画完(各自有自己的可滚动 / 表格分支,含裁切)。
|
||||
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(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);
|
||||
}
|
||||
}
|
||||
|
||||
// 逐条目面板底部按钮行:「构建为新信号」(悬停高亮);
|
||||
// 「自由标记」面板的底部固定区有三行:
|
||||
|
||||
Reference in new issue
Block a user