feat(ui): 修复光标放大后丢失、选区放大合并与右键放弃、双光标与数据点取值标注

1) 打开「选区 Y 轴自适应」时立即自适应一次
- 之前打开开关(单个网格的菜单项或全局项/工具栏按钮)不会真的改变量程,
  只有当用户再次做时间轴选区放大时才生效,观感上就是"点了没反应"
- ToggleLaneAutoFitToSelection / SetAllLanesAutoFitToSelection(enable, applyNow=true)
  在打开时按当前视图立刻执行一次 AutoRangeLaneToView
- 新增 ApplyAutoFitToSelectionNow / ApplyAllAutoFitToSelectionNow

2) 「按网格时间轴展开」同时对该网格做一次 Y 轴自适应
- FitTimeToLane 在把时间轴展开到本网格信号范围后调用 AutoRangeLaneToView,
  避免出现"时间对了但幅值区间不合适"的波形

3) 修复时间光标在大幅放大后丢失、无法拖动的问题
- 根因:缩放/平移改变 ViewMin/ViewMax 后,光标时间可能落到新视野之外,
  PaintCursor 直接 return → 光标消失,用户报告为"放大到 10ms 一格后拖动光标会直接丢失"
- 新增 ClampCursorToView(),在 ApplyTimeSelection / FitAll / FitTimeToLane /
  平移(PAN) 后把两条光标夹回可见区间;CURSOR_MOVE 也把目标时间夹在 [ViewMin, ViewMax]
- IsOnCursorLine 改为输出命中的光标序号,为双光标拖动做准备

4) 工具栏「时间轴选区」+「Y 轴选区」合并为「选区放大」,并支持右键放弃选区
- 单一 toggle(ToolbarAction::ZOOM_SELECT / PlotZoomMode::SELECT);
  按下后按首次明显位移方向决定轴向:横向=时间轴选区,纵向=该网格 Y 轴选区,
  轴向固定不变,轴向未定时不画选区(避免闪烁)
- 框选过程中按右键 = 放弃当前选区并显示提示浮层;与 Esc 不同,**不退出**选区放大模式
- 为此 WM_RBUTTONUP 在左键仍按下时不 ReleaseCapture / 不清 m_activeTarget,
  否则松左键收不到 OnLButtonUp,m_dragMode 会永远停在 RUBBER_SELECT
- 新增 PlotView 内的提示浮层(m_hintText + PaintHint):切换模式/放弃选区时显示一行说明,
  在网格内再次移动鼠标自动清除,无需定时器
- Esc 语义保持原样:放弃拖拽 + 退出选区放大模式

5) 光标功能改为三态:常态 → 单光标 → 双光标 → 常态
- TabConfig 用 PlotCursorMode{NONE,SINGLE,DOUBLE} + CursorTime/CursorTimeAlt 两个时间;
  CursorEnabled() 变成只读派生;ToggleCursor 删除,改为 CycleCursorMode()
- 单 → 双时第二条光标落在第一条附近(+12% 视图跨度,越界反向)并保证在可见范围内
- 两条光标颜色区分且同族:光标 1 = Solarized 橙 #CB4B16(theme.cursor),
  光标 2 = 青 #2AA198(新增 theme.cursorAlt),光标 2 的顶部把手画成朝上的三角
- 时间读数从绘图区顶部移到**底部**,单/双光标都始终显示(不必悬停才可见)

6) 取消原有的光标数值读数框,改为直接在数据点位置标注取值
- 删除 PaintLaneReadout/PaintLaneReadouts(网格右上角的读数框)
- PaintCursor 在光标与每条可见信号曲线的交点处绘制信号色圆角胶囊(底色=信号色,
  文字按 IsDarkColor 自动取深/浅色),一条信号一个标注,颜色与信号一一对应;
  右侧空间不足时自动标到左侧
- 双光标时以时间较小者为左;**右侧光标**显示"当前值 , ±增量"(增量 = 右 − 左),
  增量为负时用负号前缀,避免出现 "±-" 双符号

验证([1].CSV 11 信号 / VERY_BIG_DATA 1.77GB,窗口 1725x996,125% DPI):
- 三态循环:点 1 次出现单光标、2 次双光标、3 次回到无光标
- 双光标读数:左"426.316 / 427.749",右"538.583 , ±112.267 / 513.726 , ±85.9769"
- 时间读数在底部显示(橙 0.63 / 青 0.7812);第二条光标出现在第一条右侧同一视野内
- 超大文件:放大两次后开启光标仍然可见,拖动从 68.2343 变为 68.1911(原先直接消失)
- 「Y 轴自适应」在放大后的视图中点击立即把量程收到可见区间(300~520)
- 「选区 Y 轴自适应」菜单项点击后立即生效并被勾选,全局项同步勾选
- 「按网格时间轴展开」把时间轴展开回全范围并重新自适应 Y 轴
- 选区放大:横向拖拽只放大时间轴、纵向拖拽只放大 Y 轴;右键放弃后视图不变、提示出现、
  模式仍保持选中;Esc 则同时放弃并退出模式
