b0175b7cdc
Two robustness gaps from the worker migration, both raised in review: - The raster worker had no failure recovery: a thrown createImageBitmap/font error or a dropped message would leave the draw promise pending forever, stalling the serialized draw chain and hanging prepare/playback. Added worker.onerror and a per-job timeout; both settle the in-flight draw to a logged miss (texture-worker-error / -timeout) so the pipeline degrades to last-good per the spec instead of hanging. A single settleRasterization path clears the timer and resolves. - prepareSpreadTextureRecordsForFlip() called drawSpread() without awaiting it. That was safe when drawSpread was synchronous, but now that it is async (worker) the flip could race ahead of the worker draw and miss the resident texture. prewarmFlipTextures now awaits both spread draws before the resident-texture lookup. Suite 168 (added assertions for worker error/timeout recovery and the awaited prewarm). Normal draw path is behaviorally unchanged from the verified worker commit. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
267 lines
55 KiB
JavaScript
267 lines
55 KiB
JavaScript
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
const sourcePath = path.join(__dirname, '..', 'public', 'js', 'webgl-book-lab.js');
|
|
const source = fs.readFileSync(sourcePath, 'utf8');
|
|
const proceduralBookPath = path.join(__dirname, '..', 'public', 'js', 'procedural-book-model.js');
|
|
const proceduralBookSource = fs.readFileSync(proceduralBookPath, 'utf8');
|
|
const textureRendererPath = path.join(__dirname, '..', 'public', 'js', 'book-texture-renderer-module.js');
|
|
const textureRendererSource = fs.readFileSync(textureRendererPath, 'utf8');
|
|
const playbackCoordinatorPath = path.join(__dirname, '..', 'public', 'js', 'playback-coordinator-module.js');
|
|
const playbackCoordinatorSource = fs.readFileSync(playbackCoordinatorPath, 'utf8');
|
|
const uiDisplayHandlerPath = path.join(__dirname, '..', 'public', 'js', 'ui-display-handler-module.js');
|
|
const uiDisplayHandlerSource = fs.readFileSync(uiDisplayHandlerPath, 'utf8');
|
|
const bookPaginationPath = path.join(__dirname, '..', 'public', 'js', 'book-pagination-module.js');
|
|
const bookPaginationSource = fs.readFileSync(bookPaginationPath, 'utf8');
|
|
const sentenceQueuePath = path.join(__dirname, '..', 'public', 'js', 'sentence-queue-module.js');
|
|
const sentenceQueueSource = fs.readFileSync(sentenceQueuePath, 'utf8');
|
|
const storyHistoryPath = path.join(__dirname, '..', 'public', 'js', 'story-history-module.js');
|
|
const storyHistorySource = fs.readFileSync(storyHistoryPath, 'utf8');
|
|
const webglScenePath = path.join(__dirname, '..', 'public', 'js', 'webgl-book-scene-module.js');
|
|
const webglSceneSource = fs.readFileSync(webglScenePath, 'utf8');
|
|
const markupParserPath = path.join(__dirname, '..', 'public', 'js', 'markup-parser-module.js');
|
|
const markupParserSource = fs.readFileSync(markupParserPath, 'utf8');
|
|
const loaderPath = path.join(__dirname, '..', 'public', 'js', 'loader.js');
|
|
const loaderSource = fs.readFileSync(loaderPath, 'utf8');
|
|
const pageFormatPath = path.join(__dirname, '..', 'public', 'js', 'book-page-format-module.js');
|
|
const pageFormatSource = fs.readFileSync(pageFormatPath, 'utf8');
|
|
const stylePath = path.join(__dirname, '..', 'public', 'css', 'style.css');
|
|
const styleSource = fs.readFileSync(stylePath, 'utf8');
|
|
const optionsUiPath = path.join(__dirname, '..', 'public', 'js', 'options-ui-module.js');
|
|
const optionsUiSource = fs.readFileSync(optionsUiPath, 'utf8');
|
|
const persistencePath = path.join(__dirname, '..', 'public', 'js', 'persistence-manager-module.js');
|
|
const persistenceSource = fs.readFileSync(persistencePath, 'utf8');
|
|
const webglPageCachePath = path.join(__dirname, '..', 'public', 'js', 'webgl-page-cache-module.js');
|
|
const webglPageCacheSource = fs.readFileSync(webglPageCachePath, 'utf8');
|
|
const bookPlaybackTimelinePath = path.join(__dirname, '..', 'public', 'js', 'book-playback-timeline-module.js');
|
|
const bookPlaybackTimelineSource = fs.readFileSync(bookPlaybackTimelinePath, 'utf8');
|
|
const ttsFactoryPath = path.join(__dirname, '..', 'public', 'js', 'tts-factory-module.js');
|
|
const ttsFactorySource = fs.readFileSync(ttsFactoryPath, 'utf8');
|
|
const textureWorkerPath = path.join(__dirname, '..', 'public', 'js', 'book-texture-worker.js');
|
|
const textureWorkerSource = fs.readFileSync(textureWorkerPath, 'utf8');
|
|
|
|
function dependencyList(source, moduleId) {
|
|
const classStart = source.indexOf(`super('${moduleId}'`);
|
|
if (classStart < 0) return [];
|
|
const dependencyMatch = source.slice(classStart).match(/this\.dependencies\s*=\s*\[([^\]]*)\]/);
|
|
if (!dependencyMatch) return [];
|
|
return Array.from(dependencyMatch[1].matchAll(/'([^']+)'|"([^"]+)"/g)).map(match => match[1] || match[2]);
|
|
}
|
|
|
|
function directGetModules(source) {
|
|
return Array.from(source.matchAll(/getModule\('([^']+)'\)|getModule\("([^"]+)"\)/g)).map(match => match[1] || match[2]);
|
|
}
|
|
|
|
function undeclaredDirectDependencies(source, moduleId, optional = []) {
|
|
const declared = new Set(dependencyList(source, moduleId));
|
|
const allowed = new Set([moduleId, ...declared, ...optional]);
|
|
return Array.from(new Set(directGetModules(source))).filter(id => !allowed.has(id));
|
|
}
|
|
|
|
function cacheBuster(source) {
|
|
return source.match(/MODULE_CACHE_BUSTER\s*=\s*'([^']+)'/)?.[1] || null;
|
|
}
|
|
|
|
function methodBody(source, methodName) {
|
|
const start = source.indexOf(`${methodName}(`);
|
|
if (start < 0) return '';
|
|
const braceStart = source.indexOf('{', start);
|
|
if (braceStart < 0) return '';
|
|
let depth = 0;
|
|
for (let index = braceStart; index < source.length; index += 1) {
|
|
if (source[index] === '{') depth += 1;
|
|
if (source[index] === '}') {
|
|
depth -= 1;
|
|
if (depth === 0) return source.slice(braceStart + 1, index);
|
|
}
|
|
}
|
|
return '';
|
|
}
|
|
|
|
function sourceOrder(source, first, second) {
|
|
const firstIndex = source.indexOf(first);
|
|
const secondIndex = source.indexOf(second);
|
|
return firstIndex >= 0 && secondIndex >= 0 && firstIndex < secondIndex;
|
|
}
|
|
|
|
const checks = [
|
|
['scene-level SSAO import', /SSAOPass/.test(source)],
|
|
['postprocess anti-aliasing import', /SMAAPass/.test(source)],
|
|
['composer uses explicit render target', /new THREE\.WebGLRenderTarget\(1, 1/.test(source) && /new EffectComposer\(renderer, sceneComposerTarget\)/.test(source)],
|
|
['composer render path is active', /composer\.render\(\)/.test(source)],
|
|
['static table maps are loaded from disk', /table_normal_2k\.png/.test(source) && /table_dust_4k\.png/.test(source) && /table_grease_4k\.png/.test(source)],
|
|
['runtime table map generators removed from page', !/function createTableNormalTexture|function createTableDustTexture|function createTableGreaseTexture/.test(source)],
|
|
['table primitive shadow receiving disabled', /tableMesh\.receiveShadow = false/.test(source)],
|
|
['flames excluded from AO', /excludeFromAo = true/.test(source) && /aoExcludedObjects\.add\(child\)/.test(source)],
|
|
['AO pass hides excluded objects with cleanup', /sceneAoPass\.render = \(\.\.\.args\) =>/.test(source) && /finally/.test(source)],
|
|
['AO uses scene-scale sampling', /new SSAOPass\(scene, camera, 1, 1, 64\)/.test(source) && /sceneAoPass\.kernelRadius = 0\.48/.test(source) && /sceneAoPass\.minDistance = 0\.00025/.test(source) && /sceneAoPass\.maxDistance = 0\.065/.test(source)],
|
|
['AO debug shows blurred occlusion map', /tableDebugName === 'ao' && SSAOPass\.OUTPUT\?\.Blur/.test(source) && /sceneAoPass\.output = SSAOPass\.OUTPUT\.Blur/.test(source)],
|
|
['direct candle shadow lobe present', /candlePlanarShadowLobe/.test(source) && /candlePlanarShadowField/.test(source)],
|
|
['direct candle shadow contributes to final table shader', /max\(candleProjectedShadowField\(vTableWorldPosition\), candlePlanarShadowField\(vTableWorldPosition\)\)/.test(source) && /bookMeshShadowField\(vTableWorldPosition\)/.test(source)],
|
|
['book shadows use real light-space depth maps', /bookShadowTargets/.test(source) && /MeshDepthMaterial/.test(source) && /updateBookShadowMaps/.test(source) && /bookMeshShadowField/.test(source) && /bookShadowMaps\[0\]/.test(source)],
|
|
['book materials receive real shadow maps', /configureBookShadowReceiver\(materials\.leftPage/.test(source) && /bookReceiverShadowField/.test(source) && /bookShadowReceiverStrength/.test(source) && /configureMaterial\(material, part\)/.test(source)],
|
|
['book uses modular solved procedural body geometry', /createProceduralBookModel/.test(source) && /currentProceduralBookModel/.test(source) && /simulatePageLines/.test(proceduralBookSource) && /createLoftedLineBody/.test(proceduralBookSource) && /buildSupportSolvedLine/.test(proceduralBookSource)],
|
|
['proxy book shadow shortcuts are forbidden', !/bookPlanarShadowLobe|bookProjectedShadowField|bookBoxShadow|segmentBoxHit/.test(source)],
|
|
['final candle shadow is visible in composite', /candleOcclusion = clamp\(candleProjectedShadow \* 1\.46, 0\.0, 0\.82\)/.test(source) && /vec3\(0\.19, 0\.15, 0\.115\), candleOcclusion/.test(source)],
|
|
['primitive candle shadow shortcuts stay disabled', /wax\.castShadow = false/.test(source) && /wick\.castShadow = false/.test(source) && !/bookPlanarShadowLobe|bookProjectedShadowField|bookBoxShadow|segmentBoxHit/.test(source)],
|
|
['analytic contact fallback removed', !/surfaceContactOcclusion|candleContactField|candleContactOcclusion|bookContactField|candleFootOcclusion|contactAo/.test(source)],
|
|
['debug AO remains scene-level', /scene debug: SSAO/.test(source)],
|
|
['contact debug mode removed', !/contact:\s*9|tableDebugMode == 9/.test(source)],
|
|
['render readiness flag and cache key are exposed', /BookLabDebug\.ready/.test(source) && /BookLabDebug\.renderedFrames/.test(source) && /cacheKey: window\.MODULE_CACHE_BUSTER/.test(source)],
|
|
['3D playback bypasses DOM word animation scheduling', /isWebGLPlaybackMode/.test(playbackCoordinatorSource) && /if \(this\.isWebGLPlaybackMode\(\)\)/.test(playbackCoordinatorSource) && /scheduleWebGLReveal/.test(playbackCoordinatorSource)],
|
|
['3D UI defers rendered history mark until playback completes', /deferRenderedMark/.test(uiDisplayHandlerSource) && /prepareWebGLBookReveal/.test(uiDisplayHandlerSource) && /markBlockRendered\(sentence\.blockId/.test(uiDisplayHandlerSource)],
|
|
['pagination can build a pending unrendered 3D block', /preparePendingBlock/.test(bookPaginationSource) && /book-pagination:prepare-block/.test(bookPaginationSource)],
|
|
['texture renderer has separate prepare and start reveal phases', /prepareRevealBlock/.test(textureRendererSource) && /startPreparedRevealAnimation/.test(textureRendererSource) && /webgl-book:page-reveal-start/.test(textureRendererSource)],
|
|
['texture renderer publishes line reveal coordinates from final page layout', /buildRevealRegions/.test(textureRendererSource) && /lineRects/.test(textureRendererSource) && /getLineInkRect/.test(textureRendererSource) && /fixedDurationMs/.test(textureRendererSource)],
|
|
['texture renderer carries page side through reveal region normalization', /normalizeRevealRegion\(side, blockId, lineRecord/.test(textureRendererSource) && /normalizeRevealRegion\(side, blockId, lineRecord, x, y, width, height/.test(textureRendererSource) && /normalizeRevealRegion\(side, blockId, lineRecord, rect\.x, rect\.y, rect\.width, rect\.height/.test(textureRendererSource)],
|
|
['texture renderer does not call removed word reveal recorder', !/recordRevealRect/.test(textureRendererSource)],
|
|
['page reveal shader uses line coordinate mask instead of comparing page textures', /bookRevealRegionRects/.test(source) && /bookRevealRegionTimings/.test(source) && /bookRevealElapsedMs/.test(source) && !/texture2D\(bookRevealMap/.test(source)],
|
|
['page reveal shader keeps a fixed loop without dynamic break', /float enabled = step\(float\(i\) \+ 0\.5, float\(bookRevealRegionCount\)\)/.test(source) && !/if \(i >= bookRevealRegionCount\) break/.test(source)],
|
|
['texture renderer explicitly gates initial font before painting', /waitForTextureFonts/.test(textureRendererSource) && /ensureTextureFontFace/.test(textureRendererSource) && /FontFace\(family/.test(textureRendererSource) && /document\.fonts\.load\('72px "EB Garamond Initials"'\)/.test(textureRendererSource)],
|
|
['texture renderer no longer republishes stale scene-ready textures', !/addEventListener\(document, 'webgl-book:scene-ready'/.test(textureRendererSource) && !/handleSceneReady\(\)\s*{\s*this\.publishSpread\(\)/.test(textureRendererSource) && !/drawEmptySpread/.test(textureRendererSource)],
|
|
['prepared reveal never falls back to unmasked direct upload before shader compile', /pendingPageReveal/.test(source) && /applyPendingPageReveal/.test(source) && !/if \(!shader\?\.uniforms\) {\s*uploadPageTextureDirect\(side, sourceCanvas\)/.test(source)],
|
|
['ui display handler declares every direct module lookup', undeclaredDirectDependencies(uiDisplayHandlerSource, 'ui-display-handler').length === 0],
|
|
['webgl scene declares every direct module lookup', undeclaredDirectDependencies(webglSceneSource, 'webgl-book-scene').length === 0],
|
|
['loader cache key matches webgl procedural imports', cacheBuster(loaderSource) && source.includes(`procedural-book-model.js?v=${cacheBuster(loaderSource)}`) && proceduralBookSource.length > 0],
|
|
['webgl lab exposes loader timing diagnostics', /loaderTimings/.test(source) && /markLoaderTiming/.test(source) && /primeSceneForLoader/.test(source)],
|
|
['webgl lab records shader compile timing during loader prime', /markLoaderTiming\('shaderCompile:start'\)/.test(source) && /renderer\.compile\(scene, camera\)/.test(source) && /markLoaderTiming\('shaderCompile:end'\)/.test(source)],
|
|
['webgl lab sizes render targets before static loader prime', /await reportLabStep\(86, 'Preparing static shadow and mirror maps'\);\s*resize\(\);\s*primeSceneForLoader\(\);/.test(source) && /lastResizeWidth/.test(source) && /lastResizeHeight/.test(source)],
|
|
['webgl lab exposes reveal uniform diagnostics', /getRevealDebugState/.test(source) && /bookRevealActive/.test(source) && /bookRevealElapsedMs/.test(source) && /bookRevealRegionCount/.test(source)],
|
|
['webgl lab records page reveal clear reasons', /clearPageReveal\(side, reason/.test(source) && /webglRevealClearLog/.test(source)],
|
|
['webgl reveal clock starts on first render frame', /pendingStart/.test(source) && /state\.pendingStart/.test(source) && /state\.startedAt = now/.test(source)],
|
|
['webgl reveal visual clock is derived from absolute playback time', /visualElapsedMs/.test(source) && /activeRevealBlockStarts/.test(source) && /state\.visualElapsedMs = Math\.max\(0, now - state\.startedAt\)/.test(source) && !/revealFrameDeltaMs/.test(source)],
|
|
['webgl fast-forward accelerates reveal instead of clearing the mask immediately', /fastForwarding/.test(source) && /fastForwardDurationMs/.test(source) && !/clearPageReveal\(side, 'fast-forward'\)/.test(source)],
|
|
['webgl lab records page texture binding timings', /pageTextureTimings/.test(source) && /markPageTextureTiming/.test(source) && /webglPageTextureTimings/.test(source)],
|
|
['webgl lab binds visible page texture sources through the single texture store', /bindPageTextureSource/.test(source) && /bindVisibleTextureSource/.test(source) && /registerVisibleTexture/.test(webglPageCacheSource) && !/drawCanvasPageTexture/.test(methodBody(source, 'uploadPageTextureDirect')) && !/drawCanvasPageTexture/.test(methodBody(source, 'beginPageReveal'))],
|
|
['page texture dark-pixel sampling only runs in table debug mode', /function shouldSamplePageTextureDebug\(\)/.test(source) && /tableDebugMode !== tableDebugModes\.none/.test(source) && /shouldSamplePageTextureDebug\(\) \? countPageTextureDarkPixels\(canvas\) : null/.test(source)],
|
|
['texture renderer exposes reveal pipeline diagnostics', /pipelineTimings/.test(textureRendererSource) && /markPipelineTiming/.test(textureRendererSource) && /webglTexturePipeline/.test(textureRendererSource)],
|
|
['texture renderer records prepare draw publish and start reveal timing', /markPipelineTiming\('prepareRevealBlock:start'/.test(textureRendererSource) && /markPipelineTiming\('drawSpread:start'/.test(textureRendererSource) && /markPipelineTiming\('publishSpread'/.test(textureRendererSource) && /markPipelineTiming\('startPreparedRevealAnimation'/.test(textureRendererSource)],
|
|
['texture renderer diagnostics include reveal region counts', /regionCounts/.test(textureRendererSource) && /lineRects/.test(textureRendererSource) && /durationMs/.test(textureRendererSource)],
|
|
['texture renderer does not draw immediate non-pending sides during pending reveal preparation', !/immediateSides/.test(textureRendererSource) && !/this\.drawSpread\(this\.currentSpread, immediateSides\)/.test(textureRendererSource)],
|
|
['sentence queue consumes completed prepared lookahead items', /preparedSentenceCache/.test(sentenceQueueSource) && /this\.preparedSentenceCache\.get\(cacheKey\)/.test(sentenceQueueSource) && /return prefetched/.test(sentenceQueueSource)],
|
|
['sentence queue front-loads 3D book presentation before playback callback', /const sentence = await this\.getPreparedSentence\(item\);[\s\S]*await this\.prefetchWebGLBookPresentation\(sentence[\s\S]*immediate: true[\s\S]*this\.prefetchAhead\(6, queueGeneration\);[\s\S]*this\.onSentenceReadyCallback/.test(sentenceQueueSource)],
|
|
['sentence queue prefetch prepares whole future sentence instead of speech metadata only', /this\.prepareSentence\(nextItem, \{\s*blocking: false/.test(sentenceQueueSource) && /this\.prefetchWebGLBookPresentation\(prepared/.test(sentenceQueueSource)],
|
|
['sentence queue starts lookahead when items arrive during playback', /else \{\s*this\.prefetchAhead\(6, this\.queueGeneration\);/.test(sentenceQueueSource)],
|
|
['sentence queue keeps current 3D page prep immediate while future lookahead yields cooperatively', /if \(!options\.immediate\) \{[\s\S]*requestIdleCallback[\s\S]*timeout: 80/.test(sentenceQueueSource) && /prefetchAhead\(maxLookahead = 6/.test(sentenceQueueSource)],
|
|
['pagination can prepare future spreads without activating visible spread', /preparePendingBlock\(block = \{\}, options = \{\}\)/.test(bookPaginationSource) && /options\.activate !== false/.test(bookPaginationSource) && /includeUnrenderedHistory/.test(bookPaginationSource)],
|
|
['pagination preserves active inline style tags for texture lines', /getActiveStyleTags/.test(bookPaginationSource) && /activeStyleTags/.test(bookPaginationSource) && /updateStyleTagStack/.test(bookPaginationSource)],
|
|
['texture renderer stores prepared reveal plans in the shared texture store', !/preparedRevealCache/.test(textureRendererSource) && /rememberPreparedRevealPlan/.test(webglPageCacheSource) && /takePreparedRevealPlan/.test(textureRendererSource) && /publishPreparedReveal/.test(textureRendererSource) && !/hasPreparedRevealBlock/.test(textureRendererSource)],
|
|
['webgl page cache is loaded through module infrastructure', /webgl-page-cache-module\.js/.test(loaderSource) && /super\('webgl-page-cache'/.test(webglPageCacheSource) && /reportProgress\(20, 'Opening WebGL page texture cache'\)/.test(webglPageCacheSource)],
|
|
['webgl page cache uses an isolated browser database without upgrading tts history state', /this\.dbName = 'webglPageTextureCacheDB'/.test(webglPageCacheSource) && /this\.dbVersion = 1/.test(webglPageCacheSource) && /this\.dbVersion = 3/.test(ttsFactorySource) && /this\.dbVersion = 3/.test(storyHistorySource) && !/webglPageTextureStore/.test(ttsFactorySource) && !/webglPageTextureStore/.test(storyHistorySource)],
|
|
['texture renderer hands completed page canvases to the single texture store without owning write queues', /webgl-page-cache/.test(textureRendererSource) && /cachePublishedPages/.test(textureRendererSource) && /storePageCanvas\(pageMeta, canvas, \{ persist: true, resident: true \}\)/.test(textureRendererSource) && !/schedulePageCacheWrite/.test(textureRendererSource) && !/pendingPageCacheWrites/.test(textureRendererSource)],
|
|
['webgl texture store is non-optional with db memory cache prepared textures and vram cache', /maxCacheSizeBytes = 5 \* 1024 \* 1024 \* 1024/.test(webglPageCacheSource) && /maxMemoryCanvasCount = 256/.test(webglPageCacheSource) && /residentTextures = new Map/.test(webglPageCacheSource) && /preparedTextures = \{/.test(webglPageCacheSource) && /persistent page caching is in a problem state/.test(webglPageCacheSource) && !/if \(this\.memoryCanvasCache\.has\(key\)\) return true/.test(webglPageCacheSource)],
|
|
['webgl lab prewarms navigation texture window through single store before flips', /const maxResidentPageTextures = 192/.test(source) && /configureTextureRuntime/.test(source) && /prewarmNavigationTextureWindow/.test(source) && /await prewarmFlipTextures\(direction, targetSpread\)/.test(source) && /resolveFlipBackTexture\(targetBackPageMeta, prewarmedBackTexture\)/.test(source) && !/const residentPageTextures = new Map/.test(source)],
|
|
['webgl texture store records cache misses as problem states', /problemLog/.test(webglPageCacheSource) && /recordProblem/.test(webglPageCacheSource) && /db-cache-miss/.test(webglPageCacheSource) && /webglPageCacheProblems/.test(webglPageCacheSource)],
|
|
['webgl lab makes preload-only page canvases resident by explicit page metadata through store', /pageTextureStore\?\.preparePageTexture/.test(source) && /attachRevealPageMeta/.test(source) && source.includes('pageTextureStore?.rememberResidentTexture?.(effectivePageMeta.left, texture, detail.left, true)') && source.includes('pageTextureStore?.rememberResidentTexture?.(effectivePageMeta.right, texture, detail.right, true)')],
|
|
['webgl texture store keeps current visible page textures resident without disposing shared maps', /rememberResidentTexture\(pageMeta = \{\}, texture = null, sourceCanvas = null, ownsTexture = true\)/.test(webglPageCacheSource) && /ownsTexture/.test(webglPageCacheSource) && /if \(oldest\?\.ownsTexture\) oldest\.texture\?\.dispose\?\.\(\)/.test(webglPageCacheSource)],
|
|
['webgl lab reuses current-enough resident cached page textures via single store for direct stack switches', /uploadPageTextureDirect\(side, sourceCanvas, pageMeta = null\)/.test(source) && source.includes('pageTextureStore?.getResidentTextureForMeta?.(pageMeta)') && /usedResidentTexture/.test(source) && /uploadPageTextureDirect\('left', detail\.left, effectivePageMeta\.left\)/.test(source) && /uploadPageTextureDirect\('right', detail\.right, effectivePageMeta\.right\)/.test(source) && !/function getResidentPageTextureForMeta/.test(source)],
|
|
['webgl page cache preserves explicit cache keys across writes and reads', /cacheKey: pageMeta\.cacheKey/.test(webglPageCacheSource) && /makePageKey\(pageMeta\)/.test(webglPageCacheSource)],
|
|
['webgl page cache rejects older page versions for the same page key', /isOlderPageEntry/.test(webglPageCacheSource) && /contentVersion/.test(webglPageCacheSource) && /completenessScore/.test(webglPageCacheSource) && /if \(this\.isOlderPageEntry\(pageMeta, oldEntry\)\) return true/.test(webglPageCacheSource)],
|
|
['targeted page flips commit target spread before emitting finished event', /bookPaginationState = \{[\s\S]*spreadIndex: Math\.max\(0, Math\.round\(Number\(flip\.targetSpread\)\)\)[\s\S]*document\.dispatchEvent\(new CustomEvent\('webgl-book:page-flip-finished'/.test(source) && /targetSpread: Number\.isFinite\(Number\(flip\.targetSpread\)\)/.test(source)],
|
|
['webgl debug test hook awaits the same async page flip path', /startPageFlipForTest\(direction, options = \{\}\) \{[\s\S]*return startPageFlip\(direction, options\)/.test(source)],
|
|
['webgl debug test hook can deterministically finish an active page flip', /advancePageFlipForTest\(elapsedMs = normalFlipDuration \+ 16\)/.test(source) && /updateActiveFlips\(targetNow\)/.test(source)],
|
|
['sentence queue skips duplicate current-item 3D book presentation when reveal is cached', /isWebGLBookPresentationPrepared/.test(sentenceQueueSource) && /if \(!this\.isWebGLBookPresentationPrepared\(sentence\)\) \{\s*await this\.prefetchWebGLBookPresentation/.test(sentenceQueueSource) && /sentence\.webglBookPresentation = \{\s*prepared: true/.test(sentenceQueueSource)],
|
|
['3D overflow reveal commits the spread then requests a timeline flip via event before activating', /requiresSpreadTransition\(segment\)/.test(bookPlaybackTimelineSource) && /this\.commitSegmentSpread\(segment, sentence\)/.test(bookPlaybackTimelineSource) && /this\.requestPageFlip\(1, \{[\s\S]*targetSpread: segment\.targetSpreadIndex/.test(bookPlaybackTimelineSource) && /this\.activatePreparedSegment\(segment, sentence\)/.test(bookPlaybackTimelineSource) && /dispatchEvent\(new CustomEvent\('webgl-book:request-page-flip'/.test(bookPlaybackTimelineSource) && /addEventListener\('webgl-book:request-page-flip'/.test(source) && /startPageFlip\(direction, \{/.test(source)],
|
|
['texture worker paints inline bold and italic styles off the main thread', /getInlineStyleState/.test(textureWorkerSource) && /updateInlineStyleState/.test(textureWorkerSource) && /getCanvasFont/.test(textureWorkerSource) && /segment\.style/.test(textureWorkerSource) && !/drawLine\(ctx/.test(textureRendererSource)],
|
|
['texture renderer delegates page rasterization to an OffscreenCanvas worker and blits the result', /book-texture-worker\.js/.test(textureRendererSource) && /rasterizeSpread/.test(textureRendererSource) && /ctx\.drawImage\(result\.pageBitmap, 0, 0\)/.test(textureRendererSource) && /OffscreenCanvas/.test(textureWorkerSource) && /createImageBitmap/.test(textureWorkerSource)],
|
|
['texture renderer recovers from worker error/timeout so a draw promise never hangs the chain', /this\.rasterWorker\.onerror/.test(textureRendererSource) && /texture-worker-timeout/.test(textureRendererSource) && /settleRasterization/.test(textureRendererSource) && /clearTimeout\(pending\.timer\)/.test(textureRendererSource)],
|
|
['flip prewarm awaits the async worker draw before the resident-texture lookup', /await prepareSpreadTextureRecordsForFlip\(currentSpread\)/.test(source) && /await window\.BookTextureRenderer\.drawSpread\(spread, \['left', 'right'\]/.test(source)],
|
|
['webgl lab can preload page textures without swapping visible page material through texture store', /preparePageTexture\(side = 'left'/.test(webglPageCacheSource) && /takePreparedPageTexture\(side = 'left'/.test(webglPageCacheSource) && /renderer\.initTexture\(texture\)/.test(webglPageCacheSource) && /takePreparedPageTexture/.test(source) && !/const preparedPageTextures/.test(source)],
|
|
['webgl page text textures avoid mipmap generation', /function configurePageCanvasTexture/.test(source) && /texture\.minFilter = THREE\.LinearFilter/.test(source) && /texture\.generateMipmaps = false/.test(source)],
|
|
['webgl reveal shader masks against a base-page texture instead of flat color blocks', /bookRevealBaseMap/.test(source) && /bookRevealUseBaseMap/.test(source) && /revealBaseColor/.test(source) && /baseCanvas/.test(textureRendererSource)],
|
|
['webgl reveal shader masks antialiased ink and uses smooth line-dominant scan', /smoothstep\(0\.52, 0\.9, luminance\)/.test(source) && /local\.x \* 0\.96/.test(source) && /bookRevealSoftness = \{ value: 0\.025 \}/.test(source)],
|
|
['webgl reveal line timings use global area-weighted timing across split-page spreads', /assignRevealTiming/.test(textureRendererSource) && /sourceSpreads/.test(textureRendererSource) && /this\.pagination\?\.spreads/.test(textureRendererSource) && /spreadIndex/.test(textureRendererSource) && /totalDuration \* \(Math\.max\(1, region\.timingArea \|\| region\.area\) \/ totalArea\)/.test(textureRendererSource) && /durationMs: sideRegions\.reduce/.test(textureRendererSource)],
|
|
['webgl page format reduces only outer margins from previous value', /outerBaseIn: 0\.27/.test(pageFormatSource) && /outerThicknessFactor: 0\.015/.test(pageFormatSource) && /outerMaxIn: 0\.315/.test(pageFormatSource) && /innerBaseIn: 0\.42/.test(pageFormatSource)],
|
|
['webgl mode enlarges and inverts DOM overlay text without touching 2D mode', /body\.webgl-mode \{[\s\S]*font-size: 18px;/.test(styleSource) && /body\.webgl-mode \.choice-list \.choice-button/.test(styleSource) && /rgba\(246, 231, 201/.test(styleSource)],
|
|
['webgl choice overlay hides title clutter and prevents horizontal scrollbar', /body\.webgl-mode #page_left #game_title/.test(styleSource) && /body\.webgl-mode #page_left #start_prompt/.test(styleSource) && /overflow-x: hidden/.test(styleSource) && /book\.style\.width = 'min\(44rem/.test(webglSceneSource)],
|
|
['3D live text bypasses #page_right DOM rendering and uses book texture reveal directly', /const useWebGLBookReveal = this\.isWebGLMode\(\) && \(sentence\.kind === 'paragraph' \|\| sentence\.kind === 'heading'\)/.test(uiDisplayHandlerSource) && /if \(useWebGLBookReveal\) \{[\s\S]*await this\.prepareWebGLBookReveal\(sentence\);[\s\S]*await this\.playbackCoordinator\.play\(sentence\);[\s\S]*this\.markBlockRendered\(sentence\.blockId\);[\s\S]*return null;/.test(uiDisplayHandlerSource)],
|
|
['drop-cap remaining text does not reinsert discretionary hyphen markers', /extractRemainingLayoutText/.test(bookPaginationSource) && !bookPaginationSource.includes("fragments.push('|')")],
|
|
['drop-cap reservation keeps a normal text gap beside the initial', /measureDropCapReservation/.test(bookPaginationSource) && /measureNormalTextGap\(fontPx\)/.test(bookPaginationSource)],
|
|
['drop-cap reservation uses both ink bounds and font advance width', /const advanceWidth = metrics\.width \|\| 0/.test(bookPaginationSource) && /Math\.max\(inkRight, advanceWidth, lineHeightPx \* 1\.08\)/.test(bookPaginationSource)],
|
|
['webgl scene avoids duplicate initial texture publish', !/this\.triggerTextureRefresh\(\)/.test(methodBody(webglSceneSource, 'initializeScene'))],
|
|
['webgl scene does not republish 3D page textures from DOM refresh events', !/addEventListener\(document, 'story:turn-start', this\.triggerTextureRefresh\)/.test(webglSceneSource) && !/addEventListener\(document, 'story:turn-complete', this\.triggerTextureRefresh\)/.test(webglSceneSource) && !/addEventListener\(document, 'story:history-updated', this\.triggerTextureRefresh\)/.test(webglSceneSource) && !/addEventListener\(document, 'input', this\.triggerTextureRefresh/.test(webglSceneSource) && !/addEventListener\(document, 'change', this\.triggerTextureRefresh/.test(webglSceneSource)],
|
|
['webgl scene adoptPageContent does not republish 3D page textures', !/triggerTextureRefresh/.test(methodBody(webglSceneSource, 'adoptPageContent'))],
|
|
['webgl book starts at progress zero', /const DEFAULT_BOOK_PROGRESS = 0;/.test(webglSceneSource) && /appInitialState\.progress \?\? '0'/.test(source)],
|
|
['pagination opens with blank left and title right spread', /this\.createBlankPage\(0, \{ section: 'frontmatter' \}\)/.test(bookPaginationSource) && /this\.createTitlePage\(1\)/.test(bookPaginationSource) && /this\.createBlankPage\(2, \{ section: 'frontmatter' \}\)/.test(bookPaginationSource) && /this\.pages = this\.buildPages\(\[\]\);/.test(bookPaginationSource) && /this\.currentSpreadIndex = 0;[\s\S]*this\.publish\(\{ reason: 'initial-title-spread', visibility: 'future-ready' \}\);/.test(bookPaginationSource)],
|
|
['pagination normalizes every spread to explicit left and right page records', /normalizePagesForSpreads/.test(bookPaginationSource) && /const lastSpreadRightIndex/.test(bookPaginationSource) && /this\.createBlankPage\(index/.test(bookPaginationSource) && /normalizedPages\.forEach/.test(bookPaginationSource)],
|
|
['texture renderer adopts initial pagination spread so title page is painted after loader order', /this\.currentSpread = this\.pagination\?\.getCurrentSpread\?\.\(\) \|\| \{ index: 0/.test(textureRendererSource) && /this\.drawSpread\(this\.currentSpread\);/.test(textureRendererSource)],
|
|
['pagination publishes page metadata and advances near the end of physical flips', /pageMeta/.test(bookPaginationSource) && /webgl-book:page-flip-near-end/.test(bookPaginationSource) && /this\.setCurrentSpread\(this\.currentSpreadIndex \+ direction\)/.test(bookPaginationSource)],
|
|
['texture worker draws title page and page numbers; renderer marshals title data and versioned page metadata', /drawTitlePage/.test(textureWorkerSource) && /drawPageNumber/.test(textureWorkerSource) && /game_title/.test(textureRendererSource) && /buildTitleData/.test(textureRendererSource) && /pageMeta: this\.buildPublishPageMeta\(sidesToPublish\)/.test(textureRendererSource)],
|
|
['texture worker uses plural page margin metrics for page numbers', /metrics\.margins\.bottom/.test(textureWorkerSource) && !/metrics\.margin\.bottom/.test(textureWorkerSource)],
|
|
['webgl flip assigns explicit source and back page textures before animation starts', /resolveCurrentFlipSourceTexture\(sourceSide\)/.test(source) && /const targetBackSide = flip\.direction > 0 \? 'left' : 'right'/.test(source) && /const targetBackPageMeta = getPaginationPageMeta\(targetBackPageIndex\) \|\| makeBlankPageMeta\(targetBackPageIndex\)/.test(source) && /materials\.flipPageSurface\.map = sourceTexture/.test(source) && /materials\.flipPageBackSurface\.map = backDeferred \? getBlankPageTexture\(\) : \(backTexture \|\| getBlankPageTexture\(\)\)/.test(source)],
|
|
['webgl flip never falls back to the opposite visible stack for target back texture', /function resolveFlipBackTexture\(pageMeta = null, prewarmedTexture = null\)/.test(source) && source.includes('return pageTextureStore?.getResidentTextureForMeta?.(pageMeta);') && !/oppositeMaterial\?\.map/.test(methodBody(source, 'prepareStaticPageForFlip'))],
|
|
['webgl page texture record metadata normalizes omitted or null sides into explicit blank pages', /function normalizePageMetaPair/.test(source) && /function makeBlankPageMeta/.test(source) && /applyExplicitBlankPageTexture/.test(source) && /normalizePageTextureRecordDetail/.test(source) && !/hasLeftMeta/.test(methodBody(source, 'handlePageTextureRecords'))],
|
|
['texture renderer publishes both spread sides for reveal preparation', /const sides = \['left', 'right'\]/.test(textureRendererSource) && /published = await this\.drawSpread\(spread, sides/.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 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 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) && /const topMaterialIndex = direction > 0 \? 1 : 0/.test(source) && /const bottomMaterialIndex = direction > 0 \? 0 : 1/.test(source) && /geometry\.addGroup\(0, topIndices\.length, topMaterialIndex\)/.test(source) && /geometry\.addGroup\(topIndices\.length, bottomIndices\.length, bottomMaterialIndex\)/.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 = backDeferred \? getBlankPageTexture\(\) : \(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 animated page maps source and destination textures to direction-aware physical sides', /const topPageSide = direction > 0 \? targetSide : sourceSide/.test(source) && /const bottomPageSide = direction > 0 \? sourceSide : targetSide/.test(source) && /topRow\.push\(push\(point, pageThickness, pageUvForSide\(topPageSide, u, v\)\)\)/.test(source) && /bottomRow\.push\(push\(point, 0, pageUvForSide\(bottomPageSide, u, v\)\)\)/.test(source) && /side < 0 \? 1 - pageU : pageU/.test(source) && /y: v/.test(source)],
|
|
['webgl animated page UVs use the same fore-edge inset as the visible stack cap', /PAGE_TEXTURE_FORE_EDGE_INSET_RATIO/.test(source) && /const pageU = THREE\.MathUtils\.clamp\(u \/ Math\.max\(0\.0001, 1 - inset\), 0, 1\)/.test(source)],
|
|
['webgl flip geometry hinges the flip sheet at the spine using the raw page line', !/normalizeFlipLineToVisiblePage/.test(source) && /const sourceLine = topVisibleLine\(sourceSide\)/.test(source) && /const destinationLine = topVisibleLine\(-sourceSide\)/.test(source) && /lerp\(sourceLine\.anchor\.x, destinationLine\.anchor\.x, t\)/.test(source)],
|
|
['webgl flip prewarm prepares current and target spread texture records before cache lookup', /prepareSpreadTextureRecordsForFlip\(currentSpread\)/.test(source) && /prepareSpreadTextureRecordsForFlip\(nextSpread\)/.test(source) && /function prepareSpreadTextureRecordsForFlip/.test(source) && /spreadTextureRecordsReady\(spread\)/.test(source) && /window\.BookTextureRenderer\.drawSpread\(spread, \['left', 'right'\], \{[\s\S]*phase: 'prepare'/.test(source)],
|
|
['webgl animated page reuses geometry buffers during flips', /function updateFlippingPageGeometry/.test(source) && /position\.needsUpdate = true/.test(source) && /updateFlippingPageGeometry\(flip\.mesh\.geometry, surface\)/.test(source) && !/flip\.mesh\.geometry\.dispose\(\);\s*flip\.mesh\.geometry = geometry;/.test(methodBody(source, 'setActivePageGeometry'))],
|
|
['webgl scene targets 60fps with browser-frame scheduling and staggered live mirror refresh', /const targetFrameDurationMs = 1000 \/ 60/.test(source) && /const minRenderFrameIntervalMs = targetFrameDurationMs \* 0\.5/.test(source) && /requestAnimationFrame\(animate\)/.test(source) && /const refreshStaticSceneBuffers = staticSceneBuffersDirty \|\| activeFlips\.length > 0/.test(source) && /const dynamicBufferRefreshIntervalMs = 1000 \/ 30/.test(source) && /const flipDynamicBufferGraceMs = 180/.test(source) && /const bothHeavyPassesDue = shadowRefreshDue && reflectionRefreshDue/.test(source) && /const refreshReflectionThisFrame/.test(source) && /updateTableReflection\(\);/.test(source) && /mirrorRefreshesAtFps/.test(source) && !/setTimeout\(animate/.test(source)],
|
|
['texture renderer has no private reveal clock (scene render loop is the single clock)', !/this\.targetFrameDurationMs/.test(textureRendererSource) && !/tickAnimations/.test(textureRendererSource) && !/requestAnimationFrame/.test(textureRendererSource)],
|
|
['webgl scene lowers mirror target and caps table film maps to 2k', /const reflectionPixelRatio = 0\.72/.test(source) && /const tableReflectionBaseWidth = 1536/.test(source) && /const tableReflectionBaseHeight = 864/.test(source) && /tableDustTexture = loadUtilityTexture\('\/assets\/webgl\/table_dust_4k\.png', \{ maxSize: 2048 \}\)/.test(source) && /tableGreaseTexture = loadUtilityTexture\('\/assets\/webgl\/table_grease_4k\.png', \{ maxSize: 2048 \}\)/.test(source)],
|
|
['webgl debug exposes runtime invariants for visual regression tests', /getRuntimeInvariants\(\)/.test(source) && /residentPageTextureCount/.test(source) && /flipFrontBackShareMaterial/.test(source) && /mirrorRefreshesAtFps/.test(source) && /mirrorDefersDuringFlipStartMs/.test(source)],
|
|
['book pagination reloads to the continuation block spread when unrendered history exists', /getContinuationBlockId/.test(bookPaginationSource) && /const continuationBlockId = this\.getContinuationBlockId\(latestBlockId, latestRenderedBlockId\)/.test(bookPaginationSource) && /const continuationSpreadIndex = this\.findSpreadIndexForBlock\(continuationBlockId\)/.test(bookPaginationSource) && /rendered < latest \? rendered \+ 1 : latest/.test(bookPaginationSource)],
|
|
['webgl page navigation is page-count based with explicit spread mapping', /function pageToSpreadIndex/.test(source) && /Math\.floor\(page \/ 2\) \+ 1/.test(source) && /function spreadIndexToPagePosition/.test(source) && /\(spread - 1\) \* 2/.test(source)],
|
|
['webgl reading progress sync does not rebuild pagination as a page-count change', /function syncReadingProgressToCurrentPage/.test(source) && !/notifyBookPageCountChanged/.test(methodBody(source, 'syncReadingProgressToCurrentPage'))],
|
|
['webgl page reserve grows book size without shrinking', /function growBookIfWritableLimitReached/.test(source) && /bookPageCount < PROCEDURAL_BOOK\.PAGE_COUNT_MAX/.test(source) && /snapProceduralPageCount\(bookPageCount \+ PROCEDURAL_BOOK\.PAGE_COUNT_STEP\)/.test(source) && /bookPageCount = Math\.max\(nextPageCount, bookPageCount\)/.test(source)],
|
|
['webgl bottom navigation shows media buttons and endpoint labels', /webgl_book_navigation/.test(source) && /webgl_book_nav_min_label/.test(source) && /webgl_book_nav_max_label/.test(source) && /webgl-book-nav-slider-track/.test(styleSource)],
|
|
['webgl page reserve options replace old progress slider and hide fixed metadata values', /data-pref-bind': 'webgl\.pageReserve'/.test(optionsUiSource) && /hasFixedBookPageCount/.test(optionsUiSource) && /hasFixedPageReserve/.test(optionsUiSource) && !/data-pref-bind': 'webgl\.bookProgress'/.test(optionsUiSource)],
|
|
['webgl page reserve persists with sane defaults', /bookPageCount: 300/.test(persistenceSource) && /bookProgress: 0/.test(persistenceSource) && /pageReserve: 50/.test(persistenceSource)],
|
|
['markup parser strips and stores pagereserve directives', /parsePageReserveDirective/.test(markupParserSource) && /#pagereserve\\\[/.test(markupParserSource) && /unit: match\[2\] === '%' \? 'percent' : 'pages'/.test(markupParserSource)],
|
|
['game loop persists webgl book state in save slots', /webglBookState: this\.getWebGLBookState\(\)/.test(fs.readFileSync(path.join(__dirname, '..', 'public', 'js', 'game-loop-module.js'), 'utf8')) && /applyWebGLBookState\(browserSave\.webglBookState\)/.test(fs.readFileSync(path.join(__dirname, '..', 'public', 'js', 'game-loop-module.js'), 'utf8'))],
|
|
['webgl right-page reveal flips are owned by the timeline, not the scene', !/pendingRightPageFlip/.test(source) && !/handleRevealCommittedForPageFlip/.test(source) && /waitForVisualCompletion/.test(bookPlaybackTimelineSource) && /reason: 'timeline-right-page-filled'/.test(bookPlaybackTimelineSource) && /requiresRightPageFlipAfterReveal/.test(bookPlaybackTimelineSource) && /isChoiceAwaitingPlayer/.test(bookPlaybackTimelineSource)],
|
|
['webgl reveal clock follows absolute playback time and continues across page flips', /activeRevealBlockStarts/.test(source) && /state\.visualElapsedMs = Math\.max\(0, now - state\.startedAt\)/.test(source) && !/Math\.min\(revealFrameDeltaMs, targetFrameDurationMs\)/.test(source) && /prewarmFlipTextures\(1, targetSpread\)/.test(source)],
|
|
['webgl line reveal timing scales total by word-share for partial blocks and splits per-line by area', /lineWordCount/.test(bookPaginationSource) && /blockWordStart/.test(textureRendererSource) && /blockWordCount/.test(textureRendererSource) && /timingArea/.test(textureRendererSource) && /const useWordShare = totalBlockWords > 0 && collectedWords > 0 && collectedWords < totalBlockWords/.test(textureRendererSource) && /totalDuration \* \(Math\.max\(1, region\.timingArea \|\| region\.area\) \/ totalArea\)/.test(textureRendererSource) && !/const canUseLineWordSpans/.test(textureRendererSource)],
|
|
['webgl flip completion defers book rebuild out of the final animation frame', /scheduledBookRebuildFrame/.test(source) && /function scheduleBookRebuild/.test(source) && /syncReadingProgressToCurrentPage\(\{[\s\S]*rebuild: 'defer'[\s\S]*reason: 'page-flip-finished'/.test(source)],
|
|
['webgl ordinary flip near-end uses resident target textures and defers revealing sides', /applyResidentSpreadTextures\(targetSpread, 'page-flip-near-end', \{ skipSides: flip\.deferRevealSides \}\)/.test(source) && /function applyResidentSpreadTextures\(spreadIndex, reason = 'resident-spread', options = \{\}\)/.test(source) && /const skipSides = Array\.isArray\(options\.skipSides\)/.test(source) && /residentSpreadTextures:applied/.test(source) && /spreadUpdate:state-only/.test(source)],
|
|
['webgl autoplay flip source prefers currently revealing visible material over resident cache', /if \(revealStateMatchesPage\(side, pageMeta\)\) return material\?\.map \|\| null/.test(source) && /revealStateMatchesPage\(sourceSide, sourcePageMeta\) \? sourceSide : null/.test(source)],
|
|
['webgl flipping page materials mirror active reveal shader uniforms on both sides', /materials\.flipPageSurface\.userData\.bookPageReveal/.test(source) && /syncFlipRevealShaderFromSource/.test(source) && /bookRevealRegionRects/.test(source) && /materials\.flipPageSurface\.userData\.sourceRevealSide === side/.test(source) && /revealStateMatchesPage\(targetBackSide, targetBackPageMeta\) \? targetBackSide : null/.test(source)],
|
|
['webgl prepared texture records do not mutate the visible page metadata', /const incomingPageMeta = detail\.pageMeta/.test(source) && /if \(detail\.phase !== 'prepare' && detail\.pageMeta\) \{[\s\S]*currentPageMeta = incomingPageMeta/.test(source) && /pageMeta: effectivePageMeta/.test(source)],
|
|
['webgl scene force-redraws current pagination spread for initial title upload', /const initialSpread = pagination\?\.getCurrentSpread\?\.\(\)/.test(webglSceneSource) && /window\.BookTextureRenderer\.drawSpread\(initialSpread, \['left', 'right'\], \{ force: true \}\)/.test(webglSceneSource) && /options\.force !== true && phase !== 'prepare'/.test(textureRendererSource)],
|
|
['texture renderer marks committed reveal blocks complete so pauses cannot replay them', /webgl-book:reveal-committed/.test(textureRendererSource) && /completeRevealBlockIds/.test(textureRendererSource) && /this\.revealedBlockIds\.add\(id\)/.test(textureRendererSource)],
|
|
['webgl timeline recalculates placeholder zero-duration reveal timings from TTS duration', /existingTimings/.test(bookPlaybackTimelineSource) && /existingDuration/.test(bookPlaybackTimelineSource) && /ttsDuration/.test(bookPlaybackTimelineSource) && /existingTimings\.length > 0 && \(existingDuration > 0 \|\| ttsDuration <= 0\)/.test(bookPlaybackTimelineSource)],
|
|
['webgl playback coordinator trusts timeline-prepared reveal timings without recomputing', !/calculateWordTimings/.test(methodBody(playbackCoordinatorSource, 'scheduleWebGLReveal')) && /single owner of reveal timing/.test(playbackCoordinatorSource) && /sentence\.webglRevealController\(/.test(playbackCoordinatorSource)],
|
|
['texture renderer derives reveal regions from the just-drawn preview spread before committed pagination', /if \(this\.currentSpread\) sourceSpreads\.push\(this\.currentSpread\)/.test(textureRendererSource) && /paginationSpreads\.forEach/.test(textureRendererSource) && /Number\(spread\.index\) === Number\(this\.currentSpread\.index\)/.test(textureRendererSource)],
|
|
['texture renderer prepares a spanning block continuation spread in the background and reuses it (no synchronous redraw on the critical path)', /revealSpreadSourceOverride/.test(textureRendererSource) && /prepareContinuationRevealPlan/.test(textureRendererSource) && /takeContinuationRevealPlan/.test(textureRendererSource) && /`\$\{id\}:cont`/.test(textureRendererSource) && /prepareContinuationRevealPlan/.test(bookPlaybackTimelineSource) && /takeContinuationRevealPlan/.test(bookPlaybackTimelineSource) && /previewSpreads/.test(bookPaginationSource)],
|
|
['texture renderer preloads every spread touched by an active reveal block', /preloadAdditionalRevealSpreads/.test(textureRendererSource) && /spreadContainsBlock/.test(textureRendererSource) && /this\.drawSpread\(spread, \['left', 'right'\], \{ phase: 'prepare' \}\)/.test(textureRendererSource)],
|
|
['book playback timeline has one prepare path: spanning-aware plan reused at activate and continuation, no fallback', /revealSpreadSourceOverride: spanningPreview \? detail\.previewSpreads : null/.test(textureRendererSource) && /this\.revealSpreadSourceOverride = options\.revealSpreadSourceOverride/.test(textureRendererSource) && !/forceRebuild/.test(textureRendererSource) && !/forceRebuild/.test(bookPlaybackTimelineSource) && !/spanningPlanPrepared/.test(bookPlaybackTimelineSource) && /const texturePlan = this\.textureRenderer\.takeContinuationRevealPlan\(segment\.blockId, spread\.index\)/.test(bookPlaybackTimelineSource)],
|
|
['webgl visible spread is owned by scene flips, not pagination publishes', /spreadUpdate:state-only/.test(source) && /webglBookPlaybackActive/.test(source) && /spreadUpdate:jump/.test(source) && /window\.BookTextureRenderer\?\.drawSpread\?\.\(spread, \['left', 'right'\], \{ force: true \}\)/.test(source)],
|
|
['3D overflow reveal preloads target spread before forced page flip', /createRevealDetail\(sentence, previewSpread, 'prepare'\)/.test(bookPlaybackTimelineSource) && /this\.textureRenderer\.prepareRevealBlock\(\s*[\s\S]*revealDetail[\s\S]*phase: 'prepare'[\s\S]*publishEvent: false/.test(bookPlaybackTimelineSource) && /this\.prewarmSegmentTextures\(segment\)/.test(bookPlaybackTimelineSource) && /this\.assertSegmentReady\(segment, 'prepare'\)/.test(bookPlaybackTimelineSource) && /reason: 'timeline-preplay-spread-transition'/.test(bookPlaybackTimelineSource)],
|
|
['book playback timeline is loaded through module infrastructure', /book-playback-timeline-module\.js/.test(loaderSource) && /super\('book-playback-timeline'/.test(bookPlaybackTimelineSource) && /reportProgress\(100, 'Book playback timeline ready'\)/.test(bookPlaybackTimelineSource)],
|
|
['book playback timeline initializes before sentence queue without a dependency cycle', /this\.dependencies = \[[^\]]*'book-playback-timeline'[^\]]*\]/.test(sentenceQueueSource) && !/this\.dependencies = \[[^\]]*'sentence-queue'[^\]]*\]/.test(bookPlaybackTimelineSource) && /calculateAnimationTiming\(words = \[\]/.test(bookPlaybackTimelineSource)],
|
|
['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)],
|
|
['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) && /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 owned by the timeline and dispatched to the single scene clock', /sentence\.webglRevealController = \(\) => this\.startRevealForSegment\(segment\)/.test(bookPlaybackTimelineSource) && /startPreparedRevealAnimation\?\.\(segment\.blockId, \{[\s\S]*publishEvent: true/.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 scene reports reveal commits but does not own flips and no ownership flag survives', /dispatchEvent\(new CustomEvent\('webgl-book:reveal-committed'/.test(source) && !/handleRevealCommittedForPageFlip/.test(source) && !/ownsPageFlipCommit/.test(source) && !/ownsPageFlipCommit/.test(textureRendererSource) && !/ownsPageFlipCommit/.test(bookPlaybackTimelineSource)],
|
|
['webgl reveal clock explicitly freezes during physical flips', /pageRevealFreezeAt/.test(source) && /state\.startedAt \+= frozenMs/.test(source) && /activeRevealBlockStarts\.set\(blockId, Number\(value\) \+ frozenMs\)/.test(source)],
|
|
['book playback timeline waits for right reveal only when current block is on right page', /getBlockRevealSides/.test(bookPlaybackTimelineSource) && /revealSides\.includes\('right'\) && this\.requiresRightPageFlipAfterReveal/.test(bookPlaybackTimelineSource) && /visual-completion:no-right-flip-wait/.test(bookPlaybackTimelineSource)],
|
|
['book playback timeline flips at planned right-page fragment time without a stray commit timeout', /waitForPlannedRightReveal/.test(bookPlaybackTimelineSource) && /getRightRevealDurationMs/.test(bookPlaybackTimelineSource) && /segment\.revealStartedPromise/.test(bookPlaybackTimelineSource) && /const timer = setTimeout\(\(\) => finish\(true\), remaining\)/.test(bookPlaybackTimelineSource) && !/waitForRevealCommit/.test(bookPlaybackTimelineSource)],
|
|
['book playback timeline exposes reveal lifecycle benchmark entries', /benchmarkEntries/.test(bookPlaybackTimelineSource) && /markBenchmark\('reveal-start'/.test(bookPlaybackTimelineSource) && /markBenchmark\('reveal-committed'/.test(bookPlaybackTimelineSource) && /webglBookBenchmark/.test(bookPlaybackTimelineSource)],
|
|
['webgl scene records reveal start and slow-frame benchmark diagnostics', /revealState:created/.test(source) && /revealStart:applied/.test(source) && /slowFrameLog/.test(source) && /getBenchmarkState/.test(source) && /webglSlowFrames/.test(source)],
|
|
['webgl navigation is spread-based and caps at visited/written spread', /function navigateToSpread\(/.test(source) && /function navigateBySpreadDelta\(/.test(source) && /function getMaxNavigableSpread\(\)/.test(source) && /Math\.min\(visitedSpread, spreadCount - 1\)/.test(source) && /navigateBySpreadDelta\(1\)/.test(source) && /spread <= 0 \? '0' : String\(spread \* 2 \+ 1\)/.test(source) && /currentSpread < getMaxNavigableSpread\(\)/.test(source)],
|
|
['webgl save restore carries visited page limit for navigation', /maxVisitedPagePosition/.test(source) && /setMaxVisitedPagePosition/.test(source) && /state\.maxVisitedPagePosition \?\? state\.pagePosition/.test(webglSceneSource)],
|
|
['webgl page flips require resident nonblank back textures before animation starts', /prepareStaticPageForFlip\(flip, prewarm = null\)/.test(source) && /flip-back-texture-missing/.test(source) && /targetBackPageMeta\.kind !== 'blank'/.test(source) && /return false;/.test(methodBody(source, 'prepareStaticPageForFlip')) && /flipTexturePreflight:ready/.test(source) && /if \(!prepareStaticPageForFlip\(flip, options\.prewarm \|\| null\)\) \{[\s\S]*return false;[\s\S]*\}/.test(source)],
|
|
['webgl fast page flips preflight the actual target spread', /firstFlip\.targetSpread = Number\.isFinite\(Number\(options\.targetSpread\)\)/.test(source) && /if \(!prepareStaticPageForFlip\(firstFlip, options\.prewarm \|\| null\)\) return false/.test(source)],
|
|
['markup and 3d pagination accept full-page images', /'full'/.test(markupParserSource) && /size === 'full'/.test(bookPaginationSource)],
|
|
['story history can persist 3d pagination decisions', /persistPaginationMetrics/.test(bookPaginationSource) && /collectPaginationMetrics/.test(bookPaginationSource) && /pageStart/.test(storyHistorySource) && /pagination: metrics\.pagination/.test(storyHistorySource)]
|
|
];
|
|
|
|
const failures = checks.filter(([, passed]) => !passed).map(([name]) => name);
|
|
|
|
if (failures.length) {
|
|
console.error('WebGL book lab regression checks failed:');
|
|
failures.forEach((name) => console.error(`- ${name}`));
|
|
process.exit(1);
|
|
}
|
|
|
|
console.log(`WebGL book lab regression checks passed (${checks.length}).`);
|