refactor(panels): 删除 rowPoints/rowIterations 死列 + 提取 PaintPanelResampleRows

对应 Code_Review_Report §14.5 第 1 项与第 3 项。

第 1 项:删除 EdgePanelLayout::rowPoints / rowIterations(行数组 18 → 16)
- 写入方只有 AppendRow(推空矩形),读取方只有调试框 AddAll 与 PanelSlotKey 的 mix 循环,
  绘制方零(旧式“点数 / 迭代次数”输入框早被滑块版 rowLevelTracks / rowIterTracks 取代)。
- 连带同步:AppendRow、调试框、缓存键、rowgeom 导出、panels/panelscroll 长度不变式、
  文档里的列数(18/14 → 16/12)。

等价性由基线证伪,而不是靠人工 grep:
  old lines = 983, new lines = 919
  only in OLD = 64, only in NEW = 0
  only-in-OLD lines NOT belonging to rowPoints/rowIterations = 0
即除被删两列外,其余 919 条键逐字节相同。

第 3 项(前半):提取 PlotView::PaintPanelResampleRows
- PaintPanelListRows 里 `if (resamplePanel) { …; continue; }` 共 132 行整块提取,
  签名 (g, layout, rowIndex, laneIndex, ref, item);两个局部 lambda
  (paintInfoRow 只读统计行 / paintInputRow 联动输入框)随之迁移。
- maxInterval / minInterval 两个纯拷贝局部量直接用 item.maxInterval / item.minInterval。
- 通用参数行(中心轴统计 + 两行滑块)未提取:需 9 个参数,签名比函数体更难读。

§14.5 第 2 项(完整结构合并)本轮不做,方案已定并写入文档:
- 列视图(PanelColumn<T>)方案否决:拷贝忘重绑 = 静默悬垂,需额外自检兜住;
- 若要做就做“vector<PanelRow> + 具名成员访问(无视图)”,242 处读点(127 处命中判定),
  必须单独一轮。它属整洁性收益(漏推一列已被 AppendRow + 长度不变式挡住),不是紧急项。

验证
- make release:零 warning;785,408 → 784,896 B(−512);make debug 同样零 warning
- make -C Tools:零 warning
- UnitTests:checks=260 failed=0 cases=19
- OffscreenChecks -all:7 检查 / 127 断言全 PASS
- panels -mode compare:7 面板像素哈希 + 几何逐项不变(resample=2BFC74B3D9B3239C)
- rowgeom -mode compare:919 + 7 键,0 差异(基线已按 16 列重录)
- PlotViewPaint.cpp 5575 → 5565;PlotViewPanelPaint.cpp 1016 → 1020

文档:Plan §十、Progress 第 38 轮、Report §15、agent_mem 第 38 轮、Tools/README §4.4
This commit is contained in:
DeepSeek V4.1 Flash committed 2026-11-04 16:02:15 +08:00
1 parent 353c553721
commit f07ac74988
9 files changed
+384 -150

No files matched your search