- 回归:空闲 CPU 0.00%;make clean; make all 无警告无错误
This commit is contained in:
DeepSeek V4.1 Flash committed 2026-10-08 16:58:55 +08:00
1 parent 0fffb3ed09
commit 388d3b6441
8 files changed
+656 -362

No files matched your search

+87 -39
View File
@@ -23,13 +23,22 @@ namespace jdi
{
NONE = 0, /**< 未拖拽。 */
PAN, /**< 常规模式:水平平移时间轴 + 纵向偏移当前网格幅值。 */
RUBBER_TIME, /**< 时间轴缩放选区(竖向贯穿全高)。 */
RUBBER_VALUE, /**< Y 轴缩放选区(限定在某个网格内)。 */
RUBBER_SELECT, /**< 选区放大:按拖拽方向决定横向(时间轴)或纵向(幅值轴)选区。 */
SCROLL_LANES, /**< 拖动网格滚动条。 */
CURSOR_MOVE, /**< 拖拽时间光标。 */
MOVE_SIGNAL /**< 拖拽信号曲线到其它网格。 */
};
/**
* @brief 选区放大拖拽的轴向(由首次明显位移的方向决定)。
*/
enum class PlotRubberAxis : int
{
UNDECIDED = 0, /**< 尚未确定(拖拽刚刚开始)。 */
TIME = 1, /**< 横向拖拽:放大时间轴。 */
VALUE = 2 /**< 纵向拖拽:放大当前网格的幅值轴。 */
};
/**
* @brief 绘图区内的内联数值输入目标。
*/
@@ -255,13 +264,20 @@ namespace jdi
/**
* @brief 放弃当前正在进行的拖拽(缩放选区/平移/信号拖动),不改变视图范围。
* @param [IN] showHint 是否同时显示“已放弃选区”提示。
* @return 确实取消了某个拖拽返回 true。
*/
bool CancelDrag();
bool CancelDrag(bool showHint = false);
/**
* @brief 退出全部缩放模式(时间轴选区 / Y 轴选区),使工具栏缩放按钮回到未选中态。
* @return 之前处于某个缩放模式返回 true。
* @brief 判断当前是否处于「选区放大」的框选过程中(供宿主处理右键放弃)。
* @return 正在框选返回 true。
*/
bool IsRubberSelecting() const { return m_dragMode == PlotDragMode::RUBBER_SELECT; }
/**
* @brief 退出全部缩放模式(选区放大),使工具栏缩放按钮回到未选中态。
* @return 之前处于缩放模式返回 true。
*/
bool ExitZoomModes();
@@ -272,20 +288,26 @@ namespace jdi
* 必须清空缓存,避免旧几何键与新的信号意外重合而取到过期顶点。
*/
void InvalidatePolylineCache(); /**
* @brief 切换时间光标:启用时置于当前视图中心,再调用则关闭。
* @brief 三态循环切换时间光标:无 → 单光标 → 双光标 → 无。
* @return 状态发生变化返回 true。
*/
bool ToggleCursor();
bool CycleCursorMode();
/**
* @brief 判断时间光标是否已启用。
* @brief 判断时间光标是否已启用(单/双光标均为启用)。
* @return 启用返回 true。
*/
bool CursorEnabled() const;
/**
* @brief 获取当前光标对应的时间。
* @return 光标时间。
* @brief 获取当前时间光标模式。
* @return 光标模式。
*/
PlotCursorMode CursorMode() const;
/**
* @brief 获取光标 1 对应的时间。
* @return 光标 1 时间。
*/
double CursorTime() const;
@@ -330,12 +352,24 @@ namespace jdi
*/
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 SetAllLanesAutoFitToSelection(bool enabled, bool applyNow = true);
/**
* @brief 进入某网格 Y 轴上限/下限的内联输入状态。
@@ -601,23 +635,47 @@ namespace jdi
void ShiftLaneRange(int laneIndex, double deltaValue);
/**
* @brief 计算时间光标当前的横坐标。
* @brief 计算指定时间光标当前的横坐标。
* @param [IN] which 光标序号(1 或 2)。
* @return 横坐标。
*/
float CursorX() const;
float CursorX(int which) const;
/**
* @brief 计算时间读数标签(或编辑框)的矩形。
* @return 标签矩形,位于绘图区顶部以避免遮挡时间轴刻度。
* @brief 计算指定光标时间读数标签的矩形。
* @param [IN] which 光标序号(1 或 2)。
* @return 标签矩形,位于绘图区底部(不遮挡网格内数据点标注)。
*/
gd::RectF CursorLabelRect() const;
gd::RectF CursorLabelRect(int which) const;
/**
* @brief 判断给定横坐标是否落在时间光标附近(允许拖拽的容差范围)。
* @brief 判断给定横坐标是否落在某条时间光标附近(允许拖拽的容差范围)。
* @param [IN] x 横坐标。
* @param [OUT] which 命中的光标序号(1 或 2),未命中时为 0。
* @return 命中返回 true。
*/
bool IsOnCursorLine(int x) const;
bool IsOnCursorLine(int x, int& which) const;
/**
* @brief 将两条光标的时间夹取到当前可见时间范围内。
*
* 缩放/平移/自适应会改变可见区间,若不夹取,光标会落到视图之外并“消失”,
* 表现为“放大后无法再拖动光标”。
*/
void ClampCursorToView();
/**
* @brief 获取指定光标对应的显示颜色。
* @param [IN] which 光标序号(1 或 2)。
* @return 颜色。
*/
std::uint32_t CursorColor(int which) const;
/**
* @brief 设置并显示一条临时提示(在绘图区底部以浮层方式绘制)。
* @param [IN] text 提示文本;传空串则清除提示。
*/
void SetHint(const std::wstring& text);
/**
* @brief 精确检测鼠标位置是否命中某条可见信号曲线。
@@ -703,6 +761,12 @@ namespace jdi
*/
void PaintSignalInteraction(gd::Graphics& g);
/**
* @brief 在绘图区底部绘制临时提示条(如“已放弃当前选区”)。
* @param [IN,OUT] g 绘图上下文。
*/
void PaintHint(gd::Graphics& g);
/**
* @brief 绘制网格纵向滚动条。
* @param [IN,OUT] g 绘图上下文。
@@ -720,17 +784,6 @@ namespace jdi
void PaintLaneLegend(gd::Graphics& g, int laneIndex, const LaneVisual& lane, float visibleTop,
float visibleBottom);
/**
* @brief 在网格内绘制各信号的幅值读数。
* @param [IN,OUT] g 绘图上下文。
* @param [IN] laneIndex 网格下标。
* @param [IN] lane 网格绘制信息。
* @param [IN] visibleTop 网格在绘图区内的可见上边界。
* @param [IN] visibleBottom 网格在绘图区内的可见下边界。
*/
void PaintLaneReadout(gd::Graphics& g, int laneIndex, const LaneVisual& lane, float visibleTop,
float visibleBottom);
/**
* @brief 判断给定矩形是否完整落在可见纵向区间内。
* @param [IN] rect 待测矩形。
@@ -747,15 +800,6 @@ namespace jdi
*/
void PaintEmptyState(gd::Graphics& g);
/**
* @brief 绘制全部网格的时间光标读数框。
*
* 读数随时间光标实时变化,因此不能进入拖拽静态场景缓存,必须每帧单独绘制。
*
* @param [IN,OUT] g 绘图上下文。
*/
void PaintLaneReadouts(gd::Graphics& g);
const UiFonts* m_fonts = nullptr; /**< 字体资源。 */
const Theme* m_theme = nullptr; /**< 主题资源。 */
const DataSet* m_dataSet = nullptr; /**< 数据集合(只读访问)。 */
@@ -777,6 +821,7 @@ namespace jdi
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; /**< 拖拽起点横坐标。 */
@@ -790,7 +835,9 @@ namespace jdi
int m_dragTargetLane = -1; /**< 拖拽信号时的目标网格下标。 */
int m_dragMouseX = 0; /**< 拖拽过程中鼠标的当前横坐标(虚影跟随)。 */
int m_dragMouseY = 0; /**< 拖拽过程中鼠标的当前纵坐标(虚影跟随)。 */
bool m_cursorHover = false; /**< 鼠标是否悬停在时间光标上。 */
bool m_cursorHover = false; /**< 鼠标是否悬停在某条时间光标上。 */
int m_hoverCursor = 0; /**< 悬停/拖拽中的光标序号(1 或 2,0 表示无)。 */
int m_editCursor = 1; /**< 内联输入所作用的 Time 光标序号。 */
PlotEditTarget m_editTarget = PlotEditTarget::NONE; /**< 当前内联输入目标。 */
TextEdit m_edit; /**< 输入框编辑模型(光标/选区/剪贴板)。 */
int m_editLane = -1; /**< 内联输入所作用的网格下标。 */
@@ -803,6 +850,7 @@ namespace jdi
int m_hoverLane = -1; /**< 当前悬停网格下标。 */
int m_mouseX = 0; /**< 最近一次鼠标横坐标(用于悬停提示定位)。 */
int m_mouseY = 0; /**< 最近一次鼠标纵坐标(用于悬停提示定位)。 */
std::wstring m_hintText; /**< 临时提示文本(空表示不显示)。 */
std::vector<LaneVisual> m_lanes; /**< 当前网格绘制信息集合。 */
/**