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

+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())