Files
Just-DataInspector/Utilities/Inc/PaintUtil.hpp
T
DeepSeek V4.1 Flash b960b03cb5 「阶数 N」改称「迭代次数 N」;修复大文件放大后的平移卡顿与越界三角样式
一、命名(用户要求)
- 实验性功能里的「阶数 N」全部改为「迭代次数 N」(面板滑块标签、均值滤波/中心轴
  的数值框标签、构建提示语)。
- 相关标识符与函数参数统一改名:order → iterations(Lane::MeanFilterItem::iterations、
  Lane::weakBoostIterations、ComputeWeakWaveBoost/ComputeMovingAverageIterated 的形参、
  MeanFilterCurveFor/WeakBoostCurveFor/WeakBoostValueAt/EdgeCenterCountsFor、
  各曲线缓存项的字段、EdgePanelLayout::rowIterations/sliderIterations、
  PlotEditTarget::MEAN_FILTER_ITERATIONS/EDGE_CENTER_ITERATIONS、
  PlotDragMode::BOOST_ITERATIONS、kWeakBoostIterationsMin/Max/Label、
  DerivedSignalRequest::iterations 等)。网格拖拽重排里的局部 order 为避免歧义
  改名为 laneOrder;`std::memory_order_*`、`rowOrder`→`rowIterations` 一并核对无误。
- 导出信号尾缀统一为「N次迭代」:
  `<原信号> (N点均值 M次迭代)`、`<原信号> (弱波增强 X% M次迭代)`;
  弱波增强此前缺失该信息,现已补上(两种派生信号都恒定带迭代次数)。
  状态提示语同步为「… M 次迭代信号。」。

二、越界三角与光标读数的样式统一(用户要求)
- 新增 `kMarkerTriangleGap`:网格内数据点越界时,方向三角与数值标注之间**留出间隙**
  (此前三角与胶囊贴在一起,看起来像胶囊上多出的尖角);中心轴/均值滤波/弱波增强的
  派生取值标注同样处理。
- 光标时间读数(以及双光标 Δt 读数)改用与网格内数据点**完全一致**的越界样式:
  三角画在胶囊**外侧**、尖端朝外、与胶囊之间留出同样的间隙;为此把读数胶囊在越界一侧
  向内让出「三角深度 + 间隙」。旧的"三角压在胶囊上沿"的画法以及为它额外留出的叠放
  间距(kCursorTipStackGap)一并删除。

三、大文件放大后"平移 / 改配色非常卡"的根因与修复(用户要求,流畅性优先)
用离屏探针(`.preflight/perf3`)在 VERY_BIG_DATA(1.77GB、2580 万采样 × 5 信号、
1224×796 绘图区)上先量化,实测到的三个根因:
1. **包络带"梳状化"**:当信号的振荡周期与像素列宽度相当时(编码器方波在 2ms/大格下
   约 3 个采样点一列),逐列包络会在"全高"与"极窄"之间交替——实测 1224 列里有 988 处
   全高跳变。这种多边形让 GDI+ 的扫描线填充耗时暴涨:同一条 2448 顶点的路径
   **27.6ms → 1.1ms(±1 列极值扩展后快 25 倍)**。这正是"某些放大等级下平移特别卡"
   (实测 2ms/大格平移 87ms,其余等级 17~20ms)的原因。
   → `EmitEnvelopeBand` 在拼装包络带时对列窗口做 ±1 列的极值扩展(纵向最多各多 1 像素,
   肉眼不可辨),一次性消除该病理。
2. **派生预览曲线按折线逐段描边**:均值滤波/弱波增强曲线在可见采样数超过像素列数时
   仍按"每 2 像素一个顶点"的抗锯齿折线绘制,而这类曲线常叠在方波上(大量近垂直跳变),
   实测单条 12~42ms,5 个网格合计上百毫秒。
   → 稠密时改用**逐像素列的包络带**(复用 `EmitEnvelopeBand`)一次 FillPath,实测
   0.56ms / 1.53ms(分别快 22 倍 / 28 倍),视觉与细折线一致(列宽就是 1 像素)。
