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:
1 parent
45b44eae57
commit
04e98ee041
5 files changed
+169
-76
No files matched your search
@@ -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)
|
||||
{
|
||||
|
||||
Reference in new issue
Block a user