网格底衬永不空白 + 动画流畅性根治 + 光标随首个信号重现 + 「放大自动点线」新判据
一、网格底衬消失(半屏/局部/全屏,且要等一会才刷新好)—— 两个机制叠加 1) BlitWideBuffer 对「跨度已过期」的图层原用 StretchBlt 放大占位:深度放大时把一小段像素放大 几十倍 ⇒ 整块网格变成一片无网格的模糊色块(用户看到的就是「网格叠底消失、没有任何内容」)。 修法:删除占位拉伸;对拿不到(完整)图层像素的网格【就地补画网格底衬】 (新增 PaintLaneGridBacking + m_renderGridOnly,复用 PaintLane 的网格部分,跳过波形/散点/ 派生曲线/Y 轴刻度/图例)。底衬代价极低,可每帧执行 ⇒ 网格永不空白/闪烁。 ⚠️ 顺序:先画底衬(GDI+)再 g.GetHDC() 做 BitBlt;只有全部网格都贴不出像素时才交给整幅重绘。 2) 待重画队列按下标顺序消费(一次 1 个)⇒ 上百个网格时可视网格要等上百帧。 修法:RenderPendingLaneRebuilds 里做稳定划分,【可视网格优先】(LaneVisuallyRelevant); 「结算」判据由 LanesAllReadyForView() 改为 LanesVisiblyReadyForView()。 二、过渡动画卡顿/不平顺/直接闪到终态 —— 三处根因 1) 「m_layerRebuildPending 非空就每帧请求再送一帧」:上百个网格时 700ms 内挤出 124 次重绘, 而 WM_TIMER 优先级低于投递消息 ⇒ 动画定时器被饿死(实测 frames=124 / timerTicks=1)。 修法:新增 AnyVisibleLanePending(),只用「还有没有可视网格待补」决定是否请求再送一帧; 离屏网格改为机会式预热(实测 frames=25 / timerTicks=16)。 2) SetTimer 的间隔会被量化到系统 tick(默认 15.6ms):请求 16ms 实际等到下一个 tick ⇒ 实测帧距 ~26ms(只有约 38fps)。修法:kScrollTimerPeriodMs 16 → 10(实测 ~15.5ms ≈ 64fps; 真实消息循环帧距 ~25ms → ~16ms)。实测映射:8→15.8 / 10→15.5 / 12→16.5 / 16→26.4 / 31→37.9。 ⚠️ timeBeginPeriod(1) 在本机无效(已 A/B 验证)。 3) 网格滚动联动 / 「回到最上方」每帧都改 LaneScroll(),而它原本参与「宽基底可用性」判据 (SceneGeometryHash)⇒ 每一帧都退化成整幅重绘。修法:SceneContentHash 新增 includeLaneScroll 形参, 宽基底可用性判据不计网格滚动量/各网格屏幕纵向位置(它们只是图层的贴图落点); 静态场景缓存的指纹仍然计入。实测网格滚动动画单帧 18.36ms → 1.19ms。 三、光标工具激活且没有任何信号绘制时,首个信号出现后按规则重新出现(用户要求) 新增 PlotView::ResetCursorPlacementOnFreshContent()(规则与 CycleCursorMode() 的「首次出现」同源: 单光标置于视图中心;双光标第二条紧邻第一条且落在可见范围内),由 Application::SynchronizeView 的「首次出现可见信号」分支在 FitAll() 之后调用。 实测:单光标 stale=4.60 → 新的视图中心 20.92;双光标 stale=(2.89,7.18) → (20.92,26.07)。 四、「放大自动点线」规则改为:任一小格内数据点少于 5 个即自动切换(用户重新定义) 新增 PlotView::AutoPointLineNeeded()(推翻此前「一屏内数据点 < N×4」):按 GridMinorCellCount() 切成横向小格,只要存在任何一个小格内有效数据点 < kAutoPointLineMinPointsPerCell(=5) 就切「点线」。 实现:先「总有效点 < N×5」鸽巢 O(1) 充分判定;单调时间轴逐小格两次二分取区间(单格累计到 5 个即早退, 另给 kAutoPointLineCellScanCap=512 封顶);非单调时间轴一次有界扫描分桶。保留「视界内至少一个可见点」保护。 ⚠️ 光标吸附仍用旧的整屏判据 SignalSnapEnabled(用户明确要求「其它规则不变」)。 验证(全部离屏,不占用鼠标/键盘/屏幕) - 新增 .preflight/perf/gridanim_main.cpp(gridanim.exe)55/55: [A] 缩放/适应视图/「预热未完成再滚动」等每个用例逐帧断言「每个可视网格都有网格底衬 (有竖直锐边、无模糊占位、无未绘制带)」;[B] 直接驱动 + 真实消息循环两条路径统计每帧耗时、 帧距均值/最大与进度单调性;[C] 光标重现;[D] 自动点线新判据(稠密不切、稀疏切换、恢复原样式)。 - 既有回归:verify 49/49、panelanim 156/156、datacheck 69/69、edgecheck 46/46、errcheck 19/19。 - make release 零错误零警告(使用 TARGET_FILE_NAME=TargetCheck 以避免与用户正在运行的实例冲突)。 文档:Documents/agent_mem.md 新增 §9.42、§11.2 第 117–124 条与变更日志一行。
This commit is contained in:
1 parent
f845616938
commit
34375481de
7 files changed
+702
-168
No files matched your search
@@ -499,6 +499,41 @@ namespace jdi
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PlotView::ResetCursorPlacementOnFreshContent()
|
||||
{
|
||||
if (m_tab == nullptr || !Tab().CursorEnabled() || m_plotRect.Width <= 0.0f)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
// 与 `CycleCursorMode()` 的“光标首次出现”规则完全同源:
|
||||
// 单光标置于视图中心;双光标的第二条紧邻第一条、且保证落在可见时间范围内。
|
||||
const double low = Tab().ViewMin();
|
||||
const double high = Tab().ViewMax();
|
||||
const double span = (high > low) ? (high - low) : 1.0;
|
||||
const double center = (low + high) * 0.5;
|
||||
Tab().CursorTime() = center;
|
||||
if (Tab().CursorMode() == PlotCursorMode::DOUBLE)
|
||||
{
|
||||
const double offset = span * 0.12;
|
||||
double second = center + offset;
|
||||
if (second > high - span * 0.02)
|
||||
{
|
||||
second = center - offset;
|
||||
}
|
||||
if (second < low + span * 0.02 || second > high - span * 0.02)
|
||||
{
|
||||
second = low + span * 0.5;
|
||||
}
|
||||
Tab().CursorTimeAlt() = second;
|
||||
ClampCursorToView(2);
|
||||
}
|
||||
ClampCursorToView(1);
|
||||
m_cursorHover = true;
|
||||
m_hoverCursor = 1;
|
||||
m_activeCursor = 1;
|
||||
return true;
|
||||
}
|
||||
|
||||
void PlotView::SetHint(const std::wstring& text)
|
||||
{
|
||||
m_hintText = text;
|
||||
@@ -769,6 +804,132 @@ namespace jdi
|
||||
return seen > 0;
|
||||
}
|
||||
|
||||
bool PlotView::AutoPointLineNeeded(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 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;
|
||||
}
|
||||
// 视界内一个可见数据点都没有:既没有要提示的位置,也不该因为“小格全空”而无意义地抖动。
|
||||
if (!HasVisibleSample(axis, signal, begin, end, timeShift))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const double viewMin = Tab().ViewMin() - timeShift;
|
||||
const double viewMax = Tab().ViewMax() - timeShift;
|
||||
const double span = viewMax - viewMin;
|
||||
if (!(span > 0.0))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
// 鸽巢原理:有效点总数都不足“每格 5 个” ⇒ 必然存在一个稀疏小格(O(1) 充分判定)。
|
||||
const std::size_t need = static_cast<std::size_t>(cells) *
|
||||
static_cast<std::size_t>(kAutoPointLineMinPointsPerCell);
|
||||
if (signal.finiteCount < need)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
const double cellSpan = span / static_cast<double>(cells);
|
||||
if (!(cellSpan > 0.0) || !std::isfinite(cellSpan))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// 时间轴单调 ⇒ 每个小格用两次二分取出它的样本区间(代价与采样数无关)。
|
||||
if (axis.monotonic)
|
||||
{
|
||||
for (int cell = 0; cell < cells; ++cell)
|
||||
{
|
||||
const double cellMin = viewMin + cellSpan * static_cast<double>(cell);
|
||||
const double cellMax =
|
||||
(cell + 1 == cells)
|
||||
? viewMax
|
||||
: (viewMin + cellSpan * static_cast<double>(cell + 1));
|
||||
const std::size_t lo = static_cast<std::size_t>(
|
||||
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);
|
||||
std::size_t valid = 0;
|
||||
for (std::size_t index = lo; index < cap; ++index)
|
||||
{
|
||||
if (!std::isfinite(signal.values[index]))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (++valid >= static_cast<std::size_t>(kAutoPointLineMinPointsPerCell))
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (valid < static_cast<std::size_t>(kAutoPointLineMinPointsPerCell))
|
||||
{
|
||||
return true; // 该小格稀疏 ⇒ 自动切「点线」
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// 非单调时间轴:一次有界扫描,按小格分桶计数。
|
||||
std::vector<int> buckets(static_cast<std::size_t>(cells), 0);
|
||||
const std::size_t scanLimit =
|
||||
kAutoPointLineCellScanCap * static_cast<std::size_t>(cells);
|
||||
std::size_t scanned = 0;
|
||||
for (std::size_t index = begin; index < end; ++index)
|
||||
{
|
||||
if (++scanned > scanLimit)
|
||||
{
|
||||
break;
|
||||
}
|
||||
if (!std::isfinite(signal.values[index]))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const double time = axis.time[index];
|
||||
if (!std::isfinite(time) || time < viewMin || time > viewMax)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
int cell = static_cast<int>((time - viewMin) / cellSpan);
|
||||
if (cell < 0)
|
||||
{
|
||||
cell = 0;
|
||||
}
|
||||
if (cell >= cells)
|
||||
{
|
||||
cell = cells - 1;
|
||||
}
|
||||
const std::size_t slot = static_cast<std::size_t>(cell);
|
||||
if (buckets[slot] < kAutoPointLineMinPointsPerCell)
|
||||
{
|
||||
++buckets[slot];
|
||||
}
|
||||
}
|
||||
for (int cell = 0; cell < cells; ++cell)
|
||||
{
|
||||
if (buckets[static_cast<std::size_t>(cell)] < kAutoPointLineMinPointsPerCell)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool PlotView::NearestSnapSample(const TimeAxis& axis, const SignalData& signal, double time,
|
||||
double timeShift, double& candidate) const
|
||||
{
|
||||
@@ -2941,11 +3102,12 @@ namespace jdi
|
||||
style->autoScatterUserOverride = true;
|
||||
}
|
||||
|
||||
// 判定与**光标吸附完全同源**(用户 2026-10-15 明确要求):
|
||||
// 只有“一屏内数据点少于 N×4”的信号才自动切「点线」,
|
||||
// 判定改为**用户 2026-10-17 重新定义的新规则**:按缩放后的横向时间轴网格划分小格,
|
||||
// **只要存在任何一个小格内的数据点少于 5 个**就自动切「点线」(局部稀疏即提示);
|
||||
// 不再满足时(适应视图、或把视界移到信号的稠密区)自动恢复原样式。
|
||||
// ⚠️ 可见性同样按**显示时间**判断(含 `offsetTime`),与吸附保持一致。
|
||||
if (SignalSnapEnabled(*axis, *data, style->offsetTime))
|
||||
// ⚠️ 可见性同样按**显示时间**判断(含 `offsetTime`)。
|
||||
// ⚠️ 「光标吸附」仍用整屏判据(`SignalSnapEnabled`),用户明确要求“其它规则不变”。
|
||||
if (AutoPointLineNeeded(*axis, *data, style->offsetTime))
|
||||
{
|
||||
if (!style->autoScatterActive)
|
||||
{
|
||||
|
||||
Reference in new issue
Block a user