From f07ac749885b53f789b0171b60bbb32ceb1f92b5 Mon Sep 17 00:00:00 2001 From: "DeepSeek V4.1 Flash" <> Date: Wed, 4 Nov 2026 16:02:15 +0800 Subject: [PATCH] =?UTF-8?q?refactor(panels):=20=E5=88=A0=E9=99=A4=20rowPoi?= =?UTF-8?q?nts/rowIterations=20=E6=AD=BB=E5=88=97=20+=20=E6=8F=90=E5=8F=96?= =?UTF-8?q?=20PaintPanelResampleRows?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 对应 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)方案否决:拷贝忘重绑 = 静默悬垂,需额外自检兜住; - 若要做就做“vector + 具名成员访问(无视图)”,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 --- Components/PlotView/Inc/PlotView.hpp | 34 ++- Components/PlotView/Src/PlotViewPaint.cpp | 10 - .../PlotView/Src/PlotViewPanelPaint.cpp | 238 +++++++++--------- Documents/Code_Review_Plan.md | 36 +++ Documents/Code_Review_Progress.md | 59 +++++ Documents/Code_Review_Report.md | 73 ++++++ Documents/agent_mem.md | 57 +++++ Tools/OffscreenChecks/Src/Checks.cpp | 25 +- Tools/README.md | 2 +- 9 files changed, 384 insertions(+), 150 deletions(-) diff --git a/Components/PlotView/Inc/PlotView.hpp b/Components/PlotView/Inc/PlotView.hpp index 98609ee..fc8fb69 100644 --- a/Components/PlotView/Inc/PlotView.hpp +++ b/Components/PlotView/Inc/PlotView.hpp @@ -2021,13 +2021,15 @@ namespace jdi * @brief 「逐条目」面板的矩形(MEAN / CENTER / PEAK 面板共用同一套排布)。 * * 索引与 `rowRefs` 一一对应(即网格内可见信号的顺序): - * `rowChecks` 为第一排右侧的复选框;`rowPoints` / `rowIterations` 为**输入框** - *(MEAN / CENTER 使用);`rowStats` 为第三排的统计结果(中心轴边沿计数专用)。 + * `rowChecks` 为第一排右侧的复选框;`rowStats` 为第三排的统计结果(中心轴边沿计数专用)。 * 未勾选的条目对应的矩形均为空。 + * + * ⚠️ 2026-11-04 删除了 `rowPoints` / `rowIterations`:它们是**旧式输入框**(点数 / 迭代次数) + * 的矩形,自滑块版排版(`rowLevelTracks` / `rowIterTracks`)上线后就没有绘制方读它们了 —— + * 所有写入都是空矩形,读取方只剩调试框与缓存键。保留它们只会让每次 `AppendRow` + * 多两次 `push_back`、并且误导后续读者以为“这里还有一套输入框”。 */ std::vector rowChecks; - std::vector rowPoints; - std::vector rowIterations; std::vector rowStats; /** * @brief 「弱峰调整」每个被勾选信号自己的两行**滑块**(与 `rowRefs` 同序)。 @@ -2215,7 +2217,7 @@ namespace jdi /** * @brief 追加一**行**(条目栏 / 名称 / 三角 / 复选框),其余列一律补空矩形。 * - * ⚠️⚠️ 这是**唯一**允许新增行的入口。面板每行对应 18 个平行数组 + * ⚠️⚠️ 这是**唯一**允许新增行的入口。面板每行对应 16 个平行数组 * (`rowRefs` / `rowHeaders` / `rowChecks` / `rowLevelTracks` / …); * 如果每个排版分支各写一遍 `push_back`,只要某分支漏推一项, * 那一列之后的所有行都会**读到别人的矩形**("改一次布局就出错"的典型机制)。 @@ -2236,8 +2238,6 @@ namespace jdi rowNameRects.push_back(nameRect); rowTriangles.push_back(triangle); rowChecks.push_back(check); - rowPoints.push_back(gd::RectF()); - rowIterations.push_back(gd::RectF()); rowStats.push_back(gd::RectF()); rowLevelTracks.push_back(gd::RectF()); rowLevelValues.push_back(gd::RectF()); @@ -2280,6 +2280,26 @@ namespace jdi void PaintPanelListRows(gd::Graphics& g, const EdgePanelLayout& layout, const Lane& tabLane, EdgePanelKind kind, std::size_t laneIndex); + /** + * @brief 「重采样」面板**单个信号的参数区**(四行:最大间隔 / 最小间隔 / 间隔输入 / 频率输入)。 + * + * 从 `PaintPanelListRows` 里提取:这四行的排布与其它面板完全不同(两行只读统计 + 两行 + * 联动输入框、标签在左数值在右),混在同一个行循环里会让"条目栏 / 参数行 / 重采样行" + * 三种排版互相纠缠。提取后调用点只剩一行,`PaintPanelListRows` 只保留 + * "条目栏 + 通用参数行"两段。 + * 定义在 `PlotViewPanelPaint.cpp`。 + * + * @param [IN,OUT] g 绘图上下文(调用方已按内容区裁剪)。 + * @param [IN] layout 面板布局。 + * @param [IN] rowIndex 行下标(取该行的四个矩形)。 + * @param [IN] laneIndex 网格下标(判定"正在编辑的是不是这一行")。 + * @param [IN] ref 该行对应的信号(同上)。 + * @param [IN] item 该信号的「重采样」条目(已确认存在)。 + */ + void PaintPanelResampleRows(gd::Graphics& g, const EdgePanelLayout& layout, std::size_t rowIndex, + std::size_t laneIndex, const SignalRef& ref, + const Lane::ResampleItem& item); + /** * @brief 「自由标记」面板的**内容区**(分组栏 + 表头 + 条目 + 吸顶)。 * diff --git a/Components/PlotView/Src/PlotViewPaint.cpp b/Components/PlotView/Src/PlotViewPaint.cpp index 8edfb81..683687c 100644 --- a/Components/PlotView/Src/PlotViewPaint.cpp +++ b/Components/PlotView/Src/PlotViewPaint.cpp @@ -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); diff --git a/Components/PlotView/Src/PlotViewPanelPaint.cpp b/Components/PlotView/Src/PlotViewPanelPaint.cpp index 1da47b2..6eb1188 100644 --- a/Components/PlotView/Src/PlotViewPanelPaint.cpp +++ b/Components/PlotView/Src/PlotViewPanelPaint.cpp @@ -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(index)) && (m_editMeanRef == ref); - const bool editingFrequency = - (m_editTarget == PlotEditTarget::RESAMPLE_FREQUENCY) && - (m_editLane == static_cast(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(index)) && (m_editMeanRef == ref); + const bool editingFrequency = (m_editTarget == PlotEditTarget::RESAMPLE_FREQUENCY) && + (m_editLane == static_cast(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. * diff --git a/Documents/Code_Review_Plan.md b/Documents/Code_Review_Plan.md index 82222bf..79abde2 100644 --- a/Documents/Code_Review_Plan.md +++ b/Documents/Code_Review_Plan.md @@ -307,3 +307,39 @@ `SetAllVisible(false)`,而"全部不可见 ⇒ 丢弃网格"是既有设计 (`RebuildLanes` 的 MERGED 分支),固定点/面板开关会随之清空 —— 该场景要**重新播**。 +--- + +## 十、第 38 轮追加(2026-11-04):§14.5 第 1 项(删死列)+ 第 3 项前半(拆重采样行) + +> 触发:用户要求「按照上一轮报告中的 §14.5 下一步(按价值排序)继续推进」。 + +| 节点 | 内容 | 验收标准 | +| --- | --- | --- | +| **P31** | 删除 `EdgePanelLayout::rowPoints` / `rowIterations`(写入方只剩 `AppendRow`,读取方只剩调试框与缓存键,几何恒为空) | `rowgeom -mode compare` 的差异**只**来自这两列(64 条键,0 条其它差异);`panels -mode compare` 像素不变 | +| **P32** | 从 `PaintPanelListRows` 提取 `PaintPanelResampleRows`("四行只读统计 + 联动输入框"这一段) | 面板像素哈希(尤其 `resample`)与逐行几何**逐项不变** | + +### 10.1 为什么"删掉一列"也可以证明 + +`rowgeom` 的两层基线正好给出**可证伪**的等价性: + +* 旧基线 983 行、新基线 919 行;集合差 = 只在旧里的 **64** 行、只在新里的 **0** 行; +* 那 64 行**全部**属于 `rowPoints` / `rowIterations`(脚本按正则断言,非人工核对); +* ⇒ 其余 919 条键**逐字节相同**,也就是说"删掉的两列从没被赋予过非空矩形"这一判断被基线证实。 + +### 10.2 本轮新增硬约束 + +1. **`EdgePanelLayout` 行相关的平行数组现在是 16 个**(原 18)。任何"列清单" + (`rowgeom` 导出、长度不变式、调试框、缓存键)都要同步;`AppendRow` 里少推一列 + 仍会被长度不变式当场抓到。 +2. **只读统计/输入框这类"专属排版"要独立成函数**:`PaintPanelResampleRows` 的四行 + 与其它面板的滑块参数行没有共同点,混在同一个行循环里只会让三种排版互相纠缠。 +3. **§14.5 第 2 项(完整结构合并)仍处于"闸门关闭"状态**,未做(理由见 Report §15.3)。 + +### 10.3 §14.5 第 3 项的完成度 + +| 段落 | 状态 | +| --- | --- | +| 重采样行(四行只读+输入) | ✅ 已提取为 `PaintPanelResampleRows` | +| 通用参数行(中心轴统计 + 两行滑块) | ⏸ 未提取:依赖 3 个 kind 标志 + 2 套拖拽/编辑状态 + 2 个滑块 lambda,提取需传 9 个参数,可读性收益低于参数表本身 | +| 条目栏(三角 + 名称 + 复选框 + 悬停) | 保留在 `PaintPanelListRows` 里(它是三种排版的公共前缀) | + diff --git a/Documents/Code_Review_Progress.md b/Documents/Code_Review_Progress.md index 9c8ca8b..79f11fa 100644 --- a/Documents/Code_Review_Progress.md +++ b/Documents/Code_Review_Progress.md @@ -491,3 +491,62 @@ SUMMARY checks=32 failed=1 groups=1 result=FAIL | `rowgeom -mode compare -data MF4-Record.MF4` | `detail 1304 键 + scale 7 键`(566 行),0 差异 | | `panels -mode compare` | 7 个面板像素哈希 + 几何逐项不变 | +--- + +## 第 38 轮(2026-11-04):删死列 + 拆重采样参数行(§14.5 第 1 / 第 3 项) + +### P31 删除 `rowPoints` / `rowIterations` —— ✅ 完成 + +审计(§14.5 第 1 项的前提)在三处确认它们是**死列**: + +| 位置 | 原用途 | 现状 | +| --- | --- | --- | +| 写入 | `AppendRow` 里推两个空矩形 | 这是**唯一**写入点,从未被赋过非空矩形 | +| 读取 | `PlotViewPaint.cpp` 调试框 `AddAll`、`PanelSlotKey` 的 mix 循环 | 都是"遍历即忘",不产生任何可见效果 | +| 绘制 | 无 | 旧式"点数 / 迭代次数"输入框早被滑块版(`rowLevelTracks` / `rowIterTracks`)取代 | + +改动:删两个 `std::vector`(`EdgePanelLayout` 少 48 字节)、`AppendRow` 少两次 `push_back`、 +调试框与缓存键各少两个循环、`rowgeom` 导出与两处长度不变式各少两项(16 列)。 + +**等价性被基线证明**(这是本轮最值得记的一条方法论): + +```text +old lines = 983, new lines = 919 +only in OLD = 64, only in NEW = 0 +only-in-OLD lines NOT belonging to rowPoints/rowIterations = 0 +``` + +* 983 − 919 = 64,正好是两列在 7 个面板上的键数(`count` 键 + 每行键); +* 其余 919 条键**逐字节相同** ⇒ "这两列从未有非空值"的判断不是靠眼睛,而是靠基线证伪。 +* 随后用新代码重新 `record` 正式基线(`rowgeom-baseline.txt`),再次 `compare` 归零。 + +### P32 提取 `PaintPanelResampleRows` —— ✅ 完成 + +`PaintPanelListRows` 里 `if (resamplePanel) { …四行… continue; }` 这 **132 行**整块提取为 +`PlotView::PaintPanelResampleRows(g, layout, rowIndex, laneIndex, ref, item)`: + +* 两个局部 lambda(`paintInfoRow` 只读统计行 / `paintInputRow` 联动输入框)随之迁移; +* `maxInterval` / `minInterval` 两个"拷贝一份再说"的局部量直接用 `item.maxInterval` / + `item.minInterval`(原来是纯拷贝,无语义差别); +* 调用点只剩 3 行(含注释),`PaintPanelListRows` 现在只有"条目栏 + 通用参数行"两段。 + +### §14.5 第 3 项的完成度(诚实记录) + +| 段落 | 状态 | 理由 | +| --- | --- | --- | +| 重采样行 | ✅ 已提取 | 排版与其它面板完全不同、入口/出口干净(`continue` 收尾) | +| 通用参数行(中心轴统计 + 两行滑块) | ⏸ 未提取 | 依赖 3 个 kind 标志 + 2 套状态(`m_dragMode`/`m_editTarget`)+ 2 个滑块 lambda,签名要约 9 个参数,可读性收益低于参数表成本 | +| 条目栏 | 保留 | 三角 + 名称 + 复选框 + 悬停是三种排版的公共前缀 | + +### 本轮验证 + +| 项 | 结果 | +| --- | --- | +| `make release`(全量) | 零 error / 零 warning;产物 785,408 → **784,896 B**(−512) | +| `make -C Tools` | 零 error / 零 warning | +| `UnitTests` | `checks=260 failed=0 cases=19` | +| `OffscreenChecks -all` | 7/7 PASS,`checks=127 failed=0` | +| `panels -mode compare` | 7 个面板像素哈希 + 几何**逐项不变**(含 `resample=2BFC74B3D9B3239C`) | +| `rowgeom -mode compare` | 919 + 7 键,0 差异 | +| 行数 | `PlotViewPaint.cpp` 5575 → 5565;`PlotViewPanelPaint.cpp` 1016 → 1020(净增 4 行:函数头 + 文档) | + diff --git a/Documents/Code_Review_Report.md b/Documents/Code_Review_Report.md index c90b78b..53df3bb 100644 --- a/Documents/Code_Review_Report.md +++ b/Documents/Code_Review_Report.md @@ -610,3 +610,76 @@ Tools/README.md 目录结构、作用、测试项、验收标准、A/B 否则不建议动。 3. `PaintPanelListRows` 再拆三段(纯可读性)。 +--- + +## 十五、第 38 轮:§14.5 第 1 / 第 3 项 + +### 15.1 结论 + +| §14.5 建议 | 结果 | +| --- | --- | +| 1. `rowPoints` / `rowIterations` 清理 | ✅ 删除(`EdgePanelLayout` 16 列),产物 −512 B,基线证明等价 | +| 2. 完整结构合并(`vector` + 列视图) | ⏸ **未做**,闸门仍关闭(理由见 §15.3) | +| 3. `PaintPanelListRows` 再拆 | ◑ 重采样行已提取(132 行),参数行未提取(理由见 §15.4) | + +### 15.2 "删掉一列"的等价性怎么证明 + +`rowgeom` 让"删除死列"从"我看过没人用"变成**可验证**: + +```text +old lines = 983, new lines = 919 +only in OLD = 64, only in NEW = 0 +only-in-OLD lines NOT belonging to rowPoints/rowIterations = 0 +``` + +* 唯一差异就是被删两列的 64 条键;其余 919 条**逐字节相同**; +* 因为 `rowgeom` 导出的是**每个矩形的每个字段**,"这两列恒为空"这一前提被直接证伪/证实, + 不依赖人工 grep 的完整性。 + +这给出一条可复用的方法:**先有全字段基线,再删字段**;否则"删掉的东西没人用"永远只是断言。 + +### 15.3 为什么 §14.5 第 2 项仍然不做 + +报告本人的判据是"若要做,必须同时给 `EdgePanelLayout` 加视图自检,否则不建议动"。 +本轮把这一步的门槛量化了: + +| 方案 | 改动面 | 失败可见性 | +| --- | --- | --- | +| A:`std::vector` + 列视图(`PanelColumn`) | 242 处读点(其中 **127** 处在 `PlotViewInteraction.cpp` 的命中判定里)+ 自定义拷贝/赋值 + 视图自检 | 拷贝后忘重绑 = **静默悬垂**,只能靠新增自检兜住 | +| B:`std::vector` + 具名成员访问(无视图) | 同样是 242 处,但所有访问变成 `rows[i].check`,**结构上不可能悬垂** | 编译期全查;无新风险 | + +> 结论:真要做就做 **B**(无视图),A 的"零改动读点"是幻觉 —— 省下的正则改写换来一个 +> 必须靠新自检才能发现的隐患。B 的代价是 242 处机械替换(含 30 余处 range-for 与 `.size()`), +> 适合单独一轮、以 `rowgeom` + `panels` 双基线为判据,而不是塞在本轮末尾。 + +另外现在"漏推一列"已被 `AppendRow` + 长度不变式双重挡住, +B 的**剩余收益**主要是"单一分配 + 少 15 个 vector 头"(`EdgePanelLayout` 每实例省约 360 B +头部,566 个网格时约 200 KB,但那是**布局对象**、每帧临时构造,实际峰值影响很小)。 +因此 B 的优先级应低于"面板结构再简化"的实际痛点,属于**整洁性收益**而非正确性收益。 + +### 15.4 §14.5 第 3 项的取舍 + +| 段落 | 状态 | +| --- | --- | +| 重采样行(132 行:两行只读统计 + 两行联动输入框) | ✅ 提取为 `PaintPanelResampleRows`,零逻辑改动 | +| 通用参数行(中心轴统计 + 两行滑块) | ⏸ 提取需 9 个参数(`layout`/`rowIndex`/`laneIndex`/`ref`/`kind`/`resolved`/`item`/`peakItem`/`tabLane`),签名比函数体更难读 | +| 条目栏 | 保留(三种排版的公共前缀) | + +### 15.5 验证 + +| 项 | 结果 | +| --- | --- | +| `make release` | 零 warning;**784,896 B**(−512) | +| `UnitTests` | 260 / 19 PASS | +| `OffscreenChecks -all` | 7 / 127 PASS | +| `panels -mode compare` | 7 面板像素哈希逐项不变 | +| `rowgeom -mode compare` | 919 + 7 键,0 差异 | + +### 15.6 下一步(更新) + +1. **方案 B(`vector` 无视图)单独一轮**:判据 = 编译通过 + `rowgeom`/`panels` 双基线归零; + 预期收益是"字段重载(`rowRefs` 双语义)"这类问题从**结构上消失** —— 这比省字节更重要。 +2. 若要在 B 之上再做**语义化**:把 `rowRefs` 的双语义拆成 `rows[].ref` + `markerGroups[]`, + 让"MARK 面板 12 列为空"的注释消失。 +3. 面板几何算式重写(§13.6 遗留):基线已就绪,但需先完成 1 或 2。 + diff --git a/Documents/agent_mem.md b/Documents/agent_mem.md index 5023c2b..6089dce 100644 --- a/Documents/agent_mem.md +++ b/Documents/agent_mem.md @@ -10638,3 +10638,60 @@ MARK 面板**只填** `rowNameRects` / `rowNameCloses`,其余 14 列**必须 `EdgePanelLayout` 加"视图自检"(比对 `rows` 容量与视图长度)。 3. `PaintPanelListRows` 再拆三段(纯可读性)。 +--- + +## 2026-11-04 · 第 38 轮(`Code_Review` 分支):删死列 + 拆重采样参数行(§14.5 第 1 / 第 3 项) + +> 规则原文:`Documents/Code_Review_Report.md` §15、`Code_Review_Plan.md` §十、 +> `Code_Review_Progress.md` 第 38 轮。 + +### 1. ✅ 删除 `EdgePanelLayout::rowPoints` / `rowIterations`(行数组 18 → **16**) + +* 写入方**只有** `AppendRow`(推空矩形),读取方**只有**调试框 `AddAll` 与 `PanelSlotKey` 的 mix 循环, + 绘制方零(旧式"点数 / 迭代次数"输入框早被滑块版取代)。 +* 任何"面板行数组清单"现在都是 **16 列**:`rowgeom` 导出、`panels`/`panelscroll` 长度不变式、 + 调试框、缓存键、`AppendRow`。 + +### 2. ⭐ 方法论:**先有全字段基线,再删字段** + +本轮"删两列"的等价性不是靠 grep 完整性,而是靠 `rowgeom` 证伪: + +```text +old lines = 983, new lines = 919 ; only in OLD = 64 ; only in NEW = 0 +only-in-OLD lines NOT belonging to rowPoints/rowIterations = 0 +``` + +⇒ 以后删任何 `EdgePanelLayout` 字段,都走这个流程: +`record 旧 → 改代码 → record 新到独立目录 → 集合差只应包含被删字段 → 再 record 正式基线`。 + +### 3. ✅ 提取 `PaintPanelResampleRows`(132 行) + +`PaintPanelListRows` 里 `if (resamplePanel) { …; continue; }` 整块提取;签名 +`(g, layout, rowIndex, laneIndex, ref, item)`;两个局部 lambda(只读统计行 / 联动输入框)随之迁移。 +**通用参数行(中心轴统计 + 两行滑块)未提取**:需 9 个参数,签名比函数体更难读。 + +### 4. ⛔ §14.5 第 2 项(完整结构合并)**仍未做**,且方案已定 + +| 方案 | 结论 | +| --- | --- | +| A:`vector` + `PanelColumn` 列视图 | ❌ 不做:242 处读点看似"零改动",但自定义拷贝忘重绑 = **静默悬垂**,只能靠新增自检兜住 | +| B:`vector` + 具名成员访问(无视图) | ✅ 若要做就做这个:所有访问变 `rows[i].check`,**结构上不可能悬垂**,可省略自检 | + +B 的规模:242 处读点(**127** 在 `PlotViewInteraction.cpp` 命中判定里)+ 30 余处 range-for / `.size()`。 +**必须单独一轮**,判据 = 编译通过 + `rowgeom`/`panels` 双基线归零。 +B 属**整洁性收益**(漏推一列已被 `AppendRow` + 长度不变式挡住),不是正确性收益 —— 别再当紧急项。 + +### 5. 本轮数字 + +* Release 785,408 → **784,896 B**(−512);零 warning。 +* `UnitTests` 260/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。 + +### 6. 下一步 + +1. 方案 B(`vector` 无视图)单独一轮;顺带把 `rowRefs` 的双语义拆成 + `rows[].ref` + `markerGroups[]`,让"MARK 面板 12 列为空"的注释消失。 +2. 面板几何算式重写(§13.6 遗留):基线已就绪,但需先完成 1。 + diff --git a/Tools/OffscreenChecks/Src/Checks.cpp b/Tools/OffscreenChecks/Src/Checks.cpp index 2b2b4ab..72eed6a 100644 --- a/Tools/OffscreenChecks/Src/Checks.cpp +++ b/Tools/OffscreenChecks/Src/Checks.cpp @@ -345,7 +345,7 @@ namespace jdi // ---- 每一行 / 每一列 ---- // ⚠️ **每一列只按自己的长度导出**,不要假设所有列等长: // 「自由标记」面板复用 `rowRefs` 表示"信号分组",只填 - // `rowNameRects` / `rowNameCloses`,其余 14 列**全为空** + // `rowNameRects` / `rowNameCloses`,其余 12 列**全为空** // (这一点正是 `rowgeom` 第一次运行就抓出来的结构事实)。 // 按 `rowRefs.size()` 统一索引会越界崩溃。 const auto dumpRects = [&text, &key](const char* name, const std::vector& rects) { @@ -371,8 +371,6 @@ namespace jdi dumpRects("rowNameCloses", layout.rowNameCloses); dumpRects("rowTriangles", layout.rowTriangles); dumpRects("rowChecks", layout.rowChecks); - dumpRects("rowPoints", layout.rowPoints); - dumpRects("rowIterations", layout.rowIterations); dumpRects("rowStats", layout.rowStats); dumpRects("rowLevelTracks", layout.rowLevelTracks); dumpRects("rowLevelValues", layout.rowLevelValues); @@ -667,9 +665,9 @@ namespace jdi // 不变式:行的几何数组必须与行数**形状一致**。 // - // ⚠️⚠️ 判据必须**按排版类别**区分 —— 旧版"18 个数组全部全等"是**错的**: + // ⚠️⚠️ 判据必须**按排版类别**区分 —— 旧版"所有数组全部全等"是**错的**: // 「自由标记」面板复用 `rowRefs` 表示"信号分组",只填 - // `rowNameRects` / `rowNameCloses`,其余 14 列**必须为空** + // `rowNameRects` / `rowNameCloses`,其余 12 列**必须为空** // (它真正的行在 `markerRows` / `markerGroups` 里)。 // 这是 `rowgeom` 第一次带真实固定点运行时越界崩溃所暴露的结构事实。 const PanelStyle rowStyle = PanelStyleOf(kind); @@ -685,13 +683,12 @@ namespace jdi else { const std::size_t parallelSizes[] = { - layout.rowHeaders.size(), layout.rowNameRects.size(), - layout.rowTriangles.size(), layout.rowChecks.size(), - layout.rowPoints.size(), layout.rowIterations.size(), - layout.rowStats.size(), layout.rowLevelTracks.size(), - layout.rowLevelValues.size(), layout.rowLevelResets.size(), - layout.rowIterTracks.size(), layout.rowIterValues.size(), - layout.rowIterResets.size(), layout.rowInfoMaxInterval.size(), + layout.rowHeaders.size(), layout.rowNameRects.size(), + layout.rowTriangles.size(), layout.rowChecks.size(), + layout.rowStats.size(), layout.rowLevelTracks.size(), + layout.rowLevelValues.size(), layout.rowLevelResets.size(), + layout.rowIterTracks.size(), layout.rowIterValues.size(), + layout.rowIterResets.size(), layout.rowInfoMaxInterval.size(), layout.rowInfoMinInterval.size(), layout.rowIntervalField.size(), layout.rowFrequencyField.size()}; for (const std::size_t size : parallelSizes) @@ -868,8 +865,6 @@ namespace jdi initial.rowNameRects.size(), initial.rowTriangles.size(), initial.rowChecks.size(), - initial.rowPoints.size(), - initial.rowIterations.size(), initial.rowStats.size(), initial.rowLevelTracks.size(), initial.rowLevelValues.size(), @@ -953,7 +948,7 @@ namespace jdi * * 为什么需要它(Code Review 报告 §13.6 建议 1): * * 像素哈希只能回答"有没有变化",回答不了"**哪一行哪一列**变了"; - * * `EdgePanelLayout` 仍是 18 个平行数组,任何"合并成单一 `PanelRow`"或 + * * `EdgePanelLayout` 仍是 16 个平行数组,任何"合并成单一 `PanelRow`"或 * "重写布局算式"的重构都必须先能**逐项证明等价**; * * 面板改布局时的真实缺陷形态(某列读到别人的矩形)会让**成片的**行错位, * 而像素基线在面板互相重叠时还会被"下层露出"掩盖。 diff --git a/Tools/README.md b/Tools/README.md index a540cca..e013217 100644 --- a/Tools/README.md +++ b/Tools/README.md @@ -214,7 +214,7 @@ OffscreenChecks.exe -run panels -mode compare -out ..\Temp\tools\panel-before * 列表式面板(均值/中心轴/弱峰/重采样/截取):`rowRefs[i]` = 第 i 行的信号; * 「自由标记」面板:`rowRefs[i]` = 第 i 个**信号分组**,真正的条目行在 `markerRows`, - 且只填 `rowNameRects` / `rowNameCloses`,其余 14 列**必须为空**。 + 且只填 `rowNameRects` / `rowNameCloses`,其余 12 列**必须为空**。 所以判据按 `PanelStyleOf(kind).markerGroups` 分流(见 `Checks.cpp` 里 `panels` 的注释)。 早先“18 列全等”的写法对 MARK 面板是**错的**,只因既有场景恰好未触发才没暴露。