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:
DeepSeek V4.1 Flash committed 2026-10-07 11:26:43 +08:00
1 parent a3e638fa58
commit dd48cdd2e1
9 files changed
+414 -194

No files matched your search

+18 -18
View File
@@ -689,20 +689,15 @@ namespace jdi
g.SetPixelOffsetMode(gd::PixelOffsetModeHalf);
g.SetTextRenderingHint(gd::TextRenderingHintClearTypeGridFit);
// 拖拽(拖动信号曲线 / 时间光标)期间视图与数据都不变:
// 静态场景缓存已包含工具栏/标签栏/面板/全部网格,直接跳过这些区域的重复绘制,
// 实测可省下约 13ms/帧(面板绘制就占 8ms)。
const bool reuseScene = m_plot.CanReuseSceneCache();
// 工具栏/标签栏/面板各自带悬停态,必须每帧绘制;
// 真正昂贵的是绘图区的波形(大文件约 80ms/帧),
// 由 PlotView 内部的静态场景缓存消除,与数据量无关。
UiFillRect(g, gd::RectF(0.0f, 0.0f, static_cast<float>(width), static_cast<float>(height)),
m_theme.windowBg);
if (!reuseScene)
{
UiFillRect(g, gd::RectF(0.0f, 0.0f, static_cast<float>(width), static_cast<float>(height)),
m_theme.windowBg);
m_tabBar.Paint(g);
m_toolbar.Paint(g);
m_panel.Paint(g);
}
m_tabBar.Paint(g);
m_toolbar.Paint(g);
m_panel.Paint(g);
m_plot.Paint(g);
@@ -1033,8 +1028,8 @@ namespace jdi
if (m_plot.ToggleZoomMode(PlotZoomMode::TIME))
{
SetStatusMessage((m_plot.CurrentZoomMode() == PlotZoomMode::TIME)
? L"时间轴缩放:在绘图区横向拖拽选择区间即可放大。"
: L"已退出时间轴缩放模式。",
? L"时间轴选区:在绘图区横向拖拽选择区间即可放大。"
: L"已退出时间轴选区模式。",
false);
}
break;
@@ -1042,8 +1037,8 @@ namespace jdi
if (m_plot.ToggleZoomMode(PlotZoomMode::VALUE))
{
SetStatusMessage((m_plot.CurrentZoomMode() == PlotZoomMode::VALUE)
? L"Y 轴缩放:在某个网格内纵向拖拽选择区间即可放大。"
: L"已退出 Y 轴缩放模式。",
? L"Y 轴选区:在某个网格内纵向拖拽选择区间即可放大。"
: L"已退出 Y 轴选区模式。",
false);
}
break;
@@ -1126,7 +1121,7 @@ namespace jdi
// Esc 的语义:
// 1) 放弃当前正在进行的拖拽(典型场景:缩放的框选选区还没松开左键);
// 取消后不应用选区、视图范围保持不变;
// 2) 同时退出全部缩放模式,使工具栏「时间轴缩放」「Y 轴缩放」回到未选中态。
// 2) 同时退出全部缩放模式,使工具栏「时间轴选区」「Y 轴选区」回到未选中态。
// Esc 不与时间光标关联:关闭光标请用工具栏「光标」按钮,避免误关指针。
bool changed = m_plot.CancelDrag();
changed = m_plot.ExitZoomModes() || changed;
@@ -1409,6 +1404,7 @@ namespace jdi
MenuItem autoViewItem;
autoViewItem.label = L"选区 Y 轴自适应";
autoViewItem.id = PLOT_CMD_LANE_AUTO_VIEW;
autoViewItem.checked = m_plot.LaneRangeIsAutoToView(laneIndex);
items.push_back(autoViewItem);
MenuItem separator;
@@ -1458,6 +1454,10 @@ namespace jdi
const bool isAuto = m_plot.LaneRangeIsAuto(laneIndex);
m_menu.UpdateItemLabel(PLOT_CMD_LANE_AUTO, isAuto ? L"Y 轴自适应(当前)" : L"恢复 Y 轴自适应",
isAuto, !isAuto);
// “选区 Y 轴自适应”是带状态的操作,同样需要实时反映其“当前生效”标记。
m_menu.UpdateItemLabel(PLOT_CMD_LANE_AUTO_VIEW, L"选区 Y 轴自适应",
m_plot.LaneRangeIsAutoToView(laneIndex), true);
}
void Application::DispatchMenuResult(const MenuResult& result)
+75 -23
View File
@@ -253,17 +253,7 @@ namespace jdi
bool CancelDrag();
/**
* @brief 判断本帧是否可以整体复用拖拽静态场景缓存。
*
* 拖动信号曲线 / 时间光标时视图与数据都不变,缓存内容仍然有效。
* 宿王可据此跳过工具栏/标签栏/面板的重复绘制(它们也在缓存内)。
*
* @return 可复用返回 true。
*/
bool CanReuseSceneCache() const;
/**
* @brief 退出全部缩放模式(时间轴缩放 / Y 轴缩放),使工具栏缩放按钮回到未选中态。
* @brief 退出全部缩放模式(时间轴选区 / Y 轴选区),使工具栏缩放按钮回到未选中态。
* @return 之前处于某个缩放模式返回 true。
*/
bool ExitZoomModes();
@@ -320,6 +310,17 @@ namespace jdi
*/
bool LaneRangeIsAuto(int laneIndex) const;
/**
* @brief 判断某网格当前范围是否正是「选区 Y 轴自适应」的结果(用于菜单勾选标记)。
*
* 只有在该操作之后未发生任何手动幅值改动、且时间选区没有变化时才返回 true,
* 因为一旦时间选区改变,之前的选区自适应结果就不再对应当前视野。
*
* @param [IN] laneIndex 网格下标。
* @return 当前生效返回 true。
*/
bool LaneRangeIsAutoToView(int laneIndex) const;
/**
* @brief 让某网格的 Y 轴恢复自适应。
* @param [IN] laneIndex 网格下标。
@@ -844,23 +845,27 @@ namespace jdi
mutable std::deque<PolylineCacheEntry> m_polylineCache; /**< 折线顶点缓存(按几何键复用)。 */
/**
* @brief 静态场景缓存的几何键。
* @brief 静态场景缓存的几何/内容键。
*
* 只要这组参数完全一致,"背景 + 全部网格 + 时间轴"的渲染结果就逐像素相同,
* 可以直接复用离屏位图,无需重算百万级采样的包络与绘制。
* 这使鼠标悬停、拖拽选区等"只改一小块叠加层"的交互不再重绘波形:
* 大文件下整幅重绘约 80ms,命中缓存时只需 1~2ms 贴图。
*/
struct SceneCacheKey
{
int dragMode = -1; /**< 拖拽模式(拖拽中才允许复用)。 */
double viewMin = 0.0; /**< 时间轴下界。 */
double viewMax = 0.0; /**< 时间轴上界。 */
float laneScroll = 0.0f; /**< 网格滚动偏移。 */
int laneCount = 0; /**< 网格数量。 */
float areaX = 0.0f; /**< 绘图区左上角。 */
double viewMin = 0.0; /**< 时间轴下界。 */
double viewMax = 0.0; /**< 时间轴上界。 */
float laneScroll = 0.0f; /**< 网格滚动偏移。 */
int laneCount = 0; /**< 网格数量。 */
float areaX = 0.0f; /**< 绘图区左上角。 */
float areaY = 0.0f;
float areaW = 0.0f; /**< 绘图区尺寸。 */
float areaW = 0.0f; /**< 绘图区尺寸。 */
float areaH = 0.0f;
float scale = 1.0f; /**< DPI 缩放。 */
float scale = 1.0f; /**< DPI 缩放。 */
int haloFile = -1; /**< 已绘入缓存的拖拽光晕信号(无则 -1)。 */
int haloSignal = -1;
std::uint64_t contentHash = 0; /**< 信号集合/显示样式/网格幅值等内容指纹。 */
};
/**
@@ -869,6 +874,49 @@ namespace jdi
*/
SceneCacheKey MakeSceneCacheKey() const;
/**
* @brief 判断当前缓存键是否与已捕获的场景一致。
* @return 可直接贴图返回 true。
*/
bool SceneCacheMatches() const;
/**
* @brief 计算场景内容指纹(信号集合、显示样式、网格幅值、图例文案)。
*
* 视图几何以外的任何内容变化(改颜色/线宽/透明度/样式、重命名、增删信号)
* 都会改变该指纹,从而让静态场景缓存自动失效,无需在各处手工调用失效接口。
*
* @return 指纹值。
*/
std::uint64_t SceneContentHash() const;
/**
* @brief 当前应当绘入静态场景缓存的信号光晕(仅拖拽中的信号)。
*
* 拖拽信号的光晕在整个拖拽过程中不变,可以进入缓存;
* 悬停光晕随鼠标移动,必须画在缓存之外。
*
* @return 信号引用,无则返回无效引用。
*/
SignalRef SceneHaloRef() const;
/**
* @brief 绘制静态场景缓存之外的"悬停高亮"(悬停网格底色 + 悬停信号光晕)。
* @param [IN,OUT] g 绘图上下文。
*/
void PaintHoverHighlight(gd::Graphics& g);
/**
* @brief 绘制某条信号的悬停/拖拽光晕。
* @param [IN,OUT] g 绘图上下文。
* @param [IN] ref 信号引用。
* @param [IN] resolved 已解析的信号。
* @param [IN] points 折线顶点。
* @param [IN] breaks 每段折线的起始下标。
*/
void PaintSignalHalo(gd::Graphics& g, const SignalRef& ref, const ResolvedSignal& resolved,
const std::vector<gd::PointF>& points, const std::vector<std::size_t>& breaks);
/**
* @brief 确保离屏缓存位图存在且尺寸匹配。
* @param [IN] width 位图宽度(像素)。
@@ -878,7 +926,11 @@ namespace jdi
bool EnsureSceneCache(int width, int height);
/**
* @brief 把当前画布内容(背景 + 界面外框 + 全部网格 + 时间轴)拷贝进离屏缓存。
* @brief 把当前画布内容(绘图区底色 + 全部网格 + 时间轴)拷贝进离屏缓存。
*
* 缓存区域严格限定为绘图区(m_area)。悬停与拖拽产生的内容一律不进入缓存,
* 因此在任何交互过程中都可以安全复用,只贴图而不再重绘波形。
*
* @param [IN,OUT] g 当前绘图上下文(必须未设置裁剪区)。
* @return 拷贝成功返回 true。
*/
@@ -906,8 +958,8 @@ namespace jdi
HGDIOBJ m_sceneOldBitmap = nullptr; /**< 离屏 DC 原选位图。 */
int m_sceneWidth = 0; /**< 离屏位图宽度。 */
int m_sceneHeight = 0; /**< 离屏位图高度。 */
int m_sceneX = 0; /**< 离屏缓存对应的客户区横坐标。 */
int m_sceneY = 0; /**< 离屏缓存对应的客户区纵坐标。 */
int m_sceneX = 0; /**< 缓存对应的客户区横坐标(绘图区左边界)。 */
int m_sceneY = 0; /**< 缓存对应的客户区纵坐标(绘图区上边界)。 */
bool m_sceneValid = false; /**< 缓存内容是否有效。 */
SceneCacheKey m_sceneKey; /**< 缓存对应的场景参数。 */
+256 -114
View File
@@ -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())
+2 -2
View File
@@ -22,8 +22,8 @@ namespace jdi
NONE = 0, /**< 未触发任何动作。 */
OPEN_FILE, /**< 打开 CSV 文件。 */
FIT_ALL, /**< 视图自适应到可见信号范围(同时退出缩放模式)。 */
ZOOM_TIME, /**< 切换「在时间轴缩放」模式。 */
ZOOM_VALUE, /**< 切换「在 Y 轴缩放」模式。 */
ZOOM_TIME, /**< 切换「时间轴选区」模式(框选时间区间后放大)。 */
ZOOM_VALUE, /**< 切换「Y 轴选区」模式(框选幅值区间后放大)。 */
CURSOR, /**< 切换时间光标。 */
MERGE_LANES, /**< 合并为单一网格。 */
SPLIT_LANES, /**< 按信号拆分为多网格。 */
+2 -2
View File
@@ -68,8 +68,8 @@ namespace jdi
const Spec specs[] = {
{ToolbarAction::OPEN_FILE, L"导入文件", true, false, false},
{ToolbarAction::FIT_ALL, L"适应视图", false, true, false},
{ToolbarAction::ZOOM_TIME, L"时间轴缩放", false, true, true},
{ToolbarAction::ZOOM_VALUE, L"Y 轴缩放", false, true, true},
{ToolbarAction::ZOOM_TIME, L"时间轴选区", false, true, true},
{ToolbarAction::ZOOM_VALUE, L"Y 轴选区", false, true, true},
{ToolbarAction::CURSOR, L"光标", false, true, true},
{ToolbarAction::MERGE_LANES, L"合并网格", false, true, false},
{ToolbarAction::SPLIT_LANES, L"拆分网格", false, true, false},