- Lane::ResampleItem + resample/resampleItems/resamplePanel/resampleScroll, 并在 LaneCarry / SplitLane / PruneLaneSignalItems / RemapAfterSignalInsert|Removal 同步 - 面板:可滚动分组列表(信号色条目栏 + 复选框 + 展开三角),展开后 4 行: 最大/最小数据间隔(只读,勾选时一次性缓存,绘制不扫描采样)+ 重采样间隔/频率(输入) - 输入框联动:改间隔同步频率,改频率同步间隔(输入单位分别为秒 / Hz) - 「构建为新信号」:DataSet::AppendGroupAxis 追加新时间轴 + InsertDerivedSignal, 采样值先按公式/系数/绝对值/镜像/Y偏移变换再等间隔线性插值,时间轴偏移不烘值 - 新信号名 <原信号> (重采样 <自适应单位频率>);Utf8Util::FormatFrequency 为唯一实现 - 非单调轴 / 点太少 / 间隔过大 / 点数超 2000 万 ⇒ 拒绝构建并给出自绘提示 - 顺带加入 FormulaEval(公式编译/求值,供 ApplyDrawTransform 与后续 UI 使用)
508 lines
22 KiB
C++
508 lines
22 KiB
C++
// 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.style->formula);
|
||
}
|
||
// 图例文案参与绘制,重命名必须让缓存失效。
|
||
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;
|
||
entry.formula = resolved.style->formula;
|
||
}
|
||
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
|