交互修正:悬停提示跟随鼠标、信号拖拽手掌/抓握光标、增量只显示在右侧光标

- 信号悬停提示改为真正跟随鼠标(PaintSignalInteraction 用 m_mouseX/m_mouseY 定位,
  删除 m_hoverAnchorX/m_hoverAnchorY)。
  根因:OnMouseMove 只在状态变化时返回 changed,仅鼠标位置变化时返回 false,
  宿主不重绘 → 提示停在上一帧位置。现补上
  `if (m_hoverSignal.IsValid() && mouseMoved) changed = true;`(mouseMoved 由前后坐标比较得出),
  鼠标在可拖拽区域内持续移动时提示条会跟随。

- 可拖拽信号的光标响应参考 Photoshop 平移元素:
  悬停时用手掌光标(IDC_HAND),按住左键拖拽途中用抓握光标(IDC_SIZEALL)。

- 双光标增量差值:改为**只**由"右侧光标"(时间较大的那条)显示,
  按差值显式给出正负号(+ / -,不再固定用 ±);把右光标拖到左侧后,
  增量会自动改由新的右侧光标显示。(判定改用时间比较,不再用 m_activeCursor。)

- 光标时间读数标签与 Δt 差值标签:悬停(未按下)时不再改变鼠标光标,
  真正拖拽时统一使用左右箭头(IDC_SIZEWE)。
  两个标签都叠在光标线正上方,因此在 QueryCursor 中必须先于光标线判定返回 nullptr,
  否则会命中光标线给出 SIZEWE。

- Δt 差值文案由 "Δt " 改为 "Δt:"(全角冒号)。

- 一并移除遗留的 TEMP-PROBE 调试输出(HitTestSignal / PaintSignalInteraction /
  OnMouseMove 探针)与已不再使用的 m_dragOnLabel。

验证(TestData/VERY_BIG_DATA.CSV,编码器A相密集方波,125%、1725x996):
- 提示跟随:悬停 (700,250)→(700,400)→(760,620)→(900,700) 后,
  提示条出现在最后一次鼠标位置旁(截图核对),不再停留在首次出现处。
- 光标形态探测:信号悬停 = HAND;拖拽信号 = SIZEALL;
  时间读数标签/Δt 标签悬停 = ARROW(不改变);Δt 标签拖拽 = SIZEWE;
  时间读数标签拖拽 = SIZEWE;光标线上方悬停 = SIZEWE。
- 增量归属:默认双光标左 4.9417(无增量)/ 右 4.9425 , +0.0008;
  把右光标拖到左侧后变为左 0.288688(无增量)/ 右 4.9417 , +4.65301;
  把右光标拖到低电平处得到 4.80724 , -0.134458(负号正确)。
- Δt 文案:截图确认显示 "Δt: 15s 457.214ms"。
- 回归([1].CSV 双光标):左 426.316(无增量)/ 右 538.583 , +112.267;
  Δt:151ms 200us。
- make clean && make all 无警告无错误。
This commit is contained in:
DeepSeek V4.1 Flash committed 2026-10-08 21:51:16 +08:00
1 parent ed13aa7d49
commit a566d98bab
2 files changed
+34 -80

No files matched your search

