Enforce explicit WebGL book playback timeline
This commit is contained in:
@@ -28,6 +28,10 @@ class BookPlaybackTimelineModule extends BaseModule {
|
||||
'ensureAnimationTimings',
|
||||
'createPreparedSegment',
|
||||
'createRevealDetail',
|
||||
'applyTexturePlan',
|
||||
'startRevealForSegment',
|
||||
'assertSegmentReady',
|
||||
'collectRequiredPageMetas',
|
||||
'requiresSpreadTransition',
|
||||
'requiresRightPageFlipAfterReveal',
|
||||
'getBlockRevealSides',
|
||||
@@ -103,6 +107,7 @@ class BookPlaybackTimelineModule extends BaseModule {
|
||||
|
||||
await this.timeStage('activate', segment, () => this.activatePreparedSegment(segment, sentence));
|
||||
|
||||
sentence.webglRevealController = () => this.startRevealForSegment(segment);
|
||||
const visualPromise = this.waitForVisualCompletion(segment);
|
||||
const playbackPromise = this.timeStage('playback', segment, () => {
|
||||
return this.playbackCoordinator?.play?.(sentence) || Promise.resolve();
|
||||
@@ -146,7 +151,10 @@ class BookPlaybackTimelineModule extends BaseModule {
|
||||
if (!previewSpread) return null;
|
||||
|
||||
const revealDetail = this.createRevealDetail(sentence, previewSpread, 'prepare');
|
||||
this.textureRenderer.prepareRevealBlock(revealDetail, { phase: 'prepare' });
|
||||
const texturePlan = this.textureRenderer.prepareRevealBlock(revealDetail, {
|
||||
phase: 'prepare',
|
||||
publishEvent: false
|
||||
});
|
||||
|
||||
const targetSpreadIndex = Math.max(0, Number(previewSpread.index || 0));
|
||||
const currentSpreadIndex = this.getVisibleSpreadIndex();
|
||||
@@ -162,11 +170,14 @@ class BookPlaybackTimelineModule extends BaseModule {
|
||||
revealSides,
|
||||
requiresPreFlip: targetSpreadIndex > currentSpreadIndex,
|
||||
requiresRightFlip: revealSides.includes('right') && this.requiresRightPageFlipAfterReveal(previewSpread),
|
||||
preparedTexturePlan: texturePlan,
|
||||
preparedAt: performance.now(),
|
||||
status: 'prepared'
|
||||
};
|
||||
|
||||
this.applyTexturePlan(texturePlan, segment, 'prepare');
|
||||
await this.timeStage('texture-prewarm', segment, () => this.prewarmSegmentTextures(segment));
|
||||
await this.assertSegmentReady(segment, 'prepare');
|
||||
if (options.immediate !== true) {
|
||||
await new Promise(resolve => setTimeout(resolve, 0));
|
||||
}
|
||||
@@ -185,7 +196,12 @@ class BookPlaybackTimelineModule extends BaseModule {
|
||||
&& this.requiresRightPageFlipAfterReveal(segment.activeSpread || segment.previewSpread);
|
||||
|
||||
const revealDetail = this.createRevealDetail(sentence, segment.activeSpread || segment.previewSpread, 'activate');
|
||||
this.textureRenderer.prepareRevealBlock(revealDetail);
|
||||
const texturePlan = this.textureRenderer.prepareRevealBlock(revealDetail, {
|
||||
publishEvent: false
|
||||
});
|
||||
segment.activeTexturePlan = texturePlan;
|
||||
this.applyTexturePlan(texturePlan, segment, 'activate');
|
||||
await this.assertSegmentReady(segment, 'activate');
|
||||
segment.status = 'activated';
|
||||
this.recordDiagnostic('segment-activate:end', segment);
|
||||
return segment.activeSpread;
|
||||
@@ -210,6 +226,38 @@ class BookPlaybackTimelineModule extends BaseModule {
|
||||
};
|
||||
}
|
||||
|
||||
applyTexturePlan(texturePlan = null, segment = {}, phase = 'activate') {
|
||||
if (!texturePlan) {
|
||||
throw new Error(`BookPlaybackTimeline: Missing texture plan for block ${segment.blockId ?? 'unknown'} during ${phase}`);
|
||||
}
|
||||
if (typeof window.BookLabDebug?.applyPageTextureRecords !== 'function') {
|
||||
throw new Error('BookPlaybackTimeline: WebGL book lab cannot apply prepared texture plans');
|
||||
}
|
||||
window.BookLabDebug.applyPageTextureRecords({
|
||||
...texturePlan,
|
||||
phase: phase === 'prepare' ? 'prepare' : 'activate'
|
||||
});
|
||||
this.recordDiagnostic(`texture-plan-applied:${phase}`, segment);
|
||||
return true;
|
||||
}
|
||||
|
||||
startRevealForSegment(segment = {}) {
|
||||
if (!segment?.blockId) return false;
|
||||
const revealStart = this.textureRenderer?.startPreparedRevealAnimation?.(segment.blockId, {
|
||||
publishEvent: false
|
||||
});
|
||||
if (!revealStart) {
|
||||
throw new Error(`BookPlaybackTimeline: Prepared reveal animation is missing for block ${segment.blockId}`);
|
||||
}
|
||||
if (typeof window.BookLabDebug?.startRevealForBlock !== 'function') {
|
||||
throw new Error('BookPlaybackTimeline: WebGL book lab cannot start prepared reveals explicitly');
|
||||
}
|
||||
window.BookLabDebug.startRevealForBlock(segment.blockId);
|
||||
this.markBenchmark('reveal-start', segment);
|
||||
this.recordDiagnostic('reveal-started', segment);
|
||||
return true;
|
||||
}
|
||||
|
||||
requiresSpreadTransition(segment = {}) {
|
||||
return Math.max(0, Number(segment.targetSpreadIndex || 0)) > this.getVisibleSpreadIndex();
|
||||
}
|
||||
@@ -288,15 +336,20 @@ class BookPlaybackTimelineModule extends BaseModule {
|
||||
getPageMetaForIndex: this.getPageMetaForIndex,
|
||||
recordMiss: true
|
||||
});
|
||||
await this.assertSegmentReady({
|
||||
blockId: options.blockId ?? null,
|
||||
targetSpreadIndex: options.targetSpread,
|
||||
revealSides: []
|
||||
}, 'flip');
|
||||
const wait = this.waitForPageFlipFinished(options.targetSpread);
|
||||
document.dispatchEvent(new CustomEvent('webgl-book:request-page-flip', {
|
||||
detail: {
|
||||
direction,
|
||||
force: options.force === true,
|
||||
reason: options.reason || 'timeline',
|
||||
targetSpread: options.targetSpread
|
||||
}
|
||||
}));
|
||||
if (typeof window.BookLabDebug?.requestPageFlip !== 'function') {
|
||||
throw new Error('BookPlaybackTimeline: WebGL book lab cannot execute prepared flip plans');
|
||||
}
|
||||
window.BookLabDebug.requestPageFlip(direction, {
|
||||
force: options.force === true,
|
||||
reason: options.reason || 'timeline',
|
||||
targetSpread: options.targetSpread
|
||||
});
|
||||
return wait;
|
||||
}
|
||||
|
||||
@@ -316,6 +369,53 @@ class BookPlaybackTimelineModule extends BaseModule {
|
||||
return result;
|
||||
}
|
||||
|
||||
collectRequiredPageMetas(segment = {}) {
|
||||
const spreads = new Set();
|
||||
const currentSpread = this.getVisibleSpreadIndex();
|
||||
const targetSpread = Number.isFinite(Number(segment.targetSpreadIndex))
|
||||
? Math.max(0, Math.round(Number(segment.targetSpreadIndex)))
|
||||
: currentSpread;
|
||||
spreads.add(0);
|
||||
spreads.add(currentSpread);
|
||||
spreads.add(Math.max(0, currentSpread - 1));
|
||||
spreads.add(currentSpread + 1);
|
||||
spreads.add(targetSpread);
|
||||
if (segment.requiresRightFlip) spreads.add(targetSpread + 1);
|
||||
return Array.from(spreads)
|
||||
.filter(spread => spread >= 0)
|
||||
.flatMap(spread => [
|
||||
this.getPageMetaForIndex(spread * 2),
|
||||
this.getPageMetaForIndex(spread * 2 + 1)
|
||||
]);
|
||||
}
|
||||
|
||||
async assertSegmentReady(segment = {}, phase = 'play') {
|
||||
if (!this.pageCache || typeof this.pageCache.ensurePageTexture !== 'function') {
|
||||
throw new Error('BookPlaybackTimeline: Page texture cache is not available');
|
||||
}
|
||||
const metas = this.collectRequiredPageMetas(segment);
|
||||
const missing = [];
|
||||
await Promise.all(metas.map(async (meta) => {
|
||||
const texture = await this.pageCache.ensurePageTexture(meta, {
|
||||
recordMiss: true
|
||||
});
|
||||
if (!texture) missing.push(meta);
|
||||
}));
|
||||
if (missing.length > 0) {
|
||||
this.pageCache.recordProblem?.({
|
||||
type: 'timeline-cache-readiness-failed',
|
||||
phase,
|
||||
blockId: segment.blockId ?? null,
|
||||
missingPages: missing.map(meta => meta.pageIndex ?? null)
|
||||
});
|
||||
throw new Error(`BookPlaybackTimeline: Cache readiness failed during ${phase} for pages ${missing.map(meta => meta.pageIndex).join(', ')}`);
|
||||
}
|
||||
segment.cacheReady = true;
|
||||
segment.cacheReadyPhase = phase;
|
||||
this.recordDiagnostic(`cache-ready:${phase}`, segment);
|
||||
return true;
|
||||
}
|
||||
|
||||
getPageMetaForIndex(pageIndex = 0) {
|
||||
const index = Math.max(0, Math.round(Number(pageIndex || 0)));
|
||||
const spreadIndex = Math.floor(index / 2);
|
||||
|
||||
Reference in New Issue
Block a user