3. **悬停光晕用宽笔描整条折线**:实测悬停一帧多花 35ms。
   → 包络带模式下改为"把带整体外扩后填充一次"(`PaintBandHalo`),悬停帧恢复正常。

另外修掉两个相关的固定开销:
- `UiMeasureTextWidth` 增加 (字体, 文本) 记忆化缓存:它是布局代码的热点(浮动面板几何
  按「网格数 × 面板种类」重复计算,每帧重复测量同一批字符串成百上千次),实测单个面板
  的绘制因此从 3.2ms 降到 1ms 量级。字体重建(DPI 变化)时由 `UiClearMeasureCache` 作废。

实测(VERY_BIG_DATA 全选 5 信号,绘图区全重绘 = 平移一帧):
- 20ms/大格(用户报告的场景):平移 20.1ms → **11.3ms**;
- 2ms/大格(此前最糟):平移 86.7ms → **12.3ms**;
- 网格 1 开弱波增强:63.9ms → **17.6ms**;5 个网格全开:136.6ms → **31.9ms**;
- 悬停信号:整帧 49.6ms → **15.5ms**(与不悬停持平,光晕几乎免费)。
仍存的已知热点:0.5ms/大格附近(可见约 1000 个采样点、走**逐点描边**路径)平移约
28~39ms —— 该区间采样比像素还稀疏、无法用包络带替代,且用户明确要求保留抗锯齿,
本次未动。

四、验证
- 离屏 harness(窗口 SW_HIDE、不占用鼠标/键盘/屏幕)**355 项检查全部通过**,
  新增 `[AD]`:越界读数胶囊向内让位、三角在外侧、三角与胶囊间有真实间隙、
  胶囊上方不再有内容;取值标注与三角之间保留 `kMarkerTriangleGap` 的间隙(用
  散点模式排除波形干扰后逐像素断言)。`[X]/[Y]/[AA]/[AB]/[Z]` 等既有用例同步通过。
- 性能结论来自 `.preflight/perf3`(离屏探针 + 两套微基准),已随任务清理。
2026-10-10 20:13:21 +08:00