+34 -77
View File
@@ -185,13 +185,13 @@ namespace jdi
}
/**
* @brief 双光标差值的完整显示文本("Δt " + 复合单位间隔)。
* @brief 双光标差值的完整显示文本("Δt:" + 复合单位间隔)。
* @param [IN] seconds 时间间隔(秒)。
* @return 形如 "Δt 1h 1min 2s" 的文本。
* @return 形如 "Δt:1h 1min 2s" 的文本。
*/
std::wstring FormatSpanText(double seconds)
{
return L"Δt " + FormatDurationCompound(seconds);
return L"Δt:" + FormatDurationCompound(seconds);
}
/**
@@ -1677,17 +1677,6 @@ namespace jdi
SignalRef best;
float bestDistance = tolerance;
// TEMP-PROBE
{
FILE* fp = fopen(".preflight\\hit.log", "a");
if (fp != nullptr)
{
fprintf(fp, "query=(%d,%d) plot=(%.0f,%.0f,%.0f,%.0f) lane=%d\n", x, y, m_plotRect.X,
m_plotRect.Y, m_plotRect.Width, m_plotRect.Height, LaneAtY(y));
fclose(fp);
}
}
// 只为当前网格内的可见信号做精确的点到线段距离检测,避免跨网格误命中。
for (const SignalRef& ref : m_tab->Lanes()[static_cast<std::size_t>(laneIndex)].signals)
{
@@ -1729,34 +1718,8 @@ namespace jdi
}
if (windowFirst >= windowLast)
{
// TEMP-PROBE
FILE* fp = fopen(".preflight\\hit.log", "a");
if (fp != nullptr)
{
fprintf(fp, " window empty first=%zu last=%zu pts=%zu fx=%.1f tol=%.1f\n", windowFirst,
windowLast, points.size(), fx, tolerance);
fclose(fp);
}
continue;
}
// TEMP-PROBE
{
FILE* fp = fopen(".preflight\\hit.log", "a");
if (fp != nullptr)
{
float minY = 1.0e9f;
float maxY = -1.0e9f;
for (std::size_t i = windowFirst; i < windowLast; ++i)
{
minY = std::min(minY, points[i].Y);
maxY = std::max(maxY, points[i].Y);
}
fprintf(fp, " window first=%zu last=%zu pts=%zu yRange=%.1f..%.1f fy=%.1f\n",
windowFirst, windowLast, points.size(), minY, maxY, fy);
fclose(fp);
}
}
// 散点模式下把采样点本身也视为可命中目标。
if (resolved.style->mode == DrawMode::SCATTER)
{
@@ -2208,13 +2171,13 @@ namespace jdi
case PlotDragMode::SCROLL_LANES:
return LoadCursorW(nullptr, IDC_SIZENS);
case PlotDragMode::CURSOR_MOVE:
// 从时间读数标签上抓起的拖拽统一用抓握(移动)图标;
// 直接从光标线上抓起的拖拽保持左右箭头。
return LoadCursorW(nullptr, m_dragOnLabel ? IDC_SIZEALL : IDC_SIZEWE);
// 光标拖拽(无论是抓住光标线还是抓住时间读数标签)都沿时间轴移动 → 左右箭头。
return LoadCursorW(nullptr, IDC_SIZEWE);
case PlotDragMode::CURSOR_SPAN_MOVE:
// 差值标签拖拽:统一使用抓握图标(不是左右箭头)。
return LoadCursorW(nullptr, IDC_SIZEALL);
// 差值标签拖拽:两条光标整体沿时间轴平移 → 左右箭头。
return LoadCursorW(nullptr, IDC_SIZEWE);
case PlotDragMode::MOVE_SIGNAL:
// 拖动信号(参考 Photoshop 平移元素的抓握光标)。
return LoadCursorW(nullptr, IDC_SIZEALL);
default:
break;
@@ -2226,18 +2189,18 @@ namespace jdi
return LoadCursorW(nullptr, IDC_SIZENS);
}
// 双光标差值标签:悬停时用"手形"表示可交互(与拖拽中的抓握图标区分)。
// 光标时间读数标签与双光标差值标签:**悬停(未按下)时不改变鼠标形状**。
// 两个标签都叠在光标线的正上方,因此必须先于"光标线"判定返回 nullptr,
// 由调用方回退成箭头;只有真正拖拽时才切成左右箭头(见 CURSOR_MOVE / CURSOR_SPAN_MOVE)。
if (IsOnCursorSpanLabel(x, y))
{
return LoadCursorW(nullptr, IDC_HAND);
return nullptr;
}
// 光标时间读数标签同样可拖拽:悬停用手形。
{
int labelWhich = 0;
if (IsOnCursorTimeLabel(x, y, labelWhich))
{
return LoadCursorW(nullptr, IDC_HAND);
return nullptr;
}
}
@@ -2251,10 +2214,10 @@ namespace jdi
if (UiPointInRect(m_plotRect, static_cast<float>(x), static_cast<float>(y)))
{
// 命中信号曲线时给出可拖拽提示光标(仅在常规模式下)。
// 悬停命中可拖拽信号时给出"手掌"提示光标;按下拖拽过程中改用抓握光标。
if (Tab().ZoomMode() == PlotZoomMode::NONE && HitTestSignal(x, y).IsValid())
{
return LoadCursorW(nullptr, IDC_SIZEALL);
return LoadCursorW(nullptr, IDC_HAND);
}
// 选区放大模式下**未开始框选时保持默认箭头**(不改变鼠标形状);
@@ -2266,6 +2229,7 @@ namespace jdi
bool PlotView::OnMouseMove(int x, int y)
{
const bool mouseMoved = (x != m_mouseX) || (y != m_mouseY);
m_mouseX = x;
m_mouseY = y;
@@ -2456,13 +2420,6 @@ namespace jdi
}
if (hoverSignal != m_hoverSignal)
{
if (hoverSignal.IsValid())
{
// 悬停提示固定在"刚刚进入可拖拽区域"的位置:之后鼠标即使继续在该区域内
// 移动,提示也只停留在原地(不再跟随鼠标)。
m_hoverAnchorX = x;
m_hoverAnchorY = y;
}
m_hoverSignal = hoverSignal;
changed = true;
}
@@ -2474,6 +2431,13 @@ namespace jdi
m_spanHover = spanHover;
changed = true;
}
// 悬停提示跟随鼠标:只要提示可见,鼠标移动本身就要触发重绘;
// 否则鼠标在可拖拽区域内继续移动时,提示会停在上一帧的位置。
if (m_hoverSignal.IsValid() && mouseMoved)
{
changed = true;
}
return changed;
}
@@ -2494,7 +2458,6 @@ namespace jdi
m_dragMoved = false;
m_rubberRect = gd::RectF();
m_rubberAxis = PlotRubberAxis::UNDECIDED;
m_dragOnLabel = false;
m_cursorGrabOffset = 0;
// 新的交互开始:清掉上一次留下的提示(拖动过程中会重新设置粘滞提示)。
SetHint(std::wstring());
@@ -2520,7 +2483,6 @@ namespace jdi
if (IsOnCursorSpanLabel(x, y))
{
m_dragMode = PlotDragMode::CURSOR_SPAN_MOVE;
m_dragOnLabel = true;
m_spanDragStartX = x;
m_spanDragTime1 = Tab().CursorTime();
m_spanDragTime2 = Tab().CursorTimeAlt();
@@ -2533,7 +2495,6 @@ namespace jdi
if (IsOnCursorTimeLabel(x, y, labelWhich))
{
m_dragMode = PlotDragMode::CURSOR_MOVE;
m_dragOnLabel = true;
m_hoverCursor = labelWhich;
m_activeCursor = labelWhich;
m_cursorHover = true;
@@ -2658,7 +2619,6 @@ namespace jdi
m_dragLane = -1;
m_dragMoved = false;
m_rubberRect = gd::RectF();
m_dragOnLabel = false;
m_cursorGrabOffset = 0;
// 松开左键:粘滞提示结束计时,开始"停留后淡出"。
ReleaseHint();
@@ -4220,6 +4180,10 @@ namespace jdi
// 重叠时把"最近操作过的那条光标"的内容画在上层,因此先画另一条。
const int order[2] = { (m_activeCursor == 2) ? 1 : 2, (m_activeCursor == 2) ? 2 : 1 };
// 增量差值**只**显示在"右侧光标"上。右侧 = 时间较大的那条,随拖拽实时变化:
// 把右光标拖到左边后,增量会自动改由新的右侧光标显示。
const int rightCursor = (Tab().CursorTime() <= Tab().CursorTimeAlt()) ? 2 : 1;
// ---- 光标竖线 + 顶部把手 ----
for (int pass = 0; pass < cursorCount; ++pass)
{
@@ -4254,7 +4218,8 @@ namespace jdi
const int which = doubleMode ? order[pass] : 1;
const double time = (which == 2) ? Tab().CursorTimeAlt() : Tab().CursorTime();
const double otherTime = (which == 2) ? Tab().CursorTime() : Tab().CursorTimeAlt();
const bool showDelta = doubleMode && (which == order[1]);
// 只有右侧光标显示增量差值(左光标只显示自身取值)。
const bool showDelta = doubleMode && (which == rightCursor);
// 光标在可视区外时,把标注"钉"在对应的网格边上,保证取值仍然可见。
const float pinX = CursorDisplayX(which);
@@ -4331,10 +4296,9 @@ namespace jdi
if (SampleFileAt(*resolved.file, *resolved.data, otherTime, interpolate, otherValue))
{
const double delta = value - otherValue;
// 增量为右侧光标减去左侧光标;正值用 ±,负值直接用负号,
// 避免出现 “±-0.001” 这种双符号写法。
// 增量 = 右侧光标取值 − 左侧光标取值,按差值显式给出正负号。
text += L" , ";
text += (delta < 0.0) ? L"-" : L"±";
text += (delta < 0.0) ? L"-" : L"+";
text += FormatValue(std::fabs(delta));
}
}
@@ -4619,13 +4583,6 @@ namespace jdi
// 悬停提示:光标贴近曲线时给出可拖拽说明(与时间光标的提示方式一致)。
if (!m_hoverSignal.IsValid())
{
// TEMP-PROBE
FILE* fp0 = fopen(".preflight\\paint.log", "a");
if (fp0 != nullptr)
{
fprintf(fp0, "hover-invalid dragMode=%d\n", static_cast<int>(m_dragMode));
fclose(fp0);
}
return;
}
const int laneIndex = m_tab->LaneOfSignal(m_hoverSignal);
@@ -4642,17 +4599,17 @@ namespace jdi
const LaneVisual& lane = m_lanes[static_cast<std::size_t>(laneIndex)];
const std::wstring text = resolved.data->Title() + L" · 按住拖拽可移入其它网格";
// 提示锚定在"首次进入可拖拽区域"的位置,鼠标在该区域内继续移动时提示不会跟随。
// 提示跟随鼠标:在可拖拽区域内持续移动时,提示条也要跟着走。
const float textWidth = UiTextWidth(g, text, m_fonts->uiSmall) + 16.0f * m_scale;
const float height = 20.0f * m_scale;
float left = static_cast<float>(m_hoverAnchorX) + 12.0f * m_scale;
float left = static_cast<float>(m_mouseX) + 12.0f * m_scale;
if (left + textWidth > m_plotRect.X + m_plotRect.Width)
{
left = m_plotRect.X + m_plotRect.Width - textWidth;
}
left = std::max(m_plotRect.X, left);
float top = static_cast<float>(m_hoverAnchorY) + 14.0f * m_scale;
float top = static_cast<float>(m_mouseY) + 14.0f * m_scale;
top = std::min(top, m_plotRect.Y + m_plotRect.Height - height - 2.0f * m_scale);
top = std::max(top, lane.rect.Y + 2.0f * m_scale);