diff --git a/Components/Application/Src/Application.cpp b/Components/Application/Src/Application.cpp index 91a955a..d2f7c3a 100644 --- a/Components/Application/Src/Application.cpp +++ b/Components/Application/Src/Application.cpp @@ -2570,6 +2570,10 @@ namespace jdi const int laneIndex = m_plot.LaneIndexAt(x, y); if (laneIndex >= 0) { + // 菜单打开后鼠标移动被菜单独占,绘图区不再收到移动事件; + // 若不先清掉悬停状态,右键前停留在曲线上的「可拖拽光晕」与提示气泡 + // 会一直挂在原地(用户报告的"进入网格菜单后高亮/文本提示不消失")。 + m_plot.OnMouseLeave(); ShowPlotContextMenu(laneIndex, x, y); InvalidateRect(hwnd, nullptr, FALSE); } diff --git a/Components/PlotView/Src/PlotView.cpp b/Components/PlotView/Src/PlotView.cpp index 9972887..818e1cd 100644 --- a/Components/PlotView/Src/PlotView.cpp +++ b/Components/PlotView/Src/PlotView.cpp @@ -10,8 +10,6 @@ namespace jdi { - - namespace { const int kTimeTickTarget = 10; /**< 时间轴期望刻度数。 */ @@ -4761,6 +4759,12 @@ namespace jdi m_hoverSignal = SignalRef(); changed = true; } + // 图例悬停半高亮同样随鼠标离开而消失(否则右键弹菜单/移出绘图区后残留在原地)。 + if (m_hoverLegend.IsValid()) + { + m_hoverLegend = SignalRef(); + changed = true; + } if (m_cursorHover) { m_cursorHover = false; @@ -7372,17 +7376,21 @@ namespace jdi } const int otherIndex = showDelta ? FindNearestSample(*resolved.file, otherTime) : -1; - const std::size_t count = - std::min(resolved.file->time.size(), resolved.data->values.size()); - std::size_t begin = 0; - std::size_t end = 0; - VisibleIndexRange(*resolved.file, count, begin, end); - begin = std::min(begin, static_cast(sampleIndex)); - end = std::max(end, static_cast(sampleIndex) + 1); + // 本函数**只需要光标所在采样点处的派生取值**(以及双光标另一端的取值), + // 因此把请求窗口收窄到「需要的那几个下标」即可,均值滤波半径造成的取值 + // 依赖由 `CurveSpanFor` 自身的保护带负责覆盖。 + // + // ⚠️ 绝不能把窗口扩成「光标 ↔ 视界边缘」的并集:光标停在视界之外(例如 + // 平移视界把光标甩到很远,或光标远离当前缩放区间)时会得到一个上千万采样的 + // 巨大窗口,超过缓存上限后走一次性缓冲 → **每帧重算整段滤波**, + // 实测单帧 300ms+(用户报告的"平移/拖动光标异常卡顿"的直接原因)。 + std::size_t begin = static_cast(sampleIndex); + std::size_t end = begin + 1; if (otherIndex >= 0) { - begin = std::min(begin, static_cast(otherIndex)); - end = std::max(end, static_cast(otherIndex) + 1); + const std::size_t other = static_cast(otherIndex); + begin = std::min(begin, other); + end = std::max(end, other + 1); } std::size_t spanBegin = 0; const std::vector* curvePtr = diff --git a/Documents/agent_mem.md b/Documents/agent_mem.md index f19a716..2cdbac3 100644 --- a/Documents/agent_mem.md +++ b/Documents/agent_mem.md @@ -157,6 +157,36 @@ Documents/ 项目文档(含本文件) - 主菜单条目矩形的右缘为 `menu.right - kPadH`,而子菜单左缘为 `menu.right - kSubOverlap`,两者之间存在约 3px 的**内边距"死区"**(位于主菜单矩形内但落在任何条目之外)。鼠标事件密集时指针必然落入该窄带,从而触发误关闭。 - **修复**:子菜单展开期间,若指针仍位于「主菜单 ∪ 子菜单」矩形范围内、且尚未落到**其它可点击条目**上(例如掠过内边距窄带、分隔线、禁用项),则保持子菜单展开并维持父项高亮;只有明确移动到其它条目或完全离开上述范围时才收起。 +### 9.2 打开网格右键菜单后,悬停光晕与「可拖拽」提示气泡不消失(已修复) +- **症状**:鼠标停在信号曲线上(出现光晕高亮 + 「XXX · 按住拖拽可移入其它网格」提示气泡)时右键弹出网格菜单,高亮与气泡**残留在原地**,不随菜单打开而消失。 +- **根因**:菜单打开后鼠标移动被菜单独占(`Application::HandleMessage` 在 `m_menu.IsOpen()` 时只把事件交给菜单),绘图区再也收不到 `WM_MOUSEMOVE`;而打开菜单时**没有清空**绘图区的悬停状态(`m_hoverSignal` / `m_hoverLegend`),因此 `PaintHoverHighlight` 与 `PaintSignalInteraction` 继续把高亮与气泡画出来。 +- **修复**: + - `Application` 在绘图区右键弹出网格菜单前调用 `m_plot.OnMouseLeave()`,清空悬停状态; + - `PlotView::OnMouseLeave()` 补上清空 `m_hoverLegend`(原先只清 `m_hoverLane/m_hoverSignal` 等,图例半高亮会残留)。 + +### 9.3 性能:启用带预览曲线的实验性功能后,平移/拖动光标异常卡顿(已修复) +- **症状**(以 `VERY_BIG_DATA.CSV` 为例,加载并显示全部信号后):时间轴选区放大到某些区间时,左键/中键平移网格视界、或启用「均值滤波 / 中心轴边沿计数」后再平移、甚至只是左右拖动时间光标看数值,都会**异常卡顿**(实测单帧 300ms+)。 +- **排查方法**:在 `.preflight/perf/`(gitignored)用离屏窗口(不占用鼠标/键盘/屏幕)直接驱动 `PlotView`,用 `QueryPerformanceCounter` 测 `Paint()`,并对 `CurveSpanFor` / `ComputeMovingAverageIterated` / `PaintDerivedCurve` 分段埋点计时。 +- **根因**(`Components/PlotView/Src/PlotView.cpp` 的 `PaintCursor` → `paintDerivedCapsule` lambda): + - 绘制时间光标处的**派生曲线取值胶囊**时,把请求窗口取成「**可见区间 ∪ 光标采样下标**」的并集; + - 当光标不在当前视界内(平移视界会把光标甩到视界外)时,这个并集会变成 `[光标下标, 视界右端]` 或 `[视界左端, 光标下标]` —— 上千万采样的**巨大窗口**; + - 该窗口超过 `kCurveSpanCacheMaxSamples`(4M) → 走一次性缓冲、**不进缓存** → **每一帧都重算整段多次迭代滑动平均**(实测 5 条曲线合计 300ms+)。 + - 这条路径同样被平移、拖滑块、拖动光标、改色等所有「每帧重绘」的交互触发,因此它们一起变卡。 +- **修复**:`paintDerivedCapsule` 只需要光标所在采样点(以及双光标另一端)的派生取值,因此把请求窗口收窄为「**需要的那几个下标**」(`[sampleIndex, sampleIndex+1]`,双光标时并入另一端)。均值滤波半径造成的取值依赖由 `CurveSpanFor` 自身的保护带(`guard = clamp(max(窗口/8, (X+1)*N+64), 4096, 65536)`)负责覆盖 —— 对一切 `reach ≤ 65536` 的参数,取值与「整条曲线」**逐位一致**,显示效果零变化。 +- **效果**(离屏实测,`VERY_BIG_DATA`,5 网格): + - 均值滤波平移(2% 跨度):**326ms → 约 16–23ms**; + - 均值滤波平移(0.4%/0.08%/0.016%):**约 305ms → 4–6ms**; + - 中心轴边沿计数平移(2%):**约 300ms → 约 20ms**; + - 光标左右移动(静态场景):命中场景缓存,仅叠加层重绘,约 9ms。 +- **不影响显示的说明**:本次**未**改动任何几何/绘制算法(没有引入曾导致"棘状细节被并成实心块"的 ±1 列扩展),只收窄了「为了取时间光标处派生取值而请求的曲线窗口」,因此画面与实际取值完全不变。 + +### 9.4 性能经验与已知残留热点(供后续参考) +- **本仓库的性能黄金法则**:交互期间(`m_dragMode != NONE`)走「快速帧」(关闭波形/派生曲线抗锯齿),松手后补一帧全质量;静止画面走「静态场景缓存」(BitBlt 贴图 ~1ms)。任何**只在鼠标移动/拖动参数时改变**的内容都必须画在贴图之后(叠加层),绝不能进静态缓存,也不能每帧触发整条曲线/整段采样的重算。 +- **已知残留热点**(本次未动,均为"非拖拽的整幅重绘",实际拖拽时因快速帧而很快): + - 原始波形的「逐像素列包络带」用 GDI+ `FillPath` 填充;在某些跨度下包络带不被判为「梳状」,抗锯齿填充仍约 20–27ms/帧(`VERY_BIG_DATA` 的全量重绘)。这是 GDI+ 抗锯齿路径的固有代价;如需进一步提速,**只能**降低列分辨率或对该形态关闭抗锯齿 —— 两者都会改变显示效果,需用户确认后再动。 + - 「构建为新信号」等一次性操作会真正计算整条派生曲线(约 1 帧的秒级耗时,正常)。 +- **测量工具**:`.preflight/perf/perf.exe [mode]`(离屏,不弹窗)。`mode=0` 跑各交互场景耗时;`mode=1` 导出确定性渲染 PNG 以便逐像素比对(用于验证"显示效果不变")。该目录已由 `.gitignore` 忽略,不会入库。 + --- ## 10. 变更日志(本文件维护记录) @@ -164,3 +194,4 @@ Documents/ 项目文档(含本文件) | 日期 | 设备/模型 | 摘要 | | --- | --- | --- | | 2026-10-11 | DeepSeek-V4.1-Flash | 新建本文件;记录项目要点与规范;修复「实验性功能」二级菜单瞬间关闭问题。 | +| 2026-10-11 | DeepSeek-V4.1-Flash | 修复右键网格菜单后悬停光晕/提示气泡残留;修复「启用带预览曲线的实验性功能后平移/拖光标 300ms+ 卡顿」(`paintDerivedCapsule` 巨窗每帧重算,收窄请求窗口,显示效果零变化)。 |