#ifndef __PLOT_VIEW_HPP__ #define __PLOT_VIEW_HPP__ #include #include #include #include #include #include #include "DataModel.hpp" #include "PaintUtil.hpp" #include "Theme.hpp" namespace jdi { /** * @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 波形绘图区:网格、时间轴、光标、缩放、自适应与信号拖拽。 * * 绘图区同时依赖数据层(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 三态循环切换时间光标:无 → 单光标 → 双光标 → 无。 * @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& 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& 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& 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& points, std::vector& breaks, std::vector& band, std::vector& bandBreaks, bool& bandMode) 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& timeTicks); /** * @brief 绘制底部时间轴。 * @param [IN,OUT] g 绘图上下文。 * @param [IN] timeTicks 时间刻度序列。 * @param [IN] timeStep 时间刻度步长。 */ void PaintTimeAxis(gd::Graphics& g, const std::vector& timeTicks, double timeStep); /** * @brief 绘制时间光标、选区及其读数。 * @param [IN,OUT] g 绘图上下文。 * @param [IN] lanes 网格绘制信息集合。 */ void PaintCursor(gd::Graphics& g, const std::vector& lanes); /** * @brief 绘制「边沿计数」的横向光标(含两端把手、左侧幅值读数)与统计浮动面板。 * @param [IN,OUT] g 绘图上下文。 * @param [IN] lanes 网格绘制信息集合。 */ void PaintEdgeCursor(gd::Graphics& g, const std::vector& lanes); /** * @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 rowChecks; std::vector rowPoints; std::vector rowIterations; std::vector rowStats; std::vector rowRefs; }; /** * @brief 计算某个实验性功能面板的布局。 * @param [IN] laneIndex 网格下标。 * @param [IN] kind 面板种类。 * @return 布局;面板未展开时各项均为空矩形。 */ EdgePanelLayout EdgePanelLayoutFor(int laneIndex, EdgePanelKind kind) const; /** * @brief 获取多阶(迭代)均值滤波曲线,结果带缓存。 * * 语义:以「均值点数 X」作滑平半径,对输入重复做 `iterations` 次滑动平均。 * 「实验性功能 → 均值滤波」与「中心轴边沿计数」共用本函数,二者曲线因此逐位一致。 * * @param [IN] signal 目标信号。 * @param [IN] points 均值点数 X(滑平半径)。 * @param [IN] iterations 迭代次数 N(至少 1)。 * @return 曲线数值数组(长度与 `signal.values` 相同;缺失处为 NaN)。 */ const std::vector& 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)。 */ void PaintDerivedCurve(gd::Graphics& g, const ResolvedSignal& resolved, const LaneVisual& lane, const std::vector& curve) 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 m_pendingMeanFilterItems; /**< 待构建的信号及其参数。 */ std::vector m_pendingWeakBoostItems; /**< 待构建的弱波增强信号及其参数。 */ int m_boostDragLane = -1; /**< 正在拖动滑块的面板所属网格。 */ bool m_mouseOverPanel = 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 m_cursorLabelRects; /**< 本次绘制已占用的取值标注矩形(用于避让重叠)。 */ std::vector 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 m_edgeCountCache; /**< 边沿统计缓存(纯缓存,逻辑上不影响对象状态)。 */ /** 多次迭代均值滤波曲线缓存项(按「信号指针 + 均值点数 + 迭代次数」标识)。 */ struct MeanCurveEntry { const SignalData* signal = nullptr; int points = 5; int iterations = 1; std::vector values; /**< 与 signal->values 等长;缺失处为 NaN。 */ }; mutable std::deque m_meanCurveCache; /** 弱波增强曲线缓存项(按「信号指针 + 增强等级百分比 + 迭代次数」标识)。 */ struct WeakBoostCurveEntry { const SignalData* signal = nullptr; int levelPercent = 50; /**< 弱波增强等级(0–100)。 */ int iterations = 1; /**< 迭代次数 N(1–10)。 */ std::vector values; /**< 与 signal->values 等长;缺失处为 NaN。 */ }; mutable std::deque m_weakBoostCurveCache; /** 中心轴边沿统计缓存项(增量统计机制与 `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 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 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 points; /**< 折线顶点。 */ std::vector breaks; /**< 每段折线在 points 中的起始下标。 */ /** * @brief 是否处于「稠密包络」模式(采样点密度超过像素分辨率)。 * * 此时 `points` 是每列 min/max 交替的锯齿,逐段描边的光栅化开销极大 * (实测每网格约 14ms);改用下面的「包络带」一次填充后约 1.7ms, * 且保留抗锯齿,静态与拖拽帧的观感完全一致。 */ bool bandMode = false; /** 包络带顶点:每段为「上包络正向 + 下包络反向」,闭合后即为该段的填充多边形。 */ std::vector band; /** 每段包络带在 `band` 中的起始下标。 */ std::vector 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 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 是否把逐信号外观样式(颜色/不透明度/线宽/模式)计入指纹。 * @return 指纹值。 */ std::uint64_t SceneContentHash(bool includeStyles = true) const; /** * @brief 场景内容中**不含逐信号外观样式**的指纹。 * * 只包含网格结构、视图几何、网格幅值、信号集合与可见性、图例文案、选中项等 * "变化后必须整幅重建"的内容;逐信号的 `color / opacity / lineWidth / mode` * 一律不参与。 * * 用途:外观样式变化时(典型场景是拖动自定义颜色的滑块)波形几何完全不变, * 只要该指纹一致,就可以只重画"颜色变了的那个网格"再写回缓存, * 而不必整幅重绘全部网格(VERY_BIG_DATA 实测整幅 30ms+、单网格约 4ms)。 * * @return 指纹值。 */ std::uint64_t SceneGeometryHash() const; /** * @brief 静态场景缓存中"网格内信号外观样式"的快照条目。 */ struct SceneStyleEntry { std::uint32_t color = 0; /**< 信号色(未叠加不透明度)。 */ float opacity = 1.0f; /**< 不透明度。 */ float lineWidth = 1.0f; /**< 线宽。 */ int mode = 0; /**< 绘制模式。 */ }; /** * @brief 采集当前各网格内可见信号的外观样式快照。 * @param [OUT] out 采集结果(按网格分组)。 */ void CollectSceneStyles(std::vector>& out) const; /** * @brief 判断当前相对缓存是否"只有外观样式发生了变化"。 * * 成立条件:`SceneGeometryHash()` 与缓存一致(网格结构、视图几何、信号集合 * 与文字都没变),且至少有一个网格内信号的外观样式不同。 * * @param [OUT] lanes 外观发生变化的网格下标(升序)。 * @return 可以走"局部重画"快路径返回 true。 */ bool FindStyleOnlyLanes(std::vector& lanes) const; /** * @brief 只重画指定网格并把结果写回静态场景缓存。 * * 必须在 `BlitSceneCache()` 之后调用:贴图已经完成,这里把受影响网格的 * "底色 + 网格线 + 波形 + 标尺 + 图例"重画到画布上,再整块拷回缓存位图, * 使缓存内容与当前样式一致(下一帧即可重新走"完全命中"快路径)。 * * @param [IN,OUT] g 绘图上下文。 * @param [IN] lanes 需要重画的网格下标。 */ void RepaintSceneCachePatch(gd::Graphics& g, const std::vector& 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& points, const std::vector& breaks); /** * @brief 稠密包络带的光晕:把带整体外扩 `extra` 像素后填充一次。 * * 比用宽画笔描整条折线快一个数量级(含大量近垂直跳变的信号上尤其明显), * 观感也更均匀。 * * @param [IN,OUT] g 绘图上下文。 * @param [IN] band 包络带顶点(上包络正向 + 下包络反向)。 * @param [IN] bandBreaks 各段起点。 * @param [IN] extra 纵向额外外扩量(像素)。 */ void PaintBandHalo(gd::Graphics& g, const std::vector& band, const std::vector& bandBreaks, float extra); /** * @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> m_sceneStyles; /** * @brief 查询坐标命中的图例条目。 * @param [IN] x 客户区横坐标。 * @param [IN] y 客户区纵坐标。 * @return 命中的信号引用,未命中返回无效引用。 */ SignalRef LegendAtPoint(int x, int y) const; }; } // namespace jdi #endif // __PLOT_VIEW_HPP__