From 0c26da07d2aef713337841782c4c9ff4896907ca Mon Sep 17 00:00:00 2001 From: "DeepSeek V4.1 Flash" <> Date: Wed, 4 Nov 2026 17:22:06 +0800 Subject: [PATCH] =?UTF-8?q?refactor(panels):=20=E9=9D=A2=E6=9D=BF=E9=9D=99?= =?UTF-8?q?=E6=80=81=E7=89=B9=E5=BE=81=E6=94=B6=E6=95=9B=E4=B8=BA=E5=94=AF?= =?UTF-8?q?=E4=B8=80=E7=99=BB=E8=AE=B0=E8=A1=A8=20+=20=E6=A0=87=E8=AE=B0?= =?UTF-8?q?=E9=9D=A2=E6=9D=BF=E7=BB=98=E5=88=B6=E5=88=86=E6=AE=B5=EF=BC=88?= =?UTF-8?q?=C2=A717.6=20=E7=AC=AC=201/2=20=E9=A1=B9=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 pvdetail::PanelKindTraits + PanelTraitsOf(唯一 switch):标题 / 排版类别 / 滚动字段 / 面板状态 / 功能开关同表登记;新增面板只改「枚举 + 一行」, -Wswitch 仍在唯一登记点生效。PanelTitle 改为「前缀 + rangeSuffix 标志」, 不再依赖 switch(标题降级为数据,不再需要函数指针)。 - PaintPanelMarkerGroups(约 340 行)拆成 6 个具名函数,主函数只保留绘制顺序 (普通分组栏 -> 条目 -> 表头 -> 吸顶栏 -> 滚动条 -> 透明度行);纯搬移、0 字节变化。 - 顺手修正该函数体遗留的多余缩进。 验证:release/debug/tools 零 warning;UnitTests 260/19;OffscreenChecks -all 7/133; panels -mode compare 7 面板像素哈希逐项不变;rowgeom compare 965+7 / 1268+7 键 0 差异。 产物 781,824 -> 782,336 B(+512,全部来自登记表)。 --- Components/PlotView/Inc/PlotView.hpp | 36 +- Components/PlotView/Inc/PlotViewInternal.hpp | 206 +++--- .../PlotView/Src/PlotViewPanelPaint.cpp | 692 +++++++++--------- Components/PlotView/Src/PlotViewPanels.cpp | 26 +- Documents/Code_Review_Plan.md | 25 + Documents/Code_Review_Progress.md | 74 ++ Documents/Code_Review_Report.md | 93 +++ Documents/agent_mem.md | 62 ++ 8 files changed, 761 insertions(+), 453 deletions(-) diff --git a/Components/PlotView/Inc/PlotView.hpp b/Components/PlotView/Inc/PlotView.hpp index 4ef3d03..5a59c89 100644 --- a/Components/PlotView/Inc/PlotView.hpp +++ b/Components/PlotView/Inc/PlotView.hpp @@ -1201,7 +1201,8 @@ namespace jdi * @brief 设置某种面板的**内容滚动偏移**(客户区像素,0 = 顶部)—— 唯一实现。 * * ⚠️ 下面七个 `SetLane*Scroll` 都只是本函数的一行转发(保留是为了不改调用点与原语义)。 - * 新增面板时**只需**在 `LanePanelScrollOf` / `PanelStyleOf` / `PanelTitle` 三处登记, + * 新增面板时**只需**在 `pvdetail::PanelTraitsOf`(`PlotViewInternal.hpp`)加一行: + * 标题 / 排版类别 / 滚动字段 / 面板状态 / 功能开关都在那一行里, * 不要再复制一份"取字段 → 夹取 → 比较 → 写回"的实现。 * * @param [IN] laneIndex 网格下标。 @@ -2574,6 +2575,39 @@ namespace jdi */ const std::vector& FreeMarkerContents(int laneIndex) const; + // ---- 「自由标记」面板内容绘制的**分段入口**(第 41 轮)----------------------- + // `PaintPanelMarkerGroups` 只负责**顺序**(普通分组栏 → 条目 → 表头 → 吸顶分组栏 → + // 滚动条 → 透明度滑块),各段的细节在这里。提取是纯搬移, + // 判据 = `panels -mode compare`(像素哈希)+ `rowgeom -mode compare`(逐行几何)。 + // ⚠️ 顺序本身是硬约束:表头与吸顶分组栏必须画在条目**之后**,否则向上滚动的条目 + // 会盖住它们(这是原实现里唯一真正需要"读整段代码才看得见"的约束)。 + + /** @brief 把「自由标记」六列的实测文本宽度写回 `stored`(= `Lane::freeMarkerColumnWidths`)。 */ + void MeasureMarkerColumnWidths(const Lane& tabLane, + const std::vector& contents, + std::array& stored); + + /** @brief 「自由标记」列分隔竖线(数据行与表头共用同一套列边界)。 */ + void PaintMarkerColumnSeparators(gd::Graphics& g, const EdgePanelLayout& layout, float y, + float height); + + /** @brief 「自由标记」一个信号分组栏(三角 + 名称 + `N 标记点` + ✕)。 */ + void PaintMarkerGroupBar(gd::Graphics& g, const EdgePanelLayout& layout, const Lane& tabLane, + const EdgePanelLayout::MarkerGroup& group, std::size_t laneIndex); + + /** @brief 「自由标记」一个固定点条目(六列 + 分隔线 + 行尾 ✕)。 */ + void PaintMarkerRow(gd::Graphics& g, const EdgePanelLayout& layout, const Lane& tabLane, + const EdgePanelLayout::MarkerRow& markerRow, + const FreeMarkerContentEntry& entry, std::size_t laneIndex); + + /** @brief 「自由标记」一个展开分组的表头行(不透明底衬 + 六列标题)。 */ + void PaintMarkerTableHeader(gd::Graphics& g, const EdgePanelLayout& layout, + const EdgePanelLayout::MarkerGroup& group); + + /** @brief 「自由标记」底部「标记透明度」滑块行。 */ + void PaintMarkerOpacityRow(gd::Graphics& g, const EdgePanelLayout& layout, const Lane& tabLane, + std::size_t laneIndex); + /** 按网格下标的「自由标记」内容缓存(`mutable`:只在绘制时按需重建)。 */ mutable std::vector m_freeMarkerContent; diff --git a/Components/PlotView/Inc/PlotViewInternal.hpp b/Components/PlotView/Inc/PlotViewInternal.hpp index 8ad0615..d536ecb 100644 --- a/Components/PlotView/Inc/PlotViewInternal.hpp +++ b/Components/PlotView/Inc/PlotViewInternal.hpp @@ -1096,99 +1096,21 @@ namespace jdi // ⚠️ 这些 `switch` **不得写 `default:`** —— 让 `-Wswitch` 在新增 // `EdgePanelKind` 而漏登记时直接报警(编译期保护,0 运行时代价)。 - /** @brief 取某种面板的 UI 状态(展开/折起/位置)——可写版本。 */ - inline LanePanelState& LanePanelStateOf(Lane& lane, EdgePanelKind kind) - { - switch (kind) - { - case EdgePanelKind::COUNT: - return lane.edgePanel; - case EdgePanelKind::CENTER: - return lane.edgeCenterPanel; - case EdgePanelKind::MEAN: - return lane.meanFilterPanel; - case EdgePanelKind::PEAK: - return lane.weakPeakPanel; - case EdgePanelKind::MARK: - return lane.freeMarkerPanel; - case EdgePanelKind::RESAMPLE: - return lane.resamplePanel; - case EdgePanelKind::CLIP: - return lane.clipPanel; - } - // 枚举值越界(只可能来自非法强制转换):返回一个安全的占位对象。 - return lane.edgePanel; - } - - /** @brief 取某种面板的 UI 状态——只读版本。 */ - inline const LanePanelState& LanePanelStateOf(const Lane& lane, EdgePanelKind kind) - { - return LanePanelStateOf(const_cast(lane), kind); - } - - /** @brief 取某种面板的**内容滚动偏移**(客户区像素)——可写版本。 */ - inline float& LanePanelScrollOf(Lane& lane, EdgePanelKind kind) - { - switch (kind) - { - case EdgePanelKind::COUNT: - return lane.edgeCountScroll; - case EdgePanelKind::CENTER: - return lane.edgeCenterScroll; - case EdgePanelKind::MEAN: - return lane.meanFilterScroll; - case EdgePanelKind::PEAK: - return lane.weakPeakScroll; - case EdgePanelKind::MARK: - return lane.freeMarkerScroll; - case EdgePanelKind::RESAMPLE: - return lane.resampleScroll; - case EdgePanelKind::CLIP: - return lane.clipScroll; - } - return lane.meanFilterScroll; - } - - /** @brief 取某种面板的**内容滚动偏移**——只读版本。 */ - inline const float& LanePanelScrollOf(const Lane& lane, EdgePanelKind kind) - { - return LanePanelScrollOf(const_cast(lane), kind); - } - - /** @brief 该面板对应的功能是否已启用(右键菜单里的开关)。 */ - inline bool LanePanelEnabled(const Lane& lane, EdgePanelKind kind) - { - switch (kind) - { - case EdgePanelKind::COUNT: - return lane.edgeCount; - case EdgePanelKind::CENTER: - return lane.edgeCenter; - case EdgePanelKind::MEAN: - return lane.meanFilter; - case EdgePanelKind::PEAK: - return lane.weakPeak; - case EdgePanelKind::MARK: - return lane.freeMarker; - case EdgePanelKind::RESAMPLE: - return lane.resample; - case EdgePanelKind::CLIP: - return lane.clip; - } - return false; - } - - // ---- 面板的「排版类别」与「滚动几何」的唯一来源 -------------------------------- + // ---- 面板的「排版类别」与「静态特征」的唯一来源 -------------------------------- // // ⚠️⚠️ 为什么要有这一层:面板的**分类布尔量**过去在三个地方各写一份 // (`EdgePanelWidth` / `EdgePanelLayoutFor` / `PaintEdgeCursor`), // 每份都是 6 个 `kind == XXX` + 3 条组合判断。新增一种面板时若只改其中两处, // 就会出现"宽度按 A 类算、绘制按 B 类画"的错位 —— 这正是历史上 // 「元素超出浮动面板」「内容区多出一段空白」「滚动条画在表头上」这类缺陷的共同形态。 - // 现在只在这里分派一次,三个消费者都读同一份结果。 + // 现在只在这里登记一次,所有消费者都读同一份结果。 // - // ⚠️ 同样**不写 `default:`**:新增 `EdgePanelKind` 而未登记时, + // ⚠️ `PanelTraitsOf` 的 `switch` **不写 `default:`**:新增 `EdgePanelKind` 而未登记时, // `-Wswitch`(属 `-Wall`)会直接编译报警。 + // + // ⚠️ 标题里只有「边沿计数」「中心轴边沿计数」需要追加运行时后缀(`EdgeRangeSuffix()` + // 取决于双光标是否启用),因此表里存的是**标题前缀 + 一个标志**, + // 而不是 `std::wstring`(无法 constexpr)或函数指针(会把"数据"重新变成"代码")。 /** * @brief 一种浮动面板的排版类别(互斥 + 若干可选部件)。 @@ -1225,40 +1147,108 @@ namespace jdi bool footerButtons() const { return buildButton || twoButtons; } }; - /** @brief 取某种面板的排版类别(唯一分类来源)。 */ - inline PanelStyle PanelStyleOf(EdgePanelKind kind) + /** @brief `PanelStyle` 的三个具名构造函数(聚合初始化顺序易错,这里给出语义化入口)。 */ + inline constexpr PanelStyle ListPanelStyle(bool buildButton, bool twoButtons) noexcept + { + return PanelStyle{true, false, false, buildButton, twoButtons, false}; + } + + inline constexpr PanelStyle CountTablePanelStyle() noexcept + { + return PanelStyle{false, true, false, false, false, false}; + } + + inline constexpr PanelStyle MarkerPanelStyle() noexcept + { + return PanelStyle{false, false, true, false, false, true}; + } + + /** + * @brief 一种浮动面板的全部**静态特征**(排版类别 / 标题 / 三个字段偏移)。 + * + * 新增一种面板时**只允许**改三处:`EdgePanelKind` 加一个枚举值、 + * 本表加一行、`PanelTitle` 不需要动(标题就在这一行里)。 + * 其余消费者(宽度 / 布局 / 绘制 / 滚动 / 命中 / 调试盒 / 缓存键)全部自动跟上。 + */ + struct PanelKindTraits + { + PanelStyle style; /**< 排版类别与底部控件。 */ + const wchar_t* title = L""; /**< 标题前缀(`rangeSuffix` 为真时再追加区间后缀)。 */ + bool rangeSuffix = false; /**< 标题是否追加 `EdgeRangeSuffix()`(仅两种边沿计数)。 */ + float Lane::*scroll = nullptr; /**< 内容滚动偏移。 */ + LanePanelState Lane::*panel = nullptr; /**< 面板 UI 状态(展开/折起/位置)。 */ + bool Lane::*enabled = nullptr; /**< 功能开关(右键菜单里的勾选项)。 */ + }; + + /** @brief 取某种面板的全部静态特征(**唯一登记点**)。 */ + inline constexpr PanelKindTraits PanelTraitsOf(EdgePanelKind kind) { - PanelStyle style; switch (kind) { case EdgePanelKind::COUNT: - style.countTable = true; - break; + return PanelKindTraits{CountTablePanelStyle(), L"边沿计数", true, + &Lane::edgeCountScroll, &Lane::edgePanel, &Lane::edgeCount}; case EdgePanelKind::CENTER: - style.listRows = true; - break; + return PanelKindTraits{ListPanelStyle(false, false), L"中心轴边沿计数", true, + &Lane::edgeCenterScroll, &Lane::edgeCenterPanel, + &Lane::edgeCenter}; case EdgePanelKind::MEAN: - style.listRows = true; - style.buildButton = true; - break; + return PanelKindTraits{ListPanelStyle(true, false), L"均值滤波", false, + &Lane::meanFilterScroll, &Lane::meanFilterPanel, + &Lane::meanFilter}; case EdgePanelKind::PEAK: - style.listRows = true; - style.buildButton = true; - break; + return PanelKindTraits{ListPanelStyle(true, false), kWeakPeakTitle, false, + &Lane::weakPeakScroll, &Lane::weakPeakPanel, &Lane::weakPeak}; case EdgePanelKind::MARK: - style.markerGroups = true; - style.markerFooter = true; - break; + return PanelKindTraits{MarkerPanelStyle(), L"自由标记", false, + &Lane::freeMarkerScroll, &Lane::freeMarkerPanel, + &Lane::freeMarker}; case EdgePanelKind::RESAMPLE: - style.listRows = true; - style.buildButton = true; - break; + return PanelKindTraits{ListPanelStyle(true, false), kResampleTitle, false, + &Lane::resampleScroll, &Lane::resamplePanel, &Lane::resample}; case EdgePanelKind::CLIP: - style.listRows = true; - style.twoButtons = true; - break; + return PanelKindTraits{ListPanelStyle(false, true), kClipTitle, false, + &Lane::clipScroll, &Lane::clipPanel, &Lane::clip}; } - return style; + // 枚举值越界(只可能来自非法强制转换):退到「均值滤波」这份,保证指针非空。 + return PanelKindTraits{ListPanelStyle(true, false), L"", false, &Lane::meanFilterScroll, + &Lane::meanFilterPanel, &Lane::meanFilter}; + } + + /** @brief 取某种面板的排版类别(唯一分类来源)。 */ + inline PanelStyle PanelStyleOf(EdgePanelKind kind) + { + return PanelTraitsOf(kind).style; + } + + /** @brief 取某种面板的 UI 状态(展开/折起/位置)——可写版本。 */ + inline LanePanelState& LanePanelStateOf(Lane& lane, EdgePanelKind kind) + { + return lane.*(PanelTraitsOf(kind).panel); + } + + /** @brief 取某种面板的 UI 状态——只读版本。 */ + inline const LanePanelState& LanePanelStateOf(const Lane& lane, EdgePanelKind kind) + { + return LanePanelStateOf(const_cast(lane), kind); + } + + /** @brief 取某种面板的**内容滚动偏移**(客户区像素)——可写版本。 */ + inline float& LanePanelScrollOf(Lane& lane, EdgePanelKind kind) + { + return lane.*(PanelTraitsOf(kind).scroll); + } + + /** @brief 取某种面板的**内容滚动偏移**——只读版本。 */ + inline const float& LanePanelScrollOf(const Lane& lane, EdgePanelKind kind) + { + return LanePanelScrollOf(const_cast(lane), kind); + } + + /** @brief 该面板对应的功能是否已启用(右键菜单里的开关)。 */ + inline bool LanePanelEnabled(const Lane& lane, EdgePanelKind kind) + { + return lane.*(PanelTraitsOf(kind).enabled); } /** diff --git a/Components/PlotView/Src/PlotViewPanelPaint.cpp b/Components/PlotView/Src/PlotViewPanelPaint.cpp index bf217ef..d2aed22 100644 --- a/Components/PlotView/Src/PlotViewPanelPaint.cpp +++ b/Components/PlotView/Src/PlotViewPanelPaint.cpp @@ -592,347 +592,385 @@ namespace jdi } -/** - * @brief Marker panel content: per-signal group bars (collapsible) + table header + - * fixed-point rows, with sticky group bars. + // --------------------------------------------------------------------------- + // 「自由标记」面板内容(第 41 轮拆分) + // + // 原实现是一个约 340 行的单函数,内含 5 段互不相干的绘制(分组栏 / 列宽测量 / + // 条目行 / 表头 / 透明度滑块)。任何一段出问题都要在 300 行里找,而"表头与吸顶分组栏 + // 必须最后画"这条**唯一真实的顺序约束**被埋在一个循环中间。现在拆成 6 个具名函数, + // `PaintPanelMarkerGroups` 只负责顺序,判据 = `panels -mode compare` + + // `rowgeom -mode compare`(两者都必须零差异)。 + // --------------------------------------------------------------------------- + + /** + * @brief 把「自由标记」六列的实测文本宽度写回 `Lane::freeMarkerColumnWidths`。 * - * Paint order is rows -> header -> sticky group bar (the last one covers rows that - * scroll up). Rows hidden under a sticky bar are already excluded by the layout. + * 含滚出视界的条目(列宽必须稳定,否则滚动时列会跳动);下一步的布局 + * (`EdgePanelWidth` / `EdgePanelLayoutFor`)直接读这份宽度。 + */ + void PlotView::MeasureMarkerColumnWidths(const Lane& tabLane, + const std::vector& contents, + std::array& stored) + { + float widest[6] = {0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}; + const auto measure = [this, &widest](std::size_t column, const std::wstring& text) { + if (text.empty() || column >= 6) + { + return; + } + widest[column] = std::max(widest[column], UiMeasureTextWidth(m_fonts->monoSmall, text)); + }; + // 「序号」列:取本网格所有分组里最大的序号文本宽度。 + std::size_t maxSerial = 0; + std::vector serialOrder; + for (const SignalRef& ref : tabLane.signals) + { + SortedFreeMarkerIndices(tabLane, ref, serialOrder); + maxSerial = std::max(maxSerial, serialOrder.size()); + } + if (maxSerial > 0) + { + measure(EdgePanelLayout::MARKER_COL_SERIAL, FormatInteger(maxSerial)); + } + for (const FreeMarkerContentEntry& entry : contents) + { + measure(EdgePanelLayout::MARKER_COL_VALUE, entry.valueText); + measure(EdgePanelLayout::MARKER_COL_ENUM, entry.enumText); + measure(EdgePanelLayout::MARKER_COL_DELTA, entry.deltaText); + measure(EdgePanelLayout::MARKER_COL_TIME, entry.timeText); + measure(EdgePanelLayout::MARKER_COL_DT, entry.deltaTimeText); + } + for (std::size_t column = 0; column < 6; ++column) + { + // +2px 余量:`DrawTextW` 的 `DT_END_ELLIPSIS` 对"刚好装满"的文本 + // 有时仍会截成省略号(亚像素/字形伸部),留一点余量可彻底避免。 + const float want = (widest[column] > 0.0f) ? (widest[column] + 2.0f * m_scale) : 0.0f; + if (std::fabs(stored[column] - want) > 0.5f) + { + stored[column] = want; + } + } + } + + /** + * @brief 「自由标记」列分隔竖线(数据行与表头共用同一套列边界)。 + * + * 在每列右边界(加上半个列间隙)画一条淡竖线,让「序号 / 标记值 / 枚举文本 / 增量 / + * 时间点 / 时间间隔」有明显的列分界(用户 2026-10-23 要求)。 + * 最后一列不画(它右边紧接行尾 ✕)。 + */ + void PlotView::PaintMarkerColumnSeparators(gd::Graphics& g, const EdgePanelLayout& layout, + float y, float height) + { + int lastVisible = -1; + for (int column = 0; column < 6; ++column) + { + if (layout.markerColumns[column].width > 0.0f) + { + lastVisible = column; + } + } + for (int column = 0; column < lastVisible; ++column) + { + const EdgePanelLayout::MarkerColumn& cell = layout.markerColumns[column]; + if (cell.width <= 0.0f) + { + continue; + } + const float lineX = cell.left + cell.width + kFreeMarkerColumnGap * m_scale * 0.5f; + UiDrawLine(g, lineX, y + 2.0f * m_scale, lineX, y + height - 2.0f * m_scale, + WithAlpha(m_theme->borderStrong, 0x6C), 1.0f); + } + } + + /** + * @brief 「自由标记」一个信号分组栏(三角 + 名称 + `N 标记点` + ✕)。 + * + * 底色 = 信号色;文字一律用 `UiContrastInk` 算出的自适应对比色。吸顶时这一栏要压住 + * 下面滚动的条目,因此底色先铺**完全不透明**的同色、再叠 0xE8(观感不变)。 + */ + void PlotView::PaintMarkerGroupBar(gd::Graphics& g, const EdgePanelLayout& layout, + const Lane& tabLane, + const EdgePanelLayout::MarkerGroup& group, + std::size_t laneIndex) + { + const ResolvedSignal resolved = ResolveSignal(group.ref); + if (resolved.data == nullptr) + { + return; + } + const std::uint32_t signalColor = + (resolved.style != nullptr) ? StyleColor(*resolved.style) : m_theme->border; + const UiInk ink = UiContrastInk(signalColor); + UiFillRoundRect(g, group.header, 3.0f * m_scale, signalColor); + UiFillRoundRect(g, group.header, 3.0f * m_scale, WithAlpha(signalColor, 0xE8)); + + // 左侧三角(与信号选择区域组名同款):朝下 = 展开、朝右 = 收起。 + const bool triangleHot = (m_freeMarkerTriangleHoverLane == static_cast(laneIndex) && + m_freeMarkerTriangleHoverSignal == group.ref); + UiDrawTriangleIcon(g, group.triangle, triangleHot ? m_theme->accentSoft : ink.text, + group.collapsed ? IconDirection::RIGHT : IconDirection::DOWN); + + // 右侧:`N 标记点` + ✕(清除该信号全部固定点)。 + std::size_t markerCount = 0; + for (const Lane::FreeMarkerItem& entry : tabLane.freeMarkerItems) + { + if (entry.ref == group.ref) + { + ++markerCount; + } + } + const std::wstring countText = FormatInteger(markerCount) + L" 标记点"; + const float countWidth = UiMeasureTextWidth(m_fonts->monoSmall, countText) + 2.0f * m_scale; + const float countRight = group.close.X - 5.0f * m_scale; + UiTextClipped(g, countText, m_fonts->monoSmall, + gd::RectF(countRight - countWidth, group.header.Y, countWidth, + group.header.Height), + ink.text, TextAlign::RIGHT, layout.content); + + const float nameLeft = group.triangle.X + group.triangle.Width + 6.0f * m_scale; + const float nameWidth = std::max(10.0f, countRight - countWidth - 6.0f * m_scale - nameLeft); + UiTextClipped(g, UiEllipsize(resolved.data->name, m_fonts->uiSmall, nameWidth), + m_fonts->uiSmall, + gd::RectF(nameLeft, group.header.Y, nameWidth, group.header.Height), + ink.text, TextAlign::LEFT, layout.content); + + const bool closeHot = (m_freeMarkerPanelHoverLane == static_cast(laneIndex) && + m_freeMarkerPanelHoverSignal == group.ref && + m_freeMarkerPanelHoverItem == static_cast(-1)); + if (closeHot) + { + UiFillRoundRect(g, group.close, 3.0f * m_scale, WithAlpha(m_theme->danger, 0x77)); + } + const float crossPad = 3.5f * m_scale; + const std::uint32_t crossColor = closeHot ? m_theme->danger : ink.text; + UiDrawLine(g, group.close.X + crossPad, group.close.Y + crossPad, + group.close.X + group.close.Width - crossPad, + group.close.Y + group.close.Height - crossPad, crossColor, 1.5f * m_scale); + UiDrawLine(g, group.close.X + group.close.Width - crossPad, group.close.Y + crossPad, + group.close.X + crossPad, group.close.Y + group.close.Height - crossPad, + crossColor, 1.5f * m_scale); + } + + /** + * @brief 「自由标记」一个固定点条目(六列 + 分隔线 + 行尾 ✕)。 + * + * ⚠️ 面板里的数据列**不得省略**(用户 2026-10-21 明确要求):列宽已按实测文本宽度撑开。 + * ⚠️ 文字必须用 `UiTextClipped`(GDI 裁剪):`UiText` 走 `DrawTextW`,**不认 GDI+ 裁剪** + * ⇒ 滚动到内容区上方的条目会把文字画到标题栏上(用户 2026-10-22 报告的显示错误)。 + */ + void PlotView::PaintMarkerRow(gd::Graphics& g, const EdgePanelLayout& layout, + const Lane& tabLane, + const EdgePanelLayout::MarkerRow& markerRow, + const FreeMarkerContentEntry& entry, std::size_t laneIndex) + { + // ---- 行的选中 / 悬停高亮(用户 2026-10-22 要求)---- + const bool selected = (tabLane.freeMarkerSelected == static_cast(markerRow.itemIndex)); + const bool rowHot = (m_freeMarkerRowHoverLane == static_cast(laneIndex) && + m_freeMarkerRowHoverItem == markerRow.itemIndex); + if (selected || rowHot) + { + UiFillRect(g, + gd::RectF(markerRow.rect.X, markerRow.rect.Y + 1.0f * m_scale, + markerRow.rect.Width, markerRow.rect.Height - 2.0f * m_scale), + selected ? WithAlpha(m_theme->accent, 0x3C) : WithAlpha(m_theme->accent, 0x1E)); + } + + const auto columnRect = [&](EdgePanelLayout::MarkerColumnIndex column) { + return gd::RectF(layout.markerColumns[column].left, markerRow.rect.Y, + layout.markerColumns[column].width, markerRow.rect.Height); + }; + const auto paintCell = [&](EdgePanelLayout::MarkerColumnIndex column, + const std::wstring& text, std::uint32_t color) { + if (layout.markerColumns[column].width <= 0.0f || text.empty()) + { + return; + } + UiTextClipped(g, text, m_fonts->monoSmall, columnRect(column), color, TextAlign::LEFT, + layout.content); + }; + paintCell(EdgePanelLayout::MARKER_COL_SERIAL, FormatInteger(markerRow.serial), + m_theme->textMuted); + paintCell(EdgePanelLayout::MARKER_COL_VALUE, entry.valueText, m_theme->textStrong); + if (layout.markerHasEnumColumn) + { + paintCell(EdgePanelLayout::MARKER_COL_ENUM, entry.enumText, m_theme->text); + } + paintCell(EdgePanelLayout::MARKER_COL_DELTA, entry.deltaText, m_theme->textMuted); + paintCell(EdgePanelLayout::MARKER_COL_TIME, entry.timeText, m_theme->textMuted); + paintCell(EdgePanelLayout::MARKER_COL_DT, entry.deltaTimeText, m_theme->textMuted); + + // 列分隔竖线(用户 2026-10-23 要求:各列之间要有明显分界)。 + PaintMarkerColumnSeparators(g, layout, markerRow.rect.Y, markerRow.rect.Height); + + // 行尾 ✕:移除该固定点(与之前一致)。 + const gd::RectF& closeRect = markerRow.close; + const bool hot = (m_freeMarkerPanelHoverLane == static_cast(laneIndex) && + m_freeMarkerPanelHoverItem == markerRow.itemIndex); + UiFillRoundRect(g, closeRect, 3.0f * m_scale, + hot ? WithAlpha(m_theme->danger, 0x66) : m_theme->cardBg); + UiDrawRoundRect(g, closeRect, 3.0f * m_scale, hot ? m_theme->danger : m_theme->border, 1.0f); + const float crossPad = 3.5f * m_scale; + const std::uint32_t crossColor = hot ? m_theme->danger : m_theme->textMuted; + UiDrawLine(g, closeRect.X + crossPad, closeRect.Y + crossPad, + closeRect.X + closeRect.Width - crossPad, + closeRect.Y + closeRect.Height - crossPad, crossColor, 1.2f * m_scale); + UiDrawLine(g, closeRect.X + closeRect.Width - crossPad, closeRect.Y + crossPad, + closeRect.X + crossPad, closeRect.Y + closeRect.Height - crossPad, crossColor, + 1.2f * m_scale); + } + + /** + * @brief 「自由标记」一个展开分组的表头行(不透明底衬 + 六列标题)。 + * + * 不透明底衬是必须的:表头画在条目**之后**,用来盖住向上滚动的条目。 + */ + void PlotView::PaintMarkerTableHeader(gd::Graphics& g, const EdgePanelLayout& layout, + const EdgePanelLayout::MarkerGroup& group) + { + if (group.tableHeader.Width <= 0.0f || group.tableHeader.Height <= 0.0f) + { + return; + } + // 不透明底衬(与面板底色 + 少量边框色调混合,观感是“一条浅灰带”)。 + UiFillRect(g, group.tableHeader, MixColor(m_theme->cardBg, m_theme->borderStrong, 0.22)); + UiDrawLine(g, group.tableHeader.X, group.tableHeader.Y + group.tableHeader.Height - 1.0f, + group.tableHeader.X + group.tableHeader.Width, + group.tableHeader.Y + group.tableHeader.Height - 1.0f, + WithAlpha(m_theme->borderStrong, 0x99), 1.0f); + for (int column = 0; column < 6; ++column) + { + const EdgePanelLayout::MarkerColumn& cell = layout.markerColumns[column]; + if (cell.width <= 0.0f) + { + continue; + } + UiTextClipped(g, kFreeMarkerColumnLabels[column], m_fonts->uiSmall, + gd::RectF(cell.left, group.tableHeader.Y, cell.width, + group.tableHeader.Height), + m_theme->textMuted, TextAlign::LEFT, layout.content); + } + // 表头的列分隔竖线(与数据行同一套列边界)。 + PaintMarkerColumnSeparators(g, layout, group.tableHeader.Y, group.tableHeader.Height); + } + + /** + * @brief 「自由标记」底部「标记透明度」滑块行。 + * + * 这一行**总是显示**(与「自由标记点选取」是否开启无关),几何全部取自布局 + * (与命中测试同源);外观与信号样式区的「透明度」滑块完全相同(同一 `UiDrawSlider`)。 + */ + void PlotView::PaintMarkerOpacityRow(gd::Graphics& g, const EdgePanelLayout& layout, + const Lane& tabLane, std::size_t laneIndex) + { + if (layout.opacityTrack.Width <= 0.0f) + { + return; + } + const gd::RectF& panelRect = layout.panel; + const float labelGap = kEdgePanelPad * m_scale; + UiText(g, kFreeMarkerOpacityLabel, m_fonts->uiSmall, + gd::RectF(panelRect.X + labelGap, layout.opacityValue.Y, + layout.opacityTrack.X - panelRect.X - labelGap, + layout.opacityValue.Height), + m_theme->textMuted, TextAlign::LEFT); + const bool sliderActive = + (m_dragMode == PlotDragMode::MARKER_OPACITY && + m_markerOpacityDragLane == static_cast(laneIndex)) || + UiPointInRect(layout.opacityTrack, static_cast(m_mouseX), + static_cast(m_mouseY)); + // 外观与信号样式区的「透明度」滑动条完全相同(同一 `UiDrawSlider` 口径)。 + UiDrawSlider(g, layout.opacityTrack, static_cast(tabLane.freeMarkerOpacity), + m_theme->accent, sliderActive, m_scale); + const int percent = static_cast(tabLane.freeMarkerOpacity * 100.0f + 0.5f); + UiText(g, FormatInteger(percent) + L"%", m_fonts->monoSmall, layout.opacityValue, + m_theme->textStrong, TextAlign::RIGHT); + } + + /** + * @brief 「自由标记」面板的**内容区**(分组栏 + 表头 + 条目 + 吸顶)。 + * + * 绘制顺序(硬约束): + * 普通分组栏 → 条目 → 表头 → 吸顶分组栏 → 滚动条 → 透明度滑块 + * 后两段必须压在条目之上,否则向上滚动的条目会盖住表头/吸顶栏。 */ void PlotView::PaintPanelMarkerGroups(gd::Graphics& g, const EdgePanelLayout& layout, - const Lane& tabLane, std::vector& tabLanes, - std::size_t laneIndex) { + const Lane& tabLane, std::vector& tabLanes, + std::size_t laneIndex) + { const std::size_t index = laneIndex; - const float groupRowHeight = kFreeMarkerNameRowHeight * m_scale; - bool markerAny = false; - for (const Lane::FreeMarkerItem& item : tabLane.freeMarkerItems) - { - markerAny = markerAny || item.ref.IsValid(); - } + const float groupRowHeight = kFreeMarkerNameRowHeight * m_scale; + bool markerAny = false; + for (const Lane::FreeMarkerItem& item : tabLane.freeMarkerItems) + { + markerAny = markerAny || item.ref.IsValid(); + } - const int contentState = g.Save(); - g.SetClip(layout.content, gd::CombineModeIntersect); + const int contentState = g.Save(); + g.SetClip(layout.content, gd::CombineModeIntersect); - if (!markerAny) - { - // ⚠️ 未开启「自由标记点选取」时鼠标是普通鼠标,**单击不会固定**任何数据点 - //(用户 2026-10-23 要求)⇒ 提示语必须说清前提,否则用户会以为 bug。 - const wchar_t* emptyHint = tabLane.freeMarkerCursor - ? L"在网格内单击以固定数据点" - : L"开启「自由标记点选取」后单击网格可固定数据点"; - UiTextClipped(g, emptyHint, m_fonts->uiSmall, - gd::RectF(layout.content.X, layout.content.Y, layout.content.Width, - groupRowHeight), - m_theme->textMuted, TextAlign::LEFT, layout.content); - } + if (!markerAny) + { + // ⚠️ 未开启「自由标记点选取」时鼠标是普通鼠标,**单击不会固定**任何数据点 + //(用户 2026-10-23 要求)⇒ 提示语必须说清前提,否则用户会以为 bug。 + const wchar_t* emptyHint = tabLane.freeMarkerCursor + ? L"在网格内单击以固定数据点" + : L"开启「自由标记点选取」后单击网格可固定数据点"; + UiTextClipped(g, emptyHint, m_fonts->uiSmall, + gd::RectF(layout.content.X, layout.content.Y, layout.content.Width, + groupRowHeight), + m_theme->textMuted, TextAlign::LEFT, layout.content); + } - // ---- 分组栏 ---- - // ⚠️ 抽取成 lambda:**吸顶**的分组栏必须在条目之后重画一次(否则正在向上滚的 - // 条目会盖住它)。见下方“条目 → 表头 → 吸顶分组栏”的绘制顺序。 - const auto paintGroupBar = [this, &g, &layout, &tabLane, index]( - const EdgePanelLayout::MarkerGroup& group) - { - const ResolvedSignal resolved = ResolveSignal(group.ref); - if (resolved.data == nullptr) - { - return; - } - const std::uint32_t signalColor = - (resolved.style != nullptr) ? StyleColor(*resolved.style) : m_theme->border; - // 托底 = 信号色;文字一律用**自适应对比色**(与信号面板的组名徽标同规则), - // 因此无论信号是亮黄还是深蓝都能看清分组栏上的所有内容。 - const UiInk ink = UiContrastInk(signalColor); - // 先铺一层**完全不透明**的同色,再叠 0xE8(观感不变): - // 吸顶时它要压住下面滚动的条目,半透明会让条目透出来。 - UiFillRoundRect(g, group.header, 3.0f * m_scale, signalColor); - UiFillRoundRect(g, group.header, 3.0f * m_scale, WithAlpha(signalColor, 0xE8)); + // 取值与各列文本来自**与视界无关的内容缓存**(`FreeMarkerContents`): + // 大量固定点时逐帧重新采样会让"拖光标 / 平移视界"明显变卡。 + const std::vector& contents = + FreeMarkerContents(static_cast(index)); + // 六列的实测宽度存回 `Lane`:下一步的布局(`EdgePanelWidth` / `EdgePanelLayoutFor`)要用。 + MeasureMarkerColumnWidths(tabLane, contents, tabLanes[index].freeMarkerColumnWidths); - // 左侧三角(与信号选择区域组名同款):朝下 = 展开、朝右 = 收起。 - const bool triangleHot = - (m_freeMarkerTriangleHoverLane == static_cast(index) && - m_freeMarkerTriangleHoverSignal == group.ref); - UiDrawTriangleIcon(g, group.triangle, - triangleHot ? m_theme->accentSoft : ink.text, - group.collapsed ? IconDirection::RIGHT - : IconDirection::DOWN); + // ---- 普通分组栏(吸顶的那些留到最后画)---- + for (const EdgePanelLayout::MarkerGroup& group : layout.markerGroups) + { + if (!group.pinned) + { + PaintMarkerGroupBar(g, layout, tabLane, group, index); + } + } - // 右侧:`N 标记点` + ✕(清除该信号全部固定点)。 - std::size_t markerCount = 0; - for (const Lane::FreeMarkerItem& entry : tabLane.freeMarkerItems) - { - if (entry.ref == group.ref) - { - ++markerCount; - } - } - const std::wstring countText = FormatInteger(markerCount) + L" 标记点"; - const float countWidth = - UiMeasureTextWidth(m_fonts->monoSmall, countText) + 2.0f * m_scale; - const float countRight = group.close.X - 5.0f * m_scale; - UiTextClipped(g, countText, m_fonts->monoSmall, - gd::RectF(countRight - countWidth, group.header.Y, countWidth, - group.header.Height), - ink.text, TextAlign::RIGHT, layout.content); + // ---- 固定点条目(六列,列起点由布局给定 ⇒ 纵向严格对齐)---- + for (const EdgePanelLayout::MarkerRow& markerRow : layout.markerRows) + { + if (markerRow.itemIndex >= contents.size()) + { + continue; + } + PaintMarkerRow(g, layout, tabLane, markerRow, contents[markerRow.itemIndex], index); + } - const float nameLeft = group.triangle.X + group.triangle.Width + 6.0f * m_scale; - const float nameWidth = - std::max(10.0f, countRight - countWidth - 6.0f * m_scale - nameLeft); - UiTextClipped(g, UiEllipsize(resolved.data->name, m_fonts->uiSmall, nameWidth), - m_fonts->uiSmall, - gd::RectF(nameLeft, group.header.Y, nameWidth, group.header.Height), - ink.text, TextAlign::LEFT, layout.content); + // ---- 表头(每个展开分组一行,与条目共用列边界)---- + // ⚠️ 必须在条目**之后**绘制:表头底衬要不透明地盖住向上滚动的条目。 + for (const EdgePanelLayout::MarkerGroup& group : layout.markerGroups) + { + PaintMarkerTableHeader(g, layout, group); + } - const bool closeHot = (m_freeMarkerPanelHoverLane == static_cast(index) && - m_freeMarkerPanelHoverSignal == group.ref && - m_freeMarkerPanelHoverItem == static_cast(-1)); - if (closeHot) - { - UiFillRoundRect(g, group.close, 3.0f * m_scale, - WithAlpha(m_theme->danger, 0x77)); - } - const float crossPad = 3.5f * m_scale; - const std::uint32_t crossColor = closeHot ? m_theme->danger : ink.text; - UiDrawLine(g, group.close.X + crossPad, group.close.Y + crossPad, - group.close.X + group.close.Width - crossPad, - group.close.Y + group.close.Height - crossPad, crossColor, - 1.5f * m_scale); - UiDrawLine(g, group.close.X + group.close.Width - crossPad, - group.close.Y + crossPad, group.close.X + crossPad, - group.close.Y + group.close.Height - crossPad, crossColor, - 1.5f * m_scale); - }; - for (const EdgePanelLayout::MarkerGroup& group : layout.markerGroups) - { - if (!group.pinned) - { - paintGroupBar(group); - } - } + // ---- 吸顶的分组栏(最后画:压住向上滚动的条目)---- + for (const EdgePanelLayout::MarkerGroup& group : layout.markerGroups) + { + if (group.pinned) + { + PaintMarkerGroupBar(g, layout, tabLane, group, index); + } + } - // ---- 固定点条目(六列,列起点由布局给定 ⇒ 纵向严格对齐)---- - // ⚠️ 取值与各列文本来自**与视界无关的内容缓存**(`FreeMarkerContents`): - // 大量固定点时逐帧重新采样会让"拖光标 / 平移视界"明显变卡。 - const std::vector& contents = - FreeMarkerContents(static_cast(index)); - // 六列的**实测宽度**:含滚出视界的条目,保证列宽稳定、文本不被省略。 - // 存回 `Lane` 供下一步的布局(`EdgePanelWidth` / `EdgePanelLayoutFor`)使用。 - { - float widest[6] = {0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}; - const auto measure = [this, &widest](std::size_t column, const std::wstring& text) { - if (text.empty() || column >= 6) - { - return; - } - widest[column] = std::max(widest[column], - UiMeasureTextWidth(m_fonts->monoSmall, text)); - }; - // 「序号」列:取本网格所有分组里最大的序号文本宽度。 - std::size_t maxSerial = 0; - std::vector serialOrder; - for (const SignalRef& ref : tabLane.signals) - { - SortedFreeMarkerIndices(tabLane, ref, serialOrder); - maxSerial = std::max(maxSerial, serialOrder.size()); - } - if (maxSerial > 0) - { - measure(EdgePanelLayout::MARKER_COL_SERIAL, FormatInteger(maxSerial)); - } - for (const FreeMarkerContentEntry& entry : contents) - { - measure(EdgePanelLayout::MARKER_COL_VALUE, entry.valueText); - measure(EdgePanelLayout::MARKER_COL_ENUM, entry.enumText); - measure(EdgePanelLayout::MARKER_COL_DELTA, entry.deltaText); - measure(EdgePanelLayout::MARKER_COL_TIME, entry.timeText); - measure(EdgePanelLayout::MARKER_COL_DT, entry.deltaTimeText); - } - std::array& stored = - tabLanes[index].freeMarkerColumnWidths; - for (std::size_t column = 0; column < 6; ++column) - { - // +2px 余量:`DrawTextW` 的 `DT_END_ELLIPSIS` 对"刚好装满"的文本 - // 有时仍会截成省略号(亚像素/字形伸部),留一点余量可彻底避免。 - const float want = - (widest[column] > 0.0f) ? (widest[column] + 2.0f * m_scale) : 0.0f; - if (std::fabs(stored[column] - want) > 0.5f) - { - stored[column] = want; - } - } - } - // 列分隔竖线:在每列右边界(加上半个列间隙)画一条淡竖线, - // 让「序号 / 标记值 / 枚举文本 / 增量 / 时间点 / 时间间隔」有明显的列分界 - //(用户 2026-10-23 要求)。最后一列不画(它右边紧接行尾 ✕)。 - const auto paintColumnSeparators = [this, &g, &layout](float y, float height) { - int lastVisible = -1; - for (int column = 0; column < 6; ++column) - { - if (layout.markerColumns[column].width > 0.0f) - { - lastVisible = column; - } - } - for (int column = 0; column < lastVisible; ++column) - { - const EdgePanelLayout::MarkerColumn& cell = layout.markerColumns[column]; - if (cell.width <= 0.0f) - { - continue; - } - const float lineX = - cell.left + cell.width + kFreeMarkerColumnGap * m_scale * 0.5f; - UiDrawLine(g, lineX, y + 2.0f * m_scale, lineX, y + height - 2.0f * m_scale, - WithAlpha(m_theme->borderStrong, 0x6C), 1.0f); - } - }; - for (const EdgePanelLayout::MarkerRow& markerRow : layout.markerRows) - { - if (markerRow.itemIndex >= contents.size()) - { - continue; - } - const FreeMarkerContentEntry& entry = contents[markerRow.itemIndex]; + g.Restore(contentState); - // ---- 行的选中 / 悬停高亮(用户 2026-10-22 要求)---- - const bool selected = - (tabLane.freeMarkerSelected == static_cast(markerRow.itemIndex)); - const bool rowHot = (m_freeMarkerRowHoverLane == static_cast(index) && - m_freeMarkerRowHoverItem == markerRow.itemIndex); - if (selected || rowHot) - { - UiFillRect(g, - gd::RectF(markerRow.rect.X, markerRow.rect.Y + 1.0f * m_scale, - markerRow.rect.Width, - markerRow.rect.Height - 2.0f * m_scale), - selected ? WithAlpha(m_theme->accent, 0x3C) - : WithAlpha(m_theme->accent, 0x1E)); - } + // ---- 内容滚动条(只在内容超高时出现;按钮与滑块不参与滚动)---- + PaintPanelScrollBar(g, layout, *m_theme); - const auto columnRect = [&](EdgePanelLayout::MarkerColumnIndex column) { - return gd::RectF(layout.markerColumns[column].left, markerRow.rect.Y, - layout.markerColumns[column].width, - markerRow.rect.Height); - }; - // ⚠️ 面板里的数据列**不得省略**(用户 2026-10-21 明确要求): - // 列宽已按实测文本宽度撑开,因此这里直接整段输出,不再 `UiEllipsize`。 - // ⚠️ 必须用 `UiTextClipped`(GDI 裁剪):`UiText` 走 `DrawTextW`, - // **不认 GDI+ 裁剪** ⇒ 滚动到内容区上方的条目会把文字画到标题栏上 - //(用户 2026-10-22 报告的显示错误)。 - const auto paintCell = [&](EdgePanelLayout::MarkerColumnIndex column, - const std::wstring& text, std::uint32_t color) { - if (layout.markerColumns[column].width <= 0.0f || text.empty()) - { - return; - } - UiTextClipped(g, text, m_fonts->monoSmall, columnRect(column), color, - TextAlign::LEFT, layout.content); - }; - paintCell(EdgePanelLayout::MARKER_COL_SERIAL, FormatInteger(markerRow.serial), - m_theme->textMuted); - paintCell(EdgePanelLayout::MARKER_COL_VALUE, entry.valueText, - m_theme->textStrong); - if (layout.markerHasEnumColumn) - { - paintCell(EdgePanelLayout::MARKER_COL_ENUM, entry.enumText, m_theme->text); - } - paintCell(EdgePanelLayout::MARKER_COL_DELTA, entry.deltaText, - m_theme->textMuted); - paintCell(EdgePanelLayout::MARKER_COL_TIME, entry.timeText, m_theme->textMuted); - paintCell(EdgePanelLayout::MARKER_COL_DT, entry.deltaTimeText, - m_theme->textMuted); - - // 列分隔竖线(用户 2026-10-23 要求:各列之间要有明显分界)。 - paintColumnSeparators(markerRow.rect.Y, markerRow.rect.Height); - - // 行尾 ✕:移除该固定点(与之前一致)。 - const gd::RectF& closeRect = markerRow.close; - const bool hot = (m_freeMarkerPanelHoverLane == static_cast(index) && - m_freeMarkerPanelHoverItem == markerRow.itemIndex); - UiFillRoundRect(g, closeRect, 3.0f * m_scale, - hot ? WithAlpha(m_theme->danger, 0x66) : m_theme->cardBg); - UiDrawRoundRect(g, closeRect, 3.0f * m_scale, - hot ? m_theme->danger : m_theme->border, 1.0f); - const float crossPad = 3.5f * m_scale; - const std::uint32_t crossColor = hot ? m_theme->danger : m_theme->textMuted; - UiDrawLine(g, closeRect.X + crossPad, closeRect.Y + crossPad, - closeRect.X + closeRect.Width - crossPad, - closeRect.Y + closeRect.Height - crossPad, crossColor, 1.2f * m_scale); - UiDrawLine(g, closeRect.X + closeRect.Width - crossPad, closeRect.Y + crossPad, - closeRect.X + crossPad, closeRect.Y + closeRect.Height - crossPad, - crossColor, 1.2f * m_scale); - } - - // ---- 表头(每个展开分组一行,与条目共用列边界)---- - // ⚠️ 必须在条目**之后**绘制:表头底衬要不透明地盖住向上滚动的条目。 - for (const EdgePanelLayout::MarkerGroup& group : layout.markerGroups) - { - if (group.tableHeader.Width <= 0.0f || group.tableHeader.Height <= 0.0f) - { - continue; - } - // 不透明底衬(与面板底色 + 少量边框色调混合,观感是“一条浅灰带”)。 - UiFillRect(g, group.tableHeader, - MixColor(m_theme->cardBg, m_theme->borderStrong, 0.22)); - UiDrawLine(g, group.tableHeader.X, group.tableHeader.Y + group.tableHeader.Height - 1.0f, - group.tableHeader.X + group.tableHeader.Width, - group.tableHeader.Y + group.tableHeader.Height - 1.0f, - WithAlpha(m_theme->borderStrong, 0x99), 1.0f); - for (int column = 0; column < 6; ++column) - { - const EdgePanelLayout::MarkerColumn& cell = - layout.markerColumns[column]; - if (cell.width <= 0.0f) - { - continue; - } - UiTextClipped(g, kFreeMarkerColumnLabels[column], m_fonts->uiSmall, - gd::RectF(cell.left, group.tableHeader.Y, cell.width, - group.tableHeader.Height), - m_theme->textMuted, TextAlign::LEFT, layout.content); - } - // 表头的列分隔竖线(与数据行同一套列边界)。 - paintColumnSeparators(group.tableHeader.Y, group.tableHeader.Height); - } - - // ---- 吸顶的分组栏(最后画:压住向上滚动的条目)---- - for (const EdgePanelLayout::MarkerGroup& group : layout.markerGroups) - { - if (group.pinned) - { - paintGroupBar(group); - } - } - - g.Restore(contentState); - - // ---- 内容滚动条(只在内容超高时出现;按钮与滑块不参与滚动)---- - PaintPanelScrollBar(g, layout, *m_theme); - - // ---- 「标记透明度」滑动条(固定在「自由标记点选取」按钮上方)---- - if (layout.opacityTrack.Width > 0.0f) - { - // 几何全部取自布局(与命中测试同源)。 - const gd::RectF& panelRect = layout.panel; - const float labelGap = kEdgePanelPad * m_scale; - UiText(g, kFreeMarkerOpacityLabel, m_fonts->uiSmall, - gd::RectF(panelRect.X + labelGap, layout.opacityValue.Y, - layout.opacityTrack.X - panelRect.X - labelGap, - layout.opacityValue.Height), - m_theme->textMuted, TextAlign::LEFT); - const bool sliderActive = - (m_dragMode == PlotDragMode::MARKER_OPACITY && - m_markerOpacityDragLane == static_cast(index)) || - UiPointInRect(layout.opacityTrack, static_cast(m_mouseX), - static_cast(m_mouseY)); - // 外观与信号样式区的「透明度」滑动条完全相同(同一 `UiDrawSlider` 口径)。 - UiDrawSlider(g, layout.opacityTrack, - static_cast(tabLane.freeMarkerOpacity), m_theme->accent, - sliderActive, m_scale); - const int percent = - static_cast(tabLane.freeMarkerOpacity * 100.0f + 0.5f); - UiText(g, FormatInteger(percent) + L"%", m_fonts->monoSmall, - layout.opacityValue, m_theme->textStrong, TextAlign::RIGHT); - } + // ---- 「标记透明度」滑动条(固定在「自由标记点选取」按钮上方)---- + PaintMarkerOpacityRow(g, layout, tabLane, index); } } // namespace jdi diff --git a/Components/PlotView/Src/PlotViewPanels.cpp b/Components/PlotView/Src/PlotViewPanels.cpp index fa06cce..2e2c3ab 100644 --- a/Components/PlotView/Src/PlotViewPanels.cpp +++ b/Components/PlotView/Src/PlotViewPanels.cpp @@ -19,25 +19,17 @@ namespace jdi { std::wstring PlotView::PanelTitle(EdgePanelKind kind) const { - // ⚠️ 穷举且不写 `default:`(`-Wswitch` 会在新增面板时报警)。 - switch (kind) + // ⚠️ 标题前缀来自**唯一登记表**(`PanelTraitsOf`):新增面板时不需要改这个函数。 + // 只有「边沿计数」「中心轴边沿计数」需要追加运行时后缀(`EdgeRangeSuffix()` 取决于 + // 双光标是否启用),所以表里存的是"前缀 + 标志"而不是 `std::wstring`(无法 constexpr) + // 或函数指针(会把"数据"重新变成"代码",新增面板又要写一个函数)。 + const PanelKindTraits traits = PanelTraitsOf(kind); + std::wstring title(traits.title); + if (traits.rangeSuffix) { - case EdgePanelKind::COUNT: - return std::wstring(L"边沿计数") + EdgeRangeSuffix(); - case EdgePanelKind::CENTER: - return std::wstring(L"中心轴边沿计数") + EdgeRangeSuffix(); - case EdgePanelKind::MEAN: - return std::wstring(L"均值滤波"); - case EdgePanelKind::PEAK: - return std::wstring(kWeakPeakTitle); - case EdgePanelKind::MARK: - return std::wstring(L"自由标记"); - case EdgePanelKind::RESAMPLE: - return std::wstring(kResampleTitle); - case EdgePanelKind::CLIP: - return std::wstring(kClipTitle); + title += EdgeRangeSuffix(); } - return std::wstring(); + return title; } PlotView::PanelColumnMetrics PlotView::MeasurePanelColumns(const Lane& lane, diff --git a/Documents/Code_Review_Plan.md b/Documents/Code_Review_Plan.md index aa9e7d3..758f6fc 100644 --- a/Documents/Code_Review_Plan.md +++ b/Documents/Code_Review_Plan.md @@ -407,5 +407,30 @@ old keys = 919, new keys = 965 逐行状态打包进 `PanelParamState`(解析结果 / 两种条目指针 / 网格下标), 目的是把参数表压在 5 个 —— 一个个透传会让签名比函数体更难读。 +--- + +## 十三、第 41 轮(收尾):面板登记表 + 标记面板绘制分段 + +> 触发:用户要求「按上轮报告 §17.6 继续推进」,并在本轮结束时判断**是否已达到可以结束 +> 本次代码审查的程度**;体积一项按用户指示**继续暂停**(§17.6 第 3 项)。 + +| 节点 | 内容 | 验收标准 | +| --- | --- | --- | +| **P39** | `PanelStyleOf` / `PanelTitle` / `LanePanelScrollOf` / `LanePanelStateOf` / `LanePanelEnabled` 五处穷举 `switch` 合成一张 `constexpr` 登记表 `PanelTraitsOf(kind)` | 编译零 warning;`panels -mode compare` 7 面板像素哈希逐项不变 | +| **P40** | `PaintPanelMarkerGroups`(约 340 行)拆成 6 个具名函数(分组栏 / 列宽 / 分隔线 / 条目 / 表头 / 透明度行),主函数只留"顺序" | `panels -mode compare` + `rowgeom -mode compare`(默认 + MF4)0 差异 | + +### 13.1 本轮新增硬约束 + +1. **新增一种实验性功能面板只改两处**:`EdgePanelKind` 加一个枚举值 + + `pvdetail::PanelTraitsOf` 加一行(标题 / 排版类别 / 滚动字段 / 面板状态 / 功能开关全在那一行)。 + `PlotView::PanelTitle` **不再需要改** —— 它只读表里的"标题前缀 + `rangeSuffix` 标志"。 +2. **`PanelTraitsOf` 的 `switch` 一律不写 `default:`**:漏登记 `EdgePanelKind` 会被 `-Wswitch` + 当场拦下(这是"编译期保护"的唯一执行点,其余四个消费者都只是取值)。 +3. **`PanelStyle` 用三个具名构造函数**(`ListPanelStyle` / `CountTablePanelStyle` / + `MarkerPanelStyle`)构造,**禁止**在表里写 6 个裸 bool(聚合顺序错一位就变成"表格面板带按钮")。 +4. **标记面板的绘制顺序是硬约束**:普通分组栏 → 条目 → 表头 → 吸顶分组栏 → 滚动条 → 透明度行。 + 它现在**只出现在 `PaintPanelMarkerGroups` 一个地方**(过去藏在一个 340 行函数的循环中间)。 + 新增任何"吸顶/悬浮"元素时,必须加在"条目之后"。 + diff --git a/Documents/Code_Review_Progress.md b/Documents/Code_Review_Progress.md index 6ca4302..a6fd5d6 100644 --- a/Documents/Code_Review_Progress.md +++ b/Documents/Code_Review_Progress.md @@ -712,5 +712,79 @@ std::vector PlanListPanelRows(const Lane&, EdgePanelKind) const; | 行数 | `PlotViewPanelPaint.cpp` 935 → 939;`PlotViewPanels.cpp` 1729 → 1766(新增两个几何函数,净增 37) | | 样式 | 顺手把机械搬移遗留的"整个函数体多缩进 2 个 tab"修正 | +--- + +## 第 41 轮(2026-11-04):面板登记表 + 标记面板绘制分段(§17.6 第 1 / 第 2 项,收尾) + +> 本轮之后**代码审查进入收尾**:所有"已知问题"要么已修、要么有实测数字支撑地**明确不做** +>(体积、`EdgeScanFor` 哈希化、`EdgePanelLayoutFor` 表驱动化),详见报告 §18。 + +### P39 面板的五个穷举 `switch` 合成一张 `constexpr` 登记表 ✅ + +改前新增一种面板要改三处(`PanelStyleOf` / `PanelTitle` / `LanePanelScrollOf`), +实际连 `LanePanelStateOf` / `LanePanelEnabled` 一共是**五处穷举 switch** +(§17.6 只点了三处,另外两处更早就有)。 + +改后 `PlotViewInternal.hpp` 里只有**一个**登记点: + +```cpp +struct PanelKindTraits +{ + PanelStyle style; // 排版类别 + 底部控件 + const wchar_t* title = L""; // 标题前缀 + bool rangeSuffix = false; // 是否追加 EdgeRangeSuffix() + float Lane::*scroll = nullptr; // 内容滚动偏移 + LanePanelState Lane::*panel = nullptr; // 面板 UI 状态 + bool Lane::*enabled = nullptr; // 功能开关 +}; +inline constexpr PanelKindTraits PanelTraitsOf(EdgePanelKind kind); // 唯一 switch +``` + +* 五个消费者全部退化成一行取值:`return lane.*(PanelTraitsOf(kind).scroll);` +* **`PanelTitle` 不再是 switch**:标题是数据(前缀字符串 + 一个 `rangeSuffix` 标志), + 运行时后缀只对两种「边沿计数」追加 ⇒ §17.6 里"`PanelTitle` 依赖运行时字符串所以先解决 + '标题是函数'的问题"就此解决(不需要函数指针,也不需要 `std::wstring` 进表)。 +* ⚠️ `PanelStyle` 的三个具名构造函数(`ListPanelStyle` / `CountTablePanelStyle` / + `MarkerPanelStyle`)是刻意加的:表里若写 6 个裸 bool,聚合顺序错一位就变成 + "表格面板带按钮"这种没人看得出来的错误。 +* 体积代价:**+512 B**(7 份 traits 记录进 `.rdata` + `PanelTitle` 多一个分支)。 + 用 512 B 换"新增面板只改两处 + 编译期强制登记",判定值得。 + +### P40 `PaintPanelMarkerGroups` 拆成 6 个具名函数 ✅ + +| 函数 | 职责 | +| --- | --- | +| `MeasureMarkerColumnWidths` | 六列实测文本宽度 → 写回 `Lane::freeMarkerColumnWidths` | +| `PaintMarkerGroupBar` | 一个信号分组栏(三角 / 名称 / `N 标记点` / ✕) | +| `PaintMarkerColumnSeparators` | 列分隔竖线(条目行与表头共用) | +| `PaintMarkerRow` | 一个固定点条目(六列 + 分隔线 + 行尾 ✕) | +| `PaintMarkerTableHeader` | 一个展开分组的表头(不透明底衬 + 六列标题) | +| `PaintMarkerOpacityRow` | 底部「标记透明度」滑块行 | +| `PaintPanelMarkerGroups` | **只负责顺序**:普通分组栏 → 条目 → 表头 → 吸顶栏 → 滚动条 → 透明度行 | + +* 原实现把这条顺序约束埋在一个 340 行函数的循环中间("为什么表头要在这里画"), + 现在它是主函数里唯一的逻辑,注释也只剩一句"后两段必须压在上面"。 +* 提取时顺带把机械搬移遗留的"函数体多缩进 2 个 tab"修正(整个函数重新缩进)。 +* **零体积极限验证**:P39 之后产物已经是 782,336 B,P40 之后**完全相同** ⇒ 纯搬移。 + +### 过程中的一个判断(值得记) + +`contents`(固定点内容缓存)原来在"画完普通分组栏之后"才取,现在提前到最前面。 +两者唯一的区别是缓存重建的时机,而 `FreeMarkerContents` **不改任何绘制状态** +(只读采样 + 拼字符串 + 写 `m_freeMarkerContent`)⇒ 对像素无影响,双基线也证实了。 + +### 本轮验证 + +| 项 | 结果 | +| --- | --- | +| `make release` / `make debug` / `make -C Tools` | 零 error / 零 warning | +| 产物 | 781,824 → **782,336 B**(+512,全部来自 P39 的登记表;P40 为 0) | +| `UnitTests` | `checks=260 failed=0 cases=19` | +| `OffscreenChecks -all` | 7/7 PASS,`checks=133 failed=0` | +| `panels -mode compare` | 7 个面板像素哈希**逐项不变**(含 `resample=2BFC74B3D9B3239C`) | +| `rowgeom -mode compare` | 965 + 7 键 0 差异;`-data MF4-Record.MF4` 1268 + 7 键 0 差异 | +| 行数 | `PlotViewPanelPaint.cpp` 939 → 977(6 个函数头 + 说明);`PlotViewPanels.cpp` 1766 → 1758(`PanelTitle` 变短) | +| 读点 | `PanelStyleOf` / `PanelTitle` / `LanePanelScrollOf` / `LanePanelStateOf` / `LanePanelEnabled` 的调用点**一处未改**(签名不变) | + diff --git a/Documents/Code_Review_Report.md b/Documents/Code_Review_Report.md index c543c22..6b44291 100644 --- a/Documents/Code_Review_Report.md +++ b/Documents/Code_Review_Report.md @@ -825,5 +825,98 @@ old keys = 919, new keys = 965 与 §17.3 同理:先给"吸顶推挤"单独一个可测函数。 3. 体积:等有新的冗余再说(用户已明确)。 +--- + +## 十八、第 41 轮(2026-11-04,收尾):面板登记表 + 标记面板分段,以及"能否结束审查"的判定 + +### 18.1 结论 + +| §17.6 建议 | 结果 | +| --- | --- | +| 1. 三处穷举 `switch` 合成 `constexpr` 表 | ✅ 完成,而且实际收敛的是**五处**(`PanelStyleOf` / `PanelTitle` / `LanePanelScrollOf` / `LanePanelStateOf` / `LanePanelEnabled`) | +| 2. `PaintPanelMarkerGroups` 再拆 | ✅ 拆成 6 个具名函数,主函数只剩"绘制顺序" | +| 3. 体积 | ⏸ 按用户指示不做(+512 B 是本轮唯一代价,来自登记表) | + +### 18.2 本轮做了什么 + +**(1)面板静态特征收敛为唯一登记点** + +```cpp +inline constexpr PanelKindTraits PanelTraitsOf(EdgePanelKind kind); // 唯一 switch +``` + +`PanelKindTraits{style, title, rangeSuffix, scroll, panel, enabled}` —— +新增一种面板只改**两处**:`EdgePanelKind` 加枚举值 + 表里加一行。 +五个消费者的签名一字未改,调用点**一处未动**。 + +`PanelTitle` 从"返回 `std::wstring` 的 switch"变成"读表 + 可选追加 `EdgeRangeSuffix()`"。 +这解决了 §17.6 留下的那个具体障碍:"标题是函数、依赖运行时字符串,所以没法进表"—— +答案是**把标题降级为数据(前缀 + 标志)**,而不是把函数塞进表里。 + +**(2)标记面板绘制分段** + +| 函数 | 职责 | +| --- | --- | +| `MeasureMarkerColumnWidths` | 六列实测宽度 → `Lane::freeMarkerColumnWidths` | +| `PaintMarkerGroupBar` | 分组栏(三角 / 名称 / `N 标记点` / ✕) | +| `PaintMarkerColumnSeparators` | 列分隔竖线(条目与表头共用) | +| `PaintMarkerRow` | 固定点条目(六列 + 分隔线 + ✕) | +| `PaintMarkerTableHeader` | 展开分组的表头 | +| `PaintMarkerOpacityRow` | 「标记透明度」滑块行 | +| `PaintPanelMarkerGroups` | **只负责顺序**:分组栏 → 条目 → 表头 → 吸顶栏 → 滚动条 → 透明度行 | + +判据不是"看起来更清楚",而是 `panels` 的 7 个面板像素哈希 + `rowgeom` 的 +965/1268 条几何键**逐项不变**;产物字节数在 P40 前后**完全相同**(782,336 B), +说明这一段是真正的零成本搬移。 + +### 18.3 现在可以结束这次代码审查了吗?——**可以** + +这不是"我觉得差不多了",而是逐条对账的结果: + +| 审查项 | 状态 | 证据 | +| --- | --- | --- | +| 体积(890,880 → 782,336 B,−12.2%) | 完成 | `objdump -h` 节表;`-fno-rtti` + terminate handler 覆盖 + 关异步展开表;红线 1 MB,余量 ≈260 KB | +| Debug 构建"名不副实" | 已修 | `DEBUG_OPT -g` 真正生效(Debug 6.16 MB 属预期) | +| 面板分派"改一次就出错" | 已消除 | 唯一登记表 + `-Wswitch` 编译期保护 | +| 平行数组错位 / 字段重载 | 已消除 | `std::vector`(结构上不可能错位) | +| 几何算式两处/双遍历 | 已收敛 | `PanelColumnMetrics` + `PanelRowPlan` | +| 巨型函数(1900 / 340 行) | 已拆 | `PlotViewPanelPaint.cpp` 三个内容函数 + 本轮六个标记面板函数 | +| 过时文档/注释 | 已清理 | `UI_Refactor_*` 已删;`agent_mem.md` §2/§3 已校正 | +| 死代码 / 死字段 / TODO | 无 | `-Wall` 零警告 + 全成员引用计数 + 全仓 grep | +| 性能热点 | 无新发现 | 第 32 轮四处卡死已根治;三处"还能再快一点"均已量化并判定不做 | + +**明确"不做"的三项(都有数字支撑,写在这里防止后续重复尝试)**: + +1. `EdgeScanFor` 状态查找哈希化:198 状态 0.097 → 0.073 µs/次,但体积 **+5,632 B**。 +2. `EdgePanelLayoutFor` 表驱动化:实测 1.9~2.5 µs/次(每帧约 30 µs),**不是热点**。 +3. gdtoa(≈33 KB):MinGW 下只要还用 `std::to_*string`(含浮点)就必然链入, + 根因在 libstdc++ 的 `os_defines.h` 无条件 `#define __USE_MINGW_ANSI_STDIO 1`。 + +**剩下的都属"收益 < 风险/成本"**:`PaintPanelListRows` 还可再拆(纯可读性)、 +体积只在出现新冗余时再压、面板几何算式已经是单一来源。 + +### 18.4 合并前的建议动作(给用户) + +1. **远端机器务必全量重编**:本分支动过 `Makefile`(`NO_RTTI` / `RELEASE_ONLY_OPT` / + Debug `-O0 -g`),而只改 Makefile **不会**触发重编 ⇒ 先 `make clean`,再 `make release`。 + 否则会误以为"体积优化无效"或"Debug 还是 -Oz"。 +2. 合并前跑一遍**最小集**(见 `Tools/README.md` §5): + `make release` → `make -C Tools` → `UnitTests` → `OffscreenChecks -all` + → `-run panels -mode compare`(注意:面板/几何基线是**本机生成**的,跨机器不可比, + 远端首次应 `-mode record` 重建或直接跳过 compare)。 +3. 分支顺序:`Code_Review` → `dev` → `main`。本分支只含代码 / 文档 / `Tools/`, + **没有**构建产物、没有 `Temp/`、没有测试数据。 + +### 18.5 本轮验证 + +| 项 | 结果 | +| --- | --- | +| `make release` / `make debug` / `make -C Tools` | 零 error / 零 warning | +| Release 产物 | **782,336 B**(+512,全部来自 P39 登记表;P40 为 0) | +| `UnitTests` | `checks=260 failed=0 cases=19` | +| `OffscreenChecks -all` | 7 检查 / **133** 断言,全 PASS | +| `panels -mode compare` | 7 个面板像素哈希**逐项不变** | +| `rowgeom -mode compare` | 965 + 7 键 / 1268 + 7 键(MF4),**0 差异** | + diff --git a/Documents/agent_mem.md b/Documents/agent_mem.md index 8b55bb4..7be3399 100644 --- a/Documents/agent_mem.md +++ b/Documents/agent_mem.md @@ -10814,3 +10814,65 @@ std::vector PlanListPanelRows(const Lane&, EdgePanelKind) const; 2. `PaintPanelMarkerGroups`(约 260 行)按"分组栏 / 表头 / 条目吸顶"再拆。 3. **体积按用户指示暂停投入**(等后续功能更新产生新冗余时再做)。 +--- + +## 2026-11-04 第 41 轮(`Code_Review` 分支,收尾):面板登记表 + 标记面板绘制分段 + +> 本轮之后**代码审查进入收尾**(判定见 `Code_Review_Report.md` §18.3)。 +> 代码:`Components/PlotView/{Inc,Src}`。判据 = `panels -mode compare` + `rowgeom -mode compare`。 + +### 1. ✅ **取代**第 35 / 40 轮的"新增面板只改三处" + +**已作废**:旧规则「新增一种面板要在 `LanePanelScrollOf` / `PanelStyleOf` / `PanelTitle` 三处登记」。 +**现行**:新增面板只改**两处** —— `EdgePanelKind` 加枚举值 + `pvdetail::PanelTraitsOf` 加一行。 + +```cpp +struct PanelKindTraits +{ + PanelStyle style; // 排版类别 + 底部控件 + const wchar_t* title = L""; // 标题**前缀**(数据,不是函数) + bool rangeSuffix = false; // 是否追加 EdgeRangeSuffix() + float Lane::*scroll = nullptr; // 内容滚动偏移 + LanePanelState Lane::*panel = nullptr; // 面板 UI 状态 + bool Lane::*enabled = nullptr; // 功能开关 +}; +inline constexpr PanelKindTraits PanelTraitsOf(EdgePanelKind kind); // **唯一** switch +``` + +五个消费者(`PanelStyleOf` / `PanelTitle` / `LanePanelScrollOf` / `LanePanelStateOf` / +`LanePanelEnabled`)全部退化成一行取值,**签名未变、调用点一处未动**。 +`-Wswitch` 保护仍只在 `PanelTraitsOf` 这一处(不写 `default:`)。 +⚠️ `PanelStyle` 必须用三个具名构造函数(`ListPanelStyle` / `CountTablePanelStyle` / +`MarkerPanelStyle`):表里写 6 个裸 bool,聚合顺序错一位就是"表格面板带按钮"这种隐形错误。 + +**"标题是函数"的解法**:把标题降级成数据(前缀 + `rangeSuffix` 标志), +`PanelTitle` 只做 `前缀 + (rangeSuffix ? EdgeRangeSuffix() : L"")`。 +不要为此引入函数指针(等于把数据重新变成代码,新增面板又要写函数)。 + +### 2. ✅ `PaintPanelMarkerGroups` 拆成 6 个具名函数 + +`MeasureMarkerColumnWidths` / `PaintMarkerGroupBar` / `PaintMarkerColumnSeparators` / +`PaintMarkerRow` / `PaintMarkerTableHeader` / `PaintMarkerOpacityRow`; +`PaintPanelMarkerGroups` 现在**只负责绘制顺序**: + +**普通分组栏 → 条目 → 表头 → 吸顶分组栏 → 滚动条 → 透明度行** + +🔴 硬规则:**表头与吸顶分组栏必须画在条目之后**(否则向上滚动的条目会盖住它们)。 +这条约束现在只出现在一个函数里,是唯一"看代码就能看全"的顺序约束;新增悬浮/吸顶元素 +一律加在"条目之后"。 + +### 3. 本轮数字 + +* Release 781,824 → **782,336 B(+512)**,全部来自登记表(7 份 traits 记录进 `.rdata`); + **P40 拆分前后字节数完全相同** ⇒ 纯搬移。 +* release / debug / tools 全零 warning;`UnitTests` 260/19;`OffscreenChecks -all` 7 检查 / 133 断言。 +* `panels -mode compare` 7 面板像素哈希逐项不变;`rowgeom compare` 965+7 / 1268+7 键 0 差异。 +* 行数:`PlotViewPanelPaint.cpp` 939 → 977;`PlotViewPanels.cpp` 1766 → 1758。 + +### 4. 审查收尾时确认的三条"别再试" + +1. `EdgeScanFor` 状态查找哈希化:**+5,632 B** / 每轮省 ≈4.8 µs ⇒ 不做。 +2. `EdgePanelLayoutFor` 表驱动化:1.9~2.5 µs/次(每帧 ≈30 µs)⇒ 不是热点。 +3. gdtoa(≈33 KB):libstdc++ 的 `os_defines.h` 无条件 + `#define __USE_MINGW_ANSI_STDIO 1` ⇒ 只要还用 `std::to_*string`(含浮点)就去不掉。 +