Files
DeepSeek-V4.1-Flash bdd703b88b 主题自适应配色 + 预览曲线连续配色 + 样式区复位按钮 + 均值滤波面板重做 + 面板缓存环带残留 + 调试层序 + 自由标记选取与性能
① 主题自适应(`Theme.cpp`):新增 `SrgbLuminance` / `ContrastRatio` / `EnsureContrast` /
   `MixToLuminance` / `ThemeLaneBackground`;四套主题的结构色(gridWeak 1.30 / gridStrong 1.65 /
   axisLine 2.10 / borderStrong 1.70)按底色自动校正,Solarised Light 逐位不变;网格叠底改用
   `ThemeLaneBackground()`(4 处),深色主题下网格层与绘图区底色可辨。
② 派生预览曲线(`HighContrastDerived`)重写为**连续**方案:方向沿信号所在侧远离底色,
   目标亮度 = max(与底色对比度 3.2, 信号亮度 ± 0.18),再按亮度平滑混色;调用点传入真实网格底色
   (旧实现默认写死 Solarised Light 底色 ⇒ 深色主题永远选到更暗的那一侧、几乎看不见)。
③ 信号样式区「线宽 / 透明度」两行右侧新增**复位按钮**(`UiPaintResetIcon` 成为唯一实现,
   菜单/弱峰面板/样式区共用同一套笔画);默认线宽统一到 `DefaultSignalLineWidth(finiteCount)`
   (单点信号取 10.0)。按钮右边界与内容列对齐,内容列过窄时整列不显示。
④ 「均值滤波」面板重做:信号色叠底标题栏 + 复选框 + 展开三角 + 「均值点数 / 迭代次数」两行滑块
   (点击数值可改滑块上限、右侧复位)+ 内容区滚动;新增 `MeanFilterItem::{maxPoints,maxIterations,
   collapsed}` 与 `Lane::meanFilterScroll`,缓存键同步补全。
⑤ 浮动面板命中像素缓存时只贴**面板圆角形状之内**的像素(`CreateRoundRectRgn` 裁剪)并按本帧重画投影
   ⇒ 中键移视界后面板周围不再出现一圈旧波形。
⑥ 显示布局边界:每个浮动面板**独占一个绘制层**(按 `m_panelOrder` 递增),绘制侧
   `UiSetTextLayer` 在同一循环内同步推进 ⇒ 下层面板的边框/文字不再画到上层面板上。
⑦ 自由标记:采样候选距离改为**二维欧氏距离**(单点信号不再在整幅宽度内吸住光标),
   纵向光标画在鼠标位置、圆点落在真实采样点(新增 `FreeMarkerProbe::pointX`);
   `MinorCellSparse` 只对胜出信号算一次;折线缓存容量改为 `max(64, 信号数×2)` 且查找改为
   单键哈希 ⇒ MF4 的 285 信号合并网格中 `ProbeFreeMarker` 平均 0.11ms。

验证:`.preflight` 探针重建(`panelcheck` 71/71、`markercheck` 13/13、`ringcheck` 6/6,合计 90 条,
LTO 目标文件上复跑同样全绿);`make release` / `make debug` 零警告;产物 815,104 B。
文档:`agent_mem.md` 第十六轮(含补丁脚本与编码/行尾踩坑)。
2026-10-30 03:10:51 +08:00

