Esc 退出缩放模式、修复拖拽时波形变模糊、加速超大文件的模式切换

Esc 行为(用户要求):
- 网格内按 Esc 在取消拖拽选区的同时,退出全部缩放模式(时间轴缩放 / Y 轴缩放),
  工具栏两个缩放按钮随之回到未选中态;时间光标按钮不受影响。
- 新增 PlotView::ExitZoomModes(),由 Application 的 VK_ESCAPE 分支与 CancelDrag 一起调用。

修复拖拽时波形变模糊(用户报告):
- 上一轮为提升拖拽帧率引入了"交互降级":拖拽期间关闭抗锯齿 + 顶点预算减半 +
  每列只扫 32 个采样点。实测这会让包络丢掉峰值、线条失去抗锯齿,
  在选区放大 / 拖拽信号 / 拖拽光标时明显变"糊"。现已**全部移除**,
  任何交互期间都按全质量绘制。
- 用"粗粒度包络层"替代降级来保证交互帧率:
  SignalData 新增 coarseMin/coarseMax/coarseStride(每 256 个采样一组存 min/max,
  额外内存约为采样数据的 0.8%),在 BuildDataFile 统计 min/max 的同一个循环里顺带构建
  (不额外扫数据)。折线包络改为读取"每列覆盖到的分组",
  数据读取量从 O(可见采样数) 降到 O(像素列数 × 少量分组),**绘制结果与全量扫描完全一致**。
- 实测(VERY_BIG_DATA.CSV = 1.77GB、2580 万行 × 5 信号,全选显示):
  视图变化时的包络构建由 176~196ms 降到 ≈0ms;平移拖拽整帧由约 336ms 降到 90~120ms。
- 注意:包络路径按分组判断无效值(整组无有效值才断线),
  因此密集视图中孤立的 NaN 不再把波形切成两段(原先会),视觉上更干净。

加速超大文件的显示模式切换(用户报告):
- 散点绘制由逐点 FillEllipse 改为把全部散点合并进一条 GraphicsPath 后一次 FillPath:
  实测 2500 点由 63ms 降到 7ms(基准),程序内单条密集信号由约 51ms 降到约 22ms,
  仍是抗锯齿圆形,观感与逐点绘制一致。
- 线段/散点/点线切换时不再有卡顿感;空闲 CPU 仍为 0.00%。

验证:
- Esc:分别启用时间轴缩放 / Y 轴缩放后按 Esc,两个按钮均回到未选中(截图对比),
  时间光标按钮保持选中;框选拖拽中按 Esc 时间轴保持 0.2~1.2 不变。
- 清晰度:光标拖拽中与静止时对同一区域做像素哈希,结果完全一致;
  框选拖拽同样一致;信号拖拽中间态截图中源波形与虚影均为抗锯齿全质量。
- 超大文件:线段/散点/点线切换与拖拽的正常渲染(截图),1.77GB 文件加载过渡动画正常。
- 回归:菜单内联编辑 Y 轴上下限、选区 Y 轴自适应、图例点击选中、面板改名均正常。
- 内存:BIG_DATA 全选 122MB(粗粒度层约 +1MB);VERY_BIG 全选 1224MB(理论下限 1.25GB)。
- make clean && make all 零警告零错误。
This commit is contained in:
DeepSeek V4.1 Flash committed 2026-09-30 19:34:52 +08:00
1 parent 45b44eae57
commit 04e98ee041
5 files changed
+169 -76

No files matched your search

