feat(ui): 工具栏"选区"改名、菜单对勾右置与边距修正、选区自适应勾选、悬停/选区拖拽性能
1) 工具栏文案:"时间轴缩放"/"Y 轴缩放" -> "时间轴选区"/"Y 轴选区" - Toolbar.cpp 按钮标签、Toolbar.hpp 枚举注释、Application 状态提示语同步更新 2) 网格右键菜单:对勾从左侧移到右侧,并修正菜单内各元素边距 - 文本列统一为 kPadH + kItemPad(5 + 8),三类条目(HEADER/NORMAL/EDIT)共用; 原先左侧预留 22px 标记列,导致"网格 N"、"Y 轴最大值"、"选区 Y 轴自适应"等 所有文本的左边距被整体推右(用户报告的"文本左边距过大") - 对勾改画在条目右侧的 kMarkerColumn(18) 列内,文本宽度相应预留,不会与文字重叠 - EDIT 行输入框改为右对齐到条目内容区右边界,宽度由 NaturalEditFieldWidth() 统一计算, 空间不足时为左侧说明文字保留自然宽度(避免被菜单宽度上限挤成省略号) - 分隔线、色块行同样改用 kItemPad 内缩;kRightPad 已删除 3) "选区 Y 轴自适应"增加与"Y 轴自适应"一致的勾选标记 - Lane 新增 viewAutoRange / viewAutoMin / viewAutoMax,记录应用该操作时的视图时间区间 - 新增 PlotView::LaneRangeIsAutoToView():只有未被任何手动幅值改动且时间选区未变化时才为 true - 所有手动改动路径(ApplyValueSelection/ScaleLaneRange/ShiftLaneRange/ApplyLaneValueText/ EndEdit/ResetLaneRange/MoveSignalToLane)都会清除该状态; 时间选区变化后 viewAutoMin/Max 与当前视图不符,勾选自动消失 - RefreshPlotMenuValues() 同步刷新该项状态 4) 修复大文件(VERY_BIG_DATA 1.77GB / 5 信号)下悬停与选区缩放的严重卡顿 - 根因:任何悬停态变化(左侧信号列表、网格内命中曲线提示)或选区拖拽都会触发整幅重绘, 其中波形绘制约 80ms/帧,因此交互帧率只有 ~10fps - 静态场景缓存从"仅拖拽信号/光标"推广为"只要场景键一致就复用",缓存区域由整个客户区 收敛为仅绘图区:工具栏/标签栏/面板各自带悬停态必须每帧绘制(约 10.5ms), 而真正昂贵的波形改为直接贴图(约 1ms) - 场景键新增 contentHash(FNV-1a 折叠网格几何/幅值、每条信号的引用/可见性/ 颜色/透明度/线宽/样式/显示名与所属文件名):改样式、重命名、增删信号都会自动失效 - 随鼠标变化的内容全部移出缓存:新增 PaintHoverHighlight() 在贴图之后绘制 悬停网格底色(半透明叠加)与悬停信号光晕(PaintSignalHalo()); PaintLane 内只绘制拖拽中的信号光晕(key 中记录 haloFile/haloSignal) - EnsureSceneCache 改为要求位图尺寸与当前区域严格相等(原 >= 复用会在窗口缩小后 把旧像素画到绘图区之外) - Application::Render 移除 reuseScene 分支,统一每帧绘制界面框架 实测(VERY_BIG_DATA 5 信号,Release): - 整幅重绘 ~80ms -> 命中缓存贴图 ~1ms - 悬停左侧信号列表:~95ms -> ~14ms/帧 - 悬停网格内信号(含光晕高亮):~95ms -> ~28~32ms/帧 - 时间轴 / Y 轴选区拖拽:~95~105ms -> ~19~22ms/帧 - 非拖拽静止帧 ~118ms 维持不变;空闲 CPU 0.00% 构建: make clean; make all 无警告无错误
This commit is contained in:
1 parent
a3e638fa58
commit
dd48cdd2e1
9 files changed
+414
-194
No files matched your search
@@ -2,6 +2,7 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
#include <limits>
|
||||
|
||||
#include "Utf8Util.hpp"
|
||||
@@ -501,6 +502,7 @@ namespace jdi
|
||||
for (Lane& lane : m_tab->Lanes())
|
||||
{
|
||||
lane.autoRange = true;
|
||||
lane.viewAutoRange = false;
|
||||
}
|
||||
}
|
||||
BuildLaneVisuals();
|
||||
@@ -820,6 +822,7 @@ namespace jdi
|
||||
|
||||
Lane& lane = m_tab->Lanes()[static_cast<std::size_t>(laneIndex)];
|
||||
lane.autoRange = false;
|
||||
lane.viewAutoRange = false;
|
||||
lane.yMin = std::min(valueTop, valueBottom);
|
||||
lane.yMax = std::max(valueTop, valueBottom);
|
||||
return true;
|
||||
@@ -843,6 +846,22 @@ namespace jdi
|
||||
return m_tab->Lanes()[static_cast<std::size_t>(laneIndex)].autoRange;
|
||||
}
|
||||
|
||||
bool PlotView::LaneRangeIsAutoToView(int laneIndex) const
|
||||
{
|
||||
// “选区 Y 轴自适应”是带状态的操作:只有应用后未被任何手动改动、
|
||||
// 且时间选区没有变化时,当前范围才仍等于该操作的结果。
|
||||
if (m_tab == nullptr || laneIndex < 0 || laneIndex >= static_cast<int>(m_tab->Lanes().size()))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
const Lane& lane = m_tab->Lanes()[static_cast<std::size_t>(laneIndex)];
|
||||
if (!lane.viewAutoRange || lane.autoRange)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return (lane.viewAutoMin == Tab().ViewMin()) && (lane.viewAutoMax == Tab().ViewMax());
|
||||
}
|
||||
|
||||
bool PlotView::ResetLaneRange(int laneIndex)
|
||||
{
|
||||
if (m_tab == nullptr || laneIndex < 0 || laneIndex >= static_cast<int>(m_tab->Lanes().size()))
|
||||
@@ -856,6 +875,7 @@ namespace jdi
|
||||
return false;
|
||||
}
|
||||
lane.autoRange = true;
|
||||
lane.viewAutoRange = false;
|
||||
BuildLaneVisuals();
|
||||
return true;
|
||||
}
|
||||
@@ -959,6 +979,10 @@ namespace jdi
|
||||
lane.autoRange = false;
|
||||
lane.yMin = low;
|
||||
lane.yMax = high;
|
||||
// 记录应用时的视图时间区间:时间选区一变,本项就不再是“当前生效”状态。
|
||||
lane.viewAutoRange = true;
|
||||
lane.viewAutoMin = viewMin;
|
||||
lane.viewAutoMax = viewMax;
|
||||
BuildLaneVisuals();
|
||||
return true;
|
||||
}
|
||||
@@ -988,6 +1012,7 @@ namespace jdi
|
||||
|
||||
Lane& lane = m_tab->Lanes()[static_cast<std::size_t>(laneIndex)];
|
||||
lane.autoRange = false;
|
||||
lane.viewAutoRange = false;
|
||||
lane.yMin = anchorValue - ratio * newSpan;
|
||||
lane.yMax = lane.yMin + newSpan;
|
||||
BuildLaneVisuals();
|
||||
@@ -1004,6 +1029,7 @@ namespace jdi
|
||||
// 首次纵向拖动时先把当前自适应范围固化下来,再进行偏移。
|
||||
const LaneVisual& visual = m_lanes[static_cast<std::size_t>(laneIndex)];
|
||||
Lane& lane = m_tab->Lanes()[static_cast<std::size_t>(laneIndex)];
|
||||
lane.viewAutoRange = false;
|
||||
if (lane.autoRange)
|
||||
{
|
||||
lane.autoRange = false;
|
||||
@@ -1071,6 +1097,7 @@ namespace jdi
|
||||
// 手工输入即视为取消自动自适应,与拖拽框选缩放的语义一致。
|
||||
// 必须先把当前(自适应计算出的)显示范围固化到 lane 里,再只改动被编辑的一端,
|
||||
// 否则另一端会回退到 Lane 的初始占位值,导致量程被莫名其妙地放大。
|
||||
lane.viewAutoRange = false;
|
||||
if (lane.autoRange)
|
||||
{
|
||||
lane.autoRange = false;
|
||||
@@ -1410,6 +1437,7 @@ namespace jdi
|
||||
}
|
||||
const LaneVisual& visual = m_lanes[static_cast<std::size_t>(laneIndex)];
|
||||
Lane& lane = m_tab->Lanes()[static_cast<std::size_t>(laneIndex)];
|
||||
lane.viewAutoRange = false;
|
||||
if (lane.autoRange)
|
||||
{
|
||||
lane.autoRange = false;
|
||||
@@ -2359,40 +2387,29 @@ namespace jdi
|
||||
return;
|
||||
}
|
||||
|
||||
// 拖拽期间的静态场景复用:拖动信号曲线 / 时间光标时视图与数据都没有变化,
|
||||
// 每帧重绘百万级采样的包络纯属浪费(实测 5 条密集信号约 100ms/帧)。
|
||||
// 这里命中缓存时只贴图 + 重绘随鼠标变化的部分,使拖拽帧率不随数据量下降。
|
||||
const bool sceneCacheable =
|
||||
(m_dragMode == PlotDragMode::MOVE_SIGNAL || m_dragMode == PlotDragMode::CURSOR_MOVE);
|
||||
|
||||
if (sceneCacheable && m_sceneValid)
|
||||
// 静态场景复用(不再局限于拖拽):只要视图几何、信号集合与显示样式都没变,
|
||||
// "绘图区底色 + 全部网格 + 时间轴"就逐像素相同,直接贴图即可。
|
||||
// 大文件下整幅重绘约 80ms,贴图约 1ms —— 鼠标悬停、拖拽选区这些
|
||||
// "只改一小块叠加层"的交互因此不再重绘波形。
|
||||
// 随鼠标变化的内容(悬停高亮、读数框、选区、光标、拖拽虚影、输入框)一律画在贴图之后。
|
||||
if (SceneCacheMatches() && BlitSceneCache(g))
|
||||
{
|
||||
const SceneCacheKey& cached = m_sceneKey;
|
||||
const SceneCacheKey key = MakeSceneCacheKey();
|
||||
const bool same = (key.dragMode == cached.dragMode) && (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);
|
||||
if (same && BlitSceneCache(g))
|
||||
PaintHoverHighlight(g);
|
||||
PaintLaneReadouts(g);
|
||||
PaintRubberBand(g);
|
||||
PaintCursor(g, m_lanes);
|
||||
PaintSignalInteraction(g);
|
||||
if (m_editTarget == PlotEditTarget::LANE_Y_MAX ||
|
||||
m_editTarget == PlotEditTarget::LANE_Y_MIN)
|
||||
{
|
||||
PaintLaneReadouts(g);
|
||||
PaintRubberBand(g);
|
||||
PaintCursor(g, m_lanes);
|
||||
PaintSignalInteraction(g);
|
||||
if (m_editTarget == PlotEditTarget::LANE_Y_MAX ||
|
||||
m_editTarget == PlotEditTarget::LANE_Y_MIN)
|
||||
{
|
||||
PaintEditField(g);
|
||||
}
|
||||
PaintScrollBar(g);
|
||||
|
||||
// 绘图区外框(比网格内部分界线更重,形成明确整体边界)。
|
||||
gd::Pen borderPen(ToGdiColor(m_theme->axisLine), 1.6f);
|
||||
g.DrawRectangle(&borderPen, m_plotRect.X, m_plotRect.Y, m_plotRect.Width,
|
||||
m_plotRect.Height);
|
||||
return;
|
||||
PaintEditField(g);
|
||||
}
|
||||
PaintScrollBar(g);
|
||||
|
||||
// 绘图区外框(比网格内部分界线更重,形成明确整体边界)。
|
||||
gd::Pen borderPen(ToGdiColor(m_theme->axisLine), 1.6f);
|
||||
g.DrawRectangle(&borderPen, m_plotRect.X, m_plotRect.Y, m_plotRect.Width, m_plotRect.Height);
|
||||
return;
|
||||
}
|
||||
|
||||
const double viewMin = Tab().ViewMin();
|
||||
@@ -2402,7 +2419,7 @@ namespace jdi
|
||||
std::vector<double> timeTicks;
|
||||
BuildTicks(viewMin, viewMax, timeStep, timeTicks);
|
||||
|
||||
// 绘图区整体底色(缓存路径下该内容已在离屏位图中)。
|
||||
// 绘图区整体底色。
|
||||
UiFillRect(g, m_area, m_theme->plotBg);
|
||||
|
||||
// 波形区域底色。
|
||||
@@ -2420,15 +2437,9 @@ namespace jdi
|
||||
|
||||
// 此刻画布上正好是"绘图区底色 + 全部网格 + 时间轴",尚未绘制任何随鼠标变化的内容,
|
||||
// 因此是捕获静态场景缓存的最佳时点(必须在裁剪区已全部恢复之后执行)。
|
||||
if (sceneCacheable)
|
||||
{
|
||||
CaptureSceneCache(g);
|
||||
}
|
||||
else
|
||||
{
|
||||
InvalidateSceneCache();
|
||||
}
|
||||
CaptureSceneCache(g);
|
||||
|
||||
PaintHoverHighlight(g);
|
||||
PaintLaneReadouts(g);
|
||||
PaintRubberBand(g);
|
||||
PaintCursor(g, m_lanes);
|
||||
@@ -2447,10 +2458,87 @@ namespace jdi
|
||||
g.DrawRectangle(&borderPen, m_plotRect.X, m_plotRect.Y, m_plotRect.Width, m_plotRect.Height);
|
||||
}
|
||||
|
||||
void PlotView::PaintSignalHalo(gd::Graphics& g, const SignalRef& ref, const ResolvedSignal& resolved,
|
||||
const std::vector<gd::PointF>& points,
|
||||
const std::vector<std::size_t>& breaks)
|
||||
{
|
||||
const bool drawLine = (resolved.style->mode != DrawMode::SCATTER);
|
||||
if (!drawLine || breaks.empty())
|
||||
{
|
||||
// 散点模式没有光晕(原实现同样只对折线绘制光晕)。
|
||||
(void)g;
|
||||
(void)ref;
|
||||
return;
|
||||
}
|
||||
|
||||
gd::Pen halo(ToGdiColor(WithAlpha(m_theme->accent, 0x66)),
|
||||
(resolved.style->lineWidth + 4.0f) * m_scale);
|
||||
halo.SetLineJoin(gd::LineJoinRound);
|
||||
halo.SetStartCap(gd::LineCapRound);
|
||||
halo.SetEndCap(gd::LineCapRound);
|
||||
for (std::size_t k = 0; k < breaks.size(); ++k)
|
||||
{
|
||||
const std::size_t start = breaks[k];
|
||||
const std::size_t stop = (k + 1 < breaks.size()) ? breaks[k + 1] : points.size();
|
||||
if (stop <= start + 1)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
g.DrawLines(&halo, points.data() + start, static_cast<INT>(stop - start));
|
||||
}
|
||||
}
|
||||
|
||||
void PlotView::PaintHoverHighlight(gd::Graphics& g)
|
||||
{
|
||||
if (m_tab == nullptr || m_dragMode != PlotDragMode::NONE)
|
||||
{
|
||||
// 拖拽过程中不显示悬停提示(拖拽中的信号光晕已绘入静态场景缓存)。
|
||||
return;
|
||||
}
|
||||
|
||||
const int saveState = g.Save();
|
||||
g.SetClip(m_area, gd::CombineModeIntersect);
|
||||
|
||||
// 悬停网格:一层很淡的强调色叠加。为了能画在静态场景之上,
|
||||
// 这里用半透明而非不透明混色,观感与原实现基本一致。
|
||||
if (m_hoverLane >= 0 && m_hoverLane < static_cast<int>(m_lanes.size()))
|
||||
{
|
||||
const gd::RectF& laneRect = m_lanes[static_cast<std::size_t>(m_hoverLane)].rect;
|
||||
const float clipTop = std::max(laneRect.Y, m_plotRect.Y);
|
||||
const float clipBottom = std::min(laneRect.Y + laneRect.Height, m_plotRect.Y + m_plotRect.Height);
|
||||
if (clipBottom > clipTop)
|
||||
{
|
||||
// alpha 0x0A ≈ 4%,与原「不透明混色 3.5%」的观感基本一致。
|
||||
UiFillRect(g, gd::RectF(laneRect.X, clipTop, laneRect.Width, clipBottom - clipTop),
|
||||
WithAlpha(m_theme->accent, 0x0A));
|
||||
}
|
||||
}
|
||||
|
||||
// 悬停信号:外扩一圈半透明强调色作为"可拖拽"提示。
|
||||
if (m_hoverSignal.IsValid())
|
||||
{
|
||||
const ResolvedSignal resolved = ResolveSignal(m_hoverSignal);
|
||||
if (resolved.data != nullptr && resolved.style != nullptr && resolved.visible)
|
||||
{
|
||||
const int laneIndex = m_tab->LaneOfSignal(m_hoverSignal);
|
||||
if (laneIndex >= 0 && laneIndex < static_cast<int>(m_lanes.size()))
|
||||
{
|
||||
const LaneVisual& lane = m_lanes[static_cast<std::size_t>(laneIndex)];
|
||||
const PolylineCacheEntry* cached = AcquirePolyline(m_hoverSignal, resolved, lane);
|
||||
if (cached != nullptr)
|
||||
{
|
||||
PaintSignalHalo(g, m_hoverSignal, resolved, cached->points, cached->breaks);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
g.Restore(saveState);
|
||||
}
|
||||
|
||||
PlotView::SceneCacheKey PlotView::MakeSceneCacheKey() const
|
||||
{
|
||||
SceneCacheKey key;
|
||||
key.dragMode = static_cast<int>(m_dragMode);
|
||||
key.viewMin = Tab().ViewMin();
|
||||
key.viewMax = Tab().ViewMax();
|
||||
key.laneScroll = Tab().LaneScroll();
|
||||
@@ -2460,16 +2548,118 @@ namespace jdi
|
||||
key.areaW = m_area.Width;
|
||||
key.areaH = m_area.Height;
|
||||
key.scale = m_scale;
|
||||
|
||||
const SignalRef halo = SceneHaloRef();
|
||||
key.haloFile = halo.fileIndex;
|
||||
key.haloSignal = halo.signalIndex;
|
||||
key.contentHash = SceneContentHash();
|
||||
return key;
|
||||
}
|
||||
|
||||
bool PlotView::SceneCacheMatches() const
|
||||
{
|
||||
if (!m_sceneValid)
|
||||
{
|
||||
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.haloFile == cached.haloFile) &&
|
||||
(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() const
|
||||
{
|
||||
// FNV-1a:把“视图几何之外的一切影响绘图区像素的内容”折叠成一个指纹。
|
||||
// 这样颜色/线宽/透明度/样式/重命名/信号增删等变化都会自动让缓存失效,
|
||||
// 不需要在各处手工调用 InvalidateSceneCache(容易漏)。
|
||||
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);
|
||||
|
||||
for (std::size_t laneIndex = 0; laneIndex < m_lanes.size(); ++laneIndex)
|
||||
{
|
||||
const LaneVisual& visual = m_lanes[laneIndex];
|
||||
mixFloat(visual.rect.Y);
|
||||
mixFloat(visual.rect.Height);
|
||||
mixFloat(static_cast<float>(visual.yMin));
|
||||
mixFloat(static_cast<float>(visual.yMax));
|
||||
|
||||
const std::vector<SignalRef>& signals = Tab().Lanes()[laneIndex].signals;
|
||||
mix(signals.size());
|
||||
for (const SignalRef& ref : signals)
|
||||
{
|
||||
const ResolvedSignal resolved = ResolveSignal(ref);
|
||||
mix(static_cast<std::uint64_t>(ref.fileIndex));
|
||||
mix(static_cast<std::uint64_t>(ref.signalIndex));
|
||||
mix(resolved.visible ? 1ull : 0ull);
|
||||
if (resolved.data == nullptr || resolved.style == nullptr)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
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));
|
||||
// 图例文案参与绘制,重命名必须让缓存失效。
|
||||
mixText(resolved.data->Title());
|
||||
if (resolved.file != nullptr)
|
||||
{
|
||||
mixText(resolved.file->Title());
|
||||
}
|
||||
}
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
bool PlotView::EnsureSceneCache(int width, int height)
|
||||
{
|
||||
if (width <= 0 || height <= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (m_sceneDc != nullptr && m_sceneWidth >= width && m_sceneHeight >= height)
|
||||
// 位图尺寸必须与当前缓存区域**严格一致**:贴图时按位图尺寸整块拷贝,
|
||||
// 若沿用更大的旧位图,缩小窗口后会把旧像素画到绘图区之外。
|
||||
if (m_sceneDc != nullptr && m_sceneWidth == width && m_sceneHeight == height)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
@@ -2498,36 +2688,24 @@ namespace jdi
|
||||
|
||||
bool PlotView::CaptureSceneCache(gd::Graphics& g)
|
||||
{
|
||||
// 缓存覆盖**整个客户区**:拖拽期间工具栏/标签栏/面板也同样不变,
|
||||
// 宿主(Application)据此跳过这些区域的重复绘制(实测面板绘制就占 8ms/帧)。
|
||||
int clientWidth = 0;
|
||||
int clientHeight = 0;
|
||||
if (m_host != nullptr)
|
||||
// 缓存区域严格限定为绘图区本身(m_area):工具栏/标签栏/面板由宿主每帧绘制,
|
||||
// 因为它们各自带有悬停态;绘图区的波形才是真正昂贵的部分(大文件约 80ms/帧)。
|
||||
const int x = static_cast<int>(std::floor(m_area.X));
|
||||
const int y = static_cast<int>(std::floor(m_area.Y));
|
||||
const int right = static_cast<int>(std::ceil(m_area.X + m_area.Width));
|
||||
const int bottom = static_cast<int>(std::ceil(m_area.Y + m_area.Height));
|
||||
const int width = right - x;
|
||||
const int height = bottom - y;
|
||||
if (width <= 0 || height <= 0)
|
||||
{
|
||||
RECT client = {};
|
||||
GetClientRect(m_host, &client);
|
||||
clientWidth = client.right - client.left;
|
||||
clientHeight = client.bottom - client.top;
|
||||
}
|
||||
if (clientWidth <= 0 || clientHeight <= 0)
|
||||
{
|
||||
// 无宿主句柄时退化为只缓存绘图区。
|
||||
const int x = static_cast<int>(std::floor(m_area.X)) - 2;
|
||||
const int y = static_cast<int>(std::floor(m_area.Y)) - 2;
|
||||
const int right = static_cast<int>(std::ceil(m_area.X + m_area.Width)) + 2;
|
||||
const int bottom = static_cast<int>(std::ceil(m_area.Y + m_area.Height)) + 2;
|
||||
m_sceneX = x;
|
||||
m_sceneY = y;
|
||||
clientWidth = right - x;
|
||||
clientHeight = bottom - y;
|
||||
}
|
||||
else
|
||||
{
|
||||
m_sceneX = 0;
|
||||
m_sceneY = 0;
|
||||
m_sceneValid = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!EnsureSceneCache(clientWidth, clientHeight))
|
||||
m_sceneX = x;
|
||||
m_sceneY = y;
|
||||
|
||||
if (!EnsureSceneCache(width, height))
|
||||
{
|
||||
m_sceneValid = false;
|
||||
return false;
|
||||
@@ -2539,8 +2717,7 @@ namespace jdi
|
||||
m_sceneValid = false;
|
||||
return false;
|
||||
}
|
||||
const BOOL ok = BitBlt(m_sceneDc, 0, 0, clientWidth, clientHeight, source, m_sceneX, m_sceneY,
|
||||
SRCCOPY);
|
||||
const BOOL ok = BitBlt(m_sceneDc, 0, 0, width, height, source, m_sceneX, m_sceneY, SRCCOPY);
|
||||
g.ReleaseHDC(source);
|
||||
|
||||
if (ok == FALSE)
|
||||
@@ -2554,26 +2731,6 @@ namespace jdi
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PlotView::CanReuseSceneCache() const
|
||||
{
|
||||
if (!m_sceneValid)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (m_dragMode != PlotDragMode::MOVE_SIGNAL && m_dragMode != PlotDragMode::CURSOR_MOVE)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const SceneCacheKey& cached = m_sceneKey;
|
||||
const SceneCacheKey key = MakeSceneCacheKey();
|
||||
return (key.dragMode == cached.dragMode) && (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);
|
||||
}
|
||||
|
||||
bool PlotView::BlitSceneCache(gd::Graphics& g)
|
||||
{
|
||||
if (!m_sceneValid || m_sceneDc == nullptr)
|
||||
@@ -2682,12 +2839,6 @@ namespace jdi
|
||||
const int saveState = g.Save();
|
||||
g.SetClip(gd::RectF(m_area.X, clipTop, m_area.Width, clipBottom - clipTop));
|
||||
|
||||
// 悬停高亮。
|
||||
if (laneIndex == m_hoverLane && m_dragMode == PlotDragMode::NONE)
|
||||
{
|
||||
UiFillRect(g, lane.rect, MixColor(m_theme->cardBg, m_theme->accent, 0.035));
|
||||
}
|
||||
|
||||
// 幅值网格线(先画线,标尺稍后作为覆盖层绘制,避免被波形遮挡)。
|
||||
// 刻度密度随网格高度自适应:所有网格高度相同 → 各网格的刻度数量一致,
|
||||
// 不会出现“有的网格只有 1 条标签、有的有 3 条”的不一致观感。
|
||||
@@ -2777,6 +2928,9 @@ namespace jdi
|
||||
const int waveSave = g.Save();
|
||||
g.SetClip(lane.rect, gd::CombineModeIntersect);
|
||||
|
||||
// 只有拖拽中的信号光晕可以进入静态场景缓存(悬停光晕见 PaintHoverHighlight)。
|
||||
const SignalRef haloRef = SceneHaloRef();
|
||||
|
||||
for (const SignalRef& ref : laneSignals)
|
||||
{
|
||||
const ResolvedSignal resolved = ResolveSignal(ref);
|
||||
@@ -2799,24 +2953,12 @@ namespace jdi
|
||||
const bool drawScatter = (mode == DrawMode::SCATTER || mode == DrawMode::LINE_SCATTER);
|
||||
|
||||
// 悬停命中的信号先画一层浅色光晕,作为“可拖拽”的视觉提示。
|
||||
const bool highlighted = (ref == m_hoverSignal) || (ref == m_dragSignal);
|
||||
if (highlighted && drawLine && !breaks.empty())
|
||||
// 这里只处理拖拽中的信号(其光晕在拖拽期间不变,可以进入静态场景缓存);
|
||||
// 悬停光晕随鼠标变化,改由 PaintHoverHighlight 在缓存贴图之后绘制。
|
||||
const bool highlighted = (ref == haloRef);
|
||||
if (highlighted)
|
||||
{
|
||||
gd::Pen halo(ToGdiColor(WithAlpha(m_theme->accent, 0x66)),
|
||||
(resolved.style->lineWidth + 4.0f) * m_scale);
|
||||
halo.SetLineJoin(gd::LineJoinRound);
|
||||
halo.SetStartCap(gd::LineCapRound);
|
||||
halo.SetEndCap(gd::LineCapRound);
|
||||
for (std::size_t k = 0; k < breaks.size(); ++k)
|
||||
{
|
||||
const std::size_t start = breaks[k];
|
||||
const std::size_t stop = (k + 1 < breaks.size()) ? breaks[k + 1] : points.size();
|
||||
if (stop <= start + 1)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
g.DrawLines(&halo, points.data() + start, static_cast<INT>(stop - start));
|
||||
}
|
||||
PaintSignalHalo(g, ref, resolved, points, breaks);
|
||||
}
|
||||
|
||||
if (drawLine && !breaks.empty())
|
||||
|
||||
Reference in new issue
Block a user