一、「公式映射」卡顿(用户报告:只是点进输入框就卡,编辑过公式后彻底没法用) 1. 真因:菜单内联编辑提交时无条件回写初值 ⇒ 点进输入框再点别处就把 formula = "f(t)" 写进样式;而 HasFormula() 只判 !empty(),于是 DrawTransformOrderPreserving() 变 false、ComputeLaneRange() 每帧逐点扫描 整条信号(2580 万采样 ≈ 960ms/帧)。 修法:新增 FormulaIsIdentity()(空串与 f(t) 等价);提交时归一化后恒等即清空; BuildLaneVisuals() 为「网格幅值范围」加指纹缓存(数据指针/长度/统计值 + 影响数值的样式字段),键不变直接复用(实测 0.0005ms)。 2. 真实公式仍然贵:新增 PlotView::AcquireFormulaEnvelope()(变换后的逐分组 包络,65536 组,按需算一次并缓存),BuildPolyline 的包络带改用它。 实测:静止 215→21ms、中键拖动 8ms、缩放最坏 843→81ms;极值仍逐点统计 (不会漏尖峰)。 3. AcquireProgram 只比 data() 指针 + 长度 ⇒ std::wstring 短串走 SSO 时, 等长改公式会一直复用旧程序(f(t)^2→f(t)^3 波形不更新)。改为按内容判定。 二、公式语法(Utilities/FormulaEval) - 新增 ^ 幂(右结合,2^3^2=512、2^-1=0.5、-2^2=-4),支持 2^f(t) 与 f(t)^2; - 常量 e / pi(大小写均接受,保存前统一小写); - 数据点只允许 X / x / f(t):单独的 f 不再接受(且不归一化,直接报错); - 数字扫描修正:2e 里的 e 不再被当成指数吞掉。 三、「公式映射」输入框(SignalPanelMenu) - Y= 画在输入框内部(非编辑态),进入编辑态只显示公式本身; - label 留空 ⇒ 输入框占满整行,与下方「系数调整」同宽。 四、「重采样」面板 - 修复点进间隔/频率输入框后输入框整块消失(paintOverlays 的 PaintEditField 名单漏了 RESAMPLE_INTERVAL / RESAMPLE_FREQUENCY); - 最大/最小数据间隔:3 位有效数字 + Δt 复合单位,末尾追加(频率)尾缀; - 间隔显示自适应单位、编辑态用秒;频率显示自适应单位、编辑态用 Hz(均不带单位); - 面板宽度按含(频率)尾缀的文本量宽。 五、十进制格式化(Utf8Util) - 新增 FormatDecimal(绝不产生科学计数法);FormatDurationCompound / FormatDurationSeconds / FormatFrequency 改用它,Δt/间隔/频率读数不再出现 1e-05。 验证:离屏探针 .preflight/perf/probe/formulacheck_main.cpp 75/75; make release / make debug 零 warning;Release 856,064 B。
4597 lines
223 KiB
C++
4597 lines
223 KiB
C++
#ifndef __PLOT_VIEW_HPP__
|
||
#define __PLOT_VIEW_HPP__
|
||
|
||
#include <windows.h>
|
||
#include <objidl.h>
|
||
#include <gdiplus.h>
|
||
|
||
#include <array>
|
||
#include <atomic>
|
||
#include <condition_variable>
|
||
#include <deque>
|
||
#include <map>
|
||
#include <memory>
|
||
#include <mutex>
|
||
#include <string>
|
||
#include <thread>
|
||
#include <utility>
|
||
#include <vector>
|
||
|
||
#include "DataModel.hpp"
|
||
#include "PaintUtil.hpp"
|
||
#include "Theme.hpp"
|
||
#include "UiComponent.hpp"
|
||
|
||
namespace jdi
|
||
{
|
||
|
||
class UiBoxTable;
|
||
|
||
/**
|
||
* @brief 后台派生曲线计算完成后由 `PlotView` 投递给宿主的自定义消息。
|
||
*
|
||
* 宿主收到后重绘绘图区即可:绘制线程会在下一帧取到刚算好的曲线(双缓冲切换),
|
||
* 同时静态场景缓存会被作废以纳入新曲线。`WM_APP + 3` 与宿主其它自定义消息错开。
|
||
*/
|
||
constexpr UINT kMessagePlotDerivedReady = WM_APP + 3;
|
||
|
||
/**
|
||
* @brief 「可用内存严重不足,已暂停派生曲线计算」的通知消息。
|
||
*
|
||
* 只有当**连一条预览曲线本身都装不下**时才发出(`DerivedMemoryAllows`),
|
||
* 宿主收到后应给出一次可见的告警(本项目用 `SetStatusMessage(..., true)` 弹一次提示框)
|
||
* 并让界面继续可用 —— 绝不会因为内存不足而崩溃。`WM_APP + 4` 与其它自定义消息错开。
|
||
*/
|
||
constexpr UINT kMessagePlotLowMemory = WM_APP + 4;
|
||
|
||
/**
|
||
* @brief 绘图区拖拽交互模式。
|
||
*/
|
||
enum class PlotDragMode : int
|
||
{
|
||
NONE = 0, /**< 未拖拽。 */
|
||
PAN, /**< 常规模式:水平平移时间轴 + 纵向偏移当前网格幅值。 */
|
||
RUBBER_SELECT, /**< 选区放大:按拖拽方向决定横向(时间轴)或纵向(幅值轴)选区。 */
|
||
SCROLL_LANES, /**< 拖动网格滚动条。 */
|
||
CURSOR_MOVE, /**< 拖拽时间光标。 */
|
||
CURSOR_SPAN_MOVE, /**< 拖拽双光标差值标签:两条光标整体平移,间隔不变。 */
|
||
MOVE_SIGNAL, /**< 拖拽信号曲线到其它网格。 */
|
||
EDGE_MOVE, /**< 拖动「边沿计数」的横向光标(上下移动幅值)。 */
|
||
EDGE_PANEL_MOVE, /**< 拖动边沿计数浮动面板。 */
|
||
PEAK_LEVEL, /**< 拖动「弱峰调整」某信号的幅度滑块(-400% – +400%)。 */
|
||
PEAK_ITERATIONS, /**< 拖动「弱峰调整」某信号的迭代次数滑块(1 … 该信号自定义上限)。 */
|
||
LANE_REORDER, /**< 拖动网格图例中的 GX 编号徽标:上下调整网格次序。 */
|
||
MARKER_OPACITY, /**< 拖动「自由标记」面板的「标记透明度」滑块。 */
|
||
MARKER_SCROLL, /**< 拖动「自由标记」面板内容的滚动条滑块。 */
|
||
MEAN_POINTS, /**< 拖动「均值滤波」某信号的「均值点数」滑块(0 … 该信号自定义上限)。 */
|
||
MEAN_ITERATIONS,/**< 拖动「均值滤波」某信号的「迭代次数」滑块(1 … 该信号自定义上限)。 */
|
||
MEAN_SCROLL,/**< 拖动「均值滤波」面板内容的滚动条滑块。 */
|
||
RESAMPLE_SCROLL /**< 拖动「重采样」面板内容的滚动条滑块。 */
|
||
};
|
||
|
||
/**
|
||
* @brief 选区放大拖拽的轴向(由首次明显位移的方向决定)。
|
||
*/
|
||
enum class PlotRubberAxis : int
|
||
{
|
||
UNDECIDED = 0, /**< 尚未确定(拖拽刚刚开始)。 */
|
||
TIME = 1, /**< 横向拖拽:放大时间轴。 */
|
||
VALUE = 2 /**< 纵向拖拽:放大当前网格的幅值轴。 */
|
||
};
|
||
|
||
/**
|
||
* @brief 绘图区内联数值输入的目标。
|
||
*/
|
||
enum class PlotEditTarget : int
|
||
{
|
||
NONE = 0, /**< 未处于输入状态。 */
|
||
CURSOR_TIME, /**< 编辑时间光标位置。 */
|
||
CURSOR_SPAN, /**< 编辑双光标之间的时间差(前方光标不动,后方光标随之移动)。 */
|
||
LANE_Y_MAX, /**< 编辑某网格的 Y 轴最大值。 */
|
||
LANE_Y_MIN, /**< 编辑某网格的 Y 轴最小值。 */
|
||
SIGNAL_NAME, /**< 编辑图例中某信号的显示名称。 */
|
||
EDGE_CENTER_POINTS, /**< 编辑「中心轴边沿计数」的均值点数 X。 */
|
||
EDGE_CENTER_ITERATIONS, /**< 编辑「中心轴边沿计数」的迭代次数 N。 */
|
||
MEAN_FILTER_POINTS, /**< 编辑「均值滤波」的均值点数 X。 */
|
||
MEAN_FILTER_ITERATIONS, /**< 编辑「均值滤波」的迭代次数 N。 */
|
||
PEAK_ITERATIONS_MAX, /**< 编辑「弱峰调整」某信号「迭代次数 N」滑块的**上限**(正整数)。 */
|
||
/**
|
||
* @brief 编辑「均值滤波」某信号「均值点数」滑块的**上限**(正整数)。
|
||
*
|
||
* 与 `PEAK_ITERATIONS_MAX` 同一交互语义:点击滑块右侧数值 → 整行变成输入框,
|
||
* 输入框以空内容开始、并显示“输入上限(当前N)”提示。
|
||
*/
|
||
MEAN_POINTS_MAX,
|
||
/** @brief 编辑「均值滤波」某信号「迭代次数」滑块的**上限**(正整数)。 */
|
||
MEAN_ITERATIONS_MAX,
|
||
RESAMPLE_INTERVAL, /**< 编辑「重采样」某信号的**重采样间隔**(秒)。 */
|
||
RESAMPLE_FREQUENCY, /**< 编辑「重采样」某信号的**重采样频率**(Hz,与间隔联动)。 */
|
||
FREE_MARKER_TIME, /**< 编辑「自由标记」某固定点的**自身时间**(秒)。 */
|
||
FREE_MARKER_DELTA /**< 编辑「自由标记」某固定点相对前一固定点的**时间间隔**(秒)。 */
|
||
};
|
||
|
||
/**
|
||
* @brief 绘图区右键菜单命令标识(与信号面板的命令空间相互独立)。
|
||
*/
|
||
enum PlotMenuCommand : int
|
||
{
|
||
PLOT_CMD_AUTO_FIT_ALL = 1, /**< 全局切换全部网格的「选区 Y 轴自适应」。 */
|
||
PLOT_CMD_LANE_Y_MAX = 2, /**< 输入该网格 Y 轴最大值。 */
|
||
PLOT_CMD_LANE_Y_MIN = 3, /**< 输入该网格 Y 轴最小值。 */
|
||
PLOT_CMD_FIT_ALL = 4, /**< 全部网格与时间轴自适应。 */
|
||
PLOT_CMD_LANE_FIT_TIME = 5, /**< 时间轴自适应到本网格信号范围(按网格时间轴展开)。 */
|
||
PLOT_CMD_LANE_AUTO_VIEW = 6, /**< 切换本网格的「选区 Y 轴自适应」开关。 */
|
||
PLOT_CMD_LANE_EDGE_COUNT = 7, /**< 切换本网格的「实验性功能 → 边沿计数」开关。 */
|
||
PLOT_CMD_LANE_EDGE_CENTER = 8, /**< 切换本网格的「实验性功能 → 边沿计数(中心轴)」开关。 */
|
||
PLOT_CMD_LANE_SPLIT = 9, /**< 拆分当前网格:把网格内信号按个拆成独立网格并顺次下移编号。 */
|
||
PLOT_CMD_LANE_MEAN_FILTER = 10, /**< 切换本网格的「实验性功能 → 均值滤波」开关。 */
|
||
PLOT_CMD_LANE_WEAK_PEAK = 11, /**< 切换本网格的「实验性功能 → 弱峰调整」开关。 */
|
||
PLOT_CMD_LANE_FREE_MARKER = 12, /**< 切换本网格的「实验性功能 → 自由标记」开关。 */
|
||
PLOT_CMD_LANE_RESAMPLE = 13 /**< 切换本网格的「实验性功能 → 重采样」开关。 */
|
||
};
|
||
|
||
/**
|
||
* @brief 实验性功能浮动面板的种类。
|
||
*/
|
||
enum class EdgePanelKind : int
|
||
{
|
||
COUNT = 0, /**< 边沿计数。 */
|
||
CENTER = 1, /**< 边沿计数(中心轴)。 */
|
||
MEAN = 2, /**< 均值滤波。 */
|
||
PEAK = 3, /**< 弱峰调整。 */
|
||
MARK = 4, /**< 自由标记。 */
|
||
RESAMPLE = 5 /**< 重采样(把非固定频率采样规整为固定频率)。 */
|
||
};
|
||
|
||
/**
|
||
* @brief 派生(叠加预览)曲线的种类。
|
||
*
|
||
* 「中心轴边沿计数」与「均值滤波」共用 `MEAN_FILTER`(同一套参数与同一条曲线)。
|
||
*/
|
||
enum class DerivedCurveKind : int
|
||
{
|
||
MEAN_FILTER = 0, /**< 多次迭代均值滤波。 */
|
||
WEAK_PEAK = 1 /**< 弱峰调整。 */
|
||
};
|
||
|
||
/**
|
||
* @brief 波形绘图区:网格、时间轴、光标、缩放、自适应与信号拖拽。
|
||
*
|
||
* 绘图区同时依赖数据层(DataSet,只读)与当前标签页配置(TabConfig,可写),
|
||
* 视图范围、网格滚动与时间光标均保存在标签页配置中,因此各标签页互不干扰。
|
||
*/
|
||
class PlotView : public UiComponent
|
||
{
|
||
public:
|
||
PlotView();
|
||
|
||
/**
|
||
* @brief 析构:释放拖拽静态场景缓存的离屏位图。
|
||
*/
|
||
~PlotView();
|
||
|
||
/**
|
||
* @brief 设置宿主窗口句柄(用于剪贴板所有者)。
|
||
* @param [IN] host 宿主窗口句柄,允许为空。
|
||
*/
|
||
void SetHostWindow(HWND host) { m_host = host; }
|
||
|
||
/**
|
||
* @brief 已渲染的「信号图层」区域数量(仅供验证 / 诊断)。
|
||
*
|
||
* 每次把一个网格的基底(整块或滚动细条)画进图层都会 +1。计数长期不增长即说明
|
||
* 平移期间确实在复用已有像素(只做坐标偏移),没有重新绘制波形。
|
||
* @return 累计完成的图层区域数量。
|
||
*/
|
||
std::uint64_t BackgroundLayerRenderCount() const
|
||
{
|
||
return m_layerRenderCount.load(std::memory_order_relaxed);
|
||
}
|
||
|
||
/**
|
||
* @brief 绑定字体与主题资源。
|
||
* @param [IN] fonts 字体集合,需在视图生命周期内保持有效。
|
||
* @param [IN] theme 主题配色,需在视图生命周期内保持有效。
|
||
*/
|
||
void Setup(const UiFonts* fonts, const Theme* theme);
|
||
|
||
/**
|
||
* @brief 绑定数据层与当前标签页配置。
|
||
* @param [IN] dataSet 数据集合,允许为空。
|
||
* @param [IN] tab 当前标签页配置,允许为空。
|
||
*/
|
||
void SetContext(DataSet* dataSet, TabConfig* tab);
|
||
|
||
/**
|
||
* @brief 设置 DPI 缩放比例。
|
||
* @param [IN] scale 缩放比例(1.0 对应 96 DPI)。
|
||
*/
|
||
void SetScale(float scale) override;
|
||
|
||
/**
|
||
* @brief 按给定区域重新排布绘图区。
|
||
* @param [IN] area 可用区域。
|
||
*/
|
||
void Layout(const gd::RectF& area) override;
|
||
|
||
/** @brief 绘图区占用的整块区域(与 `Layout` 同源)。 */
|
||
gd::RectF Area() const override { return m_area; }
|
||
|
||
/**
|
||
* @brief 绘制绘图区。
|
||
* @param [IN,OUT] g 绘图上下文。
|
||
*/
|
||
void Paint(gd::Graphics& g) override;
|
||
|
||
/**
|
||
* @brief 上报绘图区区域、**每个网格**、GX 徽标、图例条目与浮动面板的几何 box。
|
||
*
|
||
* 只上报“布局结构”,**不上报**曲线折线、取值胶囊、悬停提示这类随数据/鼠标逐帧变化的
|
||
* 瞬态几何 —— 它们每帧都不同,叠上去只是噪声。
|
||
*
|
||
* @param [IN,OUT] boxes 收集器。
|
||
*/
|
||
void BuildBoxes(UiBoxTable& boxes) override;
|
||
|
||
/**
|
||
* @brief 强制走「整幅重绘」路径(供调试叠加层使用)。
|
||
*
|
||
* 平时 `Paint` 会用三条快路径(静态场景贴图 / 宽基底贴图 / 样式补丁)只重画变化部分,
|
||
* 这在拖动网格滚动条时会**跳过一部分像素**。而调试叠加层(「显示布局边界」)是画在
|
||
* 整个窗口上的叠加内容 —— 一旦底层没有逐像素重画,上一帧的调试边框就会与新内容叠加,
|
||
* 表现为「工具栏与网格区域上大量绘制残留」(用户 2026-10-28 报告)。
|
||
*
|
||
* 因此调试叠加层开启时必须打开本开关:牺牲帧率,换取"每帧都是完整一帧"。
|
||
*
|
||
* ⚠️ 保留而不优化的理由(2026-10-29 评估):把它改成“脏矩形 + 独立叠加位图”需要让
|
||
* `Paint` 接受宿主下发的 damage(否则持久后备缓冲区里上一帧的边框像素无处抹除),
|
||
* 而这会穿透 `PlotView` 全部四条快路径与两层缓存。收益只是“调试开关打开时的帧率”、
|
||
* 风险却是用户反复报过的拖拽错位/贴图残留一类事故 —— 故**暂不实施**,仅保留此开关。
|
||
*
|
||
* @param [IN] force 为 true 时禁用全部快路径且不作画质降级。
|
||
*/
|
||
void SetForceFullRepaint(bool force) { m_forceFullRepaint = force; }
|
||
|
||
/**
|
||
* @brief 处理鼠标移动(含拖拽)。
|
||
* @param [IN] x 客户区横坐标。
|
||
* @param [IN] y 客户区纵坐标。
|
||
* @return 需要重绘返回 true。
|
||
*/
|
||
bool OnMouseMove(int x, int y);
|
||
|
||
/**
|
||
* @brief 处理鼠标左键按下。
|
||
* @param [IN] x 客户区横坐标。
|
||
* @param [IN] y 客户区纵坐标。
|
||
* @return 需要重绘返回 true。
|
||
*/
|
||
bool OnLButtonDown(int x, int y);
|
||
|
||
/**
|
||
* @brief 处理鼠标左键抬起。
|
||
* @param [IN] x 客户区横坐标。
|
||
* @param [IN] y 客户区纵坐标。
|
||
* @return 需要重绘返回 true。
|
||
*/
|
||
bool OnLButtonUp(int x, int y);
|
||
|
||
/**
|
||
* @brief 处理鼠标右键按下(**只处理浮动面板**)。
|
||
*
|
||
* 浮动面板压在波形之上,属最上层元素:右键落在面板上必须被面板消费,
|
||
* 不得穿透到下面的网格(否则会在面板上弹出网格的 Y 轴菜单)。
|
||
*
|
||
* @param [IN] x 客户区横坐标。
|
||
* @param [IN] y 客户区纵坐标。
|
||
* @return 命中浮动面板(已消费该次右键)返回 true;否则返回 false,由宿主继续处理。
|
||
*/
|
||
bool OnRButtonDown(int x, int y);
|
||
|
||
/**
|
||
* @brief 处理鼠标中键按下:与左键在常规模式下一致,进入自由平移(水平平移时间轴 +
|
||
* 纵向偏移当前网格幅值)。**任何缩放模式下都可用**,用于在不改变当前模式的
|
||
* 前提下自由查看区域。
|
||
* @param [IN] x 客户区横坐标。
|
||
* @param [IN] y 客户区纵坐标。
|
||
* @return 需要重绘返回 true。
|
||
*/
|
||
bool OnMButtonDown(int x, int y);
|
||
|
||
/**
|
||
* @brief 处理鼠标中键抬起:结束中键平移。
|
||
* @return 需要重绘返回 true。
|
||
*/
|
||
bool OnMButtonUp();
|
||
|
||
/**
|
||
* @brief 处理鼠标滚轮(仅用于平移与网格滚动,不再承担缩放)。
|
||
* @param [IN] x 客户区横坐标。
|
||
* @param [IN] y 客户区纵坐标。
|
||
* @param [IN] delta 滚轮增量。
|
||
* @param [IN] shift 是否按下 Shift(水平平移时间轴)。
|
||
* @return 需要重绘返回 true。
|
||
*/
|
||
bool OnMouseWheel(int delta, bool shift);
|
||
|
||
/**
|
||
* @brief 处理鼠标双击(命中时间光标时进入精确时间输入)。
|
||
* @param [IN] x 客户区横坐标。
|
||
* @param [IN] y 客户区纵坐标。
|
||
* @return 需要重绘返回 true。
|
||
*/
|
||
bool OnDoubleClick(int x, int y);
|
||
|
||
/**
|
||
* @brief 双击落点是否应**当作普通「按下」**处理(该位置没有双击语义)。
|
||
*
|
||
* ⚠️ 窗口类带 `CS_DBLCLKS`:快速连点同一个控件时,第二次(及之后)按下会被 Windows
|
||
* 合成为 `WM_LBUTTONDBLCLK`。若宿主把它交给 `OnDoubleClick`,而该位置本来只有
|
||
* 「按下 / 单击」语义(浮动面板上的复选框与按钮、滚动条、GX 徽标…),那一次按下就
|
||
* **完全丢失** —— 用户看到的正是“连点好几下、状态只切换了几次”(不跟手)。
|
||
*
|
||
* ✓ 本函数为这些位置返回 true,宿主改走 `OnLButtonDown`(与单击完全相同的路径)。
|
||
* 只有真正有双击语义的位置(时间光标线 / 读数标签 / 图例条目名 / 绘图区空白处
|
||
* 的自适应)返回 false。
|
||
*
|
||
* @param [IN] x 客户区横坐标。
|
||
* @param [IN] y 客户区纵坐标。
|
||
* @return 应当作普通按下返回 true。
|
||
*/
|
||
bool DoubleClickIsPlainPress(int x, int y) const;
|
||
|
||
/**
|
||
* @brief 处理鼠标离开绘图区。
|
||
* @return 需要重绘返回 true。
|
||
*/
|
||
bool OnMouseLeave() override;
|
||
|
||
/**
|
||
* @brief 将时间轴自适应到指定网格内信号的时间范围。
|
||
* @param [IN] laneIndex 网格下标。
|
||
* @return 调整成功返回 true。
|
||
*/
|
||
bool FitTimeToLane(int laneIndex);
|
||
|
||
/**
|
||
* @brief 「拆分当前网格」:把该网格内的信号按个拆分成独立网格,新网格依次排在其下方。
|
||
* @param [IN] laneIndex 网格下标。
|
||
* @return 拆分成功返回 true。
|
||
*/
|
||
bool SplitLane(int laneIndex);
|
||
|
||
/**
|
||
* @brief 查询某网格内的信号数量(用于菜单项可用状态判定)。
|
||
* @param [IN] laneIndex 网格下标。
|
||
* @return 信号数量,网格不存在时返回 0。
|
||
*/
|
||
std::size_t LaneSignalCount(int laneIndex) const;
|
||
|
||
/**
|
||
* @brief 依据当前显示时间段内的数据对该网格做 Y 轴自适应(“选区 Y 轴自适应”)。
|
||
*
|
||
* 与普通自适应(用整段数据的极值)不同:这里只看时间轴当前可见区间内的采样点,
|
||
* 因此放大到某段局部波形后可以快速把该段的幅值铺满整个网格。
|
||
*
|
||
* @param [IN] laneIndex 网格下标。
|
||
* @return 调整成功返回 true(区间内无有效数据时返回 false)。
|
||
*/
|
||
bool AutoRangeLaneToView(int laneIndex);
|
||
|
||
/**
|
||
* @brief 对全部开启了「选区 Y 轴自适应」的网格做一次 Y 轴选区自适应。
|
||
*
|
||
* 时间轴选区放大后调用:只有开关开启的网格才自动匹配新的可见时间区间。
|
||
*/
|
||
void ApplySelectionAutoFit();
|
||
|
||
/**
|
||
* @brief 网格结构变化后,若「全局 Y 轴自适应」处于开启状态则重新做一次全局自适应。
|
||
*
|
||
* 用户要求(2026-10-15):
|
||
* * 新勾选信号产生的**新网格**要按**当前视界**自适应(新网格继承开关由
|
||
* `RebuildLanes` 负责,这里负责“立即算一次”);
|
||
* * 多信号网格内**取消显示**某条信号、或把网格**拆分**之后,剩余/新生的网格
|
||
* 都要重新收敛到当前视界(原来的幅值区间是按旧信号集合算的)。
|
||
*
|
||
* 判据与工具栏「Y 轴自适应」一致 = `AllLanesAutoFitToSelection()`(全部网格都开启)。
|
||
*/
|
||
void ReapplyGlobalAutoFitIfEnabled();
|
||
|
||
/**
|
||
* @brief 依据「每小格数据点数」自动启用/恢复「点线」绘制方式(即“放大自动点线”)。
|
||
*
|
||
* 用户 2026-10-17 **重新定义判据**(推翻此前的“一屏内数据点少于 N×4”):
|
||
* 按**缩放后的横向时间轴网格**把小格切出来,**只要存在任何一个小格内的数据点数
|
||
* 少于 `kSparseMinPointsPerCell`(=5)** 就自动切到「点线」提示用户数据点的位置
|
||
*(见 `MinorCellSparse`);一旦不再满足(适应视图、或把视界从稀疏区移到稠密区)
|
||
* 自动恢复**原来的**绘制方式。
|
||
*
|
||
* ⚠️ 自动启用期间若用户**手动**改了绘制方式(`SignalStyle::autoScatterUserOverride`
|
||
* 由本函数自动识别:自动状态下 mode 不等于 `LINE_SCATTER` 即说明是用户改的),
|
||
* 退出时**不再**恢复,用户的选择优先。
|
||
*
|
||
* @return 有任何信号的绘制方式被改变返回 true(调用方需作废缓存)。
|
||
*/
|
||
bool UpdateAutoScatterStyles();
|
||
|
||
/**
|
||
* @brief 网格底衬**一横排的小格数量 N**(与 `PaintLane` 画出来的网格同源)。
|
||
*
|
||
* 时间刻度用 `NiceStep(span, kTimeTickTarget)` 定主刻度、再五等分 ⇒
|
||
* 不管缩放等级如何,一横排总是差不多同样多的小格。
|
||
*
|
||
* @return 小格数量(无法判定时返回 0)。
|
||
*/
|
||
int GridMinorCellCount() const;
|
||
|
||
/**
|
||
* @brief 判断某信号在当前视界下是否**按小格判定为稀疏**。
|
||
*
|
||
* 用户 2026-10-17 定义的判据(**推翻**此前的“一屏内数据点 < N×4”):
|
||
* 按缩放后的**横向时间轴网格**把小格切出来(`GridMinorCellCount()`),
|
||
* **只要存在任何一个小格内的有效数据点少于 `kSparseMinPointsPerCell`(=5)** 就返回 true。
|
||
*
|
||
* ⚠️ **这一个判定同时驱动两处**(用户 2026-10-17 明确要求“同样的状态判定”):
|
||
* * 「放大自动切点线」(`UpdateAutoScatterStyles`);
|
||
* * 「光标吸附」(`SnapTimeToSample`)—— 判定**完全不看绘制方式**,
|
||
* 因此即使用户手动改过显示样式,该吸附的信号照样吸附。
|
||
*
|
||
* 判据随视界实时重算 ⇒ 疏密变化的信号在稀疏区自动吸附、稠密区自动自由移动。
|
||
* ⚠️ 额外要求视界内**至少有一个可见数据点**:数据点全在视界之外的信号既没有可吸附的
|
||
* 对象、也没有可提示的内容(否则它会被判成“小格全空”而白切一次「点线」)。
|
||
* ⚠️ 可见性一律按**显示时间**(`采样时刻 + timeShift`)判断 —— `timeShift` 就是该信号的
|
||
* `SignalStyle::offsetTime`(存在时间轴偏移时不能拿源时刻去比视界)。
|
||
*
|
||
* @param [IN] axis 该信号使用的时间轴。
|
||
* @param [IN] signal 目标信号。
|
||
* @param [IN] timeShift 该信号的显示时间偏移(`offsetTime`)。
|
||
* @return 存在少于 `kSparseMinPointsPerCell` 个数据点的小格返回 true。
|
||
*/
|
||
bool MinorCellSparse(const TimeAxis& axis, const SignalData& signal, double timeShift) const;
|
||
|
||
/**
|
||
* @brief 判断某信号在给定可见下标区间内是否存在**落在视界内**的有效采样点。
|
||
* @param [IN] axis 该信号使用的时间轴。
|
||
* @param [IN] signal 目标信号。
|
||
* @param [IN] begin 可见下标区间起点。 @param [IN] end 终点(不含)。
|
||
* @param [IN] timeShift 该信号的显示时间偏移(`offsetTime`)。
|
||
* @return 存在返回 true。
|
||
*/
|
||
bool HasVisibleSample(const TimeAxis& axis, const SignalData& signal, std::size_t begin,
|
||
std::size_t end, double timeShift) const;
|
||
|
||
/**
|
||
* @brief 找某信号**可见区间内**离给定时刻最近的有效采样点。
|
||
*
|
||
* 稀疏信号(有效点已缓存)用二分;一般信号从最近下标向两侧**有界**探测
|
||
*(NaN 只出现在缺少数据的区段)。视界之外的采样点不是合法落点,一律排除。
|
||
*
|
||
* ⚠️ `time` 与输出的 `candidate` 都是**显示时间**(画面上的时刻):内部按 `timeShift`
|
||
* 换算回源空间查找、再把结果加回去,因此存在「调整 → 时间轴偏移」时也能吸到
|
||
* **画面上**的数据点,而不是历史位置。
|
||
*
|
||
* @param [IN] axis 该信号使用的时间轴。
|
||
* @param [IN] signal 目标信号。
|
||
* @param [IN] time 目标时刻(显示时间)。
|
||
* @param [IN] timeShift 该信号的显示时间偏移(`offsetTime`)。
|
||
* @param [OUT] candidate 最近的可见有效采样点时刻(显示时间)。
|
||
* @return 找到返回 true。
|
||
*/
|
||
bool NearestSnapSample(const TimeAxis& axis, const SignalData& signal, double time,
|
||
double timeShift, double& candidate) const;
|
||
|
||
/**
|
||
* @brief 取某时刻附近的**局部采样间距**(时间单位,用于限制吸附半径)。
|
||
* @param [IN] axis 该信号使用的时间轴。
|
||
* @param [IN] signal 目标信号。
|
||
* @param [IN] time 目标时刻(显示时间)。
|
||
* @param [IN] timeShift 该信号的显示时间偏移(`offsetTime`)。
|
||
* @param [OUT] spacing 相邻采样点的时间间隔(无法判定时为 0)。
|
||
*/
|
||
void LocalSampleSpacing(const TimeAxis& axis, const SignalData& signal, double time,
|
||
double timeShift, double& spacing) const;
|
||
|
||
/**
|
||
* @brief 把时间吸附到距今最近的数据采样点。
|
||
*
|
||
* 判定与半径(用户 2026-10-17 修订):
|
||
* * **判定只针对鼠标下方所在的网格**:该网格内的信号各自按 `MinorCellSparse`
|
||
* (“任一小格内数据点少于 5 个”)判定,**符合条件的信号的数据点全部参与吸附**;
|
||
* * 网格内**既有符合条件也有不符合条件的信号**时:只吸附符合条件那条信号的数据点
|
||
* (用户明确要求);
|
||
* * 网格内**没有**符合条件的信号时:光标完全自由移动(不吸附);
|
||
* * 判定**不看绘制方式** ⇒ 即使用户手动改了显示样式,该吸附的信号照样吸附;
|
||
* * 吸附半径 = **一个网格小格的宽度**(与判定同源),且**不超过半个局部采样间距**
|
||
* ⇒ 数据点很稀疏(一屏只有一两个点)时光标在点与点之间仍可自由移动,
|
||
* 而不是被“死死吸住”(吸附只是提示性响应,不是卡住光标)。
|
||
*
|
||
* @param [IN] time 原始时间。
|
||
* @param [OUT] snapped 吸附后的时间。
|
||
* @return 成功吸附返回 true。
|
||
*/
|
||
bool SnapTimeToSample(double time, double& snapped) const;
|
||
|
||
/**
|
||
* @brief 判断坐标是否落在绘图区范围内。
|
||
* @param [IN] x 客户区横坐标。
|
||
* @param [IN] y 客户区纵坐标。
|
||
* @return 落在范围内返回 true。
|
||
*/
|
||
bool HitTest(int x, int y) const override;
|
||
|
||
/**
|
||
* @brief 查询当前鼠标位置应使用的系统光标。
|
||
* @param [IN] x 客户区横坐标。
|
||
* @param [IN] y 客户区纵坐标。
|
||
* @return 系统光标句柄,未命中特殊区域返回 nullptr。
|
||
*/
|
||
HCURSOR QueryCursor(int x, int y) const;
|
||
|
||
/**
|
||
* @brief `UiComponent` 协议的入口(转调上面的 const 版本)。
|
||
*
|
||
* `UiComponent` 声明的是非 const 版本(`SignalPanel` 的命中需要惰性重建行集合),
|
||
* 而绘图区的实现天然是 const;这里做一次薄转发,两个重载保持行为完全一致。
|
||
*/
|
||
HCURSOR QueryCursor(int x, int y) override
|
||
{
|
||
return static_cast<const PlotView*>(this)->QueryCursor(x, y);
|
||
}
|
||
|
||
/**
|
||
* @brief 「适应视图」:时间轴与幅值轴同时自适应到可见信号范围。
|
||
*
|
||
* 边界规则(用户 2026-10-15 第五轮**重新定义**,取代此前"左右各留一小格"的旧做法):
|
||
* * 常规的全局适应后,若某条边界(时间轴的左 / 右、每个网格幅值的上 / 下)被一个
|
||
* **单值信号的数据点**顶住,则该边界再向外让出**该散点的半径**,使它**完整地
|
||
* 显示在网格内**;边界若不是单值信号的数据点,则边界就落在这个信号点上
|
||
* (时间轴不留额外余量,幅值仍走 `PadValueRange` 的统一留白)。
|
||
* * 唯一特例:**所有可见信号都是单值数据点、且时刻完全相同**时,时间轴以该时刻
|
||
* 居中并缩放到"一大格 = 1s",每个网格的幅值区间保证其数据点都能完整画出。
|
||
*
|
||
* ⚠️ 没有任何可见信号时**不置位** `Tab().ViewInitialized()`:用户先连点几次
|
||
* 「适应视图」后再勾选第一个信号,初次自适应仍必须发生。
|
||
*/
|
||
void FitAll();
|
||
|
||
/**
|
||
* @brief 一个**可见信号**在「适应视图」里提供的边界贡献(见 `FitAll` 的边界规则)。
|
||
*/
|
||
struct FitContribution
|
||
{
|
||
SignalRef ref; /**< 信号引用。 */
|
||
double minTime = 0.0; /**< **显示时间**下限(= 时间轴下限 + 该信号的 `offsetTime`)。 */
|
||
double maxTime = 0.0; /**< **显示时间**上限。 */
|
||
bool singlePoint = false; /**< 是否只有一个有效数据点(画出来就是一个圆点)。 */
|
||
double pointTime = 0.0; /**< 单值时:数据点的**显示时刻**(源时刻 + `offsetTime`)。 */
|
||
double pointValue = 0.0; /**< 单值时:**绘制变换后**的取值。 */
|
||
float dotRadius = 0.0f; /**< 单值时:散点半径(设备像素)。 */
|
||
};
|
||
|
||
/**
|
||
* @brief 汇总全部可见信号的 `FitContribution`(按网格分组)。
|
||
* @param [OUT] perLane 与 `Tab().Lanes()` 同序的分组结果。
|
||
* @return 可见信号总数(0 表示当前没有任何可见信号)。
|
||
*/
|
||
std::size_t CollectFitContributions(std::vector<std::vector<FitContribution>>& perLane) const;
|
||
|
||
/**
|
||
* @brief 当前布局下**单个网格的高度**(设备像素)。
|
||
*
|
||
* 与 `BuildLaneVisuals()` 用同一公式(而不是读 `m_lanes[i].rect.Height`):
|
||
* `FitAll` 可能在网格数量刚变化、布局尚未重建时被调用,公式给出"将要生效"的值。
|
||
* @return 网格高度;无网格时返回 0。
|
||
*/
|
||
float FitLaneHeight() const;
|
||
|
||
/**
|
||
* @brief 若该网格的**幅值上/下边界**被单值散点顶住,把区间向外让出散点半径。
|
||
*
|
||
* 只在散点**会被裁切**时才改动区间("散点本来就完整可见"不动它,保持自动自适应的
|
||
* 语义);改动时把 `autoRange` 置 false 并写入显式区间。
|
||
*/
|
||
void ApplySinglePointValueMargins(int laneIndex, const std::vector<FitContribution>& items,
|
||
float laneHeight);
|
||
|
||
/**
|
||
* @brief 「所有可见数据点都是单值信号、且时刻完全相同」时的自适应。
|
||
*
|
||
* 时间轴以 `sharedTime` 居中、**一大格 = 1s**;每个网格的幅值区间保证其数据点
|
||
* 完整显示(网格内只有一个取值时以跨度 1 居中)。
|
||
*/
|
||
void FitSinglePointCluster(const std::vector<std::vector<FitContribution>>& perLane,
|
||
double sharedTime);
|
||
|
||
/**
|
||
* @brief 取某信号**唯一的有效数据点**(时刻 + 原始取值)。
|
||
*
|
||
* 优先用加载时缓存的稀疏点;没有缓存时逐个采样找(单点信号的采样数组必然很短)。
|
||
*
|
||
* @param [IN] signal 目标信号。
|
||
* @param [IN] axis 该信号使用的时间轴。
|
||
* @param [OUT] time 数据点时刻。 @param [OUT] value 数据点原始取值。
|
||
* @return 取到有效点返回 true。
|
||
*/
|
||
bool SinglePointOfSignal(const SignalData& signal, const TimeAxis& axis, double& time,
|
||
double& value) const;
|
||
|
||
/**
|
||
* @brief 以给定倍率缩放时间轴(以视图中心为锚点)。
|
||
* @param [IN] factor 缩放倍率,大于 1 表示放大。
|
||
*/
|
||
void ZoomTimeStep(double factor);
|
||
|
||
/**
|
||
* @brief 按当前视图跨度的比例平移时间轴。
|
||
* @param [IN] fraction 平移比例,正值向右(时间增大)平移。
|
||
*/
|
||
void PanTimeByFraction(double fraction);
|
||
|
||
/**
|
||
* @brief 按网格内容高度的比例纵向滚动网格。
|
||
* @param [IN] fraction 滚动比例,正值向下滚动。
|
||
*/
|
||
void ScrollLanesByFraction(double fraction);
|
||
|
||
/**
|
||
* @brief 提示浮层是否仍在显示(含淡出阶段)。
|
||
*
|
||
* 宿主可在返回 true 时保持定时重绘,使提示能"停留后淡出"。
|
||
*
|
||
* @return 需要继续重绘返回 true。
|
||
*/
|
||
bool HintNeedsAnimation() const;
|
||
|
||
/**
|
||
* @brief 以**非模态**方式向用户提示一条信息(绘图区底部浮层,停留至下一条提示替换)。
|
||
*
|
||
* 宿主(`Application`)把不宜用模态对话框打断的操作反馈(例如加载/导出错误)
|
||
* 交给本接口显示即可,也便于在无交互(隐藏窗口)场景下保持可用。
|
||
*
|
||
* @param [IN] text 提示文本;传空串则立即清除提示。
|
||
*/
|
||
void ShowPersistentHint(const std::wstring& text);
|
||
|
||
/**
|
||
* @brief 清除已启用的时间光标。
|
||
*/
|
||
void DisableCursor();
|
||
|
||
/**
|
||
* @brief 放弃当前正在进行的拖拽(缩放选区/平移/信号拖动),不改变视图范围。
|
||
* @param [IN] showHint 是否同时显示“已放弃选区”提示。
|
||
* @return 确实取消了某个拖拽返回 true。
|
||
*/
|
||
bool CancelDrag(bool showHint = false);
|
||
|
||
/**
|
||
* @brief 判断当前是否处于「选区放大」的框选过程中(供宿主处理右键放弃)。
|
||
* @return 正在框选返回 true。
|
||
*/
|
||
bool IsRubberSelecting() const { return m_dragMode == PlotDragMode::RUBBER_SELECT; }
|
||
|
||
/**
|
||
* @brief 退出全部缩放模式(选区放大),使工具栏缩放按钮回到未选中态。
|
||
* @return 之前处于缩放模式返回 true。
|
||
*/
|
||
bool ExitZoomModes();
|
||
|
||
/**
|
||
* @brief 使全部派生结果失效:折线顶点、边沿统计、中心平均值曲线与静态场景缓存。
|
||
*
|
||
* 数据层结构变化(加载/移除分组、信号增删)后信号引用与分组地址都可能改变,
|
||
* 必须整体失效,避免旧几何键与新的信号意外重合而取到过期结果。
|
||
*/
|
||
void InvalidateDerivedCaches();
|
||
|
||
/**
|
||
* @brief 主题配色变化后作废全部**像素级**缓存。
|
||
*
|
||
* 主题色被烘进了场景缓存位图、每网格宽基底图层、装饰层、光晕层与浮动面板层 ——
|
||
* 换主题后必须整体丢弃,否则这些区域会停留在旧配色上(直到因为别的原因重画)。
|
||
*/
|
||
void OnThemeChanged();
|
||
|
||
/**
|
||
* @brief 释放本组件持有的全部绘制资源(位图 / 后台线程)。
|
||
*
|
||
* ⚠️ **必须在 `GdiplusShutdown` 之前调用**:`PlotView` 持有 `gd::Bitmap`(场景/图层/
|
||
* 装饰/面板/光晕,以及并行图层 worker 各自的位图),而 `Application` 是进程级单例,
|
||
* 其成员要等到 `WinMain` 返回后才析构 —— 那时 GDI+ 已关闭,位图析构会直接访问违例
|
||
* (与拾色器当年那个 0xC0000005 同类)。`~PlotView()` 也会调一次(幂等)。
|
||
*/
|
||
void ReleaseGraphicsResources();
|
||
|
||
/**
|
||
* @brief 暂停后台派生曲线计算并作废其结果。
|
||
*
|
||
* 后台线程会读取采样数据,因此**任何会改变数据层结构(增删分组/信号、插入派生信号,
|
||
* 进而移动 `SignalGroup`/`SignalData` 存储)的操作之前**都必须先调用本函数:它会等待正在
|
||
* 进行的计算结束、清空待算队列与缓存,从而杜绝后台线程读取已释放内存。
|
||
* 之后的绘制会按需重新启动后台线程(惰性)。
|
||
*/
|
||
void PauseDerivedAsync();
|
||
|
||
/**
|
||
* @brief 当前标签页是否还有任何「带预览曲线」的实验性功能处于开启状态。
|
||
* @return 至少一个网格开启了弱峰调整 / 均值滤波 / 边沿计数(中心轴)返回 true。
|
||
*/
|
||
bool AnyDerivedPreviewEnabled() const;
|
||
|
||
/**
|
||
* @brief 若已无任何预览曲线功能,立即释放派生曲线的全部数据。
|
||
*
|
||
* 释放内容包括:后台线程池(**join 之后各线程的 `thread_local` 工作缓冲才析构、
|
||
* 内存才归还**)、待算队列、已算好的曲线条目与均值/弱峰曲线缓存。
|
||
*
|
||
* ⚠️ 这是"关掉预览后内存不下降"的修复:一条整段(千万级采样)曲线的中间缓冲
|
||
* 可达数百 MB,而 `thread_local` 只在线程退出时释放 —— 过去只有"
|
||
* 关闭整个分组"(会 `PauseDerivedAsync`)才会真正还内存。
|
||
*/
|
||
void TrimDerivedCurveDataIfIdle();
|
||
|
||
/**
|
||
* @brief 三态循环切换时间光标:无 → 单光标 → 双光标 → 无。
|
||
* @return 状态发生变化返回 true。
|
||
*/
|
||
bool CycleCursorMode();
|
||
|
||
/**
|
||
* @brief 当绘图区从「没有任何可见信号」变为「有信号」时,按当前光标模式**重新按规则摆好光标**。
|
||
*
|
||
* 用户 2026-10-17 要求:光标工具处于激活状态(单 / 双光标)且当前**没有任何信号被勾选绘制**时,
|
||
* 下一个信号首次出现时应按“光标出现的规则”重新出现一次,而不是停留在之前的
|
||
* 残留位置(那时的视界与数据都是空的,残留位置已无意义)。
|
||
*
|
||
* 规则与 `CycleCursorMode()` 的“首次出现”完全同源:单光标置于视图中心;
|
||
* 双光标的第一条置于视图中心、第二条紧邻第一条并保证落在可见范围内。
|
||
*
|
||
* @return 确实重新摆放了光标返回 true(光标工具未激活时返回 false)。
|
||
*/
|
||
bool ResetCursorPlacementOnFreshContent();
|
||
|
||
/**
|
||
* @brief 判断时间光标是否已启用(单/双光标均为启用)。
|
||
* @return 启用返回 true。
|
||
*/
|
||
bool CursorEnabled() const;
|
||
|
||
/**
|
||
* @brief 获取当前时间光标模式。
|
||
* @return 光标模式。
|
||
*/
|
||
PlotCursorMode CursorMode() const;
|
||
|
||
/**
|
||
* @brief 获取光标 1 对应的时间。
|
||
* @return 光标 1 时间。
|
||
*/
|
||
double CursorTime() const;
|
||
|
||
/**
|
||
* @brief 切换拖拽缩放模式(再次调用同一模式则取消)。
|
||
* @param [IN] mode 目标模式。
|
||
* @return 状态发生变化返回 true。
|
||
*/
|
||
bool ToggleZoomMode(PlotZoomMode mode);
|
||
|
||
/**
|
||
* @brief 获取当前拖拽缩放模式。
|
||
* @return 模式枚举。
|
||
*/
|
||
PlotZoomMode CurrentZoomMode() const;
|
||
|
||
/**
|
||
* @brief 查询坐标命中的网格下标(用于右键菜单)。
|
||
* @param [IN] x 客户区横坐标。
|
||
* @param [IN] y 客户区纵坐标。
|
||
* @return 网格下标,未命中返回 -1。
|
||
*/
|
||
int LaneIndexAt(int x, int y) const;
|
||
|
||
/**
|
||
* @brief 判断某网格是否开启了「选区 Y 轴自适应」开关(菜单勾选标记)。
|
||
* @param [IN] laneIndex 网格下标。
|
||
* @return 开启返回 true。
|
||
*/
|
||
bool LaneAutoFitToSelection(int laneIndex) const;
|
||
|
||
/**
|
||
* @brief 判断是否全部网格都开启了「选区 Y 轴自适应」(用于全局菜单项与工具栏按钮)。
|
||
* @return 存在网格且全部开启返回 true。
|
||
*/
|
||
bool AllLanesAutoFitToSelection() const;
|
||
|
||
/**
|
||
* @brief 切换某网格的「选区 Y 轴自适应」开关。
|
||
* @param [IN] laneIndex 网格下标。
|
||
* @return 切换后的状态。
|
||
*/
|
||
bool ToggleLaneAutoFitToSelection(int laneIndex);
|
||
|
||
/**
|
||
* @brief 让某网格的「选区 Y 轴自适应」开关立即生效一次(打开时调用)。
|
||
* @param [IN] laneIndex 网格下标。
|
||
*/
|
||
void ApplyAutoFitToSelectionNow(int laneIndex);
|
||
|
||
/**
|
||
* @brief 对全部网格的「选区 Y 轴自适应」开关立即生效一次(全局打开时调用)。
|
||
*/
|
||
void ApplyAllAutoFitToSelectionNow();
|
||
|
||
/**
|
||
* @brief 一次性设置全部网格的「选区 Y 轴自适应」开关(全局菜单项 / 工具栏按钮)。
|
||
* @param [IN] enabled 目标状态。
|
||
* @param [IN] applyNow 打开时是否立即按当前视图执行一次 Y 轴自适应。
|
||
* @return 任一网格状态发生变化返回 true。
|
||
*/
|
||
bool SetAllLanesAutoFitToSelection(bool enabled, bool applyNow = true);
|
||
|
||
/**
|
||
* @brief 判断某网格是否开启了「实验性功能 → 边沿计数」(菜单勾选标记)。
|
||
* @param [IN] laneIndex 网格下标。
|
||
* @return 开启返回 true。
|
||
*/
|
||
bool LaneEdgeCount(int laneIndex) const;
|
||
|
||
/**
|
||
* @brief 设置某网格的「边沿计数」开关。
|
||
* @param [IN] laneIndex 网格下标。
|
||
* @param [IN] enabled 目标状态。
|
||
* @return 状态发生变化返回 true。
|
||
*/
|
||
bool SetLaneEdgeCount(int laneIndex, bool enabled);
|
||
|
||
/**
|
||
* @brief 切换某网格的「边沿计数」开关(开启时自动初始化横向光标位置与面板)。
|
||
* @param [IN] laneIndex 网格下标。
|
||
* @return 切换后的状态。
|
||
*/
|
||
bool ToggleLaneEdgeCount(int laneIndex);
|
||
|
||
/**
|
||
* @brief 判断某网格是否开启了「实验性功能 → 边沿计数(中心轴)」。
|
||
* @param [IN] laneIndex 网格下标。
|
||
* @return 开启返回 true。
|
||
*/
|
||
bool LaneEdgeCenterCount(int laneIndex) const;
|
||
|
||
/**
|
||
* @brief 设置某网格的「边沿计数(中心轴)」开关。
|
||
* @param [IN] laneIndex 网格下标。
|
||
* @param [IN] enabled 目标状态。
|
||
* @return 状态发生变化返回 true。
|
||
*/
|
||
bool SetLaneEdgeCenterCount(int laneIndex, bool enabled);
|
||
|
||
/**
|
||
* @brief 切换某网格的「边沿计数(中心轴)」开关。
|
||
* @param [IN] laneIndex 网格下标。
|
||
* @return 切换后的状态。
|
||
*/
|
||
bool ToggleLaneEdgeCenterCount(int laneIndex);
|
||
|
||
/**
|
||
* @brief 判断某网格是否开启了「实验性功能 → 均值滤波」。
|
||
* @param [IN] laneIndex 网格下标。
|
||
* @return 开启返回 true。
|
||
*/
|
||
bool LaneMeanFilter(int laneIndex) const;
|
||
|
||
/**
|
||
* @brief 设置某网格的「均值滤波」开关。
|
||
* @param [IN] laneIndex 网格下标。
|
||
* @param [IN] enabled 目标状态。
|
||
* @return 状态发生变化返回 true。
|
||
*/
|
||
bool SetLaneMeanFilter(int laneIndex, bool enabled);
|
||
|
||
/**
|
||
* @brief 切换某网格的「均值滤波」开关。
|
||
* @param [IN] laneIndex 网格下标。
|
||
* @return 切换后的状态。
|
||
*/
|
||
bool ToggleLaneMeanFilter(int laneIndex);
|
||
|
||
/**
|
||
* @brief 「均值滤波 → 构建为新信号」的单个请求(一个被勾选的信号 + 自己的参数)。
|
||
*/
|
||
struct MeanFilterBuild
|
||
{
|
||
int groupIndex = -1; /**< 信号所属分组下标。 */
|
||
int signalIndex = -1; /**< 组内信号下标。 */
|
||
int points = 5; /**< 均值点数 X(滑平半径)。 */
|
||
int iterations = 1; /**< 迭代次数 N(至少 1)。 */
|
||
};
|
||
|
||
/**
|
||
* @brief 取出「构建为新信号」请求(由面板底部按钮触发)。
|
||
*
|
||
* 请求只描述「哪一网格、哪些被勾选的信号及其参数」,真正的信号构建与插入由宿主
|
||
* (Application)完成——数据层属于 DataSet,绘图区不能直接修改它。
|
||
*
|
||
* @param [OUT] laneIndex 触发该请求的网格下标。
|
||
* @param [OUT] items 需要构建的信号及其参数(按信号在网格内的顺序)。
|
||
* @return 存在待处理请求返回 true(取出后清空)。
|
||
*/
|
||
bool TakePendingMeanFilterBuild(int& laneIndex, std::vector<MeanFilterBuild>& items);
|
||
|
||
/**
|
||
* @brief 「实验性功能 → 弱峰调整」的「构建为新信号」单个请求
|
||
* (一个被勾选的信号 + **它自己的**增强等级与迭代次数,与均值滤波同构)。
|
||
*/
|
||
struct WeakPeakBuild
|
||
{
|
||
int groupIndex = -1; /**< 信号所属分组下标。 */
|
||
int signalIndex = -1; /**< 组内信号下标。 */
|
||
int amountPercent = 0; /**< 弱峰调整幅度(-400–400,默认 0 = 不改动)。 */
|
||
int iterations = 1; /**< 迭代次数 N(1–10)。 */
|
||
};
|
||
|
||
/**
|
||
* @brief 取出「弱峰调整 → 构建为新信号」请求(由面板底部按钮触发)。
|
||
* @param [OUT] laneIndex 触发该请求的网格下标。
|
||
* @param [OUT] items 需要构建的信号及其参数(按信号在网格内的顺序)。
|
||
* @return 存在待处理请求返回 true(取出后清空)。
|
||
*/
|
||
bool TakePendingWeakPeakBuild(int& laneIndex, std::vector<WeakPeakBuild>& items);
|
||
|
||
/**
|
||
* @brief 切换某网格的「弱峰调整」开关。
|
||
* @param [IN] laneIndex 网格下标。
|
||
* @param [IN] enabled 是否开启。
|
||
* @return 状态发生变化返回 true。
|
||
*/
|
||
bool SetLaneWeakPeak(int laneIndex, bool enabled);
|
||
|
||
/**
|
||
* @brief 查询某网格的「弱峰调整」开关。
|
||
* @param [IN] laneIndex 网格下标。
|
||
* @return 已开启返回 true。
|
||
*/
|
||
bool LaneWeakPeak(int laneIndex) const;
|
||
|
||
/**
|
||
* @brief 切换某网格的「弱峰调整」开关。
|
||
* @param [IN] laneIndex 网格下标。
|
||
* @return 切换后的状态。
|
||
*/
|
||
bool ToggleLaneWeakPeak(int laneIndex);
|
||
|
||
/**
|
||
* @brief 获取弱峰调整曲线(按「信号 + 增强等级 + 迭代次数」缓存)。
|
||
* @param [IN] signal 目标信号。
|
||
* @param [IN] amountPercent 弱峰调整幅度(-400–400,整数百分比)。
|
||
* @param [IN] iterations 迭代次数 N(1–10)。
|
||
* @return 曲线数值数组(长度与 `signal.values` 相同;缺失处为 NaN)。
|
||
*/
|
||
const std::vector<float>& WeakPeakCurveFor(const SignalData& signal, int amountPercent,
|
||
int iterations) const;
|
||
|
||
/**
|
||
* @brief 取某个时刻的弱峰调整曲线取值(用于纵向光标的取值标注)。
|
||
* @param [IN] axis 该信号使用的时间轴。
|
||
* @param [IN] signal 目标信号。
|
||
* @param [IN] amountPercent 弱峰调整幅度(-400–400)。
|
||
* @param [IN] iterations 迭代次数 N(1–10)。
|
||
* @param [IN] time 采样时刻。
|
||
* @param [OUT] value 输出取值。
|
||
* @return 取到有效值返回 true。
|
||
*/
|
||
bool WeakPeakValueAt(const TimeAxis& axis, const SignalData& signal, int amountPercent, int iterations,
|
||
double time, double& value) const;
|
||
|
||
/**
|
||
* @brief 切换某个信号在「弱峰调整」面板中的预览勾选状态。
|
||
* @param [IN] laneIndex 网格下标。
|
||
* @param [IN] ref 目标信号。
|
||
* @return 切换后的勾选状态。
|
||
*/
|
||
bool ToggleLaneWeakPeakSignal(int laneIndex, const SignalRef& ref);
|
||
|
||
/**
|
||
* @brief 查询某个信号在「弱峰调整」面板中的预览勾选状态。
|
||
* @param [IN] laneIndex 网格下标。
|
||
* @param [IN] ref 目标信号。
|
||
* @return 已勾选返回 true。
|
||
*/
|
||
bool LaneWeakPeakSignalEnabled(int laneIndex, const SignalRef& ref) const;
|
||
|
||
/**
|
||
* @brief 切换某网格的「自由标记」开关。
|
||
*
|
||
* 开启后该网格内跟随鼠标显示一条仅限本网格的纵向光标(上下各一个三角把手、
|
||
* 不显示时间读数),光标穿过的信号中**离鼠标最近**的那个会被标注取值;
|
||
* 左键原地单击即把该数据点固定下来。关闭时**保留**已固定的点(用户可再次开启
|
||
* 继续查看),需要清空请用面板上的 ✕。
|
||
*
|
||
* @param [IN] laneIndex 网格下标。
|
||
* @param [IN] enabled 是否开启。
|
||
* @return 状态发生变化返回 true。
|
||
*/
|
||
bool SetLaneFreeMarker(int laneIndex, bool enabled);
|
||
|
||
/**
|
||
* @brief 查询某网格的「自由标记」开关。
|
||
* @param [IN] laneIndex 网格下标。
|
||
* @return 已开启返回 true。
|
||
*/
|
||
bool LaneFreeMarker(int laneIndex) const;
|
||
|
||
/**
|
||
* @brief 切换某网格的「自由标记」开关。
|
||
* @param [IN] laneIndex 网格下标。
|
||
* @return 切换后的状态。
|
||
*/
|
||
bool ToggleLaneFreeMarker(int laneIndex);
|
||
|
||
/**
|
||
* @brief 「重采样 → 构建为新信号」的单个请求(一个被勾选的信号 + 自己的间隔)。
|
||
*/
|
||
struct ResampleBuild
|
||
{
|
||
int groupIndex = -1; /**< 信号所属分组下标。 */
|
||
int signalIndex = -1; /**< 组内信号下标。 */
|
||
double interval = 0.0; /**< 重采样间隔(秒,> 0)。 */
|
||
};
|
||
|
||
/**
|
||
* @brief 取出「重采样 → 构建为新信号」请求(由面板底部按钮触发)。
|
||
* @param [OUT] laneIndex 触发该请求的网格下标。
|
||
* @param [OUT] items 需要构建的信号及其间隔(按信号在网格内的顺序)。
|
||
* @return 存在待处理请求返回 true(取出后清空)。
|
||
*/
|
||
bool TakePendingResampleBuild(int& laneIndex, std::vector<ResampleBuild>& items);
|
||
|
||
/**
|
||
* @brief 开启 / 关闭某网格的「重采样」功能。
|
||
* @param [IN] laneIndex 网格下标。
|
||
* @param [IN] enabled 是否开启。
|
||
* @return 状态发生变化返回 true。
|
||
*/
|
||
bool SetLaneResample(int laneIndex, bool enabled);
|
||
|
||
/** @brief 查询某网格的「重采样」开关。 */
|
||
bool LaneResample(int laneIndex) const;
|
||
|
||
/**
|
||
* @brief 切换某网格的「重采样」开关。
|
||
* @param [IN] laneIndex 网格下标。
|
||
* @return 切换后的状态。
|
||
*/
|
||
bool ToggleLaneResample(int laneIndex);
|
||
|
||
/** @brief 切换某信号是否参与重采样(面板条目栏右侧复选框)。 */
|
||
bool ToggleLaneResampleSignal(int laneIndex, const SignalRef& ref);
|
||
|
||
/** @brief 查询某信号是否参与重采样。 */
|
||
bool LaneResampleSignalEnabled(int laneIndex, const SignalRef& ref) const;
|
||
|
||
/**
|
||
* @brief 设置「重采样」面板内容的滚动偏移(自动夹到合法范围)。
|
||
* @param [IN] laneIndex 网格下标。 @param [IN] scroll 目标偏移(像素)。
|
||
*/
|
||
bool SetLaneResampleScroll(int laneIndex, float scroll);
|
||
|
||
/**
|
||
* @brief 查询某信号的最大 / 最小数据间隔(相邻有效采样点的时间差极值)。
|
||
*
|
||
* 只统计**时间轴与采样值都有效**的相邻采样对;不足两个有效采样时返回 false。
|
||
*
|
||
* @param [IN] ref 目标信号。
|
||
* @param [OUT] maxInterval 最大间隔(秒)。
|
||
* @param [OUT] minInterval 最小间隔(秒)。
|
||
* @return 至少存在一个有效间隔返回 true。
|
||
*/
|
||
bool SignalDataInterval(const SignalRef& ref, double& maxInterval, double& minInterval) const;
|
||
|
||
/**
|
||
* @brief 设置某网格是否处于「自由标记点选取」(浮动面板底部的切换按钮)。
|
||
*
|
||
* 关闭(默认)= **普通鼠标**:不预览数据点、不画纵向光标、单击不固定任何点,
|
||
* 按下/拖动/单击完全是常规语义(平移 / 框选 / 拖信号)。
|
||
* 开启时网格内始终跟随鼠标画一条纵向光标,并在贴近曲线时预览/固定数据点。
|
||
*
|
||
* @param [IN] laneIndex 网格下标。
|
||
* @param [IN] enabled 是否启用「自由标记点选取」。
|
||
* @return 状态发生变化返回 true。
|
||
*/
|
||
bool SetLaneFreeMarkerCursor(int laneIndex, bool enabled);
|
||
|
||
/**
|
||
* @brief 查询某网格是否开启了「自由标记点选取」。
|
||
* @param [IN] laneIndex 网格下标。
|
||
* @return 启用返回 true(该网格未开启自由标记时恒为 false)。
|
||
*/
|
||
bool LaneFreeMarkerCursor(int laneIndex) const;
|
||
|
||
/**
|
||
* @brief 切换某网格的「自由标记点选取」。
|
||
* @param [IN] laneIndex 网格下标。
|
||
* @return 切换后的状态。
|
||
*/
|
||
bool ToggleLaneFreeMarkerCursor(int laneIndex);
|
||
|
||
/**
|
||
* @brief 设置某个网格「自由标记」固定点显示是否附带**括号内增量**。
|
||
*
|
||
* 只影响已固定点的取值胶囊(面板的「显示增量」开关),
|
||
* 不影响跟随鼠标的预览胶囊与统计列。
|
||
* @param [IN] laneIndex 网格下标。
|
||
* @param [IN] enabled 是否显示增量。
|
||
* @return 状态发生变化返回 true。
|
||
*/
|
||
bool SetLaneFreeMarkerShowDelta(int laneIndex, bool enabled);
|
||
|
||
/**
|
||
* @brief 设置某个网格「自由标记」固定点显示是否附带**枚举文本**。
|
||
*
|
||
* 只影响已固定点的取值胶囊(面板的「显示枚举文本」开关),
|
||
* 不影响跟随鼠标的预览胶囊与统计列。
|
||
* @param [IN] laneIndex 网格下标。
|
||
* @param [IN] enabled 是否显示枚举文本。
|
||
* @return 状态发生变化返回 true。
|
||
*/
|
||
bool SetLaneFreeMarkerShowEnum(int laneIndex, bool enabled);
|
||
|
||
/** 查询某网格固定点是否显示增量。 */
|
||
bool LaneFreeMarkerShowDelta(int laneIndex) const;
|
||
|
||
/** 查询某网格固定点是否显示枚举文本。 */
|
||
bool LaneFreeMarkerShowEnum(int laneIndex) const;
|
||
|
||
/**
|
||
* @brief 设置某网格「自由标记」固定点的**显示不透明度**(0–1)。
|
||
*
|
||
* 只影响网格内固定数据点的显示内容(数据点 / 取值胶囊 / ✕),
|
||
* 面板自身的像素与交互不受影响。
|
||
*/
|
||
bool SetLaneFreeMarkerOpacity(int laneIndex, float opacity);
|
||
|
||
/**
|
||
* @brief 设置某网格「自由标记」面板内容的滚动偏移(客户区像素,0 = 顶部)。
|
||
* @return 偏移真的发生变化返回 true。
|
||
*/
|
||
bool SetLaneFreeMarkerScroll(int laneIndex, float scroll);
|
||
|
||
/**
|
||
* @brief 设置某网格「均值滤波」面板内容的滚动偏移(客户区像素,0 = 顶部)。
|
||
* @return 偏移真的发生变化返回 true。
|
||
*/
|
||
bool SetLaneMeanFilterScroll(int laneIndex, float scroll);
|
||
|
||
/**
|
||
* @brief 设置某网格「中心轴边沿计数」面板内容的滚动偏移(客户区像素,0 = 顶部)。
|
||
* @return 偏移真的发生变化返回 true。
|
||
*/
|
||
bool SetLaneEdgeCenterScroll(int laneIndex, float scroll);
|
||
|
||
/**
|
||
* @brief 设置某网格「弱峰调整」面板内容的滚动偏移(客户区像素,0 = 顶部)。
|
||
* @return 偏移真的发生变化返回 true。
|
||
*/
|
||
bool SetLaneWeakPeakScroll(int laneIndex, float scroll);
|
||
|
||
/**
|
||
* @brief 设置某网格「边沿计数」面板内容的滚动偏移(客户区像素,0 = 顶部)。
|
||
*
|
||
* 该面板仍是旧式「每信号两行」排版,但内容可以滚动:上百个信号合并到同一网格时
|
||
* 内容高达上万像素,不限高会让面板远超绘图区(面板像素缓存因此完全失效)。
|
||
* @return 偏移真的发生变化返回 true。
|
||
*/
|
||
bool SetLaneEdgeCountScroll(int laneIndex, float scroll);
|
||
|
||
/**
|
||
* @brief 切换某网格「自由标记」面板里某个信号分组的收起状态。
|
||
* @return 切换后为「已收起」返回 true。
|
||
*/
|
||
bool ToggleLaneFreeMarkerGroup(int laneIndex, const SignalRef& ref);
|
||
|
||
/**
|
||
* @brief 选中「自由标记」面板里的某个条目(`-1` = 取消选中)。
|
||
*
|
||
* 选中的行在面板里高亮;它与网格内的固定点一一对应。
|
||
* @param [IN] laneIndex 网格下标。
|
||
* @param [IN] itemIndex `Lane::freeMarkerItems` 下标(越界会被夹成“取消选中”)。
|
||
* @return 选中状态发生变化返回 true。
|
||
*/
|
||
bool SelectLaneFreeMarker(int laneIndex, int itemIndex);
|
||
|
||
/**
|
||
* @brief 切换某个实验性功能浮动面板的**折起**状态(只显示标题栏)。
|
||
* @param [IN] laneIndex 网格下标。
|
||
* @param [IN] kind 面板种类。
|
||
* @param [IN] collapsed 是否折起。
|
||
* @return 状态发生变化返回 true。
|
||
*/
|
||
bool SetLanePanelCollapsed(int laneIndex, EdgePanelKind kind, bool collapsed);
|
||
|
||
/**
|
||
* @brief 查询某个实验性功能浮动面板是否处于折起状态。
|
||
*/
|
||
bool LanePanelCollapsed(int laneIndex, EdgePanelKind kind) const;
|
||
|
||
/**
|
||
* @brief 切换某个实验性功能浮动面板的折起状态。
|
||
* @return 切换后的状态(true = 已折起)。
|
||
*/
|
||
bool ToggleLanePanelCollapsed(int laneIndex, EdgePanelKind kind);
|
||
|
||
/**
|
||
* @brief 直接把文本解析为网格 Y 轴上限/下限并应用(供菜单内联输入框提交使用)。
|
||
* @param [IN] laneIndex 网格下标。
|
||
* @param [IN] editMax true 写上限,false 写下限。
|
||
* @param [IN] text 输入文本。
|
||
* @return 解析并应用成功返回 true。
|
||
*/
|
||
bool ApplyLaneValueText(int laneIndex, bool editMax, const std::wstring& text);
|
||
|
||
/**
|
||
* @brief 查询某网格当前的 Y 轴显示范围。
|
||
* @param [IN] laneIndex 网格下标。
|
||
* @param [OUT] yMin 输出下限。
|
||
* @param [OUT] yMax 输出上限。
|
||
* @return 网格下标合法返回 true。
|
||
*/
|
||
bool LaneRange(int laneIndex, double& yMin, double& yMax) const;
|
||
|
||
/**
|
||
* @brief 判断是否处于任意内联数值输入状态。
|
||
* @return 处于编辑态返回 true。
|
||
*/
|
||
bool IsEditing() const override { return m_editTarget != PlotEditTarget::NONE; }
|
||
|
||
/**
|
||
* @brief 取出并清除待应用的信号重命名请求。
|
||
* @param [OUT] ref 被重命名的信号引用。
|
||
* @param [OUT] name 新的显示名称。
|
||
* @return 存在待处理请求返回 true。
|
||
* @note 绘图区只持有只读数据层,重命名需由宿主窗口落到 DataSet 上。
|
||
*/
|
||
bool TakePendingRename(SignalRef& ref, std::wstring& name);
|
||
|
||
/**
|
||
* @brief 设置「外部悬停信号」(信号列表 / 样式区「目标」行悬停到某条信号时调用)。
|
||
*
|
||
* 用户 2026-10-15 要求:
|
||
* * 鼠标**在网格里移动不再触发**网格变色高亮(旧行为已取消);
|
||
* * 改为**在信号列表里悬停某条信号**时,若该信号已勾选绘制到某个网格,
|
||
* 则该网格变色高亮,并且该信号曲线本身也高亮;鼠标移开即恢复。
|
||
*
|
||
* ⚠️ 与 `m_hoverSignal`(鼠标在绘图区内直接命中的信号)**分开保存**:绘图区的
|
||
* `OnMouseLeave` 会清掉后者,而列表悬停时鼠标恰恰不在绘图区内。
|
||
*
|
||
* @param [IN] ref 悬停的信号;无效引用表示取消外部悬停。
|
||
*/
|
||
void SetExternalHoverSignal(const SignalRef& ref);
|
||
|
||
/**
|
||
* @brief 当前外部悬停的信号(供验证 / 宿主查询)。
|
||
* @return 悬停的信号引用,无则无效。
|
||
*/
|
||
const SignalRef& ExternalHoverSignal() const { return m_externalHoverSignal; }
|
||
|
||
/**
|
||
* @brief 取出并清除「请把信号列表滚动到该信号」的联动请求。
|
||
*
|
||
* 用户 2026-10-15 要求:在绘图区触发联动(点击图例 / 点击信号曲线等把某条信号选为
|
||
* 当前目标)时,信号选择区域要**平滑滚动**到该信号并完整显示它 ——
|
||
* 信号很多时(如 MF4 的 124 条)否则找不见选中的那一条。
|
||
*
|
||
* @param [OUT] ref 需要滚动到的信号。
|
||
* @return 存在待处理的请求返回 true。
|
||
*/
|
||
bool TakePanelScrollRequest(SignalRef& ref);
|
||
|
||
/**
|
||
* @brief 把指定网格**平滑滚动**到完整可见(带动画,网格不在可视区时自动调节滚动条)。
|
||
*
|
||
* 用户 2026-10-16 要求:信号很多时(例如把 MF4 的上百条信号各自放进独立网格)
|
||
* 每个网格只占绘图区的一小段,绝大多数网格都在可视区之外 —— 在信号列表里点选一条
|
||
* 信号后,用户无法从画面上的高亮看出它究竟在哪个网格。因此点选信号时:
|
||
* * 目标网格在可视区**上方** ⇒ 向上滚动直到它的上边界进入绘图区上沿;
|
||
* * 目标网格在可视区**下方** ⇒ 向下滚动直到它的下边界进入绘图区下沿;
|
||
* * 已经完整可见 ⇒ 不动(避免无意义地改变视图)。
|
||
*
|
||
* ⚠️ 过渡必须是**带动画的平滑滚动**(与信号列表的“滚动到目标信号”同源:ease-out cubic、
|
||
* 时长 `clamp(距离×0.35ms, 130, 300)`),不是直接把视界瞬移过去 —— 用户 2026-10-16 明确要求。
|
||
* 动画由宿主 16ms 定时器驱动(`AdvanceLaneScrollAnimation`)。
|
||
*
|
||
* ⚠️ 只在**点击**信号时调用(悬停只做高亮联动,绝不改变视图)。
|
||
*
|
||
* @param [IN] laneIndex 网格下标。
|
||
* @return 开始了滚动(动画或已到位)返回 true;已完整可见则返回 false。
|
||
*/
|
||
bool ScrollLaneIntoView(int laneIndex);
|
||
|
||
/**
|
||
* @brief 推进一步网格滚动动画(由宿主定时器驱动)。
|
||
* @return 动画仍在进行返回 true(宿主应保持定时重绘)。
|
||
*/
|
||
bool AdvanceLaneScrollAnimation();
|
||
|
||
/** @brief 网格滚动动画是否正在进行。 */
|
||
bool LaneScrollAnimationActive() const { return m_laneScrollAnimActive; }
|
||
|
||
/**
|
||
* @brief 把网格滚动**平滑地**回到最上方(供右下角浮动按钮调用)。
|
||
* @return 开始了动画(或已在上方)返回 true。
|
||
*/
|
||
bool ScrollLanesToTop();
|
||
|
||
/**
|
||
* @brief 右下角「回到最上方」浮动按钮是否可见(含淡入淡出过程中的可见性)。
|
||
* @return 可见返回 true。
|
||
*/
|
||
bool BackToTopVisible() const { return m_backToTopAlpha > 0.01f; }
|
||
|
||
/**
|
||
* @brief 推进一步「回到最上方」按钮的淡入/淡出过渡(由宿主定时器驱动)。
|
||
* @return 仍在过渡中返回 true(宿主应保持定时重绘)。
|
||
*/
|
||
bool AdvanceBackToTopAnimation();
|
||
|
||
/** @brief 「回到最上方」按钮是否正在淡入/淡出。 */
|
||
bool BackToTopAnimationActive() const { return m_backToTopAnimating; }
|
||
|
||
/**
|
||
* @brief 上一条绘制帧是否**冻结了实验性功能面板的缓存键**(交互帧)。
|
||
*
|
||
* 冻结期间面板显示的是旧像素(统计数字停在“统计中…”),而鼠标停下后
|
||
* **不会再有重绘请求** ⇒ 面板永远停在旧数字。宿主应在为真时武装短周期定时器,
|
||
* 直到某个非交互帧把面板按最新数字重画一次(见 `Application::Render`)。
|
||
*/
|
||
bool PanelRefreshPending() const { return m_panelKeysFrozen; }
|
||
|
||
/**
|
||
* @brief 告知绘图区「宿主正在加载文件」。
|
||
*
|
||
* 加载期间主线程会不断 `UpdateWindow` 触发整帧绘制,而面板绘制含统计推进与上百行
|
||
* GDI+ 原语 ⇒ 加载进度被拖得很慢(用户报告"统计没算完就无法开始加载")。
|
||
* 加载遮罩会盖住整个界面,因此置真时直接跳过面板绘制与统计推进。
|
||
*
|
||
* @param [IN] busy true = 正在加载。
|
||
*/
|
||
void SetLoadingBusy(bool busy) { m_loadingBusy = busy; }
|
||
|
||
|
||
/**
|
||
* @brief 取消网格滚动动画(用户自己滚动网格时调用,避免动画与手动滚动抢滚动位置)。
|
||
*/
|
||
void CancelLaneScrollAnimation() { m_laneScrollAnimActive = false; }
|
||
|
||
/**
|
||
* @brief 处理编辑态下的字符输入。
|
||
* @param [IN] character 输入字符。
|
||
* @return 界面状态发生变化返回 true。
|
||
*/
|
||
bool OnEditChar(wchar_t character);
|
||
|
||
/**
|
||
* @brief 处理编辑态下的按键。
|
||
* @param [IN] key 虚拟键码。
|
||
* @return 界面状态发生变化返回 true。
|
||
*/
|
||
bool OnEditKey(WPARAM key);
|
||
|
||
/**
|
||
* @brief 编辑态下按下鼠标右键:取消选区并把插入符移到文本末尾。
|
||
* @return 发生改变返回 true;不在编辑态返回 false。
|
||
*/
|
||
bool CollapseEditSelectionToEnd();
|
||
|
||
/**
|
||
* @brief 编辑态下在输入框内按下鼠标左键:定位插入符并开始拖拽框选。
|
||
* @param [IN] x 客户区横坐标。
|
||
* @param [IN] y 客户区纵坐标。
|
||
* @return 命中输入框返回 true。
|
||
*/
|
||
bool OnEditMouseDown(int x, int y);
|
||
|
||
/**
|
||
* @brief 编辑态下拖拽鼠标:扩展选区。
|
||
* @param [IN] x 客户区横坐标。
|
||
* @return 选区发生变化返回 true。
|
||
*/
|
||
bool OnEditMouseMove(int x);
|
||
|
||
/**
|
||
* @brief 编辑态下松开鼠标左键:结束拖拽框选。
|
||
* @return 之前处于拖拽中返回 true。
|
||
*/
|
||
bool OnEditMouseUp();
|
||
|
||
/**
|
||
* @brief 重新生成网格几何(可见性或网格划分变化后调用)。
|
||
*/
|
||
void RefreshLanes();
|
||
|
||
/**
|
||
* @brief 设置「前台正在播放 UI 动画」标志(信号列表滚动 / 分组折起展开动画)。
|
||
*
|
||
* ⚠️ 为 true 时**暂停网格图层预热**(`WarmLaneLayersStep`):预热每帧要重画 1 个网格图层,
|
||
* 而单个巨型信号(2500 万采样)的图层重建实测 **10~30ms** —— 它会直接把 60fps 的
|
||
* 动画打成一帧一跳(用户 2026-10-18 报告:信号列表只有文字,滚动/展开却明显卡顿)。
|
||
* 动画结束后预热会自然继续(待重画队列不丢),因此只是"推迟",不改变最终画面。
|
||
* @param [IN] busy 是否正在播放动画。
|
||
*/
|
||
void SetAnimationBusy(bool busy) { m_animationBusy = busy; }
|
||
|
||
private:
|
||
/** @brief 前台是否正在播放 UI 动画(见 `SetAnimationBusy`)。 */
|
||
bool m_animationBusy = false;
|
||
|
||
/** @brief 是否禁用全部快速绘制路径(见 `SetForceFullRepaint`)。 */
|
||
bool m_forceFullRepaint = false;
|
||
|
||
public:
|
||
/**
|
||
* @brief 单个网格的绘制信息。
|
||
*/
|
||
struct LaneVisual
|
||
{
|
||
gd::RectF rect; /**< 网格波形区域。 */
|
||
double yMin = 0.0; /**< 幅值下限。 */
|
||
double yMax = 1.0; /**< 幅值上限。 */
|
||
};
|
||
|
||
/**
|
||
* @brief 已解析的信号视图:数据与显示配置的组合。
|
||
*/
|
||
struct ResolvedSignal
|
||
{
|
||
const SignalData* data = nullptr; /**< 信号数据(含全局共享的显示外观)。 */
|
||
const SignalStyle* style = nullptr; /**< 显示外观。 */
|
||
const SignalGroup* group = nullptr; /**< 信号所属分组(仅用于分组级信息)。 */
|
||
const TimeAxis* axis = nullptr; /**< 该信号使用的时间轴(采样值 ↔ 时刻的唯一依据)。 */
|
||
bool visible = false; /**< 在当前标签页中是否可见。 */
|
||
};
|
||
|
||
/**
|
||
* @brief 获取当前生效的标签页配置(未绑定时返回内部占位对象)。
|
||
* @return 标签页配置引用。
|
||
*/
|
||
TabConfig& Tab() { return (m_tab != nullptr) ? *m_tab : m_localTab; }
|
||
|
||
/**
|
||
* @brief 获取当前生效的标签页配置(只读)。
|
||
* @return 标签页配置引用。
|
||
*/
|
||
const TabConfig& Tab() const { return (m_tab != nullptr) ? *m_tab : m_localTab; }
|
||
|
||
/**
|
||
* @brief 判断是否存在可绘制内容(已加载数据且至少有一条可见信号)。
|
||
* @return 可绘制返回 true。
|
||
*/
|
||
bool HasDrawableContent() const;
|
||
|
||
/**
|
||
* @brief 计算当前视图应使用的时间范围。
|
||
*
|
||
* 返回的是**显示时间**(采样实际画在 `t + SignalStyle::offsetTime` 处),
|
||
* 因此"适应视图"与视图裁剪都会把时间轴偏移算进去。
|
||
* @param [OUT] low 时间下限。
|
||
* @param [OUT] high 时间上限。
|
||
*/
|
||
void TimeBounds(double& low, double& high) const;
|
||
|
||
/**
|
||
* @brief 解析信号引用,得到数据与显示配置。
|
||
* @param [IN] ref 信号引用。
|
||
* @return 解析结果,引用无效时各指针为空。
|
||
*/
|
||
ResolvedSignal ResolveSignal(const SignalRef& ref) const;
|
||
|
||
/**
|
||
* @brief 判断某网格内的可见信号是否来自多个分组。
|
||
* @param [IN] laneIndex 网格下标。
|
||
* @return 跨越多个分组返回 true。
|
||
*/
|
||
bool LaneSpansMultipleGroups(int laneIndex) const;
|
||
|
||
/**
|
||
* @brief 获取当前选中信号(跨标签页共享,切换视图后选中项保持一致)。
|
||
* @return 信号引用。
|
||
*/
|
||
const SignalRef& SelectedRef() const;
|
||
|
||
/**
|
||
* @brief 设置当前选中信号(跨标签页共享)。
|
||
* @param [IN] ref 信号引用。
|
||
*/
|
||
void SelectSignal(const SignalRef& ref);
|
||
|
||
/**
|
||
* @brief 重新计算各网格的布局与幅值范围。
|
||
*/
|
||
void BuildLaneVisuals();
|
||
|
||
/**
|
||
* @brief 限制网格滚动偏移量。
|
||
*/
|
||
void ClampLaneScroll();
|
||
|
||
/**
|
||
* @brief 将时间映射为绘图区横坐标。
|
||
* @param [IN] time 时间值。
|
||
* @return 横坐标。
|
||
*/
|
||
float TimeToX(double time) const;
|
||
|
||
/**
|
||
* @brief 将绘图区横坐标还原为时间。
|
||
* @param [IN] x 横坐标。
|
||
* @return 时间值。
|
||
*/
|
||
double XToTime(float x) const;
|
||
|
||
/**
|
||
* @brief 将幅值映射为纵坐标。
|
||
* @param [IN] value 幅值。
|
||
* @param [IN] lane 网格绘制信息。
|
||
* @return 纵坐标。
|
||
*/
|
||
float ValueToY(double value, const LaneVisual& lane) const;
|
||
|
||
/**
|
||
* @brief 将纵坐标还原为幅值。
|
||
* @param [IN] y 纵坐标。
|
||
* @param [IN] lane 网格绘制信息。
|
||
* @return 幅值。
|
||
*/
|
||
double YToValue(float y, const LaneVisual& lane) const;
|
||
|
||
/**
|
||
* @brief 查询指定纵坐标落在哪个网格内。
|
||
* @param [IN] y 纵坐标。
|
||
* @return 网格下标,未命中返回 -1。
|
||
*/
|
||
int LaneAtY(int y) const;
|
||
|
||
/**
|
||
* @brief 限制时间视图范围,避免过度平移到数据之外。
|
||
*/
|
||
void ClampTimeView();
|
||
|
||
/**
|
||
* @brief 以指定锚点缩放时间轴。
|
||
* @param [IN] factor 缩放倍率,大于 1 表示放大。
|
||
* @param [IN] anchorTime 锚点时间。
|
||
*/
|
||
void ZoomTimeAt(double factor, double anchorTime);
|
||
|
||
/**
|
||
* @brief 将时间轴缩放到指定的时间区间(拖拽选区释放时调用)。
|
||
* @param [IN] x0 选区起点横坐标。
|
||
* @param [IN] x1 选区终点横坐标。
|
||
* @return 产生了有效缩放返回 true。
|
||
*/
|
||
bool ApplyTimeSelection(float x0, float x1);
|
||
|
||
/**
|
||
* @brief 将某个网格的幅值轴缩放到指定区间(拖拽选区释放时调用)。
|
||
* @param [IN] laneIndex 目标网格下标。
|
||
* @param [IN] y0 选区起点纵坐标。
|
||
* @param [IN] y1 选区终点纵坐标。
|
||
* @return 产生了有效缩放返回 true。
|
||
*/
|
||
bool ApplyValueSelection(int laneIndex, float y0, float y1);
|
||
|
||
/**
|
||
* @brief 平移某网格的幅值轴(偏移波形显示位置)。
|
||
* @param [IN] laneIndex 目标网格下标。
|
||
* @param [IN] deltaValue 幅值偏移量。
|
||
*/
|
||
void ShiftLaneRange(int laneIndex, double deltaValue);
|
||
|
||
/**
|
||
* @brief 计算指定时间光标当前的横坐标。
|
||
* @param [IN] which 光标序号(1 或 2)。
|
||
* @return 横坐标。
|
||
*/
|
||
float CursorX(int which) const;
|
||
|
||
/**
|
||
* @brief 计算指定光标在屏幕上的**显示**横坐标(滞留在可视区外时贴到网格边缘)。
|
||
* @param [IN] which 光标序号(1 或 2)。
|
||
* @return 显示横坐标。
|
||
*/
|
||
float CursorDisplayX(int which) const;
|
||
|
||
/**
|
||
* @brief 计算指定光标时间读数标签的矩形。
|
||
* @param [IN] which 光标序号(1 或 2)。
|
||
* @return 标签矩形,位于绘图区底部(不遮挡网格内数据点标注)。
|
||
*/
|
||
gd::RectF CursorLabelRect(int which) const;
|
||
|
||
/**
|
||
* @brief 判断给定横坐标是否落在某条时间光标附近(允许拖拽的容差范围)。
|
||
* @param [IN] x 横坐标。
|
||
* @param [OUT] which 命中的光标序号(1 或 2),未命中时为 0。
|
||
* @return 命中返回 true。
|
||
*/
|
||
bool IsOnCursorLine(int x, int& which) const;
|
||
|
||
/**
|
||
* @brief 将两条光标的时间夹取到当前可见时间范围内。
|
||
*
|
||
* ⚠️ **仅在拖拽光标时使用**。视图变化(平移 / 缩放 / 自适应 / 滚轮 / 方向键)**一律不**调用:
|
||
* 光标是一个固定的**时间位置**,视界移动时它必须留在原处(可以滑出可视区),
|
||
* 此时仅把它的取值/时间读数贴在网格边缘显示。
|
||
*
|
||
* @param [IN] which 要夹取的光标序号(1 或 2)。
|
||
*/
|
||
void ClampCursorToView(int which);
|
||
|
||
/**
|
||
* @brief 计算当前数据下**最小可用的时间跨度**(时间视界的下限)。
|
||
*
|
||
* 下限由两件事共同决定:
|
||
* * 数据本身:`数据跨度 × 1e-9`(与滚轮缩放一致)——小于它就是把视界缩进采样间隔内部;
|
||
* * `double` 的可表示精度:视界宽度小于 `ULP(时间) × 画布宽度` 时,
|
||
* 「像素 → 时间」的映射会退化(相邻像素映射到同一个 double),
|
||
* 鼠标每动一下只能落在少数几个可表示的时间值上,
|
||
* 表现为**光标跟不上鼠标、一拖就跨几百像素或直接贴到视界边缘**。
|
||
*
|
||
* @param [IN] low 视界下限(用于估算时间量级)。
|
||
* @param [IN] high 视界上限。
|
||
* @return 最小可用跨度。
|
||
*/
|
||
double MinimumTimeSpan(double low, double high) const;
|
||
|
||
/**
|
||
* @brief **时间视界的唯一写入入口**(所有不变量的守门人)。
|
||
*
|
||
* 负责:拒绝非有限值、纠正左右颠倒、把跨度抬到 `MinimumTimeSpan` 之上、
|
||
* 以及最后按数据范围做边界收敛。
|
||
*
|
||
* ⚠️ 缩放(滚轮 / 选区放大)、平移、适应视图都必须经过这里:
|
||
* 过去每个路径各自维护“跨度下限”,新增的路径(如选区放大)漏掉了这件事,
|
||
* 于是“能一直放大到视界只有几个 ULP”这类问题反复出现。
|
||
*
|
||
* @param [IN] low 新的视界下限。
|
||
* @param [IN] high 新的视界上限。
|
||
*/
|
||
void SetTimeView(double low, double high);
|
||
|
||
/**
|
||
* @brief **光标时间位置的统一写入入口**(拖拽光标时必须经由此处)。
|
||
*
|
||
* 负责:拒绝非有限值、按 `SnapTimeToSample` 吸附加视界内采样点、
|
||
* 以及把结果夹回可见区间。三步缺一不可——光标必须在**可见区间内**且与鼠标位置对应,
|
||
* 否则用户看到的是“拖拽时光标自己飞到视界边缘”。
|
||
*
|
||
* @param [IN] which 光标序号(1 或 2)。
|
||
* @param [IN] time 目标时间(鼠标位置换算)。
|
||
*/
|
||
void PlaceCursorTime(int which, double time);
|
||
|
||
/**
|
||
* @brief 计算双光标之间的时间差(绝对值)。
|
||
* @return 时间差;非双光标模式返回 0。
|
||
*/
|
||
double CursorSpan() const;
|
||
|
||
/**
|
||
* @brief 判断指定光标是否滞留在可视区之外(时间读数被钉在网格边缘)。
|
||
* @param [IN] which 光标序号(1 或 2)。
|
||
* @param [OUT] offRight true 表示偏出**右**侧视界,false 表示偏出左侧。
|
||
* @return 越界返回 true;光标在视界内返回 false。
|
||
*/
|
||
bool CursorOffViewSide(int which, bool& offRight) const;
|
||
|
||
/**
|
||
* @brief 在读数胶囊上方绘制"数据在视界之外"的方向三角指示。
|
||
* @param [IN] labelRect 胶囊矩形(三角与其水平居中对齐)。
|
||
* @param [IN] offRight true 表示指向右侧(光标在右视界之外)。
|
||
* @param [IN] color 三角颜色(与胶囊同色)。
|
||
*/
|
||
void PaintCursorOffViewTip(gd::Graphics& g, const gd::RectF& labelRect, bool offRight,
|
||
std::uint32_t color);
|
||
|
||
/**
|
||
* @brief 计算双光标差值标签的矩形(时间轴上方、两条时间读数的中间位置)。
|
||
* @param [OUT] insideAxis 为 true 时表示两条光标过近,标签落在主时间轴内部。
|
||
* @return 标签矩形;非双光标模式返回空矩形。
|
||
*/
|
||
gd::RectF CursorSpanRect(bool& insideAxis) const;
|
||
/**
|
||
* @brief 获取指定光标对应的显示颜色。
|
||
* @param [IN] which 光标序号(1 或 2)。
|
||
* @return 颜色。
|
||
*/
|
||
std::uint32_t CursorColor() const;
|
||
|
||
/**
|
||
* @brief 设置并显示一条临时提示(在绘图区底部以浮层方式绘制,短暂停留后淡出)。
|
||
* @param [IN] text 提示文本;传空串则立即清除提示。
|
||
*/
|
||
void SetHint(const std::wstring& text);
|
||
|
||
/**
|
||
* @brief 设置一条"粘滞"提示:直到 `ReleaseHint()` 为止不会淡出。
|
||
*
|
||
* 用于"拖动过程中维持显示"的场景(例如选区拖拽时的右键/ESC 说明)。
|
||
*
|
||
* @param [IN] text 提示文本。
|
||
*/
|
||
void SetHintSticky(const std::wstring& text);
|
||
|
||
/**
|
||
* @brief 结束粘滞状态,并从这里开始重新计时(停留后淡出)。
|
||
*/
|
||
void ReleaseHint();
|
||
|
||
/**
|
||
* @brief 判断坐标是否落在某条光标的**时间读数标签**上。
|
||
* @param [IN] x 客户区横坐标。
|
||
* @param [IN] y 客户区纵坐标。
|
||
* @param [OUT] which 命中的光标序号(1 或 2),未命中时为 0。
|
||
* @return 命中返回 true。
|
||
*/
|
||
bool IsOnCursorTimeLabel(int x, int y, int& which) const;
|
||
|
||
/**
|
||
* @brief 判断坐标是否落在双光标差值标签上。
|
||
* @param [IN] x 客户区横坐标。
|
||
* @param [IN] y 客户区纵坐标。
|
||
* @return 命中返回 true。
|
||
*/
|
||
bool IsOnCursorSpanLabel(int x, int y) const;
|
||
|
||
/**
|
||
* @brief 画文字时给底色胶囊加一层细描边,提升与背景/曲线的对比度。
|
||
* @param [IN] rect 胶囊矩形。
|
||
* @param [IN] radius 圆角半径。
|
||
* @param [IN] outlineColor 描边色(由 `LabelInkFor` 与文字色一并算出的反相色)。
|
||
*/
|
||
void DrawLabelOutline(gd::Graphics& g, const gd::RectF& rect, float radius,
|
||
std::uint32_t outlineColor);
|
||
|
||
/**
|
||
* @brief 精确检测鼠标位置是否命中某条可见信号曲线。
|
||
* @param [IN] x 客户区横坐标。
|
||
* @param [IN] y 客户区纵坐标。
|
||
* @return 命中的信号引用,未命中返回无效引用。
|
||
*/
|
||
SignalRef HitTestSignal(int x, int y) const;
|
||
|
||
/**
|
||
* @brief 计算当前内联输入框的矩形(依编辑目标而定)。
|
||
* @return 输入框矩形,未处于编辑态时返回空矩形。
|
||
*/
|
||
gd::RectF EditFieldRect() const;
|
||
|
||
/**
|
||
* @brief 以给定目标与初始文本进入内联输入状态。
|
||
* @param [IN] target 编辑目标。
|
||
* @param [IN] laneIndex 目标网格下标(光标编辑时忽略)。
|
||
* @param [IN] initialText 初始文本。
|
||
* @return 进入成功返回 true。
|
||
*/
|
||
bool BeginEdit(PlotEditTarget target, int laneIndex, const std::wstring& initialText,
|
||
const gd::RectF& anchorRect = gd::RectF());
|
||
|
||
/**
|
||
* @brief 结束内联输入。
|
||
* @param [IN] commit 是否提交当前输入。
|
||
* @return 状态发生变化返回 true。
|
||
*/
|
||
bool EndEdit(bool commit);
|
||
|
||
/**
|
||
* @brief 构建单条信号在当前视图下的折线点序列。
|
||
* @param [IN] resolved 已解析的信号视图。
|
||
* @param [IN] lane 所属网格绘制信息。
|
||
* @param [OUT] points 输出点序列。
|
||
* @param [OUT] breaks 输出每段折线的起始下标(用于处理缺失值断线)。
|
||
*/
|
||
void BuildPolyline(const ResolvedSignal& resolved, const LaneVisual& lane,
|
||
std::vector<gd::PointF>& points, std::vector<std::size_t>& breaks,
|
||
std::vector<gd::PointF>& band, std::vector<std::size_t>& bandBreaks,
|
||
bool& bandMode, bool& bandSpiky) const;
|
||
|
||
/**
|
||
* @brief 「公式映射」信号的逐分组包络缓存条目(见 `AcquireFormulaEnvelope`)。
|
||
*
|
||
* 键 = 信号 + 采样数组 + 采样数 + 公式文本 + 影响数值的样式字段。
|
||
* `minimums` / `maximums` 存的是**已经套用完整绘制变换**(公式 → 系数 → 绝对值 →
|
||
* 镜像 → Y 偏移)之后、每 `stride` 个采样一个分组的极值(无有效值的分组为 NaN)。
|
||
*/
|
||
struct FormulaEnvelopeEntry
|
||
{
|
||
const void* signal = nullptr; /**< `resolved.data`(区分不同信号)。 */
|
||
const void* values = nullptr; /**< `values.data()`(区分重新加载的数据)。 */
|
||
std::size_t count = 0; /**< 参与统计的采样数。 */
|
||
std::size_t stride = 0; /**< 每个分组覆盖的采样数。 */
|
||
std::wstring formula; /**< 公式文本(空 = 不适用)。 */
|
||
float gain = 1.0f;
|
||
bool absolute = false;
|
||
bool mirrorVertical = false;
|
||
float offsetY = 0.0f;
|
||
std::vector<double> minimums; /**< 每组的绘制值下限。 */
|
||
std::vector<double> maximums; /**< 每组的绘制值上限。 */
|
||
bool valid = false; /**< 是否已成功建立。 */
|
||
};
|
||
|
||
/**
|
||
* @brief 取得(必要时建立)「公式映射」信号的逐分组包络。
|
||
*
|
||
* ⚠️⚠️ **为什么必须单独建一层**:`SignalData::coarseMin/coarseMax` 只存**原始采样**的
|
||
* 分组极值,只有在数值变换**保序**时才能直接缩放使用(见 `DrawTransformOrderPreserving`)。
|
||
* 公式(如 `f(t)^2`、`1/f(t)`)不保序,旧实现在这种情况会退化成**逐点扫描整条信号**:
|
||
* `VERY_BIG_DATA` 单条 2580 万采样时,一次折线构建 ≈ 850ms(缩放一帧就卡一次)。
|
||
* 这里把“变换后的分组极值”算**一次**缓存起来,之后每帧/每次缩放只按分组聚合
|
||
* (几十微秒),且极值仍是逐点统计出来的 ⇒ **不会漏掉尖峰**。
|
||
*
|
||
* 只在公式信号上使用;无公式时返回 nullptr(继续走既有的 `coarseMin/Max` 快路径)。
|
||
*
|
||
* @param [IN] resolved 已解析的信号视图。
|
||
* @return 缓存条目指针(内容有效);不适用或无法建立时返回 nullptr。
|
||
*/
|
||
const FormulaEnvelopeEntry* AcquireFormulaEnvelope(const ResolvedSignal& resolved) const;
|
||
|
||
|
||
/**
|
||
* @brief 绘制单个网格。
|
||
* @param [IN,OUT] g 绘图上下文。
|
||
* @param [IN] laneIndex 网格下标。
|
||
* @param [IN] lane 网格绘制信息。
|
||
* @param [IN] timeTicks 时间刻度序列。
|
||
*/
|
||
void PaintLane(gd::Graphics& g, int laneIndex, const LaneVisual& lane,
|
||
const std::vector<double>& timeTicks);
|
||
|
||
/**
|
||
* @brief 绘制某网格的全部**派生预览曲线**(均值滤波 / 中心轴 / 弱峰调整),叠加在原信号之上。
|
||
*
|
||
* 从 `PaintLane` 抽出,供「宽缓冲区」前台路径复用:宽缓冲只存"基底"(网格/波形),
|
||
* 派生曲线每帧作为叠加层单独绘制,因此拖派生参数滑块时无需重画波形。
|
||
*
|
||
* @param [IN,OUT] g 绘图上下文。
|
||
* @param [IN] laneIndex 网格下标。
|
||
* @param [IN] lane 网格绘制信息。
|
||
*/
|
||
void PaintLaneDerivedOverlays(gd::Graphics& g, int laneIndex, const LaneVisual& lane) const;
|
||
|
||
/**
|
||
* @brief 绘制全部派生预览曲线(可只画悬停高亮的那一条)。
|
||
*
|
||
* `PaintLaneDerivedOverlays`(基底/缓存路径,不带高亮)与
|
||
* `PaintDerivedHoverHighlight`(前景高亮)共用本函数,保证两者的几何完全一致。
|
||
*
|
||
* ⚠️ **高亮绝不能画进静态场景缓存**(用户 2026-11-02 报告“悬停时预览曲线的高亮丢失”):
|
||
* 悬停状态不参与 `SceneContentHash`,命中场景缓存时叠加层根本不会被重画 ——
|
||
* 于是“悬停 → 高亮”永远看不到;而一旦参与哈希,又会退化成“每动一下鼠标就整幅重绘”。
|
||
* 正确做法:缓存里只画普通曲线,高亮那一帧在前景(贴图之后)单独叠加。
|
||
*
|
||
* @param [IN,OUT] g 绘图上下文。
|
||
* @param [IN] laneIndex 网格下标。
|
||
* @param [IN] lane 网格绘制信息。
|
||
* @param [IN] hoverOnly true = 只画 `m_meanRowHoverRef` 在 `m_meanRowHoverKind` 面板里的那条曲线
|
||
* (并带高亮);false = 画全部曲线且不带高亮。
|
||
*/
|
||
void PaintDerivedCurvesFor(gd::Graphics& g, int laneIndex, const LaneVisual& lane,
|
||
bool hoverOnly) const;
|
||
|
||
/**
|
||
* @brief 在**前景**叠加绘制“鼠标所在面板条目栏”对应的预览曲线高亮。
|
||
* @param [IN,OUT] g 绘图上下文。
|
||
*/
|
||
void PaintDerivedHoverHighlight(gd::Graphics& g) const;
|
||
|
||
/**
|
||
* @brief 绘制底部时间轴。
|
||
* @param [IN,OUT] g 绘图上下文。
|
||
* @param [IN] timeTicks 时间刻度序列。
|
||
* @param [IN] timeStep 时间刻度步长。
|
||
*/
|
||
void PaintTimeAxis(gd::Graphics& g, const std::vector<double>& timeTicks, double timeStep);
|
||
|
||
/**
|
||
* @brief 绘制时间光标、选区及其读数。
|
||
* @param [IN,OUT] g 绘图上下文。
|
||
* @param [IN] lanes 网格绘制信息集合。
|
||
*/
|
||
void PaintCursor(gd::Graphics& g, const std::vector<LaneVisual>& lanes);
|
||
|
||
/**
|
||
* @brief 绘制「边沿计数」的横向光标(含两端把手、左侧幅值读数)与统计浮动面板。
|
||
* @param [IN,OUT] g 绘图上下文。
|
||
* @param [IN] lanes 网格绘制信息集合。
|
||
*/
|
||
void PaintEdgeCursor(gd::Graphics& g, const std::vector<LaneVisual>& lanes);
|
||
|
||
/**
|
||
* @brief 确保面板图层缓存位图可用(尺寸不符时重建)。
|
||
* @return 可用返回 true。
|
||
*/
|
||
bool EnsurePanelCache();
|
||
|
||
/** @brief 释放面板图层缓存位图。 */
|
||
void ReleasePanelCache();
|
||
|
||
/**
|
||
* @brief 计算「全局」面板缓存键。
|
||
*
|
||
* 它涵盖所有面板共同依赖的状态(几何、缩放、主题、光标、选区、悬停、统计区间等);
|
||
* 一旦变化,所有面板缓存一律作废(整层重画)。
|
||
*/
|
||
std::uint64_t PanelGlobalKey() const;
|
||
|
||
/**
|
||
* @brief 判断坐标是否命中某网格的「边沿计数」横向光标线。
|
||
* @param [IN] x 客户区横坐标。
|
||
* @param [IN] y 客户区纵坐标。
|
||
* @param [OUT] laneIndex 命中的网格下标。
|
||
* @return 命中返回 true。
|
||
*/
|
||
bool IsOnEdgeCursorLine(int x, int y, int& laneIndex) const;
|
||
|
||
/**
|
||
* @brief 实验性功能浮动面板的几何布局(三类面板共用同一套样式)。
|
||
*/
|
||
struct EdgePanelLayout
|
||
{
|
||
gd::RectF panel; /**< 面板整体矩形。 */
|
||
gd::RectF title; /**< 标题栏(**唯一可拖拽区域**)。 */
|
||
gd::RectF close; /**< ✕ 关闭按钮。 */
|
||
/**
|
||
* @brief 标题栏左侧的「展开 / 折起」三角指示器(三角 + 后方一条横线)。
|
||
*
|
||
* 单击它切换面板的折起状态;折起后面板只剩标题栏(内容区完全省略)。
|
||
* 它位于「GX」编号徽标的**左边**。
|
||
*/
|
||
gd::RectF disclosure;
|
||
/** 面板当前是否处于折起状态(只显示标题栏)。 */
|
||
bool collapsed = false;
|
||
/**
|
||
* @brief 标题栏左侧的「GX」编号徽标。
|
||
*
|
||
* 它是**绘制与命中的唯一几何来源**:悬停提示、手型光标、以及
|
||
* “点击把该网格滚动到视界内”都依赖它(用户 2026-10-19 要求)。
|
||
* 面板未展开时为空矩形。
|
||
*/
|
||
gd::RectF badge;
|
||
gd::RectF edit; /**< 面板底部编辑行(无编辑内容时宽度为 0)。 */
|
||
gd::RectF button; /**< 面板底部按钮(无按钮时宽度为 0)。 */
|
||
int rows = 0; /**< 统计行数(每条可见信号一行)。 */
|
||
|
||
/** @brief 表格的一列(左边界 + 宽度,客户区坐标)。 */
|
||
struct TableColumn
|
||
{
|
||
float left = 0.0f;
|
||
float width = 0.0f;
|
||
};
|
||
/**
|
||
* @brief 「中心轴边沿计数」表格的**表头行**(固定不滚动;内容区在它下面)。
|
||
*
|
||
* 该面板自 2026-11-02 起改为表格:表头三列 = 信号名 / 上升沿计数 / 下降沿计数,
|
||
* 下面每条**已勾选**的信号占一行,行底色 = 该信号的配色,三列与表头严格对齐。
|
||
* 单击行(非复选框)展开该信号自己的两行参数滑块。
|
||
*/
|
||
gd::RectF tableHeader;
|
||
/** @brief 表格三列的几何(`0` 信号名 / `1` 上升沿计数 / `2` 下降沿计数)。 */
|
||
std::array<TableColumn, 3> tableColumns{};
|
||
/**
|
||
* @brief 「逐条目」面板的矩形(MEAN / CENTER / PEAK 面板共用同一套排布)。
|
||
*
|
||
* 索引与 `rowRefs` 一一对应(即网格内可见信号的顺序):
|
||
* `rowChecks` 为第一排右侧的复选框;`rowPoints` / `rowIterations` 为**输入框**
|
||
*(MEAN / CENTER 使用);`rowStats` 为第三排的统计结果(中心轴边沿计数专用)。
|
||
* 未勾选的条目对应的矩形均为空。
|
||
*/
|
||
std::vector<gd::RectF> rowChecks;
|
||
std::vector<gd::RectF> rowPoints;
|
||
std::vector<gd::RectF> rowIterations;
|
||
std::vector<gd::RectF> rowStats;
|
||
/**
|
||
* @brief 「弱峰调整」每个被勾选信号自己的两行**滑块**(与 `rowRefs` 同序)。
|
||
*
|
||
* `rowLevelTracks/rowLevelValues` = 增强等级滑块与它右侧的数值文本;
|
||
* `rowIterTracks/rowIterValues` = 迭代次数滑块与它右侧的数值文本
|
||
*(**点值数值文本可以把滑块上限改为别的正整数**)。
|
||
* 未勾选或非 PEAK 面板时对应元素为空矩形(数组也可能为空)。
|
||
*/
|
||
std::vector<gd::RectF> rowLevelTracks;
|
||
std::vector<gd::RectF> rowLevelValues;
|
||
std::vector<gd::RectF> rowIterTracks;
|
||
std::vector<gd::RectF> rowIterValues;
|
||
/**
|
||
* @brief 「弱峰调整」每行滑块右侧的**复位按钮**矩形(与 `rowLevelTracks` 同序)。
|
||
*
|
||
* 点击把该行参数恢复为默认值(幅度 0 / 迭代次数 1 且上限回到初始上限)。
|
||
* 未勾选或非 PEAK 面板时对应元素为空矩形(数组也可能为空)。
|
||
*/
|
||
std::vector<gd::RectF> rowLevelResets;
|
||
std::vector<gd::RectF> rowIterResets;
|
||
/**
|
||
* @brief 「重采样」面板每个被勾选信号的四行内容(与 `rowRefs` 同序)。
|
||
*
|
||
* `rowInfoMaxInterval` / `rowInfoMinInterval` = 只读统计(最大 / 最小数据间隔);
|
||
* `rowIntervalField` / `rowFrequencyField` = 两行**联动输入框**(间隔秒 / 频率 Hz)。
|
||
* 未勾选或非 RESAMPLE 面板时对应元素为空矩形(数组也可能为空)。
|
||
*/
|
||
std::vector<gd::RectF> rowInfoMaxInterval;
|
||
std::vector<gd::RectF> rowInfoMinInterval;
|
||
std::vector<gd::RectF> rowIntervalField;
|
||
std::vector<gd::RectF> rowFrequencyField;
|
||
/**
|
||
* @brief 「均值滤波」面板每个信号的**信号色叠底标题栏**矩形(与 `rowRefs` 同序)。
|
||
*
|
||
* 排版与「自由标记」面板的信号分组栏一致(左侧展开三角 + 信号名),
|
||
* 但右端是**复选框**(`rowChecks`)而不是 ✕。未勾选时标题栏仍显示(用于勾选)。
|
||
*/
|
||
std::vector<gd::RectF> rowHeaders;
|
||
/** @brief 「均值滤波」面板每个信号标题栏左侧的**展开 / 收起三角**(仅勾选后出现)。 */
|
||
std::vector<gd::RectF> rowTriangles;
|
||
std::vector<SignalRef> rowRefs;
|
||
/**
|
||
* @brief 「自由标记」面板的一行(一个固定点)。
|
||
*
|
||
* `itemIndex` 是 `Lane::freeMarkerItems` 的下标(绘制文本、增删都通过它定位,
|
||
* 不依赖面板里的显示顺序);`rect` 是整行矩形(悬停/双击编辑时间用),
|
||
* `close` 是该行末尾的 ✕ 按钮。
|
||
*/
|
||
struct MarkerRow
|
||
{
|
||
std::size_t itemIndex = 0;
|
||
/**
|
||
* @brief 该固定点在**本分组内**的序号(从 1 开始,组内按时间升序)。
|
||
*
|
||
* 面板最左侧「序号」列直接显示它;按分组重置(每组 = 一个信号)。
|
||
*/
|
||
std::size_t serial = 0;
|
||
gd::RectF rect;
|
||
gd::RectF close;
|
||
};
|
||
/** 「自由标记」:每个固定点一行(按信号分组、组内按时间升序展开后的顺序)。 */
|
||
std::vector<MarkerRow> markerRows;
|
||
/**
|
||
* @brief 「自由标记」的一个**信号分组栏**(以信号色为托底底色)。
|
||
*
|
||
* 左侧 = 展开/收起三角 + 信号名;右侧 = 标记点个数(`N 标记点`)+ ✕
|
||
*(清除该信号全部固定点)。单击分组栏(✕ 除外)切换收起状态。
|
||
* 只有至少有一个固定点的信号才会有分组栏。
|
||
*/
|
||
struct MarkerGroup
|
||
{
|
||
SignalRef ref;
|
||
gd::RectF header; /**< 分组栏托底矩形。 */
|
||
/**
|
||
* @brief 分组栏下方的一行**列表头**(取值 / 枚举文本 / 增量 / 时间点 / 时间间隔)。
|
||
*
|
||
* 与条目列共用 `markerColumns` 的列边界 ⇒ 表头与数据严格对齐。
|
||
* 分组被收起(或还没登记)时为空矩形。
|
||
* ⚠️ 滚动时它可能与分组栏一起**悬浮**在内容区顶部(吸顶),
|
||
* 因此它的 Y 不一定是“分组栏 Y + 行高”。
|
||
*/
|
||
gd::RectF tableHeader;
|
||
gd::RectF triangle; /**< 展开/收起三角的命中矩形。 */
|
||
gd::RectF close; /**< 右侧✕的命中矩形。 */
|
||
/**
|
||
* @brief 右侧**复选框**的命中矩形(仅「均值滤波」面板使用;「自由标记」为空)。
|
||
*
|
||
* 均值滤波面板的组头右端是"是否预览该信号"的复选框(用户 2026-10-30 要求),
|
||
* 只有它被勾选时左侧的展开三角才出现、才能展开参数行。
|
||
*/
|
||
gd::RectF check;
|
||
bool collapsed = false;
|
||
/**
|
||
* @brief 该分组栏是否处于**吸顶**(滚动时被钉在内容区顶部)。
|
||
*
|
||
* 吸顶时分组栏与表头会盖住正在向上滚的条目 ⇒ 绘制侧必须把它们画在**条目之后**
|
||
* (否则会被条目覆盖),交互侧也已把这些被盖住的条目从 `markerRows` 里剔除。
|
||
*/
|
||
bool pinned = false;
|
||
};
|
||
/** 「自由标记」:信号分组栏(顺序与面板显示顺序一致)。 */
|
||
std::vector<MarkerGroup> markerGroups;
|
||
/**
|
||
* @brief 「自由标记」面板的**可滚动内容区**(分组栏 + 固定点条目)。
|
||
*
|
||
* 内容高度超过它时出现上下滚动条(不参与滚动的按钮与透明度滑块在它下方)。
|
||
*/
|
||
gd::RectF content;
|
||
/** 「自由标记」面板的**内容总高度**(含被滚动出去的条目),用于换算滚动比例。 */
|
||
float markerContentHeight = 0.0f;
|
||
/**
|
||
* @brief 内容区当前生效的**滚动偏移**(像素,≥ 0)。
|
||
*
|
||
* 「自由标记」与「均值滤波」两个面板的内容区都可能需要滚动;取值来源不同
|
||
* (`Lane::freeMarkerScroll` / `Lane::meanFilterScroll`),但布局之后一律从这里读,
|
||
* 绘制与命中共用同一份偏移(避免两处各自夹取导致"画的和点的不一致")。
|
||
*/
|
||
float scrollOffset = 0.0f;
|
||
gd::RectF scrollTrack; /**< 内容区右侧的滚动条轨道(不需要滚动时为空矩形)。 */
|
||
gd::RectF scrollThumb; /**< 滚动条滑块。 */
|
||
gd::RectF opacityTrack; /**< 「标记透明度」滑动条轨道(总是显示,与选取模式无关)。 */
|
||
gd::RectF opacityValue; /**< 「标记透明度」数值文本矩形。 */
|
||
/**
|
||
* @brief 「显示增量」开关按钮(位于「自由标记点选取」下方那一行的左半)。
|
||
*
|
||
* 它只控制**已固定点**的取值胶囊是否附带括号内增量,
|
||
* 不影响跟随鼠标的预览胶囊,也不影响下面的统计列。
|
||
*/
|
||
gd::RectF showDelta;
|
||
/** @brief 「显示枚举文本」开关按钮(同一行的右半)。 */
|
||
gd::RectF showEnum;
|
||
/**
|
||
* @brief 「自由标记」:每个信号名右侧的「清除该信号全部固定点」✕。
|
||
*
|
||
* 与 `rowRefs` 一一对应;该信号没有任何固定点时为空矩形。
|
||
*/
|
||
std::vector<gd::RectF> rowNameCloses;
|
||
/**
|
||
* @brief 「自由标记」:每个信号**名称行**的矩形(与 `rowRefs` 一一对应)。
|
||
*
|
||
* 绘制只读这个矩形(与 `rowNameCloses` 同一套横坐标),人为主观反推容易与布局漂移。
|
||
*/
|
||
std::vector<gd::RectF> rowNameRects;
|
||
/**
|
||
* @brief 「自由标记」面板的**六列**文本区
|
||
* (① 序号 ② 标记值 ③ 枚举文本 ④ 增量 ⑤ 时间点 ⑥ 时间间隔)。
|
||
*
|
||
* 每行都在这六列的**同一横坐标**上左对齐输出,因此同一信息在纵向上严格对齐;
|
||
* 相邻列之间画一条竖线作为分隔。
|
||
* 列宽取「缺省下限」「实测内容宽度」「列标题宽度」三者的较大者;
|
||
* 当整个面板里没有任何枚举文本时,③ 枚举列的宽度为 0(整列缺省)。
|
||
*/
|
||
struct MarkerColumn
|
||
{
|
||
float left = 0.0f;
|
||
float width = 0.0f;
|
||
};
|
||
std::array<MarkerColumn, 6> markerColumns{};
|
||
/** 面板里是否存在枚举文本(为 false 时 ③ 列整列缺省)。 */
|
||
bool markerHasEnumColumn = false;
|
||
/** 六列的语义下标(`markerColumns` 的索引)。 */
|
||
enum MarkerColumnIndex
|
||
{
|
||
MARKER_COL_SERIAL = 0,
|
||
MARKER_COL_VALUE = 1,
|
||
MARKER_COL_ENUM = 2,
|
||
MARKER_COL_DELTA = 3,
|
||
MARKER_COL_TIME = 4,
|
||
MARKER_COL_DT = 5
|
||
};
|
||
};
|
||
|
||
/**
|
||
* @brief 计算某个实验性功能面板的布局。
|
||
* @param [IN] laneIndex 网格下标。
|
||
* @param [IN] kind 面板种类。
|
||
* @return 布局;面板未展开时各项均为空矩形。
|
||
*/
|
||
EdgePanelLayout EdgePanelLayoutFor(int laneIndex, EdgePanelKind kind) const;
|
||
|
||
/**
|
||
* @brief 「自由标记」跟随光标的解析结果(每次鼠标移动/每帧重算,绘制与命中共用)。
|
||
*
|
||
* 它描述「鼠标此刻所在的、开启了自由标记的网格」里应当标注的那个数据点:
|
||
* 该网格内所有可见信号中**离鼠标最近**的那一个。
|
||
*
|
||
* 两种情形由 `Lane::freeMarkerCursor` 决定:
|
||
* * **未开启「自由标记点选取」**:**没有结果**(`valid == false`)—— 鼠标就是普通鼠标,
|
||
* 不预览数据点、不画纵向光标、单击也不固定任何点(用户 2026-10-23 要求);
|
||
* * **已开启「自由标记点选取」**:网格内始终 `valid == true`(画纵向光标),但只有鼠标
|
||
* **贴近某条信号曲线**时才有可固定的数据点(`ref` 有效)。⚠️ 判据 = 鼠标到**画出来的折线**
|
||
* 的垂直距离 ≤ `kSignalHitTolerance`(与「常规状态下触发信号曲线高亮」**完全同一范围**;
|
||
* 因此阶跃的竖直跳变段也能探测到)。命中后标注时刻会被吸附到最近的**采样时刻**
|
||
* (固定点必须落在真实数据点上),但“已吸附”圆环只在命中「自动点线」稀疏判据时画。
|
||
*/
|
||
struct FreeMarkerProbe
|
||
{
|
||
bool valid = false; /**< 是否有结果(开启选取模式后恒真;`ref` 才是可固定的数据点)。 */
|
||
bool cursorMode = false; /**< 该网格是否处于「自由标记点选取」(决定是否画纵向光标)。 */
|
||
int laneIndex = -1; /**< 该网格下标。 */
|
||
double time = 0.0; /**< 标注时刻(显示时刻;吸附时 = 采样时刻)。 */
|
||
float x = 0.0f; /**< 纵向光标的横坐标(= 鼠标位置,跟随鼠标)。 */
|
||
/**
|
||
* @brief 被标注的那个**数据点**的屏幕横坐标。
|
||
*
|
||
* 与 `x`(鼠标位置)分开:纵向光标画在 `x`,而取值圆点必须落在**真实采样点**上
|
||
*(`TimeToX(time)`)—— 稀疏信号下两者可能相差十几个像素。
|
||
* 用户 2026-10-30 明确要求"光标跟着鼠标走,提示靠纵向最近的数据点"。
|
||
*/
|
||
float pointX = 0.0f;
|
||
SignalRef ref; /**< 就近信号(网格内没有可用信号时无效)。 */
|
||
double value = 0.0; /**< 已叠加「调整」变换的取值(用于标注与增量)。 */
|
||
double enumValue = 0.0; /**< 采样原值(用于查枚举表)。 */
|
||
float y = 0.0f; /**< 数据点的屏幕纵坐标。 */
|
||
bool snapped = false; /**< 是否吸附到了采样点(需要给出吸附提示)。 */
|
||
};
|
||
|
||
/**
|
||
* @brief 解析「自由标记」跟随光标应当标注的数据点。
|
||
* @param [IN] mouseX 鼠标客户区横坐标。
|
||
* @param [IN] mouseY 鼠标客户区纵坐标。
|
||
* @return 解析结果;鼠标不在任何开启该功能的网格内时 `valid == false`。
|
||
*/
|
||
FreeMarkerProbe ProbeFreeMarker(int mouseX, int mouseY) const;
|
||
|
||
/**
|
||
* @brief 绘制「自由标记」:跟随光标 + 全部已固定点(含各自的 ✕)。
|
||
*
|
||
* 同时重建 `m_freeMarkerHits`(固定点 ✕ 的命中矩形),供鼠标悬停与点击使用。
|
||
*/
|
||
void PaintFreeMarker(gd::Graphics& g, const std::vector<LaneVisual>& lanes);
|
||
|
||
/** @brief 一个固定点 ✕ 的命中矩形(绘制时重建,命中测试时读取)。 */
|
||
struct FreeMarkerHit
|
||
{
|
||
int laneIndex = -1;
|
||
std::size_t itemIndex = 0;
|
||
gd::RectF close;
|
||
};
|
||
|
||
/**
|
||
* @brief 「自由标记」某个固定点的**内容**(取值与各种文本)。
|
||
*
|
||
* 这些值与**视界无关**(只取决于采样数据与信号样式),因此单独缓存:
|
||
* 平移视界、拖动时间光标时不需要重新采样/重新格式化。
|
||
* 面板的五列与网格内的取值胶囊共用这一份内容。
|
||
*/
|
||
struct FreeMarkerContentEntry
|
||
{
|
||
std::size_t itemIndex = 0; /**< `Lane::freeMarkerItems` 下标。 */
|
||
bool valid = false; /**< 取值是否有效(无效时全部文本为 `N/A`)。 */
|
||
double value = 0.0; /**< 绘制用取值(用于算胶囊/圆点的纵向位置)。 */
|
||
std::wstring capsuleText; /**< 网格内取值胶囊的文本(`值(±增量) = 枚举`)。 */
|
||
std::wstring valueText; /**< 面板:取值列。 */
|
||
std::wstring enumText; /**< 面板:枚举列(可能是空串)。 */
|
||
std::wstring deltaText; /**< 面板:增量列。 */
|
||
std::wstring timeText; /**< 面板:时刻列。 */
|
||
std::wstring deltaTimeText; /**< 面板:间隔(Δt)列。 */
|
||
std::uint32_t color = 0; /**< 信号色(信号样式里的颜色,已乘不透明度)。 */
|
||
};
|
||
|
||
/** 一个网格的「自由标记」内容缓存(`key` 只含与视界无关的输入)。 */
|
||
struct FreeMarkerContentCache
|
||
{
|
||
std::uint64_t key = 0;
|
||
std::vector<FreeMarkerContentEntry> entries;
|
||
};
|
||
|
||
/**
|
||
* @brief 取某个网格的「自由标记」内容列表(必要时重建,**与视界无关**)。
|
||
*
|
||
* ⚠️ 性能硬约束:大量固定点时,逐帧重新 `SampleDisplayedSignalAt` + 拼字符串
|
||
* 是“拖动全局光标/移动视界变卡”的主因。内容只依赖数据与样式,因此缓存起来;
|
||
* 命中缓存时每帧只做几何与绘制。
|
||
*/
|
||
const std::vector<FreeMarkerContentEntry>& FreeMarkerContents(int laneIndex) const;
|
||
|
||
/** 按网格下标的「自由标记」内容缓存(`mutable`:只在绘制时按需重建)。 */
|
||
mutable std::vector<FreeMarkerContentCache> m_freeMarkerContent;
|
||
|
||
/**
|
||
* @brief 计算某个网格里「第 index 个固定点」的标注几何(绘制与命中共源)。
|
||
*
|
||
* @param [IN] laneIndex 网格下标。
|
||
* @param [IN] itemIndex `Lane::freeMarkerItems` 下标。
|
||
* @param [OUT] label 取值胶囊矩形(**不含** ✕)。
|
||
* @param [OUT] close ✕ 按钮矩形。
|
||
* @param [OUT] text 取值文本(`值(±增量) = 枚举`,绘制侧直接复用,避免重复求解)。
|
||
* @return 该固定点可显示(在视界内、取值有效)返回 true。
|
||
*/
|
||
bool FreeMarkerLabelRects(int laneIndex, std::size_t itemIndex, gd::RectF& label,
|
||
gd::RectF& close, std::wstring& text) const;
|
||
|
||
/**
|
||
* @brief 画一个「取值胶囊」(信号色底 + 高对比度文字 + 可选 ✕ 按钮)。
|
||
*
|
||
* 网格内的固定点与跟随鼠标的预览点共用这一段(两者外观规则完全相同)。
|
||
*
|
||
* @param [IN,OUT] g 绘图上下文。
|
||
* @param [IN] label 胶囊矩形。
|
||
* @param [IN] text 文本。
|
||
* @param [IN] color 信号色。
|
||
* @param [IN] close ✕ 按钮矩形(宽度为 0 表示不画)。
|
||
* @param [IN] closeHot ✕ 是否处于悬停态。
|
||
* @param [IN] alphaScale 整体不透明度系数(1 = 不透明;固定点受「标记透明度」控制)。
|
||
* @param [IN] vectorText 强制用 GDI+(`DrawString`)画文字,即使完全不透明。
|
||
* ⚠️ GDI `DrawTextW`(`UiText`)**不认 GDI+ 裁剪区**;胶囊压在网格的 GX 标签 /
|
||
* 图例上时必须走 GDI+,否则文字会“穿透”到图例上方(用户 2026-10-27 要求标记点
|
||
* 显示在 GX 标签与图例之下)。正常情形仍用 `UiText`(与既有像素保持一致)。
|
||
*/
|
||
void PaintFreeMarkerCapsule(gd::Graphics& g, const gd::RectF& label, const std::wstring& text,
|
||
std::uint32_t color, const gd::RectF& close, bool closeHot,
|
||
float alphaScale = 1.0f, bool vectorText = false);
|
||
|
||
/**
|
||
* @brief 计算某个固定点相对**同信号**前一个固定点的增量与其时间间隔。
|
||
*
|
||
* @param [IN] lane 目标网格。
|
||
* @param [IN] itemIndex `Lane::freeMarkerItems` 下标。
|
||
* @param [IN] value 当前点的取值(已叠加「调整」变换)。
|
||
* @param [OUT] hasPrev 是否存在前一个同信号固定点。
|
||
* @param [OUT] delta 取值增量(当前 − 前一个)。
|
||
* @param [OUT] timeDelta 时间间隔(当前 − 前一个,恒为正)。
|
||
* @return 当前点取值有效返回 true。
|
||
*/
|
||
bool FreeMarkerDelta(const Lane& lane, std::size_t itemIndex, double value, bool& hasPrev,
|
||
double& delta, double& timeDelta) const;
|
||
|
||
/**
|
||
* @brief 计算某个固定点的取值(已叠加「调整」变换)。
|
||
* @param [IN] lane 目标网格。
|
||
* @param [IN] item 固定点。
|
||
* @param [OUT] value 输出取值(已含界面变换)。
|
||
* @param [OUT] enumValue 输出采样原值(用于查枚举表)。
|
||
* @return 取到有效值返回 true。
|
||
*/
|
||
bool FreeMarkerValue(const Lane& lane, const Lane::FreeMarkerItem& item, double& value,
|
||
double& enumValue) const;
|
||
|
||
/**
|
||
* @brief 在面板布局里找出某个信号对应的「逐条目」行下标。
|
||
* @param [IN] layout 面板布局。
|
||
* @param [IN] ref 目标信号。
|
||
* @return 行下标(与 `rowRefs` 同序);未找到返回 -1。
|
||
*/
|
||
int PeakRowIndexOf(const EdgePanelLayout& layout, const SignalRef& ref) const;
|
||
|
||
/**
|
||
* @brief 计算单个面板的缓存键(全局键之外,面板自身的内容)。
|
||
* @param [IN] laneIndex 面板所属网格。
|
||
* @param [IN] kind 面板种类。
|
||
* @param [IN] layout 面板布局。
|
||
* @return 缓存键。
|
||
*/
|
||
std::uint64_t PanelSlotKey(int laneIndex, EdgePanelKind kind,
|
||
const EdgePanelLayout& layout) const;
|
||
|
||
/**
|
||
* @brief 判断某个矩形是否完全落在面板图层位图之内。
|
||
* @param [IN] rect 目标矩形(客户区坐标)。
|
||
* @return 完全落在图层内返回 true。
|
||
*/
|
||
bool PanelRectInsideLayer(const gd::RectF& rect) const;
|
||
|
||
/**
|
||
* @brief 获取多阶(迭代)均值滤波曲线,结果带缓存。
|
||
*
|
||
* 语义:以「均值点数 X」作滑平半径,对输入重复做 `iterations` 次滑动平均。
|
||
* 「实验性功能 → 均值滤波」与「中心轴边沿计数」共用本函数,二者曲线因此逐位一致。
|
||
*
|
||
* @param [IN] signal 目标信号。
|
||
* @param [IN] points 均值点数 X(滑平半径)。
|
||
* @param [IN] iterations 迭代次数 N(至少 1)。
|
||
* @return 曲线数值数组(长度与 `signal.values` 相同;缺失处为 NaN)。
|
||
*/
|
||
const std::vector<float>& MeanFilterCurveFor(const SignalData& signal, int points,
|
||
int iterations) const;
|
||
|
||
/**
|
||
* @brief 取某个时刻的多次迭代均值滤波曲线取值(用于纵向光标的取值标注)。
|
||
* @param [IN] axis 该信号使用的时间轴。
|
||
* @param [IN] signal 目标信号。
|
||
* @param [IN] points 均值点数 X。
|
||
* @param [IN] iterations 迭代次数 N。
|
||
* @param [IN] time 采样时刻。
|
||
* @param [OUT] value 输出取值。
|
||
* @return 取到有效值返回 true。
|
||
*/
|
||
bool MeanFilterValueAt(const TimeAxis& axis, const SignalData& signal, int points, int iterations,
|
||
double time, double& value) const;
|
||
|
||
/**
|
||
* @brief 切换某个信号在「均值滤波」面板中的预览勾选状态。
|
||
* @param [IN] laneIndex 网格下标。
|
||
* @param [IN] ref 目标信号。
|
||
* @return 切换后的勾选状态。
|
||
*/
|
||
bool ToggleLaneMeanFilterSignal(int laneIndex, const SignalRef& ref);
|
||
|
||
/**
|
||
* @brief 查询某个信号在「均值滤波」面板中的预览勾选状态。
|
||
* @param [IN] laneIndex 网格下标。
|
||
* @param [IN] ref 目标信号。
|
||
* @return 已勾选返回 true。
|
||
*/
|
||
bool LaneMeanFilterSignalEnabled(int laneIndex, const SignalRef& ref) const;
|
||
|
||
/**
|
||
* @brief 统计信号穿越“均值滤波曲线”的上/下边沿数量,结果带缓存。
|
||
*
|
||
* 曲线由「均值点数 X + 迭代次数 N」确定,与「实验性功能 → 均值滤波」的预览曲线逐位一致。
|
||
*
|
||
* ⚠️ 判定对象是**画出来的两条曲线**:信号与中心曲线都要先按 `style` 做水平镜像映射、
|
||
* 再套用 `ApplyDrawTransform`(系数 → 绝对值 → 垂直镜像 → Y 偏移)。
|
||
*
|
||
* @param [IN] axis 该信号使用的时间轴。
|
||
* @param [IN] signal 目标信号。
|
||
* @param [IN] style 该信号的外观(含水平镜像与「调整」变换)。
|
||
* @param [IN] points 均值点数 X。
|
||
* @param [IN] iterations 迭代次数 N。
|
||
* @param [IN] hasRange 是否只统计某个时间区间(双光标模式下为光标区间)。
|
||
* @param [IN] rangeMin 区间左端点时间。
|
||
* @param [IN] rangeMax 区间右端点时间。
|
||
* @param [OUT] rising 上升沿数量(信号从下往上越过均值滤波曲线)。
|
||
* @param [OUT] falling 下降沿数量(信号从上往下穿过均值滤波曲线)。
|
||
* @param [OUT] complete 是否为完整精确值(false = 仍在推进,界面显示“…”)。
|
||
*/
|
||
void EdgeCenterCountsFor(const TimeAxis& axis, const SignalData& signal, const SignalStyle& style,
|
||
int points, int iterations, bool hasRange, double rangeMin,
|
||
double rangeMax, std::size_t& rising, std::size_t& falling,
|
||
bool& complete) const;
|
||
|
||
/**
|
||
* @brief 切换某个信号在「中心轴边沿计数」面板中的勾选状态。
|
||
* @param [IN] laneIndex 网格下标。
|
||
* @param [IN] ref 目标信号。
|
||
* @return 切换后的勾选状态。
|
||
*/
|
||
bool ToggleLaneEdgeCenterSignal(int laneIndex, const SignalRef& ref);
|
||
|
||
/**
|
||
* @brief 查询某个信号在「中心轴边沿计数」面板中的勾选状态。
|
||
* @param [IN] laneIndex 网格下标。
|
||
* @param [IN] ref 目标信号。
|
||
* @return 已勾选返回 true。
|
||
*/
|
||
bool LaneEdgeCenterSignalEnabled(int laneIndex, const SignalRef& ref) const;
|
||
|
||
/**
|
||
* @brief 绘制一段派生曲线(中心轴均值滤波 / 多次迭代均值滤波共用同一实现)。
|
||
* @param [IN,OUT] g 绘图上下文。
|
||
* @param [IN] resolved 信号视图。
|
||
* @param [IN] lane 网格绘制信息。
|
||
* @param [IN] curve 曲线数值数组(缺失处为 NaN)。
|
||
* @param [IN] curveBegin `curve[0]` 对应的采样下标。
|
||
* @param [IN] highlight 是否高亮(鼠标悬停在对应的面板条目上时):
|
||
* 先画一圈半透明的"光晕"再把曲线本身画得更粗更亮,让用户一眼看出这条预览曲线对应哪个条目。
|
||
*/
|
||
void PaintDerivedCurve(gd::Graphics& g, const ResolvedSignal& resolved, const LaneVisual& lane,
|
||
const std::vector<float>& curve, std::size_t curveBegin,
|
||
bool highlight = false) const;
|
||
|
||
/**
|
||
* @brief 取得某个采样区间内的派生曲线(带缓存)。
|
||
*
|
||
* ⚠️ **性能关键**:派生曲线只用于“看得见的那一段”,而整条信号可能有数千万个采样点
|
||
* (VERY_BIG_DATA 为 2576 万)。若每次都重算整条曲线(实测弱峰调整 ~800ms/条),
|
||
* 拖动等级/迭代次数滑块时每帧要花几秒钟,交互完全卡死。
|
||
* 本函数只计算“所需区间 ± 保护带”,并按(信号 + 参数 + 对齐后的区间)缓存:
|
||
* 实测单条降到 1~3ms,滑块拖动与平移因此恢复流畅。
|
||
*
|
||
* 保护带保证可见区间内的取值与“整条曲线”一致:均值滤波的核是有限宽度,
|
||
* 弱峰调整的修正沿相邻极值向内传播(每迭代一次传一个极值),
|
||
* 因此只要求保护带足够宽(见 `kDerivedCurveGuard` 系列常量)。
|
||
*
|
||
* @param [IN] kind 曲线种类。
|
||
* @param [IN] signal 目标信号。
|
||
* @param [IN] points 均值点数 X(仅 `MEAN_FILTER` 使用)。
|
||
* @param [IN] iterations 迭代次数 N。
|
||
* @param [IN] amountPercent 弱峰调整幅度(仅 `WEAK_PEAK` 使用,-400–400)。
|
||
* @param [IN] begin 需要的采样区间起点(含)。
|
||
* @param [IN] end 需要的采样区间终点(不含)。
|
||
* @param [OUT] spanBegin 返回数组对应的首个采样下标。
|
||
* @param [IN] allowAsyncCompute 是否允许「后台异步计算」:窗口很大时**不在绘制线程里计算**,
|
||
* 而是交后台线程算,本帧先用已算好的旧曲线顶替(双缓冲),保证交互始终跟手。
|
||
* 仅供**预览绘制**传 true;需要精确取值的统计(中心轴边沿计数、取值胶囊)
|
||
* 传 false,走同步精确计算。
|
||
* @param [IN] computeIfMissing 缓存未命中时是否**进行计算**并发布新条目。
|
||
*
|
||
* ⚠️ 光标取值胶囊必须传 false(用户 2026-11-02 反馈“拖光标仍有明显卡顿”):
|
||
* 胶囊只要“光标所在的那两个采样”,而缓存键含请求窗口、窗口随光标每帧变化 ⇒
|
||
* 每帧都未命中 ⇒ 每帧同步计算 + 发布。15 信号 × 3 种曲线 = 每帧 45 次发布,
|
||
* 条目列表与线性搜索同步膨胀,实测拖动光标单帧 200ms(其中约 150ms 在这里)。
|
||
* 传 false 后:命中就取(绘制路径发布的窗口带保护带、覆盖整个可见区间,
|
||
* 正常情况必然命中),未命中则退到同 (信号, 种类) 的最新曲线,再无则本帧不标注。
|
||
* @return 曲线数组指针(覆盖 `[spanBegin, spanBegin + size())`);暂无可显示曲线时返回 nullptr。
|
||
*/
|
||
const std::vector<float>* CurveSpanFor(DerivedCurveKind kind, const SignalData& signal,
|
||
int points, int iterations, int amountPercent,
|
||
std::size_t begin, std::size_t end,
|
||
std::size_t& spanBegin,
|
||
bool allowAsyncCompute = false,
|
||
bool computeIfMissing = true) const;
|
||
|
||
/**
|
||
* @brief 派生曲线缓存的键(决定一条派生曲线数值的全部输入)。
|
||
*
|
||
* 数据层在两次结构变化之间是只读的,因此 `signal` 指针在此期间稳定;
|
||
* 任何结构变化(增删分组/信号、插入派生信号、切换标签页)前都会暂停后台线程并清空缓存
|
||
*(见 `PauseDerivedAsync`),因此不会出现悬空读取。
|
||
*/
|
||
struct CurveKey
|
||
{
|
||
const SignalData* signal = nullptr; /**< 目标信号。 */
|
||
int kind = 0; /**< `DerivedCurveKind`。 */
|
||
int points = 5; /**< 均值点数 X。 */
|
||
int iterations = 1; /**< 迭代次数 N。 */
|
||
int amountPercent = 0; /**< 弱峰调整幅度(-400–400)。 */
|
||
|
||
bool operator==(const CurveKey& other) const
|
||
{
|
||
return signal == other.signal && kind == other.kind && points == other.points &&
|
||
iterations == other.iterations && amountPercent == other.amountPercent;
|
||
}
|
||
};
|
||
|
||
/**
|
||
* @brief 一条已算好的派生曲线窗口(发布后**不可变**)。
|
||
*
|
||
* 用 `shared_ptr<const CurveEntry>` 持有:绘制线程取到后钉在 `m_curvePins` 中,整帧都引用
|
||
* 同一块内存;后台线程只**追加**新条目、从不修改已发布条目,因此「显示旧缓冲、后台算新缓冲」
|
||
* 的双缓冲模型天然无数据竞争。
|
||
*/
|
||
struct CurveEntry
|
||
{
|
||
CurveKey key; /**< 曲线键。 */
|
||
std::size_t begin = 0; /**< 已计算窗口起点(采样下标)。 */
|
||
std::size_t end = 0; /**< 已计算窗口终点(不含)。 */
|
||
std::uint64_t serial = 0; /**< 发布序号(越大越新,用于选出最新覆盖项)。 */
|
||
std::vector<float> values; /**< 曲线数值(长度 = end - begin)。 */
|
||
};
|
||
using CurveEntryPtr = std::shared_ptr<const CurveEntry>;
|
||
|
||
/** @brief 后台派生曲线计算任务(`windowBegin/End` 已含保护带)。 */
|
||
struct CurveJob
|
||
{
|
||
CurveKey key; /**< 曲线键。 */
|
||
std::size_t windowBegin = 0; /**< 计算窗口起点(含保护带)。 */
|
||
std::size_t windowEnd = 0; /**< 计算窗口终点(不含)。 */
|
||
std::uint64_t generation = 0; /**< 入队时的数据代次(代次变化即丢弃结果)。 */
|
||
std::uint64_t requestSerial = 0; /**< 入队时的“最新请求序号”(被更新请求超越则本任务已过时)。 */
|
||
bool huge = false; /**< 是否为“大窗口”任务(低内存时串行执行并降低并发)。 */
|
||
int hugeLimit = 1; /**< 允许同时运行的“大窗口”任务数(内存充足时 2,紧张时 1)。 */
|
||
};
|
||
|
||
/**
|
||
* @brief 计算派生曲线「含保护带」的实际计算窗口(同步与后台共用同一公式)。
|
||
* @param [IN] kind 曲线种类(均值滤波的保护带需覆盖滤波半径)。
|
||
* @param [IN] alignedBegin 对齐后的需要区间起点。
|
||
* @param [IN] alignedEnd 对齐后的需要区间终点(不含)。
|
||
* @param [IN] points 均值点数 X。
|
||
* @param [IN] iterations 迭代次数 N。
|
||
* @param [IN] count 采样点总数。
|
||
* @param [OUT] begin 计算窗口起点。
|
||
* @param [OUT] end 计算窗口终点(不含)。
|
||
*/
|
||
void DerivedGuardWindow(DerivedCurveKind kind, std::size_t alignedBegin, std::size_t alignedEnd,
|
||
int points, int iterations, std::size_t count, std::size_t& begin,
|
||
std::size_t& end) const;
|
||
|
||
/**
|
||
* @brief 取得覆盖 `[begin, end]` 的最新已算好条目并钉住(防止本帧被回收)。
|
||
* @param [IN] key 曲线键。
|
||
* @param [IN] begin 需要区间起点。
|
||
* @param [IN] end 需要区间终点(不含)。
|
||
* @param [OUT] spanBegin 命中条目的窗口起点。
|
||
* @return 命中条目,未命中返回 nullptr。
|
||
*/
|
||
const CurveEntry* AcquireCoveringCurve(const CurveKey& key, std::size_t begin, std::size_t end,
|
||
std::size_t& spanBegin) const;
|
||
|
||
/**
|
||
* @brief 取得该曲线键下最新的已算好条目并钉住(双缓冲的「旧缓冲」)。
|
||
*
|
||
* ⚠️ **只按「信号 + 曲线种类」匹配,忽略参数**:拖参数滑块时每帧参数都在变,
|
||
* 若要求键完全相同就永远找不到上一帧的曲线,本帧便无曲线可画 → 预览曲线**闪烁**。
|
||
* 旧曲线只是参数略旧,按采样下标映射位置依然正确,先顶上再等后台算好新曲线。
|
||
*
|
||
* @param [IN] key 曲线键(只用其 `signal` 与 `kind`)。
|
||
* @param [IN] begin 需要区间起点(用于优先挑「覆盖该区间」的条目)。
|
||
* @param [IN] end 需要区间终点(不含)。
|
||
* @param [OUT] spanBegin 命中条目的窗口起点。
|
||
* @return 命中条目,无则返回 nullptr。
|
||
*/
|
||
const CurveEntry* AcquireNewestCurve(const CurveKey& key, std::size_t begin, std::size_t end,
|
||
std::size_t& spanBegin) const;
|
||
|
||
/**
|
||
* @brief 发布一条新算好的曲线窗口并钉住。
|
||
* @param [IN] key 曲线键。
|
||
* @param [IN] begin 窗口起点。
|
||
* @param [IN] end 窗口终点(不含)。
|
||
* @param [IN] values 曲线数值(移动入条目)。
|
||
* @param [OUT] spanBegin 发布条目的窗口起点。
|
||
* @return 发布条目的只读指针。
|
||
*/
|
||
const CurveEntry* PublishCurve(const CurveKey& key, std::size_t begin, std::size_t end,
|
||
std::vector<float>&& values, std::size_t& spanBegin) const;
|
||
|
||
/**
|
||
* @brief 把一个派生曲线计算任务排入后台队列(同一曲线键只保留一个在算任务)。
|
||
* @param [IN] key 曲线键。
|
||
* @param [IN] alignedBegin 对齐后的需要区间起点。
|
||
* @param [IN] alignedEnd 对齐后的需要区间终点(不含)。
|
||
* @param [IN] count 采样点总数。
|
||
*/
|
||
void ScheduleCurveJob(const CurveKey& key, std::size_t alignedBegin, std::size_t alignedEnd,
|
||
std::size_t count) const;
|
||
|
||
/** @brief 惰性启动后台计算线程。 */
|
||
void EnsureCurveWorker() const;
|
||
|
||
/** @brief 停止并回收后台计算线程(等待在算任务结束)。 */
|
||
void StopCurveWorker() const;
|
||
|
||
/** @brief 后台计算线程主体。 */
|
||
void CurveWorkerLoop() const;
|
||
|
||
/**
|
||
* @brief 判断一条任务是否已经「过时」(同一信号+曲线种类已有更新的请求)。
|
||
*
|
||
* 拖动参数滑块时参数每帧都在变,旧参数的任务再算下去没有意义;工作线程每阶之间
|
||
* 与开始前都用它检查一次,一旦过时**立即退出**(尽早归还工作缓冲)。
|
||
*
|
||
* @param [IN] key 曲线键。
|
||
* @param [IN] requestSerial 任务入队时的请求序号。
|
||
* @return 已过时返回 true。
|
||
*/
|
||
bool CurveJobIsStale(const CurveKey& key, std::uint64_t requestSerial) const;
|
||
|
||
/**
|
||
* @brief 计算「要装下这条派生曲线」所需的最小内存(字节)。
|
||
* @param [IN] samples 采样点数。
|
||
* @return 最小内存字节数(输出曲线本体 + 内部工作缓冲的经验值)。
|
||
*/
|
||
static std::size_t DerivedMinimumBytes(std::size_t samples);
|
||
|
||
/**
|
||
* @brief 查询当前可用物理内存(字节);查询失败返回极大值(视为“内存充足”)。
|
||
* @return 可用物理内存字节数。
|
||
*/
|
||
static std::size_t AvailablePhysicalBytes();
|
||
|
||
/**
|
||
* @brief 判断现在是否适合开始一条「整段信号」的派生曲线计算。
|
||
*
|
||
* * 可用内存 ≥ 最小需求的 4 倍 → 正常(并行、不节流,尽可能快);
|
||
* * 可用内存在最小需求 ~ 4 倍之间 → **低内存模式**(串行 + 节流,少量多次慢慢算完);
|
||
* * 可用内存 < 最小需求 → **严重不足**:不开始计算,并(仅一次)通知宿主告警。
|
||
*
|
||
* @param [IN] samples 采样点数。
|
||
* @param [OUT] throttle 输出是否应节流(低内存模式)。
|
||
* @return 允许开始计算返回 true。
|
||
*/
|
||
bool DerivedMemoryAllows(std::size_t samples, bool& throttle) const;
|
||
|
||
/** @brief 告知宿主“可用内存严重不足,已暂停派生曲线计算”(同一轮只提示一次)。 */
|
||
void NotifyDerivedLowMemory() const;
|
||
|
||
/** @brief 清理已被更新条目覆盖或超出内存预算的缓存条目(调用方须持有 `m_curveMutex`)。 */
|
||
void PruneCurveEntriesLocked() const;
|
||
|
||
/**
|
||
* @brief 计算当前视图对应的采样下标区间(时间单调时二分定位,否则为整段)。
|
||
* @param [IN] axis 目标时间轴。
|
||
* @param [IN] count 可用采样点数。
|
||
* @param [OUT] begin 区间起点(含)。
|
||
* @param [OUT] end 区间终点(不含)。
|
||
* @param [IN] timeShift 信号的时间偏移:采样画在 `t + timeShift` 处,因此可见采样对应的
|
||
* 源时刻区间为 `[ViewMin - timeShift, ViewMax - timeShift]`(偏移 0 时同视界)。
|
||
*/
|
||
void VisibleIndexRange(const TimeAxis& axis, std::size_t count, std::size_t& begin,
|
||
std::size_t& end, double timeShift = 0.0) const;
|
||
|
||
/**
|
||
* @brief 绘制某条信号的多次迭代均值滤波曲线(与中心平均值曲线同一配色派生规则)。
|
||
* @param [IN,OUT] g 绘图上下文。
|
||
* @param [IN] resolved 信号视图。
|
||
* @param [IN] lane 网格绘制信息。
|
||
* @param [IN] points 均值点数 X(滑平半径)。
|
||
* @param [IN] iterations 迭代次数 N(至少 1)。
|
||
* @param [IN] highlight 是否高亮(鼠标悬停在该网格均值滤波面板的对应条目上)。
|
||
*/
|
||
void PaintMeanFilterCurve(gd::Graphics& g, const ResolvedSignal& resolved, const LaneVisual& lane,
|
||
int points, int iterations, bool highlight = false) const;
|
||
|
||
/**
|
||
* @brief 绘制某条信号的弱峰调整曲线(与均值滤波曲线同一配色派生规则)。
|
||
* @param [IN,OUT] g 绘图上下文。
|
||
* @param [IN] resolved 信号视图。
|
||
* @param [IN] lane 网格绘制信息。
|
||
* @param [IN] amountPercent 弱峰调整幅度(-400–400)。
|
||
* @param [IN] iterations 迭代次数 N(1–10)。
|
||
* @param [IN] highlight 是否高亮(鼠标悬停在该网格弱峰调整面板的对应条目上)。
|
||
*/
|
||
void PaintWeakPeakCurve(gd::Graphics& g, const ResolvedSignal& resolved, const LaneVisual& lane,
|
||
int amountPercent, int iterations, bool highlight = false) const;
|
||
|
||
/**
|
||
* @brief 数据点标记的位置与"越界方向"(用于决定画圆点还是贴边三角)。
|
||
*/
|
||
struct DataMarker
|
||
{
|
||
float x = 0.0f; /**< 标记锚点横坐标(已夹在可视区内)。 */
|
||
float y = 0.0f; /**< 标记锚点纵坐标(已夹在可视区内)。 */
|
||
bool offLeft = false; /**< 数据点位于左侧视界之外。 */
|
||
bool offRight = false; /**< 数据点位于右侧视界之外。 */
|
||
bool offUp = false; /**< 数据点位于上方视界之外。 */
|
||
bool offDown = false; /**< 数据点位于下方视界之外。 */
|
||
};
|
||
|
||
/**
|
||
* @brief 计算数据点标记:把越界的数据点"钉"在网格可见边缘,并记录越界方向。
|
||
* @param [IN] rawX 数据点的原始横坐标。
|
||
* @param [IN] rawY 数据点的原始纵坐标。
|
||
* @param [IN] pinX 横向贴边位置(光标在视界外时的钉住横坐标)。
|
||
* @param [IN] clipTop 网格可见区上边界。
|
||
* @param [IN] clipBottom 网格可见区下边界。
|
||
* @param [IN] topReserve 上边界额外需要预留的高度(例如时间光标的顶部三角把手,
|
||
* 避免数据点三角压住光标把手)。默认 0。
|
||
* @return 标记几何。
|
||
*/
|
||
DataMarker MakeDataMarker(float rawX, float rawY, float pinX, float clipTop,
|
||
float clipBottom, float topReserve = 0.0f) const;
|
||
|
||
/**
|
||
* @brief 绘制数据点标记:视界内为圆点,越界时在对应边缘绘制指示方向的三角。
|
||
* @param [IN,OUT] g 绘图上下文。
|
||
* @param [IN] marker 标记几何。
|
||
* @param [IN] radius 圆点半径。
|
||
* @param [IN] fillColor 前景色(圆点/三角内部)。
|
||
* @param [IN] ringColor 描边色(圆环/三角外框)。
|
||
*/
|
||
void PaintDataMarker(gd::Graphics& g, const DataMarker& marker, float radius,
|
||
std::uint32_t fillColor, std::uint32_t ringColor) const;
|
||
|
||
/**
|
||
* @brief 放置一个光标取值胶囊:先夹到网格可见范围内,再向下(必要时向上)避让
|
||
* 已占用的标注矩形,最后登记到 `m_cursorLabelRects`。
|
||
* @param [IN,OUT] rect 胶囊矩形(Y 会被调整)。
|
||
* @param [IN] preferredTop 期望的顶边纵坐标(避让前)。
|
||
* @param [IN] textHeight 胶囊高度。
|
||
* @param [IN] clipTop 网格可见区上边界。
|
||
* @param [IN] clipBottom 网格可见区下边界。
|
||
*/
|
||
void PlaceCursorLabel(gd::RectF& rect, float preferredTop, float textHeight, float clipTop,
|
||
float clipBottom);
|
||
|
||
/**
|
||
* @brief 判断数据点圆点是否被「光标时间读数 / Δt 面板」盖住。
|
||
*
|
||
* 光标的时间读数与 Δt 胶囊绘制在绘图区底部、且在取值标注**之后**绘制,
|
||
* 因此靠近底部的数据点圆点会被它们完全遮住(用户 2026-10-19 报告“看不清数据点在哪”)。
|
||
* 判定为“盖住”时调用方应把圆点改成**向下的三角**(与数据点在视界下方同一语义)。
|
||
*
|
||
* @param [IN] marker 数据点标记。
|
||
* @param [IN] radius 圆点半径。
|
||
* @param [OUT] hit 遮住它的面板矩形(用于把三角贴在面板上沿)。
|
||
* @return 被盖住返回 true。
|
||
*/
|
||
bool MarkerCoveredByCursorLabel(const DataMarker& marker, float radius, gd::RectF& hit) const;
|
||
|
||
/**
|
||
* @brief 若数据点圆点被底部时间读数/Δt 面板盖住,把它改成**向下的三角**。
|
||
*
|
||
* 语义与“数据点位于视界下方”一致(用户 2026-10-19 要求):圆点既然看不见,
|
||
* 就用贴面板上沿的向下三角告知“这里有个数据点、它在下侧”。
|
||
* 已经是越界三角的标记不做任何修改。
|
||
*
|
||
* @param [IN,OUT] marker 标记几何。
|
||
* @param [IN] radius 圆点半径。
|
||
* @param [IN] clipTop 网格可见区上边界(三角不会越过它)。
|
||
*/
|
||
void CoverHiddenMarker(DataMarker& marker, float radius, float clipTop) const;
|
||
|
||
/**
|
||
* @brief 统计某信号穿越横向光标幅值的上/下边沿数量,结果带缓存。
|
||
*
|
||
* 判定定义(用户明确要求):把信号**绘制出来的曲线**与横向光标所在的幅值比较,
|
||
* 曲线从**下往上**穿过该幅值 → 上升沿;从**上往下**穿过 → 下降沿。
|
||
*
|
||
* ⚠️ 因此判定值必须是**画出来的值**:先按 `style` 做水平镜像映射,再套用
|
||
* `ApplyDrawTransform`(系数 → 绝对值 → 垂直镜像 → Y 偏移)。
|
||
* 只比较原始采样值会导致「应用过调整的信号数不到任何边沿」。
|
||
* ⚠️ 时间区间**之外**的采样一律不参与计数(只有两个端点都在区间内的采样对才计入)。
|
||
*
|
||
* ⚠️ **本函数绝不长时间阻塞 UI 线程**(千万级信号整段扫描实测 ≈115ms/条,
|
||
* 横向光标每一帧都会换阈值 → 拖动时每帧都要扫,5 条信号就是 ≈575ms/帧):
|
||
* 扫描按「逐块累积」进行,**每帧**只消耗 `kEdgeScanPairsPerFrame` 个采样对的预算
|
||
*(且**每个状态各自持有配额**,多条信号不会互相饿死),未扫完时 `complete` 返回 false
|
||
*(界面显示上一个完整结果 + “…”)。
|
||
* 阈值变化(横向光标拖动中)就地把累积状态重置(不新建条目),且当帧不展示部分值
|
||
*(`justReset`),避免“错误且不更新的数字”。
|
||
*
|
||
* @param [IN] axis 该信号使用的时间轴。
|
||
* @param [IN] signal 目标信号。
|
||
* @param [IN] style 该信号的外观(含水平镜像与「调整」变换)。
|
||
* @param [IN] edgeValue 横向光标所在的幅值。
|
||
* @param [IN] hasRange 是否只统计某个时间区间(双光标模式下为光标区间)。
|
||
* @param [IN] rangeMin 区间左端点时间。
|
||
* @param [IN] rangeMax 区间右端点时间。
|
||
* @param [OUT] rising 上升沿数量(从下往上穿越)。
|
||
* @param [OUT] falling 下降沿数量(从上往下穿越)。
|
||
* @param [OUT] complete 结果是否为该阈值的**完整**精确值(false = 仍在推进,当前是临时值)。
|
||
*/
|
||
void EdgeCountsFor(const TimeAxis& axis, const SignalData& signal, const SignalStyle& style,
|
||
double edgeValue, bool hasRange, double rangeMin, double rangeMax,
|
||
std::size_t& rising, std::size_t& falling, bool& complete) const;
|
||
|
||
/**
|
||
* @brief 计算实验性功能面板所需的宽度(排版与绘制共用,保证文字完整不被省略)。
|
||
* @param [IN] laneIndex 网格下标。
|
||
* @param [IN] kind 面板种类。
|
||
* @return 面板宽度(像素)。
|
||
*/
|
||
float EdgePanelWidth(int laneIndex, EdgePanelKind kind) const;
|
||
|
||
/**
|
||
* @brief 判断边沿统计是否限定在双光标区间内(双光标模式)。
|
||
* @return 限定在光标区间内返回 true。
|
||
*/
|
||
bool EdgeRangeActive() const;
|
||
|
||
/**
|
||
* @brief 边沿统计时间区间的左端点(取两条光标的较小时间)。
|
||
* @return 区间左端点。
|
||
*/
|
||
double EdgeRangeMin() const;
|
||
|
||
/**
|
||
* @brief 边沿统计时间区间的右端点(取两条光标的较大时间)。
|
||
* @return 区间右端点。
|
||
*/
|
||
double EdgeRangeMax() const;
|
||
|
||
/**
|
||
* @brief 双光标模式下面板标题的尾缀文本(“(光标区间内)”)。
|
||
* @return 尾缀文本,非双光标模式返回空串。
|
||
*/
|
||
std::wstring EdgeRangeSuffix() const;
|
||
|
||
/**
|
||
* @brief 绘制拖动中的缩放选区。
|
||
* @param [IN,OUT] g 绘图上下文。
|
||
*/
|
||
void PaintRubberBand(gd::Graphics& g);
|
||
|
||
/**
|
||
* @brief 绘制内联数值输入框(时间光标或网格 Y 轴上下限)。
|
||
* @param [IN,OUT] g 绘图上下文。
|
||
*/
|
||
void PaintEditField(gd::Graphics& g);
|
||
|
||
/**
|
||
* @brief 绘制信号悬停提示与拖拽虚影。
|
||
* @param [IN,OUT] g 绘图上下文。
|
||
*/
|
||
void PaintSignalInteraction(gd::Graphics& g);
|
||
|
||
/**
|
||
* @brief 在绘图区底部绘制临时提示条(如“已放弃当前选区”)。
|
||
* @param [IN,OUT] g 绘图上下文。
|
||
*/
|
||
void PaintHint(gd::Graphics& g);
|
||
|
||
/**
|
||
* @brief 绘制网格纵向滚动条。
|
||
* @param [IN,OUT] g 绘图上下文。
|
||
*/
|
||
void PaintScrollBar(gd::Graphics& g);
|
||
|
||
/**
|
||
* @brief 计算右下角「回到最上方」浮动按钮的矩形(客户区坐标)。
|
||
* @return 按钮矩形;当前不该显示时返回空矩形(`m_backToTopAlpha` 为 0 时仍返回几何,
|
||
* 便于命中测试与过渡动画)。
|
||
*/
|
||
gd::RectF BackToTopRect() const;
|
||
|
||
/**
|
||
* @brief 绘制右下角「回到最上方」浮动按钮(含按 `m_backToTopAlpha` 的淡入淡出)。
|
||
* @param [IN,OUT] g 绘图上下文。
|
||
*/
|
||
void PaintBackToTop(gd::Graphics& g);
|
||
|
||
/**
|
||
* @brief 依据当前滚动位置刷新「回到最上方」按钮的目标可见性(需要时启动过渡)。
|
||
* @return 视觉状态发生变化返回 true。
|
||
*/
|
||
bool UpdateBackToTopTarget();
|
||
|
||
/**
|
||
* @brief 绘制网格标题(信号图例)。
|
||
* @param [IN,OUT] g 绘图上下文。
|
||
* @param [IN] laneIndex 网格下标。
|
||
* @param [IN] lane 网格绘制信息。
|
||
* @param [IN] visibleTop 网格在绘图区内的可见上边界。
|
||
* @param [IN] visibleBottom 网格在绘图区内的可见下边界。
|
||
*/
|
||
void PaintLaneLegend(gd::Graphics& g, int laneIndex, const LaneVisual& lane, float visibleTop,
|
||
float visibleBottom);
|
||
|
||
/**
|
||
* @brief 判断给定矩形是否完整落在可见纵向区间内。
|
||
* @param [IN] rect 待测矩形。
|
||
* @param [IN] visibleTop 可见区上边界。
|
||
* @param [IN] visibleBottom 可见区下边界。
|
||
* @return 完整可见返回 true。
|
||
* @note GDI 文本绘制不会遵循 GDI+ 的裁剪区,必须显式判定,否则滚动时文字会溢出到工具栏。
|
||
*/
|
||
static bool IsFullyVisible(const gd::RectF& rect, float visibleTop, float visibleBottom);
|
||
|
||
/**
|
||
* @brief 绘制空状态提示文本。
|
||
* @param [IN,OUT] g 绘图上下文。
|
||
*/
|
||
void PaintEmptyState(gd::Graphics& g);
|
||
|
||
const UiFonts* m_fonts = nullptr; /**< 字体资源。 */
|
||
const Theme* m_theme = nullptr; /**< 主题资源。 */
|
||
const DataSet* m_dataSet = nullptr; /**< 数据集合(只读访问)。 */
|
||
DataSet* m_dataSetWritable = nullptr; /**< 数据集合(选中状态写入)。 */
|
||
TabConfig* m_tab = nullptr; /**< 当前标签页配置。 */
|
||
TabConfig m_localTab; /**< 未绑定标签页时使用的占位配置。 */
|
||
HWND m_host = nullptr; /**< 剪贴板所有者窗口句柄。 */
|
||
float m_scale = 1.0f; /**< DPI 缩放比例。 */
|
||
|
||
gd::RectF m_area; /**< 绘图区整体区域。 */
|
||
gd::RectF m_plotRect; /**< 波形绘制区域。 */
|
||
gd::RectF m_timeAxisRect; /**< 时间轴区域。 */
|
||
gd::RectF m_scrollBarRect; /**< 网格滚动条区域。 */
|
||
|
||
float m_labelBandWidth = 66.0f; /**< 网格内侧纵轴标尺带宽度(逻辑像素)。 */
|
||
float m_timeAxisHeight = 32.0f; /**< 时间轴高度(逻辑像素)。 */
|
||
/** @brief 网格最小高度(**逻辑像素**,使用处乘 `m_scale`)。用户 2026-10-19 要求为 81(原 54 的 1.5 倍)。 */
|
||
float m_minLaneHeight = 81.0f;
|
||
float m_laneContentHeight = 0.0f; /**< 网格内容总高度。 */
|
||
bool m_scrollBarVisible = false; /**< 是否需要显示滚动条。 */
|
||
|
||
/** 网格滚动(“点选信号 ⇒ 把目标网格滚入可视区”)的平滑过渡状态。 */
|
||
bool m_laneScrollAnimActive = false; /**< 动画是否正在进行。 */
|
||
float m_laneScrollAnimFrom = 0.0f; /**< 动画起始滚动偏移。 */
|
||
float m_laneScrollAnimTo = 0.0f; /**< 动画目标滚动偏移。 */
|
||
std::uint64_t m_laneScrollAnimStartMs = 0; /**< 动画起始时刻(`GetTickCount64()`)。 */
|
||
std::uint32_t m_laneScrollAnimDurationMs = 0; /**< 动画总时长(毫秒)。 */
|
||
|
||
/**
|
||
* @brief 右下角「回到最上方」浮动按钮(只在网格需要滚动条、且已向下滚动时出现)。
|
||
*
|
||
* 用户 2026-10-16 要求:参考长文档网页的常见做法,用一个向上箭头的浮动按钮;
|
||
* 仅在向下滚动时出现、回到上方时消失,并且**出现/消失都要有过渡动画**(不能突变)。
|
||
*/
|
||
float m_backToTopAlpha = 0.0f; /**< 当前不透明度(0 = 完全隐藏)。 */
|
||
bool m_backToTopFadeIn = false; /**< 当前过渡方向(true = 正在淡入)。 */
|
||
bool m_backToTopAnimating = false; /**< 是否正在淡入/淡出。 */
|
||
bool m_backToTopHover = false; /**< 鼠标是否悬停在按钮上。 */
|
||
bool m_backToTopPressed = false; /**< 是否正在按钮上按住左键。 */
|
||
std::uint64_t m_backToTopAnimStartMs = 0; /**< 本段过渡的起始时刻。 */
|
||
|
||
PlotDragMode m_dragMode = PlotDragMode::NONE; /**< 当前拖拽模式。 */
|
||
PlotRubberAxis m_rubberAxis = PlotRubberAxis::UNDECIDED; /**< 选区放大拖拽的轴向。 */
|
||
int m_dragLane = -1; /**< 拖拽所作用的网格下标。 */
|
||
bool m_dragMoved = false; /**< 拖拽过程中是否发生了位移。 */
|
||
int m_dragStartX = 0; /**< 拖拽起点横坐标。 */
|
||
int m_dragStartY = 0; /**< 拖拽起点纵坐标。 */
|
||
int m_dragLastX = 0; /**< 上一次拖拽横坐标。 */
|
||
int m_dragLastY = 0; /**< 上一次拖拽纵坐标。 */
|
||
gd::RectF m_rubberRect; /**< 拖拽选区矩形。 */
|
||
|
||
SignalRef m_hoverSignal; /**< 鼠标悬停命中的信号(曲线高亮)。 */
|
||
/**
|
||
* @brief 「外部悬停信号」:信号列表 / 样式区「目标」行悬停到某条信号时的联动高亮源。
|
||
*
|
||
* 与 `m_hoverSignal` 分开(绘图区的 `OnMouseLeave` 会清掉后者,而列表悬停时
|
||
* 鼠标并不在绘图区内);二者的高亮效果相同:网格变色 + 曲线光晕。
|
||
*/
|
||
SignalRef m_externalHoverSignal;
|
||
/**
|
||
* @brief 待宿主处理的「把信号列表滚动到该信号」请求(见 `TakePanelScrollRequest`)。
|
||
*
|
||
* 绘图区自身无法访问信号面板,因此把请求记在这里,由宿主在鼠标消息处理后取走。
|
||
*/
|
||
SignalRef m_pendingPanelScroll;
|
||
bool m_pendingPanelScrollFlag = false;
|
||
SignalRef m_hoverLegend; /**< 鼠标悬停命中的图例条目(半高亮提示,不触发曲线拖拽提示)。 */
|
||
SignalRef m_dragSignal; /**< 正在被拖拽的信号。 */
|
||
int m_dragTargetLane = -1; /**< 拖拽信号时的目标网格下标。 */
|
||
int m_dragMouseX = 0; /**< 拖拽过程中鼠标的当前横坐标(虚影跟随)。 */
|
||
int m_dragMouseY = 0; /**< 拖拽过程中鼠标的当前纵坐标(虚影跟随)。 */
|
||
bool m_cursorHover = false; /**< 鼠标是否悬停在某条时间光标上。 */
|
||
int m_hoverCursor = 0; /**< 悬停/拖拽中的光标序号(1 或 2,0 表示无)。 */
|
||
int m_activeCursor = 1; /**< 最近操作过的光标:内容重叠时它绘制在上层。 */
|
||
int m_editCursor = 1; /**< 内联输入所作用的 Time 光标序号。 */
|
||
int m_spanDragStartX = 0; /**< 差值标签拖拽起点横坐标。 */
|
||
double m_spanDragTime1 = 0.0; /**< 差值标签拖拽起始时两条光标的时间。 */
|
||
double m_spanDragTime2 = 0.0;
|
||
int m_cursorGrabOffset = 0; /**< 从光标时间标签上抓起时的横向偏移(像素)。 */
|
||
PlotEditTarget m_editTarget = PlotEditTarget::NONE; /**< 当前内联输入目标。 */
|
||
TextEdit m_edit; /**< 输入框编辑模型(光标/选区/剪贴板)。 */
|
||
int m_editLane = -1; /**< 内联输入所作用的网格下标。 */
|
||
SignalRef m_editMeanRef; /**< 均值滤波参数编辑所作用的信号。 */
|
||
/** 网格拖拽重排状态(拖动图例中的 GX 徽标)。 */
|
||
int m_laneDragFrom = -1; /**< 被拖拽的网格下标,-1 表示未拖拽。 */
|
||
int m_laneDragTarget = -1; /**< 当前插入位置(0..网格数)。 */
|
||
int m_laneDragMouseY = 0; /**< 最近一次鼠标纵坐标(被拖网格自由跟随)。 */
|
||
float m_laneDragGrabOffset = 0.0f; /**< 抓取时鼠标相对网格顶边的偏移。 */
|
||
SignalRef m_editSignal; /**< 内联输入所作用的信号(图例改名)。 */
|
||
gd::RectF m_editFieldRect; /**< 本次编辑输入框的矩形(用于点外提交判定)。 */
|
||
SignalRef m_pendingRenameRef; /**< 待宿主窗口应用的信号重命名引用。 */
|
||
std::wstring m_pendingRenameName; /**< 待宿主窗口应用的信号新名称。 */
|
||
bool m_pendingRename = false; /**< 是否存在待应用的重命名请求。 */
|
||
|
||
int m_pendingMeanFilterLane = -1; /**< 待构建的均值滤波信号所属网格下标(-1 表示无请求)。 */
|
||
int m_pendingWeakPeakLane = -1; /**< 待构建的弱峰调整信号所属网格下标(-1 表示无请求)。 */
|
||
std::vector<MeanFilterBuild> m_pendingMeanFilterItems; /**< 待构建的信号及其参数。 */
|
||
std::vector<WeakPeakBuild> m_pendingWeakPeakItems; /**< 待构建的弱峰调整信号及其参数。 */
|
||
int m_pendingResampleLane = -1; /**< 待构建的重采样信号所属网格下标(-1 表示无请求)。 */
|
||
std::vector<ResampleBuild> m_pendingResampleItems; /**< 待构建的重采样信号及其间隔。 */
|
||
bool m_mouseOverPanel = false; /**< 上一帧鼠标是否落在某个实验性功能面板上。 */
|
||
int m_peakDragLane = -1; /**< 正在拖动弱峰滑块的面板所属网格。 */
|
||
SignalRef m_peakDragRef; /**< 正在拖动弱峰滑块的**信号**(每个信号一套参数)。 */
|
||
int m_meanDragLane = -1; /**< 正在拖动「均值滤波」参数滑块的面板所属网格。 */
|
||
SignalRef m_meanDragRef; /**< 正在拖动「均值滤波」参数滑块的信号(每信号一套参数)。 */
|
||
/**
|
||
* @brief 当前「列表式面板」参数滑块 / 滚动条拖拽的目标面板种类。
|
||
*
|
||
* 「均值滤波」「中心轴边沿计数」「弱峰调整」自 2026-11-02 起共用同一套**可滚动列表**
|
||
* (信号色叠底条目栏 + 右侧复选框 + 展开后的参数滑块)与同一套拖拽模式
|
||
*(`MEAN_POINTS` / `MEAN_ITERATIONS` / `MEAN_SCROLL`);本字段说明它们的写入目标是
|
||
* `Lane::meanFilterItems` / `edgeCenterItems` / `weakPeakItems` 中的哪一个。
|
||
* 「边沿计数」面板(旧式两行排版)也用它区分滚动目标。
|
||
*/
|
||
EdgePanelKind m_meanDragKind = EdgePanelKind::MEAN;
|
||
/**
|
||
* @brief 正在编辑「均值滤波」某个滑块上限时,输入框内灰色提示用的**当前上限值**。
|
||
*
|
||
* 与 `m_peakEditCurrentMax` 同一语义(输入框以空内容开始,空内容时显示当前上限)。
|
||
*/
|
||
int m_meanEditCurrentMax = 0;
|
||
|
||
/**
|
||
* @brief 正在编辑「滑块上限」的目标面板种类(均值滤波 / 中心轴边沿计数)。
|
||
*
|
||
* 与 `m_meanDragKind` 同一思路:两者共用 `MEAN_POINTS_MAX` / `MEAN_ITERATIONS_MAX`
|
||
* 两个编辑目标,本字段说明提交时要写到 `Lane::edgeCenterItems` 还是 `meanFilterItems`。
|
||
* 「弱峰调整」仍用自己的 `PEAK_ITERATIONS_MAX` 与 `m_peakEditCurrentMax`。
|
||
*/
|
||
EdgePanelKind m_editMeanKind = EdgePanelKind::MEAN;
|
||
/**
|
||
* @brief 正在编辑「迭代次数 N 上限」时,输入框内灰色提示用的**当前上限值**。
|
||
*
|
||
* 输入框以空内容开始,空内容时显示这个值作为提示;进入编辑时记录,提交后清零。
|
||
*/
|
||
int m_peakEditCurrentMax = 0;
|
||
/**
|
||
* @brief 本帧是否使用「快速绘制」(交互期间关闭波形与派生曲线的抗锯齿)。
|
||
*
|
||
* 用户明确要求"先用低分辨率,绘制完成后再用高分辨率替换":抗锯齿是单帧最贵的
|
||
* 一项(同一条包络带实测 27.6ms → 3.2ms),而它的几何完全不变,
|
||
* 因此拖拽/拖动滑块期间关闭它,松手后由宿主整帧失效自动恢复全质量绘制。
|
||
*/
|
||
bool m_fastFrame = false;
|
||
|
||
/**
|
||
* @brief 实验性功能浮动面板的 ✕ 处于"已按下、等待抬起确认"状态。
|
||
*
|
||
* 关闭类 ✕ 统一改为左键**抬起**时生效:按下只记录目标,抬起时指针仍在该 ✕ 上才真正
|
||
* 关闭对应功能;按住后移开再松开视为放弃。
|
||
*/
|
||
bool m_pendingPanelClose = false; /**< 是否有待确认的面板关闭请求。 */
|
||
int m_pendingPanelCloseLane = -1; /**< 待关闭面板所属网格下标。 */
|
||
EdgePanelKind m_pendingPanelCloseKind = EdgePanelKind::COUNT; /**< 待关闭面板的功能类型。 */
|
||
|
||
int m_hoverLane = -1; /**< 当前悬停网格下标。 */
|
||
/**
|
||
* @brief 取本次绘制应该高亮的网格下标(-1 表示无)。
|
||
*
|
||
* 来源两处:① 鼠标悬停在实验性功能浮动面板上(该面板所属网格);
|
||
* ② 外部悬停信号(信号列表 / 样式区)且它已绘制到某个网格时,高亮该网格。
|
||
* ⚠️ 鼠标在网格里移动**不再**触发高亮(用户 2026-10-15 要求取消旧行为)。
|
||
*/
|
||
int EffectiveHoverLane() const;
|
||
/**
|
||
* @brief 上一帧鼠标是否悬停在某个网格的“GX”编号徽标上(是则记录网格下标,否则 -1)。
|
||
*
|
||
* 徽标本体绘制在静态场景 / 装饰缓存里(不随鼠标变化),悬停高亮是**贴图之后的叠加层**
|
||
*(见 PaintLegendHighlight)。鼠标在同一个网格的徽标范围内移动时 `m_hoverLane` 不变,
|
||
* 因此必须单独记住这个状态才能触发重绘与取消高亮。
|
||
*/
|
||
int m_hoverBadge = -1;
|
||
/**
|
||
* @brief 上一帧鼠标是否悬停在某个浮动面板标题栏的「GX」徽标上(是则记录网格下标 / 面板种类)。
|
||
*
|
||
* 徽标本体绘制在面板**像素图层**里(缓存键只在鼠标位于面板内时才混入坐标),
|
||
* 因此需要单独记录状态才能让高亮跟着鼠标进出而刷新。
|
||
* ⚠️ 用户 2026-10-21 要求:徽标悬停**不改变鼠标形状**、也**不弹提示气泡**。
|
||
*/
|
||
int m_hoverPanelBadgeLane = -1;
|
||
int m_hoverPanelBadgeKind = -1;
|
||
/** 「弱峰调整」每行滑块右侧的复位控件是否处于悬停态(用于“进入/离开”时只提示一次)。 */
|
||
bool m_peakResetHover = false;
|
||
|
||
/**
|
||
* @brief 最近一次鼠标活动(移动 / 按下 / 滚轮)的时间戳(`GetTickCount64()`)。
|
||
*
|
||
* 用途:**边沿统计的每帧预算**要区分“交互中”和“静止”两种状态(见
|
||
* `kEdgeScanPairsPerFrameInteractive`)。交互期间压小预算保住跟手;鼠标停下
|
||
* `kEdgeScanInteractiveIdleMs` 之后自动恢复高预算,把统计快速补齐。
|
||
* 初值 0 ⇒ 启动后第一帧就算作“静止”(可以全速推进统计)。
|
||
*/
|
||
std::uint64_t m_lastInputActivityMs = 0;
|
||
|
||
/**
|
||
* @brief 当前是否处于「交互中」(正在拖拽,或鼠标刚刚移动过)。
|
||
*
|
||
* 两个用途(都用它保证同一个判定口径):
|
||
* * `Paint` 决定边沿统计本帧的扫描预算(交互 ⇒ 小预算,保住跟手);
|
||
* * `PanelGlobalKey` / `PanelSlotKey` 决定**是否把边沿统计数字与光标时间计入面板像素缓存键**:
|
||
* 交互期间不计入 ⇒ 键保持稳定 ⇒ 面板直接复用上一帧像素、**不做**每帧数百次统计查询
|
||
* 与整块重绘。
|
||
*
|
||
* ⚠️ 用户 2026-11-02 报告:288 个信号合并到同一网格、开启中心轴边沿计数后,
|
||
* 拖动横向光标/移动鼠标每帧要 30ms。根因正是「每帧 × 每个信号 × 一次
|
||
* `EdgeCenterCountsFor` 区间汇总」被塞进了缓存键计算里,且结果每帧都让键变化
|
||
* ⇒ 面板缓存永远不命中、每帧重画整块面板。
|
||
*/
|
||
bool InputIsInteractive() const;
|
||
|
||
/**
|
||
* @brief 上一帧鼠标悬停在「均值滤波 / 弱峰调整」面板的哪个**条目栏**上。
|
||
*
|
||
* 用途(用户 2026-10-30 要求):悬停某条信号的条目栏时,把该信号的**预览曲线**
|
||
* 高亮出来(画一圈光晕 + 加粗),这样"列表里这一行 ↔ 网格里那条曲线"一眼对应。
|
||
*
|
||
* 与 `m_freeMarkerRowHover*` 同一套路:只在悬停状态**变化**时返回 `changed`,
|
||
* 避免每帧都请求重绘。
|
||
*/
|
||
int m_meanRowHoverLane = -1;
|
||
SignalRef m_meanRowHoverRef;
|
||
/**
|
||
* @brief 悬停所在的**面板种类**(MEAN / CENTER / PEAK)。
|
||
*
|
||
* ⚠️ 必须区分:同一条信号可能在多个面板里各有一行(均值滤波、中心轴边沿计数、
|
||
* 弱峰调整都用同一套条目栏),只比 `(lane, ref)` 会把**所有**面板的同名行一起点亮,
|
||
* 并且无法判断“该高亮哪一条预览曲线”。
|
||
*
|
||
* 用户 2026-11-02 报告:只开弱峰调整 + 中心轴边沿计数时悬停**毫无反应**;
|
||
* 一旦再开均值滤波,三个面板悬停都只高亮均值滤波曲线 ——
|
||
* 根因就是绘制侧只给 MEAN 分支传了 `highlight`,而悬停状态没有记录面板种类。
|
||
*/
|
||
EdgePanelKind m_meanRowHoverKind = EdgePanelKind::MEAN;
|
||
|
||
/**
|
||
* @brief 上一帧绘制面板时是否使用了「冻结的缓存键」(交互期间)。
|
||
*
|
||
* 用途:冻结期间面板显示的是上一帧像素(统计数字停留在“统计中…”),
|
||
* 而**鼠标停下后不会再有重绘请求** ⇒ 面板永远停在旧数字
|
||
*(用户 2026-11-02 报告:“松开左键后一直统计中…,动一下鼠标或悬停面板才更新”)。
|
||
* 宿主每帧检查它:为真就武装一个短周期定时器,直到某个非交互帧重画面板为止。
|
||
*/
|
||
mutable bool m_panelKeysFrozen = false;
|
||
|
||
// ---------------------------------------------------------------- 自由标记
|
||
|
||
/** 最近一次解析出的「自由标记」跟随数据点(绘制与命中共用)。 */
|
||
FreeMarkerProbe m_freeMarker;
|
||
|
||
/**
|
||
* @brief 上一帧鼠标悬停在「自由标记」面板的哪个**条目行**上(-1 = 没有)。
|
||
*
|
||
* 与 `m_freeMarkerPanelHoverLane/Item`(只表示 ✕ 的悬停)**分开**:
|
||
* 悬停在整行上时要把网格内对应的固定点整体高亮(取值胶囊 + 数据点),
|
||
* 但**不能**因此把该行的 ✕ 画成 danger 色(那是“准备删除”的语义)。
|
||
*/
|
||
int m_freeMarkerRowHoverLane = -1;
|
||
std::size_t m_freeMarkerRowHoverItem = static_cast<std::size_t>(-1);
|
||
|
||
/**
|
||
* @brief 上一次绘制时登记的固定点 ✕ 命中矩形。
|
||
*
|
||
* 固定点 ✕ 的位置由「取值文本宽度」决定(文本又取决于数值),因此无法在命中测试里
|
||
* 重新推算 —— 改为绘制时登记、命中时读取(与 `m_legendHits` 同一套路)。
|
||
*/
|
||
mutable std::vector<FreeMarkerHit> m_freeMarkerHits;
|
||
|
||
/** 网格内固定点 ✕ 的悬停态:`m_freeMarkerHoverItem` 为 `SIZE_MAX` 表示没有悬停。 */
|
||
int m_freeMarkerHoverLane = -1;
|
||
std::size_t m_freeMarkerHoverItem = static_cast<std::size_t>(-1);
|
||
|
||
/** 面板内 ✕ 的悬停态:信号名 ✕(`itemIndex == SIZE_MAX`)或某一固定点行 ✕。 */
|
||
int m_freeMarkerPanelHoverLane = -1;
|
||
std::size_t m_freeMarkerPanelHoverItem = static_cast<std::size_t>(-1);
|
||
SignalRef m_freeMarkerPanelHoverSignal;
|
||
|
||
/**
|
||
* @brief 本次按下是否**可能**固定一个数据点(抬起时若"原地未拖动"就真正固定)。
|
||
*
|
||
* 自由标记网格 + 选区放大模式共存时,单击 = 固定数据点、按下后拖动 = 框选,
|
||
* 因此按下时不能立即固定,必须等抬起时判断是否发生过位移。
|
||
*/
|
||
bool m_freeMarkerPendingFix = false;
|
||
int m_freeMarkerPendingLane = -1;
|
||
|
||
/** 正在编辑的固定点(`m_editLane` 网格内的 `freeMarkerItems` 下标)。 */
|
||
std::size_t m_editMarkerIndex = static_cast<std::size_t>(-1);
|
||
|
||
/** 「标记透明度」滑块拖拽的目标网格(-1 = 未拖拽)。 */
|
||
int m_markerOpacityDragLane = -1;
|
||
/** 面板内容滚动条拖拽的目标网格(-1 = 未拖拽)。 */
|
||
int m_markerScrollDragLane = -1;
|
||
/** 按下滑块时鼠标相对滚动条滑块上沿的偏移(避免滑块跳动)。 */
|
||
float m_markerScrollGrabOffset = 0.0f;
|
||
|
||
/** 「自由标记」面板:分组栏三角的悬停态(用于给出可点击提示)。 */
|
||
int m_freeMarkerTriangleHoverLane = -1;
|
||
SignalRef m_freeMarkerTriangleHoverSignal;
|
||
|
||
int m_mouseX = 0; /**< 最近一次鼠标横坐标(用于悬停提示定位)。 */
|
||
int m_mouseY = 0; /**< 最近一次鼠标纵坐标(用于悬停提示定位)。 */
|
||
bool m_spanHover = false; /**< 鼠标是否悬停在双光标差值标签上。 */
|
||
std::wstring m_hintText; /**< 临时提示文本(空表示不显示)。 */
|
||
std::uint64_t m_hintShownAt = 0; /**< 提示出现的时刻(毫秒,GetTickCount64)。 */
|
||
bool m_hintSticky = false; /**< 提示是否处于粘滞状态(拖动期间维持显示,不淡出)。 */
|
||
gd::RectF m_cursorSpanRect; /**< 最近一次绘制时差值标签的矩形(用于命中拖拽/双击)。 */
|
||
std::vector<gd::RectF> m_cursorLabelRects; /**< 本次绘制已占用的取值标注矩形(用于避让重叠)。 */
|
||
std::vector<LaneVisual> m_lanes; /**< 当前网格绘制信息集合。 */
|
||
|
||
/**
|
||
* @brief 「网格幅值范围」缓存指纹(见 `BuildLaneVisuals`)。
|
||
*
|
||
* `ComputeLaneRange()` 对启用了「公式映射」的信号必须**逐点扫描整条信号**
|
||
*(公式不保证单调,端点映射会算错)。而它在 `BuildLaneVisuals` 里**每帧**为
|
||
* 每个网格各调一次 ⇒ 千万级采样时每帧几百毫秒。范围只依赖「可见信号 + 数值样式 +
|
||
* 数据本身」,与视界无关,因此按指纹缓存:指纹一致就复用 `m_laneRangeYMin/Max`。
|
||
*/
|
||
std::uint64_t m_laneRangeKey = 0;
|
||
std::vector<double> m_laneRangeYMin; /**< 各网格缓存的 yMin(与上述指纹配套)。 */
|
||
std::vector<double> m_laneRangeYMax; /**< 各网格缓存的 yMax(与上述指纹配套)。 */
|
||
|
||
/**
|
||
* @brief 「边沿统计」的累积状态(普通边沿计数与中心轴边沿计数**共用**)。
|
||
*
|
||
* 千万级信号整段扫描一次就要 ≈115ms(5 条信号同一网格 = 575ms/帧,界面完全失去响应),
|
||
* 因此统计改为「逐块累积 + 每 tick 有限预算推进」(与 `RenderPendingLaneRebuilds(1)`
|
||
* 同一套"宁可晚一点、绝不卡住前台"的思路):
|
||
* * 采样对 `k`(采样 `k-1 → k`)按 `kEdgeBlockSamples` 分块,每块各存上升/下降沿数;
|
||
* * `scannedPair` = 下一个待扫描的采样对,[1, scannedPair) 已扫完;
|
||
* * 查询区间时内部整块用块和相加、两端边界块就地精确扫描(≤ 2 块);
|
||
* * 未扫完时**主动请宿主再送一帧**(`RequestEdgeScanFrame`),因此**不需要移动鼠标**
|
||
* 也会持续推进(修掉"面板一直显示统计中,把鼠标移到面板上才更新")。
|
||
*
|
||
* 键 = 「时间轴 + 信号 + 种类 + 样式指纹 + 参数 +(仅非单调时间轴)区间」;
|
||
* 阈值/参数变化时就地重新开始(不新建条目,避免拖动时状态爆炸)。
|
||
*/
|
||
struct EdgeScanState
|
||
{
|
||
const TimeAxis* axis = nullptr; /**< 该信号使用的时间轴(统计身份 = 时间轴 + 信号)。 */
|
||
const SignalData* signal = nullptr; /**< 目标信号。 */
|
||
bool centre = false; /**< false = 穿越横向光标阈值;true = 穿越中心平均值曲线。 */
|
||
std::uint64_t styleKey = 0; /**< 「调整」变换 + 水平镜像的指纹。 */
|
||
int points = 0; /**< 中心轴:均值点数 X(普通计数恒为 0)。 */
|
||
int iterations = 1; /**< 中心轴:迭代次数 N。 */
|
||
bool hasRange = false; /**< 是否把区间计入键(仅非单调时间轴需要)。 */
|
||
double rangeMin = 0.0; /**< 计入键的区间左端。 */
|
||
double rangeMax = 0.0; /**< 计入键的区间右端。 */
|
||
double threshold = 0.0; /**< 当前阈值(普通计数)。 */
|
||
std::vector<std::size_t> blockRising; /**< 每块的上升沿数量(已扫到的部分)。 */
|
||
std::vector<std::size_t> blockFalling; /**< 每块的下降沿数量(已扫到的部分)。 */
|
||
std::size_t scannedPair = 1; /**< 下一个待扫描的采样对([1, scannedPair) 已扫完)。 */
|
||
std::size_t rising = 0; /**< 已扫部分的上升沿总数。 */
|
||
std::size_t falling = 0; /**< 已扫部分的下降沿总数。 */
|
||
bool complete = false; /**< 整条信号是否已扫完。 */
|
||
bool justReset = false; /**< 本 tick 刚因阈值/参数变化重新开始(不展示部分值)。 */
|
||
/**
|
||
* @brief 本状态**上一次推进所在的帧序号**与当时已消耗的配额。
|
||
*
|
||
* 每个状态**每帧各自持有配额**(而不是所有状态共享一个总额):旧实现下推进总是
|
||
* 从列表第一个状态扫起,5 条信号同一网格时只有第一条有进展、其余长期显示“统计中…”
|
||
*(用户报告“很多项都停在了统计中不更新”)。
|
||
*/
|
||
std::uint64_t pumpedFrame = 0; /**< 最近一次推进的帧序号(与 `m_edgeScanFrame` 比较)。 */
|
||
std::size_t pumpedPairs = 0; /**< 本帧已消耗的采样对配额(新帧开始时重置)。 */
|
||
/**
|
||
* @brief 中心轴扫描**就地计算**的一小段均值滤波曲线(源下标空间)。
|
||
*
|
||
* ⚠️ 不要改为依赖全局曲线缓存(`AcquireCoveringCurve`/`CurveSpanFor` 的整条曲线):
|
||
* 千万级信号的整条曲线是 ~103MB/条,多条信号超出曲线缓存预算后条目会被淘汰 →
|
||
* 该信号被显示成“统计中…”并从头重扫(**偶发**,取决于当时缓存里有多少条目),
|
||
* 而且几条信号的统计会通过缓存互相牵连(用户报告:“改一个信号的参数,
|
||
* 其它几个信号也重新统计”)。
|
||
* 本地区间只需「当前扫描块 ± 滤波半径」(设 `points * iterations` 即可精确),
|
||
* 内存只有几十 KB,且完全与其它信号/其它曲线解耦。
|
||
*/
|
||
std::vector<float> localCurve; /**< 本地均滤波取值(长度 = localEnd - localBegin)。 */
|
||
std::size_t localBegin = 0; /**< 本地区间起点(源采样下标,含)。 */
|
||
std::size_t localEnd = 0; /**< 本地区间终点(源采样下标,不含)。 */
|
||
bool localValid = false; /**< 本地区间是否可用。 */
|
||
};
|
||
mutable std::vector<EdgeScanState> m_edgeScans; /**< 边沿统计累积状态(纯缓存)。 */
|
||
mutable std::size_t m_edgeScanBudget = 0; /**< 本帧剩余可扫描的采样对数量(跨状态共享,限制单帧总开销)。 */
|
||
/**
|
||
* @brief 帧序号(`Paint` 每帧递增)。
|
||
*
|
||
* ⚠️ 预算必须**按帧**重置,不能按 `GetTickCount64()` 分桶:系统时钟粒度约 15.6ms,
|
||
* 帧率一高(`PostMessage` 自持重绘会把它拉高)就会在同一 tick 内连画多帧而不推进,
|
||
* 用户看到的就是“按住光标不动时统计卡住、要动鼠标才更新”。
|
||
*/
|
||
mutable std::uint64_t m_edgeScanFrame = 0;
|
||
/**
|
||
* @brief 上一次“请求下一帧”发生在哪一帧(收敛成**每帧最多一次** `PostMessage`)。
|
||
*
|
||
* ⚠️ 旧实现在 `EdgeScanFor` 里按“未完成状态”逐个 Post:288 条信号时单帧 288 次,
|
||
* 把消息队列和重绘打满,`WM_TIMER`(分组折起动画)因此被饿死(用户 2026-11-02 报告)。
|
||
*/
|
||
mutable std::uint64_t m_edgeScanRequestFrame = static_cast<std::uint64_t>(-1);
|
||
/** 诊断计数:中心轴本地曲线计算失败次数(内存不足等)。0 = 一切正常。 */
|
||
mutable std::size_t m_edgeScanLocalCurveFails = 0;
|
||
|
||
/**
|
||
* @brief **统计数据版本号**:任何累积状态推进(`scannedPair` 前进)或扫完都会自增。
|
||
*
|
||
* 用途:面板像素缓存键不能再逐个信号查询统计数字(288 个信号 ⇒ 单帧 288 次查询,
|
||
* 且中间数字每帧都变 ⇒ 缓存永不命中 ⇒ 每帧整块重画上百行)。改为混入"把
|
||
* `m_edgeScanRevision` 按节流规则转成的 `m_panelStatsVersion`"(O(1)、零查询):
|
||
* 统计推进 ⇒ 版本号变化 ⇒ 面板重绘;没推进 ⇒ 键稳定 ⇒ 贴回缓存。
|
||
*/
|
||
mutable std::uint64_t m_edgeScanRevision = 0;
|
||
/** @brief 已反映到面板缓存键里的统计版本号(见 `m_edgeScanRevision`)。 */
|
||
mutable std::uint64_t m_panelStatsVersion = 0;
|
||
/** @brief 上一次同步时看到的 `m_edgeScanRevision`(用于判断"有新进展")。 */
|
||
mutable std::uint64_t m_panelStatsSeen = 0;
|
||
|
||
/**
|
||
* @brief 本帧统计扫描的**时间预算截止点**(QPC 计数)。
|
||
*
|
||
* 旧实现用"每帧采样对配额"限制开销,但同样的配额在稀疏/稠密、普通/中心轴之间
|
||
* 的实际耗时能差 10 倍:配额小 ⇒ 大文件统计极慢(用户 2026-11-02 报告
|
||
* "VERY_BIG_DATA 统计太慢");配额大 ⇒ 中心轴(每个采样对还要算窗口均值)单帧卡住。
|
||
* 改为**按时间切**:扫描循环每处理一个块检查一次是否超时,既不会卡帧又能把
|
||
* "便宜"的普通计数一次性推进得很快。
|
||
*/
|
||
mutable std::int64_t m_edgeScanDeadline = 0;
|
||
|
||
/**
|
||
* @brief 宿主正在加载文件(模态遮罩期间):暂停统计推进与面板绘制。
|
||
*
|
||
* ⚠️ 加载线程的主循环会不断 `UpdateWindow` 触发 `Render()`,而面板绘制里含统计推进
|
||
* 与上百行 GDI+ 原语 ⇒ 加载进度被拖慢(用户报告"统计没算完就无法加载文件")。
|
||
* 加载期间画布被遮罩覆盖,面板本来也看不见,因此直接跳过。
|
||
*/
|
||
bool m_loadingBusy = false;
|
||
|
||
/**
|
||
* @brief 累积状态上限(随可见信号总数缩放)。
|
||
*
|
||
* 每个信号最多两类状态(普通边沿计数 / 中心轴边沿计数),因此上限 ≥ `2 × 信号数`。
|
||
* 固定上限(旧 64)在 288 条信号合并到同一网格时会让所有状态互相淘汰 ⇒
|
||
* 面板永远停在“统计中…”(用户 2026-11-02 报告)。
|
||
*/
|
||
std::size_t EdgeScanStateLimit() const;
|
||
|
||
/**
|
||
* @brief 边沿统计的统一实现(普通边沿计数与中心轴边沿计数共用)。
|
||
*
|
||
* 判定基于**画出来的曲线**:先按 `style` 做水平镜像映射,再套用 `ApplyDrawTransform`;
|
||
* 中心轴时两侧(信号与中心曲线)都套用同一套变换。
|
||
* 时间区间**之外**的采样一律不参与计数(只有两个端点都在区间内的采样对才计入)。
|
||
*
|
||
* @param [IN] axis 该信号使用的时间轴。 @param [IN] signal 目标信号。
|
||
* @param [IN] style 信号外观。 @param [IN] centre 是否为中心轴判定。
|
||
* @param [IN] threshold 阈值(`centre = false` 时使用)。
|
||
* @param [IN] points 均值点数 X(`centre = true` 时使用)。
|
||
* @param [IN] iterations 迭代次数 N(`centre = true` 时使用)。
|
||
* @note 中心轴所需的小段均值滤波曲线由本函数**就地计算**,
|
||
* 不依赖 `CurveSpanFor` 的整条曲线缓存(见 `EdgeScanState::localCurve`)。
|
||
* @param [IN] hasRange 是否只统计某个时间区间。
|
||
* @param [IN] rangeMin 区间左端。 @param [IN] rangeMax 区间右端。
|
||
* @param [OUT] rising 上升沿数量。 @param [OUT] falling 下降沿数量。
|
||
* @param [OUT] complete 是否为完整精确值(false = 仍在推进)。
|
||
*/
|
||
void EdgeScanFor(const TimeAxis& axis, const SignalData& signal, const SignalStyle& style,
|
||
bool centre, double threshold, int points, int iterations, bool hasRange,
|
||
double rangeMin, double rangeMax, std::size_t& rising, std::size_t& falling,
|
||
bool& complete) const;
|
||
|
||
/** @brief 边沿统计未完成时请宿主再送一帧(无需鼠标移动也能持续推进)。 */
|
||
void RequestEdgeScanFrame() const;
|
||
|
||
/**
|
||
* @brief **定向**作废边沿统计状态(参数变化时只影响被改动的那个信号)。
|
||
*
|
||
* ⚠️ 绝不要用 `m_edgeScans.clear()` 代替:那会把**所有**信号的累积状态都丢掉,
|
||
* 于是两个浮动面板里的每一个统计值都会从头重新扫描
|
||
*(用户报告:“改变一个信号的中心轴参数,其它未改变参数的统计也重新计算了”)。
|
||
* 其它信号的键没有变化,它们的块和依然有效,必须保留。
|
||
*
|
||
* @param [IN] signal 参数被改动的信号(nullptr 时不做任何事)。
|
||
* @param [IN] centreOnly true = 只作废该信号的中心轴统计(改中心轴参数时用)。
|
||
*/
|
||
void InvalidateEdgeScansFor(const SignalData* signal, bool centreOnly);
|
||
|
||
/** 多次迭代均值滤波曲线缓存项(按「信号指针 + 均值点数 + 迭代次数」标识)。 */
|
||
struct MeanCurveEntry
|
||
{
|
||
const SignalData* signal = nullptr;
|
||
int points = 5;
|
||
int iterations = 1;
|
||
std::vector<float> values; /**< 与 signal->values 等长;缺失处为 NaN。 */
|
||
};
|
||
mutable std::deque<MeanCurveEntry> m_meanCurveCache;
|
||
|
||
/** 弱峰调整曲线缓存项(按「信号指针 + 调整幅度百分比 + 迭代次数」标识)。 */
|
||
struct WeakPeakCurveEntry
|
||
{
|
||
const SignalData* signal = nullptr;
|
||
int amountPercent = 0; /**< 弱峰调整幅度(-400–400,默认 0 = 不改动)。 */
|
||
int iterations = 1; /**< 迭代次数 N(1–10)。 */
|
||
std::vector<float> values; /**< 与 signal->values 等长;缺失处为 NaN。 */
|
||
};
|
||
mutable std::deque<WeakPeakCurveEntry> m_weakPeakCurveCache;
|
||
|
||
// ---- 异步派生曲线计算(后台线程 + 双缓冲)----
|
||
// 设计:预览曲线(均值滤波 / 弱峰调整)在窗口很大时**不在绘制线程里计算**,而是交给后台
|
||
// 线程;绘制线程本帧先用上一条已算好的曲线顶替(双缓冲),后台算完后通过
|
||
// `kMessagePlotDerivedReady` 通知宿主重绘,从而切换为最新曲线。这样滑块 / 光标 / 视界 /
|
||
// 缩放等交互的反馈始终跟手,不会被计算拖住,同时把重计算放到后台充分利用多核。
|
||
mutable std::mutex m_curveMutex; /**< 保护任务队列与已算好条目。 */
|
||
mutable std::condition_variable m_curveCv; /**< 唤醒后台线程。 */
|
||
mutable std::vector<std::thread> m_curveWorkers; /**< 后台计算线程池(惰性启动,多核并行)。 */
|
||
mutable bool m_curveWorkerStop = false; /**< 请求后台线程退出。 */
|
||
mutable std::uint64_t m_curveGeneration = 0; /**< 数据代次(结构变化时自增)。 */
|
||
mutable std::uint64_t m_curveSerial = 0; /**< 已发布条目序号。 */
|
||
mutable std::deque<CurveJob> m_curveJobs; /**< 待计算任务。 */
|
||
mutable std::deque<CurveEntryPtr> m_curveEntries; /**< 已算好条目(双缓冲的「就绪缓冲」)。 */
|
||
mutable std::vector<CurveEntryPtr> m_curvePins; /**< 本帧正在使用的条目(阻止其被回收)。 */
|
||
mutable std::atomic<bool> m_curveSceneDirty{false}; /**< 后台发布了新条目 → 需重建静态场景缓存。 */
|
||
/**
|
||
* @brief 「每个(信号 + 曲线种类)的最新请求序号」。
|
||
*
|
||
* 拖动参数滑块时同一 (信号, 种类) 会被反复请求;已入队/正在计算的任务一旦不是最新请求,
|
||
* 其参数已被取代 → 工作线程看到后**立即退出并归还缓冲**(“过时任务立即清退”)。
|
||
* 值随每次 `ScheduleCurveJob` 单调递增,因此可以表达“比谁新”。
|
||
*/
|
||
mutable std::map<std::pair<const SignalData*, int>, std::uint64_t> m_curveLatest;
|
||
mutable std::atomic<std::uint64_t> m_curveRequestSerial{0}; /**< 全局请求序号发生器。 */
|
||
mutable int m_curveHugeActive = 0; /**< 正在执行的“大窗口”任务数(最多 1,低内存时保证串行)。 */
|
||
/**
|
||
* @brief 正在计算的曲线键集合(由工作线程在计算期间增删)。
|
||
*
|
||
* 有了它,`ScheduleCurveJob` 才能做到**幂等**:统计侧(边沿计数)会在“曲线还没算好”时
|
||
* 每帧请求一次整条曲线的计算;若每次都当作新请求,就会把上一份正在算的任务判为过时,
|
||
* 于是永远算不完(请求 → 作废 → 重请求 的死循环)。
|
||
* 用**集合**而不是单个键:线程池可能同时算多条不同曲线。
|
||
*/
|
||
mutable std::vector<CurveKey> m_curveRunningKeys;
|
||
mutable std::atomic<bool> m_curveCancelAll{false}; /**< 请求全部在算任务立即退出(结构变化 / 关闭预览)。 */
|
||
mutable bool m_curveLowMemoryWarned = false; /**< 「可用内存严重不足」是否已告警(避免反复弹窗)。 */
|
||
|
||
// 「边沿计数」横向光标 / 面板的拖拽状态。
|
||
int m_edgeDragLane = -1; /**< 正在拖动的横向光标所属网格。 */
|
||
int m_edgePanelDragLane = -1; /**< 正在拖动的统计面板所属网格。 */
|
||
EdgePanelKind m_edgePanelDragKind = EdgePanelKind::COUNT; /**< 正在拖动的面板种类。 */
|
||
gd::PointF m_edgePanelDragOffset; /**< 抓取面板时的相对偏移(保持不跳变)。 */
|
||
|
||
/**
|
||
* @brief 图例条目的命中信息(用于点选与双击改名),同时记录"悬停高亮"重绘内容所需
|
||
* 的全部几何与颜色。
|
||
*
|
||
* 悬停高亮画在**装饰缓存贴图之后**(随鼠标变化,不能进缓存),且托底必须**不透明**
|
||
*(半透明叠加会让底下的旧像素透出来)。为了在"不透明托底"上仍能看到色块与文字,
|
||
* 这里把绘制时用到的色块矩形 / 文本矩形 / 颜色也一并记下来,供悬停时原样重绘。
|
||
*/
|
||
struct LegendHit
|
||
{
|
||
SignalRef ref; /**< 对应信号。 */
|
||
gd::RectF rect; /**< 条目托底矩形(同时是命中区域)。 */
|
||
float radius = 0.0f; /**< 托底圆角半径(悬停高亮重绘时使用)。 */
|
||
gd::RectF swatch; /**< 色块矩形。 */
|
||
gd::RectF text; /**< 文本矩形。 */
|
||
std::wstring label; /**< 已经过省略处理的显示文本。 */
|
||
std::uint32_t swatchColor = 0; /**< 色块颜色。 */
|
||
float swatchOpacity = 1.0f; /**< 色块不透明度。 */
|
||
std::uint32_t textColor = 0; /**< 文本颜色。 */
|
||
};
|
||
|
||
std::vector<LegendHit> m_legendHits; /**< 最近一次绘制时记录的图例条目矩形。 */
|
||
|
||
/**
|
||
* @brief 折线顶点缓存条目。
|
||
*
|
||
* 折线抽稀是 O(采样点数) 的(百万级采样约 2~3ms/条),
|
||
* 而绘制、悬停命中检测每帧都要用到同一批顶点。
|
||
* 用“构建折线时读取的全部输入”作为键缓存顶点,
|
||
* 视图/网格几何未变化时可直接复用,避免重复扫描全量采样。
|
||
*/
|
||
struct PolylineCacheEntry
|
||
{
|
||
SignalRef ref; /**< 信号引用。 */
|
||
const SignalGroup* group = nullptr; /**< 所属分组(缓存键的一部分)。 */
|
||
/**
|
||
* @brief 全部键字段的 64 位哈希(FNV-1a)。
|
||
*
|
||
* ⚠️ **缓存查找只比这一个值**:旧实现逐条比较下面十几个字段,
|
||
* 而"上百个信号合并到同一网格"时(例如 MF4-Record 的 283 个信号)每次查找
|
||
* 都要扫过整张表 ⇒ 绘制与 `ProbeFreeMarker` 每帧做数万次字段比较,
|
||
* 鼠标在网格内移动会明显卡顿(用户 2026-10-30 报告)。
|
||
* 命中哈希后再校验 `ref` / `sampleCount` / 视界三项(极低成本),可忽略碰撞风险。
|
||
*/
|
||
std::uint64_t key = 0;
|
||
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; /**< 网格幅值上界。 */
|
||
float gain = 1.0f; /**< 绘制前的系数。 */
|
||
bool absolute = false; /**< 绘制前是否取绝对值。 */
|
||
bool mirrorVertical = false; /**< 绘制前是否按 Y=0 上下镜像。 */
|
||
bool mirrorHorizontal = false; /**< 是否水平镜像(取值下标反序)。 */
|
||
float offsetY = 0.0f; /**< Y 轴偏移。 */
|
||
double offsetTime = 0.0; /**< 时间轴偏移(秒)。 */
|
||
/**
|
||
* @brief 线宽(逻辑像素)。
|
||
*
|
||
* ⚠️ **必须进缓存键**:`BuildPolyline` 会把线宽**烘进包络带几何**
|
||
* (`strokeHalf = max(1, 线宽) * scale * 0.5`,逐列上下包络各向外扩这么多),
|
||
* 且「梳状」判定也依赖扩展后的列跨度。旧实现漏了这一项 ⇒ 改变线宽时
|
||
* 稠密信号会命中旧条目、一直按**旧线宽**绘制(用户 2026-10-16 报告:
|
||
* 拖动线宽滑块时信号线宽不变,松开后再移动鼠标又变回去)。
|
||
*/
|
||
float lineWidth = 1.0f;
|
||
std::vector<gd::PointF> points; /**< 折线顶点。 */
|
||
std::vector<std::size_t> breaks; /**< 每段折线在 points 中的起始下标。 */
|
||
|
||
/**
|
||
* @brief 是否处于「稠密包络」模式(采样点密度超过像素分辨率)。
|
||
*
|
||
* 此时 `points` 是每列 min/max 交替的锯齿,逐段描边的光栅化开销极大
|
||
* (实测每网格约 14ms);改用下面的「包络带」一次填充后约 1.7ms,
|
||
* 且保留抗锯齿,静态与拖拽帧的观感完全一致。
|
||
*/
|
||
bool bandMode = false;
|
||
|
||
/**
|
||
* @brief 该包络带是否为「梳状」(相邻列的上下包络几乎不相交)。
|
||
*
|
||
* 振荡周期与像素列宽相当时(例如编码器方波在 2ms/大格下约 3 个采样点一列),
|
||
* 逐列包络会在"全高"与"极窄"之间交替。这种形状下 GDI+ 的抗锯齿扫描线代价暴涨
|
||
* (实测同一条 2448 顶点的路径:27.6ms vs 无抗锯齿 3.2ms),
|
||
* 而抗锯齿对它的平滑作用几乎看不出来 —— 因此绘制时改用无抗锯齿填充。
|
||
* ⚠️ 几何本身仍然是**精确**的(不做任何加宽),否则波形会被"糊"成实心块。
|
||
*/
|
||
bool bandSpiky = false;
|
||
|
||
/** 包络带顶点:每段为「上包络正向 + 下包络反向」,闭合后即为该段的填充多边形。 */
|
||
std::vector<gd::PointF> band;
|
||
|
||
/** 每段包络带在 `band` 中的起始下标。 */
|
||
std::vector<std::size_t> bandBreaks;
|
||
};
|
||
|
||
/**
|
||
* @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 「公式映射」信号的逐分组包络缓存(见 `AcquireFormulaEnvelope`)。
|
||
*
|
||
* 只在信号带公式时建立;条目键含公式文本与影响数值的样式字段,
|
||
* 公式 / 系数 / 镜像等变化时自然失效。用 `deque` 保证追加时已有引用不失效。
|
||
*/
|
||
mutable std::deque<FormulaEnvelopeEntry> m_formulaEnvelopes;
|
||
/**
|
||
* @brief 折线顶点缓存的**容量上限**(条目数)。
|
||
*
|
||
* 由 `BuildLaneVisuals` 按"当前全部网格的信号总数 × 2"刷新(下限 `kPolylineCacheLimit`)。
|
||
* ⚠️ 固定 64 条在"多信号合并网格"下会**持续抖动**(装满即整表清空 ⇒ 下一帧全部重建),
|
||
* 这正是"上百个信号合并到同一网格时自由标记非常卡"的根因之一。
|
||
*/
|
||
mutable std::size_t m_polylineCacheCapacity = 64;
|
||
|
||
/**
|
||
* @brief 图层**横向补条**渲染期间的折线裁剪范围(省掉整段折线的重建与描边)。
|
||
*
|
||
* 横向补条只覆盖图层位图的**一条窄竖带**(`m_area` 的 x 区间),但折线原本会按
|
||
* `m_plotRect`(整条宽窗口 = 2× 视界)构建整段顶点并交给 GDI+ 描边 —— 即使只画
|
||
* 一条窄带,`DrawLines`/`FillPath` 仍要逐段提交,实测在"逐点描边"区间单帧高达
|
||
* **13ms**(这正是"深放大下中键拖拽非常卡顿"的根因)。
|
||
* 因此补条渲染时把折线的采样区间按条带的 x 范围裁剪(两侧各留少量像素余量以避免
|
||
* 接缝),只构建并绘制落在条带内的那一段顶点。条带之外的像素由已有图层像素提供,
|
||
* 因此结果仍与整块重画**逐像素一致**。
|
||
*/
|
||
bool m_stripRenderActive = false;
|
||
float m_stripX0 = 0.0f; /**< 条带左边界(与 `m_plotRect` 同坐标系)。 */
|
||
float m_stripX1 = 0.0f; /**< 条带右边界。 */
|
||
/** 条带渲染专用的折线结果(**不进缓存** —— 它只覆盖条带区间,不能当作整段折线复用)。 */
|
||
mutable PolylineCacheEntry m_stripPolylineEntry;
|
||
|
||
/**
|
||
* @brief 静态场景缓存的几何/内容键。
|
||
*
|
||
* 只要这组参数完全一致,"背景 + 全部网格 + 时间轴"的渲染结果就逐像素相同,
|
||
* 可以直接复用离屏位图,无需重算百万级采样的包络与绘制。
|
||
* 这使鼠标悬停、拖拽选区等"只改一小块叠加层"的交互不再重绘波形:
|
||
* 大文件下整幅重绘约 80ms,命中缓存时只需 1~2ms 贴图。
|
||
*/
|
||
struct SceneCacheKey
|
||
{
|
||
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 缩放。 */
|
||
int haloGroup = -1; /**< 已绘入缓存的拖拽光晕信号(无则 -1)。 */
|
||
int haloSignal = -1;
|
||
std::uint64_t contentHash = 0; /**< 信号集合/显示样式/网格幅值等内容指纹。 */
|
||
};
|
||
|
||
/**
|
||
* @brief 构造当前场景的缓存键。
|
||
* @return 缓存键。
|
||
*/
|
||
SceneCacheKey MakeSceneCacheKey() const;
|
||
|
||
/**
|
||
* @brief 判断当前缓存键是否与已捕获的场景一致。
|
||
* @return 可直接贴图返回 true。
|
||
*/
|
||
bool SceneCacheMatches() const;
|
||
|
||
/**
|
||
* @brief **单个网格**的内容指纹(用于该网格自己的「信号图层」)。
|
||
*
|
||
* 图层位图里只含该网格的内容(底色 + 网格线 + 波形),位图高度 = 该网格高度、
|
||
* 内部以 `y=0` 为网格顶边,因此该指纹只需要「网格高度 + 幅值范围 + 本网格各信号的
|
||
* 可见性与外观 + 图例文案」。
|
||
*
|
||
* ⚠️ 这**必须**是逐网格的:早期版本用全局内容指纹,于是一个网格的幅值变化
|
||
* 会同时作废**全部**网格的图层(拖拽时纵向分量让 5 个网格排队重画 → 图层内容
|
||
* 相互不一致 = 错误绘制;且每帧都入队 → 永远补不完)。
|
||
*
|
||
* @param [IN] laneIndex 网格下标。
|
||
* @return 指纹值。
|
||
*/
|
||
std::uint64_t LaneLayerHash(int laneIndex) const;
|
||
|
||
/**
|
||
* @brief 计算场景内容指纹(信号集合、显示样式、网格幅值、图例文案)。
|
||
*
|
||
* 视图几何以外的任何内容变化(改颜色/线宽/透明度/样式、重命名、增删信号)
|
||
* 都会改变该指纹,从而让静态场景缓存自动失效,无需在各处手工调用失效接口。
|
||
*
|
||
* @param [IN] includeStyles 是否把逐信号外观样式(颜色/不透明度/线宽/模式)计入指纹。
|
||
* @param [IN] includeDerived 是否把派生预览曲线的开关与参数计入指纹。
|
||
* @param [IN] includeLaneRange 是否把各网格的**幅值范围**(`yMin/yMax`)计入指纹。
|
||
* ⚠️ **拖拽平移时的纵向分量**会每帧小幅修改被拖网格的幅值范围;若它参与
|
||
* “宽基底可用性”判据,每一帧都会退化成整幅重绘(质量下降 + 卡顿)。
|
||
* 因此宽基底判据用 `includeLaneRange = false` 的几何指纹,
|
||
* 幅值变化改由**逐网格图层自己的指纹**处理(只有该网格需要重画)。
|
||
* @param [IN] includeLaneScroll 是否把**网格纵向滚动量**与各网格的屏幕纵向位置
|
||
* (`LaneScroll()` / `lane.rect.Y`)计入指纹。
|
||
* ⚠️ 静态场景缓存**必须**计入(缓存位图的内容取决于它们);
|
||
* 但“宽基底可用性”判据**不能**计入:网格滚动只改变各网格的**贴图落点**,
|
||
* 图层内容完全不变(`BlitWideBuffer` 本来就按 `lane.rect` 逐网格贴图)——
|
||
* 若计入,则“网格滚动到可视区 / 回到最上方”的每一帧都会退化成整幅重绘
|
||
* (用户 2026-10-17 报告:滚动动画卡顿)。
|
||
* @return 指纹值。
|
||
*/
|
||
std::uint64_t SceneContentHash(bool includeStyles = true, bool includeDerived = true,
|
||
bool includeLaneRange = true,
|
||
bool includeLaneScroll = true) const;
|
||
|
||
/**
|
||
* @brief 场景内容中**不含逐信号外观样式**的指纹。
|
||
*
|
||
* 只包含网格结构、视图几何、信号集合与可见性、图例文案、选中项等
|
||
* "变化后必须整幅重建"的内容;逐信号的 `color / opacity / lineWidth / mode`
|
||
* 一律不参与,**各网格的幅值范围(`yMin/yMax`)也不参与**(由逐网格图层指纹负责)。
|
||
*
|
||
* 用途:外观样式变化时(典型场景是拖动自定义颜色的滑块)波形几何完全不变,
|
||
* 只要该指纹一致,就可以只重画"颜色变了的那个网格"再写回缓存,
|
||
* 而不必整幅重绘全部网格(VERY_BIG_DATA 实测整幅 30ms+、单网格约 4ms)。
|
||
*
|
||
* @return 指纹值。
|
||
*/
|
||
std::uint64_t SceneGeometryHash() const;
|
||
|
||
/**
|
||
* @brief 装饰层(图例 + Y 轴刻度)的指纹:含外观样式,但**不含派生曲线参数**。
|
||
*
|
||
* 拖弱峰调整等级/迭代次数滑块时,图例与 Y 轴刻度不变,仅网格内预览曲线变化;
|
||
* 因此装饰层缓存不应因参数变化而失效(否则拖滑块每帧都要重绘图例与刻度)。
|
||
*
|
||
* @return 指纹值。
|
||
*/
|
||
std::uint64_t SceneDecorHash() const;
|
||
|
||
/**
|
||
* @brief 静态场景缓存中"网格内信号外观样式"的快照条目。
|
||
*/
|
||
struct SceneStyleEntry
|
||
{
|
||
std::uint32_t color = 0; /**< 信号色(未叠加不透明度)。 */
|
||
float opacity = 1.0f; /**< 不透明度。 */
|
||
float lineWidth = 1.0f; /**< 线宽。 */
|
||
int mode = 0; /**< 绘制模式。 */
|
||
float gain = 1.0f; /**< 系数。 */
|
||
bool absolute = false; /**< 是否取绝对值。 */
|
||
bool mirrorVertical = false; /**< 是否按 Y=0 上下镜像。 */
|
||
bool mirrorHorizontal = false; /**< 是否水平镜像(数据反序)。 */
|
||
float offsetY = 0.0f; /**< Y 轴偏移。 */
|
||
double offsetTime = 0.0; /**< 时间轴偏移(秒)。 */
|
||
std::wstring formula; /**< 「公式映射」表达式(空 = 不映射)。 */
|
||
|
||
/** @brief 相等比较(用于判断"逐信号外观样式是否变化")。 */
|
||
bool operator==(const SceneStyleEntry& other) const
|
||
{
|
||
return color == other.color && opacity == other.opacity &&
|
||
lineWidth == other.lineWidth && mode == other.mode &&
|
||
gain == other.gain && absolute == other.absolute &&
|
||
mirrorVertical == other.mirrorVertical &&
|
||
mirrorHorizontal == other.mirrorHorizontal && offsetY == other.offsetY &&
|
||
offsetTime == other.offsetTime && formula == other.formula;
|
||
}
|
||
};
|
||
|
||
/**
|
||
* @brief 单个网格内「叠加预览曲线」的状态(开关 + 每条被勾选信号的参数)。
|
||
*
|
||
* 它**只影响网格内容的绘制**(不影响网格结构/视图几何),因此不进入
|
||
* `SceneGeometryHash`,而是与外观样式一起作为“局部重画”的判据:
|
||
* 拖动弱峰调整的等级/迭代次数滑块时,只需重画受影响的网格,
|
||
* 不必整幅重绘(也不会把静态场景缓存整体作废)。
|
||
*/
|
||
struct SceneDerivedEntry
|
||
{
|
||
int edgeCenter = 0; /**< 中心轴边沿计数开关。 */
|
||
int meanFilter = 0; /**< 均值滤波开关。 */
|
||
int weakPeak = 0; /**< 弱峰调整开关。 */
|
||
/** 各条目展开为 [分组, 信号, 参数…],按原顺序(稳定)。 */
|
||
std::vector<int> edgeCenterItems;
|
||
std::vector<int> meanFilterItems;
|
||
std::vector<int> weakPeakItems;
|
||
};
|
||
|
||
/**
|
||
* @brief 采集当前各网格内可见信号的外观样式快照。
|
||
* @param [OUT] out 采集结果(按网格分组)。
|
||
*/
|
||
void CollectSceneStyles(std::vector<std::vector<SceneStyleEntry>>& out) const;
|
||
|
||
/**
|
||
* @brief 采集当前各网格的「叠加预览曲线」状态快照。
|
||
* @param [OUT] out 采集结果(按网格分组)。
|
||
*/
|
||
void CollectSceneDerived(std::vector<SceneDerivedEntry>& 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 当前应当绘入静态场景缓存的信号光晕(仅拖拽中的信号)。
|
||
*
|
||
* 拖拽信号的光晕在整个拖拽过程中不变,可以进入缓存;
|
||
* 悬停光晕随鼠标移动,必须画在缓存之外。
|
||
*
|
||
* @return 信号引用,无则返回无效引用。
|
||
*/
|
||
SignalRef SceneHaloRef() const;
|
||
|
||
/**
|
||
* @brief 绘制静态场景缓存之外的"悬停高亮"(悬停网格底色 + 悬停信号光晕)。
|
||
* @param [IN,OUT] g 绘图上下文。
|
||
*/
|
||
void PaintHoverHighlight(gd::Graphics& g);
|
||
|
||
/**
|
||
* @brief 绘制图例条目的悬停半高亮提示(在静态场景贴图之后绘制)。
|
||
* @param [IN,OUT] g 绘图上下文。
|
||
*/
|
||
void PaintLegendHighlight(gd::Graphics& g);
|
||
|
||
/**
|
||
* @brief 绘制某条信号的悬停/拖拽光晕(按缓存的折线模式选择最省的画法)。
|
||
* @param [IN,OUT] g 绘图上下文。
|
||
* @param [IN] resolved 已解析的信号。
|
||
* @param [IN] lane 该信号所在网格(单点信号需要它把数据点换算到屏幕坐标)。
|
||
* @param [IN] cached 该信号的折线缓存项(包络带模式下会走带外扩填充)。
|
||
*/
|
||
void PaintSignalHaloFor(gd::Graphics& g, const ResolvedSignal& resolved, const LaneVisual& lane,
|
||
const PolylineCacheEntry& cached);
|
||
|
||
/**
|
||
* @brief 绘制**悬停**光晕:把整条光晕渲染进一张离屏位图并缓存,命中时只贴图。
|
||
*
|
||
* ⚠️ 悬停光晕必须**沿整条曲线**发光(用户 2026-10-18 第二轮报告:上一版只画
|
||
* 鼠标左右 ±72px 的一段,在满幅方波上看起来就是一个**长方形色块**,像选区框)。
|
||
* 而整条光晕的直接绘制代价很高(稠密包络带填充实测 ~14ms/帧,加粗折线描边更是
|
||
* 200ms+/帧)。折中方案:光晕的几何只取决于「信号 + 视图 + 网格几何 + 样式」,
|
||
* **与鼠标位置无关**,因此渲染一次进离屏位图后每帧只贴图(~0.5ms),
|
||
* 并随视图/几何/样式变化自动失效重建。
|
||
*
|
||
* @param [IN,OUT] g 绘图上下文(调用方已设置绘图区裁剪)。
|
||
* @param [IN] resolved 已解析的信号。
|
||
* @param [IN] lane 该信号所在网格。
|
||
* @param [IN] cached 该信号的折线缓存项。
|
||
*/
|
||
void PaintSignalHaloCached(gd::Graphics& g, const ResolvedSignal& resolved,
|
||
const LaneVisual& lane, const PolylineCacheEntry& cached);
|
||
|
||
/**
|
||
* @brief 悬停光晕的离屏缓存(见 `PaintSignalHaloCached`)。
|
||
*
|
||
* 键 = 决定光晕几何的全部输入(与 `AcquirePolyline` 的键同源),
|
||
* 唯独**不含网格的屏幕纵向位置**:滚动网格时只需按新位置贴同一张位图。
|
||
*/
|
||
struct HaloLayerCache
|
||
{
|
||
bool valid = false; /**< 位图内容是否有效。 */
|
||
SignalRef ref; /**< 信号引用。 */
|
||
const SignalGroup* group = nullptr; /**< 所属分组。 */
|
||
std::size_t sampleCount = 0; /**< 采样点数。 */
|
||
double viewMin = 0.0; /**< 时间轴显示下界。 */
|
||
double viewMax = 0.0; /**< 时间轴显示上界。 */
|
||
float plotX = 0.0f; /**< 绘图区左边界。 */
|
||
float plotWidth = 0.0f; /**< 绘图区宽度。 */
|
||
float laneHeight = 0.0f; /**< 网格高度。 */
|
||
double laneYMin = 0.0; /**< 网格幅值下界。 */
|
||
double laneYMax = 0.0; /**< 网格幅值上界。 */
|
||
float lineWidth = 1.0f; /**< 线宽。 */
|
||
DrawMode mode = DrawMode::LINE; /**< 绘制方式。 */
|
||
float gain = 1.0f; /**< 绘制系数。 */
|
||
bool absolute = false; /**< 是否绝对值。 */
|
||
bool mirrorVertical = false; /**< 是否垂直镜像。 */
|
||
bool mirrorHorizontal = false; /**< 是否水平镜像。 */
|
||
float offsetY = 0.0f; /**< Y 偏移。 */
|
||
double offsetTime = 0.0; /**< 时间偏移。 */
|
||
std::wstring formula; /**< 「公式映射」表达式(空 = 不映射)。 */
|
||
int originX = 0; /**< 位图左上角(屏幕 x)。 */
|
||
int originY = 0; /**< 位图左上角(屏幕 y)。 */
|
||
std::unique_ptr<gd::Bitmap> bitmap; /**< 32bpp ARGB 光晕图层(直通 alpha)。 */
|
||
|
||
/**
|
||
* @brief 预乘 alpha 的 DIB 副本(每帧真正贴出去的那张)。
|
||
*
|
||
* ⚠️ 每帧贴图必须走 GDI `AlphaBlend` + `AC_SRC_ALPHA`:
|
||
* `gd::Graphics::DrawImage` 贴一张 32bpp 半透明位图实测约 **6ms**(每帧都付),
|
||
* 而 `AlphaBlend` 对同一张图约 **0.3ms**。
|
||
* `AlphaBlend` 要求源位图的 RGB **已按 alpha 预乘**,因此重建时把
|
||
* `bitmap` 拷进这个 DIB 并顺带做预乘(只付一次)。
|
||
*/
|
||
HDC dc = nullptr;
|
||
HBITMAP dib = nullptr;
|
||
void* bits = nullptr;
|
||
int dibWidth = 0;
|
||
int dibHeight = 0;
|
||
};
|
||
|
||
mutable HaloLayerCache m_haloLayer;
|
||
|
||
/** @brief 释放悬停光晕的离屏位图(析构 / 重建尺寸时使用)。 */
|
||
void ReleaseHaloLayer();
|
||
|
||
/**
|
||
* @brief 把 `m_haloLayer.bitmap` 拷进预乘 alpha 的 DIB(必要时先创建/重建 DIB)。
|
||
* @param [IN] width 需要的位图宽度。
|
||
* @param [IN] height 需要的位图高度。
|
||
* @return DIB 可用返回 true。
|
||
*/
|
||
bool UploadHaloLayer(int width, int height);
|
||
|
||
/**
|
||
* @brief 绘制某条信号的悬停/拖拽光晕(折线描边版本)。
|
||
*
|
||
* ⚠️ 只有**单个有效数据点**的信号没有折线可描边(其默认样式是「点线」),
|
||
* 此时改为围绕那个数据点画一圈强调色圆环 —— 否则悬停时完全没有视觉反馈。
|
||
*
|
||
* @param [IN,OUT] g 绘图上下文。
|
||
* @param [IN] resolved 已解析的信号。
|
||
* @param [IN] lane 该信号所在网格。
|
||
* @param [IN] points 折线顶点。
|
||
* @param [IN] breaks 每段折线的起始下标。
|
||
*/
|
||
void PaintSignalHalo(gd::Graphics& g, const ResolvedSignal& resolved, const LaneVisual& lane,
|
||
const std::vector<gd::PointF>& points, const std::vector<std::size_t>& breaks);
|
||
|
||
/**
|
||
* @brief 单点信号那个数据点绘制时的半径(与散点绘制同源)。
|
||
* @param [IN] resolved 已解析的信号。
|
||
* @return 半径(像素)。
|
||
*/
|
||
float SinglePointDotRadius(const ResolvedSignal& resolved) const;
|
||
|
||
/**
|
||
* @brief 取某信号**唯一数据点**在屏幕上的位置(含时间偏移与「调整」变换)。
|
||
* @param [IN] resolved 已解析的信号(要求 `finiteCount == 1`)。
|
||
* @param [IN] lane 该信号所在网格。
|
||
* @param [OUT] out 屏幕坐标。
|
||
* @return 取到有效位置返回 true。
|
||
*/
|
||
bool SinglePointScreenPos(const ResolvedSignal& resolved, const LaneVisual& lane,
|
||
gd::PointF& out) const;
|
||
|
||
/**
|
||
* @brief 计算某网格图例中 GX 编号徽标的矩形(绘制与命中测试共用,保证一致)。
|
||
* @param [IN] laneIndex 网格下标。
|
||
* @return 徽标矩形;网格不可见时返回空矩形。
|
||
*/
|
||
gd::RectF LaneBadgeRect(int laneIndex) const;
|
||
|
||
/**
|
||
* @brief 查询坐标命中的网格编号徽标(用于发起网格次序拖拽)。
|
||
* @param [IN] x 客户区横坐标。
|
||
* @param [IN] y 客户区纵坐标。
|
||
* @return 命中的网格下标,未命中返回 -1。
|
||
*/
|
||
int LaneBadgeAtPoint(int x, int y) const;
|
||
|
||
/**
|
||
* @brief 网格拖拽重排时某条网格的显示位移(像素)。
|
||
*
|
||
* 被拖拽的网格自由跟随鼠标;它路径上的其它网格整体上/下让位一格。
|
||
* @param [IN] laneIndex 网格下标。
|
||
* @return 纵向位移。
|
||
*/
|
||
float LaneDragOffset(int laneIndex) const;
|
||
|
||
/**
|
||
* @brief 网格拖拽重排:按鼠标位置计算插入位置(0..网格数)。
|
||
* @param [IN] mouseY 鼠标客户区纵坐标。
|
||
* @return 插入位置。
|
||
*/
|
||
int LaneDragTargetAt(int mouseY) const;
|
||
|
||
/**
|
||
* @brief 绘制网格拖拽重排过程中的内容(逐网格按位移贴图 + 目标位置高亮)。
|
||
* @param [IN,OUT] g 绘图上下文。
|
||
*/
|
||
void PaintLaneReorder(gd::Graphics& g);
|
||
|
||
/**
|
||
* @brief 结束网格拖拽重排:若目标位置与原位置不同则真正重排并同步编号。
|
||
*/
|
||
void FinishLaneReorder();
|
||
|
||
/**
|
||
* @brief 确保离屏缓存位图存在且尺寸匹配。
|
||
* @param [IN] width 位图宽度(像素)。
|
||
* @param [IN] height 位图高度(像素)。
|
||
* @return 可用返回 true。
|
||
*/
|
||
bool EnsureSceneCache(int width, int height);
|
||
|
||
/**
|
||
* @brief 把当前画布内容(绘图区底色 + 全部网格 + 时间轴)拷贝进离屏缓存。
|
||
*
|
||
* 缓存区域严格限定为绘图区(m_area)。悬停与拖拽产生的内容一律不进入缓存,
|
||
* 因此在任何交互过程中都可以安全复用,只贴图而不再重绘波形。
|
||
*
|
||
* @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; /**< 缓存对应的场景参数。 */
|
||
/** 缓存捕获时"不含逐信号外观样式"的内容指纹(用于识别仅样式变化)。 */
|
||
std::uint64_t m_sceneGeometryHash = 0;
|
||
/** 缓存捕获时各网格内信号的外观样式快照(按网格分组,与 m_sceneStyles 同序)。 */
|
||
std::vector<std::vector<SceneStyleEntry>> m_sceneStyles;
|
||
|
||
/** 缓存捕获时的“叠加预览曲线”状态快照(用于判断只有曲线参数变化)。 */
|
||
std::vector<SceneDerivedEntry> m_sceneDerived;
|
||
|
||
/**
|
||
* @brief 静态场景缓存里存的是否为「快速帧」(交互期间关掉了抗锯齿)。
|
||
*
|
||
* 为 true 时 `SceneCacheMatches()` 直接判不命中,强制重画一次全质量版本 ——
|
||
* 这就是用户要求的“先用低分辨率、绘制完成后再用高分辨率替换”。
|
||
*/
|
||
bool m_sceneDegraded = false;
|
||
|
||
// ---- 「装饰层」像素缓存(Y 轴刻度胶囊 + GX 编号 + 图例)----
|
||
// 装饰只取决于网格布局 / 幅值范围 / 信号外观与图例文案,与时间视界无关;但它
|
||
// **不适合**放进「宽基底」:平移是从基底里截取子矩形,烘进基底的装饰会跟着一起
|
||
// 移动 → 在网格左缘之外留下一排旧图例/刻度(重影)。因此单独缓存一层的像素。
|
||
//
|
||
// ⚠️ **本缓存带真 Alpha**(32bpp DIB + `AlphaBlend`),而不是"从后备缓冲区抓一块不透明
|
||
// 矩形":装饰是**圆角**图形,抓矩形会把圆角之外(属于波形)的像素也一并烘进缓存,
|
||
// 平移贴回时露出"圆角矩形外围多出一圈直角白边 / 旧波形像素残留"(用户报告)。
|
||
// 现在改为:**把装饰单独绘制进一张透明 DIB**(圆角之外保持全透明),贴回时用
|
||
// `AlphaBlend` 合成 ⇒ 圆角之外透出的是**当前**波形,既干净又可跨视界复用。
|
||
//
|
||
// ⚠️ 关键不变量:**每一帧要显示装饰时,都用 `m_decorKey == SceneDecorHash()` 判定缓存
|
||
// 是否同源**;不同源就必须先 `RenderDecorCache()` 再贴回。
|
||
/**
|
||
* @brief 确保装饰缓存位图尺寸正确(不足则重建为 32bpp 的 DIB 段)。
|
||
* @param [IN] width 位图宽度。 @param [IN] height 位图高度。
|
||
* @return 可用返回 true。
|
||
*/
|
||
bool EnsureDecorCache(int width, int height);
|
||
/** @brief 释放装饰缓存位图并作废缓存。 */
|
||
void ReleaseDecorCache();
|
||
/**
|
||
* @brief 把装饰(Y 轴胶囊 + GX 编号 + 图例)**单独绘制**进透明的装饰缓存位图。
|
||
*
|
||
* 位图覆盖范围 = 客户区原点到绘图区右下角(`m_area` 的右下角),因此绘制坐标与
|
||
* 正常绘制**完全一致**(GDI 文本不认 GDI+ 变换,必须避免任何坐标平移)。
|
||
* 结束后做一次 alpha 修补:GDI 文本会把写入像素的 alpha 清成 0,需要还原为不透明。
|
||
* @return 成功返回 true。
|
||
*/
|
||
bool RenderDecorCache();
|
||
/**
|
||
* @brief 把装饰缓存里的装饰按 `AlphaBlend` 贴回后备缓冲区。
|
||
* @param [IN,OUT] g 绘图上下文。 @return 贴图成功返回 true。
|
||
*/
|
||
bool BlitDecorCache(gd::Graphics& g);
|
||
/**
|
||
* @brief 取 `AlphaBlend` 函数指针(`gdi32!GdiAlphaBlend`,必要时回退到 `msimg32!AlphaBlend`)。
|
||
*
|
||
* 走 `GetProcAddress` 而不是链接库:本项目的 Makefile 只链接 gdi32 等固定几个库,
|
||
* 且两台协作设备的工具链不同,动态解析可避免改动构建脚本、也避免引入新的链接依赖。
|
||
* @return 可用返回函数指针,否则返回 nullptr。
|
||
*/
|
||
static void* ResolveAlphaBlend();
|
||
|
||
mutable HDC m_decorDc = nullptr; /**< 装饰缓存设备上下文。 */
|
||
mutable HBITMAP m_decorBitmap = nullptr; /**< 装饰缓存位图。 */
|
||
mutable HGDIOBJ m_decorOldBitmap = nullptr; /**< 装饰 DC 原选位图。 */
|
||
mutable void* m_decorBits = nullptr; /**< 位图像素(32bpp DIB 段,可写 alpha)。 */
|
||
mutable int m_decorW = 0; /**< 装饰缓存宽度。 */
|
||
mutable int m_decorH = 0; /**< 装饰缓存高度。 */
|
||
mutable bool m_decorValid = false; /**< 装饰缓存内容是否有效。 */
|
||
mutable std::uint64_t m_decorKey = 0; /**< 装饰缓存对应的内容指纹。 */
|
||
mutable bool m_useDecorCache = false; /**< 本帧是否跳过绘制装饰、改用缓存贴回。 */
|
||
/** 本帧需要捕获的装饰矩形(各网格的 Y 轴胶囊 + GX 编号 + 图例托底)。 */
|
||
mutable std::vector<gd::RectF> m_decorRects;
|
||
/**
|
||
* @brief 缓存里那批像素对应的装饰矩形(与像素同时生成)。
|
||
*
|
||
* ⚠️ 贴回必须用这一份,**不能**用当前的 `m_decorRects`:
|
||
* 命中缓存时各网格会跳过绘制、根本不会重建矩形列表(为空),
|
||
* 若用当前列表就会贴错位置或一个都不贴(表现为装饰消失 / 黑块 / 错位)。
|
||
*/
|
||
mutable std::vector<gd::RectF> m_decorCapturedRects;
|
||
|
||
// ---- 「派生预览曲线」像素缓存(均值滤波 / 中心轴边沿计数 / 弱峰调整的叠加曲线)----
|
||
//
|
||
// 为什么需要单独一层像素缓存(用户 2026-11-02 反馈“极端工况下所有交互都卡”):
|
||
// 每条预览曲线都要按「逐像素列包络带」填充,代价 ∝ **覆盖像素数**。15 个信号 ×
|
||
// 3 个功能 = 45 条曲线、哪怕单条只要 2ms,一帧也要 90ms —— 而其中**绝大多数帧
|
||
// 画面根本没变**(拖动光标、悬停、静止)。因此与装饰层同一套思路:
|
||
// 曲线画进一张带 alpha 的 32bpp DIB,正常帧只做一次 `AlphaBlend`(约 1~2ms)。
|
||
//
|
||
// 平移复用(关键):视界**只平移不改跨度**时,画面内容只是横向平移 —— 于是把 DIB
|
||
// 的像素整体 memmove 一个 dx,再只重画**新露出的那条竖带**即可(与宽缓冲区
|
||
// 的滚动逻辑同构),因此拖动视界的每帧代价与信号数无关。
|
||
//
|
||
// ⚠️ 与装饰层的区别:本层的**内容随视界变化**,因此必须记录渲染时的视界
|
||
// (`m_derivedViewMin/Max`);跨度一变就必须整块重画。
|
||
/**
|
||
* @brief 确保派生曲线缓存位图尺寸正确(不足则重建为 32bpp DIB 段)。
|
||
* @param [IN] width 位图宽度。 @param [IN] height 位图高度。
|
||
* @return 可用返回 true。
|
||
*/
|
||
bool EnsureDerivedCache(int width, int height);
|
||
/** @brief 释放派生曲线缓存位图并作废缓存。 */
|
||
void ReleaseDerivedCache();
|
||
/**
|
||
* @brief 把全部派生预览曲线绘制进透明缓存位图。
|
||
*
|
||
* `m_derivedStripActive == true` 时只重画 `[m_derivedStripX0, m_derivedStripX1)` 这段竖带
|
||
* (平移后的“新露出”区域),否则整块重画。
|
||
* @return 成功返回 true。
|
||
*/
|
||
bool RenderDerivedCache();
|
||
/**
|
||
* @brief 把派生曲线缓存按 `AlphaBlend` 贴回后备缓冲区(仅绘图区)。
|
||
* @param [IN,OUT] g 绘图上下文。 @return 贴图成功返回 true。
|
||
*/
|
||
bool BlitDerivedCache(gd::Graphics& g);
|
||
/**
|
||
* @brief 派生曲线缓存的内容指纹(**不含视界**:视界单独比对,用于判断能否滚动复用)。
|
||
* @return 指纹。
|
||
*/
|
||
std::uint64_t DerivedOverlayKey() const;
|
||
/**
|
||
* @brief 按当前视界/内容刷新派生曲线缓存,并把结果贴回后备缓冲区。
|
||
*
|
||
* 三种情形:内容与视界都相同 → 只贴图;内容相同且跨度不变(纯平移)→ 滚动 + 补条带;
|
||
* 其余 → 整块重画。
|
||
* @param [IN,OUT] g 绘图上下文。
|
||
*/
|
||
void PaintDerivedOverlayLayer(gd::Graphics& g);
|
||
|
||
mutable HDC m_derivedDc = nullptr; /**< 派生曲线缓存设备上下文。 */
|
||
mutable HBITMAP m_derivedBitmap = nullptr; /**< 派生曲线缓存位图。 */
|
||
mutable HGDIOBJ m_derivedOldBitmap = nullptr; /**< DC 原选位图。 */
|
||
mutable void* m_derivedBits = nullptr; /**< 位图像素(32bpp DIB 段)。 */
|
||
mutable int m_derivedW = 0; /**< 缓存宽度。 */
|
||
mutable int m_derivedH = 0; /**< 缓存高度。 */
|
||
mutable bool m_derivedValid = false; /**< 缓存内容是否有效。 */
|
||
mutable std::uint64_t m_derivedContentKey = 0; /**< 缓存对应的内容指纹(不含视界)。 */
|
||
mutable double m_derivedViewMin = 0.0; /**< 缓存渲染时的视界下限。 */
|
||
mutable double m_derivedViewMax = 0.0; /**< 缓存渲染时的视界上限。 */
|
||
mutable bool m_derivedStripActive = false; /**< 当前是否只重画一条竖带。 */
|
||
mutable float m_derivedStripX0 = 0.0f; /**< 竖带左边界(客户区像素)。 */
|
||
mutable float m_derivedStripX1 = 0.0f; /**< 竖带右边界(客户区像素)。 */
|
||
|
||
// ---- 每网格「信号图层」(前台同步维护;交互帧只做贴图 / 坐标偏移) ----
|
||
//
|
||
// 平移只改变绘制坐标、不改变画面内容。因此**每个网格各自维护一块离屏位图**
|
||
// (宽度 = 视界宽 + 左右各半个视界的余量),称为该网格的「信号图层」。
|
||
// 图层只含基底(底色 + 网格线 + 波形),**不含装饰**(Y 轴刻度 / 图例)——
|
||
// 装饰锚定在网格左缘、不随时间视界移动,若烘进图层会被平移带走(重影),
|
||
// 因此装饰由前台单独绘制(见 `m_decorCapturedRects` 那套缓存)。
|
||
//
|
||
// ⚠️ **图层一律在 UI 线程内同步维护**(`Paint` 全程持有 `m_renderMutex`),
|
||
// 因为渲染图层会临时改写 `m_area` / `m_plotRect` / `Tab().ViewMin()/ViewMax()` /
|
||
// 各网格矩形 —— 这些状态**同时被 UI 线程的消息处理无锁读取**
|
||
// (`OnMouseMove` 平移读视界、命中测试读网格矩形)。若把图层渲染搬到后台线程,
|
||
// 后台的"保存 → 改写 → 恢复"会**覆盖用户正在进行的平移**(视界被弹回原位),
|
||
// 命中测试也会读到错位的网格 → 表现为「拖动时视界跳回 / 波形抖动 / 绘制错乱」。
|
||
// 这正是先前"后台图层线程"被移除的原因:**不要把图层渲染移回后台**。
|
||
//
|
||
// 每个图层记录自己的时间映射(`LaneLayer::wideMin`):不同时刻渲染出的图层锚点
|
||
// 并不相同,滚动与贴图一律**逐网格**按各自的 `wideMin` 计算,**绝不共用一个全局锚点**
|
||
// (共用全局锚点会让各网格被平移不同的真实距离 → 网格之间时间错位)。
|
||
/**
|
||
* @brief 单个网格的「信号图层」(一块只含该网格基底的离屏位图)。
|
||
*/
|
||
struct LaneLayer
|
||
{
|
||
HDC dc = nullptr; /**< 离屏 DC。 */
|
||
HBITMAP bitmap = nullptr; /**< 离屏位图。 */
|
||
HGDIOBJ oldBitmap = nullptr; /**< DC 原选位图。 */
|
||
int width = 0; /**< 位图宽度(含左右各半屏余量)。 */
|
||
int height = 0; /**< 位图高度(含上下各半个网格高的余量)。 */
|
||
int marginY = 0; /**< 位图在**可见网格上方**预留的像素行数(纵向预渲染余量)。 */
|
||
double wideMin = 0.0; /**< 位图 x=0 对应的时间。 */
|
||
double span = 0.0; /**< 渲染时的视界跨度(决定像素/时间)。 */
|
||
/**
|
||
* @brief 渲染时的**纵向锚点**:该网格的 `yMax`(可见窗口顶行对应的数值)。
|
||
*
|
||
* 与 `wideMin` 同理,平移(纵向是幅值平移)时只移动**贴图偏移**、不重画像素:
|
||
* 需要贴到屏幕顶行的位图行 = `marginY + round((anchorYMax - 当前 yMax) * pxPerValue)`。
|
||
*/
|
||
double anchorYMax = 0.0;
|
||
double pxPerValue = 0.0; /**< 位图纵向像素 / 数值单位(= 可见网格高 / 幅值跨度)。 */
|
||
std::uint64_t key = 0; /**< 内容指纹(含跨度、网格几何与外观)。 */
|
||
bool valid = false; /**< 是否有可显示的像素。 */
|
||
};
|
||
|
||
/** @brief 确保某网格图层位图尺寸正确(不足则重建 DC/位图)。 */
|
||
bool EnsureLaneLayer(LaneLayer& layer, int width, int height);
|
||
/** @brief 释放单个图层位图。 */
|
||
void ReleaseLaneLayer(LaneLayer& layer);
|
||
/** @brief 按当前视界与内容计算某网格图层的内容指纹。 */
|
||
std::uint64_t LaneLayerKey(int laneIndex) const;
|
||
/**
|
||
* @brief 把某网格的基底渲染进指定图层(同步,调用者需持有 `m_renderMutex`)。
|
||
* @param [IN] laneIndex 网格下标。
|
||
* @param [IN,OUT] layer 目标图层(会被写入尺寸 / 时间映射 / 指纹)。
|
||
* @param [IN] stripX 只渲染位图的这一段 x 区间(滚动时补画细条用)。
|
||
* @param [IN] stripW 该区间的宽度;传 0 表示横向整块。
|
||
* @param [IN] stripY 只渲染位图的这一段 y 区间(纵向滚动补条用)。
|
||
* @param [IN] stripH 该区间的高度;传 0 表示纵向整块。
|
||
* @param [IN] wideMin 位图 `x=0` 对应的时间。
|
||
* **必须与调用方的意图一致**:整块重画传 `LayerDefaultWideMin()`(以当前视界居中);
|
||
* 滚动补细条则传 `layer.wideMin`(与该图层已有像素同一映射,否则会留下接缝)。
|
||
* @param [IN] anchorYMax 本次渲染的像素所对应的**纵向锚点**(= 该网格的 `yMax`)。
|
||
* ⚠️ **必须与"本次改动的那部分像素"一致**:
|
||
* - 整块重画 / 纵向补条(整宽):整块图层都对齐到锚点 → 传**当前** `yMax`
|
||
* (纵向补条则传 `layer.anchorYMax` 滚动后的值,与已滚动的像素同一映射);
|
||
* - **横向补细条(整高)**:只改一条竖带,其余像素仍在**原锚点**上,
|
||
* 因此必须传 `layer.anchorYMax`(**绝不能传当前 `yMax`**)—— 否则贴图偏移被
|
||
* 静默清零、而像素并未上移,整个网格的波形会纵向错位(用户报告的
|
||
* "垂直拖动后再横向平移出现纵向跳变/绘制错误")。
|
||
* @return 渲染成功返回 true。
|
||
*/
|
||
bool RenderLaneLayerRegion(int laneIndex, LaneLayer& layer, int stripX, int stripW, int stripY,
|
||
int stripH, double wideMin, double anchorYMax);
|
||
/**
|
||
* @brief 前台补画各网格的**分界线**(网格顶部那条较重的横线)。
|
||
*
|
||
* ⚠️ 分界线锚定在网格的**屏幕位置**(`lane.rect.Y`),幅值平移时它**不跟着移动**,
|
||
* 因此不能烘进"纵向可平移"的图层里(否则纵向拖动时它会跟着波形跑)。
|
||
* 整幅重绘由 `PaintLane` 直接绘制;宽基底快路径(贴图层像素)必须在这里补画一次。
|
||
*/
|
||
void PaintLaneDividers(gd::Graphics& g) const;
|
||
/** @brief 「以当前视界居中」的默认图层映射(位图 `x=0` 对应的时间)。 */
|
||
double LayerDefaultWideMin() const;
|
||
/** @brief 图层位图的横向余量(左右各半屏 → 可横向平移 ±半屏)。 */
|
||
int LayerMarginX() const;
|
||
/**
|
||
* @brief 图层位图的纵向余量(上下各半个网格高 → 可纵向平移 ±半个网格高)。
|
||
*
|
||
* ⚠️ 纵向余量让「拖拽平移的纵向分量」(幅值平移)与横向平移一样**只改贴图偏移**,
|
||
* 不必每帧重画整块图层 —— 这正是「左右平移流畅、上下平移卡顿」的根因所在。
|
||
* 余量越大可连续拖拽的范围越大,但每个横向补条 / 整块重画的代价也按余量线性增长
|
||
*(因此取「半个网格高」:一整屏的额外内容已足够,且不拖慢已经流畅的横向平移)。
|
||
*/
|
||
int LayerMarginY(float laneHeight) const;
|
||
|
||
/**
|
||
* @brief 以当前视界为中心重建全部网格图层(同步;跨度变化 / 首帧 / 图层缺失时使用)。
|
||
* @return 全部成功返回 true。
|
||
*/
|
||
bool RebuildWideBuffer();
|
||
/**
|
||
* @brief 让各图层"滚动"以跟随视界:逐网格整数像素自贴图平移 + 只补画新暴露的细条。
|
||
*
|
||
* ⚠️ **本函数绝不做"整块重画"**:内容指纹过期 / 锚点离视界太远的网格只被排进
|
||
* `m_layerRebuildPending`(随后由 `RenderPendingLaneRebuilds` 按预算补画),本帧仍按
|
||
* 现有像素做整数像素平移,因此**前台线程永远不会因为"图层没准备好"而卡顿**。
|
||
*/
|
||
void RollWideBuffer();
|
||
/**
|
||
* @brief 逐个网格截取图层子矩形贴图到后备缓冲区(O(1) 位图搬运)。
|
||
*
|
||
* 图层只覆盖"视界 ± 半屏":若当前视界有一部分落在图层之外,这里**只贴有像素的那一段**
|
||
* (其余留由底色填充,下一帧补齐),而不是跳过整块 —— 这正是"未绘制的部分先暂时不管"。
|
||
* @param [IN,OUT] g 绘图上下文。
|
||
* @return 至少贴了若干像素返回 true。
|
||
*/
|
||
bool BlitWideBuffer(gd::Graphics& g, const std::vector<double>& timeTicks);
|
||
|
||
/**
|
||
* @brief 为单个网格绘制**底衬**(网格底色 + 横向/竖向网格线 + 网格分界线)。
|
||
*
|
||
* 不含波形、派生预览曲线、Y 轴刻度与图例 —— 这些要么代价高、要么属于装饰层。
|
||
* 用途:宽基底快路径中本帧拿不到图层像素的网格用它兜底,保证**网格底衬这种
|
||
* 廉价绘制永远不会空白/闪烁**(用户 2026-10-17 明确要求)。
|
||
* @param [IN,OUT] g 绘图上下文。
|
||
* @param [IN] laneIndex 网格下标。
|
||
* @param [IN] timeTicks 当前视界的时间主刻度。
|
||
* @return 绘制了内容返回 true。
|
||
*/
|
||
bool PaintLaneGridBacking(gd::Graphics& g, int laneIndex, const std::vector<double>& timeTicks);
|
||
/**
|
||
* @brief 全部图层是否**结构可用**(数量 / 位图尺寸 / 视界跨度都与当前状态一致)。
|
||
*
|
||
* 结构不可用意味着"像素的时间映射已经不对",只能整批重建(跨度变化 / 布局变化 / 首帧);
|
||
* 内容指纹是否最新由 `LaneLayerReady()` 逐个判断。
|
||
* @return 可以直接滚动 + 贴图层像素返回 true。
|
||
*/
|
||
bool LayersStructurallyReady() const;
|
||
/**
|
||
* @brief 图层数组是否已经**与当前网格数量对齐**(首次绘制 / 增删网格后为 false)。
|
||
*
|
||
* ⚠️ 只有它返回 false 时才允许**同步**整批重建(否则没有可拉伸的旧像素可占位)。
|
||
* 跨度变化(缩放)**不**走同步重建:改为"占位拉伸 + 按预算逐个补齐"。
|
||
* @return 数量对齐返回 true。
|
||
*/
|
||
bool LayersAllocated() const;
|
||
/**
|
||
* @brief 某个网格的图层是否**完全就绪**(结构可用且内容指纹与当前状态一致)。
|
||
* @param [IN] laneIndex 网格下标。
|
||
* @return 就绪返回 true。
|
||
*/
|
||
bool LaneLayerReady(int laneIndex) const;
|
||
/** @brief 全部网格图层是否都完全就绪(用于判断能否做"结算"整幅精确帧)。 */
|
||
bool LanesAllReadyForView() const;
|
||
/**
|
||
* @brief 该网格是否**在屏幕可视区内**(其纵向带与绘图区相交)。
|
||
*
|
||
* ⚠️ 只有可视网格的图层才影响画面。整批重建时必须**优先补画可视网格**,
|
||
* 否则(上百个网格时)队列按下标顺序推进,可视网格可能要等上百帧才轮到,
|
||
* 期间画面一直停留在旧的兜底状态(用户报告:需要等一会才正确加载)。
|
||
* @param [IN] laneIndex 网格下标。
|
||
* @return 可视返回 true。
|
||
*/
|
||
bool LaneVisuallyRelevant(int laneIndex) const;
|
||
/**
|
||
* @brief 在「可视」基础上再多算 `prefetchScreens` 个绘图区高度的**预取带**。
|
||
*
|
||
* ⚠️ 预取带只用于决定「是否还要请求下一帧去补图层」(`AnyVisibleLanePending` / 预热):
|
||
* 网格滚动动画很可能在几百毫秒内扫过一整个屏高,若只预建「严格可见」的网格,
|
||
* 动画扫进去的那几帧会落在一堆**尚未建过图层**的网格上 ⇒ 每帧都要靠底衬兜底
|
||
* 画十几份网格(实测 ~8ms/帧)⇒ 动画明显卡顿(用户 2026-10-17 报告)。
|
||
* 预取带让静置期间就把附近网格的图层建好。
|
||
* ⚠️ **「结算」不能带预取带**(它要求的是“当前可见区域像素精确”)。
|
||
* @param [IN] laneIndex 网格下标。
|
||
* @param [IN] prefetchScreens 预取带宽度(倍绘图区高度,0 = 严格可见)。
|
||
* @return 可视(或在预取带内)返回 true。
|
||
*/
|
||
bool LaneVisuallyRelevant(int laneIndex, int prefetchScreens) const;
|
||
/** @brief 全部**可视**网格图层是否都已就绪(用于判断能否做结算的整幅精确帧)。 */
|
||
bool LanesVisiblyReadyForView() const;
|
||
/**
|
||
* @brief 待重画队列里是否还有**可视**网格。
|
||
*
|
||
* ⚠️⚠️ 用它(而不是“队列非空”)来决定“是否要请求宿主再送一帧”:
|
||
* 上百个网格时队列里会长期残留大量**离屏**网格,若按“队列非空”每帧都请求重绘,
|
||
* 就会形成持续 1 秒以上、每帧一次的高频重绘(用户在“显示全部信号 → 折起分组”
|
||
* 时正好会撞上),把动画的 `WM_TIMER`(优先级低于投递消息)饿死 ⇒ 动画卡住 / 闪烁。
|
||
* 离屏网格改为**机会式**预热:任何一帧(悬停、动画、滚动…)顺手补一个即可。
|
||
* @return 仍有可视网格待重画返回 true。
|
||
*/
|
||
bool AnyVisibleLanePending() const;
|
||
|
||
/**
|
||
* @brief 「图层预热」是否允许再请求一帧(**限速**)。
|
||
*
|
||
* ⚠️⚠️ 背景:待重画队列在“全部信号各自一块网格”的场景下可能有上百个**离屏**网格。
|
||
* 若“队列非空就每帧都请求重绘”,会持续一秒以上地高频重绘,而 `WM_TIMER` 的优先级
|
||
* **低于投递消息** ⇒ 动画定时器被饿死(实测 `frames=124 / 动画 tick=1`)。
|
||
* 若反过来“离屏的一律不问”,则用户快速滚动到那些网格时它们**还没建过图层**,
|
||
* 每帧要靠底衬兜底画十几份网格(实测 ~8ms/帧)⇒ 动画卡顿
|
||
*(用户 2026-10-17 报告:MF4 + VERY_BIG_DATA 时点选最后一个信号,滚动明显卡)。
|
||
*
|
||
* ✓ 折中:以约 `kPrefetchMinIntervalMs` 的间隔**慢速**继续请求,让整批图层在静置期间
|
||
* 逐步建好(对 130 个网格大约几秒),期间不会把动画的定时器挤出预算。
|
||
*
|
||
* @return 允许请求返回 true(并记录本次时刻)。
|
||
*/
|
||
bool PrefetchStepAllowed();
|
||
/** @brief 把某网格排进"待重画"队列(去重;视界/跨度后续变化时会在渲染时再次校验)。 */
|
||
void QueueLaneRebuild(int laneIndex);
|
||
/**
|
||
* @brief 按预算补画"待重画"的网格图层(**本帧最多 `budget` 个**)。
|
||
*
|
||
* 这是"前台不做整批重绘"的关键:即使 5 个网格都需要重画,也一帧只画一个,
|
||
* 其余留到后续帧(缺的部分留空 / 用旧像素)。调用方若发现队列非空应请求再次重绘。
|
||
* @param [IN] budget 本次最多重画的网格数量。
|
||
* @return 队列中仍有待重画网格返回 true。
|
||
*/
|
||
bool RenderPendingLaneRebuilds(int budget);
|
||
/**
|
||
* @brief **空闲预热**:静置期间用既有预算机制**每帧至多补画 1 个网格图层**。
|
||
*
|
||
* 图层不存在(刚加载 / 网格数量变化)或还是旧跨度时,若等到用户**第一次**
|
||
* 平移 / 选区放大才去建,`Paint` 会在那一帧里**同步重建全部网格图层**
|
||
*(5 个网格实测 30~80ms)—— 这就是"选区放大的第一帧特别卡"的根因
|
||
*(该帧其余工作只有约 16ms 的占位拉伸)。
|
||
*
|
||
* 这里只在**视界没有变化**(命中静态场景缓存 ⇒ 本帧视图与缓存同源)且**不在拖拽中**时
|
||
* 调用一次,把整批重建的代价摊到若干静止帧上;任何交互帧都不再承担它。
|
||
*/
|
||
void WarmLaneLayersStep();
|
||
|
||
/**
|
||
* @brief 预建「宽缓冲基底」:只为**尚未建立 / 已失效**的网格补图层。
|
||
*
|
||
* 用户 2026-10-17 报告:每次开/关一个实验性功能(以及关闭浮动面板)都会卡一下。
|
||
* 根因是旧实现直接 `RebuildWideBuffer()` —— 它**同步重渲染全部网格的图层**:
|
||
* “全部信号各自一块网格”时可达上百个网格、其中可能含着千万级采样的巨型信号,
|
||
* 整批重建要**几百毫秒**。
|
||
*
|
||
* ✓ 现在:只有「图层数组与网格数量不符」(首次绘制 / 增删网格)才同步整批建立;
|
||
* 其余只把缺失 / 过期的网格登记进待重画队列,由既有预算机制逐帧补齐。
|
||
* 这已经足够达成原本的目的(避免“首次拖动参数滑块时的一次性重建尖峰”):
|
||
* 拖动滑块时视界在变 ⇒ 本就不做重建、直接用现有基底。
|
||
*/
|
||
void EnsureLaneLayersPrefetched();
|
||
/** @brief 使全部图层失效。 */
|
||
void InvalidateWideBuffer(); /** @brief 释放全部图层位图。 */
|
||
void ReleaseWideBuffer();
|
||
|
||
/**
|
||
* @brief **并行**重建多个网格的信号图层(多核;用于"视界已停"时一次性补齐所有过期网格)。
|
||
*
|
||
* ⚠️ 并行化方式:为每条工作线程准备一个**隔离的 `PlotView`**(它把 `m_tab` 置空、
|
||
* 用自带的 `m_localTab` 副本),因此 `RenderLaneLayerRegion` 改写 `m_area` / `m_plotRect` /
|
||
* 视界 / 网格矩形时**只作用于该线程自己的副本**,不会与 UI 线程或其它工作线程互相覆盖
|
||
*(这正是 2026-10-12 移除"后台图层线程"的根因;用隔离上下文即可安全并行)。
|
||
* 合成只需各线程把自己那块位图交给主线程(`std::swap` 交换 `m_layerFront`)。
|
||
* @param [IN] laneIndices 要重建的网格下标(各自独立)。
|
||
* @param [IN] wideMin 图层位图 `x=0` 对应的时间(各网格一致,即"以当前视界居中")。
|
||
*/
|
||
void RebuildLaneLayersParallel(const std::vector<int>& laneIndices, double wideMin);
|
||
/** @brief 创建(或按需扩容)并行图层渲染的工作线程上下文。 */
|
||
void EnsureLayerWorkers();
|
||
/** @brief 释放并行图层渲染的工作线程上下文与暂存图层。 */
|
||
void ReleaseLayerWorkers();
|
||
|
||
/** 并行图层渲染的工作上下文(每线程一个,惰性创建;每个都有自己的 `m_workerTab`)。 */
|
||
std::vector<std::unique_ptr<PlotView>> m_layerWorkers;
|
||
int m_layerWorkerCount = 0;
|
||
/**
|
||
* @brief 工作线程实例**自有的**标签页配置(`m_tab` 指向它)。
|
||
*
|
||
* 工作线程的 `RenderLaneLayerRegion` 会改写视界 / 网格矩形;让它改在自己这份副本上,
|
||
* 就不会碰到 UI 线程共享的 `TabConfig`(安全并行的前提)。
|
||
* ⚠️ `m_tab` **必须非空** —— `BuildLaneVisuals()` 在 `m_tab == nullptr` 时会清空 `m_lanes`。
|
||
*/
|
||
TabConfig m_workerTab;
|
||
|
||
std::vector<LaneLayer> m_layerFront; /**< 各网格的「信号图层」(与 `m_lanes` 同序)。 */
|
||
/** 待重画的网格下标(去重;`RollWideBuffer` 登记,`RenderPendingLaneRebuilds` 消费)。 */
|
||
std::vector<int> m_layerRebuildPending;
|
||
/**
|
||
* @brief 图层元数据的互斥(当前图层只在 UI 线程维护,故它实际不承担跨线程保护;
|
||
* 保留是为了给 `BlitWideBuffer()` 这类 `const` 读取路径一个明确的一致性边界)。
|
||
*/
|
||
mutable std::mutex m_layerMutex;
|
||
std::recursive_mutex m_renderMutex; /**< 串行化"改写渲染状态"的绘制(整帧 / 单网格图层);可重入。 */
|
||
/** 已渲染的图层区域数量(原子,仅用于验证 / 诊断)。 */
|
||
mutable std::atomic<std::uint64_t> m_layerRenderCount{0};
|
||
|
||
bool m_renderWide = false; /**< 正在渲染信号图层(重入 `PaintLane` 时跳过装饰)。 */
|
||
|
||
/**
|
||
* @brief 本次 `PaintLane` 只绘制「网格底衬」(底色 + 网格线 + 网格分界线)。
|
||
*
|
||
* 用于宽基底快路径的兜底:本帧拿不到图层像素的网格必须**立刻**画出正确的网格底衬,
|
||
* 否则该网格会显示成一块空白(用户 2026-10-17 报告「网格叠底消失」)。
|
||
* 底色/网格线的绘制代价极低,因此这条兜底路径可以**每帧**执行。
|
||
*/
|
||
bool m_renderGridOnly = false;
|
||
|
||
/**
|
||
* @brief 渲染信号图层时,横向网格线(幅值刻度)的「参考跨度 / 参考高度」。
|
||
*
|
||
* ⚠️ **必须是前台可见网格的值**,而不是图层画布的(含纵向余量的)值:
|
||
* 图层的幅值刻度必须以该跨度为基准取「整齐步长」,否则画布里的网格线密度与
|
||
* 整幅渲染不一致 —— 贴图后拖动时网格线会跳变。步长一致时刻度落在同一个
|
||
* 绝对值格点上,于是纵向平移就是纯粹的像素平移(逐像素同源)。
|
||
*/
|
||
double m_wideTickSpan = 0.0;
|
||
float m_wideTickHeight = 0.0f;
|
||
/**
|
||
* @brief 渲染「信号图层(宽基底)」时使用的时间主刻步长(秒);0 = 按当前视界自行推算。
|
||
*
|
||
* ⚠️ **必须由 `RenderLaneLayerRegion` 显式传入(前台视界跨度算出的步长)**:
|
||
* 图层画布比前台宽约一倍(左右各半屏),而 `Tab().ViewMin/ViewMax` 在图层的临场渲染里
|
||
* 被改成了**宽窗口**区间 —— 若 `PaintLane` 直接拿这个区间去 `NiceStep`,算出的主刻步长会
|
||
* 比前台大(往往正好 2 倍),于是图层里的竖向网格线(含五等分辅助线)**落在与前台不同的位置**、
|
||
* 并且“与主刻重合则跳过”的判据也错位 → 用户看到网格线**错位/部分整条丢失**、
|
||
* 且在“拖拽帧(用图层)”与“结算帧(整幅重绘)”之间来回切换时**闪烁**。
|
||
* 与纵向的 `m_wideTickSpan/m_wideTickHeight` 是同一套思路(那两处已经做对了)。
|
||
*/
|
||
double m_renderTimeStep = 0.0;
|
||
|
||
/**
|
||
* @brief 本次 `PaintLane` 只绘制「装饰」(Y 轴刻度胶囊 + 图例 + 网格分界线)。
|
||
*
|
||
* 宽缓冲区只存「基底」(底色 + 网格线 + 波形 + 派生曲线),**不含装饰**:
|
||
* 因为装饰锚定在网格左缘、随视界横向移动,若烘进基底,平移截取时会产生重影。
|
||
* 因此前台每帧用本标志重画一次装饰(复用与整幅渲染**完全相同**的绘制代码,
|
||
* 保证逐像素一致),而不是去贴回任何缓存像素。
|
||
*/
|
||
bool m_renderDecorOnly = false;
|
||
/** 上一次走"宽缓冲快路径"时的内容指纹(含视界)。当前帧指纹与之相同 → 视界已停止变化,
|
||
* 此时改走一次整幅重绘("结算"),保证最终呈现的是**像素精确**的全质量帧。 */
|
||
std::uint64_t m_wideLastKey = 0;
|
||
bool m_wideLastValid = false;
|
||
|
||
/**
|
||
* @brief 上一次为「图层预热」请求宿主重绘的时刻(`GetTickCount64()`)。
|
||
*
|
||
* 用来给预热**限速**(见 `PrefetchStepAllowed`):既保证离屏网格最终都会建好图层,
|
||
* 又不会高频请求重绘把动画的 `WM_TIMER` 饿死。
|
||
*/
|
||
mutable std::uint64_t m_lastPrefetchRequestMs = 0;
|
||
|
||
/**
|
||
* @brief 实验性功能浮动面板的像素图层项。
|
||
*
|
||
* 面板被完整绘制进 `m_panelDc`;只要 `key` 不变,下一帧就直接按
|
||
* `[x0, y0, x1, y1)` 贴图,不再重复绘制(15 个面板全画约 33ms,贴图 <1ms)。
|
||
*/
|
||
struct PanelSlot
|
||
{
|
||
int laneIndex = -1; /**< 所属网格。 */
|
||
int kind = 0; /**< `EdgePanelKind`。 */
|
||
std::uint64_t key = 0; /**< 面板自身的缓存键。 */
|
||
int x0 = 0; /**< 已缓存矩形(含阴影外扩),左。 */
|
||
int y0 = 0; /**< 上。 */
|
||
int x1 = 0; /**< 右(不含)。 */
|
||
int y1 = 0; /**< 下(不含)。 */
|
||
/**
|
||
* @brief 该面板内**全部文本的墨迹矩形**(客户区坐标)。
|
||
*
|
||
* 面板是"先画进图层缓存、再贴图"的:命中缓存时不会真的画文字,
|
||
* 于是调试用的文本框这一帧就**凭空消失** —— 观感是"拖动视界时浮动面板上
|
||
* 的布局边界不停闪烁"(用户 2026-10-29 报告)。
|
||
* 因此渲染时把文本框一并存下来,贴图时按原坐标回放,几何与是否命中缓存无关。
|
||
*/
|
||
std::vector<gd::RectF> textRects;
|
||
};
|
||
|
||
HBITMAP m_panelBitmap = nullptr; /**< 面板图层位图(原点与客户区一致)。 */
|
||
HDC m_panelDc = nullptr; /**< 面板图层设备上下文。 */
|
||
HGDIOBJ m_panelOldBitmap = nullptr; /**< 面板图层 DC 原选位图。 */
|
||
int m_panelWidth = 0; /**< 面板图层位图宽度。 */
|
||
int m_panelHeight = 0; /**< 面板图层位图高度。 */
|
||
bool m_panelValid = false; /**< 图层内容是否有效。 */
|
||
std::uint64_t m_panelGlobalKey = 0; /**< 上一层对应的全局键。 */
|
||
std::vector<PanelSlot> m_panelSlots; /**< 上一层缓存的面板(本帧比对用)。 */
|
||
|
||
/**
|
||
* @brief 浮动面板的**层序**(从下到上)。
|
||
*
|
||
* 面板允许互相重叠,因此必须有明确的层序:
|
||
* * **绘制**按此顺序从下往上;**命中测试**从后往前(最上层优先)。
|
||
* * 点击某个面板会把它移到队尾(置顶),因此重叠时总能操作到"用户想要"的那一个
|
||
* (否则上层面板的 ✕ / 标题栏会被下层控件的命中判定抢走)。
|
||
* * 新出现的面板追加到队尾(在最上层),不会被已有面板盖住。
|
||
*/
|
||
struct PanelRef
|
||
{
|
||
int laneIndex = -1; /**< 所属网格。 */
|
||
int kind = 0; /**< `EdgePanelKind` 的整型值。 */
|
||
|
||
bool operator==(const PanelRef& other) const
|
||
{
|
||
return laneIndex == other.laneIndex && kind == other.kind;
|
||
}
|
||
};
|
||
|
||
/** 同步 `m_panelOrder` 与当前"可见的面板集合"(新增的追加到最上层;消失的移除)。 */
|
||
void SyncPanelOrder() const;
|
||
/** @brief 把某个面板移到最上层(点击面板时调用)。 */
|
||
void BringPanelToFront(int laneIndex, int kind);
|
||
/**
|
||
* @brief 从最上层往下找出包含该点的面板。
|
||
* @param [IN] x 客户区横坐标。
|
||
* @param [IN] y 客户区纵坐标。
|
||
* @param [OUT] laneIndex 命中面板所属网格。
|
||
* @param [OUT] kind 命中面板的种类(`EdgePanelKind`)。
|
||
* @return 命中返回 true。
|
||
*/
|
||
bool TopPanelAtPoint(float x, float y, int& laneIndex, int& kind) const;
|
||
|
||
mutable std::vector<PanelRef> m_panelOrder; /**< 浮动面板层序(队尾 = 最上层;mutable 供 const 光标查询同步层序)。 */
|
||
|
||
/**
|
||
* @brief 查询坐标命中的图例条目。
|
||
* @param [IN] x 客户区横坐标。
|
||
* @param [IN] y 客户区纵坐标。
|
||
* @return 命中的信号引用,未命中返回无效引用。
|
||
*/
|
||
SignalRef LegendAtPoint(int x, int y) const;
|
||
};
|
||
|
||
} // namespace jdi
|
||
|
||
#endif // __PLOT_VIEW_HPP__
|