417 lines
16 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#ifndef __PAINT_UTIL_HPP__
#define __PAINT_UTIL_HPP__
#include <windows.h>
#include <objidl.h>
#include <gdiplus.h>
#include <cstddef>
#include <cstdint>
#include <string>
#include <vector>
#include "TextEdit.hpp"
#include "Theme.hpp"
namespace jdi
{
/**
* @brief 文本水平对齐方式(垂直方向始终居中)。
*/
enum class TextAlign : int
{
LEFT = 0, /**< 左对齐。 */
CENTER = 1, /**< 水平居中。 */
RIGHT = 2 /**< 右对齐。 */
};
/**
* @brief 图标朝向。
*/
enum class IconDirection : int
{
LEFT = 0, /**< 指向左侧。 */
RIGHT = 1, /**< 指向右侧。 */
UP = 2, /**< 指向上方。 */
DOWN = 3 /**< 指向下方。 */
};
/**
* @brief 按 DPI 创建并缓存整套界面字体(GDI 字体句柄)。
*/
struct UiFonts
{
HFONT ui = nullptr; /**< 常规界面字体。 */
HFONT uiBold = nullptr; /**< 加粗界面字体。 */
HFONT uiSmall = nullptr; /**< 小号界面字体。 */
HFONT uiSmallBold = nullptr;/**< 小号加粗界面字体。 */
HFONT title = nullptr; /**< 标题字体。 */
HFONT mono = nullptr; /**< 等宽数值字体。 */
HFONT monoSmall = nullptr; /**< 小号等宽字体。 */
UINT dpi = 96; /**< 创建时所使用的 DPI。 */
/**
* @brief 依据 DPI 创建全部字体。
* @param [IN] dpi 目标 DPI(96 对应 100% 缩放)。
* @return 创建成功返回 true。
*/
bool Create(UINT dpi);
/**
* @brief 释放全部字体句柄。
*/
void Destroy();
~UiFonts() { Destroy(); }
};
/**
* @brief 将 0xAARRGGBB 颜色值转换为 GDI COLORREF。
* @param [IN] argb 颜色值。
* @return GDI 颜色值。
*/
inline COLORREF ToColorRef(std::uint32_t argb)
{
return RGB((argb >> 16) & 0xFFu, (argb >> 8) & 0xFFu, argb & 0xFFu);
}
/**
* @brief 判断点是否落在矩形内部。
* @param [IN] rect 目标矩形。
* @param [IN] x 横坐标。
* @param [IN] y 纵坐标。
* @return 落在内部返回 true。
*/
inline bool UiPointInRect(const gd::RectF& rect, float x, float y)
{
return x >= rect.X && x <= (rect.X + rect.Width) && y >= rect.Y && y <= (rect.Y + rect.Height);
}
/**
* @brief 填充矩形区域。
* @param [IN,OUT] g 绘图上下文。
* @param [IN] rect 目标矩形。
* @param [IN] argb 填充颜色。
*/
void UiFillRect(gd::Graphics& g, const gd::RectF& rect, std::uint32_t argb);
/**
* @brief 填充圆角矩形区域。
* @param [IN,OUT] g 绘图上下文。
* @param [IN] rect 目标矩形。
* @param [IN] radius 圆角半径。
* @param [IN] argb 填充颜色。
*/
void UiFillRoundRect(gd::Graphics& g, const gd::RectF& rect, float radius, std::uint32_t argb);
/**
* @brief 描边圆角矩形。
* @param [IN,OUT] g 绘图上下文。
* @param [IN] rect 目标矩形。
* @param [IN] radius 圆角半径。
* @param [IN] argb 线条颜色。
* @param [IN] width 线条宽度。
*/
void UiDrawRoundRect(gd::Graphics& g, const gd::RectF& rect, float radius, std::uint32_t argb, float width);
/**
* @brief 绘制一条直线。
* @param [IN,OUT] g 绘图上下文。
* @param [IN] x1 起点横坐标。
* @param [IN] y1 起点纵坐标。
* @param [IN] x2 终点横坐标。
* @param [IN] y2 终点纵坐标。
* @param [IN] argb 线条颜色。
* @param [IN] width 线条宽度。
*/
void UiDrawLine(gd::Graphics& g, float x1, float y1, float x2, float y2, std::uint32_t argb, float width);
/**
* @brief 在指定矩形内绘制单行文本(垂直居中,超长自动省略)。
* @param [IN,OUT] g 绘图上下文。
* @param [IN] text 文本内容。
* @param [IN] font 字体句柄。
* @param [IN] rect 文本布局矩形。
* @param [IN] argb 文本颜色。
* @param [IN] align 水平对齐方式。
*/
void UiText(gd::Graphics& g, const std::wstring& text, HFONT font, const gd::RectF& rect, std::uint32_t argb,
TextAlign align = TextAlign::LEFT);
/**
* @brief 测量单行文本的像素宽度(无需 GDI+ 上下文,可在布局阶段调用)。
* @param [IN] font 字体句柄。
* @param [IN] text 文本内容。
* @return 文本像素宽度。
*/
float UiMeasureTextWidth(HFONT font, const std::wstring& text);
/**
* @brief 在指定裁剪矩形内以显式起点绘制一段文本(使用 GDI ExtTextOut 裁剪)。
*
* `UiText` 走 `DrawTextW`,**不遵循 GDI+ 裁剪区**;输入框在长内容、水平滚动的情况下
* 会把字符画到框体之外。本函数用 `ETO_CLIPPING` 让 GDI 自己裁剪,彻底避免越界绘制。
*
* @param [IN,OUT] g 绘图上下文。
* @param [IN] text 文本内容。
* @param [IN] font 字体句柄。
* @param [IN] x 文本起始横坐标。
* @param [IN] rect 垂直居中参考矩形。
* @param [IN] argb 文本颜色。
* @param [IN] clipRect 裁剪矩形(超出部分不绘制)。
*/
void UiTextAtClipped(gd::Graphics& g, const std::wstring& text, HFONT font, float x, const gd::RectF& rect,
std::uint32_t argb, const gd::RectF& clipRect);
/**
* @brief 计算输入框文本的绘制起点(已考虑超出框宽时的水平滚动)。
* @param [IN] rect 输入框矩形。
* @param [IN] edit 编辑模型。
* @param [IN] font 字体句柄。
* @param [IN] scale 缩放比例。
* @param [OUT] innerWidth 框内可用宽度输出。
* @return 文本绘制起点横坐标。
*/
float UiEditTextLeft(const gd::RectF& rect, const TextEdit& edit, HFONT font, float scale,
float& innerWidth);
/**
* @brief 把输入框内的横坐标换算为字符偏移(就近取整,用于鼠标定位插入符)。
* @param [IN] font 字体句柄。
* @param [IN] text 当前文本。
* @param [IN] localX 相对文本起点的横坐标(可超出 [0, 文本宽度])。
* @return 字符偏移。
*/
std::size_t UiEditOffsetAt(HFONT font, const std::wstring& text, float localX);
/**
* @brief 在超出可用宽度时对文本做尾部省略截断(不绘制任何内容)。
* @param [IN] text 原始文本。
* @param [IN] font 字体句柄。
* @param [IN] maxWidth 允许的最大像素宽度。
* @return 截断后的文本。
*/
std::wstring UiEllipsize(const std::wstring& text, HFONT font, float maxWidth);
/**
* @brief 处理输入框内的鼠标按下:命中则把插入符定位到最近的字符边界并开始拖拽框选。
* @param [IN] font 字体句柄。
* @param [IN] rect 输入框矩形。
* @param [IN,OUT] edit 编辑模型。
* @param [IN] scale 缩放比例。
* @param [IN] x 客户区横坐标。
* @param [IN] y 客户区纵坐标。
* @return 命中输入框并开始拖拽返回 true。
*/
bool UiEditFieldMouseDown(HFONT font, const gd::RectF& rect, TextEdit& edit, float scale, int x, int y);
/**
* @brief 处理输入框内的鼠标拖拽移动(扩展选区)。
* @param [IN] font 字体句柄。
* @param [IN] rect 输入框矩形。
* @param [IN,OUT] edit 编辑模型。
* @param [IN] scale 缩放比例。
* @param [IN] x 客户区横坐标。
* @return 选区发生变化返回 true。
*/
bool UiEditFieldMouseMove(HFONT font, const gd::RectF& rect, TextEdit& edit, float scale, int x);
/**
* @brief 在矩形中心绘制一个加号图标。
* @param [IN,OUT] g 绘图上下文。
* @param [IN] rect 图标布局矩形。
* @param [IN] argb 线条颜色。
* @param [IN] strokeWidth 线条宽度。
*/
void UiDrawPlusIcon(gd::Graphics& g, const gd::RectF& rect, std::uint32_t argb, float strokeWidth);
/**
* @brief 用 GDI 一次性绘制若干条**不透明 1px 实线**(每条线段相互独立)。
*
* 性能关键:GDI+ 的 `DrawLine` 实测每条约 0.1–0.19 ms(带抗锯齿的长线更慢),
* 而一张网格通常要画几十条网格线 —— 逐条调用会让单帧多花十几毫秒。
* 这里改用 GDI(一次取 HDC + 一支画笔 + 多个 LineTo),快一两个数量级,
* 而且像素级网格线本来就不需要抗锯齿,观感反而更清晰。
*
* ⚠️ GDI 绘制**不受 GDI+ 裁剪区约束**,因此必须显式传入裁剪矩形
* (内部用 `SaveDC` + `IntersectClipRect` 保证不越界)。
* ⚠️ 只能用于**不透明**颜色(GDI 不做 alpha 混合);半透明色需调用方先与底色混合。
*
* @param [IN,OUT] g 绘图上下文。
* @param [IN] clip 裁剪矩形(客户区坐标)。
* @param [IN] segments 线段数组,每 4 个 float 为一条(x1,y1,x2,y2)。
* @param [IN] segmentCount 线段数量。
* @param [IN] argb 颜色(Alpha 必须为 0xFF,否则不做任何绘制)。
* @param [IN] width 线宽(像素,>= 1;仅精确支持 1,更宽时按整数宽画)。
*/
void UiDrawSolidLines(gd::Graphics& g, const gd::RectF& clip, const float* segments,
std::size_t segmentCount, std::uint32_t argb, int width = 1);
/**
* @brief 把半透明颜色与已知不透明底色混合,得到可直接用于 GDI 的不透明颜色。
* @param [IN] background 不透明底色(0xFFRRGGBB)。
* @param [IN] argb 待叠加颜色(含 alpha)。
* @return 混合后的不透明颜色。
*/
std::uint32_t UiBlendOver(std::uint32_t background, std::uint32_t argb);
/**
* @brief 作废「文本宽度测量」的记忆化缓存。
*
* `UiMeasureTextWidth` 内部按 (字体句柄, 文本) 缓存测量结果以摊薄 GDI 调用开销;
* 字体句柄被重建(DPI 变化等)时必须调用本函数,否则系统复用的同一句柄值会命中
* 旧宽度,造成布局错位。
*/
void UiClearMeasureCache();
/**
* @brief 一次性填充多个圆角矩形(同色同圆角)。
*
* 每帧要画几十个刻度数值底板时,逐个调用 `UiFillRoundRect` 会带来可观的 GDI+ 调用开销;
* 合并成一条 GraphicsPath 后只 FillPath 一次(实测约快 3 倍)。
*
* @param [IN,OUT] g 绘图上下文。
* @param [IN] rects 矩形集合。
* @param [IN] radius 圆角半径。
* @param [IN] argb 填充色。
*/
void UiFillRoundRects(gd::Graphics& g, const std::vector<gd::RectF>& rects, float radius,
std::uint32_t argb);
/**
* @brief 在矩形中心绘制一个叉号(关闭)图标。
* @param [IN,OUT] g 绘图上下文。
* @param [IN] rect 图标布局矩形。
* @param [IN] argb 线条颜色。
* @param [IN] strokeWidth 线条宽度。
*/
void UiDrawCrossIcon(gd::Graphics& g, const gd::RectF& rect, std::uint32_t argb, float strokeWidth);
/**
* @brief 在矩形中心绘制一个实心三角形指示器。
* @param [IN,OUT] g 绘图上下文。
* @param [IN] rect 图标布局矩形。
* @param [IN] argb 填充颜色。
* @param [IN] direction 三角形朝向。
*/
void UiDrawTriangleIcon(gd::Graphics& g, const gd::RectF& rect, std::uint32_t argb, IconDirection direction);
/**
* @brief 将滑块轨道上的横坐标换算为取值比例。
* @param [IN] trackRect 轨道矩形。
* @param [IN] x 横坐标。
* @return 取值比例,范围 0-1。
*/
double UiSliderRatioFromX(const gd::RectF& trackRect, float x);
/**
* @brief 绘制水平滑块(轨道 + 已填充部分 + 圆钮)。
* @param [IN,OUT] g 绘图上下文。
* @param [IN] track 轨道矩形。
* @param [IN] ratio 当前值比例(0-1)。
* @param [IN] fillColor 已填充部分颜色。
* @param [IN] active 是否处于悬停或拖拽状态(影响圆钮半径)。
* @param [IN] scale DPI 缩放比例。
*/
void UiDrawSlider(gd::Graphics& g, const gd::RectF& track, double ratio, std::uint32_t fillColor,
bool active, float scale);
/**
* @brief 绘制单行内联输入框(含选区高亮、插入符与水平滚动)。
*
* 初始状态为“整体选中”时以强调色底衬显示全部内容,首字符键入即整体替换,
* 交互观感与常见登录界面的账号/密码输入框一致;文本超出框宽时自动横向滚动,
* 保证插入符始终可见。
*
* @param [IN,OUT] g 绘图上下文。
* @param [IN] rect 输入框矩形。
* @param [IN] edit 输入框编辑模型。
* @param [IN] font 文本字体。
* @param [IN] scale DPI 缩放比例。
*/
void UiDrawEditField(gd::Graphics& g, const gd::RectF& rect, const TextEdit& edit, HFONT font,
float scale);
/**
* @brief 只绘制输入框的**内容**(选区高亮 / 文本 / 插入符),不绘制底板与边框。
*
* 用于"宿主已经画好了外框"的场景(例如信号面板的「筛选信号」输入框):若直接调用
* UiDrawEditField,会在外框内部再套一个带边框的圆角框,形成一圈多余的内圈。
*
* @param [IN,OUT] g 绘图上下文。
* @param [IN] rect 输入框矩形(内容区,几何与 UiDrawEditField 完全一致)。
* @param [IN] edit 输入框编辑模型。
* @param [IN] font 文本字体。
* @param [IN] scale DPI 缩放比例。
*/
void UiDrawEditFieldContent(gd::Graphics& g, const gd::RectF& rect, const TextEdit& edit, HFONT font,
float scale);
/**
* @brief 取值胶囊底色上的高对比度文字色与描边色。
*/
struct UiInk
{
std::uint32_t text = 0xFF073642u; /**< 文字色(不透明)。 */
std::uint32_t outline = 0x99FDF6E3u; /**< 描边色(半透明,恒与文字色反相)。 */
};
/**
* @brief 颜色的相对亮度(0-1,逐通道线性加权)。
* @param [IN] color 0xAARRGGBB 颜色值。
* @return 相对亮度。
*/
double UiColorLuminance(std::uint32_t color);
/**
* @brief 依据胶囊底色求高对比度的文字色与描边色(对底色配色作线性响应)。
*
* 求值分两步:
* 1. **亮度**:按"取最大对比"的一侧选定亮文字或暗文字。这是唯一的阈值判断,但它
* 本身就是"任意底色下都保持高对比"的必要条件 —— 任何连续的亮度取值都会在中性灰
* 附近同时远离两侧极值,对比度必然塌陷。
* 2. **配色**:把该侧的极值色与**底色的互补色**按固定比例线性混合。互补色的相对亮度
* 恰为 `1 - 底色亮度`,因此文字/描边的亮度是底色亮度的**线性**函数,色相也随信号
* 配色连续变化(不再是固定的黑/白两色)。
*
* 描边色取相反的一侧,保证"描边—文字—底色"三段式对比。
*
* @param [IN] background 胶囊底色(0xAARRGGBB)。
* @return 文字色与描边色。
*/
UiInk UiContrastInk(std::uint32_t background);
/**
* @brief 绘制一个带边框的复选框。
* @param [IN,OUT] g 绘图上下文。
* @param [IN] rect 复选框矩形。
* @param [IN] state 选中状态:0 未选,1 全选,2 部分选。
* @param [IN] radius 圆角半径。
*/
void UiDrawCheckBox(gd::Graphics& g, const gd::RectF& rect, int state, float radius);
/**
* @brief 绘制颜色样块:叠加不透明度填充,透明度过低时补充描边以保证可辨识。
* @param [IN,OUT] g 绘图上下文。
* @param [IN] rect 样块矩形。
* @param [IN] color 基础颜色(0xAARRGGBB,其 Alpha 通道会被不透明度覆盖)。
* @param [IN] opacity 不透明度(0-1)。
* @param [IN] radius 圆角半径。
* @param [IN] scale DPI 缩放比例,用于推导描边粗细。
* @return 实际填充颜色的 0xAARRGGBB 值(供需要同步其他元素的调用方使用)。
*/
std::uint32_t UiDrawSwatch(gd::Graphics& g, const gd::RectF& rect, std::uint32_t color, float opacity,
float radius, float scale);
/**
* @brief 判断给定不透明度是否低到需要为样块补充描边。
* @param [IN] opacity 不透明度(0-1)。
* @return 需要描边返回 true。
*/
bool UiSwatchNeedsOutline(float opacity);
} // namespace jdi
#endif // __PAINT_UTIL_HPP__