「所属网格」菜单:标题固定 + 10 行上限 + 滚动条;网格滚动联动改为动态过渡
1) 菜单滚动时最上方的「所属网格」标题不再跟着滚走
- 根因:上一轮只实现了“尾部固定区”(最后一个分隔线 + 「新建网格」钉在底部),
而滚动区是从条目 0 开始的,于是标题也被一起滚掉。
- 修法:BuildLayout 新增 m_scrollFirst(= 前导 HEADER / HEADER_ACTION 的个数),
滚动区收敛为半开区间 [m_scrollFirst, m_pinnedIndex):
· 前导标题从菜单顶向下固定排(不参与滚动);
· 滚动区条目受 m_scrollOffset 影响;
· 尾部固定区从菜单底向上对齐。
- 配套:Paint 的裁剪与“跳过不可见条目”范围、ItemAtPoint 的“必须落在滚动视口内”判定、
EnsureItemVisible 的下界全部改用该区间;新增 ScrollRegion(first, last) 供查询/测试。
2) 菜单长度上限改为「最多 10 条网格」并加滚动条
- MenuLayoutConstraint 新增 maxScrollRows(滚动区最多显示的标准行数;与 maxHeight
同时给出时取更严格者);OpenLaneMenu 传 10(kLaneMenuVisibleRows),
另保留 maxHeight = max(180, 客户区高 × 0.8) 作为矮窗口的硬上限。
- 新增滚动条:轨道贴滚动条目区右内侧(不盖住标题与底部固定项),滑块高度按视口/内容比
(下限 22 逻辑像素);滚动区条目宽度让出 kScrollBarWidth + kScrollBarGap,
文本与对勾不会压在滑块上。
- 滚动条可拖拽:按下滑块 → 拖动(只改偏移、不更新悬停);点击轨道空白 → 滑块中心移过去;
松开结束拖拽且不下发命令、菜单保持打开。新增 ScrollOffsetForThumbTop 反算偏移。
- ⚠️ BuildLayout 绝不能复位 m_scrollBarDragging:拖拽时每帧都会 RebuildLayout(),
在那里清零会让拖拽“拖一下就脱手”(该状态只在 Open / Close 里复位)。
3) 网格滚动联动改为动态过渡(旧实现直接把视界瞬移过去,过于僵硬)
- PlotView::ScrollLaneIntoView 只负责启动平滑动画:与信号列表的“滚动到目标信号”同源
(ease-out cubic;时长 clamp(距离×0.35ms, 130, 300)),新增
m_laneScrollAnimActive/From/To/StartMs/DurationMs。
- 宿主 16ms 定时器(kScrollTimerId)调用 AdvanceLaneScrollAnimation() 逐帧推进,
与信号面板动画共用同一条定时器;Render 的“需要保持定时重绘”判定加入
LaneScrollAnimationActive()。
- 每帧 ClampLaneScroll() + BuildLaneVisuals()(网格滚动量参与场景/图层指纹与各网格 rect);
结束时精确落到目标。已完整可见的网格仍然不启动动画。
- ⚠️ 用户自己滚动网格(滚轮 / 拖滚动条)会 CancelLaneScrollAnimation(),避免与动画抢滚动位置。
验证(全部离屏执行:窗口 SW_HIDE + 只注入合成消息 + 进程与线程 BELOW_NORMAL,
不移动真实光标、不抢焦点、不截屏):
- .preflight/check 扩到 95 项,95/95 通过;新增断言:
· 菜单顶部标题在滚动时不动(Y 坐标不变)、滚动视口高度恰为 10 × 行高;
· 滚动条轨道/滑块存在、随滚动移动、按住滑块拖到底(菜单不关、不下发命令、末条目入视口)、
滚轮回到顶部;
· ScrollLaneIntoView 不瞬移(刚触发时几乎未动)、中间帧单调递进、收尾精确到位、
已可见时不启动动画、滚轮/手动滚动会取消动画。
- A/B:回退三处(标题固定 / 10 行上限 / 滚动动画)⇒ 10 项如实失败;恢复后 95/95。
- make release / make debug 零警告。
文档:Documents/agent_mem.md 新增 §9.39、§11.2 第 104–107 条与变更日志一行。
This commit is contained in:
1 parent
2904699eae
commit
82b5ebe4f9
10 files changed
+496
-78
No files matched your search
@@ -194,30 +194,81 @@ namespace jdi
|
||||
const float contentTop = rect.Y - m_plotRect.Y + Tab().LaneScroll();
|
||||
const float laneHeight = rect.Height;
|
||||
|
||||
const float previous = Tab().LaneScroll();
|
||||
float scroll = previous;
|
||||
if (contentTop - scroll < 0.0f)
|
||||
float desired = Tab().LaneScroll();
|
||||
if (contentTop - desired < 0.0f)
|
||||
{
|
||||
// 目标网格在可视区上方:把它的上边界对齐到绘图区上沿。
|
||||
scroll = contentTop;
|
||||
desired = contentTop;
|
||||
}
|
||||
else if (contentTop + laneHeight - scroll > m_plotRect.Height)
|
||||
else if (contentTop + laneHeight - desired > m_plotRect.Height)
|
||||
{
|
||||
// 目标网格在可视区下方:把它的下边界对齐到绘图区下沿。
|
||||
scroll = contentTop + laneHeight - m_plotRect.Height;
|
||||
desired = contentTop + laneHeight - m_plotRect.Height;
|
||||
}
|
||||
else
|
||||
{
|
||||
return false; // 已经完整可见
|
||||
}
|
||||
|
||||
Tab().LaneScroll() = scroll;
|
||||
ClampLaneScroll();
|
||||
if (std::fabs(Tab().LaneScroll() - previous) <= 0.01f)
|
||||
// **平滑滚动**(用户 2026-10-16 要求):与信号列表的“滚动到目标信号”同源,
|
||||
// 先用 ease-out cubic 过渡过去,而不是把视界瞬移过去。
|
||||
const float maxOffset = std::max(0.0f, m_laneContentHeight - m_plotRect.Height);
|
||||
desired = static_cast<float>(ClampDouble(desired, 0.0, maxOffset));
|
||||
const float distance = std::fabs(desired - Tab().LaneScroll());
|
||||
if (distance < 1.0f)
|
||||
{
|
||||
// 差值极小:直接到位(不再为 1px 抖动做动画)。
|
||||
Tab().LaneScroll() = desired;
|
||||
ClampLaneScroll();
|
||||
m_laneScrollAnimActive = false;
|
||||
BuildLaneVisuals();
|
||||
return true;
|
||||
}
|
||||
|
||||
const float durationMs = std::max(
|
||||
kLaneScrollAnimMinMs,
|
||||
std::min(kLaneScrollAnimMaxMs, distance * kLaneScrollAnimMsPerPixel));
|
||||
m_laneScrollAnimFrom = Tab().LaneScroll();
|
||||
m_laneScrollAnimTo = desired;
|
||||
m_laneScrollAnimStartMs = GetTickCount64();
|
||||
m_laneScrollAnimDurationMs = static_cast<std::uint32_t>(durationMs + 0.5f);
|
||||
m_laneScrollAnimActive = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PlotView::AdvanceLaneScrollAnimation()
|
||||
{
|
||||
if (!m_laneScrollAnimActive)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
// 网格滚动量参与场景/图层指纹,因此重建可视几何(各网格 rect 与 yMin/yMax 依赖滚动量)。
|
||||
|
||||
const std::uint64_t now = GetTickCount64();
|
||||
const std::uint64_t elapsed =
|
||||
(now >= m_laneScrollAnimStartMs) ? (now - m_laneScrollAnimStartMs) : 0;
|
||||
float progress = (m_laneScrollAnimDurationMs > 0u)
|
||||
? static_cast<float>(elapsed) / static_cast<float>(m_laneScrollAnimDurationMs)
|
||||
: 1.0f;
|
||||
if (progress > 1.0f)
|
||||
{
|
||||
progress = 1.0f;
|
||||
}
|
||||
// ease-out cubic:起步快、靠近目标时减速(与信号列表完全一致的手感)。
|
||||
const float eased = 1.0f - std::pow(1.0f - progress, 3.0f);
|
||||
Tab().LaneScroll() = m_laneScrollAnimFrom + (m_laneScrollAnimTo - m_laneScrollAnimFrom) * eased;
|
||||
|
||||
if (progress >= 1.0f)
|
||||
{
|
||||
// 结束:精确落到终点并夹取(内容高度可能在动画期间变化过)。
|
||||
Tab().LaneScroll() = m_laneScrollAnimTo;
|
||||
m_laneScrollAnimActive = false;
|
||||
ClampLaneScroll();
|
||||
BuildLaneVisuals();
|
||||
return false;
|
||||
}
|
||||
|
||||
// 网格滚动量参与场景/图层指纹与各网格 `rect`,每帧重建可视几何。
|
||||
ClampLaneScroll();
|
||||
BuildLaneVisuals();
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1085,6 +1085,8 @@ namespace jdi
|
||||
}
|
||||
const double travel =
|
||||
static_cast<double>(y - m_dragLastY) / static_cast<double>(m_plotRect.Height);
|
||||
// 用户自己拖滚动条 ⇒ 立即取消“点点信号后自动滚入可视区”的动画(否则两者会抢滚动位置)。
|
||||
CancelLaneScrollAnimation();
|
||||
Tab().LaneScroll() += static_cast<float>(travel * m_laneContentHeight);
|
||||
m_dragLastY = y;
|
||||
m_dragMoved = true;
|
||||
@@ -2094,6 +2096,8 @@ namespace jdi
|
||||
return false;
|
||||
}
|
||||
|
||||
// 用户自己滚轮滚动 ⇒ 立即取消自动滚入可视区的动画。
|
||||
CancelLaneScrollAnimation();
|
||||
Tab().LaneScroll() -= static_cast<float>(notches * 48.0 * m_scale);
|
||||
BuildLaneVisuals();
|
||||
return true;
|
||||
|
||||
@@ -64,6 +64,15 @@ namespace jdi
|
||||
* 折线缓存(键含幅值映射)能命中,避免"每帧滚 1 行 → 每帧重算折线"的性能陷阱。
|
||||
*/
|
||||
inline const int kLayerRollQuantum = 16;
|
||||
/**
|
||||
* @brief 「点选信号 ⇒ 把目标网格滚入可视区」的平滑过渡参数(与信号列表同源)。
|
||||
*
|
||||
* 时长 = `clamp(距离 × kLaneScrollAnimMsPerPixel, kLaneScrollAnimMinMs, kLaneScrollAnimMaxMs)`,
|
||||
* 进度再过 ease-out cubic(起步快、靠近目标时减速)⇒ 用户要求的“动态过渡而不是瞬移”。
|
||||
*/
|
||||
inline const float kLaneScrollAnimMinMs = 130.0f; /**< 最短时长(短距离)。 */
|
||||
inline const float kLaneScrollAnimMaxMs = 300.0f; /**< 最长时长(长距离)。 */
|
||||
inline const float kLaneScrollAnimMsPerPixel = 0.35f; /**< 每像素距离增加的时长。 */
|
||||
inline const float kEnvelopeMinPixelSpan = 0.5f; /**< 包络纵向跨度小于该像素值时合并为单个顶点。 */
|
||||
inline const float kCursorHandleReserve = 9.0f; /**< 光标顶部三角把手占用的高度(数据点贴边时需避让)。 */
|
||||
inline const float kMarkerEdgeInset = 7.0f; /**< 数据点越界贴边时距网格可见边界的间距。 */
|
||||
|
||||
Reference in new issue
Block a user