Prepare spanning-block continuation spread in background (kill post-flip redraw)

For a paragraph that overflows onto the next spread, the continuation page was redrawn
synchronously after the flip (drawSpread on the main thread), so the next page stayed
blank for ~2.7s and then the carried-over lines popped in already ~24% revealed instead
of animating from the start.

Move that work off the critical path: during lookahead, prepare and cache the
continuation spread's reveal plan using the not-yet-committed preview spreads (so per-line
timing is computed across both spreads), then reuse it after the flip instead of redrawing.

- pagination: expose the preview spread layout on the returned preview spread so the owner
  can detect the continuation spread (race-free; each call owns its preparedSpreads).
- renderer: revealSpreadSourceOverride lets region collection use preview spreads during
  lookahead; prepareContinuationRevealPlan draws+caches the continuation plan (publishEvent
  off); takeContinuationRevealPlan reuses it, re-stamped as an activate-phase publish.
- timeline: prepare the continuation plan during background (non-immediate) prepares;
  revealContinuationSpread reuses it, falling back to the redraw when none was prepared.

Verified live on a spanning block: continuation now appears ~0.25s after the flip (was
~2.7s) at ve~3471 = the right line's duration, i.e. it animates from the start (no pop-in),
runs to ~full over the TTS, no fast-forward, no continuation-missing problems.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-19 00:04:21 +02:00
parent e72594b3ff
commit 47af10d60c
4 changed files with 117 additions and 7 deletions
+25 -2
View File
@@ -212,6 +212,26 @@ class BookPlaybackTimelineModule extends BaseModule {
publishEvent: false
});
// If this block overflows onto the next spread, prepare that continuation spread's
// reveal plan now (lookahead/background) so revealContinuationSpread reuses it after the
// flip instead of redrawing synchronously. Only during background prepares and when the
// preview layout is available; otherwise the continuation falls back to the redraw path.
if (options.immediate !== true && Array.isArray(previewSpread.previewSpreads)) {
const startIndex = Math.max(0, Number(previewSpread.index || 0));
const continuationSpread = previewSpread.previewSpreads
.filter(spread => spread
&& Number(spread.index) > startIndex
&& this.getBlockRevealSides(spread, sentence.blockId).length > 0)
.sort((a, b) => Number(a.index) - Number(b.index))[0] || null;
if (continuationSpread) {
this.textureRenderer.prepareContinuationRevealPlan?.({
...revealDetail,
previewSpreads: previewSpread.previewSpreads,
continuationSpread
});
}
}
const targetSpreadIndex = Math.max(0, Number(previewSpread.index || 0));
const currentSpreadIndex = this.getVisibleSpreadIndex();
const revealSides = this.getBlockRevealSides(previewSpread, sentence.blockId);
@@ -477,8 +497,11 @@ class BookPlaybackTimelineModule extends BaseModule {
async revealContinuationSpread(segment = {}, spread = null) {
const sentence = segment.sentence;
if (!sentence || !spread) return false;
const revealDetail = this.createRevealDetail(sentence, spread, 'activate');
const texturePlan = this.textureRenderer.prepareRevealBlock(revealDetail, { publishEvent: false });
// Reuse the continuation plan prepared during lookahead (no synchronous redraw on the
// critical path). Falls back to rebuilding the plan if none was prepared.
const reused = this.textureRenderer.takeContinuationRevealPlan?.(segment.blockId, spread.index);
const texturePlan = reused
|| this.textureRenderer.prepareRevealBlock(this.createRevealDetail(sentence, spread, 'activate'), { publishEvent: false });
if (!texturePlan) {
this.pageCache?.recordProblem?.({
type: 'timeline-reveal-continuation-missing',