Fix WebGL page readiness gating
This commit is contained in:
@@ -32,6 +32,7 @@ class BookPlaybackTimelineModule extends BaseModule {
|
|||||||
'startRevealForSegment',
|
'startRevealForSegment',
|
||||||
'assertSegmentReady',
|
'assertSegmentReady',
|
||||||
'collectRequiredPageMetas',
|
'collectRequiredPageMetas',
|
||||||
|
'collectTexturePlanPageMetas',
|
||||||
'requiresSpreadTransition',
|
'requiresSpreadTransition',
|
||||||
'requiresRightPageFlipAfterReveal',
|
'requiresRightPageFlipAfterReveal',
|
||||||
'getBlockRevealSides',
|
'getBlockRevealSides',
|
||||||
@@ -375,7 +376,7 @@ class BookPlaybackTimelineModule extends BaseModule {
|
|||||||
targetSpread: options.targetSpread,
|
targetSpread: options.targetSpread,
|
||||||
endSpread: Math.max(0, Number(this.pagination?.spreads?.length || 1) - 1),
|
endSpread: Math.max(0, Number(this.pagination?.spreads?.length || 1) - 1),
|
||||||
getPageMetaForIndex: this.getPageMetaForIndex,
|
getPageMetaForIndex: this.getPageMetaForIndex,
|
||||||
recordMiss: true
|
recordMiss: false
|
||||||
});
|
});
|
||||||
await this.assertSegmentReady({
|
await this.assertSegmentReady({
|
||||||
blockId: options.blockId ?? null,
|
blockId: options.blockId ?? null,
|
||||||
@@ -410,31 +411,44 @@ class BookPlaybackTimelineModule extends BaseModule {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
collectRequiredPageMetas(segment = {}) {
|
collectRequiredPageMetas(segment = {}, phase = 'play') {
|
||||||
const spreads = new Set();
|
if (phase === 'prepare') {
|
||||||
|
return this.collectTexturePlanPageMetas(segment.preparedTexturePlan);
|
||||||
|
}
|
||||||
|
if (phase === 'activate' || phase === 'play') {
|
||||||
|
return this.collectTexturePlanPageMetas(segment.activeTexturePlan || segment.preparedTexturePlan);
|
||||||
|
}
|
||||||
const currentSpread = this.getVisibleSpreadIndex();
|
const currentSpread = this.getVisibleSpreadIndex();
|
||||||
const targetSpread = Number.isFinite(Number(segment.targetSpreadIndex))
|
const targetSpread = Number.isFinite(Number(segment.targetSpreadIndex))
|
||||||
? Math.max(0, Math.round(Number(segment.targetSpreadIndex)))
|
? Math.max(0, Math.round(Number(segment.targetSpreadIndex)))
|
||||||
: currentSpread;
|
: currentSpread;
|
||||||
spreads.add(0);
|
return Array.from(new Set([currentSpread, targetSpread]))
|
||||||
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 => [
|
.flatMap(spread => [
|
||||||
this.getPageMetaForIndex(spread * 2),
|
this.getPageMetaForIndex(spread * 2),
|
||||||
this.getPageMetaForIndex(spread * 2 + 1)
|
this.getPageMetaForIndex(spread * 2 + 1)
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
collectTexturePlanPageMetas(texturePlan = null) {
|
||||||
|
const pageMeta = texturePlan?.pageMeta || {};
|
||||||
|
const records = Array.isArray(texturePlan?.records) ? texturePlan.records : [];
|
||||||
|
const metas = records
|
||||||
|
.map(record => record?.pageMeta || pageMeta?.[record?.side])
|
||||||
|
.filter(meta => meta && Number.isFinite(Number(meta.pageIndex)));
|
||||||
|
['left', 'right'].forEach((side) => {
|
||||||
|
const meta = pageMeta?.[side];
|
||||||
|
if (!meta || !Number.isFinite(Number(meta.pageIndex))) return;
|
||||||
|
if (metas.some(existing => Number(existing.pageIndex) === Number(meta.pageIndex))) return;
|
||||||
|
metas.push(meta);
|
||||||
|
});
|
||||||
|
return metas;
|
||||||
|
}
|
||||||
|
|
||||||
async assertSegmentReady(segment = {}, phase = 'play') {
|
async assertSegmentReady(segment = {}, phase = 'play') {
|
||||||
if (!this.pageCache || typeof this.pageCache.ensurePageTexture !== 'function') {
|
if (!this.pageCache || typeof this.pageCache.ensurePageTexture !== 'function') {
|
||||||
throw new Error('BookPlaybackTimeline: Page texture cache is not available');
|
throw new Error('BookPlaybackTimeline: Page texture cache is not available');
|
||||||
}
|
}
|
||||||
const metas = this.collectRequiredPageMetas(segment);
|
const metas = this.collectRequiredPageMetas(segment, phase);
|
||||||
const missing = [];
|
const missing = [];
|
||||||
await Promise.all(metas.map(async (meta) => {
|
await Promise.all(metas.map(async (meta) => {
|
||||||
const texture = await this.pageCache.ensurePageTexture(meta, {
|
const texture = await this.pageCache.ensurePageTexture(meta, {
|
||||||
@@ -464,8 +478,19 @@ class BookPlaybackTimelineModule extends BaseModule {
|
|||||||
const spread = typeof this.pagination?.getSpread === 'function'
|
const spread = typeof this.pagination?.getSpread === 'function'
|
||||||
? this.pagination.getSpread(spreadIndex)
|
? this.pagination.getSpread(spreadIndex)
|
||||||
: this.pagination?.spreads?.[spreadIndex];
|
: this.pagination?.spreads?.[spreadIndex];
|
||||||
const source = spread?.pageMeta?.[side] || {};
|
|
||||||
const metrics = this.textureRenderer?.metrics || {};
|
const metrics = this.textureRenderer?.metrics || {};
|
||||||
|
if (!spread) {
|
||||||
|
return {
|
||||||
|
pageIndex: index,
|
||||||
|
width: metrics.width,
|
||||||
|
height: metrics.height,
|
||||||
|
kind: 'blank',
|
||||||
|
section: index < 3 ? 'frontmatter' : 'body',
|
||||||
|
pageNumber: null,
|
||||||
|
omitPageNumber: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const source = spread?.pageMeta?.[side] || {};
|
||||||
return {
|
return {
|
||||||
...source,
|
...source,
|
||||||
pageIndex: index,
|
pageIndex: index,
|
||||||
|
|||||||
+1
-1
@@ -24,7 +24,7 @@ const ModuleState = {
|
|||||||
ERROR: 'ERROR'
|
ERROR: 'ERROR'
|
||||||
};
|
};
|
||||||
|
|
||||||
const MODULE_CACHE_BUSTER = '20260610-book-timeline-b';
|
const MODULE_CACHE_BUSTER = '20260610-book-timeline-d';
|
||||||
window.MODULE_CACHE_BUSTER = MODULE_CACHE_BUSTER;
|
window.MODULE_CACHE_BUSTER = MODULE_CACHE_BUSTER;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { RenderPass } from 'https://esm.sh/three@0.165.0/examples/jsm/postproces
|
|||||||
import { SSAOPass } from 'https://esm.sh/three@0.165.0/examples/jsm/postprocessing/SSAOPass.js';
|
import { SSAOPass } from 'https://esm.sh/three@0.165.0/examples/jsm/postprocessing/SSAOPass.js';
|
||||||
import { SMAAPass } from 'https://esm.sh/three@0.165.0/examples/jsm/postprocessing/SMAAPass.js';
|
import { SMAAPass } from 'https://esm.sh/three@0.165.0/examples/jsm/postprocessing/SMAAPass.js';
|
||||||
import { OutputPass } from 'https://esm.sh/three@0.165.0/examples/jsm/postprocessing/OutputPass.js';
|
import { OutputPass } from 'https://esm.sh/three@0.165.0/examples/jsm/postprocessing/OutputPass.js';
|
||||||
import { PROCEDURAL_BOOK, createProceduralBookModel, snapProceduralPageCount } from './procedural-book-model.js?v=20260610-book-timeline-b';
|
import { PROCEDURAL_BOOK, createProceduralBookModel, snapProceduralPageCount } from './procedural-book-model.js?v=20260610-book-timeline-d';
|
||||||
|
|
||||||
const canvas = document.getElementById('scene');
|
const canvas = document.getElementById('scene');
|
||||||
canvas.style.cursor = 'grab';
|
canvas.style.cursor = 'grab';
|
||||||
@@ -2105,7 +2105,7 @@ function handlePageTextureRecords(event) {
|
|||||||
source: 'book-texture-renderer'
|
source: 'book-texture-renderer'
|
||||||
});
|
});
|
||||||
markPageTextureTiming('handlePageTextureRecords:end');
|
markPageTextureTiming('handlePageTextureRecords:end');
|
||||||
prewarmNavigationTextureWindow('page-texture-records').catch((error) => {
|
prewarmNavigationTextureWindow('page-texture-records', { recordMiss: false }).catch((error) => {
|
||||||
pageTextureStore?.recordProblem?.({
|
pageTextureStore?.recordProblem?.({
|
||||||
type: 'navigation-window-prewarm-error',
|
type: 'navigation-window-prewarm-error',
|
||||||
message: error?.message || String(error)
|
message: error?.message || String(error)
|
||||||
@@ -2300,7 +2300,7 @@ async function prewarmNavigationTextureWindow(reason = 'navigation-window', opti
|
|||||||
targetSpread: options.targetSpread,
|
targetSpread: options.targetSpread,
|
||||||
endSpread,
|
endSpread,
|
||||||
getPageMetaForIndex: makePageMetaForCache,
|
getPageMetaForIndex: makePageMetaForCache,
|
||||||
recordMiss: options.recordMiss !== false
|
recordMiss: options.recordMiss === true
|
||||||
});
|
});
|
||||||
markPageTextureTiming('textureStorePrewarm:end', {
|
markPageTextureTiming('textureStorePrewarm:end', {
|
||||||
reason,
|
reason,
|
||||||
|
|||||||
@@ -381,7 +381,7 @@ class WebGLPageCacheModule extends BaseModule {
|
|||||||
if (!Number.isFinite(pageIndex)) return null;
|
if (!Number.isFinite(pageIndex)) return null;
|
||||||
const key = this.makeResidentKey(pageMeta);
|
const key = this.makeResidentKey(pageMeta);
|
||||||
const resident = this.residentTextures.get(key);
|
const resident = this.residentTextures.get(key);
|
||||||
if (!resident || this.isOlderPageMeta(pageMeta, resident.pageMeta)) return null;
|
if (!resident) return null;
|
||||||
return this.getResidentTexture(pageMeta);
|
return this.getResidentTexture(pageMeta);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -189,7 +189,7 @@ const checks = [
|
|||||||
['texture renderer publishes both spread sides for reveal preparation and fallback start', /this\.drawSpread\(this\.currentSpread \|\| this\.pagination\?\.getCurrentSpread\?\.\(\), \['left', 'right'\]\)/.test(textureRendererSource) && /const sides = \['left', 'right'\]/.test(textureRendererSource) && /pageMeta: prepared\.pageMeta \|\| \{\}/.test(textureRendererSource)],
|
['texture renderer publishes both spread sides for reveal preparation and fallback start', /this\.drawSpread\(this\.currentSpread \|\| this\.pagination\?\.getCurrentSpread\?\.\(\), \['left', 'right'\]\)/.test(textureRendererSource) && /const sides = \['left', 'right'\]/.test(textureRendererSource) && /pageMeta: prepared\.pageMeta \|\| \{\}/.test(textureRendererSource)],
|
||||||
['texture renderer marks page canvases with content versions before cache writes', /pageContentVersions/.test(textureRendererSource) && /buildPublishPageMeta/.test(textureRendererSource) && /completenessScore: \(maxBlockId \* 1000\) \+ lineCount/.test(textureRendererSource)],
|
['texture renderer marks page canvases with content versions before cache writes', /pageContentVersions/.test(textureRendererSource) && /buildPublishPageMeta/.test(textureRendererSource) && /completenessScore: \(maxBlockId \* 1000\) \+ lineCount/.test(textureRendererSource)],
|
||||||
['texture store queues newer same-page cache writes instead of dropping them', /storePageCanvas/.test(webglPageCacheSource) && /isOlderPageMeta/.test(webglPageCacheSource) && /const previousWrite = pending\?\.promise \|\| Promise\.resolve\(\)/.test(webglPageCacheSource) && /pendingPageWrites\.set\(key, \{[\s\S]*pageMeta: \{ \.\.\.\(pageMeta \|\| \{\}\) \}/.test(webglPageCacheSource)],
|
['texture store queues newer same-page cache writes instead of dropping them', /storePageCanvas/.test(webglPageCacheSource) && /isOlderPageMeta/.test(webglPageCacheSource) && /const previousWrite = pending\?\.promise \|\| Promise\.resolve\(\)/.test(webglPageCacheSource) && /pendingPageWrites\.set\(key, \{[\s\S]*pageMeta: \{ \.\.\.\(pageMeta \|\| \{\}\) \}/.test(webglPageCacheSource)],
|
||||||
['webgl texture store resident cache rejects older page versions before direct reuse', /isOlderPageMeta/.test(webglPageCacheSource) && /getResidentTextureForMeta/.test(webglPageCacheSource) && /getResidentTextureForMeta\(pageMeta\)/.test(webglPageCacheSource)],
|
['webgl texture store resident cache reuses newest page version for older readiness requests', /isOlderPageMeta/.test(webglPageCacheSource) && /getResidentTextureForMeta/.test(webglPageCacheSource) && /if \(!resident\) return null/.test(webglPageCacheSource) && !/if \(!resident \|\| this\.isOlderPageMeta\(pageMeta, resident\.pageMeta\)\) return null/.test(webglPageCacheSource)],
|
||||||
['webgl flip page is a two-sided body using paper-thickness constants', /flipPageBackSurface/.test(source) && /flipPageEdge/.test(source) && /new THREE\.Mesh\(geometry, \[\s*materials\.flipPageSurface,\s*materials\.flipPageBackSurface,\s*materials\.flipPageEdge\s*\]\)/.test(source) && /PROCEDURAL_BOOK\.SHEET_THICKNESS_MODEL/.test(source) && /geometry\.addGroup\(0, topIndices\.length, 0\)/.test(source)],
|
['webgl flip page is a two-sided body using paper-thickness constants', /flipPageBackSurface/.test(source) && /flipPageEdge/.test(source) && /new THREE\.Mesh\(geometry, \[\s*materials\.flipPageSurface,\s*materials\.flipPageBackSurface,\s*materials\.flipPageEdge\s*\]\)/.test(source) && /PROCEDURAL_BOOK\.SHEET_THICKNESS_MODEL/.test(source) && /geometry\.addGroup\(0, topIndices\.length, 0\)/.test(source)],
|
||||||
['webgl animated page front and back maps are independently switchable before animation starts', /materials\.flipPageBackSurface = materials\.flipPageSurface\.clone\(\)/.test(source) && /materials\.flipPageSurface\.map = sourceTexture/.test(source) && /materials\.flipPageBackSurface\.map = backTexture \|\| getBlankPageTexture\(\)/.test(source) && /flip\.sourceTexture = sourceTexture/.test(source) && /flip\.backTexture = backTexture \|\| getBlankPageTexture\(\)/.test(source)],
|
['webgl animated page front and back maps are independently switchable before animation starts', /materials\.flipPageBackSurface = materials\.flipPageSurface\.clone\(\)/.test(source) && /materials\.flipPageSurface\.map = sourceTexture/.test(source) && /materials\.flipPageBackSurface\.map = backTexture \|\| getBlankPageTexture\(\)/.test(source) && /flip\.sourceTexture = sourceTexture/.test(source) && /flip\.backTexture = backTexture \|\| getBlankPageTexture\(\)/.test(source)],
|
||||||
['webgl flip preflight exposes texture side and orientation invariants for browser tests', /lastFlipTexturePreflight/.test(source) && /sourceTextureMatchesBackTexture/.test(source) && /targetBackSide/.test(source) && /getRuntimeInvariants/.test(source)],
|
['webgl flip preflight exposes texture side and orientation invariants for browser tests', /lastFlipTexturePreflight/.test(source) && /sourceTextureMatchesBackTexture/.test(source) && /targetBackSide/.test(source) && /getRuntimeInvariants/.test(source)],
|
||||||
@@ -217,7 +217,7 @@ const checks = [
|
|||||||
['3D display playback is owned by book playback timeline', /book-playback-timeline/.test(uiDisplayHandlerSource) && /playWebGLBookSentence/.test(uiDisplayHandlerSource) && /timeline\.playSentence\(sentence\)/.test(uiDisplayHandlerSource) && /if \(useWebGLBookReveal\) \{[\s\S]*await this\.playWebGLBookSentence\(sentence\);[\s\S]*this\.markBlockRendered\(sentence\.blockId\);[\s\S]*return null;/.test(uiDisplayHandlerSource)],
|
['3D display playback is owned by book playback timeline', /book-playback-timeline/.test(uiDisplayHandlerSource) && /playWebGLBookSentence/.test(uiDisplayHandlerSource) && /timeline\.playSentence\(sentence\)/.test(uiDisplayHandlerSource) && /if \(useWebGLBookReveal\) \{[\s\S]*await this\.playWebGLBookSentence\(sentence\);[\s\S]*this\.markBlockRendered\(sentence\.blockId\);[\s\S]*return null;/.test(uiDisplayHandlerSource)],
|
||||||
['sentence queue lookahead prepares 3D book timeline segments', /book-playback-timeline/.test(sentenceQueueSource) && /bookPlaybackTimeline\.prepareSentence\(sentence/.test(sentenceQueueSource) && /timelineSegment: segment/.test(sentenceQueueSource)],
|
['sentence queue lookahead prepares 3D book timeline segments', /book-playback-timeline/.test(sentenceQueueSource) && /bookPlaybackTimeline\.prepareSentence\(sentence/.test(sentenceQueueSource) && /timelineSegment: segment/.test(sentenceQueueSource)],
|
||||||
['book playback timeline prewarms texture window before prepared playback and flips', /prewarmSegmentTextures/.test(bookPlaybackTimelineSource) && /pageCache\.prewarmNavigationWindow/.test(bookPlaybackTimelineSource) && /this\.prewarmSegmentTextures\(segment\)/.test(bookPlaybackTimelineSource) && /await this\.pageCache\?\.prewarmNavigationWindow/.test(bookPlaybackTimelineSource)],
|
['book playback timeline prewarms texture window before prepared playback and flips', /prewarmSegmentTextures/.test(bookPlaybackTimelineSource) && /pageCache\.prewarmNavigationWindow/.test(bookPlaybackTimelineSource) && /this\.prewarmSegmentTextures\(segment\)/.test(bookPlaybackTimelineSource) && /await this\.pageCache\?\.prewarmNavigationWindow/.test(bookPlaybackTimelineSource)],
|
||||||
['book playback timeline enforces resident page textures before prepared playback', /assertSegmentReady/.test(bookPlaybackTimelineSource) && /collectRequiredPageMetas/.test(bookPlaybackTimelineSource) && /this\.pageCache\.ensurePageTexture\(meta/.test(bookPlaybackTimelineSource) && /timeline-cache-readiness-failed/.test(bookPlaybackTimelineSource)],
|
['book playback timeline enforces resident page textures before prepared playback', /assertSegmentReady/.test(bookPlaybackTimelineSource) && /collectRequiredPageMetas/.test(bookPlaybackTimelineSource) && /collectTexturePlanPageMetas/.test(bookPlaybackTimelineSource) && /this\.pageCache\.ensurePageTexture\(meta/.test(bookPlaybackTimelineSource) && /timeline-cache-readiness-failed/.test(bookPlaybackTimelineSource) && !/spreads\.add\(currentSpread \+ 1\)/.test(bookPlaybackTimelineSource)],
|
||||||
['3D reveal start is controlled by the prepared timeline instead of texture events', /sentence\.webglRevealController = \(\) => this\.startRevealForSegment\(segment\)/.test(bookPlaybackTimelineSource) && /startPreparedRevealAnimation\?\.\(segment\.blockId, \{[\s\S]*publishEvent: false/.test(bookPlaybackTimelineSource) && /PlaybackCoordinator: WebGL playback requires a prepared timeline reveal controller/.test(playbackCoordinatorSource) && !/document\.dispatchEvent\(new CustomEvent\('book-texture:reveal-block'/.test(methodBody(playbackCoordinatorSource, 'scheduleWebGLReveal'))],
|
['3D reveal start is controlled by the prepared timeline instead of texture events', /sentence\.webglRevealController = \(\) => this\.startRevealForSegment\(segment\)/.test(bookPlaybackTimelineSource) && /startPreparedRevealAnimation\?\.\(segment\.blockId, \{[\s\S]*publishEvent: false/.test(bookPlaybackTimelineSource) && /PlaybackCoordinator: WebGL playback requires a prepared timeline reveal controller/.test(playbackCoordinatorSource) && !/document\.dispatchEvent\(new CustomEvent\('book-texture:reveal-block'/.test(methodBody(playbackCoordinatorSource, 'scheduleWebGLReveal'))],
|
||||||
['webgl lab delegates right-page reveal commits to timeline owner', /BookPlaybackTimeline\?\.ownsPageFlipCommit === true/.test(source) && /handleRevealCommittedForPageFlip/.test(source)],
|
['webgl lab delegates right-page reveal commits to timeline owner', /BookPlaybackTimeline\?\.ownsPageFlipCommit === true/.test(source) && /handleRevealCommittedForPageFlip/.test(source)],
|
||||||
['webgl reveal clock explicitly freezes during physical flips', /pageRevealFreezeAt/.test(source) && /state\.startedAt \+= frozenMs/.test(source) && /activeRevealBlockStarts\.set\(blockId, Number\(value\) \+ frozenMs\)/.test(source)],
|
['webgl reveal clock explicitly freezes during physical flips', /pageRevealFreezeAt/.test(source) && /state\.startedAt \+= frozenMs/.test(source) && /activeRevealBlockStarts\.set\(blockId, Number\(value\) \+ frozenMs\)/.test(source)],
|
||||||
|
|||||||
Reference in New Issue
Block a user