-10
View File
@@ -815,8 +815,6 @@ namespace jdi
boxes.Add(layout.clipDestructiveButton, 3);
boxes.AddAll(layout.rowChecks, 4);
boxes.AddAll(layout.rowPoints, 4);
boxes.AddAll(layout.rowIterations, 4);
boxes.AddAll(layout.rowStats, 4);
boxes.AddAll(layout.rowLevelTracks, 4);
boxes.AddAll(layout.rowLevelValues, 4);
@@ -4088,14 +4086,6 @@ namespace jdi
{
mixRect(rect);
}
for (const gd::RectF& rect : layout.rowPoints)
{
mixRect(rect);
}
for (const gd::RectF& rect : layout.rowIterations)
{
mixRect(rect);
}
for (const gd::RectF& rect : layout.rowStats)
{
mixRect(rect);
+121 -117
View File
@@ -328,124 +328,10 @@ namespace jdi
// 「重采样」:四行 = 最大间隔(只读)/ 最小间隔(只读)/
// 重采样间隔(输入)/ 重采样频率(输入)。标签在左、数值在右。
// 排布细节见 `PaintPanelResampleRows`(从本循环里提取的独立函数)。
if (resamplePanel)
{
const float labelWidth =
UiMeasureTextWidth(m_fonts->uiSmall, kResampleMaxIntervalLabel);
const auto paintInfoRow = [&](const gd::RectF& row, const wchar_t* label,
const std::wstring& text)
{
if (row.Width <= 0.0f)
{
return;
}
UiTextClipped(g, label, m_fonts->uiSmall,
gd::RectF(row.X, row.Y, std::max(10.0f, labelWidth),
row.Height),
m_theme->textMuted, TextAlign::LEFT, layout.content);
// ⚠️ 统计数字**绝不省略**(用户 2026-11-03 要求):
// 面板宽度已按真实文本量宽(`EdgePanelWidth` 逐信号实测),
// 因此这里用 `TextAlign::RIGHT` + 完整矩形即可;
// 万一仍被窗口边界夹住,也宁可超出面板右缘(由内容区裁剪兜底),
// **不要**退化成 `…` 让用户看不到真实数值。
UiTextClipped(g, text, m_fonts->monoSmall,
gd::RectF(row.X + labelWidth + 8.0f * m_scale, row.Y,
std::max(10.0f, row.Width - labelWidth -
8.0f * m_scale),
row.Height),
m_theme->textStrong, TextAlign::RIGHT,
layout.content);
};
const auto paintInputRow = [&](const gd::RectF& field, const wchar_t* label,
const std::wstring& text, bool editing)
{
if (field.Width <= 0.0f)
{
return;
}
// 标签与数值同高:标签从内容区左缘画到字段左缘。
UiTextClipped(
g, label, m_fonts->uiSmall,
gd::RectF(layout.content.X, field.Y,
std::max(10.0f, field.X - 8.0f * m_scale - layout.content.X),
field.Height),
m_theme->textMuted, TextAlign::LEFT, layout.content);
if (editing)
{
return; // 正在编辑:内容由 `PaintEditField` 画
}
const gd::RectF box(field.X, field.Y + 2.0f * m_scale, field.Width,
std::max(10.0f, field.Height - 4.0f * m_scale));
UiFillRoundRect(g, box, 3.0f * m_scale, m_theme->pressed);
UiDrawRoundRect(g, box, 3.0f * m_scale, m_theme->border, 1.0f);
UiTextClipped(g, text, m_fonts->monoSmall,
gd::RectF(box.X + 4.0f * m_scale, box.Y,
std::max(6.0f, box.Width - 8.0f * m_scale),
box.Height),
m_theme->textStrong, TextAlign::LEFT, layout.content);
};
double maxInterval = 0.0;
double minInterval = 0.0;
const bool hasInterval =
(resampleItem->maxInterval > 0.0) && (resampleItem->minInterval > 0.0);
maxInterval = resampleItem->maxInterval;
minInterval = resampleItem->minInterval;
// 「最大 / 最小数据间隔」= 自适应单位的**小数**文本(绝不用科学计数法),
// 末尾追加一个括号内的「对应频率」尾缀(单位在 Hz/kHz/MHz/GHz 之间自适应)。
//
// ⚠️⚠️ 全部使用**真实值**(用户 2026-11-03 明确要求):
// * 时间间隔:`FormatDurationCompound` 会把次级单位保留 3 位小数
// ⇒ `0.003696631s` 显示为 `3ms 696.631us`(旧实现舍入到 3 位有效数字
// 得到 `3ms 700us`,把真实尾数抹掉了);
// * 频率:由**未舍入的真实间隔**换算 ⇒ `270.517 Hz`(旧实现用舍入后的
// `3.70ms` 换算得到 `270.27 Hz`,与输入框不一致)。
// 两条路径都用真实值 ⇒ 统计行与输入框天然一致。
const auto intervalWithFrequency = [](double seconds) -> std::wstring
{
std::wstring text = FormatDurationCompound(seconds);
if (std::isfinite(seconds) && seconds > 0.0)
{
text += L"(";
text += FormatFrequencyText(1.0 / seconds);
text += L")";
}
return text;
};
if (rowIndex < layout.rowInfoMaxInterval.size())
{
paintInfoRow(layout.rowInfoMaxInterval[rowIndex], kResampleMaxIntervalLabel,
hasInterval ? intervalWithFrequency(maxInterval) : L"—");
}
if (rowIndex < layout.rowInfoMinInterval.size())
{
paintInfoRow(layout.rowInfoMinInterval[rowIndex], kResampleMinIntervalLabel,
hasInterval ? intervalWithFrequency(minInterval) : L"—");
}
const double interval = (resampleItem->interval > 0.0)
? resampleItem->interval
: minInterval;
const bool editingInterval =
(m_editTarget == PlotEditTarget::RESAMPLE_INTERVAL) &&
(m_editLane == static_cast<int>(index)) && (m_editMeanRef == ref);
const bool editingFrequency =
(m_editTarget == PlotEditTarget::RESAMPLE_FREQUENCY) &&
(m_editLane == static_cast<int>(index)) && (m_editMeanRef == ref);
if (rowIndex < layout.rowIntervalField.size())
{
// 非编辑态:自适应单位 + 小数(与「最大/最小数据间隔」同一套单位形式)。
// 编辑态:只显示**秒**数值(不带单位),由 `PaintEditField` 绘制。
paintInputRow(layout.rowIntervalField[rowIndex], kResampleIntervalLabel,
FormatIntervalText(interval), editingInterval);
}
if (rowIndex < layout.rowFrequencyField.size())
{
// ⚠️ 频率同样用**真实间隔**换算(与时间间隔行同源)⇒ `270.517 Hz`。
const double hertz = (interval > 0.0) ? (1.0 / interval) : 0.0;
// 非编辑态:频率单位自适应(Hz/kHz/MHz/GHz);编辑态:只显示 Hz 数值。
paintInputRow(layout.rowFrequencyField[rowIndex], kResampleFrequencyLabel,
(hertz > 0.0) ? FormatFrequencyText(hertz) : L"—",
editingFrequency);
}
PaintPanelResampleRows(g, layout, rowIndex, index, ref, *resampleItem);
continue;
}
@@ -669,7 +555,125 @@ namespace jdi
PaintPanelScrollBar(g, layout, *m_theme);
}
/**
/**
* @brief 「重采样」面板**单个信号的参数区**(四行)。
*
* 从 `PaintPanelListRows` 里整块提取(第 38 轮):这四行的排布(两行只读统计 +
* 两行联动输入框、标签在左数值在右)与其它面板的滑块参数行完全不同,
* 混在同一个行循环里只会让"条目栏 / 参数行 / 重采样行"三种排版互相纠缠。
* **零逻辑改动**,判据 = `rowgeom -mode compare` + `panels -mode compare` 全绿。
*/
void PlotView::PaintPanelResampleRows(gd::Graphics& g, const EdgePanelLayout& layout,
std::size_t rowIndex, std::size_t laneIndex,
const SignalRef& ref, const Lane::ResampleItem& item)
{
const std::size_t index = laneIndex;
const float labelWidth =
UiMeasureTextWidth(m_fonts->uiSmall, kResampleMaxIntervalLabel);
const auto paintInfoRow = [&](const gd::RectF& row, const wchar_t* label,
const std::wstring& text)
{
if (row.Width <= 0.0f)
{
return;
}
UiTextClipped(g, label, m_fonts->uiSmall,
gd::RectF(row.X, row.Y, std::max(10.0f, labelWidth), row.Height),
m_theme->textMuted, TextAlign::LEFT, layout.content);
// ⚠️ 统计数字**绝不省略**(用户 2026-11-03 要求):
// 面板宽度已按真实文本量宽(`EdgePanelWidth` 逐信号实测),
// 因此这里用 `TextAlign::RIGHT` + 完整矩形即可;
// 万一仍被窗口边界夹住,也宁可超出面板右缘(由内容区裁剪兜底),
// **不要**退化成 `…` 让用户看不到真实数值。
UiTextClipped(g, text, m_fonts->monoSmall,
gd::RectF(row.X + labelWidth + 8.0f * m_scale, row.Y,
std::max(10.0f, row.Width - labelWidth - 8.0f * m_scale),
row.Height),
m_theme->textStrong, TextAlign::RIGHT, layout.content);
};
const auto paintInputRow = [&](const gd::RectF& field, const wchar_t* label,
const std::wstring& text, bool editing)
{
if (field.Width <= 0.0f)
{
return;
}
// 标签与数值同高:标签从内容区左缘画到字段左缘。
UiTextClipped(
g, label, m_fonts->uiSmall,
gd::RectF(layout.content.X, field.Y,
std::max(10.0f, field.X - 8.0f * m_scale - layout.content.X),
field.Height),
m_theme->textMuted, TextAlign::LEFT, layout.content);
if (editing)
{
return; // 正在编辑:内容由 `PaintEditField` 画
}
const gd::RectF box(field.X, field.Y + 2.0f * m_scale, field.Width,
std::max(10.0f, field.Height - 4.0f * m_scale));
UiFillRoundRect(g, box, 3.0f * m_scale, m_theme->pressed);
UiDrawRoundRect(g, box, 3.0f * m_scale, m_theme->border, 1.0f);
UiTextClipped(g, text, m_fonts->monoSmall,
gd::RectF(box.X + 4.0f * m_scale, box.Y,
std::max(6.0f, box.Width - 8.0f * m_scale), box.Height),
m_theme->textStrong, TextAlign::LEFT, layout.content);
};
const bool hasInterval = (item.maxInterval > 0.0) && (item.minInterval > 0.0);
// 「最大 / 最小数据间隔」= 自适应单位的**小数**文本(绝不用科学计数法),
// 末尾追加一个括号内的「对应频率」尾缀(单位在 Hz/kHz/MHz/GHz 之间自适应)。
//
// ⚠️⚠️ 全部使用**真实值**(用户 2026-11-03 明确要求):
// * 时间间隔:`FormatDurationCompound` 会把次级单位保留 3 位小数
// ⇒ `0.003696631s` 显示为 `3ms 696.631us`(旧实现舍入到 3 位有效数字
// 得到 `3ms 700us`,把真实尾数抹掉了);
// * 频率:由**未舍入的真实间隔**换算 ⇒ `270.517 Hz`(旧实现用舍入后的
// `3.70ms` 换算得到 `270.27 Hz`,与输入框不一致)。
// 两条路径都用真实值 ⇒ 统计行与输入框天然一致。
const auto intervalWithFrequency = [](double seconds) -> std::wstring
{
std::wstring text = FormatDurationCompound(seconds);
if (std::isfinite(seconds) && seconds > 0.0)
{
text += L"(";
text += FormatFrequencyText(1.0 / seconds);
text += L")";
}
return text;
};
if (rowIndex < layout.rowInfoMaxInterval.size())
{
paintInfoRow(layout.rowInfoMaxInterval[rowIndex], kResampleMaxIntervalLabel,
hasInterval ? intervalWithFrequency(item.maxInterval) : L"—");
}
if (rowIndex < layout.rowInfoMinInterval.size())
{
paintInfoRow(layout.rowInfoMinInterval[rowIndex], kResampleMinIntervalLabel,
hasInterval ? intervalWithFrequency(item.minInterval) : L"—");
}
const double interval = (item.interval > 0.0) ? item.interval : item.minInterval;
const bool editingInterval = (m_editTarget == PlotEditTarget::RESAMPLE_INTERVAL) &&
(m_editLane == static_cast<int>(index)) && (m_editMeanRef == ref);
const bool editingFrequency = (m_editTarget == PlotEditTarget::RESAMPLE_FREQUENCY) &&
(m_editLane == static_cast<int>(index)) && (m_editMeanRef == ref);
if (rowIndex < layout.rowIntervalField.size())
{
// 非编辑态:自适应单位 + 小数(与「最大/最小数据间隔」同一套单位形式)。
// 编辑态:只显示**秒**数值(不带单位),由 `PaintEditField` 绘制。
paintInputRow(layout.rowIntervalField[rowIndex], kResampleIntervalLabel,
FormatIntervalText(interval), editingInterval);
}
if (rowIndex < layout.rowFrequencyField.size())
{
// ⚠️ 频率同样用**真实间隔**换算(与时间间隔行同源)⇒ `270.517 Hz`。
const double hertz = (interval > 0.0) ? (1.0 / interval) : 0.0;
// 非编辑态:频率单位自适应(Hz/kHz/MHz/GHz);编辑态:只显示 Hz 数值。
paintInputRow(layout.rowFrequencyField[rowIndex], kResampleFrequencyLabel,
(hertz > 0.0) ? FormatFrequencyText(hertz) : L"—", editingFrequency);
}
}
/**
* @brief Marker panel content: per-signal group bars (collapsible) + table header +
* fixed-point rows, with sticky group bars.
*