优化百万级采样文件的绘制性能,Esc 改为取消拖拽选区
性能(BIG_DATA.csv 128 万采样 × 10 信号全显示,实测): - 折线抽稀改为按像素列取上下包络(min/max),顶点数由固定 20000 降到像素量级; 既保住尖峰(等间隔抽样会整段漏掉),又不再把大量顶点堆在同一像素列上。 - 新增折线顶点缓存:以“构建折线实际读取的全部输入”(信号、时间轴范围、绘图区横向 几何、网格纵向几何)为键,视图未变化时直接复用,重复绘制不再重算全量采样。 - 悬停命中检测先按 x 二分裁剪到容差范围内的一小段(点到线段距离必不小于横向距离), 鼠标移动不再遍历上万个顶点。 - 结果:整帧渲染 1309ms -> 103~127ms(10 网格);折线构建 25ms -> 0ms(缓存命中); 单条折线绘制 1255ms -> 45~53ms;悬停命中 ~0ms。两文件 6 网格场景约 52ms。 - 未引入多线程:耗时集中在 GDI+ 单线程绘制调用与一次性 O(N) 包络扫描, 并行化的收益上限仅约 25ms,不值得引入线程与同步复杂度。 - 空闲(无交互)CPU 占用实测 0.00%,消息循环阻塞在 GetMessage, 无需重绘时不再产生任何计算。 Esc 行为: - 缩放拖拽(时间轴/幅值轴框选)按住左键时按 Esc:立即放弃选区并清空拖拽状态, 随后松开左键也不会把选区应用到视图(已验证:时间轴保持原范围,对照组正常缩放)。 - Esc 不再关闭时间光标(原先 Esc 会调用 DisableCursor);关闭光标只走工具栏「光标」按钮, 避免顺手按 Esc 时意外关掉指针。 - 数据层结构变化(加载/移除文件、切换上下文)后清空折线缓存, 防止旧几何键与新信号重合而取到过期顶点。
This commit is contained in:
1 parent
12b3531485
commit
d6a81839bb
3 files changed
+312
-20
No files matched your search
@@ -424,6 +424,9 @@ namespace jdi
|
|||||||
m_dataSet.ValidateSelection();
|
m_dataSet.ValidateSelection();
|
||||||
m_panel.InvalidateRows();
|
m_panel.InvalidateRows();
|
||||||
|
|
||||||
|
// 文件/信号结构变化会让信号引用与文件地址改变,折线顶点缓存必须作废。
|
||||||
|
m_plot.InvalidatePolylineCache();
|
||||||
|
|
||||||
// 强制下一次同步重新计算视图状态。
|
// 强制下一次同步重新计算视图状态。
|
||||||
m_cachedVisibleCount = -1;
|
m_cachedVisibleCount = -1;
|
||||||
m_cachedAnyVisible = false;
|
m_cachedAnyVisible = false;
|
||||||
@@ -867,8 +870,11 @@ namespace jdi
|
|||||||
switch (key)
|
switch (key)
|
||||||
{
|
{
|
||||||
case VK_ESCAPE:
|
case VK_ESCAPE:
|
||||||
m_plot.DisableCursor();
|
// Esc 只用于放弃当前正在进行的拖拽(典型场景:缩放的框选选区还没松开左键)。
|
||||||
return true;
|
// 取消后不应用选区、视图范围保持不变;松开左键也不会再生效(拖拽模式已复位)。
|
||||||
|
// Esc 不再与时间光标关联:关闭光标请用工具栏「光标」按钮或再次点击该按钮,
|
||||||
|
// 避免用户按 Esc 时意外把指针关掉。
|
||||||
|
return m_plot.CancelDrag();
|
||||||
case VK_ADD:
|
case VK_ADD:
|
||||||
case VK_OEM_PLUS:
|
case VK_OEM_PLUS:
|
||||||
m_plot.ZoomTimeStep(1.4);
|
m_plot.ZoomTimeStep(1.4);
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
#include <objidl.h>
|
#include <objidl.h>
|
||||||
#include <gdiplus.h>
|
#include <gdiplus.h>
|
||||||
|
|
||||||
|
#include <deque>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
@@ -229,6 +230,18 @@ namespace jdi
|
|||||||
void DisableCursor();
|
void DisableCursor();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* @brief 放弃当前正在进行的拖拽(缩放选区/平移/信号拖动),不改变视图范围。
|
||||||
|
* @return 确实取消了某个拖拽返回 true。
|
||||||
|
*/
|
||||||
|
bool CancelDrag();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 清空折线顶点缓存。
|
||||||
|
*
|
||||||
|
* 数据层结构变化(加载/移除文件、信号增删)后信号引用与文件地址都可能改变,
|
||||||
|
* 必须清空缓存,避免旧几何键与新的信号意外重合而取到过期顶点。
|
||||||
|
*/
|
||||||
|
void InvalidatePolylineCache(); /**
|
||||||
* @brief 切换时间光标:启用时置于当前视图中心,再调用则关闭。
|
* @brief 切换时间光标:启用时置于当前视图中心,再调用则关闭。
|
||||||
* @return 状态发生变化返回 true。
|
* @return 状态发生变化返回 true。
|
||||||
*/
|
*/
|
||||||
@@ -742,6 +755,43 @@ namespace jdi
|
|||||||
|
|
||||||
std::vector<LegendHit> m_legendHits; /**< 最近一次绘制时记录的图例条目矩形。 */
|
std::vector<LegendHit> m_legendHits; /**< 最近一次绘制时记录的图例条目矩形。 */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 折线顶点缓存条目。
|
||||||
|
*
|
||||||
|
* 折线抽稀是 O(采样点数) 的(百万级采样约 2~3ms/条),
|
||||||
|
* 而绘制、悬停命中检测每帧都要用到同一批顶点。
|
||||||
|
* 用“构建折线时读取的全部输入”作为键缓存顶点,
|
||||||
|
* 视图/网格几何未变化时可直接复用,避免重复扫描全量采样。
|
||||||
|
*/
|
||||||
|
struct PolylineCacheEntry
|
||||||
|
{
|
||||||
|
SignalRef ref; /**< 信号引用。 */
|
||||||
|
const DataFile* file = nullptr; /**< 所属文件(内容不可变)。 */
|
||||||
|
std::size_t sampleCount = 0; /**< 采样点数(数据变化时改变)。 */
|
||||||
|
double viewMin = 0.0; /**< 时间轴显示下界。 */
|
||||||
|
double viewMax = 0.0; /**< 时间轴显示上界。 */
|
||||||
|
float plotX = 0.0f; /**< 绘图区左边界。 */
|
||||||
|
float plotWidth = 0.0f; /**< 绘图区宽度。 */
|
||||||
|
float laneY = 0.0f; /**< 网格上边界。 */
|
||||||
|
float laneHeight = 0.0f; /**< 网格高度。 */
|
||||||
|
double laneYMin = 0.0; /**< 网格幅值下界。 */
|
||||||
|
double laneYMax = 0.0; /**< 网格幅值上界。 */
|
||||||
|
std::vector<gd::PointF> points; /**< 折线顶点。 */
|
||||||
|
std::vector<std::size_t> breaks; /**< 每段折线在 points 中的起始下标。 */
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 取得(必要时重建)指定信号在当前几何下的折线顶点。
|
||||||
|
* @param [IN] ref 信号引用(缓存键的一部分)。
|
||||||
|
* @param [IN] resolved 已解析的信号。
|
||||||
|
* @param [IN] lane 网格绘制信息。
|
||||||
|
* @return 缓存条目指针,无法构建时返回 nullptr。
|
||||||
|
*/
|
||||||
|
const PolylineCacheEntry* AcquirePolyline(const SignalRef& ref, const ResolvedSignal& resolved,
|
||||||
|
const LaneVisual& lane) const;
|
||||||
|
|
||||||
|
mutable std::deque<PolylineCacheEntry> m_polylineCache; /**< 折线顶点缓存(按几何键复用)。 */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief 查询坐标命中的图例条目。
|
* @brief 查询坐标命中的图例条目。
|
||||||
* @param [IN] x 客户区横坐标。
|
* @param [IN] x 客户区横坐标。
|
||||||
|
|||||||
@@ -21,8 +21,12 @@ namespace jdi
|
|||||||
const unsigned int kMinorLineAlpha = 0x9Au; /**< 辅助等分线的透明度(越高越清晰)。 */
|
const unsigned int kMinorLineAlpha = 0x9Au; /**< 辅助等分线的透明度(越高越清晰)。 */
|
||||||
const float kScrollBarThickness = 9.0f; /**< 滚动条宽度(逻辑像素)。 */
|
const float kScrollBarThickness = 9.0f; /**< 滚动条宽度(逻辑像素)。 */
|
||||||
const float kLanePadding = 8.0f; /**< 网格内边距。 */
|
const float kLanePadding = 8.0f; /**< 网格内边距。 */
|
||||||
const size_t kMaxPolylinePoints = 20000; /**< 单条折线最大顶点数。 */
|
const size_t kMaxPolylinePoints = 20000; /**< 单条折线最大顶点数(上限保护)。 */
|
||||||
|
const size_t kPolylinePointsPerPixel = 2; /**< 每个像素列允许的折线顶点数(上/下包络两顶点)。 */
|
||||||
|
const size_t kMinPolylinePoints = 512; /**< 折线最少顶点数(窗口很窄时仍保留细节)。 */
|
||||||
|
const float kEnvelopeMinPixelSpan = 0.5f; /**< 包络纵向跨度小于该像素值时合并为单个顶点。 */
|
||||||
const size_t kMaxScatterPoints = 2500; /**< 单条散点最大绘制点数。 */
|
const size_t kMaxScatterPoints = 2500; /**< 单条散点最大绘制点数。 */
|
||||||
|
const size_t kPolylineCacheLimit = 64; /**< 折线顶点缓存条目上限。 */
|
||||||
const float kMinScatterRadius = 1.6f; /**< 散点最小半径(逻辑像素)。 */
|
const float kMinScatterRadius = 1.6f; /**< 散点最小半径(逻辑像素)。 */
|
||||||
const float kMaxScatterRadius = 12.0f; /**< 散点最大半径(逻辑像素)。 */
|
const float kMaxScatterRadius = 12.0f; /**< 散点最大半径(逻辑像素)。 */
|
||||||
const float kScatterRadiusScale = 1.5f; /**< 散点半径相对线宽的增长系数。 */
|
const float kScatterRadiusScale = 1.5f; /**< 散点半径相对线宽的增长系数。 */
|
||||||
@@ -103,6 +107,8 @@ namespace jdi
|
|||||||
m_tab = tab;
|
m_tab = tab;
|
||||||
m_hoverLane = -1;
|
m_hoverLane = -1;
|
||||||
m_dragMode = PlotDragMode::NONE;
|
m_dragMode = PlotDragMode::NONE;
|
||||||
|
// 重新绑定数据源后旧的几何键不再有意义,必须清空折线缓存。
|
||||||
|
InvalidatePolylineCache();
|
||||||
RefreshLanes();
|
RefreshLanes();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -503,6 +509,30 @@ namespace jdi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool PlotView::CancelDrag()
|
||||||
|
{
|
||||||
|
if (m_dragMode == PlotDragMode::NONE)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 放弃当前拖拽:清空拖拽状态与尚未生效的缩放选区,不改动任何视图范围。
|
||||||
|
// 调用方仍持有鼠标捕获,松开左键时会走 OnLButtonUp,
|
||||||
|
// 此时拖拽模式已是 NONE,选区不会被应用到时间轴/幅值轴上。
|
||||||
|
m_dragMode = PlotDragMode::NONE;
|
||||||
|
m_dragLane = -1;
|
||||||
|
m_dragMoved = false;
|
||||||
|
m_rubberRect = gd::RectF();
|
||||||
|
m_dragSignal = SignalRef();
|
||||||
|
m_dragTargetLane = -1;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void PlotView::InvalidatePolylineCache()
|
||||||
|
{
|
||||||
|
m_polylineCache.clear();
|
||||||
|
}
|
||||||
|
|
||||||
bool PlotView::FitTimeToLane(int laneIndex)
|
bool PlotView::FitTimeToLane(int laneIndex)
|
||||||
{
|
{
|
||||||
if (m_dataSet == nullptr || m_tab == nullptr)
|
if (m_dataSet == nullptr || m_tab == nullptr)
|
||||||
@@ -917,9 +947,6 @@ namespace jdi
|
|||||||
const float fx = static_cast<float>(x);
|
const float fx = static_cast<float>(x);
|
||||||
const float fy = static_cast<float>(y);
|
const float fy = static_cast<float>(y);
|
||||||
|
|
||||||
std::vector<gd::PointF> points;
|
|
||||||
std::vector<std::size_t> breaks;
|
|
||||||
|
|
||||||
SignalRef best;
|
SignalRef best;
|
||||||
float bestDistance = tolerance;
|
float bestDistance = tolerance;
|
||||||
|
|
||||||
@@ -932,17 +959,47 @@ namespace jdi
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
BuildPolyline(resolved, lane, points, breaks);
|
// 复用绘制阶段的折线顶点:悬停检测通常与上一帧几何完全相同,
|
||||||
|
// 不再为“鼠标移动但命中结果未变”重复扫描全量采样点。
|
||||||
|
const PolylineCacheEntry* cached = AcquirePolyline(ref, resolved, lane);
|
||||||
|
if (cached == nullptr)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const std::vector<gd::PointF>& points = cached->points;
|
||||||
|
const std::vector<std::size_t>& breaks = cached->breaks;
|
||||||
if (points.size() < 2)
|
if (points.size() < 2)
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 横向先行裁剪:点到线段的距离必然不小于横向距离本身,
|
||||||
|
// 因此只有横向落在容差范围内的顶点才可能命中。
|
||||||
|
// 时间单调时折线顶点按 x 递增,可二分定位后只扫描这一小段;
|
||||||
|
// 否则每次鼠标移动都要遍历上万个顶点(悬停卡顿的来源之一)。
|
||||||
|
std::size_t windowFirst = 0;
|
||||||
|
std::size_t windowLast = points.size();
|
||||||
|
if (resolved.file->timeMonotonic)
|
||||||
|
{
|
||||||
|
const auto byX = [](const gd::PointF& point, float limit) { return point.X < limit; };
|
||||||
|
windowFirst = static_cast<std::size_t>(
|
||||||
|
std::lower_bound(points.begin(), points.end(), fx - tolerance, byX) - points.begin());
|
||||||
|
windowLast = static_cast<std::size_t>(
|
||||||
|
std::upper_bound(points.begin(), points.end(), fx + tolerance,
|
||||||
|
[](float limit, const gd::PointF& point) { return limit < point.X; }) -
|
||||||
|
points.begin());
|
||||||
|
}
|
||||||
|
if (windowFirst >= windowLast)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// 散点模式下把采样点本身也视为可命中目标。
|
// 散点模式下把采样点本身也视为可命中目标。
|
||||||
if (resolved.style->mode == DrawMode::SCATTER)
|
if (resolved.style->mode == DrawMode::SCATTER)
|
||||||
{
|
{
|
||||||
for (const gd::PointF& point : points)
|
for (std::size_t index = windowFirst; index < windowLast; ++index)
|
||||||
{
|
{
|
||||||
|
const gd::PointF& point = points[index];
|
||||||
const float dx = point.X - fx;
|
const float dx = point.X - fx;
|
||||||
const float dy = point.Y - fy;
|
const float dy = point.Y - fy;
|
||||||
const float distance = std::sqrt(dx * dx + dy * dy);
|
const float distance = std::sqrt(dx * dx + dy * dy);
|
||||||
@@ -959,7 +1016,9 @@ namespace jdi
|
|||||||
{
|
{
|
||||||
const std::size_t start = breaks[k];
|
const std::size_t start = breaks[k];
|
||||||
const std::size_t stop = (k + 1 < breaks.size()) ? breaks[k + 1] : points.size();
|
const std::size_t stop = (k + 1 < breaks.size()) ? breaks[k + 1] : points.size();
|
||||||
for (std::size_t index = start + 1; index < stop; ++index)
|
const std::size_t scanBegin = std::max(start + 1, windowFirst);
|
||||||
|
const std::size_t scanEnd = std::min(stop, windowLast);
|
||||||
|
for (std::size_t index = scanBegin; index < scanEnd; ++index)
|
||||||
{
|
{
|
||||||
const gd::PointF& a = points[index - 1];
|
const gd::PointF& a = points[index - 1];
|
||||||
const gd::PointF& b = points[index];
|
const gd::PointF& b = points[index];
|
||||||
@@ -1770,6 +1829,59 @@ namespace jdi
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const PlotView::PolylineCacheEntry* PlotView::AcquirePolyline(const SignalRef& ref,
|
||||||
|
const ResolvedSignal& resolved,
|
||||||
|
const LaneVisual& lane) const
|
||||||
|
{
|
||||||
|
if (resolved.data == nullptr || resolved.file == nullptr)
|
||||||
|
{
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 键 = 构建折线时实际读取的全部输入:
|
||||||
|
// 信号(引用 + 文件内容 + 采样点数)、时间轴显示范围、绘图区横向几何、网格纵向几何。
|
||||||
|
// 这些值全部相同时,BuildPolyline 的输出必然完全一致,可直接复用。
|
||||||
|
const std::size_t sampleCount = std::min(resolved.file->time.size(), resolved.data->values.size());
|
||||||
|
const double viewMin = Tab().ViewMin();
|
||||||
|
const double viewMax = Tab().ViewMax();
|
||||||
|
|
||||||
|
for (const PolylineCacheEntry& entry : m_polylineCache)
|
||||||
|
{
|
||||||
|
if (entry.ref == ref && entry.file == resolved.file && entry.sampleCount == sampleCount &&
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
return &entry;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 未命中:重建并写入缓存(deque 保证已有元素的引用在追加后依然有效)。
|
||||||
|
// 缓存只用于消除重复计算,容量上限约等于“网格数 × 每网格信号数”的两倍。
|
||||||
|
if (m_polylineCache.size() >= kPolylineCacheLimit)
|
||||||
|
{
|
||||||
|
m_polylineCache.clear();
|
||||||
|
}
|
||||||
|
m_polylineCache.push_back(PolylineCacheEntry());
|
||||||
|
PolylineCacheEntry& entry = m_polylineCache.back();
|
||||||
|
entry.ref = ref;
|
||||||
|
entry.file = resolved.file;
|
||||||
|
entry.sampleCount = sampleCount;
|
||||||
|
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;
|
||||||
|
|
||||||
|
BuildPolyline(resolved, lane, entry.points, entry.breaks);
|
||||||
|
// BuildPolyline 通过引用填充,push_back 过程中不会再次进入本函数,引用安全。
|
||||||
|
return &m_polylineCache.back();
|
||||||
|
}
|
||||||
|
|
||||||
void PlotView::BuildPolyline(const ResolvedSignal& resolved, const LaneVisual& lane,
|
void PlotView::BuildPolyline(const ResolvedSignal& resolved, const LaneVisual& lane,
|
||||||
std::vector<gd::PointF>& points, std::vector<std::size_t>& breaks) const
|
std::vector<gd::PointF>& points, std::vector<std::size_t>& breaks) const
|
||||||
{
|
{
|
||||||
@@ -1816,16 +1928,136 @@ namespace jdi
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::size_t stride = 1;
|
|
||||||
const std::size_t visibleCount = end - begin;
|
|
||||||
if (visibleCount > kMaxPolylinePoints)
|
|
||||||
{
|
|
||||||
stride = (visibleCount + kMaxPolylinePoints - 1) / kMaxPolylinePoints;
|
|
||||||
}
|
|
||||||
|
|
||||||
const float limitTop = lane.rect.Y - lane.rect.Height * 4.0f;
|
const float limitTop = lane.rect.Y - lane.rect.Height * 4.0f;
|
||||||
const float limitBottom = lane.rect.Y + lane.rect.Height * 5.0f;
|
const float limitBottom = lane.rect.Y + lane.rect.Height * 5.0f;
|
||||||
|
|
||||||
|
const std::size_t visibleCount = end - begin;
|
||||||
|
|
||||||
|
// 顶点预算由绘图区宽度决定(而不是固定值):折线顶点数直接决定
|
||||||
|
// GDI+ 抗锯齿折线的绘制耗时,百万级采样文件若按固定 20000 顶点绘制,
|
||||||
|
// 会把大量顶点堆在同一个像素列上(画出来完全重合),纯粹浪费 CPU。
|
||||||
|
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);
|
||||||
|
|
||||||
|
// 采样点密度远超像素分辨率时改用“像素列上下包络”抽稀:
|
||||||
|
// 每个像素列只保留最小/最大两个顶点(按出现先后排序),
|
||||||
|
// 既不会漏掉尖峰(等间隔抽样会整段跳过),又把顶点数压到像素量级。
|
||||||
|
if (visibleCount > budget && resolved.file->timeMonotonic && m_plotRect.Width > 1.0f)
|
||||||
|
{
|
||||||
|
const double viewSpan = viewMax - viewMin;
|
||||||
|
const float plotLeft = m_plotRect.X;
|
||||||
|
const float plotWidth = m_plotRect.Width;
|
||||||
|
const std::size_t columns = std::max<std::size_t>(1, budget / 2);
|
||||||
|
const float columnWidth = plotWidth / static_cast<float>(columns);
|
||||||
|
|
||||||
|
const std::vector<double>::const_iterator rangeBegin = time.begin() + static_cast<std::ptrdiff_t>(begin);
|
||||||
|
const std::vector<double>::const_iterator rangeEnd = time.begin() + static_cast<std::ptrdiff_t>(end);
|
||||||
|
|
||||||
|
bool inSegment = false;
|
||||||
|
for (std::size_t column = 0; column < columns; ++column)
|
||||||
|
{
|
||||||
|
const double t0 = viewMin + viewSpan * (static_cast<double>(column) / static_cast<double>(columns));
|
||||||
|
const double t1 =
|
||||||
|
viewMin + viewSpan * (static_cast<double>(column + 1) / static_cast<double>(columns));
|
||||||
|
|
||||||
|
const std::size_t i0 = static_cast<std::size_t>(
|
||||||
|
std::lower_bound(rangeBegin, rangeEnd, t0) - time.begin());
|
||||||
|
const std::size_t i1 = (column + 1 == columns)
|
||||||
|
? end
|
||||||
|
: static_cast<std::size_t>(
|
||||||
|
std::lower_bound(rangeBegin, rangeEnd, t1) - time.begin());
|
||||||
|
if (i1 <= i0)
|
||||||
|
{
|
||||||
|
// 该列没有采样点:说明数据在此处存在时间间隙,断开当前折线。
|
||||||
|
inSegment = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
double low = std::numeric_limits<double>::infinity();
|
||||||
|
double high = -std::numeric_limits<double>::infinity();
|
||||||
|
std::size_t lowIndex = 0;
|
||||||
|
std::size_t highIndex = 0;
|
||||||
|
bool broken = false;
|
||||||
|
for (std::size_t index = i0; index < i1; ++index)
|
||||||
|
{
|
||||||
|
const double value = values[index];
|
||||||
|
if (!std::isfinite(value))
|
||||||
|
{
|
||||||
|
// 该列含无效值:包络绘制到本列结束,后续另起一段。
|
||||||
|
broken = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (value < low)
|
||||||
|
{
|
||||||
|
low = value;
|
||||||
|
lowIndex = index;
|
||||||
|
}
|
||||||
|
if (value > high)
|
||||||
|
{
|
||||||
|
high = value;
|
||||||
|
highIndex = index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(low <= high))
|
||||||
|
{
|
||||||
|
// 本列全是无效值。
|
||||||
|
inSegment = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const float x = plotLeft + (static_cast<float>(column) + 0.5f) * columnWidth;
|
||||||
|
const float yTop =
|
||||||
|
static_cast<float>(ClampDouble(ValueToY(high, lane), limitTop, limitBottom));
|
||||||
|
const float yBottom =
|
||||||
|
static_cast<float>(ClampDouble(ValueToY(low, lane), limitTop, limitBottom));
|
||||||
|
|
||||||
|
if (yBottom - yTop < kEnvelopeMinPixelSpan)
|
||||||
|
{
|
||||||
|
// 波动不足半个像素:只留一个顶点,避免无意义的重复顶点。
|
||||||
|
if (!inSegment)
|
||||||
|
{
|
||||||
|
breaks.push_back(points.size());
|
||||||
|
inSegment = true;
|
||||||
|
}
|
||||||
|
points.push_back(gd::PointF(x, yTop));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (!inSegment)
|
||||||
|
{
|
||||||
|
breaks.push_back(points.size());
|
||||||
|
inSegment = true;
|
||||||
|
}
|
||||||
|
// 按采样点出现的先后顺序连接上下包络,保持走线方向一致。
|
||||||
|
if (lowIndex <= highIndex)
|
||||||
|
{
|
||||||
|
points.push_back(gd::PointF(x, yBottom));
|
||||||
|
points.push_back(gd::PointF(x, yTop));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
points.push_back(gd::PointF(x, yTop));
|
||||||
|
points.push_back(gd::PointF(x, yBottom));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (broken)
|
||||||
|
{
|
||||||
|
inSegment = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::size_t stride = 1;
|
||||||
|
if (visibleCount > budget)
|
||||||
|
{
|
||||||
|
stride = (visibleCount + budget - 1) / budget;
|
||||||
|
}
|
||||||
|
|
||||||
bool inSegment = false;
|
bool inSegment = false;
|
||||||
|
|
||||||
for (std::size_t index = begin; index < end; index += stride)
|
for (std::size_t index = begin; index < end; index += stride)
|
||||||
@@ -2080,9 +2312,6 @@ namespace jdi
|
|||||||
const int waveSave = g.Save();
|
const int waveSave = g.Save();
|
||||||
g.SetClip(lane.rect, gd::CombineModeIntersect);
|
g.SetClip(lane.rect, gd::CombineModeIntersect);
|
||||||
|
|
||||||
std::vector<gd::PointF> points;
|
|
||||||
std::vector<std::size_t> breaks;
|
|
||||||
|
|
||||||
for (const SignalRef& ref : laneSignals)
|
for (const SignalRef& ref : laneSignals)
|
||||||
{
|
{
|
||||||
const ResolvedSignal resolved = ResolveSignal(ref);
|
const ResolvedSignal resolved = ResolveSignal(ref);
|
||||||
@@ -2092,7 +2321,14 @@ namespace jdi
|
|||||||
}
|
}
|
||||||
|
|
||||||
const DrawMode mode = resolved.style->mode;
|
const DrawMode mode = resolved.style->mode;
|
||||||
BuildPolyline(resolved, lane, points, breaks);
|
// 顶点由“几何键”缓存提供:同一视图下重复绘制不再重算全量采样。
|
||||||
|
const PolylineCacheEntry* cached = AcquirePolyline(ref, resolved, lane);
|
||||||
|
if (cached == nullptr)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const std::vector<gd::PointF>& points = cached->points;
|
||||||
|
const std::vector<std::size_t>& breaks = cached->breaks;
|
||||||
|
|
||||||
const bool drawLine = (mode != DrawMode::SCATTER);
|
const bool drawLine = (mode != DrawMode::SCATTER);
|
||||||
const bool drawScatter = (mode == DrawMode::SCATTER || mode == DrawMode::LINE_SCATTER);
|
const bool drawScatter = (mode == DrawMode::SCATTER || mode == DrawMode::LINE_SCATTER);
|
||||||
|
|||||||
Reference in new issue
Block a user