滚动过渡动画根治 + 快速连点跟手 + 分组名单击展开 + 光标吸附同源 + 按钮淡出对齐 + 滚动位置复位
一、滚动过渡动画卡顿(用户报告:甚至不如手动拖滚动条)—— 三处独立根因
1) WM_TIMER(kScrollTimerId) 无条件整窗失效 ⇒ WM_PAINT 看到更新区域不在绘图区内 ⇒ 把
m_chromeDirty 置真 ⇒ 每一帧都重绘标签栏/工具栏/面板。
修法:用「推进之前」的面板动画状态决定失效范围 —— 面板动画(滚动到目标信号 / 分组折起展开)
确实要改面板像素 ⇒ 整窗失效;只有绘图区动画(网格滚动到可视区 / 回到最上方)⇒ 只失效绘图区。
实测:绘图区动画帧 13.6ms → 3.5ms;手动拖滚动条参照 4.2ms。
⚠️ 未改变 §9.41 的回送结构(仍是 if (fullFrame) {整窗} else {绘图区})。
2) 图层预热不足:上百个离屏网格长期不建图层,长距离滚动动画扫进去时每一帧都有十几个网格
没有图层像素,只能靠「网格底衬兜底」重画(约 0.5ms/网格 ⇒ +8ms/帧)。
修法:LaneVisuallyRelevant(laneIndex, prefetchScreens) 增加 ±1 屏预取带(AnyVisibleLanePending 用),
并新增 PrefetchStepAllowed()(kPrefetchMinIntervalMs=32 ⇒ 约 31 次/秒)限速地继续请求重绘,
让整批图层在静置期间逐步建好。
实测:lane-only + VERY_BIG_DATA 帧 13.9ms → 5.3ms;signal-click + VERY_BIG_DATA 18.3ms → 10.3ms。
3) 网格滚动量原本参与「宽基底可用性」判据(上一轮已修:SceneContentHash(..., includeLaneScroll=false))。
二、光标吸附改用「放大自动点线」的同一判定(用户 2026-10-17 修订)
AutoPointLineNeeded 改名 MinorCellSparse(中性名,同一判定两处用途):任一小格内有效数据点 < 5 即为稀疏。
- 「放大自动切点线」与「光标吸附」共用它;判定完全不看绘制方式 ⇒ 即使用户手动改过显示样式也照样吸附。
- 鼠标下方网格内既有符合又有不符合的信号时,只有符合的那条的数据点参与吸附;没有符合的则自由移动。
- 删除旧的整屏判据 SignalSnapEnabled 与 kSnapVisibleCellsPerPoint(死代码)。
三、「回到最上方」按钮淡出与「浮动提示」一致
kBackToTopFadeOutMs = kHintFadeMs(500ms)+ 线性 alpha,到达顶部立即开始淡出(不停留)。
淡入保留 170ms ease-out。实测 50% 时刻 alpha = 0.500(线性),旧 ease-out 会是 0.875。
四、快速连点跟手(根因:CS_DBLCLKS 合成 WM_LBUTTONDBLCLK 导致按下丢失)
组件各自实现 DoubleClickIsPlainPress(x, y)(该落点有没有双击语义),宿主 WM_LBUTTONDBLCLK 据此
选择走 OnLButtonDown(当作按下)还是 OnDoubleClick:
- PlotView:浮动面板(含复选框/滑块/复位/✕/标题栏)、「回到最上方」按钮、绘图区滚动条、GX 徽标 ⇒ 按下;
时间光标线/读数标签/图例条目名/绘图区空白 ⇒ 保留双击。
- SignalPanel:样式区数值框、「目标」行名称与 GX 标签、行名称区域 ⇒ 保留双击;其余全部 ⇒ 按下。
- 弹出菜单 / 拾色器打开时,WM_LBUTTONDBLCLK 也按各自 OnLButtonDown 走。
五、分组行:名称恢复单击展开收起 + 只有快速双击进入改名
- OnLButtonDown 分组分支不再排除 RowPart::BODY:三角 /「组X」/ 名称本体统一折叠展开。
- OnDoubleClick 名称区域:只有快速双击(kGroupRenameDoubleClickMs=300,比系统 500ms 紧)才进入改名;
超阈值的「慢双击」只当又一次单击;快速双击前先撤销按下那一次的折叠切换,保证进编辑时状态与点击前一致。
六、信号重现时网格滚动回到最上方
“有过需滚动条的网格 → 没有任何信号显示 → 又出现需滚动条数量的网格”这一类情形必须从最上方开始看。
修法:Application::SynchronizeView 的「首次出现可见信号」分支里,在 FitAll() 之后 tab.LaneScroll() = 0。
验证(全部离屏,不占用鼠标/键盘/屏幕)
- grabdanim(.preflight/perf/gridanim_main.cpp)93/93:真实消息循环帧距 + 逐帧渲染耗时(与手动拖滚动条
对比)、光标吸附新判定、按钮淡出线性、快速连点路由、分组名单击/双击/慢双击、滚动位置复位。
- 既有回归:verify 49/49、panelanim 158/158(按新分组名规则更新 8 条断言)、datacheck 69/69、
edgecheck 46/46、errcheck 19/19。
- make release / make debug 零错误零警告(用 TARGET_FILE_NAME=TargetCheck/TargetCheckD 避免与正在运行的实例冲突)。
文档:Documents/agent_mem.md 新增 §9.43、§11.2 第 125–133 条与变更日志一行。
This commit is contained in:
1 parent
34375481de
commit
436dcbfd52
12 files changed
+618
-182
No files matched your search
@@ -736,76 +736,8 @@ namespace jdi
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PlotView::SignalSnapEnabled(const TimeAxis& axis, const SignalData& signal,
|
||||
double timeShift) const
|
||||
{
|
||||
const int cells = GridMinorCellCount();
|
||||
if (cells <= 0 || signal.finiteCount == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
const std::size_t threshold =
|
||||
static_cast<std::size_t>(cells) * static_cast<std::size_t>(kSnapVisibleCellsPerPoint);
|
||||
if (threshold == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::size_t count = std::min(axis.time.size(), signal.values.size());
|
||||
if (count == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
std::size_t begin = 0;
|
||||
std::size_t end = 0;
|
||||
VisibleIndexRange(axis, count, begin, end, timeShift);
|
||||
if (end <= begin)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// ⚠️ 视界内**至少要有一个可见数据点**:一个数据点都不在屏幕上的信号既没有可吸附的
|
||||
// 对象、也没有可提示的内容。少了这一条,“数据点全都落在视界之外”的信号会被判成
|
||||
// “稀疏”(0 < N×4)而被自动切成「点线」,属于无意义的状态抖动。
|
||||
if (signal.finiteCount < threshold)
|
||||
{
|
||||
// 有效点总数本来就不足阈值 ⇒ 只需确认视界内确实有点(稀疏点已缓存时二分判定)。
|
||||
return HasVisibleSample(axis, signal, begin, end, timeShift);
|
||||
}
|
||||
|
||||
// 一般信号:统计**当前视界内**的有效点数量(攒够阈值立刻早退,代价有界)。
|
||||
// 连续 NaN 不计入有效点,因此再给一个**有界**的扫描窗口,防止极端情况下长循环。
|
||||
const std::size_t window =
|
||||
std::min(std::min(end, signal.values.size()), begin + threshold * kSnapScanWindowFactor + 8u);
|
||||
std::size_t seen = 0;
|
||||
for (std::size_t index = begin; index < window; ++index)
|
||||
{
|
||||
if (!std::isfinite(signal.values[index]))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
// ⚠️ 必须按**绘制时刻**判断可见性(`t + offsetTime`),否则存在时间轴偏移时
|
||||
// 会把“已经移出视界”的采样点算进来(用户报告吸附到历史位置)。
|
||||
const double time = axis.time[index];
|
||||
if (!std::isfinite(time))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const double displayTime = time + timeShift;
|
||||
if (displayTime < Tab().ViewMin() || displayTime > Tab().ViewMax())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (++seen >= threshold)
|
||||
{
|
||||
return false; // 一屏内的数据点已经足够密:保持自由移动
|
||||
}
|
||||
}
|
||||
return seen > 0;
|
||||
}
|
||||
|
||||
bool PlotView::AutoPointLineNeeded(const TimeAxis& axis, const SignalData& signal,
|
||||
double timeShift) const
|
||||
bool PlotView::MinorCellSparse(const TimeAxis& axis, const SignalData& signal,
|
||||
double timeShift) const
|
||||
{
|
||||
const int cells = GridMinorCellCount();
|
||||
if (cells <= 0 || signal.finiteCount == 0)
|
||||
@@ -839,7 +771,7 @@ namespace jdi
|
||||
}
|
||||
// 鸽巢原理:有效点总数都不足“每格 5 个” ⇒ 必然存在一个稀疏小格(O(1) 充分判定)。
|
||||
const std::size_t need = static_cast<std::size_t>(cells) *
|
||||
static_cast<std::size_t>(kAutoPointLineMinPointsPerCell);
|
||||
static_cast<std::size_t>(kSparseMinPointsPerCell);
|
||||
if (signal.finiteCount < need)
|
||||
{
|
||||
return true;
|
||||
@@ -864,7 +796,7 @@ namespace jdi
|
||||
std::lower_bound(axis.time.begin(), axis.time.end(), cellMin) - axis.time.begin());
|
||||
const std::size_t hi = static_cast<std::size_t>(
|
||||
std::lower_bound(axis.time.begin(), axis.time.end(), cellMax) - axis.time.begin());
|
||||
const std::size_t cap = std::min(hi, lo + kAutoPointLineCellScanCap);
|
||||
const std::size_t cap = std::min(hi, lo + kSparseCellScanCap);
|
||||
std::size_t valid = 0;
|
||||
for (std::size_t index = lo; index < cap; ++index)
|
||||
{
|
||||
@@ -872,12 +804,12 @@ namespace jdi
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (++valid >= static_cast<std::size_t>(kAutoPointLineMinPointsPerCell))
|
||||
if (++valid >= static_cast<std::size_t>(kSparseMinPointsPerCell))
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (valid < static_cast<std::size_t>(kAutoPointLineMinPointsPerCell))
|
||||
if (valid < static_cast<std::size_t>(kSparseMinPointsPerCell))
|
||||
{
|
||||
return true; // 该小格稀疏 ⇒ 自动切「点线」
|
||||
}
|
||||
@@ -888,7 +820,7 @@ namespace jdi
|
||||
// 非单调时间轴:一次有界扫描,按小格分桶计数。
|
||||
std::vector<int> buckets(static_cast<std::size_t>(cells), 0);
|
||||
const std::size_t scanLimit =
|
||||
kAutoPointLineCellScanCap * static_cast<std::size_t>(cells);
|
||||
kSparseCellScanCap * static_cast<std::size_t>(cells);
|
||||
std::size_t scanned = 0;
|
||||
for (std::size_t index = begin; index < end; ++index)
|
||||
{
|
||||
@@ -915,14 +847,14 @@ namespace jdi
|
||||
cell = cells - 1;
|
||||
}
|
||||
const std::size_t slot = static_cast<std::size_t>(cell);
|
||||
if (buckets[slot] < kAutoPointLineMinPointsPerCell)
|
||||
if (buckets[slot] < kSparseMinPointsPerCell)
|
||||
{
|
||||
++buckets[slot];
|
||||
}
|
||||
}
|
||||
for (int cell = 0; cell < cells; ++cell)
|
||||
{
|
||||
if (buckets[static_cast<std::size_t>(cell)] < kAutoPointLineMinPointsPerCell)
|
||||
if (buckets[static_cast<std::size_t>(cell)] < kSparseMinPointsPerCell)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
@@ -1073,9 +1005,11 @@ namespace jdi
|
||||
// 少了这一步,信号一旦设置过「调整 → 时间轴偏移」,吸附就会落在**历史位置**
|
||||
// (用户 2026-10-15 报告“存在时间轴偏移调整时无法吸附到正确位置”)。
|
||||
const double timeShift = (style != nullptr) ? style->offsetTime : 0.0;
|
||||
// 只有“一屏内数据点少于 N×4”的信号参与吸附;判据随视界实时重算,
|
||||
// 因此疏密变化的信号在稀疏区自动吸附、移到稠密区自动恢复自由移动。
|
||||
if (!SignalSnapEnabled(*axis, *signal, timeShift))
|
||||
// ⚠️ 判定与「放大自动切点线」**完全同源**(用户 2026-10-17 明确要求“同样的状态判定”):
|
||||
// 只要存在任何一个小格内的数据点少于 5 个就参与吸附;
|
||||
// 判定**不看绘制方式** ⇒ 即使用户手动改过显示样式,该吸附的照样吸附。
|
||||
// 网格内既有符合又有不符合的信号时,只有符合的那条的数据点进入比较(用户要求)。
|
||||
if (!MinorCellSparse(*axis, *signal, timeShift))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
@@ -3106,8 +3040,8 @@ namespace jdi
|
||||
// **只要存在任何一个小格内的数据点少于 5 个**就自动切「点线」(局部稀疏即提示);
|
||||
// 不再满足时(适应视图、或把视界移到信号的稠密区)自动恢复原样式。
|
||||
// ⚠️ 可见性同样按**显示时间**判断(含 `offsetTime`)。
|
||||
// ⚠️ 「光标吸附」仍用整屏判据(`SignalSnapEnabled`),用户明确要求“其它规则不变”。
|
||||
if (AutoPointLineNeeded(*axis, *data, style->offsetTime))
|
||||
// ⚠️ 「光标吸附」用的是**同一个判定**(`MinorCellSparse`),用户要求两者“同样的状态判定”。
|
||||
if (MinorCellSparse(*axis, *data, style->offsetTime))
|
||||
{
|
||||
if (!style->autoScatterActive)
|
||||
{
|
||||
|
||||
Reference in new issue
Block a user