Files
Just-DataInspector/Components/PlotView/Inc/PlotView.hpp
T
DeepSeek V4.1 Flash e988da7963 宽缓冲改为滚动式:消除平移过程中的周期性重建尖峰
视界每帧移动 d 像素时,把缓冲内容整体自贴图平移 round(d) 像素并只补画新暴露的 |round(d)| 像素宽细条(细条渲染复用整条宽窗口的映射,故网格/波形 x 坐标与刻度密度均正确)。用锚点把内容相对时间轴的亚像素偏移限制在半个像素内;缓冲只覆盖绘图区,时间轴由前台每帧绘制。实测 VERY_BIG_DATA 全信号、视界 [40.1756,40.1885]s、5 网格:持续平移拖动约 6ms/帧、长距最大 10.5ms(原为每屏一次 33ms 尖峰);弱波增强滑块 6ms、样式 8ms;静止帧与 HEAD 逐像素 0 差异。
2026-10-11 23:08:46 +08:00

2290 lines
95 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#ifndef __PLOT_VIEW_HPP__
#define __PLOT_VIEW_HPP__
#include <windows.h>
#include <objidl.h>
#include <gdiplus.h>
#include <atomic>
#include <condition_variable>
#include <deque>
#include <memory>
#include <mutex>
#include <string>
#include <thread>
#include <vector>
#include "DataModel.hpp"
#include "PaintUtil.hpp"
#include "Theme.hpp"
namespace jdi
{
/**
* @brief 后台派生曲线计算完成后由 `PlotView` 投递给宿主的自定义消息。
*
* 宿主收到后重绘绘图区即可:绘制线程会在下一帧取到刚算好的曲线(双缓冲切换),
* 同时静态场景缓存会被作废以纳入新曲线。`WM_APP + 3` 与宿主其它自定义消息错开。
*/
constexpr UINT kMessagePlotDerivedReady = WM_APP + 3;
/**
* @brief 绘图区拖拽交互模式。
*/
enum class PlotDragMode : int
{
NONE = 0, /**< 未拖拽。 */
PAN, /**< 常规模式:水平平移时间轴 + 纵向偏移当前网格幅值。 */
RUBBER_SELECT, /**< 选区放大:按拖拽方向决定横向(时间轴)或纵向(幅值轴)选区。 */
SCROLL_LANES, /**< 拖动网格滚动条。 */
CURSOR_MOVE, /**< 拖拽时间光标。 */
CURSOR_SPAN_MOVE, /**< 拖拽双光标差值标签:两条光标整体平移,间隔不变。 */
MOVE_SIGNAL, /**< 拖拽信号曲线到其它网格。 */
EDGE_MOVE, /**< 拖动「边沿计数」的横向光标(上下移动幅值)。 */
EDGE_PANEL_MOVE, /**< 拖动边沿计数浮动面板。 */
BOOST_LEVEL, /**< 拖动「弱波增强」面板中的等级滑块。 */
BOOST_ITERATIONS, /**< 拖动「弱波增强」面板中的迭代次数 N 滑块。 */
LANE_REORDER /**< 拖动网格图例中的 GX 编号徽标:上下调整网格次序。 */
};
/**
* @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。 */
};
/**
* @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_BOOST = 11 /**< 切换本网格的「实验性功能 → 弱波增强」开关。 */
};
/**
* @brief 实验性功能浮动面板的种类。
*/
enum class EdgePanelKind : int
{
COUNT = 0, /**< 边沿计数。 */
CENTER = 1, /**< 边沿计数(中心轴)。 */
MEAN = 2, /**< 均值滤波。 */
BOOST = 3 /**< 弱波增强。 */
};
/**
* @brief 派生(叠加预览)曲线的种类。
*
* 「中心轴边沿计数」与「均值滤波」共用 `MEAN_FILTER`(同一套参数与同一条曲线)。
*/
enum class DerivedCurveKind : int
{
MEAN_FILTER = 0, /**< 多次迭代均值滤波。 */
WEAK_BOOST = 1 /**< 弱波增强。 */
};
/**
* @brief 波形绘图区:网格、时间轴、光标、缩放、自适应与信号拖拽。
*
* 绘图区同时依赖数据层(DataSet,只读)与当前标签页配置(TabConfig,可写),
* 视图范围、网格滚动与时间光标均保存在标签页配置中,因此各标签页互不干扰。
*/
class PlotView
{
public:
PlotView();
/**
* @brief 析构:释放拖拽静态场景缓存的离屏位图。
*/
~PlotView();
/**
* @brief 设置宿主窗口句柄(用于剪贴板所有者)。
* @param [IN] host 宿主窗口句柄,允许为空。
*/
void SetHostWindow(HWND host) { m_host = host; }
/**
* @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);
/**
* @brief 按给定区域重新排布绘图区。
* @param [IN] area 可用区域。
*/
void Layout(const gd::RectF& area);
/**
* @brief 绘制绘图区。
* @param [IN,OUT] g 绘图上下文。
*/
void Paint(gd::Graphics& g);
/**
* @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 处理鼠标中键按下:与左键在常规模式下一致,进入自由平移(水平平移时间轴 +
* 纵向偏移当前网格幅值)。**任何缩放模式下都可用**,用于在不改变当前模式的
* 前提下自由查看区域。
* @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 处理鼠标离开绘图区。
* @return 需要重绘返回 true。
*/
bool OnMouseLeave();
/**
* @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 把时间吸附到距今最近的数据采样点。
*
* 仅在横向放大到采样点间距足够大(可精确分辨)时生效,
* 避免缩小时光标出现“抖动”难以微调。
*
* @param [IN] time 原始时间。
* @param [OUT] snapped 吸附后的时间。
* @return 成功吸附返回 true。
*/
bool SnapTimeToSample(double time, double& snapped) const;
/**
* @brief 计算指定文件中某一时刻附近采样点的时间间隔。
* @param [IN] file 目标文件。
* @param [IN] time 目标时刻。
* @param [OUT] pixels 间隔换算到屏幕上的像素宽度。
* @return 计算成功返回 true。
*/
bool SampleSpacingPixels(const DataFile& file, double time, double& pixels) const;
/**
* @brief 判断坐标是否落在绘图区范围内。
* @param [IN] x 客户区横坐标。
* @param [IN] y 客户区纵坐标。
* @return 落在范围内返回 true。
*/
bool HitTest(int x, int y) const;
/**
* @brief 查询当前鼠标位置应使用的系统光标。
* @param [IN] x 客户区横坐标。
* @param [IN] y 客户区纵坐标。
* @return 系统光标句柄,未命中特殊区域返回 nullptr。
*/
HCURSOR QueryCursor(int x, int y) const;
/**
* @brief 时间轴与幅值轴同时自适应到可见信号范围。
*/
void FitAll();
/**
* @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 清除已启用的时间光标。
*/
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 暂停后台派生曲线计算并作废其结果。
*
* 后台线程会读取采样数据,因此**任何会改变数据层结构(增删文件/信号、插入派生信号,
* 进而移动 `DataFile`/`SignalData` 存储)的操作之前**都必须先调用本函数:它会等待正在
* 进行的计算结束、清空待算队列与缓存,从而杜绝后台线程读取已释放内存。
* 之后的绘制会按需重新启动后台线程(惰性)。
*/
void PauseDerivedAsync();
/**
* @brief 三态循环切换时间光标:无 → 单光标 → 双光标 → 无。
* @return 状态发生变化返回 true。
*/
bool CycleCursorMode();
/**
* @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 fileIndex = -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 WeakBoostBuild
{
int fileIndex = -1; /**< 信号所属文件下标。 */
int signalIndex = -1; /**< 文件内信号下标。 */
int levelPercent = 50; /**< 弱波增强等级(0–100)。 */
int iterations = 1; /**< 迭代次数 N(1–10)。 */
};
/**
* @brief 取出「弱波增强 → 构建为新信号」请求(由面板底部按钮触发)。
* @param [OUT] laneIndex 触发该请求的网格下标。
* @param [OUT] items 需要构建的信号及其参数(按信号在网格内的顺序)。
* @return 存在待处理请求返回 true(取出后清空)。
*/
bool TakePendingWeakBoostBuild(int& laneIndex, std::vector<WeakBoostBuild>& items);
/**
* @brief 切换某网格的「弱波增强」开关。
* @param [IN] laneIndex 网格下标。
* @param [IN] enabled 是否开启。
* @return 状态发生变化返回 true。
*/
bool SetLaneWeakBoost(int laneIndex, bool enabled);
/**
* @brief 查询某网格的「弱波增强」开关。
* @param [IN] laneIndex 网格下标。
* @return 已开启返回 true。
*/
bool LaneWeakBoost(int laneIndex) const;
/**
* @brief 切换某网格的「弱波增强」开关。
* @param [IN] laneIndex 网格下标。
* @return 切换后的状态。
*/
bool ToggleLaneWeakBoost(int laneIndex);
/**
* @brief 读取某网格的弱波增强等级(0–1)。
* @param [IN] laneIndex 网格下标。
* @return 增强等级;网格不存在时返回 0.5。
*/
double LaneWeakBoostLevel(int laneIndex) const;
/**
* @brief 读取某网格的弱波增强迭代次数 N(1–10)。
* @param [IN] laneIndex 网格下标。
* @return 迭代次数;网格不存在时返回 1。
*/
int LaneWeakBoostIterations(int laneIndex) const;
/**
* @brief 设置某网格的弱波增强迭代次数 N(1–10,超出范围会被夹取)。
* @param [IN] laneIndex 网格下标。
* @param [IN] iterations 迭代次数。
* @return 迭代次数发生变化返回 true。
*/
bool SetLaneWeakBoostIterations(int laneIndex, int iterations);
/**
* @brief 设置某网格的弱波增强等级(0–1,超出范围会被夹取)。
* @param [IN] laneIndex 网格下标。
* @param [IN] level 增强等级。
* @return 等级发生变化返回 true。
*/
bool SetLaneWeakBoostLevel(int laneIndex, double level);
/**
* @brief 获取弱波增强曲线(按「信号 + 增强等级 + 迭代次数」缓存)。
* @param [IN] signal 目标信号。
* @param [IN] levelPercent 弱波增强等级(0–100,整数百分比)。
* @param [IN] iterations 迭代次数 N(1–10)。
* @return 曲线数值数组(长度与 `signal.values` 相同;缺失处为 NaN)。
*/
const std::vector<float>& WeakBoostCurveFor(const SignalData& signal, int levelPercent,
int iterations) const;
/**
* @brief 取某个时刻的弱波增强曲线取值(用于纵向光标的取值标注)。
* @param [IN] file 信号所属文件。
* @param [IN] signal 目标信号。
* @param [IN] levelPercent 弱波增强等级(0–100)。
* @param [IN] iterations 迭代次数 N(1–10)。
* @param [IN] time 采样时刻。
* @param [OUT] value 输出取值。
* @return 取到有效值返回 true。
*/
bool WeakBoostValueAt(const DataFile& file, const SignalData& signal, int levelPercent, int iterations,
double time, double& value) const;
/**
* @brief 切换某个信号在「弱波增强」面板中的预览勾选状态。
* @param [IN] laneIndex 网格下标。
* @param [IN] ref 目标信号。
* @return 切换后的勾选状态。
*/
bool ToggleLaneWeakBoostSignal(int laneIndex, const SignalRef& ref);
/**
* @brief 查询某个信号在「弱波增强」面板中的预览勾选状态。
* @param [IN] laneIndex 网格下标。
* @param [IN] ref 目标信号。
* @return 已勾选返回 true。
*/
bool LaneWeakBoostSignalEnabled(int laneIndex, const SignalRef& ref) const;
/**
* @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 { return m_editTarget != PlotEditTarget::NONE; }
/**
* @brief 取出并清除待应用的信号重命名请求。
* @param [OUT] ref 被重命名的信号引用。
* @param [OUT] name 新的显示名称。
* @return 存在待处理请求返回 true。
* @note 绘图区只持有只读数据层,重命名需由宿主窗口落到 DataSet 上。
*/
bool TakePendingRename(SignalRef& ref, std::wstring& name);
/**
* @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();
private:
/**
* @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 DataFile* file = 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 计算当前视图应使用的时间范围。
* @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 LaneSpansMultipleFiles(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 计算双光标之间的时间差(绝对值)。
* @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 绘制单个网格。
* @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 绘制底部时间轴。
* @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; /**< ✕ 关闭按钮。 */
gd::RectF edit; /**< 面板底部编辑行(无编辑内容时宽度为 0)。 */
gd::RectF button; /**< 面板底部按钮(无按钮时宽度为 0)。 */
gd::RectF slider; /**< 「弱波增强」等级滑块的轨道矩形(无滑块时宽度为 0)。 */
gd::RectF sliderIterations; /**< 「弱波增强」迭代次数 N 滑块的轨道矩形(无滑块时宽度为 0)。 */
int rows = 0; /**< 统计行数(每条可见信号一行)。 */
/**
* @brief 「逐条目」面板的矩形(MEAN 与 CENTER 面板共用同一套排布)。
*
* 索引与 `rowRefs` 一一对应(即网格内可见信号的顺序):
* `rowChecks` 为第一排右侧的复选框;`rowPoints` / `rowIterations` 为第二排的两个输入框;
* `rowStats` 为第三排的统计结果(中心轴边沿计数专用)。
* 未勾选的条目对应的矩形均为空。
*/
std::vector<gd::RectF> rowChecks;
std::vector<gd::RectF> rowPoints;
std::vector<gd::RectF> rowIterations;
std::vector<gd::RectF> rowStats;
std::vector<SignalRef> rowRefs;
};
/**
* @brief 计算某个实验性功能面板的布局。
* @param [IN] laneIndex 网格下标。
* @param [IN] kind 面板种类。
* @return 布局;面板未展开时各项均为空矩形。
*/
EdgePanelLayout EdgePanelLayoutFor(int laneIndex, EdgePanelKind kind) 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] file 信号所属文件。
* @param [IN] signal 目标信号。
* @param [IN] points 均值点数 X。
* @param [IN] iterations 迭代次数 N。
* @param [IN] time 采样时刻。
* @param [OUT] value 输出取值。
* @return 取到有效值返回 true。
*/
bool MeanFilterValueAt(const DataFile& file, 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」确定,与「实验性功能 → 均值滤波」的预览曲线逐位一致。
*
* @param [IN] file 信号所属文件。
* @param [IN] signal 目标信号。
* @param [IN] points 均值点数 X。
* @param [IN] iterations 迭代次数 N。
* @param [IN] hasRange 是否只统计某个时间区间(双光标模式下为光标区间)。
* @param [IN] rangeMin 区间左端点时间。
* @param [IN] rangeMax 区间右端点时间。
* @param [OUT] rising 上升沿数量(信号从下往上越过均值滤波曲线)。
* @param [OUT] falling 下降沿数量(信号从上往下穿过均值滤波曲线)。
*/
void EdgeCenterCountsFor(const DataFile& file, const SignalData& signal, int points, int iterations,
bool hasRange, double rangeMin, double rangeMax, std::size_t& rising,
std::size_t& falling) 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]` 对应的采样下标。
*/
void PaintDerivedCurve(gd::Graphics& g, const ResolvedSignal& resolved, const LaneVisual& lane,
const std::vector<float>& curve, std::size_t curveBegin) 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] levelPercent 弱波增强等级(仅 `WEAK_BOOST` 使用,0–100)。
* @param [IN] begin 需要的采样区间起点(含)。
* @param [IN] end 需要的采样区间终点(不含)。
* @param [OUT] spanBegin 返回数组对应的首个采样下标。
* @param [IN] allowAsyncCompute 是否允许「后台异步计算」:窗口很大时**不在绘制线程里计算**,
* 而是交后台线程算,本帧先用已算好的旧曲线顶替(双缓冲),保证交互始终跟手。
* 仅供**预览绘制**传 true;需要精确取值的统计(中心轴边沿计数、取值胶囊)
* 传 false,走同步精确计算。
* @return 曲线数组指针(覆盖 `[spanBegin, spanBegin + size())`);暂无可显示曲线时返回 nullptr。
*/
const std::vector<float>* CurveSpanFor(DerivedCurveKind kind, const SignalData& signal,
int points, int iterations, int levelPercent,
std::size_t begin, std::size_t end,
std::size_t& spanBegin,
bool allowAsyncCompute = false) const;
/**
* @brief 派生曲线缓存的键(决定一条派生曲线数值的全部输入)。
*
* 数据层在两次结构变化之间是只读的,因此 `signal` 指针在此期间稳定;
* 任何结构变化(增删文件/信号、插入派生信号、切换标签页)前都会暂停后台线程并清空缓存
*(见 `PauseDerivedAsync`),因此不会出现悬空读取。
*/
struct CurveKey
{
const SignalData* signal = nullptr; /**< 目标信号。 */
int kind = 0; /**< `DerivedCurveKind`。 */
int points = 5; /**< 均值点数 X。 */
int iterations = 1; /**< 迭代次数 N。 */
int levelPercent = 0; /**< 弱波增强等级(0–100)。 */
bool operator==(const CurveKey& other) const
{
return signal == other.signal && kind == other.kind && points == other.points &&
iterations == other.iterations && levelPercent == other.levelPercent;
}
};
/**
* @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; /**< 入队时的数据代次(代次变化即丢弃结果)。 */
};
/**
* @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 曲线键。
* @param [OUT] spanBegin 命中条目的窗口起点。
* @return 命中条目,无则返回 nullptr。
*/
const CurveEntry* AcquireNewestCurve(const CurveKey& key, 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 清理已被更新条目覆盖或超出内存预算的缓存条目(调用方须持有 `m_curveMutex`)。 */
void PruneCurveEntriesLocked() const;
/**
* @brief 计算当前视图对应的采样下标区间(时间单调时二分定位,否则为整段)。
* @param [IN] file 目标文件。
* @param [IN] count 可用采样点数。
* @param [OUT] begin 区间起点(含)。
* @param [OUT] end 区间终点(不含)。
*/
void VisibleIndexRange(const DataFile& file, std::size_t count, std::size_t& begin,
std::size_t& end) const;
/**
* @brief 绘制某条信号的多次迭代均值滤波曲线(与中心平均值曲线同一配色派生规则)。
* @param [IN,OUT] g 绘图上下文。
* @param [IN] resolved 信号视图。
* @param [IN] lane 网格绘制信息。
* @param [IN] points 均值点数 X(滑平半径)。
* @param [IN] iterations 迭代次数 N(至少 1)。
*/
void PaintMeanFilterCurve(gd::Graphics& g, const ResolvedSignal& resolved, const LaneVisual& lane,
int points, int iterations) const;
/**
* @brief 绘制某条信号的弱波增强曲线(与均值滤波曲线同一配色派生规则)。
* @param [IN,OUT] g 绘图上下文。
* @param [IN] resolved 信号视图。
* @param [IN] lane 网格绘制信息。
* @param [IN] levelPercent 弱波增强等级(0–100)。
* @param [IN] iterations 迭代次数 N(1–10)。
*/
void PaintWeakBoostCurve(gd::Graphics& g, const ResolvedSignal& resolved, const LaneVisual& lane,
int levelPercent, int iterations) 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 统计某信号穿越横向光标幅值的上/下边沿数量,结果带缓存。
* @param [IN] file 信号所属文件。
* @param [IN] signal 目标信号。
* @param [IN] edgeValue 横向光标所在的幅值。
* @param [IN] hasRange 是否只统计某个时间区间(双光标模式下为光标区间)。
* @param [IN] rangeMin 区间左端点时间。
* @param [IN] rangeMax 区间右端点时间。
* @param [OUT] rising 上升沿数量(从下往上穿越)。
* @param [OUT] falling 下降沿数量(从上往下穿越)。
*/
void EdgeCountsFor(const DataFile& file, const SignalData& signal, double edgeValue,
bool hasRange, double rangeMin, double rangeMax, std::size_t& rising,
std::size_t& falling) 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 绘制网格标题(信号图例)。
* @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; /**< 时间轴高度(逻辑像素)。 */
float m_minLaneHeight = 54.0f; /**< 网格最小高度(逻辑像素)。 */
float m_laneContentHeight = 0.0f; /**< 网格内容总高度。 */
bool m_scrollBarVisible = false; /**< 是否需要显示滚动条。 */
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; /**< 鼠标悬停命中的信号(曲线高亮)。 */
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_pendingWeakBoostLane = -1; /**< 待构建的弱波增强信号所属网格下标(-1 表示无请求)。 */
std::vector<MeanFilterBuild> m_pendingMeanFilterItems; /**< 待构建的信号及其参数。 */
std::vector<WeakBoostBuild> m_pendingWeakBoostItems; /**< 待构建的弱波增强信号及其参数。 */
int m_boostDragLane = -1; /**< 正在拖动滑块的面板所属网格。 */
bool m_mouseOverPanel = false; /**< 上一帧鼠标是否落在某个实验性功能面板上。 */
/**
* @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; /**< 当前悬停网格下标。 */
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 边沿统计缓存项。
*
* 计数只由「采样下标区间 [begin, end) 与判定基准(横向光标幅值)」决定,
* 因此**时间区间不进缓存键**:拖动时间光标时区间每帧都在变,若按时间区间做键就会
* 每帧全量重扫整段区间(亿级采样下正是拖拽卡顿的主因,实测单帧数十毫秒)。
* 改为记住「上一帧统计过的下标区间」:新区间与旧区间共享一个端点时(拖动时必然如此)
* 只扫描两端差集并增量加减,单帧代价与光标位移成正比、与区间长度无关。
*/
struct EdgeCountEntry
{
const DataFile* file = nullptr; /**< 所属数据分组。 */
const SignalData* signal = nullptr; /**< 目标信号。 */
double edgeValue = 0.0; /**< 判定阈值(横向光标幅值)。 */
bool hasRange = false; /**< 是否限定在时间区间内统计。 */
bool reusable = false; /**< 计数是否完全由 [begin, end) 决定。 */
std::size_t begin = 0; /**< 已统计的采样区间起点(含)。 */
std::size_t end = 0; /**< 已统计的采样区间终点(不含)。 */
std::size_t rising = 0; /**< 该区间内的上升沿数量。 */
std::size_t falling = 0; /**< 该区间内的下降沿数量。 */
};
mutable std::vector<EdgeCountEntry> m_edgeCountCache; /**< 边沿统计缓存(纯缓存,逻辑上不影响对象状态)。 */
/** 多次迭代均值滤波曲线缓存项(按「信号指针 + 均值点数 + 迭代次数」标识)。 */
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 WeakBoostCurveEntry
{
const SignalData* signal = nullptr;
int levelPercent = 50; /**< 弱波增强等级(0–100)。 */
int iterations = 1; /**< 迭代次数 N(1–10)。 */
std::vector<float> values; /**< 与 signal->values 等长;缺失处为 NaN。 */
};
mutable std::deque<WeakBoostCurveEntry> m_weakBoostCurveCache;
// ---- 异步派生曲线计算(后台线程 + 双缓冲)----
// 设计:预览曲线(均值滤波 / 弱波增强)在窗口很大时**不在绘制线程里计算**,而是交给后台
// 线程;绘制线程本帧先用上一条已算好的曲线顶替(双缓冲),后台算完后通过
// `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}; /**< 后台发布了新条目 → 需重建静态场景缓存。 */
/** 中心轴边沿统计缓存项(增量统计机制与 `EdgeCountEntry` 完全一致)。 */
struct EdgeCenterCountEntry
{
const DataFile* file = nullptr; /**< 所属数据分组。 */
const SignalData* signal = nullptr; /**< 目标信号。 */
int points = 5; /**< 均值点数 X。 */
int iterations = 1; /**< 迭代次数 N。 */
bool hasRange = false; /**< 是否限定在时间区间内统计。 */
bool reusable = false; /**< 计数是否完全由 [begin, end) 决定。 */
std::size_t begin = 0; /**< 已统计的采样区间起点(含)。 */
std::size_t end = 0; /**< 已统计的采样区间终点(不含)。 */
std::size_t rising = 0; /**< 该区间内的上升沿数量。 */
std::size_t falling = 0; /**< 该区间内的下降沿数量。 */
};
mutable std::vector<EdgeCenterCountEntry> m_edgeCenterCountCache; /**< 中心轴边沿统计缓存。 */
// 「边沿计数」横向光标 / 面板的拖拽状态。
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; /**< 条目矩形(已含色块与文本)。 */
};
std::vector<LegendHit> m_legendHits; /**< 最近一次绘制时记录的图例条目矩形。 */
/**
* @brief 折线顶点缓存条目。
*
* 折线抽稀是 O(采样点数) 的(百万级采样约 2~3ms/条),
* 而绘制、悬停命中检测每帧都要用到同一批顶点。
* 用“构建折线时读取的全部输入”作为键缓存顶点,
* 视图/网格几何未变化时可直接复用,避免重复扫描全量采样。
*/
struct PolylineCacheEntry
{
SignalRef ref; /**< 信号引用。 */
const DataFile* file = nullptr; /**< 所属文件(内容不可变)。 */
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; /**< 网格幅值上界。 */
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 静态场景缓存的几何/内容键。
*
* 只要这组参数完全一致,"背景 + 全部网格 + 时间轴"的渲染结果就逐像素相同,
* 可以直接复用离屏位图,无需重算百万级采样的包络与绘制。
* 这使鼠标悬停、拖拽选区等"只改一小块叠加层"的交互不再重绘波形:
* 大文件下整幅重绘约 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 haloFile = -1; /**< 已绘入缓存的拖拽光晕信号(无则 -1)。 */
int haloSignal = -1;
std::uint64_t contentHash = 0; /**< 信号集合/显示样式/网格幅值等内容指纹。 */
};
/**
* @brief 构造当前场景的缓存键。
* @return 缓存键。
*/
SceneCacheKey MakeSceneCacheKey() const;
/**
* @brief 判断当前缓存键是否与已捕获的场景一致。
* @return 可直接贴图返回 true。
*/
bool SceneCacheMatches() const;
/**
* @brief 计算场景内容指纹(信号集合、显示样式、网格幅值、图例文案)。
*
* 视图几何以外的任何内容变化(改颜色/线宽/透明度/样式、重命名、增删信号)
* 都会改变该指纹,从而让静态场景缓存自动失效,无需在各处手工调用失效接口。
*
* @param [IN] includeStyles 是否把逐信号外观样式(颜色/不透明度/线宽/模式)计入指纹。
* @param [IN] includeDerived 是否把派生预览曲线的开关与参数计入指纹。
* @return 指纹值。
*/
std::uint64_t SceneContentHash(bool includeStyles = true, bool includeDerived = true) const;
/**
* @brief 场景内容中**不含逐信号外观样式**的指纹。
*
* 只包含网格结构、视图几何、网格幅值、信号集合与可见性、图例文案、选中项等
* "变化后必须整幅重建"的内容;逐信号的 `color / opacity / lineWidth / mode`
* 一律不参与。
*
* 用途:外观样式变化时(典型场景是拖动自定义颜色的滑块)波形几何完全不变,
* 只要该指纹一致,就可以只重画"颜色变了的那个网格"再写回缓存,
* 而不必整幅重绘全部网格(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; /**< 绘制模式。 */
/** @brief 相等比较(用于判断"逐信号外观样式是否变化")。 */
bool operator==(const SceneStyleEntry& other) const
{
return color == other.color && opacity == other.opacity &&
lineWidth == other.lineWidth && mode == other.mode;
}
};
/**
* @brief 单个网格内「叠加预览曲线」的状态(开关 + 每条被勾选信号的参数)。
*
* 它**只影响网格内容的绘制**(不影响网格结构/视图几何),因此不进入
* `SceneGeometryHash`,而是与外观样式一起作为“局部重画”的判据:
* 拖动弱波增强的等级/迭代次数滑块时,只需重画受影响的网格,
* 不必整幅重绘(也不会把静态场景缓存整体作废)。
*/
struct SceneDerivedEntry
{
int edgeCenter = 0; /**< 中心轴边沿计数开关。 */
int meanFilter = 0; /**< 均值滤波开关。 */
int weakBoost = 0; /**< 弱波增强开关。 */
int weakBoostLevelPercent = 0; /**< 弱波增强等级(0–100)。 */
int weakBoostIterations = 1; /**< 弱波增强迭代次数 N。 */
/** 各条目展开为 [文件, 信号, 均值点数 X, 迭代次数 N],按原顺序(稳定)。 */
std::vector<int> edgeCenterItems;
std::vector<int> meanFilterItems;
std::vector<int> weakBoostItems;
};
/**
* @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] cached 该信号的折线缓存项(包络带模式下会走带外扩填充)。
*/
void PaintSignalHaloFor(gd::Graphics& g, const ResolvedSignal& resolved,
const PolylineCacheEntry& cached);
/**
* @brief 绘制某条信号的悬停/拖拽光晕(折线描边版本)。
* @param [IN,OUT] g 绘图上下文。
* @param [IN] resolved 已解析的信号。
* @param [IN] points 折线顶点。
* @param [IN] breaks 每段折线的起始下标。
*/
void PaintSignalHalo(gd::Graphics& g, const ResolvedSignal& resolved,
const std::vector<gd::PointF>& points, const std::vector<std::size_t>& breaks);
/**
* @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 轴刻度胶囊 + 图例)----
// 平移会让视图每帧变化 → 静态场景缓存必然失效 → 每帧都要重画波形**以及**网格线、
// Y 轴刻度文字、图例(实测后者占整幅重绘的一半以上)。但这些装饰内容**只取决于网格几何与
// 信号外观,与时间轴视图无关**,平移时不变化。因此在每张「全质量帧」后把装饰区域的像素
// 捕获下来,拖拽(快速帧)时直接贴回,省去每帧重绘文字与圆角底板的开销。
// 仅在完全相同(`m_decorKey` 命中)时启用,避免几何/外观变化时用到过期像素。
/**
* @brief 确保装饰缓存位图尺寸正确(不足则重建)。
* @param [IN] width 位图宽度。 @param [IN] height 位图高度。
* @return 可用返回 true。
*/
bool EnsureDecorCache(int width, int height);
/** @brief 释放装饰缓存位图。 */
void ReleaseDecorCache();
/**
* @brief 把本帧记录下来的装饰矩形从后备缓冲区捕获进装饰缓存。
* @param [IN,OUT] g 绘图上下文。 @return 捕获成功返回 true。
*/
bool CaptureDecorCache(gd::Graphics& g);
/**
* @brief 把装饰缓存里的装饰贴回后备缓冲区(快速帧用)。
* @param [IN,OUT] g 绘图上下文。 @return 贴图成功返回 true。
*/
bool BlitDecorCache(gd::Graphics& g);
mutable HDC m_decorDc = nullptr; /**< 装饰缓存设备上下文。 */
mutable HBITMAP m_decorBitmap = nullptr; /**< 装饰缓存位图。 */
mutable HGDIOBJ m_decorOldBitmap = nullptr; /**< 装饰 DC 原选位图。 */
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 轴胶囊与图例带)。 */
mutable std::vector<gd::RectF> m_decorRects;
// ---- 平移「宽缓冲区」:平移只改变绘制坐标、不改变画面内容 ----
// 视界平移时整个波形并不改变,只是横向平移。因此以当前视界为中心,向左右各多渲染
// 半个视界宽(位图宽度 = 视界宽 ×2),保存为一张**更宽**的离屏位图;平移时前台只需
// 从中**截取对应的横向子矩形贴图**(O(1),一次 BitBlt),而不必每帧重算重画。
// 当视界移动到该缓冲区边缘(超出已渲染范围)时,才以新视界为中心重建一次。
/**
* @brief 确保宽缓冲区位图尺寸正确(不足则重建)。
* @param [IN] width 位图宽度。 @param [IN] height 位图高度。
* @return 可用返回 true。
*/
bool EnsureWideBuffer(int width, int height);
/** @brief 释放宽缓冲区位图。 */
void ReleaseWideBuffer();
/**
* @brief 以当前视界为中心重建宽缓冲区(离屏渲染一张更宽的图像)。
*
* 渲染时临时把绘制区域的横向映射"加宽",因此调用的是与正常绘制**完全相同**的
* `PaintLane`/`PaintTimeAxis`,保证像素与正常绘制一致;仅**跳过图例与 Y 轴刻度**
*(它们锚定在网格左缘、随视界移动,需由前景叠加层绘制)。
*
* @return 重建成功返回 true。
*/
bool RebuildWideBuffer();
/**
* @brief 若当前视界落在宽缓冲区内,则截取子矩形贴图到后备缓冲区。
* @param [IN,OUT] g 绘图上下文。
* @return 命中并贴图返回 true(调用方随后只画叠加层即可)。
*/
bool BlitWideBuffer(gd::Graphics& g) const;
/**
* @brief 让宽缓冲区"滚动"以跟随视界:按整数像素平移已渲染内容,只补画新暴露的细条。
*
* 视界每帧移动 d 像素时,把缓冲区内容整体挪动 `round(d)` 像素(自贴图),
* 再只渲染新暴露出来的 `|round(d)|` 像素宽细条。这样**每帧代价恒为 O(细条)**,
* 不会像"移出边界就整幅重建 2×视界"那样出现周期性大尖峰。
* 用一个亚像素残差累加器把"内容相对时间轴的偏移"始终限制在半个像素内。
*
* @return 缓冲区仍可用返回 true。
*/
bool RollWideBuffer();
/** @brief 渲染宽缓冲区中因滚动而新暴露的一条细条(宽 |shiftPixels|)。 */
void RenderWideStrip(int shiftPixels);
/** @brief 宽缓冲区内容是否仍与当前视图/内容一致。 */
bool WideBufferMatches() const;
/** @brief 使宽缓冲区失效。 */
void InvalidateWideBuffer();
HDC m_wideDc = nullptr; /**< 宽缓冲区设备上下文。 */
HBITMAP m_wideBitmap = nullptr; /**< 宽缓冲区位图(比视界更宽)。 */
HGDIOBJ m_wideOldBitmap = nullptr; /**< 宽缓冲 DC 原选位图。 */
int m_wideW = 0; /**< 宽缓冲区位图宽度(像素)。 */
int m_wideH = 0; /**< 宽缓冲区位图高度(像素)。 */
double m_wideMin = 0.0; /**< 宽缓冲区最左侧对应的时间。 */
double m_wideAnchorMin = 0.0; /**< 视界左端当前在宽缓冲中的锚点时间(滚动基准)。 */
double m_wideSpan = 0.0; /**< 宽缓冲区制作时的视界跨度。 */
bool m_wideValid = false; /**< 宽缓冲区内容是否有效。 */
std::uint64_t m_wideKey = 0; /**< 宽缓冲区对应的内容指纹(外观/派生参数等)。 */
bool m_renderWide = false; /**< 正在渲染宽缓冲区(重入 `PaintLane` 时跳过装饰)。 */
/**
* @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; /**< 下(不含)。 */
};
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 查询坐标命中的图例条目。
* @param [IN] x 客户区横坐标。
* @param [IN] y 客户区纵坐标。
* @return 命中的信号引用,未命中返回无效引用。
*/
SignalRef LegendAtPoint(int x, int y) const;
};
} // namespace jdi
#endif // __PLOT_VIEW_HPP__