fix(plot): 默认线宽 1.0px、菜单文本左对齐、散点白边修复与大文件拖拽性能优化
1) 默认线宽改为 1.0px - SignalStyle::lineWidth 初值 1.6f -> 1.0f (DataModel.hpp / DataModel.cpp) - 新增 LaneScroll() const 重载,供 const 的 MakeSceneCacheKey() 读取 2) 网格右键菜单文本左对齐 - PopupMenu 的 HEADER / NORMAL / EDIT 三类条目统一使用 kMarkerColumn(22.0f) 作为文本列,替换原先写死的 8.0f,使 "网格 N"、"Y 轴最大值"、 "Y 轴最小值"、"选区 Y 轴自适应" 等所有条目左边缘严格对齐 3) 修复散点/点线模式下数据点重叠处的白色缝隙 - 根因: GDI+ GraphicsPath 默认 FillModeAlternate(奇偶填充),重叠圆的交叠区域 被反向填充,视觉上表现为白色缺口/白边 - 主绘制与拖拽 ghost 的 GraphicsPath 均显式 SetFillMode(FillModeWinding) - ghost 由逐点 FillEllipse 改为单个 GraphicsPath 一次 FillPath(批量绘制) 4) 大文件拖拽流畅性优化 (VERY_BIG_DATA.CSV 1.77GB / 5 信号) - 新增拖拽静态场景缓存: 在 MOVE_SIGNAL / CURSOR_MOVE 期间把整个客户区 (绘图区 + 标签页 + 工具栏 + 信号面板) 捕快到 m_sceneDc 位图,后续帧直接 BitBlt,仅在 SceneCacheKey(dragMode/viewMin/viewMax/laneScroll/laneCount/ 区域/scale) 变化时重绘。Application::Render 在复用场景时跳过窗口背景与 各子控件绘制,缓存位图由 ~PlotView() 释放 - 失效点: SetContext / SetScale / Layout / InvalidatePolylineCache - 光标数值读数从 PaintLane 抽出为 PaintLaneReadouts(),在缓存 blit 之后绘制, 避免读数被缓存导致拖动时显示旧值 - 拖拽 ghost 复用已有 PolylineCacheEntry,改用 TranslateTransform 偏移, 不再重复构造折线顶点 - 实测: 拖拽帧耗时 134-146ms -> 22-25ms;散点模式拖拽约 24.6ms; 非拖拽重绘维持约 118ms;空闲 CPU 0.00% 构建: make clean; make all 无警告无错误
This commit is contained in:
1 parent
04e98ee041
commit
a3e638fa58
6 files changed
+407
-29
No files matched your search
@@ -689,12 +689,21 @@ namespace jdi
|
||||
g.SetPixelOffsetMode(gd::PixelOffsetModeHalf);
|
||||
g.SetTextRenderingHint(gd::TextRenderingHintClearTypeGridFit);
|
||||
|
||||
UiFillRect(g, gd::RectF(0.0f, 0.0f, static_cast<float>(width), static_cast<float>(height)),
|
||||
m_theme.windowBg);
|
||||
// 拖拽(拖动信号曲线 / 时间光标)期间视图与数据都不变:
|
||||
// 静态场景缓存已包含工具栏/标签栏/面板/全部网格,直接跳过这些区域的重复绘制,
|
||||
// 实测可省下约 13ms/帧(面板绘制就占 8ms)。
|
||||
const bool reuseScene = m_plot.CanReuseSceneCache();
|
||||
|
||||
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);
|
||||
|
||||
UiFillRect(g, m_splitterRect, m_theme.border);
|
||||
|
||||
@@ -66,6 +66,11 @@ namespace jdi
|
||||
public:
|
||||
PlotView();
|
||||
|
||||
/**
|
||||
* @brief 析构:释放拖拽静态场景缓存的离屏位图。
|
||||
*/
|
||||
~PlotView();
|
||||
|
||||
/**
|
||||
* @brief 设置宿主窗口句柄(用于剪贴板所有者)。
|
||||
* @param [IN] host 宿主窗口句柄,允许为空。
|
||||
@@ -247,6 +252,16 @@ namespace jdi
|
||||
*/
|
||||
bool CancelDrag();
|
||||
|
||||
/**
|
||||
* @brief 判断本帧是否可以整体复用拖拽静态场景缓存。
|
||||
*
|
||||
* 拖动信号曲线 / 时间光标时视图与数据都不变,缓存内容仍然有效。
|
||||
* 宿王可据此跳过工具栏/标签栏/面板的重复绘制(它们也在缓存内)。
|
||||
*
|
||||
* @return 可复用返回 true。
|
||||
*/
|
||||
bool CanReuseSceneCache() const;
|
||||
|
||||
/**
|
||||
* @brief 退出全部缩放模式(时间轴缩放 / Y 轴缩放),使工具栏缩放按钮回到未选中态。
|
||||
* @return 之前处于某个缩放模式返回 true。
|
||||
@@ -722,6 +737,15 @@ namespace jdi
|
||||
*/
|
||||
void PaintEmptyState(gd::Graphics& g);
|
||||
|
||||
/**
|
||||
* @brief 绘制全部网格的时间光标读数框。
|
||||
*
|
||||
* 读数随时间光标实时变化,因此不能进入拖拽静态场景缓存,必须每帧单独绘制。
|
||||
*
|
||||
* @param [IN,OUT] g 绘图上下文。
|
||||
*/
|
||||
void PaintLaneReadouts(gd::Graphics& g);
|
||||
|
||||
const UiFonts* m_fonts = nullptr; /**< 字体资源。 */
|
||||
const Theme* m_theme = nullptr; /**< 主题资源。 */
|
||||
const DataSet* m_dataSet = nullptr; /**< 数据集合(只读访问)。 */
|
||||
@@ -819,6 +843,74 @@ namespace jdi
|
||||
|
||||
mutable std::deque<PolylineCacheEntry> m_polylineCache; /**< 折线顶点缓存(按几何键复用)。 */
|
||||
|
||||
/**
|
||||
* @brief 静态场景缓存的几何键。
|
||||
*
|
||||
* 只要这组参数完全一致,"背景 + 全部网格 + 时间轴"的渲染结果就逐像素相同,
|
||||
* 可以直接复用离屏位图,无需重算百万级采样的包络与绘制。
|
||||
*/
|
||||
struct SceneCacheKey
|
||||
{
|
||||
int dragMode = -1; /**< 拖拽模式(拖拽中才允许复用)。 */
|
||||
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 areaH = 0.0f;
|
||||
float scale = 1.0f; /**< DPI 缩放。 */
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief 构造当前场景的缓存键。
|
||||
* @return 缓存键。
|
||||
*/
|
||||
SceneCacheKey MakeSceneCacheKey() const;
|
||||
|
||||
/**
|
||||
* @brief 确保离屏缓存位图存在且尺寸匹配。
|
||||
* @param [IN] width 位图宽度(像素)。
|
||||
* @param [IN] height 位图高度(像素)。
|
||||
* @return 可用返回 true。
|
||||
*/
|
||||
bool EnsureSceneCache(int width, int height);
|
||||
|
||||
/**
|
||||
* @brief 把当前画布内容(背景 + 界面外框 + 全部网格 + 时间轴)拷贝进离屏缓存。
|
||||
* @param [IN,OUT] g 当前绘图上下文(必须未设置裁剪区)。
|
||||
* @return 拷贝成功返回 true。
|
||||
*/
|
||||
bool CaptureSceneCache(gd::Graphics& g);
|
||||
|
||||
/**
|
||||
* @brief 把离屏缓存贴回画布。
|
||||
* @param [IN,OUT] g 绘图上下文。
|
||||
* @return 命中缓存并贴图成功返回 true。
|
||||
*/
|
||||
bool BlitSceneCache(gd::Graphics& g);
|
||||
|
||||
/**
|
||||
* @brief 丢弃缓存标记(下次绘制会重新捕获)。
|
||||
*/
|
||||
void InvalidateSceneCache();
|
||||
|
||||
/**
|
||||
* @brief 释放离屏缓存占用的 GDI 对象。
|
||||
*/
|
||||
void ReleaseSceneCache();
|
||||
|
||||
HBITMAP m_sceneBitmap = nullptr; /**< 离屏缓存位图。 */
|
||||
HDC m_sceneDc = nullptr; /**< 离屏缓存设备上下文。 */
|
||||
HGDIOBJ m_sceneOldBitmap = nullptr; /**< 离屏 DC 原选位图。 */
|
||||
int m_sceneWidth = 0; /**< 离屏位图宽度。 */
|
||||
int m_sceneHeight = 0; /**< 离屏位图高度。 */
|
||||
int m_sceneX = 0; /**< 离屏缓存对应的客户区横坐标。 */
|
||||
int m_sceneY = 0; /**< 离屏缓存对应的客户区纵坐标。 */
|
||||
bool m_sceneValid = false; /**< 缓存内容是否有效。 */
|
||||
SceneCacheKey m_sceneKey; /**< 缓存对应的场景参数。 */
|
||||
|
||||
/**
|
||||
* @brief 查询坐标命中的图例条目。
|
||||
* @param [IN] x 客户区横坐标。
|
||||
|
||||
@@ -10,6 +10,7 @@ namespace jdi
|
||||
{
|
||||
|
||||
|
||||
|
||||
namespace
|
||||
{
|
||||
const int kTimeTickTarget = 10; /**< 时间轴期望刻度数。 */
|
||||
@@ -95,6 +96,11 @@ namespace jdi
|
||||
|
||||
PlotView::PlotView() = default;
|
||||
|
||||
PlotView::~PlotView()
|
||||
{
|
||||
ReleaseSceneCache();
|
||||
}
|
||||
|
||||
void PlotView::Setup(const UiFonts* fonts, const Theme* theme)
|
||||
{
|
||||
m_fonts = fonts;
|
||||
@@ -110,12 +116,14 @@ namespace jdi
|
||||
m_dragMode = PlotDragMode::NONE;
|
||||
// 重新绑定数据源后旧的几何键不再有意义,必须清空折线缓存。
|
||||
InvalidatePolylineCache();
|
||||
InvalidateSceneCache();
|
||||
RefreshLanes();
|
||||
}
|
||||
|
||||
void PlotView::SetScale(float scale)
|
||||
{
|
||||
m_scale = (scale > 0.1f) ? scale : 1.0f;
|
||||
InvalidateSceneCache();
|
||||
}
|
||||
|
||||
bool PlotView::CursorEnabled() const
|
||||
@@ -184,6 +192,8 @@ namespace jdi
|
||||
void PlotView::Layout(const gd::RectF& area)
|
||||
{
|
||||
m_area = area;
|
||||
// 布局变化(窗口缩放、面板宽度调整、网格数量变化)后静态场景不再有效。
|
||||
InvalidateSceneCache();
|
||||
|
||||
m_labelBandWidth = 66.0f * m_scale;
|
||||
m_timeAxisHeight = 34.0f * m_scale;
|
||||
@@ -549,6 +559,7 @@ namespace jdi
|
||||
void PlotView::InvalidatePolylineCache()
|
||||
{
|
||||
m_polylineCache.clear();
|
||||
InvalidateSceneCache();
|
||||
}
|
||||
|
||||
bool PlotView::FitTimeToLane(int laneIndex)
|
||||
@@ -2339,16 +2350,51 @@ namespace jdi
|
||||
return;
|
||||
}
|
||||
|
||||
UiFillRect(g, m_area, m_theme->plotBg);
|
||||
|
||||
BuildLaneVisuals();
|
||||
|
||||
if (m_lanes.empty())
|
||||
{
|
||||
InvalidateSceneCache();
|
||||
PaintEmptyState(g);
|
||||
return;
|
||||
}
|
||||
|
||||
// 拖拽期间的静态场景复用:拖动信号曲线 / 时间光标时视图与数据都没有变化,
|
||||
// 每帧重绘百万级采样的包络纯属浪费(实测 5 条密集信号约 100ms/帧)。
|
||||
// 这里命中缓存时只贴图 + 重绘随鼠标变化的部分,使拖拽帧率不随数据量下降。
|
||||
const bool sceneCacheable =
|
||||
(m_dragMode == PlotDragMode::MOVE_SIGNAL || m_dragMode == PlotDragMode::CURSOR_MOVE);
|
||||
|
||||
if (sceneCacheable && m_sceneValid)
|
||||
{
|
||||
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))
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
const double viewMin = Tab().ViewMin();
|
||||
const double viewMax = Tab().ViewMax();
|
||||
const double span = viewMax - viewMin;
|
||||
@@ -2356,6 +2402,9 @@ namespace jdi
|
||||
std::vector<double> timeTicks;
|
||||
BuildTicks(viewMin, viewMax, timeStep, timeTicks);
|
||||
|
||||
// 绘图区整体底色(缓存路径下该内容已在离屏位图中)。
|
||||
UiFillRect(g, m_area, m_theme->plotBg);
|
||||
|
||||
// 波形区域底色。
|
||||
UiFillRect(g, m_plotRect, m_theme->cardBg);
|
||||
|
||||
@@ -2368,6 +2417,19 @@ namespace jdi
|
||||
}
|
||||
|
||||
PaintTimeAxis(g, timeTicks, timeStep);
|
||||
|
||||
// 此刻画布上正好是"绘图区底色 + 全部网格 + 时间轴",尚未绘制任何随鼠标变化的内容,
|
||||
// 因此是捕获静态场景缓存的最佳时点(必须在裁剪区已全部恢复之后执行)。
|
||||
if (sceneCacheable)
|
||||
{
|
||||
CaptureSceneCache(g);
|
||||
}
|
||||
else
|
||||
{
|
||||
InvalidateSceneCache();
|
||||
}
|
||||
|
||||
PaintLaneReadouts(g);
|
||||
PaintRubberBand(g);
|
||||
PaintCursor(g, m_lanes);
|
||||
PaintSignalInteraction(g);
|
||||
@@ -2385,6 +2447,178 @@ namespace jdi
|
||||
g.DrawRectangle(&borderPen, m_plotRect.X, m_plotRect.Y, m_plotRect.Width, m_plotRect.Height);
|
||||
}
|
||||
|
||||
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();
|
||||
key.laneCount = static_cast<int>(m_lanes.size());
|
||||
key.areaX = m_area.X;
|
||||
key.areaY = m_area.Y;
|
||||
key.areaW = m_area.Width;
|
||||
key.areaH = m_area.Height;
|
||||
key.scale = m_scale;
|
||||
return key;
|
||||
}
|
||||
|
||||
bool PlotView::EnsureSceneCache(int width, int height)
|
||||
{
|
||||
if (width <= 0 || height <= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (m_sceneDc != nullptr && m_sceneWidth >= width && m_sceneHeight >= height)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
ReleaseSceneCache();
|
||||
|
||||
HDC screen = GetDC(nullptr);
|
||||
if (screen == nullptr)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
m_sceneDc = CreateCompatibleDC(screen);
|
||||
m_sceneBitmap = CreateCompatibleBitmap(screen, width, height);
|
||||
ReleaseDC(nullptr, screen);
|
||||
|
||||
if (m_sceneDc == nullptr || m_sceneBitmap == nullptr)
|
||||
{
|
||||
ReleaseSceneCache();
|
||||
return false;
|
||||
}
|
||||
m_sceneOldBitmap = SelectObject(m_sceneDc, m_sceneBitmap);
|
||||
m_sceneWidth = width;
|
||||
m_sceneHeight = height;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PlotView::CaptureSceneCache(gd::Graphics& g)
|
||||
{
|
||||
// 缓存覆盖**整个客户区**:拖拽期间工具栏/标签栏/面板也同样不变,
|
||||
// 宿主(Application)据此跳过这些区域的重复绘制(实测面板绘制就占 8ms/帧)。
|
||||
int clientWidth = 0;
|
||||
int clientHeight = 0;
|
||||
if (m_host != nullptr)
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
if (!EnsureSceneCache(clientWidth, clientHeight))
|
||||
{
|
||||
m_sceneValid = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
HDC source = g.GetHDC();
|
||||
if (source == nullptr)
|
||||
{
|
||||
m_sceneValid = false;
|
||||
return false;
|
||||
}
|
||||
const BOOL ok = BitBlt(m_sceneDc, 0, 0, clientWidth, clientHeight, source, m_sceneX, m_sceneY,
|
||||
SRCCOPY);
|
||||
g.ReleaseHDC(source);
|
||||
|
||||
if (ok == FALSE)
|
||||
{
|
||||
m_sceneValid = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
m_sceneKey = MakeSceneCacheKey();
|
||||
m_sceneValid = true;
|
||||
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)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
HDC target = g.GetHDC();
|
||||
if (target == nullptr)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
const BOOL ok = BitBlt(target, m_sceneX, m_sceneY, m_sceneWidth, m_sceneHeight, m_sceneDc, 0, 0,
|
||||
SRCCOPY);
|
||||
g.ReleaseHDC(target);
|
||||
return ok != FALSE;
|
||||
}
|
||||
|
||||
void PlotView::InvalidateSceneCache()
|
||||
{
|
||||
m_sceneValid = false;
|
||||
}
|
||||
|
||||
void PlotView::ReleaseSceneCache()
|
||||
{
|
||||
m_sceneValid = false;
|
||||
if (m_sceneDc != nullptr)
|
||||
{
|
||||
if (m_sceneOldBitmap != nullptr)
|
||||
{
|
||||
SelectObject(m_sceneDc, m_sceneOldBitmap);
|
||||
m_sceneOldBitmap = nullptr;
|
||||
}
|
||||
DeleteDC(m_sceneDc);
|
||||
m_sceneDc = nullptr;
|
||||
}
|
||||
if (m_sceneBitmap != nullptr)
|
||||
{
|
||||
DeleteObject(m_sceneBitmap);
|
||||
m_sceneBitmap = nullptr;
|
||||
}
|
||||
m_sceneWidth = 0;
|
||||
m_sceneHeight = 0;
|
||||
}
|
||||
|
||||
void PlotView::PaintRubberBand(gd::Graphics& g)
|
||||
{
|
||||
if (!m_dragMoved || m_rubberRect.Width <= 0.0f || m_rubberRect.Height <= 0.0f)
|
||||
@@ -2616,9 +2850,11 @@ namespace jdi
|
||||
std::max<std::size_t>(1, points.size() / kMaxScatterPoints);
|
||||
// 所有散点合并到一条 GraphicsPath 后只调用一次 FillPath:
|
||||
// 逐点 FillEllipse 每次都要完整走一遍 GDI+ 的状态检查与光栅化入口,
|
||||
// 实测 2500 点需要约 51ms;合并后约 1/9(实测基准 63ms → 7ms),
|
||||
// 且仍是抗锯齿圆形,观感与逐点绘制完全一致。
|
||||
// 实测 2500 点需要约 51ms;合并后约 1/9(实测基准 63ms → 7ms)。
|
||||
// **必须显式设为缠绕填充规则**:GraphicsPath 默认是交替(奇偶)规则,
|
||||
// 两个重叠的圆在交集处会互相抵消形成空洞,表现为"数据点重叠处出现异常白边"。
|
||||
gd::GraphicsPath scatterPath;
|
||||
scatterPath.SetFillMode(gd::FillModeWinding);
|
||||
for (std::size_t k = 0; k < points.size(); k += scatterStride)
|
||||
{
|
||||
scatterPath.AddEllipse(points[k].X - radius, points[k].Y - radius, radius * 2.0f,
|
||||
@@ -2705,15 +2941,43 @@ namespace jdi
|
||||
if (lane.rect.Height >= 24.0f * m_scale)
|
||||
{
|
||||
PaintLaneLegend(g, laneIndex, lane, clipTop, clipBottom);
|
||||
if (Tab().CursorEnabled() && lane.rect.Height >= 40.0f * m_scale)
|
||||
{
|
||||
PaintLaneReadout(g, laneIndex, lane, clipTop, clipBottom);
|
||||
}
|
||||
}
|
||||
|
||||
g.Restore(saveState);
|
||||
}
|
||||
|
||||
void PlotView::PaintLaneReadouts(gd::Graphics& g)
|
||||
{
|
||||
// 读数框随时间光标实时变化,因此**不能**进入拖拽静态场景缓存,
|
||||
// 必须每帧单独绘制(否则拖动光标时读数会停在旧值上)。
|
||||
if (m_tab == nullptr || !Tab().CursorEnabled())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (std::size_t index = 0; index < m_lanes.size(); ++index)
|
||||
{
|
||||
const LaneVisual& lane = m_lanes[index];
|
||||
if (lane.rect.Height < 24.0f * m_scale || lane.rect.Height < 40.0f * m_scale)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
const float clipTop = std::max(lane.rect.Y, m_plotRect.Y);
|
||||
const float clipBottom =
|
||||
std::min(lane.rect.Y + lane.rect.Height, m_plotRect.Y + m_plotRect.Height);
|
||||
if (clipBottom <= clipTop)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
const int saveState = g.Save();
|
||||
g.SetClip(gd::RectF(m_area.X, clipTop, m_area.Width, clipBottom - clipTop));
|
||||
PaintLaneReadout(g, static_cast<int>(index), lane, clipTop, clipBottom);
|
||||
g.Restore(saveState);
|
||||
}
|
||||
}
|
||||
|
||||
SignalRef PlotView::LegendAtPoint(int x, int y) const
|
||||
{
|
||||
const float fx = static_cast<float>(x);
|
||||
@@ -3176,15 +3440,18 @@ namespace jdi
|
||||
g.SetClip(gd::RectF(m_plotRect.X, m_plotRect.Y, m_plotRect.Width, m_plotRect.Height),
|
||||
gd::CombineModeIntersect);
|
||||
|
||||
std::vector<gd::PointF> points;
|
||||
std::vector<std::size_t> breaks;
|
||||
BuildPolyline(resolved, sourceVisual, points, breaks);
|
||||
|
||||
for (gd::PointF& point : points)
|
||||
// 虚影的几何与源网格里的信号完全一致,直接复用折线顶点缓存,
|
||||
// 再用平移变换让整体跟随鼠标:避免每帧重新构建并逐点搬运上万顶点。
|
||||
const PolylineCacheEntry* ghostSource = AcquirePolyline(m_dragSignal, resolved, sourceVisual);
|
||||
if (ghostSource == nullptr)
|
||||
{
|
||||
point.X += offsetX;
|
||||
point.Y += offsetY;
|
||||
g.Restore(saveState);
|
||||
return;
|
||||
}
|
||||
const std::vector<gd::PointF>& points = ghostSource->points;
|
||||
const std::vector<std::size_t>& breaks = ghostSource->breaks;
|
||||
|
||||
g.TranslateTransform(offsetX, offsetY);
|
||||
|
||||
const DrawMode mode = resolved.style->mode;
|
||||
const bool drawLine = (mode != DrawMode::SCATTER);
|
||||
@@ -3223,11 +3490,18 @@ namespace jdi
|
||||
m_scale);
|
||||
const std::size_t scatterStride =
|
||||
std::max<std::size_t>(1, points.size() / kMaxScatterPoints);
|
||||
gd::SolidBrush brush(ToGdiColor(ghostColor));
|
||||
// 与主绘制一致:合并成一条路径一次填充,并显式使用缠绕规则避免重叠处出现空洞。
|
||||
gd::GraphicsPath ghostPath;
|
||||
ghostPath.SetFillMode(gd::FillModeWinding);
|
||||
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);
|
||||
ghostPath.AddEllipse(points[k].X - radius, points[k].Y - radius, radius * 2.0f,
|
||||
radius * 2.0f);
|
||||
}
|
||||
gd::SolidBrush brush(ToGdiColor(ghostColor));
|
||||
if (ghostPath.GetPointCount() > 0)
|
||||
{
|
||||
g.FillPath(&brush, &ghostPath);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -92,7 +92,7 @@ namespace jdi
|
||||
std::uint32_t color = 0xFF268BD2; /**< 波形颜色,格式 0xAARRGGBB。 */
|
||||
std::uint32_t customColor = 0xFF6C71C4; /**< 自定义颜色记忆值(切换预设色后仍保留)。 */
|
||||
float opacity = 1.0f; /**< 不透明度,取值 0.0-1.0,默认 1.0。 */
|
||||
float lineWidth = 1.6f; /**< 线宽(逻辑像素,未乘 DPI 缩放)。 */
|
||||
float lineWidth = 1.0f; /**< 线宽(逻辑像素,未乘 DPI 缩放)。 */
|
||||
DrawMode mode = DrawMode::LINE; /**< 绘制方式。 */
|
||||
};
|
||||
|
||||
@@ -549,6 +549,8 @@ namespace jdi
|
||||
bool& ViewInitialized() { return m_viewInitialized; }
|
||||
/// @brief 网格纵向滚动偏移量(可读写)。
|
||||
float& LaneScroll() { return m_laneScroll; }
|
||||
/// @brief 网格纵向滚动偏移量(只读)。
|
||||
float LaneScroll() const { return m_laneScroll; }
|
||||
/// @brief 时间光标是否启用(可读写)。
|
||||
bool& CursorEnabled() { return m_cursorEnabled; }
|
||||
/// @brief 时间光标是否启用(只读)。
|
||||
|
||||
@@ -432,7 +432,7 @@ namespace jdi
|
||||
: 0xFF268BD2u;
|
||||
signal.style.customColor = signal.style.color;
|
||||
signal.style.opacity = 1.0f;
|
||||
signal.style.lineWidth = 1.6f;
|
||||
signal.style.lineWidth = 1.0f;
|
||||
signal.style.mode = DrawMode::LINE;
|
||||
}
|
||||
|
||||
|
||||
@@ -86,7 +86,7 @@ namespace jdi
|
||||
}
|
||||
else if (item.kind == MenuItemKind::HEADER)
|
||||
{
|
||||
const float width = (kPadH + 8.0f) * m_scale +
|
||||
const float width = (kPadH + kMarkerColumn) * m_scale +
|
||||
UiMeasureTextWidth(m_fonts->uiSmallBold, item.label) + kRightPad * m_scale;
|
||||
contentWidth = std::max(contentWidth, width);
|
||||
}
|
||||
@@ -102,7 +102,7 @@ namespace jdi
|
||||
const float fieldWidth =
|
||||
std::max(kEditFieldWidth * m_scale,
|
||||
UiMeasureTextWidth(m_fonts->uiSmall, item.value) + 20.0f * m_scale);
|
||||
const float width = (kPadH + 8.0f) * m_scale +
|
||||
const float width = (kPadH + kMarkerColumn) * m_scale +
|
||||
UiMeasureTextWidth(m_fonts->uiSmall, item.label) + kEditFieldGap * m_scale +
|
||||
fieldWidth + kRightPad * m_scale;
|
||||
contentWidth = std::max(contentWidth, width);
|
||||
@@ -753,7 +753,7 @@ namespace jdi
|
||||
if (item.kind == MenuItemKind::HEADER)
|
||||
{
|
||||
UiText(g, item.label, m_fonts->uiSmallBold,
|
||||
gd::RectF(rect.X + 8.0f * m_scale, rect.Y, rect.Width, rect.Height),
|
||||
gd::RectF(rect.X + kMarkerColumn * m_scale, rect.Y, rect.Width, rect.Height),
|
||||
m_theme->textMuted, TextAlign::LEFT);
|
||||
continue;
|
||||
}
|
||||
@@ -771,7 +771,8 @@ namespace jdi
|
||||
}
|
||||
|
||||
const std::uint32_t labelColor = item.enabled ? m_theme->text : m_theme->textMuted;
|
||||
const float labelLeft = rect.X + 8.0f * m_scale;
|
||||
// 说明文字与普通项使用同一文字列(kMarkerColumn),保证菜单内所有文本左对齐。
|
||||
const float labelLeft = rect.X + kMarkerColumn * m_scale;
|
||||
const gd::RectF fieldRect = EditFieldRect(static_cast<int>(index));
|
||||
const float labelWidth = std::max(8.0f, fieldRect.X - labelLeft - kEditFieldGap * m_scale);
|
||||
UiText(g, UiEllipsize(g, item.label, m_fonts->uiSmall, labelWidth), m_fonts->uiSmall,
|
||||
|
||||
Reference in new issue
Block a user