505 lines
21 KiB
C++
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// PlotView 实现分片 —— 内容指纹与缓存键("为什么这一帧可以复用上一帧的像素")。
//
// 本分片只包含 `PlotView` 的**成员函数定义**(从 `PlotViewPaint.cpp` 机械搬移,行为与搬移前逐字节一致)。
// 职责:
// * 场景缓存键 `SceneCacheKey` 的构造与比较;
// * 场景内容指纹(`SceneContentHash` / `SceneGeometryHash` / `SceneDecorHash`);
// * 逐信号样式快照与"只改了外观"的网格定位(`CollectSceneStyles` / `FindStyleOnlyLanes`);
// * 派生曲线参数快照(`CollectSceneDerived`);
// * 样式补丁帧(贴图 → 只重画受影响网格 → 写回缓存)。
//
// ⚠️ 硬约束:任何"会影响像素"的状态都必须进指纹;漏一个字段的症状是"贴旧像素"
// (本项目历史上至少 4 次真实 BUG 都是这个原因)。
// ⚠️ 本文件**不做**像素分配与贴图(那些在 `PlotViewRaster.cpp`),也不做绘制(`PlotViewPaint.cpp`)。
#include "PlotViewInternal.hpp"
namespace jdi
{
PlotView::SceneCacheKey PlotView::MakeSceneCacheKey() const
{
SceneCacheKey key;
key.viewMin = Tab().ViewMin();
key.viewMax = Tab().ViewMax();
key.laneScroll = Tab().LaneScroll();
key.laneCount = static_cast<int>(m_lanes.size());
key.areaX = m_area.X;
key.areaY = m_area.Y;
key.areaW = m_area.Width;
key.areaH = m_area.Height;
key.scale = m_scale;
const SignalRef halo = SceneHaloRef();
key.haloGroup = halo.groupIndex;
key.haloSignal = halo.signalIndex;
key.contentHash = SceneContentHash();
return key;
}
bool PlotView::SceneCacheMatches() const
{
if (!m_sceneValid)
{
return false;
}
// 缓存里存的是「快速帧」(关掉了抗锯齿)→ 不能直接命中:
// 必须重画一次全质量版本,否则降级的画面会一直留在屏幕上。
if (m_sceneDegraded)
{
return false;
}
const SceneCacheKey& cached = m_sceneKey;
const SceneCacheKey key = MakeSceneCacheKey();
return (key.viewMin == cached.viewMin) && (key.viewMax == cached.viewMax) &&
(key.laneScroll == cached.laneScroll) && (key.laneCount == cached.laneCount) &&
(key.areaX == cached.areaX) && (key.areaY == cached.areaY) &&
(key.areaW == cached.areaW) && (key.areaH == cached.areaH) &&
(key.scale == cached.scale) && (key.haloGroup == cached.haloGroup) &&
(key.haloSignal == cached.haloSignal) && (key.contentHash == cached.contentHash);
}
SignalRef PlotView::SceneHaloRef() const
{
// 只有拖拽中的信号光晕是“整个拖拽过程都不变”的,可以安全地绘入静态场景缓存;
// 悬停光晕随鼠标移动,必须画在缓存之外(见 PaintHoverHighlight)。
if (m_dragMode == PlotDragMode::MOVE_SIGNAL && m_dragSignal.IsValid())
{
return m_dragSignal;
}
return SignalRef();
}
std::uint64_t PlotView::SceneContentHash(bool includeStyles, bool includeDerived,
bool includeLaneRange, bool includeLaneScroll) const
{
// FNV-1a:把“视图几何之外的一切影响绘图区像素的内容”折叠成一个指纹。
// 这样颜色/线宽/透明度/样式/重命名/信号增删等变化都会自动让缓存失效,
// 不需要在各处手工调用 InvalidateSceneCache(容易漏)。
// `includeStyles == false` 时跳过逐信号的外观样式(颜色/不透明度/线宽/模式),
// 得到的就是几何指纹:它一致说明“只有外观样式变了”。
std::uint64_t hash = 1469598103934665603ull;
const auto mix = [&hash](std::uint64_t value)
{
hash ^= value;
hash *= 1099511628211ull;
};
const auto mixFloat = [&mix](float value)
{
std::uint32_t bits = 0;
std::memcpy(&bits, &value, sizeof(bits));
mix(bits);
};
const auto mixText = [&mix](const std::wstring& text)
{
mix(text.size());
for (const wchar_t character : text)
{
mix(static_cast<std::uint64_t>(character));
}
};
mixFloat(m_plotRect.X);
mixFloat(m_plotRect.Y);
mixFloat(m_plotRect.Width);
mixFloat(m_plotRect.Height);
mixFloat(m_laneContentHeight);
mix(m_scrollBarVisible ? 1ull : 0ull);
// ⚠️ 网格纵向滚动量必须参与**静态场景缓存**的指纹:它只改变各网格的绘制位置。
// 若不参与,平移宽缓冲区 / 静态场景缓存会把旧滚动位置的像素当成当前内容贴回,
// 表现为“网格位置错位 / 拖影 / 残留”等错误元素。
// ⚠️ 但它是**纯屏幕落点**(不影响任何网格内容),因此“宽基底可用性”判据
//(`SceneGeometryHash`)把它排除:网格滚动时 `BlitWideBuffer` 本来就按 `lane.rect`
// 逐网格贴图,内容完全复用 —— 这样“网格滚动到可视区 / 回到最上方”这类
// **每帧都在改滚动量的动画**才不会退化成每帧整幅重绘(用户 2026-10-17 报告卡顿)。
if (includeLaneScroll)
{
mixFloat(Tab().LaneScroll());
}
// 选中信号会改变图例条目的底色与文字颜色(绘制在缓存的静态场景里),
// 因此必须参与指纹:否则在左侧信号面板点选后图例不会高亮联动。
{
const SignalRef selected = SelectedRef();
mix(selected.IsValid() ? 1ull : 0ull);
mix(static_cast<std::uint64_t>(selected.groupIndex));
mix(static_cast<std::uint64_t>(selected.signalIndex));
}
for (std::size_t laneIndex = 0; laneIndex < m_lanes.size(); ++laneIndex)
{
const LaneVisual& visual = m_lanes[laneIndex];
// ⚠️ 各网格的屏幕纵向位置同样只是“贴图落点”(见 `includeLaneScroll` 的说明)。
if (includeLaneScroll)
{
mixFloat(visual.rect.Y);
}
mixFloat(visual.rect.Height);
// ⚠️ 幅值范围只在 `includeLaneRange` 时计入:拖拽平移时的纵向分量会每帧小幅改动它,
// 若参与“宽基底可用性”判据,每一帧都会退化成整幅重绘(质量下降 + 卡顿)。
if (includeLaneRange)
{
mixFloat(static_cast<float>(visual.yMin));
mixFloat(static_cast<float>(visual.yMax));
}
const std::vector<SignalRef>& signals = Tab().Lanes()[laneIndex].signals;
// 「边沿计数(中心轴)」「均值滤波」「弱峰调整」的开关与每条被勾选信号的参数
// 会改变网格内的曲线绘制。它们**只影响网格内容**(不影响网格结构/几何),
// 因此只在 includeStyles 时计入 —— 于是拖参数滑块可以走“局部重画”快路径,
// 而不必整幅重绘(`FindStyleOnlyLanes` 用 `m_sceneDerived` 定位受影响的网格)。
// `includeDerived == false` 时连这些参数也不计入(用于装饰层指纹:图例/Y 轴刻度
// 与派生曲线参数无关,拖滑块时装饰层无需失效)。
if (includeStyles && includeDerived)
{
mix(Tab().Lanes()[laneIndex].edgeCenter ? 1ull : 0ull);
mix(static_cast<std::uint64_t>(Tab().Lanes()[laneIndex].edgeCenterItems.size()));
for (const Lane::EdgeCenterItem& item : Tab().Lanes()[laneIndex].edgeCenterItems)
{
mix(static_cast<std::uint64_t>(item.ref.groupIndex));
mix(static_cast<std::uint64_t>(item.ref.signalIndex));
mix(static_cast<std::uint64_t>(item.points));
mix(static_cast<std::uint64_t>(item.iterations));
}
mix(Tab().Lanes()[laneIndex].meanFilter ? 1ull : 0ull);
mix(static_cast<std::uint64_t>(Tab().Lanes()[laneIndex].meanFilterItems.size()));
for (const Lane::MeanFilterItem& item : Tab().Lanes()[laneIndex].meanFilterItems)
{
mix(static_cast<std::uint64_t>(item.ref.groupIndex));
mix(static_cast<std::uint64_t>(item.ref.signalIndex));
mix(static_cast<std::uint64_t>(item.points));
mix(static_cast<std::uint64_t>(item.iterations));
}
mix(Tab().Lanes()[laneIndex].weakPeak ? 1ull : 0ull);
mix(static_cast<std::uint64_t>(Tab().Lanes()[laneIndex].weakPeakItems.size()));
for (const Lane::WeakPeakItem& item : Tab().Lanes()[laneIndex].weakPeakItems)
{
// 逐信号参数(等级 + 迭代次数)都参与指纹:改任一条的参数都会重画。
mix(static_cast<std::uint64_t>(item.ref.groupIndex));
mix(static_cast<std::uint64_t>(item.ref.signalIndex));
mix(static_cast<std::uint64_t>(item.amountPercent));
mix(static_cast<std::uint64_t>(item.iterations));
}
}
mix(signals.size());
for (const SignalRef& ref : signals)
{
const ResolvedSignal resolved = ResolveSignal(ref);
mix(static_cast<std::uint64_t>(ref.groupIndex));
mix(static_cast<std::uint64_t>(ref.signalIndex));
mix(resolved.visible ? 1ull : 0ull);
if (resolved.data == nullptr || resolved.style == nullptr)
{
continue;
}
// 以下四项是“只影响外观”的样式:仅在 includeStyles 时计入指纹。
// (不参与时得到的就是 SceneGeometryHash:它一致说明只有外观变了,
// 于是可以只重画颜色变了的网格,而不必整幅重绘。)
if (includeStyles)
{
mix(static_cast<std::uint64_t>(resolved.style->color));
mixFloat(resolved.style->opacity);
mixFloat(resolved.style->lineWidth);
mix(static_cast<std::uint64_t>(resolved.style->mode));
// 绘制前的数值变换(系数 / 绝对值 / 镜像)会改变波形形状。
mixFloat(resolved.style->gain);
mix(resolved.style->absolute ? 1ull : 0ull);
mix(resolved.style->mirrorVertical ? 1ull : 0ull);
mix(resolved.style->mirrorHorizontal ? 1ull : 0ull);
// 偏移(Y 平移 + 时间平移)同样会改变波形。
mixFloat(resolved.style->offsetY);
mixFloat(static_cast<float>(resolved.style->offsetTime));
}
// 图例文案参与绘制,重命名必须让缓存失效。
mixText(resolved.data->name);
if (resolved.group != nullptr)
{
mixText(resolved.group->name);
}
}
}
return hash;
}
std::uint64_t PlotView::SceneGeometryHash() const
{
// 外观样式不参与;各网格的幅值范围也不参与(那由逐网格图层指纹负责,
// 这样拖拽时的纵向分量不会把整幅重绘的快路径打掉);
// ⚠️ 网格纵向滚动量 / 各网格屏幕纵向位置也不参与:那只是图层的**贴图落点**,
// 不影响任何网格的像素内容(`BlitWideBuffer` 按 `lane.rect` 逐网格贴图),
// 因此滚动网格时可以继续复用宽基底(否则“滚动到可视区 / 回到最上方”的
// 每一帧都会退化成整幅重绘 —— 用户 2026-10-17 报告的滚动动画卡顿)。
return SceneContentHash(false, true, false, false);
}
std::uint64_t PlotView::SceneDecorHash() const
{
// 装饰层(图例 + Y 轴刻度)的指纹:含外观样式,但**不含派生曲线参数**
//(拖弱峰调整等级/迭代次数滑块时图例与刻度不变,装饰层缓存不应失效)。
return SceneContentHash(true, false);
}
void PlotView::CollectSceneStyles(std::vector<std::vector<SceneStyleEntry>>& out) const
{
out.clear();
out.resize(m_lanes.size());
for (std::size_t laneIndex = 0; laneIndex < m_lanes.size(); ++laneIndex)
{
const std::vector<SignalRef>& signals = Tab().Lanes()[laneIndex].signals;
std::vector<SceneStyleEntry>& lane = out[laneIndex];
lane.reserve(signals.size());
for (const SignalRef& ref : signals)
{
const ResolvedSignal resolved = ResolveSignal(ref);
SceneStyleEntry entry;
if (resolved.style != nullptr)
{
entry.color = resolved.style->color;
entry.opacity = resolved.style->opacity;
entry.lineWidth = resolved.style->lineWidth;
entry.mode = static_cast<int>(resolved.style->mode);
entry.gain = resolved.style->gain;
entry.absolute = resolved.style->absolute;
entry.mirrorVertical = resolved.style->mirrorVertical;
entry.mirrorHorizontal = resolved.style->mirrorHorizontal;
entry.offsetY = resolved.style->offsetY;
entry.offsetTime = resolved.style->offsetTime;
}
lane.push_back(entry);
}
}
}
bool PlotView::FindStyleOnlyLanes(std::vector<int>& lanes) const
{
lanes.clear();
if (!m_sceneValid || m_sceneGeometryHash != SceneGeometryHash())
{
// 网格结构 / 视图几何 / 信号集合 / 图例文案有变化:必须整幅重绘。
return false;
}
// ⚠️ **必须额外确认“视图几何与缓存完全一致”**:`SceneGeometryHash()` 刻意不含视界,
// 所以它相等**并不能**说明视图没变。若视界已经变了还走“只重画受影响网格”的局部补丁,
// 就会把缓存里**旧视界**的画面(时间轴、其它网格、底色)留在屏上 —— 表现为用户反馈的
// “拖影 / 残留 / 错误元素”。因此这里逐项比较除内容指纹之外的缓存键。
{
const SceneCacheKey key = MakeSceneCacheKey();
if (key.viewMin != m_sceneKey.viewMin || key.viewMax != m_sceneKey.viewMax ||
key.laneScroll != m_sceneKey.laneScroll || key.laneCount != m_sceneKey.laneCount ||
key.areaX != m_sceneKey.areaX || key.areaY != m_sceneKey.areaY ||
key.areaW != m_sceneKey.areaW || key.areaH != m_sceneKey.areaH ||
key.scale != m_sceneKey.scale || key.haloGroup != m_sceneKey.haloGroup ||
key.haloSignal != m_sceneKey.haloSignal)
{
return false;
}
}
std::vector<std::vector<SceneStyleEntry>> current;
CollectSceneStyles(current);
if (current.size() != m_sceneStyles.size())
{
return false;
}
// 叠加预览曲线(均值滤波 / 中心轴 / 弱峰调整)的参数变化同样只需局部重画。
std::vector<SceneDerivedEntry> currentDerived;
CollectSceneDerived(currentDerived);
const bool derivedComparable = (currentDerived.size() == m_sceneDerived.size());
for (std::size_t laneIndex = 0; laneIndex < current.size(); ++laneIndex)
{
const std::vector<SceneStyleEntry>& now = current[laneIndex];
const std::vector<SceneStyleEntry>& before = m_sceneStyles[laneIndex];
if (now.size() != before.size())
{
return false;
}
bool differs = false;
for (std::size_t index = 0; index < now.size() && !differs; ++index)
{
// ⚠️ 必须与 `SceneContentHash(includeStyles) / SceneGeometryHash` 的口径**完全一致**:
// 几何哈希刻意不含外观,而外观哈希含 color/opacity/lineWidth/mode **以及**
// 系数 / 绝对值 / 镜像 / Y 偏移 / 时间偏移。旧实现只比前 4 项 ⇒ 一个网格改颜色、
// 另一个网格同时改「调整」时,后者不会被登记为脏;随后 `RepaintSceneCachePatch`
// 又写入包含这些字段的新 `m_sceneKey`,那个网格就带着旧像素被当成“已是最新”,
// 一直残留到某次无关的失效。
differs = !(now[index] == before[index]);
}
if (!differs && derivedComparable)
{
const SceneDerivedEntry& nowDerived = currentDerived[laneIndex];
const SceneDerivedEntry& beforeDerived = m_sceneDerived[laneIndex];
differs = (nowDerived.edgeCenter != beforeDerived.edgeCenter) ||
(nowDerived.meanFilter != beforeDerived.meanFilter) ||
(nowDerived.weakPeak != beforeDerived.weakPeak) ||
(nowDerived.edgeCenterItems != beforeDerived.edgeCenterItems) ||
(nowDerived.meanFilterItems != beforeDerived.meanFilterItems) ||
(nowDerived.weakPeakItems != beforeDerived.weakPeakItems);
}
if (differs)
{
lanes.push_back(static_cast<int>(laneIndex));
}
}
return !lanes.empty();
}
void PlotView::CollectSceneDerived(std::vector<SceneDerivedEntry>& out) const
{
out.clear();
out.resize(m_lanes.size());
for (std::size_t laneIndex = 0; laneIndex < m_lanes.size(); ++laneIndex)
{
const Lane& lane = Tab().Lanes()[laneIndex];
SceneDerivedEntry entry;
entry.edgeCenter = lane.edgeCenter ? 1 : 0;
entry.meanFilter = lane.meanFilter ? 1 : 0;
entry.weakPeak = lane.weakPeak ? 1 : 0;
const auto flatten = [](const auto& items, std::vector<int>& target, auto valueOf)
{
target.clear();
target.reserve(items.size() * 4);
for (const auto& item : items)
{
target.push_back(item.ref.groupIndex);
target.push_back(item.ref.signalIndex);
// 均值滤波/中心轴取 points,弱峰调整取 amountPercent(由 `valueOf` 决定)。
target.push_back(static_cast<int>(valueOf(item)));
target.push_back(item.iterations);
}
};
flatten(lane.edgeCenterItems, entry.edgeCenterItems,
[](const Lane::EdgeCenterItem& item) { return item.points; });
flatten(lane.meanFilterItems, entry.meanFilterItems,
[](const Lane::MeanFilterItem& item) { return item.points; });
flatten(lane.weakPeakItems, entry.weakPeakItems,
[](const Lane::WeakPeakItem& item) { return item.amountPercent; });
out[laneIndex] = std::move(entry);
}
}
void PlotView::RepaintSceneCachePatch(gd::Graphics& g, const std::vector<int>& lanes)
{
if (lanes.empty() || m_sceneDc == nullptr)
{
return;
}
// 重画范围 = 受影响网格与绘图区的交集(并且不超出缓存区域)。
float top = 1.0e9f;
float bottom = -1.0e9f;
for (const int laneIndex : lanes)
{
if (laneIndex < 0 || laneIndex >= static_cast<int>(m_lanes.size()))
{
continue;
}
const gd::RectF& rect = m_lanes[static_cast<std::size_t>(laneIndex)].rect;
top = std::min(top, rect.Y);
bottom = std::max(bottom, rect.Y + rect.Height);
}
if (bottom <= top)
{
return;
}
top = std::max(top, m_plotRect.Y);
bottom = std::min(bottom, m_plotRect.Y + m_plotRect.Height);
if (bottom <= top)
{
return;
}
// ⚠️ 补丁矩形必须**向内**对齐到整数像素(ceil 上边界、floor 下边界):
// 网格矩形是浮点数,而 GDI `FillRect`/`BitBlt` 会按 floor/ceil 向外取整,
// "向外"会多擦掉/多写回一整行,那一行属于相邻网格(例如它的分界线),
// 于是补丁边界上会留下一像素接缝。向内对齐后,边界外的行一律由缓存提供
// (缓存内容与整幅重绘逐像素一致,已由对照帧验证)。
const int x0 = std::max(static_cast<int>(std::ceil(m_area.X)), static_cast<int>(std::ceil(m_area.X)));
const int y0 = std::max(static_cast<int>(std::ceil(top)), static_cast<int>(std::ceil(m_area.Y)));
const int x1 = std::min(static_cast<int>(std::floor(m_area.X + m_area.Width)),
static_cast<int>(std::floor(m_area.X + m_area.Width)));
const int y1 = std::min(static_cast<int>(std::floor(bottom)),
static_cast<int>(std::floor(m_area.Y + m_area.Height)));
if (x1 <= x0 || y1 <= y0)
{
return;
}
const gd::RectF patch(static_cast<float>(x0), static_cast<float>(y0),
static_cast<float>(x1 - x0), static_cast<float>(y1 - y0));
// 补丁区域必须被**完整**重画:凡是与它相交的网格都要重画一遍
// (补丁已向内对齐,正常只会命中受影响的那几个网格)。
std::vector<int> repaint;
for (std::size_t laneIndex = 0; laneIndex < m_lanes.size(); ++laneIndex)
{
const gd::RectF& rect = m_lanes[laneIndex].rect;
if (rect.Y < static_cast<float>(y1) && rect.Y + rect.Height > static_cast<float>(y0))
{
repaint.push_back(static_cast<int>(laneIndex));
}
}
if (repaint.empty())
{
return;
}
// 与整幅重绘保持完全相同的绘制顺序:底色 → 逐网格内容(PaintLane 会与当前裁剪区求交)。
{
const int saveState = g.Save();
g.SetClip(patch, gd::CombineModeIntersect);
// ⚠️ 底色填充必须**按补丁矩形本身**填充,不能用 m_area / m_plotRect:
// `UiFillRect` 对大面积不透明矩形会走 GDI `FillRect` 快路径,而 GDI 绘制
// **不受 GDI+ 裁剪区约束**,会把已经贴好的缓存内容整片覆盖掉。
UiFillRect(g, patch, m_theme->plotBg);
const float cardTop = std::max(patch.Y, m_plotRect.Y);
const float cardBottom = std::min(patch.Y + patch.Height, m_plotRect.Y + m_plotRect.Height);
if (cardBottom > cardTop)
{
UiFillRect(g, gd::RectF(patch.X, cardTop, patch.Width, cardBottom - cardTop),
ThemeLaneBackground(*m_theme));
}
const double viewMin = Tab().ViewMin();
const double viewMax = Tab().ViewMax();
const double span = viewMax - viewMin;
const double timeStep = NiceStep((span > 0.0) ? span : 1.0, kTimeTickTarget);
std::vector<double> timeTicks;
BuildTicks(viewMin, viewMax, timeStep, timeTicks);
// ⚠️ **补丁必须就地绘制装饰**(而不是"跳过装饰"):补丁范围覆盖整个受影响网格
// (含网格左缘的 Y 轴胶囊与图例),若跳过绘制,这部分像素会被网格底色/波形覆盖,
// 而补丁结果还会写回场景缓存 —— 表现为"改样式时图例/Y 轴读数闪掉一下"。
const bool savedUseDecorCache = m_useDecorCache;
m_useDecorCache = false;
for (const int laneIndex : repaint)
{
if (laneIndex >= 0 && laneIndex < static_cast<int>(m_lanes.size()))
{
PaintLane(g, laneIndex, m_lanes[static_cast<std::size_t>(laneIndex)], timeTicks);
}
}
m_useDecorCache = savedUseDecorCache;
g.Restore(saveState);
}
// 把重画结果写回缓存位图,使缓存与当前样式一致(下一帧即回到"完全命中"快路径)。
{
HDC target = g.GetHDC();
if (target != nullptr)
{
BitBlt(m_sceneDc, x0 - m_sceneX, y0 - m_sceneY, x1 - x0, y1 - y0, target, x0, y0, SRCCOPY);
g.ReleaseHDC(target);
}
}
m_sceneKey = MakeSceneCacheKey();
m_sceneGeometryHash = SceneGeometryHash();
CollectSceneStyles(m_sceneStyles);
CollectSceneDerived(m_sceneDerived);
m_sceneDegraded = m_sceneDegraded || m_fastFrame;
}
} // namespace jdi