调色性能优化 + 新增色域方块拾色 + 取值胶囊高对比配色 + 数据点标记修复
1. 修复自定义颜色滑动卡顿(VERY_BIG_DATA 全信号可见时 36ms/步 → 约 12ms/步)
- 根因:场景缓存的内容指纹把逐信号的颜色/不透明度/线宽/模式都算了进去,
改一次颜色就整幅失效,于是所有网格的波形都要重新光栅化。
- 新增"不含外观样式"的几何指纹与样式快照:只有在网格结构、视图几何、信号集合与
文字都不变、仅外观样式变化时,走"局部重画"快路径 —— 只重画颜色变了的那个网格,
再把结果写回场景缓存(下一帧回到纯贴图)。
- 补丁矩形向内对齐到整数像素,避免把属于相邻网格的一行擦掉而留下 1px 接缝;
底色填充按补丁矩形本身进行(大面积不透明填充会走 GDI 快路径,不受 GDI+ 裁剪约束)。
- 该优化对调色板点击、线宽/不透明度滑块、绘制模式切换同样生效。
2. 自定义颜色新增"方块色域图"拾色
- 新增饱和度-明度方块(横轴饱和度、纵轴明度)与纵向色相条,可直接在色域图上取色。
- 与面板内其它控件完全联动:RGB 滑块、十六进制输入、预览色块同源(RGB 为唯一数据源),
并记忆色相,消色差时仍可用色相条换回颜色。
- 色域方块按当前色相预渲染缓存,色相不变时不重复绘制;色域内用十字光标、色相条用纵向光标。
3. 取值胶囊的对比度描边与文本色改为对信号配色线性响应
- 原实现只有一个亮度阈值,文字/描边只有黑、白两套固定取值。现按底色亮度选定亮/暗一侧,
再与底色的互补色按固定比例线性混合(互补色亮度恰为 1−底色亮度),
因此文字与描边的亮度随信号配色线性变化,且任意底色下都保持高对比。
4. 修复光标数据点越界三角的判定与绘制
- 是否改用三角,以"数据点本身是否超出绘制边界"为准,不再因为数值标注被贴边而改变形状。
- 三角方向修正为尖端朝外(此前上越界画下三角、左越界画右三角,方向完全相反),
内三角改按质心收缩,描边不再是歪的;上下越界时三角与显示的数值纵向居中对齐。
This commit is contained in:
1 parent
d3c83237f1
commit
40b12d6e8b
7 files changed
+877
-124
No files matched your search
@@ -835,12 +835,13 @@ namespace jdi
|
||||
bool IsOnCursorSpanLabel(int x, int y) const;
|
||||
|
||||
/**
|
||||
* @brief 画文字时给底色胶囊加一层与文字反色的细描边,提升与背景/曲线的对比度。
|
||||
* @brief 画文字时给底色胶囊加一层细描边,提升与背景/曲线的对比度。
|
||||
* @param [IN] rect 胶囊矩形。
|
||||
* @param [IN] radius 圆角半径。
|
||||
* @param [IN] textDark 文字是否用深色。
|
||||
* @param [IN] outlineColor 描边色(由 `LabelInkFor` 与文字色一并算出的反相色)。
|
||||
*/
|
||||
void DrawLabelOutline(gd::Graphics& g, const gd::RectF& rect, float radius, bool textDark);
|
||||
void DrawLabelOutline(gd::Graphics& g, const gd::RectF& rect, float radius,
|
||||
std::uint32_t outlineColor);
|
||||
|
||||
/**
|
||||
* @brief 精确检测鼠标位置是否命中某条可见信号曲线。
|
||||
@@ -1404,9 +1405,65 @@ namespace jdi
|
||||
* 视图几何以外的任何内容变化(改颜色/线宽/透明度/样式、重命名、增删信号)
|
||||
* 都会改变该指纹,从而让静态场景缓存自动失效,无需在各处手工调用失效接口。
|
||||
*
|
||||
* @param [IN] includeStyles 是否把逐信号外观样式(颜色/不透明度/线宽/模式)计入指纹。
|
||||
* @return 指纹值。
|
||||
*/
|
||||
std::uint64_t SceneContentHash() const;
|
||||
std::uint64_t SceneContentHash(bool includeStyles = true) const;
|
||||
|
||||
/**
|
||||
* @brief 场景内容中**不含逐信号外观样式**的指纹。
|
||||
*
|
||||
* 只包含网格结构、视图几何、网格幅值、信号集合与可见性、图例文案、选中项等
|
||||
* "变化后必须整幅重建"的内容;逐信号的 `color / opacity / lineWidth / mode`
|
||||
* 一律不参与。
|
||||
*
|
||||
* 用途:外观样式变化时(典型场景是拖动自定义颜色的滑块)波形几何完全不变,
|
||||
* 只要该指纹一致,就可以只重画"颜色变了的那个网格"再写回缓存,
|
||||
* 而不必整幅重绘全部网格(VERY_BIG_DATA 实测整幅 30ms+、单网格约 4ms)。
|
||||
*
|
||||
* @return 指纹值。
|
||||
*/
|
||||
std::uint64_t SceneGeometryHash() const;
|
||||
|
||||
/**
|
||||
* @brief 静态场景缓存中"网格内信号外观样式"的快照条目。
|
||||
*/
|
||||
struct SceneStyleEntry
|
||||
{
|
||||
std::uint32_t color = 0; /**< 信号色(未叠加不透明度)。 */
|
||||
float opacity = 1.0f; /**< 不透明度。 */
|
||||
float lineWidth = 1.0f; /**< 线宽。 */
|
||||
int mode = 0; /**< 绘制模式。 */
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief 采集当前各网格内可见信号的外观样式快照。
|
||||
* @param [OUT] out 采集结果(按网格分组)。
|
||||
*/
|
||||
void CollectSceneStyles(std::vector<std::vector<SceneStyleEntry>>& out) const;
|
||||
|
||||
/**
|
||||
* @brief 判断当前相对缓存是否"只有外观样式发生了变化"。
|
||||
*
|
||||
* 成立条件:`SceneGeometryHash()` 与缓存一致(网格结构、视图几何、信号集合
|
||||
* 与文字都没变),且至少有一个网格内信号的外观样式不同。
|
||||
*
|
||||
* @param [OUT] lanes 外观发生变化的网格下标(升序)。
|
||||
* @return 可以走"局部重画"快路径返回 true。
|
||||
*/
|
||||
bool FindStyleOnlyLanes(std::vector<int>& lanes) const;
|
||||
|
||||
/**
|
||||
* @brief 只重画指定网格并把结果写回静态场景缓存。
|
||||
*
|
||||
* 必须在 `BlitSceneCache()` 之后调用:贴图已经完成,这里把受影响网格的
|
||||
* "底色 + 网格线 + 波形 + 标尺 + 图例"重画到画布上,再整块拷回缓存位图,
|
||||
* 使缓存内容与当前样式一致(下一帧即可重新走"完全命中"快路径)。
|
||||
*
|
||||
* @param [IN,OUT] g 绘图上下文。
|
||||
* @param [IN] lanes 需要重画的网格下标。
|
||||
*/
|
||||
void RepaintSceneCachePatch(gd::Graphics& g, const std::vector<int>& lanes);
|
||||
|
||||
/**
|
||||
* @brief 当前应当绘入静态场景缓存的信号光晕(仅拖拽中的信号)。
|
||||
@@ -1485,6 +1542,10 @@ namespace jdi
|
||||
int m_sceneY = 0; /**< 缓存对应的客户区纵坐标(绘图区上边界)。 */
|
||||
bool m_sceneValid = false; /**< 缓存内容是否有效。 */
|
||||
SceneCacheKey m_sceneKey; /**< 缓存对应的场景参数。 */
|
||||
/** 缓存捕获时"不含逐信号外观样式"的内容指纹(用于识别仅样式变化)。 */
|
||||
std::uint64_t m_sceneGeometryHash = 0;
|
||||
/** 缓存捕获时各网格内信号的外观样式快照(按网格分组,与 m_sceneStyles 同序)。 */
|
||||
std::vector<std::vector<SceneStyleEntry>> m_sceneStyles;
|
||||
|
||||
/**
|
||||
* @brief 查询坐标命中的图例条目。
|
||||
|
||||
@@ -29,6 +29,8 @@ namespace jdi
|
||||
const size_t kMinPolylinePoints = 512; /**< 折线最少顶点数(窗口很窄时仍保留细节)。 */
|
||||
const float kEnvelopeMinPixelSpan = 0.5f; /**< 包络纵向跨度小于该像素值时合并为单个顶点。 */
|
||||
const float kCursorHandleReserve = 9.0f; /**< 光标顶部三角把手占用的高度(数据点贴边时需避让)。 */
|
||||
const float kMarkerEdgeInset = 7.0f; /**< 数据点越界贴边时距网格可见边界的间距。 */
|
||||
const float kMarkerInsideMargin = 2.0f; /**< 数据点在视界内时允许贴到边界的最小余量。 */
|
||||
const size_t kMaxScatterPoints = 2500; /**< 单条散点最大绘制点数。 */
|
||||
const size_t kPolylineCacheLimit = 64; /**< 折线顶点缓存条目上限。 */
|
||||
const float kMinScatterRadius = 1.6f; /**< 散点最小半径(逻辑像素)。 */
|
||||
@@ -407,19 +409,6 @@ namespace jdi
|
||||
return (argb & 0x00FFFFFFu) | (a << 24);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 判断颜色是否偏暗(用于在信号色底板上选择可读的文字颜色)。
|
||||
* @param [IN] color 0xAARRGGBB 颜色值。
|
||||
* @return 相对亮度低于阈值返回 true。
|
||||
*/
|
||||
bool IsDarkColor(std::uint32_t color)
|
||||
{
|
||||
const double r = static_cast<double>((color >> 16) & 0xFFu);
|
||||
const double g = static_cast<double>((color >> 8) & 0xFFu);
|
||||
const double b = static_cast<double>(color & 0xFFu);
|
||||
return (0.299 * r + 0.587 * g + 0.114 * b) < 140.0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 将数值限制到合法区间。
|
||||
* @param [IN] value 输入值。
|
||||
@@ -4234,12 +4223,9 @@ namespace jdi
|
||||
return;
|
||||
}
|
||||
|
||||
// 静态场景复用(不再局限于拖拽):只要视图几何、信号集合与显示样式都没变,
|
||||
// "绘图区底色 + 全部网格 + 时间轴"就逐像素相同,直接贴图即可。
|
||||
// 大文件下整幅重绘约 80ms,贴图约 1ms —— 鼠标悬停、拖拽选区这些
|
||||
// "只改一小块叠加层"的交互因此不再重绘波形。
|
||||
// 随鼠标变化的内容(悬停高亮、读数框、选区、光标、拖拽虚影、输入框)一律画在贴图之后。
|
||||
if (SceneCacheMatches() && BlitSceneCache(g))
|
||||
// 贴图之后、静态场景之外的全部内容(随鼠标变化或位于最上层)。
|
||||
// 三个分支(完全命中 / 局部重画 / 整幅重绘)共用同一段,保证观感完全一致。
|
||||
const auto paintOverlays = [this, &g]()
|
||||
{
|
||||
PaintHoverHighlight(g);
|
||||
PaintLegendHighlight(g);
|
||||
@@ -4248,6 +4234,7 @@ namespace jdi
|
||||
PaintEdgeCursor(g, m_lanes);
|
||||
PaintSignalInteraction(g);
|
||||
PaintHint(g);
|
||||
// 网格 Y 轴上下限的输入框绘制在最上层。
|
||||
if (m_editTarget == PlotEditTarget::LANE_Y_MAX ||
|
||||
m_editTarget == PlotEditTarget::LANE_Y_MIN)
|
||||
{
|
||||
@@ -4258,9 +4245,32 @@ namespace jdi
|
||||
// 绘图区外框(比网格内部分界线更重,形成明确整体边界)。
|
||||
gd::Pen borderPen(ToGdiColor(m_theme->axisLine), 1.6f);
|
||||
g.DrawRectangle(&borderPen, m_plotRect.X, m_plotRect.Y, m_plotRect.Width, m_plotRect.Height);
|
||||
};
|
||||
|
||||
// 静态场景复用(不再局限于拖拽):只要视图几何、信号集合与显示样式都没变,
|
||||
// "绘图区底色 + 全部网格 + 时间轴"就逐像素相同,直接贴图即可。
|
||||
// 大文件下整幅重绘约 80ms,贴图约 1ms —— 鼠标悬停、拖拽选区这些
|
||||
// "只改一小块叠加层"的交互因此不再重绘波形。
|
||||
// 随鼠标变化的内容(悬停高亮、读数框、选区、光标、拖拽虚影、输入框)一律画在贴图之后。
|
||||
if (SceneCacheMatches() && BlitSceneCache(g))
|
||||
{
|
||||
paintOverlays();
|
||||
return;
|
||||
}
|
||||
|
||||
// 只有"外观样式"变化(典型场景:拖动自定义颜色滑块实时调色):网格结构、视图几何、
|
||||
// 波形采样都没变,只是颜色/不透明度/线宽/模式不同 —— 此时只重画受影响的网格,
|
||||
// 再写回缓存。(VERY_BIG_DATA 实测:整幅重绘 30ms+,单网格重画约 4ms。)
|
||||
{
|
||||
std::vector<int> patchLanes;
|
||||
if (FindStyleOnlyLanes(patchLanes) && BlitSceneCache(g))
|
||||
{
|
||||
RepaintSceneCachePatch(g, patchLanes);
|
||||
paintOverlays();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const double viewMin = Tab().ViewMin();
|
||||
const double viewMax = Tab().ViewMax();
|
||||
const double span = viewMax - viewMin;
|
||||
@@ -4288,25 +4298,7 @@ namespace jdi
|
||||
// 因此是捕获静态场景缓存的最佳时点(必须在裁剪区已全部恢复之后执行)。
|
||||
CaptureSceneCache(g);
|
||||
|
||||
PaintHoverHighlight(g);
|
||||
PaintLegendHighlight(g);
|
||||
PaintRubberBand(g);
|
||||
PaintCursor(g, m_lanes);
|
||||
PaintEdgeCursor(g, m_lanes);
|
||||
PaintSignalInteraction(g);
|
||||
PaintHint(g);
|
||||
|
||||
// 网格 Y 轴上下限的输入框绘制在最上层。
|
||||
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);
|
||||
paintOverlays();
|
||||
}
|
||||
|
||||
void PlotView::PaintSignalHalo(gd::Graphics& g, const ResolvedSignal& resolved,
|
||||
@@ -4479,11 +4471,13 @@ namespace jdi
|
||||
return SignalRef();
|
||||
}
|
||||
|
||||
std::uint64_t PlotView::SceneContentHash() const
|
||||
std::uint64_t PlotView::SceneContentHash(bool includeStyles) const
|
||||
{
|
||||
// FNV-1a:把“视图几何之外的一切影响绘图区像素的内容”折叠成一个指纹。
|
||||
// 这样颜色/线宽/透明度/样式/重命名/信号增删等变化都会自动让缓存失效,
|
||||
// 不需要在各处手工调用 InvalidateSceneCache(容易漏)。
|
||||
// `includeStyles == false` 时跳过逐信号的外观样式(颜色/不透明度/线宽/模式),
|
||||
// 得到的就是几何指纹:它一致说明“只有外观样式变了”。
|
||||
std::uint64_t hash = 1469598103934665603ull;
|
||||
const auto mix = [&hash](std::uint64_t value)
|
||||
{
|
||||
@@ -4547,10 +4541,16 @@ namespace jdi
|
||||
{
|
||||
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));
|
||||
// 以下四项是“只影响外观”的样式:仅在 includeStyles 时计入指纹。
|
||||
// (不参与时得到的就是 SceneGeometryHash:它一致说明只有外观变了,
|
||||
// 于是可以只重画颜色变了的网格,而不必整幅重绘。)
|
||||
if (includeStyles)
|
||||
{
|
||||
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)
|
||||
@@ -4562,6 +4562,190 @@ namespace jdi
|
||||
return hash;
|
||||
}
|
||||
|
||||
std::uint64_t PlotView::SceneGeometryHash() const
|
||||
{
|
||||
return SceneContentHash(false);
|
||||
}
|
||||
|
||||
void PlotView::CollectSceneStyles(std::vector<std::vector<SceneStyleEntry>>& out) const
|
||||
{
|
||||
out.clear();
|
||||
out.resize(m_lanes.size());
|
||||
for (std::size_t laneIndex = 0; laneIndex < m_lanes.size(); ++laneIndex)
|
||||
{
|
||||
const std::vector<SignalRef>& signals = Tab().Lanes()[laneIndex].signals;
|
||||
std::vector<SceneStyleEntry>& lane = out[laneIndex];
|
||||
lane.reserve(signals.size());
|
||||
for (const SignalRef& ref : signals)
|
||||
{
|
||||
const ResolvedSignal resolved = ResolveSignal(ref);
|
||||
SceneStyleEntry entry;
|
||||
if (resolved.style != nullptr)
|
||||
{
|
||||
entry.color = resolved.style->color;
|
||||
entry.opacity = resolved.style->opacity;
|
||||
entry.lineWidth = resolved.style->lineWidth;
|
||||
entry.mode = static_cast<int>(resolved.style->mode);
|
||||
}
|
||||
lane.push_back(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool PlotView::FindStyleOnlyLanes(std::vector<int>& lanes) const
|
||||
{
|
||||
lanes.clear();
|
||||
if (!m_sceneValid || m_sceneGeometryHash != SceneGeometryHash())
|
||||
{
|
||||
// 网格结构 / 视图几何 / 信号集合 / 图例文案有变化:必须整幅重绘。
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<std::vector<SceneStyleEntry>> current;
|
||||
CollectSceneStyles(current);
|
||||
if (current.size() != m_sceneStyles.size())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
for (std::size_t laneIndex = 0; laneIndex < current.size(); ++laneIndex)
|
||||
{
|
||||
const std::vector<SceneStyleEntry>& now = current[laneIndex];
|
||||
const std::vector<SceneStyleEntry>& before = m_sceneStyles[laneIndex];
|
||||
if (now.size() != before.size())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
bool differs = false;
|
||||
for (std::size_t index = 0; index < now.size() && !differs; ++index)
|
||||
{
|
||||
differs = (now[index].color != before[index].color) ||
|
||||
(now[index].opacity != before[index].opacity) ||
|
||||
(now[index].lineWidth != before[index].lineWidth) ||
|
||||
(now[index].mode != before[index].mode);
|
||||
}
|
||||
if (differs)
|
||||
{
|
||||
lanes.push_back(static_cast<int>(laneIndex));
|
||||
}
|
||||
}
|
||||
return !lanes.empty();
|
||||
}
|
||||
|
||||
void PlotView::RepaintSceneCachePatch(gd::Graphics& g, const std::vector<int>& lanes)
|
||||
{
|
||||
if (lanes.empty() || m_sceneDc == nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// 重画范围 = 受影响网格与绘图区的交集(并且不超出缓存区域)。
|
||||
float top = 1.0e9f;
|
||||
float bottom = -1.0e9f;
|
||||
for (const int laneIndex : lanes)
|
||||
{
|
||||
if (laneIndex < 0 || laneIndex >= static_cast<int>(m_lanes.size()))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const gd::RectF& rect = m_lanes[static_cast<std::size_t>(laneIndex)].rect;
|
||||
top = std::min(top, rect.Y);
|
||||
bottom = std::max(bottom, rect.Y + rect.Height);
|
||||
}
|
||||
if (bottom <= top)
|
||||
{
|
||||
return;
|
||||
}
|
||||
top = std::max(top, m_plotRect.Y);
|
||||
bottom = std::min(bottom, m_plotRect.Y + m_plotRect.Height);
|
||||
if (bottom <= top)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// ⚠️ 补丁矩形必须**向内**对齐到整数像素(ceil 上边界、floor 下边界):
|
||||
// 网格矩形是浮点数,而 GDI `FillRect`/`BitBlt` 会按 floor/ceil 向外取整,
|
||||
// "向外"会多擦掉/多写回一整行,那一行属于相邻网格(例如它的分界线),
|
||||
// 于是补丁边界上会留下一像素接缝。向内对齐后,边界外的行一律由缓存提供
|
||||
// (缓存内容与整幅重绘逐像素一致,已由对照帧验证)。
|
||||
const int x0 = std::max(static_cast<int>(std::ceil(m_area.X)), static_cast<int>(std::ceil(m_area.X)));
|
||||
const int y0 = std::max(static_cast<int>(std::ceil(top)), static_cast<int>(std::ceil(m_area.Y)));
|
||||
const int x1 = std::min(static_cast<int>(std::floor(m_area.X + m_area.Width)),
|
||||
static_cast<int>(std::floor(m_area.X + m_area.Width)));
|
||||
const int y1 = std::min(static_cast<int>(std::floor(bottom)),
|
||||
static_cast<int>(std::floor(m_area.Y + m_area.Height)));
|
||||
if (x1 <= x0 || y1 <= y0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const gd::RectF patch(static_cast<float>(x0), static_cast<float>(y0),
|
||||
static_cast<float>(x1 - x0), static_cast<float>(y1 - y0));
|
||||
|
||||
// 补丁区域必须被**完整**重画:凡是与它相交的网格都要重画一遍
|
||||
// (补丁已向内对齐,正常只会命中受影响的那几个网格)。
|
||||
std::vector<int> repaint;
|
||||
for (std::size_t laneIndex = 0; laneIndex < m_lanes.size(); ++laneIndex)
|
||||
{
|
||||
const gd::RectF& rect = m_lanes[laneIndex].rect;
|
||||
if (rect.Y < static_cast<float>(y1) && rect.Y + rect.Height > static_cast<float>(y0))
|
||||
{
|
||||
repaint.push_back(static_cast<int>(laneIndex));
|
||||
}
|
||||
}
|
||||
if (repaint.empty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// 与整幅重绘保持完全相同的绘制顺序:底色 → 逐网格内容(PaintLane 会与当前裁剪区求交)。
|
||||
{
|
||||
const int saveState = g.Save();
|
||||
g.SetClip(patch, gd::CombineModeIntersect);
|
||||
// ⚠️ 底色填充必须**按补丁矩形本身**填充,不能用 m_area / m_plotRect:
|
||||
// `UiFillRect` 对大面积不透明矩形会走 GDI `FillRect` 快路径,而 GDI 绘制
|
||||
// **不受 GDI+ 裁剪区约束**,会把已经贴好的缓存内容整片覆盖掉。
|
||||
UiFillRect(g, patch, m_theme->plotBg);
|
||||
const float cardTop = std::max(patch.Y, m_plotRect.Y);
|
||||
const float cardBottom = std::min(patch.Y + patch.Height, m_plotRect.Y + m_plotRect.Height);
|
||||
if (cardBottom > cardTop)
|
||||
{
|
||||
UiFillRect(g, gd::RectF(patch.X, cardTop, patch.Width, cardBottom - cardTop),
|
||||
m_theme->cardBg);
|
||||
}
|
||||
|
||||
const double viewMin = Tab().ViewMin();
|
||||
const double viewMax = Tab().ViewMax();
|
||||
const double span = viewMax - viewMin;
|
||||
const double timeStep = NiceStep((span > 0.0) ? span : 1.0, kTimeTickTarget);
|
||||
std::vector<double> timeTicks;
|
||||
BuildTicks(viewMin, viewMax, timeStep, timeTicks);
|
||||
|
||||
for (const int laneIndex : repaint)
|
||||
{
|
||||
if (laneIndex >= 0 && laneIndex < static_cast<int>(m_lanes.size()))
|
||||
{
|
||||
PaintLane(g, laneIndex, m_lanes[static_cast<std::size_t>(laneIndex)], timeTicks);
|
||||
}
|
||||
}
|
||||
g.Restore(saveState);
|
||||
}
|
||||
|
||||
// 把重画结果写回缓存位图,使缓存与当前样式一致(下一帧即回到"完全命中"快路径)。
|
||||
{
|
||||
HDC target = g.GetHDC();
|
||||
if (target != nullptr)
|
||||
{
|
||||
BitBlt(m_sceneDc, x0 - m_sceneX, y0 - m_sceneY, x1 - x0, y1 - y0, target, x0, y0, SRCCOPY);
|
||||
g.ReleaseHDC(target);
|
||||
}
|
||||
}
|
||||
|
||||
m_sceneKey = MakeSceneCacheKey();
|
||||
m_sceneGeometryHash = SceneGeometryHash();
|
||||
CollectSceneStyles(m_sceneStyles);
|
||||
}
|
||||
|
||||
bool PlotView::EnsureSceneCache(int width, int height)
|
||||
{
|
||||
if (width <= 0 || height <= 0)
|
||||
@@ -4638,6 +4822,8 @@ namespace jdi
|
||||
}
|
||||
|
||||
m_sceneKey = MakeSceneCacheKey();
|
||||
m_sceneGeometryHash = SceneGeometryHash();
|
||||
CollectSceneStyles(m_sceneStyles);
|
||||
m_sceneValid = true;
|
||||
return true;
|
||||
}
|
||||
@@ -4663,6 +4849,8 @@ namespace jdi
|
||||
void PlotView::InvalidateSceneCache()
|
||||
{
|
||||
m_sceneValid = false;
|
||||
m_sceneStyles.clear();
|
||||
m_sceneGeometryHash = 0;
|
||||
}
|
||||
|
||||
void PlotView::ReleaseSceneCache()
|
||||
@@ -4793,7 +4981,11 @@ namespace jdi
|
||||
}
|
||||
|
||||
const int saveState = g.Save();
|
||||
g.SetClip(gd::RectF(m_area.X, clipTop, m_area.Width, clipBottom - clipTop));
|
||||
// 与当前裁剪区求交(而不是替换):局部重画(RepaintSceneCachePatch)时
|
||||
// 外层已经按“需要重画的网格范围”设过裁剪区,这里必须保留,否则会重画到
|
||||
// 其他网格上。整幅重绘时外层没有裁剪区,等同于原来的行为。
|
||||
g.SetClip(gd::RectF(m_area.X, clipTop, m_area.Width, clipBottom - clipTop),
|
||||
gd::CombineModeIntersect);
|
||||
|
||||
|
||||
// 幅值网格线(先画线,标尺稍后作为覆盖层绘制,避免被波形遮挡)。
|
||||
@@ -5383,12 +5575,12 @@ namespace jdi
|
||||
}
|
||||
|
||||
|
||||
void PlotView::DrawLabelOutline(gd::Graphics& g, const gd::RectF& rect, float radius, bool textDark)
|
||||
void PlotView::DrawLabelOutline(gd::Graphics& g, const gd::RectF& rect, float radius,
|
||||
std::uint32_t outlineColor)
|
||||
{
|
||||
// 用与文字同色的半透明细描边:深色文字 → 浅色描边,浅色文字 → 深色描边。
|
||||
// 描边色由 `UiContrastInk` 与文字色一起算出(恒与文字色反相、且随信号配色线性变化)。
|
||||
// 这样胶囊压在同类色背景或信号曲线上时仍有清晰的边界。
|
||||
const std::uint32_t outline = textDark ? 0x99FDF6E3u : 0x99073642u;
|
||||
UiDrawRoundRect(g, rect, radius, outline, 1.0f);
|
||||
UiDrawRoundRect(g, rect, radius, outlineColor, 1.0f);
|
||||
}
|
||||
|
||||
gd::RectF PlotView::CursorSpanRect(bool& insideAxis) const
|
||||
@@ -5549,16 +5741,18 @@ namespace jdi
|
||||
const float rawY = ValueToY(value, lane);
|
||||
// 上下越界的数据点不再被丢弃:与左右越界一致,贴边绘制指示方向的三角。
|
||||
// 上边界额外预留光标顶部把手的高度,避免数据点三角压住光标把手。
|
||||
const DataMarker marker =
|
||||
// 非 const:上下越界时下面还要把方向三角与取值标注纵向居中对齐。
|
||||
DataMarker marker =
|
||||
MakeDataMarker(rawX, rawY, pinX, clipTop, clipBottom, kCursorHandleReserve * m_scale);
|
||||
|
||||
const std::uint32_t signalColor = StyleColor(*resolved.style);
|
||||
|
||||
// 数据点:视界内为圆点,任一方向越界时改为贴边三角。
|
||||
// **先算好取值标注、再画数据点**:上下越界时的方向三角需要与标注
|
||||
// 纵向居中对齐,而标注的位置要经过避让处理才能最终确定。
|
||||
const float radius = 3.4f * m_scale;
|
||||
PaintDataMarker(g, marker, radius, signalColor, m_theme->cardBg);
|
||||
|
||||
// 取值标注:底色用信号色、文字用反色,并加一层细描边提升对比度。
|
||||
// 取值标注:底色用信号色,文字/描边用随信号配色线性相关的高对比度配色。
|
||||
std::wstring text = FormatValue(value);
|
||||
if (showDelta)
|
||||
{
|
||||
@@ -5593,11 +5787,18 @@ namespace jdi
|
||||
// 依次向下(再向上)避让已占用的标注,保证不会互相覆盖。
|
||||
PlaceCursorLabel(labelRect, labelTop, textHeight, clipTop, clipBottom);
|
||||
|
||||
// 上下越界的方向三角与最终落位的取值标注纵向居中对齐
|
||||
// (用户明确要求“超过上下视界时三角需要与显示的数值居中对齐”)。
|
||||
if (marker.offUp || marker.offDown)
|
||||
{
|
||||
marker.y = labelRect.Y + labelRect.Height * 0.5f;
|
||||
}
|
||||
PaintDataMarker(g, marker, radius, signalColor, m_theme->cardBg);
|
||||
|
||||
const UiInk ink = UiContrastInk(signalColor);
|
||||
UiFillRoundRect(g, labelRect, 3.0f * m_scale, WithAlpha(signalColor, 0xF0));
|
||||
const bool dark = IsDarkColor(signalColor);
|
||||
DrawLabelOutline(g, labelRect, 3.0f * m_scale, dark);
|
||||
UiText(g, text, m_fonts->monoSmall, labelRect,
|
||||
dark ? 0xFFFDF6E3u : 0xFF073642u, TextAlign::CENTER);
|
||||
DrawLabelOutline(g, labelRect, 3.0f * m_scale, ink.outline);
|
||||
UiText(g, text, m_fonts->monoSmall, labelRect, ink.text, TextAlign::CENTER);
|
||||
|
||||
// 「边沿计数(中心轴)」/「均值滤波」开启时,纵向光标还要额外标注**派生曲线**
|
||||
// 在该时刻的取值(它不是信号,因此单独标注,不并入上面的取值文案)。
|
||||
@@ -5617,12 +5818,11 @@ namespace jdi
|
||||
}
|
||||
|
||||
const float rawCenterY = ValueToY(derivedValue, lane);
|
||||
const DataMarker centerMarker = MakeDataMarker(
|
||||
DataMarker centerMarker = MakeDataMarker(
|
||||
rawX, rawCenterY, pinX, clipTop, clipBottom, kCursorHandleReserve * m_scale);
|
||||
// 派生数据点的配色与原始信号一致(便于一一对应);
|
||||
// 但**始终按线段模式**绘制(不跟随原信号的散点模式)。
|
||||
const std::uint32_t centerColor = StyleColor(*resolved.style);
|
||||
PaintDataMarker(g, centerMarker, radius, centerColor, m_theme->cardBg);
|
||||
|
||||
// 取值文本:数值在前,尾缀「(中心轴)/(均值滤波)」;双光标的右侧光标
|
||||
// 还要附上增量,形如「(+0.0008 中心轴)」(参考常规信号的增量显示)。
|
||||
@@ -5665,12 +5865,18 @@ namespace jdi
|
||||
std::min(centerTop, clipBottom - textHeight - 1.0f * m_scale));
|
||||
gd::RectF centerRect(centerLeft, centerTop, centerTextWidth, textHeight);
|
||||
PlaceCursorLabel(centerRect, centerTop, textHeight, clipTop, clipBottom);
|
||||
// 与原始信号同一规则:上下越界的方向三角与取值标注纵向居中对齐。
|
||||
if (centerMarker.offUp || centerMarker.offDown)
|
||||
{
|
||||
centerMarker.y = centerRect.Y + centerRect.Height * 0.5f;
|
||||
}
|
||||
PaintDataMarker(g, centerMarker, radius, centerColor, m_theme->cardBg);
|
||||
// 颜色样式与原始信号一致(同底色/同描边/同文字配色)。
|
||||
const bool centerDark = IsDarkColor(centerColor);
|
||||
const UiInk centerInk = UiContrastInk(centerColor);
|
||||
UiFillRoundRect(g, centerRect, 3.0f * m_scale, WithAlpha(centerColor, 0xE0));
|
||||
DrawLabelOutline(g, centerRect, 3.0f * m_scale, centerDark);
|
||||
UiText(g, centerText, m_fonts->monoSmall, centerRect,
|
||||
centerDark ? 0xFFFDF6E3u : 0xFF073642u, TextAlign::CENTER);
|
||||
DrawLabelOutline(g, centerRect, 3.0f * m_scale, centerInk.outline);
|
||||
UiText(g, centerText, m_fonts->monoSmall, centerRect, centerInk.text,
|
||||
TextAlign::CENTER);
|
||||
};
|
||||
|
||||
if (m_tab->Lanes()[laneIndex].edgeCenter)
|
||||
@@ -5705,11 +5911,10 @@ namespace jdi
|
||||
const std::uint32_t color = CursorColor();
|
||||
UiFillRoundRect(g, labelRect, 3.0f * m_scale,
|
||||
highlighted ? color : WithAlpha(color, 0xE0));
|
||||
const bool dark = IsDarkColor(color);
|
||||
DrawLabelOutline(g, labelRect, 3.0f * m_scale, dark);
|
||||
const UiInk ink = UiContrastInk(color);
|
||||
DrawLabelOutline(g, labelRect, 3.0f * m_scale, ink.outline);
|
||||
UiText(g, FormatCursorTime(which == 2 ? Tab().CursorTimeAlt() : Tab().CursorTime()),
|
||||
m_fonts->monoSmall, labelRect,
|
||||
dark ? 0xFFFDF6E3u : 0xFF073642u, TextAlign::CENTER);
|
||||
m_fonts->monoSmall, labelRect, ink.text, TextAlign::CENTER);
|
||||
}
|
||||
|
||||
// 双光标差值:显示在两条时间读数中间(时间轴上方;过近时落到主时间轴上)。
|
||||
@@ -5724,10 +5929,10 @@ namespace jdi
|
||||
const std::uint32_t color = CursorColor();
|
||||
UiFillRoundRect(g, spanRect, 3.0f * m_scale,
|
||||
m_spanHover ? color : WithAlpha(color, 0xE0));
|
||||
const bool dark = IsDarkColor(color);
|
||||
DrawLabelOutline(g, spanRect, 3.0f * m_scale, dark);
|
||||
UiText(g, FormatSpanText(CursorSpan()), m_fonts->monoSmall, spanRect,
|
||||
dark ? 0xFFFDF6E3u : 0xFF073642u, TextAlign::CENTER);
|
||||
const UiInk ink = UiContrastInk(color);
|
||||
DrawLabelOutline(g, spanRect, 3.0f * m_scale, ink.outline);
|
||||
UiText(g, FormatSpanText(CursorSpan()), m_fonts->monoSmall, spanRect, ink.text,
|
||||
TextAlign::CENTER);
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -5868,15 +6073,33 @@ namespace jdi
|
||||
float clipBottom, float topReserve) const
|
||||
{
|
||||
DataMarker marker;
|
||||
// 上下越界时"钉"在网格可见边缘(留出三角形的高度,保证整体可见);
|
||||
// 另外预留 topReserve(时间光标的顶部三角把手),避免数据点三角压住把手。
|
||||
const float margin = 7.0f * m_scale;
|
||||
// 形状**只**由“数据点本身是否越出绘制边界”决定:
|
||||
// 旧实现拿“夹取之后”的位置反推越界方向,于是贴近边界、其实仍在视界内的数据点
|
||||
// 也会被误画成三角(用户报告的“数值显示超出边界就改变数据点形状”)。
|
||||
const float left = m_plotRect.X;
|
||||
const float right = m_plotRect.X + m_plotRect.Width;
|
||||
marker.offLeft = (rawX < left);
|
||||
marker.offRight = (rawX > right);
|
||||
marker.offUp = (rawY < clipTop);
|
||||
marker.offDown = (rawY > clipBottom);
|
||||
|
||||
marker.x = pinX;
|
||||
marker.y = std::max(clipTop + margin + topReserve, std::min(rawY, clipBottom - margin));
|
||||
marker.offLeft = (rawX < marker.x - 0.5f);
|
||||
marker.offRight = (rawX > marker.x + 0.5f);
|
||||
marker.offUp = (rawY < marker.y - 0.5f);
|
||||
marker.offDown = (rawY > marker.y + 0.5f);
|
||||
if (marker.offUp)
|
||||
{
|
||||
// 上越界:为方向三角与顶部把手预留高度,避免压住把手。
|
||||
marker.y = clipTop + kMarkerEdgeInset * m_scale + topReserve;
|
||||
}
|
||||
else if (marker.offDown)
|
||||
{
|
||||
marker.y = clipBottom - kMarkerEdgeInset * m_scale;
|
||||
}
|
||||
else
|
||||
{
|
||||
// 视界内:保留数据点的真实纵坐标(越界判定已在上面完成,这里夹取只为了让
|
||||
// 圆点整体可见,不会影响形状)。
|
||||
marker.y = std::max(clipTop + kMarkerInsideMargin * m_scale,
|
||||
std::min(rawY, clipBottom - kMarkerInsideMargin * m_scale));
|
||||
}
|
||||
return marker;
|
||||
}
|
||||
|
||||
@@ -5885,44 +6108,56 @@ namespace jdi
|
||||
{
|
||||
gd::SolidBrush ring(ToGdiColor(ringColor));
|
||||
gd::SolidBrush fill(ToGdiColor(fillColor));
|
||||
const float arm = 5.0f * m_scale;
|
||||
const float inset = 1.4f * m_scale;
|
||||
const float arm = 5.0f * m_scale; /**< 三角“半宽/半高”(垂直于朝向的半边长)。 */
|
||||
const float depth = arm * 1.4f; /**< 三角沿朝向的深度(与光标把手同比例)。 */
|
||||
|
||||
// 左右越界:三角底边在外侧、尖端朝向网格内部。
|
||||
// 越界三角:**尖端朝外**(指向数据点所在的方向,即视界之外),底边在网格内侧;
|
||||
// 三角外接矩形在横/纵两个方向都与锚点**居中**(上下越界时因此与取值标注纵向居中)。
|
||||
// 旧实现方向是反的(上越界画下三角、左越界画右三角),而且内三角按“平移”而非
|
||||
// “向质心收缩”构造,填充色会从三角尖端戳出来。
|
||||
const auto paintTriangle = [&](const gd::PointF* outer)
|
||||
{
|
||||
g.FillPolygon(&ring, outer, 3);
|
||||
const float centerX = (outer[0].X + outer[1].X + outer[2].X) / 3.0f;
|
||||
const float centerY = (outer[0].Y + outer[1].Y + outer[2].Y) / 3.0f;
|
||||
// 按质心等比收缩得到内三角:几何上恒在外三角内部,描边宽度也就均匀了。
|
||||
const float shrink = 0.58f;
|
||||
gd::PointF inner[3];
|
||||
for (int index = 0; index < 3; ++index)
|
||||
{
|
||||
inner[index] = gd::PointF(centerX + (outer[index].X - centerX) * shrink,
|
||||
centerY + (outer[index].Y - centerY) * shrink);
|
||||
}
|
||||
g.FillPolygon(&fill, inner, 3);
|
||||
};
|
||||
|
||||
// 左右越界:三角指向左/右(视界之外)。
|
||||
if (marker.offLeft || marker.offRight)
|
||||
{
|
||||
const bool offLeft = marker.offLeft;
|
||||
const float baseX = offLeft ? (marker.x - arm) : (marker.x + arm);
|
||||
const float inward = offLeft ? inset : -inset;
|
||||
const float tipX = offLeft ? (marker.x - depth) : (marker.x + depth);
|
||||
const float baseX = marker.x;
|
||||
gd::PointF outer[3];
|
||||
outer[0] = gd::PointF(baseX, marker.y - arm);
|
||||
outer[1] = gd::PointF(baseX, marker.y + arm);
|
||||
outer[2] = gd::PointF(marker.x, marker.y);
|
||||
g.FillPolygon(&ring, outer, 3);
|
||||
gd::PointF inner[3];
|
||||
inner[0] = gd::PointF(baseX + inward, marker.y - arm * 0.72f);
|
||||
inner[1] = gd::PointF(baseX + inward, marker.y + arm * 0.72f);
|
||||
inner[2] = gd::PointF(marker.x + inward, marker.y);
|
||||
g.FillPolygon(&fill, inner, 3);
|
||||
outer[2] = gd::PointF(tipX, marker.y);
|
||||
paintTriangle(outer);
|
||||
return;
|
||||
}
|
||||
|
||||
// 上下越界:三角底边在上/下方、尖端朝向网格内部(与左右越界同一套观感)。
|
||||
// 上下越界:三角指向上/下(视界之外),外接矩形以 marker.y 为纵向中心
|
||||
// → 与取值标注的纵向中心对齐。
|
||||
if (marker.offUp || marker.offDown)
|
||||
{
|
||||
const bool offUp = marker.offUp;
|
||||
const float baseY = offUp ? (marker.y - arm) : (marker.y + arm);
|
||||
const float inward = offUp ? inset : -inset;
|
||||
const float half = depth * 0.5f;
|
||||
const float tipY = offUp ? (marker.y - half) : (marker.y + half);
|
||||
const float baseY = offUp ? (marker.y + half) : (marker.y - half);
|
||||
gd::PointF outer[3];
|
||||
outer[0] = gd::PointF(marker.x - arm, baseY);
|
||||
outer[1] = gd::PointF(marker.x + arm, baseY);
|
||||
outer[2] = gd::PointF(marker.x, marker.y);
|
||||
g.FillPolygon(&ring, outer, 3);
|
||||
gd::PointF inner[3];
|
||||
inner[0] = gd::PointF(marker.x - arm * 0.72f, baseY + inward);
|
||||
inner[1] = gd::PointF(marker.x + arm * 0.72f, baseY + inward);
|
||||
inner[2] = gd::PointF(marker.x, marker.y + inward);
|
||||
g.FillPolygon(&fill, inner, 3);
|
||||
outer[2] = gd::PointF(marker.x, tipY);
|
||||
paintTriangle(outer);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -6115,7 +6350,7 @@ namespace jdi
|
||||
std::vector<Lane>& tabLanes = m_tab->Lanes();
|
||||
const std::size_t count = std::min(lanes.size(), tabLanes.size());
|
||||
const std::uint32_t color = CursorColor();
|
||||
const bool dark = IsDarkColor(color);
|
||||
const UiInk ink = UiContrastInk(color);
|
||||
|
||||
// ---- 横向光标(线与两端把手 + 左侧幅值读数)----
|
||||
for (std::size_t index = 0; index < count; ++index)
|
||||
@@ -6172,9 +6407,8 @@ namespace jdi
|
||||
clipBottom - textHeight - 1.0f * m_scale));
|
||||
const gd::RectF labelRect(labelLeft, labelTop, textWidth, textHeight);
|
||||
UiFillRoundRect(g, labelRect, 3.0f * m_scale, WithAlpha(color, 0xE0));
|
||||
DrawLabelOutline(g, labelRect, 3.0f * m_scale, dark);
|
||||
UiText(g, text, m_fonts->monoSmall, labelRect, dark ? 0xFFFDF6E3u : 0xFF073642u,
|
||||
TextAlign::CENTER);
|
||||
DrawLabelOutline(g, labelRect, 3.0f * m_scale, ink.outline);
|
||||
UiText(g, text, m_fonts->monoSmall, labelRect, ink.text, TextAlign::CENTER);
|
||||
}
|
||||
|
||||
// ---- 统计浮动面板(最后绘制,覆盖在波形之上)----
|
||||
|
||||
Reference in new issue
Block a user