From 8f735d51be959ef6240a45a6b77f37f87aed149f Mon Sep 17 00:00:00 2001 From: "DeepSeek V4.1 Flash" <> Date: Wed, 21 Oct 2026 17:21:09 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=8B=86=E5=88=86=E7=BD=91?= =?UTF-8?q?=E6=A0=BC=E8=AF=AF=E7=BB=A7=E6=89=BF=E5=AE=9E=E9=AA=8C=E6=80=A7?= =?UTF-8?q?=E5=8A=9F=E8=83=BD=20/=20=E9=98=B6=E8=B7=83=E7=BA=BF=E5=AE=BD?= =?UTF-8?q?=20/=20=E9=9D=A2=E6=9D=BF=E6=8B=96=E6=8B=BD=E6=AE=8B=E5=BD=B1?= =?UTF-8?q?=20/=20=E8=87=AA=E7=94=B1=E6=A0=87=E8=AE=B0=E5=8D=A1=E9=A1=BF?= =?UTF-8?q?=E4=B8=8E=E4=BA=A4=E4=BA=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 拆分网格:实验性功能只由原网格的首个信号继承(LaneCarry::owner); SplitLane 只让第一份继承;新增 PruneLaneSignalItems 按 lane.signals 过滤四类 逐信号条目(修复"新网格出现别人的固定点、面板里却查不到") - 阶跃信号:包络带横向膨胀半径 ceil -> floor(1px 线宽不再把竖直沿撑成 3px) - 浮动面板:面板位置移出 PanelGlobalKey(拖动单个面板不再重画全部面板), 新增 vacatedRects 让"上一帧被移动/缩小/关闭的面板腾出的区域"也强制重画 - 自由标记:新增 FreeMarkerContents 内容缓存(键不含视界),大量固定点时拖光标/平移不再逐帧采样; 透明度滑块与光标模式解耦(始终显示);悬停胶囊恢复不透明(瞄着被遮住的数据点时除外); 胶囊与面板五列按实测文本撑开,不再省略 - 面板交互:折起指示器与信号列表组名同款三角;GX 徽标/标题栏悬停不改光标、移除徽标气泡; 标题栏双击滚动到该网格(单击不触发,双击不再折起) - 验证:新增离屏 .preflight/fixcheck 37/37(三处 A/B 均能如实失败); 回归 round3check 31/31、round4check 34/34、round5check 5/5、editcheck 20/20、convcheck 18/18; make release/debug 零警告 --- Components/PlotView/Inc/PlotView.hpp | 54 ++- .../PlotView/Src/PlotViewInteraction.cpp | 35 +- Components/PlotView/Src/PlotViewInternal.hpp | 8 +- Components/PlotView/Src/PlotViewPaint.cpp | 456 ++++++++++++------ Documents/agent_mem.md | 138 ++++++ Utilities/Inc/DataModel.hpp | 12 + Utilities/Src/DataModelInternal.hpp | 57 ++- Utilities/Src/DataModelTab.cpp | 92 ++-- 8 files changed, 654 insertions(+), 198 deletions(-) diff --git a/Components/PlotView/Inc/PlotView.hpp b/Components/PlotView/Inc/PlotView.hpp index 2d5d588..256d221 100644 --- a/Components/PlotView/Inc/PlotView.hpp +++ b/Components/PlotView/Inc/PlotView.hpp @@ -1836,6 +1836,46 @@ namespace jdi gd::RectF close; }; + /** + * @brief 「自由标记」某个固定点的**内容**(取值与各种文本)。 + * + * 这些值与**视界无关**(只取决于采样数据与信号样式),因此单独缓存: + * 平移视界、拖动时间光标时不需要重新采样/重新格式化。 + * 面板的五列与网格内的取值胶囊共用这一份内容。 + */ + struct FreeMarkerContentEntry + { + std::size_t itemIndex = 0; /**< `Lane::freeMarkerItems` 下标。 */ + bool valid = false; /**< 取值是否有效(无效时全部文本为 `N/A`)。 */ + double value = 0.0; /**< 绘制用取值(用于算胶囊/圆点的纵向位置)。 */ + std::wstring capsuleText; /**< 网格内取值胶囊的文本(`值(±增量) = 枚举`)。 */ + std::wstring valueText; /**< 面板:取值列。 */ + std::wstring enumText; /**< 面板:枚举列(可能是空串)。 */ + std::wstring deltaText; /**< 面板:增量列。 */ + std::wstring timeText; /**< 面板:时刻列。 */ + std::wstring deltaTimeText; /**< 面板:间隔(Δt)列。 */ + std::uint32_t color = 0; /**< 信号色(信号样式里的颜色,已乘不透明度)。 */ + }; + + /** 一个网格的「自由标记」内容缓存(`key` 只含与视界无关的输入)。 */ + struct FreeMarkerContentCache + { + std::uint64_t key = 0; + std::vector entries; + }; + + /** + * @brief 取某个网格的「自由标记」内容列表(必要时重建,**与视界无关**)。 + * + * ⚠️ 性能硬约束:大量固定点时,逐帧重新 `SampleDisplayedSignalAt` + 拼字符串 + * 是“拖动全局光标/移动视界变卡”的主因。内容只依赖数据与样式,因此缓存起来; + * 命中缓存时每帧只做几何与绘制。 + */ + const std::vector& FreeMarkerContents(int laneIndex) const; + + /** 按网格下标的「自由标记」内容缓存(`mutable`:只在绘制时按需重建)。 */ + mutable std::vector m_freeMarkerContent; + /** * @brief 计算某个网格里「第 index 个固定点」的标注几何(绘制与命中共源)。 * @@ -2407,15 +2447,6 @@ namespace jdi */ void PaintSignalInteraction(gd::Graphics& g); - /** - * @brief 绘制浮动面板「GX」徽标的悬停提示气泡(用户 2026-10-19 要求)。 - * - * 气泡锚定在徽标下方,内容为“G 编号”与当前可执行的操 - *(目标网格已在视界内 / 点击滚动到该网格)。 - * ⚠️ 必须在**面板像素图层之后**绘制(见 `PaintEdgeCursor` 末尾),否则会被缓存进面板位图。 - */ - void PaintPanelBadgeTooltip(gd::Graphics& g); - /** * @brief 在绘图区底部绘制临时提示条(如“已放弃当前选区”)。 * @param [IN,OUT] g 绘图上下文。 @@ -2620,8 +2651,9 @@ namespace jdi /** * @brief 上一帧鼠标是否悬停在某个浮动面板标题栏的「GX」徽标上(是则记录网格下标 / 面板种类)。 * - * 徽标绘制在面板**像素图层**里(缓存键只在鼠标位于面板内时才混入坐标), - * 而提示气泡是贴图之后的叠加层(见 `PaintPanelBadgeTooltip`),因此需要单独记录状态。 + * 徽标本体绘制在面板**像素图层**里(缓存键只在鼠标位于面板内时才混入坐标), + * 因此需要单独记录状态才能让高亮跟着鼠标进出而刷新。 + * ⚠️ 用户 2026-10-21 要求:徽标悬停**不改变鼠标形状**、也**不弹提示气泡**。 */ int m_hoverPanelBadgeLane = -1; int m_hoverPanelBadgeKind = -1; diff --git a/Components/PlotView/Src/PlotViewInteraction.cpp b/Components/PlotView/Src/PlotViewInteraction.cpp index ad413b8..fae17af 100644 --- a/Components/PlotView/Src/PlotViewInteraction.cpp +++ b/Components/PlotView/Src/PlotViewInteraction.cpp @@ -934,12 +934,13 @@ namespace jdi // ✕ 按钮:与面板/标签栏的其它 ✕ 一致,用手型光标作为可点击提示。 return LoadCursorW(nullptr, IDC_HAND); } - // 「GX」编号徽标:悬停给出提示 + 手型光标;点击把该网格滚动到视界内。 - // ⚠️ 必须排在标题栏之前(徽标位于标题栏矩形内部,否则会被判成“可拖拽”)。 + // 「GX」编号徽标:用户 2026-10-21 明确要求**悬停不改变鼠标形状**, + // 也不弹出“是否在视界内”的浮动提示(点击行为不变:滚动到该网格)。 + // ⚠️ 必须提前 return(而不是落到下面的标题栏判定),否则会被当成“可拖拽”。 if (layout.badge.Width > 0.0f && UiPointInRect(layout.badge, static_cast(x), static_cast(y))) { - return LoadCursorW(nullptr, IDC_HAND); + return nullptr; } // 「展开 / 折起」三角指示器:可点击控件 ⇒ 手型光标。 if (layout.disclosure.Width > 0.0f && @@ -949,7 +950,9 @@ namespace jdi } if (UiPointInRect(layout.title, static_cast(x), static_cast(y))) { - return LoadCursorW(nullptr, IDC_SIZEALL); + // 标题栏:悬停**保持箭头**,只有按住左键拖动时(`EDGE_PANEL_MOVE`, + // 见本函数开头的拖拽分支)才变成十字箭头(用户 2026-10-21 要求)。 + return nullptr; } if ((static_cast(panelKind) == EdgePanelKind::MEAN || static_cast(panelKind) == EdgePanelKind::PEAK) && @@ -2720,8 +2723,9 @@ namespace jdi int panelKind = 0; if (TopPanelAtPoint(static_cast(x), static_cast(y), panelLane, panelKind)) { - // 标题栏双击 = 展开 / 折起该面板(折起的面板只显示标题栏)。 - // ⚠️ 折起状态下标题栏是唯一可见区域,因此这里也是“双击重新展开”的入口。 + // 标题栏双击 = 把该面板所属网格**滚动到视界内**(与单击 GX 徽标完全同效)。 + // 用户 2026-10-21 明确要求:**只有双击标题栏**才触发这个联动,单击标题栏不触发 + //(单击标题栏只是准备拖拽面板);面板的折起/展开只由左侧三角指示器负责。 { const EdgePanelLayout titleLayout = EdgePanelLayoutFor(panelLane, static_cast(panelKind)); @@ -2737,7 +2741,11 @@ namespace jdi UiPointInRect(titleLayout.disclosure, static_cast(x), static_cast(y)))) { - ToggleLanePanelCollapsed(panelLane, static_cast(panelKind)); + m_dragMode = PlotDragMode::NONE; + if (ScrollLaneIntoView(panelLane)) + { + SetHint(L"正在滚动到 G" + std::to_wstring(panelLane + 1) + L" 网格…"); + } return true; } } @@ -2884,7 +2892,18 @@ namespace jdi int panelKind = 0; if (TopPanelAtPoint(fx, fy, panelLane, panelKind)) { - return true; + // ⚠️ 例外:**标题栏**(排除徽标 / ✕ / 折起三角)双击有明确语义 —— + // 把该面板所属网格滚动到视界内(用户 2026-10-21 要求,与单击 GX 徽标一致)。 + // 其余落点仍然只有“按下”语义。 + const EdgePanelLayout layout = + EdgePanelLayoutFor(panelLane, static_cast(panelKind)); + const bool onTitle = layout.title.Width > 0.0f && + UiPointInRect(layout.title, fx, fy) && + !(layout.badge.Width > 0.0f && UiPointInRect(layout.badge, fx, fy)) && + !(layout.close.Width > 0.0f && UiPointInRect(layout.close, fx, fy)) && + !(layout.disclosure.Width > 0.0f && + UiPointInRect(layout.disclosure, fx, fy)); + return !onTitle; } } // 右下角「回到最上方」浮动按钮:按下只登记、抬起才生效 ⇒ 只有单击语义。 diff --git a/Components/PlotView/Src/PlotViewInternal.hpp b/Components/PlotView/Src/PlotViewInternal.hpp index 2b432ba..5b22c6f 100644 --- a/Components/PlotView/Src/PlotViewInternal.hpp +++ b/Components/PlotView/Src/PlotViewInternal.hpp @@ -1074,7 +1074,13 @@ namespace jdi // 列间距由前两列推断(同一段内等宽等距);退化时按 1px 处理。 const float spacing = columns[1].x - columns[0].x; const float step = (spacing > 0.01f) ? spacing : 1.0f; - std::size_t radius = static_cast(std::ceil(halfWidth / step)); + // ⚠️ **必须向下取整**(用户 2026-10-21 报告的阶跃信号线宽不一致): + // `ceil` 会让 1px 线宽(`halfWidth` ≈ 0.5–0.7 设备像素)也膨胀 1 列, + // 阶跃的竖直沿因此从 1 列变成 3 列(≈3px),而水平段只有 ~1.25px ⇒ + // 视觉上“竖直跳变的线宽明显大于水平持续段”(放大到走逐点描边路径后又正常了)。 + // `floor` 下 1px 线宽不膨胀(竖直沿 = 1 列),粗线(≥ 2 列宽)仍按整数列膨胀。 + const std::size_t radius = + static_cast(std::floor(halfWidth / step + 1e-4f)); if (radius > 0 && radius * 2 + 1 < count) { widened.resize(count); diff --git a/Components/PlotView/Src/PlotViewPaint.cpp b/Components/PlotView/Src/PlotViewPaint.cpp index 66f73b1..728eef9 100644 --- a/Components/PlotView/Src/PlotViewPaint.cpp +++ b/Components/PlotView/Src/PlotViewPaint.cpp @@ -144,12 +144,42 @@ namespace jdi if (kind == EdgePanelKind::MARK) { - // 「自由标记」面板:**宽度用固定值**,不做逐行文本测量。 - // ⚠️ `EdgePanelWidth` 被 `EdgePanelLayoutFor` 调用,而后者又在 `TopPanelAtPoint` / - // `SyncPanelOrder` 里被高频调用(每次鼠标移动数次)——若在这里逐行取采样值算文本宽度, - // 就变成"鼠标一动就做几十次采样查询",会明显拖慢交互。 - // 取值的文本超长时由绘制侧的 `UiEllipsize` 截断即可。 - width = std::max(width, kFreeMarkerPanelWidth * m_scale); + // 「自由标记」面板:宽度 = 五列(取「缺省列宽」与**实测内容宽度**的较大者)+ 行 ✕ + // + 左右内边距 + **滚动条预留位**(内容超高时右侧要放滚动条)。 + // ⚠️ 用户 2026-10-21 要求面板里的数据列**不得出现省略号**,因此不能再按固定列宽。 + // 逐行取采样值算宽度会拖慢交互(本函数被 `TopPanelAtPoint` / `SyncPanelOrder` + // 高频调用),所以走 `Lane::freeMarkerColumnWidths`:绘制该面板时顺便量好真实 + // 文本宽度存起来,这里只读。还没量过(全 0)时退回缺省列宽。 + const bool hasEnum = [this, &lane]() { + for (const SignalRef& ref : lane.signals) + { + const ResolvedSignal resolved = ResolveSignal(ref); + if (resolved.data != nullptr && resolved.visible && + !resolved.data->enumText.Empty() && lane.HasFreeMarkers(ref)) + { + return true; + } + } + return false; + }(); + float columnsWidth = 0.0f; + for (int column = 0; column < 5; ++column) + { + const bool omitted = (column == EdgePanelLayout::MARKER_COL_ENUM) && !hasEnum; + if (omitted) + { + continue; + } + columnsWidth += std::max(kFreeMarkerColumnWidths[column] * m_scale, + lane.freeMarkerColumnWidths[static_cast(column)]); + columnsWidth += kFreeMarkerColumnGap * m_scale; + } + // 行尾 ✕(按胶囊同高的正方形取宽)+ 滚动条预留 + 少量余量。 + const float rowCloseWidth = kFreeMarkerItemRowHeight * m_scale + 2.0f * m_scale; + const float markerWidth = kEdgePanelPad * 2.0f * m_scale + + (kFreeMarkerScrollWidth + 4.0f) * m_scale + columnsWidth + + rowCloseWidth; + width = std::max(width, std::max(kFreeMarkerPanelWidth * m_scale, markerWidth)); } // 夹在绘图区宽度之内(过窄的网格也不至于把面板挤到外面)。 @@ -5441,8 +5471,8 @@ namespace jdi const float groupRow = kFreeMarkerNameRowHeight * m_scale; const float itemRow = kFreeMarkerItemRowHeight * m_scale; const float markerButtonHeight = kEdgePanelButtonHeight * m_scale; - const float sliderRow = - lane.freeMarkerCursor ? (kFreeMarkerSliderRowHeight * m_scale) : 0.0f; + // 「标记透明度」滑块行**始终显示**(用户 2026-10-21 要求:不再依附于光标模式按钮)。 + const float sliderRow = kFreeMarkerSliderRowHeight * m_scale; float content = 0.0f; std::vector order; for (const SignalRef& ref : lane.signals) @@ -5735,16 +5765,17 @@ namespace jdi const float itemRow = kFreeMarkerItemRowHeight * m_scale; const float closeSize = kFreeMarkerCloseSize * m_scale; const float markerButtonHeight = kEdgePanelButtonHeight * m_scale; - const float sliderRow = lane.freeMarkerCursor ? (kFreeMarkerSliderRowHeight * m_scale) : 0.0f; + // 「标记透明度」独立于光标模式按钮:**总是**显示在按钮上方。 + const float sliderRow = kFreeMarkerSliderRowHeight * m_scale; const float scrollW = kFreeMarkerScrollWidth * m_scale; // 底部固定区:切换按钮(整行宽度、与面板下边界留一个 pad)。 layout.button = gd::RectF(left + pad, top + height - pad - markerButtonHeight, std::max(0.0f, width - pad * 2.0f), markerButtonHeight); - if (sliderRow > 0.0f) { // 「标记透明度」行:与信号样式区的「透明度」行同一套排版 - //(左侧标签 + 中间轨道 + 右侧百分比),只是位置固定在按钮上方。 + //(左侧标签 + 中间轨道 + 右侧百分比),位置固定在按钮上方。 + // ⚠️ 无论是否启用「光标选取模式」都显示(用户 2026-10-21 要求)。 const float rowTop = layout.button.Y - sliderRow; const float valueWidth = std::max(36.0f * m_scale, @@ -5829,8 +5860,11 @@ namespace jdi for (int column = 0; column < 5; ++column) { const bool omitted = (column == EdgePanelLayout::MARKER_COL_ENUM) && !hasEnum; + // ⚠️ 列宽取「缺省列宽」与**实测内容宽度**的较大者 ⇒ 面板里不再出现省略号。 const float columnWidth = - omitted ? 0.0f : (kFreeMarkerColumnWidths[column] * m_scale); + omitted ? 0.0f + : std::max(kFreeMarkerColumnWidths[column] * m_scale, + lane.freeMarkerColumnWidths[static_cast(column)]); layout.markerColumns[column].left = columnX; layout.markerColumns[column].width = columnWidth; columnX += columnWidth + (omitted ? 0.0f : kFreeMarkerColumnGap * m_scale); @@ -6303,18 +6337,23 @@ namespace jdi const std::vector& lanes = m_tab->Lanes(); for (const Lane& lane : lanes) { - mixFloat(static_cast(lane.edgePanel.x)); - mixFloat(static_cast(lane.edgePanel.y)); - mixFloat(static_cast(lane.edgeCenterPanel.x)); - mixFloat(static_cast(lane.edgeCenterPanel.y)); - mixFloat(static_cast(lane.meanFilterPanel.x)); - mixFloat(static_cast(lane.meanFilterPanel.y)); - mixFloat(static_cast(lane.weakPeakPanel.x)); - mixFloat(static_cast(lane.weakPeakPanel.y)); + // ⚠️⚠️ **面板的位置 / 尺寸绝不能进全局键**(用户 2026-10-21 报告“拖动面板时 + // 下方另一个面板出现拖拽残影”):位置只影响该面板自己,已在 `PanelSlotKey` + // 里(它混合 `layout.panel/title/...`)。以前把 4 类面板的 x/y 混进全局键 ⇒ + // 拖动任意面板都会让**所有**面板重画(15 个 ≈33ms/帧),而「自由标记」面板的 + // 位置偏偏漏了没混 ⇒ 拖动它时其它面板照旧贴旧像素 → 正好在下方留残影。 + // 现在:拖动面板只重画它自己;它**腾出的区域**由 `PaintEdgeCursor` 的 + // `vacatedRects` 机制兜底(覆盖到该区域的面板必须重画)。 mix(lane.edgePanel.open ? 1ull : 0ull); mix(lane.edgeCenterPanel.open ? 1ull : 0ull); mix(lane.meanFilterPanel.open ? 1ull : 0ull); mix(lane.weakPeakPanel.open ? 1ull : 0ull); + mix(lane.freeMarkerPanel.open ? 1ull : 0ull); + mix(lane.edgePanel.collapsed ? 1ull : 0ull); + mix(lane.edgeCenterPanel.collapsed ? 1ull : 0ull); + mix(lane.meanFilterPanel.collapsed ? 1ull : 0ull); + mix(lane.weakPeakPanel.collapsed ? 1ull : 0ull); + mix(lane.freeMarkerPanel.collapsed ? 1ull : 0ull); mix(lane.edgeValueValid ? 1ull : 0ull); mixDouble(lane.edgeValue); } @@ -6614,6 +6653,103 @@ namespace jdi return true; } + const std::vector& PlotView::FreeMarkerContents(int laneIndex) const + { + static const std::vector kNoEntries; + if (m_tab == nullptr || laneIndex < 0 || + laneIndex >= static_cast(m_tab->Lanes().size())) + { + return kNoEntries; + } + const Lane& lane = m_tab->Lanes()[static_cast(laneIndex)]; + if (static_cast(laneIndex) >= m_freeMarkerContent.size()) + { + m_freeMarkerContent.resize(static_cast(laneIndex) + 1); + } + FreeMarkerContentCache& cache = m_freeMarkerContent[static_cast(laneIndex)]; + + // 缓存键:只混入**与视界无关**的输入(条目、信号样式、数据地址与长度)。 + // ⚠️ 绝不能混入 `ViewMin/ViewMax`、鼠标位置或网格几何 —— 否则平移视界、 + // 拖动时间光标时缓存永远命中不了,又回到"每帧对每个固定点采样一次"。 + std::uint64_t key = 1469598103934665603ull; + const auto mix = [&key](std::uint64_t value) { + key ^= value; + key *= 1099511628211ull; + }; + const auto mixFloat = [&mix](float value) { + std::uint32_t bits = 0; + std::memcpy(&bits, &value, sizeof(bits)); + mix(static_cast(bits)); + }; + const auto mixDouble = [&mix](double value) { + std::uint64_t bits = 0; + std::memcpy(&bits, &value, sizeof(bits)); + mix(bits); + }; + mix(static_cast(lane.freeMarkerItems.size())); + for (const Lane::FreeMarkerItem& item : lane.freeMarkerItems) + { + mix(static_cast(static_cast(item.ref.groupIndex))); + mix(static_cast(static_cast(item.ref.signalIndex))); + mixDouble(item.time); + const ResolvedSignal resolved = ResolveSignal(item.ref); + if (resolved.data == nullptr || resolved.style == nullptr) + { + mix(0ull); + continue; + } + mix(reinterpret_cast(resolved.data)); + mix(static_cast(resolved.data->values.size())); + mix(resolved.visible ? 1ull : 2ull); + mix(static_cast(resolved.style->color)); + mixFloat(resolved.style->opacity); + mixFloat(resolved.style->gain); + mixFloat(resolved.style->offsetY); + mixDouble(resolved.style->offsetTime); + mix(static_cast(static_cast(resolved.style->mode))); + mix((resolved.style->absolute ? 1ull : 0ull) | (resolved.style->mirrorVertical ? 2ull : 0ull) | + (resolved.style->mirrorHorizontal ? 4ull : 0ull)); + } + + if (cache.key != key) + { + cache.key = key; + cache.entries.clear(); + cache.entries.reserve(lane.freeMarkerItems.size()); + for (std::size_t itemIndex = 0; itemIndex < lane.freeMarkerItems.size(); ++itemIndex) + { + const Lane::FreeMarkerItem& item = lane.freeMarkerItems[itemIndex]; + FreeMarkerContentEntry entry; + entry.itemIndex = itemIndex; + double value = 0.0; + double enumValue = 0.0; + const bool valueOk = FreeMarkerValue(lane, item, value, enumValue); + bool hasPrev = false; + double delta = 0.0; + double timeDelta = 0.0; + if (valueOk) + { + FreeMarkerDelta(lane, itemIndex, value, hasPrev, delta, timeDelta); + } + const ResolvedSignal resolved = ResolveSignal(item.ref); + const std::wstring* enumPtr = + (resolved.data != nullptr) ? resolved.data->enumText.Find(enumValue) : nullptr; + entry.valid = valueOk; + entry.value = value; + entry.color = (resolved.style != nullptr) ? StyleColor(*resolved.style) : 0u; + entry.capsuleText = FreeMarkerValueText(value, hasPrev, delta, enumPtr); + entry.valueText = valueOk ? FormatValue(value) : std::wstring(kFreeMarkerNotAvailable); + entry.enumText = (enumPtr != nullptr) ? *enumPtr : std::wstring(); + entry.deltaText = (valueOk && hasPrev) ? FormatSignedValue(delta) + : std::wstring(kFreeMarkerNotAvailable); + entry.timeText = FreeMarkerTimeText(item.time); + entry.deltaTimeText = FreeMarkerDeltaText(valueOk && hasPrev, timeDelta); + cache.entries.push_back(std::move(entry)); + } + } + return cache.entries; + } + bool PlotView::FreeMarkerLabelRects(int laneIndex, std::size_t itemIndex, gd::RectF& label, gd::RectF& close, std::wstring& text) const { @@ -6632,16 +6768,14 @@ namespace jdi return false; } const Lane::FreeMarkerItem& item = lane.freeMarkerItems[itemIndex]; - double value = 0.0; - double enumValue = 0.0; - if (!FreeMarkerValue(lane, item, value, enumValue)) + // 取值与文本来自**与视界无关**的内容缓存(大量固定点 + 拖光标/平移时不重复采样)。 + const std::vector& contents = FreeMarkerContents(laneIndex); + if (itemIndex >= contents.size() || !contents[itemIndex].valid) { return false; } - bool hasPrev = false; - double delta = 0.0; - double timeDelta = 0.0; - FreeMarkerDelta(lane, itemIndex, value, hasPrev, delta, timeDelta); + const FreeMarkerContentEntry& entry = contents[itemIndex]; + const double value = entry.value; // 超出视界**直接不显示**(用户明确要求:不按常规数据点那样贴边定格 + 三角指示)。 const float rawX = TimeToX(item.time); @@ -6660,14 +6794,12 @@ namespace jdi } // 枚举文本按**采样原值**查找(与光标读数完全一致)。 - const ResolvedSignal labelResolved = ResolveSignal(item.ref); - const std::wstring* enumText = (labelResolved.data != nullptr) - ? labelResolved.data->enumText.Find(enumValue) - : nullptr; - text = FreeMarkerValueText(value, hasPrev, delta, enumText); + text = entry.capsuleText; // ✕ 按钮 = **与取值胶囊同高的正方形**(用户明确要求:长宽都等于胶囊的上下高度)。 - const float textWidth = UiMeasureTextWidth(m_fonts->monoSmall, text) + 10.0f * m_scale; + // ⚠️ 宽度多加 2px 余量:`DrawTextW` 的 `DT_END_ELLIPSIS` 有时会因为亚像素/字形 + // 伸部而把“刚好装满”的文本截成省略号(用户报告枚举文本被 … 缺省)。 + const float textWidth = UiMeasureTextWidth(m_fonts->monoSmall, text) + 12.0f * m_scale; const float textHeight = 17.0f * m_scale; const float closeSize = textHeight; const float gap = kFreeMarkerCloseGap * m_scale; @@ -6803,6 +6935,8 @@ namespace jdi continue; } const float markerAlpha = std::max(0.0f, std::min(1.0f, lane.freeMarkerOpacity)); + // 内容(取值 / 文本 / 信号色)来自与视界无关的缓存,不逐帧重新采样。 + const std::vector& contents = FreeMarkerContents(static_cast(laneIndex)); std::vector placed; for (std::size_t itemIndex = 0; itemIndex < lane.freeMarkerItems.size(); ++itemIndex) { @@ -6814,20 +6948,18 @@ namespace jdi continue; // 超出视界直接不显示(不贴边、不画三角) } const Lane::FreeMarkerItem& item = lane.freeMarkerItems[itemIndex]; - const ResolvedSignal resolved = ResolveSignal(item.ref); - if (resolved.style == nullptr) + if (itemIndex >= contents.size()) { continue; } - const std::uint32_t signalColor = StyleColor(*resolved.style); + const FreeMarkerContentEntry& entry = contents[itemIndex]; + const std::uint32_t signalColor = entry.color; const LaneVisual& visual = lanes[laneIndex]; const float clipTop = std::max(visual.rect.Y, m_plotRect.Y); const float clipBottom = std::min(visual.rect.Y + visual.rect.Height, m_plotRect.Y + m_plotRect.Height); - double value = 0.0; - double enumValue = 0.0; - const bool valueOk = FreeMarkerValue(lane, item, value, enumValue); + const bool valueOk = entry.valid; // ---- 纵向避让:与本次已放置的胶囊重叠时,向下 / 向上交替错开 ---- const auto occupied = [&placed](const gd::RectF& rect) { @@ -6877,13 +7009,32 @@ namespace jdi const float radius = 3.4f * m_scale; const int dotAlpha = static_cast(std::lround(255.0f * markerAlpha)); gd::SolidBrush dot(ToGdiColor(WithAlpha(signalColor, dotAlpha))); - const float dotY = valueOk ? ValueToY(value, visual) : (clipTop + clipBottom) * 0.5f; + const float dotY = + valueOk ? ValueToY(entry.value, visual) : (clipTop + clipBottom) * 0.5f; g.FillEllipse(&dot, x - radius, dotY - radius, radius * 2.0f, radius * 2.0f); // ✕ 按钮的悬停态决定它的配色,因此先算再画胶囊。 const bool hot = (m_freeMarkerHoverLane == static_cast(laneIndex) && m_freeMarkerHoverItem == itemIndex); - PaintFreeMarkerCapsule(g, label, text, signalColor, close, hot, markerAlpha); + // 「标记透明度」:鼠标停在某个取值胶囊上时把**这一个**胶囊恢复为全不透明 + //(用户 2026-10-21 要求:既能看淡全局标记,又能读清当前关注的数值)。 + // ⚠️ 例外:如果此刻跟随探针正瞄着一个**被这个胶囊遮住的数据点** + //(用户意图标记被遮住的那条曲线),则不恢复 —— 否则胶囊会重新遮住目标点、 + // 无法看出自己到底瞄到了哪个采样点。 + float capsuleAlpha = markerAlpha; + const float fx = static_cast(m_mouseX); + const float fy = static_cast(m_mouseY); + if (markerAlpha < 0.999f && UiPointInRect(label, fx, fy)) + { + const bool probeHiddenByLabel = + m_freeMarker.valid && m_freeMarker.laneIndex == static_cast(laneIndex) && + UiPointInRect(label, m_freeMarker.x, m_freeMarker.y); + if (!probeHiddenByLabel) + { + capsuleAlpha = 1.0f; + } + } + PaintFreeMarkerCapsule(g, label, text, signalColor, close, hot, capsuleAlpha); FreeMarkerHit hit; hit.laneIndex = static_cast(laneIndex); @@ -7261,8 +7412,62 @@ namespace jdi // ⚠️ **按层序从下往上绘制**(与命中测试、像素缓存的层叠顺序完全一致): // `m_panelOrder` 队尾 = 最上层 = 最后绘制。 SyncPanelOrder(); - for (const PanelRef& panelRef : m_panelOrder) + + // 预扫描一:本帧各面板的布局(循环里直接复用,避免每帧重复计算)。 + std::vector panelLayouts(m_panelOrder.size()); + for (std::size_t ordinal = 0; ordinal < m_panelOrder.size(); ++ordinal) { + panelLayouts[ordinal] = EdgePanelLayoutFor( + m_panelOrder[ordinal].laneIndex, static_cast(m_panelOrder[ordinal].kind)); + } + + // 预扫描二:上一帧已缓存、但本帧**位置/尺寸变了(或被移除)**的面板——它们腾出的 + // 区域可能仍然留在**其它面板**的缓存像素里(那块像素是上一帧连它一起拷下来的), + // 于是鼠标拖动面板时,下方面板上会出现拖拽残影(用户 2026-10-21 报告)。 + // 把腾空区域也当“脏区”,强制覆盖到它的面板重画,就能彻底消除残影。 + std::vector vacatedRects; + if (panelCacheReady && !m_panelSlots.empty()) + { + const float margin = 4.0f * m_scale; + for (const PanelSlot& slot : m_panelSlots) + { + int cx0 = slot.x0; + int cy0 = slot.y0; + int cx1 = slot.x1; + int cy1 = slot.y1; + bool alive = false; + for (std::size_t ordinal = 0; ordinal < m_panelOrder.size(); ++ordinal) + { + if (m_panelOrder[ordinal].laneIndex != slot.laneIndex || + m_panelOrder[ordinal].kind != slot.kind) + { + continue; + } + const gd::RectF& panel = panelLayouts[ordinal].panel; + if (panel.Width <= 0.0f || panel.Height <= 0.0f) + { + break; + } + cx0 = std::max(0, static_cast(std::floor(panel.X - margin))); + cy0 = std::max(0, static_cast(std::floor(panel.Y - margin))); + cx1 = std::max(cx0, static_cast(std::ceil(panel.X + panel.Width + margin))); + cy1 = std::max(cy0, static_cast(std::ceil(panel.Y + panel.Height + margin))); + alive = true; + break; + } + if (!alive || cx0 != slot.x0 || cy0 != slot.y0 || cx1 != slot.x1 || cy1 != slot.y1) + { + vacatedRects.push_back(gd::RectF(static_cast(slot.x0), + static_cast(slot.y0), + static_cast(slot.x1 - slot.x0), + static_cast(slot.y1 - slot.y0))); + } + } + } + + for (std::size_t panelOrdinal = 0; panelOrdinal < m_panelOrder.size(); ++panelOrdinal) + { + const PanelRef& panelRef = m_panelOrder[panelOrdinal]; const EdgePanelKind kind = static_cast(panelRef.kind); const std::size_t index = static_cast(panelRef.laneIndex); const bool centerAxis = (kind == EdgePanelKind::CENTER); @@ -7278,7 +7483,8 @@ namespace jdi continue; } const Lane& tabLane = tabLanes[index]; - const EdgePanelLayout layout = EdgePanelLayoutFor(static_cast(index), kind); + // 布局已在预扫描里算好(同一帧内复用,避免重复计算)。 + const EdgePanelLayout& layout = panelLayouts[panelOrdinal]; const gd::RectF& panel = layout.panel; if (panel.Width <= 0.0f || panel.Height <= 0.0f) { @@ -7302,6 +7508,19 @@ namespace jdi break; } } + if (!forcedDirty) + { + // 上一帧有面板在这里腾出了位置(移动 / 缩小 / 关闭)⇒ 本面板的缓存像素可能 + // 仍然包含那块旧内容,必须重画(否则拖动上面那个面板时这里会留残影)。 + for (const gd::RectF& vacated : vacatedRects) + { + if (vacated.IntersectsWith(cachedRect)) + { + forcedDirty = true; + break; + } + } + } std::uint64_t slotKey = 0; const std::size_t slotIndex = nextSlots.size(); @@ -7360,8 +7579,9 @@ namespace jdi UiFillRoundRect(g, gd::RectF(panel.X, panel.Y, panel.Width, titleHeight), 5.0f * m_scale, WithAlpha(m_theme->accent, 0x2A)); - // 标题栏左侧的「展开 / 折起」指示器:三角(朝下 = 已展开、朝右 = 已折起) - // + 三角后方的一条短横线(经典展开/折起标识),位于 GX 徽标左边。 + // 标题栏左侧的「展开 / 折起」指示器:**与信号列表组名最左边的折叠三角完全同款** + //(同一尺寸 12 逻辑像素、同一个 `UiDrawTriangleIcon`、同样只在悬停时变强调色、 + // 不再带三角后方那条横线 —— 用户 2026-10-21 要求“统一使用相同标记”)。 // 单击它切换折起状态;折起后面板只剩标题栏,这里是唯一的“重新展开”按钮。 { const gd::RectF& disclosureRect = layout.disclosure; @@ -7373,17 +7593,14 @@ namespace jdi static_cast(m_mouseY)); const std::uint32_t disclosureColor = disclosureHot ? m_theme->accent : m_theme->text; - const float centerX = disclosureRect.X + 5.5f * m_scale; + const float centerX = disclosureRect.X + disclosureRect.Width * 0.5f; const float centerY = disclosureRect.Y + disclosureRect.Height * 0.5f; UiDrawTriangleIcon( g, - gd::RectF(centerX - 4.5f * m_scale, centerY - 4.5f * m_scale, - 9.0f * m_scale, 9.0f * m_scale), + gd::RectF(centerX - 6.0f * m_scale, centerY - 6.0f * m_scale, + 12.0f * m_scale, 12.0f * m_scale), disclosureColor, layout.collapsed ? IconDirection::RIGHT : IconDirection::DOWN); - UiDrawLine(g, centerX + 6.0f * m_scale, centerY, - disclosureRect.X + disclosureRect.Width, centerY, disclosureColor, - 1.4f * m_scale); } } @@ -7751,60 +7968,79 @@ namespace jdi } // ---- 固定点条目(五列,列起点由布局给定 ⇒ 纵向严格对齐)---- + // ⚠️ 取值与各列文本来自**与视界无关的内容缓存**(`FreeMarkerContents`): + // 大量固定点时逐帧重新采样会让"拖光标 / 平移视界"明显变卡。 + const std::vector& contents = + FreeMarkerContents(static_cast(index)); + // 五列的**实测宽度**:含滚出视界的条目,保证列宽稳定、文本不被省略。 + // 存回 `Lane` 供下一步的布局(`EdgePanelWidth` / `EdgePanelLayoutFor`)使用。 + { + float widest[5] = {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 >= 5) + { + return; + } + widest[column] = std::max(widest[column], + UiMeasureTextWidth(m_fonts->monoSmall, text)); + }; + 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 < 5; ++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; + } + } + } for (const EdgePanelLayout::MarkerRow& markerRow : layout.markerRows) { - const Lane::FreeMarkerItem& item = tabLane.freeMarkerItems[markerRow.itemIndex]; - const ResolvedSignal resolved = ResolveSignal(item.ref); - if (resolved.data == nullptr || resolved.style == nullptr) + if (markerRow.itemIndex >= contents.size()) { continue; } - double value = 0.0; - double enumValue = 0.0; - const bool valueOk = FreeMarkerValue(tabLane, item, value, enumValue); - bool hasPrev = false; - double delta = 0.0; - double timeDelta = 0.0; - if (valueOk) - { - FreeMarkerDelta(tabLane, markerRow.itemIndex, value, hasPrev, delta, - timeDelta); - } - const std::wstring* enumText = resolved.data->enumText.Find(enumValue); - const std::wstring valueText = - valueOk ? FormatValue(value) : std::wstring(kFreeMarkerNotAvailable); - const std::wstring deltaText = - (valueOk && hasPrev) ? FormatSignedValue(delta) - : std::wstring(kFreeMarkerNotAvailable); - const std::wstring timeText = FreeMarkerTimeText(item.time); - const std::wstring timeDeltaText = - FreeMarkerDeltaText(valueOk && hasPrev, timeDelta); + const FreeMarkerContentEntry& entry = contents[markerRow.itemIndex]; 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`。 const auto paintCell = [&](EdgePanelLayout::MarkerColumnIndex column, const std::wstring& text, std::uint32_t color) { if (layout.markerColumns[column].width <= 0.0f || text.empty()) { return; } - const gd::RectF rect = columnRect(column); - UiText(g, UiEllipsize(text, m_fonts->monoSmall, rect.Width), - m_fonts->monoSmall, rect, color, TextAlign::LEFT); + UiText(g, text, m_fonts->monoSmall, columnRect(column), color, + TextAlign::LEFT); }; - paintCell(EdgePanelLayout::MARKER_COL_VALUE, valueText, m_theme->textStrong); + paintCell(EdgePanelLayout::MARKER_COL_VALUE, entry.valueText, + m_theme->textStrong); if (layout.markerHasEnumColumn) { - paintCell(EdgePanelLayout::MARKER_COL_ENUM, - (enumText != nullptr) ? *enumText : std::wstring(), - m_theme->text); + paintCell(EdgePanelLayout::MARKER_COL_ENUM, entry.enumText, m_theme->text); } - paintCell(EdgePanelLayout::MARKER_COL_DELTA, deltaText, m_theme->textMuted); - paintCell(EdgePanelLayout::MARKER_COL_TIME, timeText, m_theme->textMuted); - paintCell(EdgePanelLayout::MARKER_COL_DT, timeDeltaText, m_theme->textMuted); + 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); // 行尾 ✕:移除该固定点(与之前一致)。 const gd::RectF& closeRect = markerRow.close; @@ -7960,58 +8196,6 @@ namespace jdi m_panelSlots = std::move(nextSlots); m_panelGlobalKey = panelGlobalKey; m_panelValid = panelCacheReady; - - // 「GX」徽标的悬停提示气泡:必须在**全部面板像素都存进图层之后**绘制, - // 否则气泡会被烘进面板缓存(下一帧即使鼠标移开也会重现)。 - PaintPanelBadgeTooltip(g); - } - - void PlotView::PaintPanelBadgeTooltip(gd::Graphics& g) - { - if (m_hoverPanelBadgeLane < 0 || m_fonts == nullptr || m_fonts->uiSmall == nullptr || - m_tab == nullptr || m_hoverPanelBadgeKind < 0) - { - return; - } - const EdgePanelKind kind = static_cast(m_hoverPanelBadgeKind); - const EdgePanelLayout layout = EdgePanelLayoutFor(m_hoverPanelBadgeLane, kind); - if (layout.badge.Width <= 0.0f) - { - return; - } - - // 目标网格是否已完整可见:决定提示文案(不可见时提示“点击会滚动过去”)。 - bool laneVisible = false; - const std::size_t laneIndex = static_cast(m_hoverPanelBadgeLane); - if (laneIndex < m_lanes.size()) - { - const gd::RectF& rect = m_lanes[laneIndex].rect; - laneVisible = (rect.Y >= m_plotRect.Y - 0.5f) && - (rect.Y + rect.Height <= m_plotRect.Y + m_plotRect.Height + 0.5f); - } - const std::wstring label = L"G" + std::to_wstring(m_hoverPanelBadgeLane + 1); - const std::wstring text = - label + (laneVisible ? L" · 已在视界内" : L" · 点击滚动到该网格"); - - const float padX = 8.0f * m_scale; - const float height = 22.0f * m_scale; - const float width = std::min(UiMeasureTextWidth(m_fonts->uiSmall, text) + padX * 2.0f, - std::max(10.0f, m_plotRect.Width - 4.0f * m_scale)); - float left = layout.badge.X - 2.0f * m_scale; - left = std::max(m_plotRect.X + 2.0f * m_scale, - std::min(left, m_plotRect.X + m_plotRect.Width - width - 2.0f * m_scale)); - // 默认贴在徽标下方;下方放不下就翻到上方(与弹出菜单的翻转规则一致)。 - float top = layout.badge.Y + layout.badge.Height + 4.0f * m_scale; - if (top + height > m_plotRect.Y + m_plotRect.Height - 2.0f * m_scale) - { - top = layout.badge.Y - height - 4.0f * m_scale; - } - top = std::max(m_plotRect.Y + 2.0f * m_scale, top); - - const gd::RectF bubble(left, top, width, height); - UiFillRoundRect(g, bubble, 4.0f * m_scale, m_theme->readoutBg); - UiDrawRoundRect(g, bubble, 4.0f * m_scale, m_theme->borderStrong, 1.0f); - UiText(g, text, m_fonts->uiSmall, bubble, m_theme->textStrong, TextAlign::CENTER); } void PlotView::PaintEditField(gd::Graphics& g) diff --git a/Documents/agent_mem.md b/Documents/agent_mem.md index bc0f7cf..240a84c 100644 --- a/Documents/agent_mem.md +++ b/Documents/agent_mem.md @@ -3015,6 +3015,8 @@ else if (panelOnly) { /* 只回送“本帧重绘过的区域并集” */ } | 2026-10-21 | DeepSeek-V4.1-Flash(**本机**) | **「自由标记」体验重做 + 全部实验性面板可折起(详见 §12 末尾「2026-10-21 第二轮」与 §11.2 第 194–201 条)**。① **网格内固定点**:取值胶囊互相重叠时做**纵向避让**;✕ 改为**与胶囊同高的正方形**;**删除纵向半透明竖线**;新增「标记透明度」(0–100%,作用于本网格全部固定点:圆点 / 胶囊 / 文字 / ✕);⚠️ **GDI `DrawTextW` 忽略 alpha**,因此半透明时文字改走 **GDI+ `DrawString`**(不透明度 100% 时仍走 `UiText`,像素零变化),实测 0% 时胶囊区域与“无标记”帧逐像素相同(1433→**0**)。② **面板布局重做**:每个有固定点的信号一个**信号色托底分组栏**(三角 + 名称 + `N 标记点` + ✕,文字/三角/✕ 一律用 `UiContrastInk` 自适应对比色,点击分组栏收起该组);条目改成**五列**(取值 / 枚举 / 增量 / 时刻 / 间隔),首项增列与Δt列显示 `N/A`,**整个面板没有枚举文本时枚举列整列缺省**。③ **内容滚动**:内容超高时右侧出现滚动条(分组栏与条目一起滚动),滚轮在内容区翻页;底部按钮与透明度滑块**不参与滚动**。④ **面板折起**:`LanePanelState` 新增 `collapsed`,标题栏 GX 徽标左侧新增「三角 + 横线」指示器(`kEdgePanelDisclosureWidth`),单击折起/展开、双击标题栏切换;折起后只剩标题栏(五类面板全部支持)。验证:新增离屏 `.preflight/perf/mark2check.exe`(**69/69**,含 A/B:恢复竖线 ⇒ 1 项失败;禁用避让 ⇒ 2 项失败);回归 `datacheck` 69/69、`verify` 49/49、`panelanim` 158/158、`errcheck` 19/19;`make release`/`make debug` 零警告;`TargetCheck.exe` 866,816 字节。 | +| 2026-10-21 | DeepSeek-V4.1-Flash(**本机**) | **四处 BUG 修复 + 两处交互改进(详见 §12 末尾「2026-10-21 第三轮」与 §11.2 第 203–212 条)**。① **合并 → 拆分会把一个网格的实验性功能复制到每个拆分结果**(用户:第二个网格也自动启用了功能;「自由标记」还会在新网格里画出**别的信号的固定点**、而面板里查不到):`LaneCarry` 新增 `owner`(只有原网格**首个信号**继承功能状态),`SplitLane` 只让第一份继承;新增 `dmdetail::PruneLaneSignalItems` 在每次重建/拆分后按 `lane.signals` 过滤四类逐信号条目。② **阶跃信号的竖直沿比水平段粗**:`EmitEnvelopeBand` 的横向膨胀半径由 `ceil` 改 `floor`(1px 线宽不再膨胀 1 列)。③ **拖动浮动面板时下方另一个面板出现拖拽残影**:面板位置从 `PanelGlobalKey` 移除(曾让拖动任一面板都重画全部面板,而 MARK 面板的位置漏混 ⇒ 残影),并新增 `vacatedRects`(上一帧被移动/缩小/关闭面板腾出的矩形 → 与之相交的面板强制重画)。④ **大量固定点时拖全局光标/平移卡顿**:新增 `FreeMarkerContents` 内容缓存(取值 + 五列文本 + 信号色,键**不含视界**)。⑤ **面板外观/交互统一**:折起指示器改成与信号列表组名同款三角(去掉横线);GX 徽标与标题栏悬停**不改光标**、徽标气泡移除;标题栏**双击**滚动到该网格(单击不触发,折起只走三角)。⑥ **自由标记**:「标记透明度」滑块与光标模式解耦(始终显示);悬停胶囊恢复不透明(探针瞄着被遮住的数据点时除外);胶囊与面板五列**不再省略**(列宽按实测内容撑开)。验证:新增离屏 `.preflight/fixcheck` **37/37**(三处 A/B:恢复 `ceil` ⇒ 线宽断言失败;恢复 `owner`/`PruneLaneSignalItems` ⇒ 归属断言失败;禁用 `vacatedRects` ⇒ 残影 111px);回归 `round3check` 31/31(更新一条被用户推翻的旧断言)、`round4check` 34/34、`round5check` 5/5、`editcheck` 20/20、`convcheck` 18/18;`make release`/`make debug` 零警告;`TargetCheck.exe` **852,480 字节(832.5 KiB)**。 | + ## 11. 跨设备协作须知(起源设备视角) > **本文件是唯一权威记忆**。所有设备 / Agent 共用它,**不要再新建 `agent_mem_origin.md` 等旁支记忆文件**。 @@ -3948,6 +3950,47 @@ else if (panelOnly) { /* 只回送“本帧重绘过的区域并集” */ } 202. ⚠️ **固定点取值胶囊的重叠避让只在绘制侧做**(`PaintFreeMarker` 内按已放置矩形纵向错开), 命中矩形(`m_freeMarkerHits`)由绘制时登记 ⇒ 自动跟随避让后的位置,**不要**在命中侧重算。 +203. ⚠️⚠️ **实验性功能是「网格级」开关,但只能由该网格的*首个信号*继承** + (`LaneCarry::owner` + `RebuildLanes` + `SplitLane`):`applyCarry` 对 + `!carry.matched || !carry.owner` 直接 **return**(保持 `Lane` 默认值 = 功能全关)。 + 否则「合并网格 → 拆分网格」会把一个网格的功能(以及它的逐信号条目)复制到**每一个** + 拆出来的网格上(用户 2026-10-21 报告的“第二个网格也自动启用了实验性功能”)。 +204. ⚠️⚠️ **任何“按信号保存的条目”在网格重建/拆分后必须按 `lane.signals` 过滤** + (`dmdetail::PruneLaneSignalItems`:`meanFilterItems` / `edgeCenterItems` / `weakPeakItems` / + `freeMarkerItems` / `freeMarkerCollapsedGroups`)。不过滤的症状:拆分后的网格里**画着别的信号的 + 固定点**(“野生的镜像标记点”),而浮动面板里又查不到它们(面板只列本网格的信号)。 +205. ⚠️⚠️ **包络带的横向(线宽)膨胀半径必须 `floor`、不能 `ceil`** + (`EmitEnvelopeBand` 的 `radius = floor(halfWidth / step)`):`ceil` 会让 1px 线宽 + (`halfWidth ≈ 0.5–0.7` 设备像素)也膨胀 1 列 ⇒ 阶跃/方波的**竖直沿 3 列宽(≈3px)** + 而水平段只有 ~1.25px,用户看到“竖直跳变的线宽明显大于水平持续段”; + 放大到走逐点描边路径后又正常(那条路径线宽天然一致)。修复后实测 1px 线宽:水平 1px / 竖直 1px。 +206. ⚠️⚠️ **面板像素缓存:面板位置绝不要混进 `PanelGlobalKey`**。它只影响该面板自己 + (`PanelSlotKey` 已混 `layout.panel/title/...`)。曾把 4 类面板的 x/y 混进全局键 ⇒ + 拖动任一面板都会让**所有**面板重画(15 个 ≈33ms/帧),而「自由标记」面板的位置偏偏漏混 + ⇒ 拖它时其它面板贴旧像素,正好在下方留**拖拽残影**。 +207. ⚠️⚠️ **面板“腾出的区域”必须算脏区**(`PaintEdgeCursor` 的 `vacatedRects`): + 上一帧缓存过的面板若本帧移动 / 缩小 / 关闭,它腾出的矩形仍留在**其它面板**的缓存像素里, + 只靠“新矩形与谁相交”的级联是漏的(新矩形可能已不与任何人相交)。做法:绘制前预扫描 + `m_panelSlots`,与本次 `EdgePanelLayoutFor` 的缓存矩形比对,不相等的旧矩形一律进 + `vacatedRects`,与它相交的面板全部强制重画。 +208. ⚠️ **`DoubleClickIsPlainPress` 对浮动面板默认返回 true(当作普通按下);例外是标题栏** + (排除 ✕ / GX 徽标 / 折起三角):标题栏双击有“把所属网格滚动到视界内”的语义(与单击 GX 徽标一致), + 单击标题栏**不**触发联动(只是准备拖面板),折起/展开只走左侧三角。 +209. ⚠️ **面板标题栏悬停保持箭头光标**(只有 `EDGE_PANEL_MOVE` 拖拽中才是 `IDC_SIZEALL`); + GX 徽标悬停同样**不改光标、不弹提示气泡**(高亮保留)。 +210. ⚠️⚠️ **「自由标记」的内容(取值 / 五列文本 / 信号色)必须缓存且键**不含视界**: + `PlotView::FreeMarkerContents`(`mutable m_freeMarkerContent`)只混入条目、信号样式、 + 数据地址与长度。大量固定点时逐帧 `SampleDisplayedSignalAt` + 拼字符串是“拖全局光标 / 平移变卡” + 的主因;平移视界、拖光标时缓存必须命中(键里加 ViewMin/ViewMax 就等于没缓存)。 +211. ⚠️ **「标记透明度」滑块与光标模式按钮解耦**:滑块行**始终**显示在按钮上方 + (`kFreeMarkerSliderRowHeight`,面板高度算式同步);鼠标悬停在某个取值胶囊上时该胶囊恢复 + 全不透明,但若探针正瞄着**被该胶囊遮住的数据点**(`m_freeMarker.probe` 落在胶囊内)则不恢复 + —— 否则无法看出自己瞄到了哪个采样点。 +212. ⚠️⚠️ **面板里的数据列不得出现省略号**:五列宽度取「缺省列宽」与 + `Lane::freeMarkerColumnWidths`(绘制该面板时把真实文本量一遍存回 `Lane`)的**较大者**, + 面板宽度随之撑开;`DT_END_ELLIPSIS` 对“刚好装满”的文本仍可能截断,因此每列额外留 2px 余量。 + 网格内取值胶囊同理(宽度额外留 12px 余量)。 + ### 11.3 离屏验证方法学(本机偏好,强烈建议沿用) - **不要占用用户的鼠标 / 键盘 / 屏幕**:优先 `.preflight/uitest`(离屏 harness:`SW_HIDE` 窗口 + 直接注入消息 + `BitBlt` 从 `m_backDc` 截图)或 `.preflight/perf`(离屏计时 / 逐像素对比)。 @@ -7429,3 +7472,98 @@ DG3 data=0 cycle=0 (空组,一个字符串通道) + +--- + +## 本轮(2026-10-21 第三轮,本机):拆分网格功能归属 + 阶跃线宽 + 面板残影 + 自由标记性能/交互 + +> 用户需求(逐条): +> ① **BUG**:两个信号独立网格 → 给其中一个开实验性功能 → 「合并网格」→「拆分网格」后**第二个网格也 +> 开启了实验性功能**;「自由标记」更严重:合并再拆分后第二个网格上出现**野生的固定点** +> (第一个网格标记点的镜像),但新网格的浮动面板里又没有这些点的信息。 +> ② **BUG**:`TestData/SCATTER_TEST.csv` 的 step 信号在默认 1px 线宽下,**垂直阶跃段的线宽明显 +> 大于水平持续段**;把跳变时刻放大后线宽又正常。 +> ③ **BUG**:拖动一个浮动面板经过另一个浮动面板时,下方/其它面板上留下**拖拽残影**。 +> ④ **BUG**:自由标记打上大量固定点后,左右拖动全局时间光标变卡(移动视界/滚动网格偶发卡顿)。 +> ⑤ **改进**:所有浮动面板的折起指示器与信号列表组名的折叠三角**同款**;GX 徽标悬停**不改光标**、 +> 不显示"是否在视界内"的浮动提示;标题栏**双击**把目标网格滚动到视界内(单击不触发); +> 标题栏悬停不改光标,只有按下左键拖动时才是十字箭头。 +> ⑥ **改进**:标记透明度独立于光标模式按钮(始终显示);悬停某个固定点的数值显示时该显示恢复全不透明 +> (但若鼠标正瞄准被它遮住的数据点则不恢复);固定点数值与面板数据列**不出现省略号**。 + +### A. 拆分网格:实验性功能只属于"首个信号"(§11.2 第 203–204 条) + +- 根因两处:`RebuildLanes` 把"开着"的面板状态**按信号逐个**继承(`inheritedCarry(ref)`),而合并后的 + 网格里每条信号都指向同一份状态 ⇒ 拆回每信号一格时**每一格**都带着功能;`SplitLane` 则直接把 + 源网格的 `*.open` 复制给每一份。 +- 修法:`LaneCarry` 新增 `owner`,只有 `lane.signals.front()` 为 true;`applyCarry` 对 + `!carry.matched || !carry.owner` **直接 return**(保持默认 = 功能全关)。`SplitLane` 只让 + `signalSlot == 0` 的那一份继承(并把 `freeMarker / freeMarkerCursor / freeMarkerPanel / + freeMarkerOpacity` 一并补上——旧实现漏了自由标记)。 +- "野生标记点"的真正来源:条目是**按网格整体**携带的,拆分后每份都拿到了全部 `freeMarkerItems`。 + 新增 `dmdetail::PruneLaneSignalItems`(按 `lane.signals` 过滤四类逐信号条目 + 收起分组), + 在 `RebuildLanes` 末尾与 `SplitLane` 的每一份上都调用。 + +### B. 阶跃信号的竖直沿比水平段粗(§11.2 第 205 条) + +- 仅发生在**包络带路径**(可见采样数 > 绘图区宽度)。根因:横向膨胀半径用 + `ceil(halfWidth / step)`,而 1px 线宽的 `strokeHalf = 1 × m_scale × 0.5 ≈ 0.625` 设备像素 + ⇒ `ceil` 得到 1 列 ⇒ 竖直沿从"1 个像素列"变成 3 列(≈3px),水平段却只有 ~1.25px。 + 放大到走逐点描边路径(线宽天然一致)后就"正常"了——与用户描述完全吻合。 +- 修法:`radius = floor(halfWidth / step)`(1px 不膨胀;≥2 列宽仍按整数列膨胀,10px 仍被撑粗)。 + 实测(125% 缩放、1px 线宽):**水平 1px / 竖直 1px**(改前 1 / 2)。 + +### C. 面板拖拽残影(§11.2 第 206–207 条) + +- 根因链:`PanelGlobalKey` 里混了 4 类面板的 `x/y`(拖动任一面板 ⇒ 所有面板重画,15 个 ≈33ms/帧), + 但**漏了 `freeMarkerPanel`**;于是拖「自由标记」面板时其它面板照旧贴旧像素,而它们上一帧的缓存里 + 含被拖面板的图像 ⇒ 残影。另外只靠"新矩形与谁相交"的级联重画是漏的:面板可能已移到**不与任何 + 面板相交**的位置,而别处的缓存像素里还留着它的旧图像。 +- 修法两条:① 面板位置**从 `PanelGlobalKey` 移除**(它只影响自己,`PanelSlotKey` 已含 + `layout.panel`),只保留 `open / collapsed` 与 `edgeValue`;② 绘制前预扫描 `m_panelSlots`, + 与本帧布局的缓存矩形比较,不相等(或被移除)的旧矩形进 `vacatedRects`,任何与之相交的面板强制重画。 +- 附带收益:拖动单个面板不再重画全部面板(原来 15 个 ≈33ms/帧)。 + +### D. 大量固定点时的卡顿(§11.2 第 210 条) + +- 每个固定点每帧都要 `ResolveSignal` ×2 + `SampleDisplayedSignalAt` ×2 + 拼 5 段文本 + 量宽度, + 而拖光标/平移时这些值**根本没变**。新增 `PlotView::FreeMarkerContents(laneIndex)`(`mutable` 缓存), + 条目 `FreeMarkerContentEntry{capsuleText, valueText, enumText, deltaText, timeText, + deltaTimeText, color, value, valid}`;键只混"条目 + 样式 + 数据地址/长度",**不含视界与鼠标**。 + 网格内胶囊与面板五列共用它(`FreeMarkerLabelRects` 也改读缓存)。 + +### E. 面板外观 / 交互(§11.2 第 208–209 条) + +- 折起指示器:改成与 `SignalPanel` 组名**完全相同**的 `UiDrawTriangleIcon`(12 逻辑像素、悬停变 accent), + 去掉三角后方那条短横线。 +- `QueryCursor`:GX 徽标与标题栏悬停都返回 `nullptr`(箭头);`EDGE_PANEL_MOVE` 拖拽中仍是 `IDC_SIZEALL`。 +- 删除 `PaintPanelBadgeTooltip`(含声明)——徽标只保留高亮,不再弹"已在视界内 / 点击滚动到该网格"。 +- `OnDoubleClick`:标题栏(排除 ✕ / 徽标 / 三角)改成 `ScrollLaneIntoView` + 提示; + `DoubleClickIsPlainPress` 对面板默认 `true`,**只有标题栏**返回 false(否则双击永远到不了 `OnDoubleClick`)。 + +### F. 自由标记:透明度 / 悬停恢复 / 不省略(§11.2 第 211–212 条) + +- 「标记透明度」滑块行**始终**显示(面板高度算式同步去掉 `freeMarkerCursor` 条件)。 +- 悬停取值胶囊 ⇒ 该胶囊 `alpha = 1`;例外:`m_freeMarker.valid` 且其 `(x, y)` 落在胶囊矩形内 + (正瞄着被遮住的数据点)时保持原透明度。 +- 不省略:`Lane::freeMarkerColumnWidths[5]`(绘制该面板时按真实文本量宽度存回 `Lane`),列宽取 + 「缺省列宽」与它的大者、面板宽度随之撑开、每列多留 2px;胶囊宽度余量 10 → 12px。 + +### G. 验证(全离屏,未占用鼠标/键盘/屏幕) + +- 新增 `.preflight/fixcheck`(`build.ps1` + `main.cpp`)**37/37**: + [A] 合并→拆分的功能归属 + "没有别人的固定点";[B] 阶跃线宽(水平 vs 竖直,跳过图例色块); + [D/E] 内容缓存命中(平移视界键不变 / 改样式键失效)、滑块始终存在且在按钮上方、 + 列宽 ≥ 实测文本、悬停恢复不透明、探针遮挡时保持半透明; + [C] 面板残影(拖动中的一帧 vs 作废面板图层后的重画,逐像素); + [F] 徽标/标题光标、单击不滚动、双击滚动、双击不折起。 +- **A/B(判别力已证)**:`ceil` 还原 ⇒ 竖直 2px vs 水平 1px ⇒ 1 项 FAIL;`carry.owner` 还原 ⇒ + 功能归属断言 FAIL;禁用 `vacatedRects` ⇒ 残影 111 像素 ⇒ FAIL。恢复后 37/37。 +- 回归:`round3check` **31/31**(其中「徽标显示手型光标」是用户本轮明确推翻的旧规则,已改为箭头)、 + `round4check` 34/34、`round5check` 5/5(1px→2px、10px→26px 仍成立)、`editcheck` 20/20、 + `convcheck` 18/18。 +- `make release` / `make debug` **零警告**;`TargetCheck.exe` **852,480 字节(832.5 KiB)**。 +- ⚠️ 探针坑:`Grab` 用 `GetDIBits(app.m_backDc, app.m_backBitmap, …)`;残影只存在于"另一块面板的 + **缓存矩形**"里,比较区域要取两个缓存矩形的交集(面板主体不透明会盖住大半残影);直接改 + `lane.*Panel.x/y` 就等价于"拖动中的某一帧"(走鼠标注入会先 `BringPanelToFront` 并改全局键, + 那一帧所有面板都重画,反而看不到残影)。 diff --git a/Utilities/Inc/DataModel.hpp b/Utilities/Inc/DataModel.hpp index 3017e0e..451010c 100644 --- a/Utilities/Inc/DataModel.hpp +++ b/Utilities/Inc/DataModel.hpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -1013,6 +1014,17 @@ namespace jdi */ std::vector freeMarkerCollapsedGroups; + /** + * @brief 「自由标记」面板五列的**实测内容宽度**(设备像素,0 = 还没量过)。 + * + * 用户 2026-10-21 明确要求:面板里的取值 / 枚举文本 / 增量 / 时刻 / 间隔 + * **不得出现省略号(…)**。因此列宽不能再用固定值:绘制该面板时顺便把每格 + * 真实文本量一遍,取最大值存到这里;布局(`EdgePanelWidth` / `EdgePanelLayoutFor`) + * 用它与缺省列宽 `kFreeMarkerColumnWidths` 的较大者,于是列宽自然贴合内容。 + * ⚠️ 存的是**设备像素**(已经乘过 DPI 缩放),与字体直接对应。 + */ + std::array freeMarkerColumnWidths{}; + /** 某信号分组在「自由标记」面板里是否被收起。 */ bool FreeMarkerGroupCollapsed(const SignalRef& ref) const { diff --git a/Utilities/Src/DataModelInternal.hpp b/Utilities/Src/DataModelInternal.hpp index 5b2945d..ddf522d 100644 --- a/Utilities/Src/DataModelInternal.hpp +++ b/Utilities/Src/DataModelInternal.hpp @@ -61,6 +61,38 @@ namespace jdi PadValueRange(yMin, yMax); } + /** + * @brief 清掉网格里**不属于本网格信号**的逐信号条目(均值滤波 / 中心轴 / 弱峰调整 / 自由标记)。 + * + * 这些条目按信号保存,但只有“该信号就在本网格里”时才有意义。 + * 网格重建(合并 → 拆分)时上一帧的条目是按网格整体携带的,若不过滤就会出现: + * 拆分出来的新网格里仍然画着**其它信号**的固定点(用户 2026-10-21 报告: + * 第二个网格上出现“野生的”自由标记点,而浮动面板里又没有任何条目)。 + * + * @param [IN,OUT] lane 目标网格(`signals` 必须已经就位)。 + */ + inline void PruneLaneSignalItems(Lane& lane) + { + const auto inLane = [&lane](const SignalRef& ref) + { + return std::find(lane.signals.begin(), lane.signals.end(), ref) != lane.signals.end(); + }; + const auto prune = [&inLane](auto& items) + { + items.erase(std::remove_if(items.begin(), items.end(), + [&inLane](const auto& item) { return !inLane(item.ref); }), + items.end()); + }; + prune(lane.meanFilterItems); + prune(lane.edgeCenterItems); + prune(lane.weakPeakItems); + prune(lane.freeMarkerItems); + lane.freeMarkerCollapsedGroups.erase( + std::remove_if(lane.freeMarkerCollapsedGroups.begin(), lane.freeMarkerCollapsedGroups.end(), + [&inLane](const SignalRef& ref) { return !inLane(ref); }), + lane.freeMarkerCollapsedGroups.end()); + } + /** * @brief 依据全部可见信号重新生成网格划分。 * @param [IN] visible 各信号的可见性(按 [分组][信号] 索引)。 @@ -74,6 +106,16 @@ namespace jdi struct LaneCarry { bool matched = false; /**< 是否找到该信号原本所在的网格(false = 新出现的信号)。 */ + /** + * @brief 该信号是否是原网格的**首个信号**(= 这个网格的“实验性功能”持有者)。 + * + * ⚠️ 实验性功能(边沿计数 / 中心轴 / 均值滤波 / 弱峰调整 / 自由标记)是**网格级**开关, + * 但一个网格可能与其它信号共用(合并网格)。若把“开着”的状态按信号逐个继承, + * 那么「合并网格 → 拆分网格」之后**每个**拆出来的网格都会带着这些功能 + * (用户 2026-10-21 报告:第二个网格也自动启用了实验性功能、并出现了 + * 其它信号的固定点镜像)。因此只有**首个信号**继承功能状态,其余信号视为默认(关闭)。 + */ + bool owner = false; bool fitYToSelection = false; bool edgeCount = false; double edgeValue = 0.0; @@ -139,9 +181,12 @@ namespace jdi carry.freeMarkerOpacity = lane.freeMarkerOpacity; carry.freeMarkerScroll = lane.freeMarkerScroll; carry.freeMarkerCollapsedGroups = lane.freeMarkerCollapsedGroups; - for (const SignalRef& ref : lane.signals) + for (std::size_t signalIndex = 0; signalIndex < lane.signals.size(); ++signalIndex) { - previousCarry.emplace_back(ref, carry); + LaneCarry entry = carry; + // 只有首个信号是这些网格级开关的持有者(见 `LaneCarry::owner` 说明)。 + entry.owner = (signalIndex == 0); + previousCarry.emplace_back(lane.signals[signalIndex], entry); } } const auto inheritedCarry = [&previousCarry](const SignalRef& ref) @@ -160,6 +205,12 @@ namespace jdi const LaneCarry carry = inheritedCarry(ref); // 新出现的信号(之前没有网格):继承「全局 Y 轴自适应」是否开启。 lane.fitYToSelection = carry.matched ? carry.fitYToSelection : previousAllAutoFit; + // ⚠️ 实验性功能的状态**只由原网格的首个信号继承**(见 `LaneCarry::owner`): + // 否则「合并网格 → 拆分网格」会把一个网格的功能复制到全部拆分结果上。 + if (!carry.matched || !carry.owner) + { + return; // 其它信号:保持 `Lane` 的默认值(全部功能关闭、条目为空)。 + } lane.edgeCount = carry.edgeCount; lane.edgeValue = carry.edgeValue; lane.edgeValueValid = carry.edgeValueValid; @@ -348,9 +399,11 @@ namespace jdi } } + // 清掉“不属于本网格信号”的逐信号条目(合并网格时它们会被整块携带过来)。 // 统一重命名,保证编号连续。 for (std::size_t index = 0; index < lanes.size(); ++index) { + PruneLaneSignalItems(lanes[index]); lanes[index].name = L"网格 " + std::to_wstring(index + 1); } } diff --git a/Utilities/Src/DataModelTab.cpp b/Utilities/Src/DataModelTab.cpp index 0c374e1..2a5386b 100644 --- a/Utilities/Src/DataModelTab.cpp +++ b/Utilities/Src/DataModelTab.cpp @@ -419,61 +419,73 @@ namespace jdi std::vector pieces; pieces.reserve(signals.size()); - for (const SignalRef& ref : signals) + for (std::size_t signalSlot = 0; signalSlot < signals.size(); ++signalSlot) { + const SignalRef& ref = signals[signalSlot]; + // ⚠️ 只有**首个信号**所在的网格继承原网格的实验性功能(用户 2026-10-21 要求: + // 拆分后只有“拆分前的网格内的第一个信号”保留已应用的功能)。 + // 否则拆分会把一个网格的功能(以及它的逐信号条目)复制到每一份上。 + const bool primary = (signalSlot == 0); Lane lane; lane.autoRange = source.autoRange; lane.yMin = source.yMin; lane.yMax = source.yMax; lane.fitYToSelection = source.fitYToSelection; - lane.edgeCount = source.edgeCount; - lane.edgeValue = source.edgeValue; - lane.edgeValueValid = source.edgeValueValid; - lane.edgePanel.open = source.edgePanel.open; - lane.edgePanel.collapsed = source.edgePanel.collapsed; - lane.edgePanel.x = source.edgePanel.x; - lane.edgePanel.y = source.edgePanel.y; - lane.edgeCenter = source.edgeCenter; - lane.edgeCenterPanel.open = source.edgeCenterPanel.open; - lane.edgeCenterPanel.collapsed = source.edgeCenterPanel.collapsed; - lane.edgeCenterPanel.x = source.edgeCenterPanel.x; - lane.edgeCenterPanel.y = source.edgeCenterPanel.y; - lane.weakPeak = source.weakPeak; - lane.weakPeakPanel.open = source.weakPeakPanel.open; - lane.weakPeakPanel.collapsed = source.weakPeakPanel.collapsed; - lane.weakPeakPanel.x = source.weakPeakPanel.x; - lane.weakPeakPanel.y = source.weakPeakPanel.y; - lane.meanFilter = source.meanFilter; - lane.meanFilterPanel.open = source.meanFilterPanel.open; - lane.meanFilterPanel.collapsed = source.meanFilterPanel.collapsed; - lane.meanFilterPanel.x = source.meanFilterPanel.x; - lane.meanFilterPanel.y = source.meanFilterPanel.y; - // 拆分后每个子网格只保留自己那个信号的均值滤波 / 中心轴 / 弱峰调整条目。 - lane.meanFilterItems.clear(); - lane.edgeCenterItems.clear(); - lane.weakPeakItems.clear(); - for (const Lane::MeanFilterItem& item : source.meanFilterItems) + if (primary) { - if (item.ref == ref) + lane.edgeCount = source.edgeCount; + lane.edgeValue = source.edgeValue; + lane.edgeValueValid = source.edgeValueValid; + lane.edgePanel = source.edgePanel; + lane.edgeCenter = source.edgeCenter; + lane.edgeCenterPanel = source.edgeCenterPanel; + lane.weakPeak = source.weakPeak; + lane.weakPeakPanel = source.weakPeakPanel; + lane.meanFilter = source.meanFilter; + lane.meanFilterPanel = source.meanFilterPanel; + lane.freeMarker = source.freeMarker; + lane.freeMarkerCursor = source.freeMarkerCursor; + lane.freeMarkerPanel = source.freeMarkerPanel; + lane.freeMarkerOpacity = source.freeMarkerOpacity; + // 拆分后每个子网格只保留自己那个信号的均值滤波 / 中心轴 / 弱峰调整 / 自由标记条目。 + for (const Lane::MeanFilterItem& item : source.meanFilterItems) { - lane.meanFilterItems.push_back(item); + if (item.ref == ref) + { + lane.meanFilterItems.push_back(item); + } } - } - for (const Lane::EdgeCenterItem& item : source.edgeCenterItems) - { - if (item.ref == ref) + for (const Lane::EdgeCenterItem& item : source.edgeCenterItems) { - lane.edgeCenterItems.push_back(item); + if (item.ref == ref) + { + lane.edgeCenterItems.push_back(item); + } } - } - for (const Lane::WeakPeakItem& item : source.weakPeakItems) - { - if (item.ref == ref) + for (const Lane::WeakPeakItem& item : source.weakPeakItems) { - lane.weakPeakItems.push_back(item); + if (item.ref == ref) + { + lane.weakPeakItems.push_back(item); + } + } + for (const Lane::FreeMarkerItem& item : source.freeMarkerItems) + { + if (item.ref == ref) + { + lane.freeMarkerItems.push_back(item); + } + } + for (const SignalRef& collapsed : source.freeMarkerCollapsedGroups) + { + if (collapsed == ref) + { + lane.freeMarkerCollapsedGroups.push_back(collapsed); + } } } lane.signals.push_back(ref); + PruneLaneSignalItems(lane); pieces.push_back(std::move(lane)); } // 用 pieces 替换原网格(插入到原位置,因此其后方的网格整体顺次后移)。