+10 -5
View File
@@ -1113,11 +1113,16 @@ namespace jdi
switch (key)
{
case VK_ESCAPE:
// Esc 只用于放弃当前正在进行的拖拽(典型场景:缩放的框选选区还没松开左键)。
// 取消后不应用选区、视图范围保持不变;松开左键也不会再生效(拖拽模式已复位)。
// Esc 不再与时间光标关联:关闭光标请用工具栏「光标」按钮或再次点击该按钮,
// 避免用户按 Esc 时意外把指针关掉。
return m_plot.CancelDrag();
{
// Esc 的语义:
// 1) 放弃当前正在进行的拖拽(典型场景:缩放的框选选区还没松开左键);
// 取消后不应用选区、视图范围保持不变;
// 2) 同时退出全部缩放模式,使工具栏「时间轴缩放」「Y 轴缩放」回到未选中态。
// Esc 不与时间光标关联:关闭光标请用工具栏「光标」按钮,避免误关指针。
bool changed = m_plot.CancelDrag();
changed = m_plot.ExitZoomModes() || changed;
return changed;
}
case VK_ADD:
case VK_OEM_PLUS:
m_plot.ZoomTimeStep(1.4);
+8 -5
View File
@@ -247,6 +247,12 @@ namespace jdi
*/
bool CancelDrag();
/**
* @brief 退出全部缩放模式(时间轴缩放 / Y 轴缩放),使工具栏缩放按钮回到未选中态。
* @return 之前处于某个缩放模式返回 true。
*/
bool ExitZoomModes();
/**
* @brief 清空折线顶点缓存。
*
@@ -623,11 +629,10 @@ namespace jdi
* @brief 构建单条信号在当前视图下的折线点序列。
* @param [IN] resolved 已解析的信号视图。
* @param [IN] lane 所属网格绘制信息。
* @param [IN] coarse 交互降级模式:限制每列扫描的采样点数,降低拖拽帧耗时。
* @param [OUT] points 输出点序列。
* @param [OUT] breaks 输出每段折线的起始下标(用于处理缺失值断线)。
*/
void BuildPolyline(const ResolvedSignal& resolved, const LaneVisual& lane, bool coarse,
void BuildPolyline(const ResolvedSignal& resolved, const LaneVisual& lane,
std::vector<gd::PointF>& points, std::vector<std::size_t>& breaks) const;
/**
@@ -798,7 +803,6 @@ namespace jdi
float laneHeight = 0.0f; /**< 网格高度。 */
double laneYMin = 0.0; /**< 网格幅值下界。 */
double laneYMax = 0.0; /**< 网格幅值上界。 */
bool coarse = false; /**< 是否为交互降级顶点(拖拽期间)。 */
std::vector<gd::PointF> points; /**< 折线顶点。 */
std::vector<std::size_t> breaks; /**< 每段折线在 points 中的起始下标。 */
};
@@ -808,11 +812,10 @@ namespace jdi
* @param [IN] ref 信号引用(缓存键的一部分)。
* @param [IN] resolved 已解析的信号。
* @param [IN] lane 网格绘制信息。
* @param [IN] coarse 是否使用交互降级顶点(全质量与降级结果分别缓存)。
* @return 缓存条目指针,无法构建时返回 nullptr。
*/
const PolylineCacheEntry* AcquirePolyline(const SignalRef& ref, const ResolvedSignal& resolved,
const LaneVisual& lane, bool coarse) const;
const LaneVisual& lane) const;
mutable std::deque<PolylineCacheEntry> m_polylineCache; /**< 折线顶点缓存(按几何键复用)。 */
+93 -62
View File
@@ -529,6 +529,23 @@ namespace jdi
return true;
}
bool PlotView::ExitZoomModes()
{
if (m_tab == nullptr)
{
return false;
}
// 退出全部缩放模式:工具栏的「时间轴缩放」「Y 轴缩放」按钮会在
// Application::Render 中按 ZoomMode 同步为未选中态。
if (Tab().ZoomMode() == PlotZoomMode::NONE)
{
return false;
}
Tab().ZoomMode() = PlotZoomMode::NONE;
return true;
}
void PlotView::InvalidatePolylineCache()
{
m_polylineCache.clear();
@@ -1127,7 +1144,7 @@ namespace jdi
// 复用绘制阶段的折线顶点:悬停检测通常与上一帧几何完全相同,
// 不再为“鼠标移动但命中结果未变”重复扫描全量采样点。
const PolylineCacheEntry* cached = AcquirePolyline(ref, resolved, lane, false);
const PolylineCacheEntry* cached = AcquirePolyline(ref, resolved, lane);
if (cached == nullptr)
{
continue;
@@ -1997,7 +2014,7 @@ namespace jdi
const PlotView::PolylineCacheEntry* PlotView::AcquirePolyline(const SignalRef& ref,
const ResolvedSignal& resolved,
const LaneVisual& lane, bool coarse) const
const LaneVisual& lane) const
{
if (resolved.data == nullptr || resolved.file == nullptr)
{
@@ -2017,7 +2034,7 @@ namespace jdi
entry.viewMin == viewMin && entry.viewMax == viewMax && entry.plotX == m_plotRect.X &&
entry.plotWidth == m_plotRect.Width && entry.laneY == lane.rect.Y &&
entry.laneHeight == lane.rect.Height && entry.laneYMin == lane.yMin &&
entry.laneYMax == lane.yMax && entry.coarse == coarse)
entry.laneYMax == lane.yMax)
{
return &entry;
}
@@ -2042,14 +2059,13 @@ namespace jdi
entry.laneHeight = lane.rect.Height;
entry.laneYMin = lane.yMin;
entry.laneYMax = lane.yMax;
entry.coarse = coarse;
BuildPolyline(resolved, lane, coarse, entry.points, entry.breaks);
BuildPolyline(resolved, lane, entry.points, entry.breaks);
// BuildPolyline 通过引用填充,push_back 过程中不会再次进入本函数,引用安全。
return &m_polylineCache.back();
}
void PlotView::BuildPolyline(const ResolvedSignal& resolved, const LaneVisual& lane, bool coarse,
void PlotView::BuildPolyline(const ResolvedSignal& resolved, const LaneVisual& lane,
std::vector<gd::PointF>& points, std::vector<std::size_t>& breaks) const
{
points.clear();
@@ -2103,9 +2119,8 @@ namespace jdi
// 顶点预算由绘图区宽度决定(而不是固定值):折线顶点数直接决定
// GDI+ 抗锯齿折线的绘制耗时,百万级采样文件若按固定 20000 顶点绘制,
// 会把大量顶点堆在同一个像素列上(画出来完全重合),纯粹浪费 CPU。
// 拖拽期间进一步降半:顶点数就是绘制耗时,交互时用“2 像素一列”的包络即可。
const std::size_t pointsPerPixel = coarse ? 1 : kPolylinePointsPerPixel;
std::size_t budget = static_cast<std::size_t>(std::max(1.0f, m_plotRect.Width)) * pointsPerPixel;
std::size_t budget =
static_cast<std::size_t>(std::max(1.0f, m_plotRect.Width)) * kPolylinePointsPerPixel;
budget = std::max(budget, kMinPolylinePoints);
budget = std::min(budget, kMaxPolylinePoints);
@@ -2122,10 +2137,14 @@ namespace jdi
const std::vector<double>::const_iterator rangeEnd = time.begin() + static_cast<std::ptrdiff_t>(end);
// 交互降级:拖拽期间限定每列扫描的采样点数。
// 全量扫描是 O(可见采样数),千万级采样 × 多条信号会让拖拽掉到 10FPS 以下;
// 每列取 32 个点已经足够看出包络形状,松手后会重新做一次全质量绘制。
const std::size_t coarsePointsPerColumn = 32;
// 粗粒度包络层(若已构建):每列只需读取覆盖到的分组,
// 数据量从“可见采样数”降到“像素列数 × 少量分组”,
// 且分组内存的就是真实极值,绘制结果与逐点扫描完全一致。
const std::vector<double>& coarseMin = resolved.data->coarseMin;
const std::vector<double>& coarseMax = resolved.data->coarseMax;
const std::size_t coarseStride = resolved.data->coarseStride;
const bool hasCoarse =
(coarseStride > 0) && !coarseMin.empty() && (coarseMin.size() == coarseMax.size());
bool inSegment = false;
std::size_t searchFrom = begin;
@@ -2154,42 +2173,61 @@ namespace jdi
continue;
}
std::size_t scanStep = 1;
if (coarse)
{
const std::size_t columnPoints = i1 - i0;
if (columnPoints > coarsePointsPerColumn)
{
scanStep = columnPoints / coarsePointsPerColumn;
}
}
double low = std::numeric_limits<double>::infinity();
double high = -std::numeric_limits<double>::infinity();
std::size_t lowIndex = 0;
std::size_t highIndex = 0;
std::size_t lowOrder = 0;
std::size_t highOrder = 0;
bool broken = false;
for (std::size_t index = i0; index < i1; index += scanStep)
// 一列至少覆盖两个分组时才用分组层(否则直接扫原始采样更准且更省)。
if (hasCoarse && (i1 - i0) >= 2 * coarseStride)
{
const double value = values[index];
if (!std::isfinite(value))
const std::size_t firstBucket = i0 / coarseStride;
const std::size_t lastBucket =
std::min((i1 - 1) / coarseStride, coarseMin.size() - 1);
for (std::size_t bucket = firstBucket; bucket <= lastBucket; ++bucket)
{
// 该列含无效值:包络绘制到本列结束,后续另起一段。
if (!coarse)
const double bucketLow = coarseMin[bucket];
const double bucketHigh = coarseMax[bucket];
if (!std::isfinite(bucketLow))
{
// 该分组全部是无效值:包络到此断开,后续另起一段。
broken = true;
continue;
}
if (bucketLow < low)
{
low = bucketLow;
lowOrder = bucket;
}
if (bucketHigh > high)
{
high = bucketHigh;
highOrder = bucket;
}
continue;
}
if (value < low)
}
else
{
for (std::size_t index = i0; index < i1; ++index)
{
low = value;
lowIndex = index;
}
if (value > high)
{
high = value;
highIndex = index;
const double value = values[index];
if (!std::isfinite(value))
{
// 该列含无效值:包络绘制到本列结束,后续另起一段。
broken = true;
continue;
}
if (value < low)
{
low = value;
lowOrder = index;
}
if (value > high)
{
high = value;
highOrder = index;
}
}
}
@@ -2224,7 +2262,7 @@ namespace jdi
inSegment = true;
}
// 按采样点出现的先后顺序连接上下包络,保持走线方向一致。
if (lowIndex <= highIndex)
if (lowOrder <= highOrder)
{
points.push_back(gd::PointF(x, yBottom));
points.push_back(gd::PointF(x, yTop));
@@ -2505,15 +2543,6 @@ namespace jdi
const int waveSave = g.Save();
g.SetClip(lane.rect, gd::CombineModeIntersect);
// 交互(拖拽平移/框选/拖动信号)期间关闭抗锯齿:波形折线是整帧中最贵的绘制调用,
// 关闭抗锯齿可显著降低单帧耗时,使拖拽跟手;松开后立即恢复抗锯齿全质量重绘。
const bool interactive = (m_dragMode != PlotDragMode::NONE);
const gd::SmoothingMode previousSmoothing = g.GetSmoothingMode();
if (interactive)
{
g.SetSmoothingMode(gd::SmoothingModeNone);
}
for (const SignalRef& ref : laneSignals)
{
const ResolvedSignal resolved = ResolveSignal(ref);
@@ -2524,9 +2553,7 @@ namespace jdi
const DrawMode mode = resolved.style->mode;
// 顶点由“几何键”缓存提供:同一视图下重复绘制不再重算全量采样。
// 拖拽期间用降级顶点(扫描点数受限),松手后自然回到全质量版本。
const bool coarse = interactive;
const PolylineCacheEntry* cached = AcquirePolyline(ref, resolved, lane, coarse);
const PolylineCacheEntry* cached = AcquirePolyline(ref, resolved, lane);
if (cached == nullptr)
{
continue;
@@ -2587,21 +2614,25 @@ namespace jdi
m_scale);
const std::size_t scatterStride =
std::max<std::size_t>(1, points.size() / kMaxScatterPoints);
gd::SolidBrush brush(ToGdiColor(StyleColor(*resolved.style)));
// 所有散点合并到一条 GraphicsPath 后只调用一次 FillPath:
// 逐点 FillEllipse 每次都要完整走一遍 GDI+ 的状态检查与光栅化入口,
// 实测 2500 点需要约 51ms;合并后约 1/9(实测基准 63ms → 7ms),
// 且仍是抗锯齿圆形,观感与逐点绘制完全一致。
gd::GraphicsPath scatterPath;
for (std::size_t k = 0; k < points.size(); k += scatterStride)
{
g.FillEllipse(&brush, points[k].X - radius, points[k].Y - radius, radius * 2.0f,
radius * 2.0f);
scatterPath.AddEllipse(points[k].X - radius, points[k].Y - radius, radius * 2.0f,
radius * 2.0f);
}
gd::SolidBrush brush(ToGdiColor(StyleColor(*resolved.style)));
if (scatterPath.GetPointCount() > 0)
{
g.FillPath(&brush, &scatterPath);
}
}
}
// 交互期间临时关闭的抗锯齿在此恢复(g.Restore 也会还原状态,这里显式恢复以免依赖栈深)。
if (interactive)
{
g.SetSmoothingMode(previousSmoothing);
}
// g.Restore 也会还原绘制状态,此处直接恢复图形状态即可。
g.Restore(waveSave);
}
@@ -3147,7 +3178,7 @@ namespace jdi
std::vector<gd::PointF> points;
std::vector<std::size_t> breaks;
BuildPolyline(resolved, sourceVisual, false, points, breaks);
BuildPolyline(resolved, sourceVisual, points, breaks);
for (gd::PointF& point : points)
{