Checkpoint WebGL book playback refactor state
This commit is contained in:
@@ -6,16 +6,16 @@ This document captures the agreed direction for the WebGL book UI. Later decisio
|
||||
|
||||
Build a beautiful, readable, extensible WebGL book interface for the interactive fiction UI.
|
||||
|
||||
The current stable milestone is `public/webgl-book-lab.html`: an open procedural book lying on a polished wooden table, lit by flickering candles, with the existing application page content rendered into textures that are applied to the actual top surfaces of the paper stacks.
|
||||
The current stable milestone is the integrated 3D game UI at `http://localhost:3001/`: an open procedural book lying on a polished wooden table, lit by flickering candles, with game content typeset directly into texture-space canvases and applied to the actual top surfaces of the paper stacks.
|
||||
|
||||
The later product goal is a procedural book UI that supports virtual scrolling, animated page flips, dynamic page stacks, and content backfilling across spreads.
|
||||
The product goal is a procedural book UI that supports virtual scrolling, animated page flips, dynamic page stacks, and content backfilling across spreads.
|
||||
|
||||
## Current Implementation Snapshot
|
||||
|
||||
This section records the current state after the procedural book integration work.
|
||||
|
||||
- The active standalone scene is `public/webgl-book-lab.html`.
|
||||
- The intended local test URL is `http://localhost:3001/webgl-book-lab.html`.
|
||||
- The active integrated scene is loaded through the game at `http://localhost:3001/`.
|
||||
- `public/webgl-book-lab.html` remains a reference/prototype file, not the primary implementation target.
|
||||
- The current test server is expected to be the single Node process listening on port `3001`.
|
||||
- The procedural book model lives in `public/js/procedural-book-model.js`.
|
||||
- The WebGL lab integration lives in `public/js/webgl-book-lab.js`.
|
||||
@@ -29,7 +29,7 @@ This section records the current state after the procedural book integration wor
|
||||
- `PAGE_SPLINE_LENGTH` must match `PAGE_WIDTH`.
|
||||
- Cover width is derived from page width plus cover overhang.
|
||||
- The spine bottom is aligned to the table plane with only a tiny render clearance.
|
||||
- The book controls are in the top bar: fast backward, backward, progress slider, page-count slider, forward, and fast forward.
|
||||
- The book navigation controls are the bottom media-style navigation controls: return to beginning, flip backward, page-position scrollbar, flip forward, and go to end.
|
||||
- Slow page flips and fast 10-page transitions are implemented.
|
||||
- Fast transitions run overlapping flip animations before shifting the book by one bundle.
|
||||
- The readable page content belongs on the visible top cap of the paper stacks.
|
||||
@@ -52,6 +52,41 @@ This section records the current state after the procedural book integration wor
|
||||
- The custom book material shader includes a small local indirect/bounce-light term for book surfaces. This is not emissive; it is multiplied by material albedo so paper, leather, and head/end-band colors remain distinct while shadowed side faces stay readable.
|
||||
- Temporary screenshots and generated debug images are not product assets unless explicitly promoted.
|
||||
|
||||
## Current Page-Flow Architecture
|
||||
|
||||
The 3D book pipeline is module-owned. No page content should be generated by ad hoc scene code.
|
||||
|
||||
- `ui-display-handler` owns the live 3D prepare -> optional page flip -> activate sequence for story text.
|
||||
- `sentence-queue` may prepare future page presentations during lookahead using the same pagination and texture renderer modules.
|
||||
- `book-pagination` owns page/spread construction, page metadata, widows/orphans/hyphenation decisions, image placement decisions, and explicit blank/title/body page records.
|
||||
- `book-texture-renderer` owns drawing final page canvases, reveal-region coordinates, reveal timing metadata, and publishing explicit `webgl-book:page-texture-records`.
|
||||
- `webgl-page-cache` owns persistent page canvases, memory canvases, prepared reveal plans, prepared GPU textures, resident VRAM textures, blank page texture, and visible texture bindings.
|
||||
- `webgl-book-lab` owns the Three.js scene, materials, geometry, pointer projection, page flip meshes, and consuming page texture records. It must not become a second page cache.
|
||||
|
||||
Problem states must be surfaced instead of hidden:
|
||||
|
||||
- A missing persistent page canvas during prewarm is a `db-cache-miss`.
|
||||
- A missing source or required back texture before a page flip is a `flip-source-texture-missing` or `flip-back-texture-missing`.
|
||||
- These problem states must appear in `webglPageCacheProblems` and must not be silently fixed by borrowing unrelated visible stack textures.
|
||||
|
||||
## Event Surface
|
||||
|
||||
The preferred 3D book content path is direct module calls through `ui-display-handler` for live playback and `sentence-queue` for lookahead preparation.
|
||||
|
||||
The following events remain formal integration events and must keep their meaning stable while they exist:
|
||||
|
||||
- `webgl-book:page-texture-records` publishes explicit page texture records from `book-texture-renderer` to the WebGL scene.
|
||||
- `webgl-book:page-reveal-start` starts shader reveal timing for the prepared block.
|
||||
- `webgl-book:page-reveal-fast-forward` accelerates reveal timing without replacing the page pipeline.
|
||||
- `webgl-book:reveal-committed` reports that a page-side reveal completed; if `pageFlipAfterReveal` is true, the WebGL scene may arm a page flip.
|
||||
- `webgl-book:request-page-flip` requests a physical page flip through the WebGL scene.
|
||||
- `webgl-book:page-flip-started`, `webgl-book:page-flip-near-end`, and `webgl-book:page-flip-finished` describe the physical flip lifecycle.
|
||||
|
||||
Deprecated or forbidden event contracts:
|
||||
|
||||
- `webgl-book:page-canvases` is obsolete. New code must use `webgl-book:page-texture-records`.
|
||||
- `preloadOnly` and `allowFutureUnrendered` are obsolete boolean flags. New code must use explicit `phase` and `visibility` values.
|
||||
|
||||
## Non-Negotiable Workflow Rules
|
||||
|
||||
- Do not continue visual coding without a concrete plan for the current sprint.
|
||||
|
||||
@@ -94,7 +94,7 @@ class BookPaginationModule extends BaseModule {
|
||||
this.pages = this.buildPages([]);
|
||||
this.spreads = this.buildSpreadsFromPages(this.pages);
|
||||
this.currentSpreadIndex = 0;
|
||||
this.publish({ reason: 'initial-title-spread', allowFutureUnrendered: true });
|
||||
this.publish({ reason: 'initial-title-spread', visibility: 'future-ready' });
|
||||
this.reportProgress(100, 'Book pagination ready');
|
||||
return true;
|
||||
}
|
||||
@@ -146,7 +146,7 @@ class BookPaginationModule extends BaseModule {
|
||||
: renderedSpreadIndex >= 0
|
||||
? renderedSpreadIndex
|
||||
: Math.max(0, Math.min(this.currentSpreadIndex, Math.max(0, this.spreads.length - 1)));
|
||||
this.publish({ reason: 'history-refresh', allowFutureUnrendered: true });
|
||||
this.publish({ reason: 'history-refresh', visibility: 'future-ready' });
|
||||
}
|
||||
|
||||
getContinuationBlockId(latestBlockId = 0, latestRenderedBlockId = 0) {
|
||||
@@ -187,7 +187,7 @@ class BookPaginationModule extends BaseModule {
|
||||
spreadIndex: cached.targetSpread?.index ?? this.currentSpreadIndex,
|
||||
latestBlockId: pendingBlockId,
|
||||
latestRenderedBlockId,
|
||||
preloadOnly: false,
|
||||
phase: 'activate',
|
||||
reusedPreparedPagination: true
|
||||
}
|
||||
}));
|
||||
@@ -240,7 +240,7 @@ class BookPaginationModule extends BaseModule {
|
||||
spreadIndex: targetSpread?.index ?? this.currentSpreadIndex,
|
||||
latestBlockId: pendingBlockId,
|
||||
latestRenderedBlockId,
|
||||
preloadOnly: options.activate === false
|
||||
phase: options.activate === false ? 'prepare' : 'activate'
|
||||
}
|
||||
}));
|
||||
return targetSpread || (options.activate === false ? preparedSpreads[0] : this.getCurrentSpread());
|
||||
@@ -371,7 +371,8 @@ class BookPaginationModule extends BaseModule {
|
||||
buildSpreadsFromPages(pages = []) {
|
||||
const spreads = [];
|
||||
const linesPerPage = this.getLinesPerPage();
|
||||
pages.forEach((page, pageIndex) => {
|
||||
const normalizedPages = this.normalizePagesForSpreads(pages);
|
||||
normalizedPages.forEach((page, pageIndex) => {
|
||||
const spreadIndex = Math.floor(pageIndex / 2);
|
||||
const side = pageIndex % 2 === 0 ? 'left' : 'right';
|
||||
if (!spreads[spreadIndex]) {
|
||||
@@ -398,6 +399,22 @@ class BookPaginationModule extends BaseModule {
|
||||
return spreads.filter(Boolean);
|
||||
}
|
||||
|
||||
normalizePagesForSpreads(pages = []) {
|
||||
const source = Array.isArray(pages) ? pages : [];
|
||||
const lastPageIndex = source.reduce((max, page, index) => {
|
||||
const explicitIndex = Number(page?.index);
|
||||
return Math.max(max, Number.isFinite(explicitIndex) ? explicitIndex : index);
|
||||
}, 1);
|
||||
const lastSpreadRightIndex = Math.max(1, lastPageIndex % 2 === 0 ? lastPageIndex + 1 : lastPageIndex);
|
||||
const normalized = [];
|
||||
for (let index = 0; index <= lastSpreadRightIndex; index += 1) {
|
||||
normalized[index] = source[index] || this.createBlankPage(index, {
|
||||
section: index < 3 ? 'frontmatter' : 'body'
|
||||
});
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
applyPageReserveDirective(block = {}) {
|
||||
const directive = block?.metadata?.pageReserve || block?.pageReserve || null;
|
||||
const blockId = Number(block?.blockId || block?.metadata?.blockId || 0);
|
||||
@@ -967,7 +984,7 @@ class BookPaginationModule extends BaseModule {
|
||||
|
||||
setCurrentSpread(index = 0) {
|
||||
this.currentSpreadIndex = Math.max(0, Math.min(Math.round(Number(index || 0)), Math.max(0, this.spreads.length - 1)));
|
||||
this.publish({ reason: 'set-current-spread', allowFutureUnrendered: true });
|
||||
this.publish({ reason: 'set-current-spread', visibility: 'future-ready' });
|
||||
return this.currentSpreadIndex;
|
||||
}
|
||||
|
||||
@@ -982,7 +999,7 @@ class BookPaginationModule extends BaseModule {
|
||||
latestBlockId: this.latestBlockId,
|
||||
latestRenderedBlockId: this.latestRenderedBlockId,
|
||||
reason: options.reason || 'publish',
|
||||
allowFutureUnrendered: options.allowFutureUnrendered === true
|
||||
visibility: options.visibility || 'current'
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -29,7 +29,6 @@ class BookTextureRendererModule extends BaseModule {
|
||||
this.activeAnimations = new Map();
|
||||
this.revealedBlockIds = new Set();
|
||||
this.pendingRevealBlockIds = new Set();
|
||||
this.preparedRevealCache = new Map();
|
||||
this.revealBaseCanvases = null;
|
||||
this.revealPublishBlockIds = null;
|
||||
this.lastDrawSignature = null;
|
||||
@@ -39,7 +38,6 @@ class BookTextureRendererModule extends BaseModule {
|
||||
this.targetFrameDurationMs = 1000 / 60;
|
||||
this.pipelineTimings = [];
|
||||
this.imageCache = new Map();
|
||||
this.pendingPageCacheWrites = new Map();
|
||||
this.pageContentVersions = new Map();
|
||||
|
||||
this.bindMethods([
|
||||
@@ -63,6 +61,7 @@ class BookTextureRendererModule extends BaseModule {
|
||||
'drawLine',
|
||||
'drawWord',
|
||||
'buildRevealRegions',
|
||||
'shouldFlipAfterSideReveal',
|
||||
'collectRevealRegionCandidates',
|
||||
'createRevealRegionForLine',
|
||||
'assignRevealTiming',
|
||||
@@ -82,6 +81,7 @@ class BookTextureRendererModule extends BaseModule {
|
||||
'spreadContainsBlock',
|
||||
'hasPreparedRevealBlock',
|
||||
'createAnimationState',
|
||||
'getDrawPhase',
|
||||
'publishPreparedReveal',
|
||||
'startPreparedRevealAnimation',
|
||||
'fastForwardAnimations',
|
||||
@@ -92,10 +92,8 @@ class BookTextureRendererModule extends BaseModule {
|
||||
'requestAnimationFrame',
|
||||
'tickAnimations',
|
||||
'publishSpread',
|
||||
'buildPageTextureRecords',
|
||||
'cachePublishedPages',
|
||||
'getPageCacheWriteKey',
|
||||
'isOlderPageMeta',
|
||||
'schedulePageCacheWrite',
|
||||
'getPageCanvas',
|
||||
'getHitMap',
|
||||
'handlePageCountChanged'
|
||||
@@ -120,11 +118,12 @@ class BookTextureRendererModule extends BaseModule {
|
||||
const spreadIndex = Math.max(0, Number(event.detail?.spreadIndex ?? spread?.index ?? 0));
|
||||
const latestBlockId = event.detail?.latestBlockId;
|
||||
const latestRenderedBlockId = Math.max(0, Number(event.detail?.latestRenderedBlockId || 0));
|
||||
const visibility = event.detail?.visibility || 'current';
|
||||
this.currentSpread = spread || { left: [], right: [] };
|
||||
if (latestBlockId && Number(latestBlockId) > latestRenderedBlockId) {
|
||||
this.markPendingReveal(latestBlockId);
|
||||
const id = String(latestBlockId);
|
||||
if (event.detail?.allowFutureUnrendered === true && !this.activeAnimations.has(id)) {
|
||||
if (visibility === 'future-ready' && !this.activeAnimations.has(id)) {
|
||||
this.drawSpread(this.stripUnrenderedLines(this.currentSpread, latestRenderedBlockId), ['left', 'right']);
|
||||
return;
|
||||
}
|
||||
@@ -132,8 +131,8 @@ class BookTextureRendererModule extends BaseModule {
|
||||
this.revealPublishBlockIds = new Set([id]);
|
||||
const visibleSpread = Math.max(0, Number(window.BookLabDebug?.getBookState?.().spreadIndex || 0));
|
||||
const flipActive = document.documentElement.dataset.webglPageFlipActive === 'true';
|
||||
if (!flipActive && event.detail?.allowFutureUnrendered !== true && spreadIndex > visibleSpread) {
|
||||
this.drawSpread(this.currentSpread, ['left', 'right'], { preloadOnly: true });
|
||||
if (!flipActive && visibility !== 'future-ready' && spreadIndex > visibleSpread) {
|
||||
this.drawSpread(this.currentSpread, ['left', 'right'], { phase: 'prepare' });
|
||||
return;
|
||||
}
|
||||
this.drawSpread(this.currentSpread, ['left', 'right']);
|
||||
@@ -226,20 +225,21 @@ class BookTextureRendererModule extends BaseModule {
|
||||
this.currentSpread = spread || { left: [], right: [] };
|
||||
const sidesToDraw = Array.isArray(sides) && sides.length ? sides : ['left', 'right'];
|
||||
const hasReveal = this.revealPublishBlockIds && this.revealPublishBlockIds.size > 0;
|
||||
const phase = this.getDrawPhase(options);
|
||||
const drawSignature = this.getDrawSignature(this.currentSpread, sidesToDraw);
|
||||
if (!options.preloadOnly && !hasReveal && drawSignature === this.lastDrawSignature) {
|
||||
if (phase !== 'prepare' && !hasReveal && drawSignature === this.lastDrawSignature) {
|
||||
const now = performance.now();
|
||||
if (now - this.lastDrawSkipLoggedAt > 1000) {
|
||||
this.lastDrawSkipLoggedAt = now;
|
||||
this.markPipelineTiming('drawSpread:skip', { sides: sidesToDraw });
|
||||
}
|
||||
if (options.preloadOnly) this.currentSpread = previousSpread;
|
||||
if (phase === 'prepare') this.currentSpread = previousSpread;
|
||||
return null;
|
||||
}
|
||||
this.markPipelineTiming('drawSpread:start', {
|
||||
sides: sidesToDraw,
|
||||
revealBlockIds: this.revealPublishBlockIds ? Array.from(this.revealPublishBlockIds) : [],
|
||||
preloadOnly: Boolean(options.preloadOnly)
|
||||
phase
|
||||
});
|
||||
this.revealBaseCanvases = { left: null, right: null };
|
||||
sidesToDraw.forEach((side) => {
|
||||
@@ -253,15 +253,20 @@ class BookTextureRendererModule extends BaseModule {
|
||||
const published = this.publishSpread(sidesToDraw, options);
|
||||
this.markPipelineTiming('drawSpread:end', {
|
||||
sides: sidesToDraw,
|
||||
preloadOnly: Boolean(options.preloadOnly)
|
||||
phase
|
||||
});
|
||||
this.revealBaseCanvases = null;
|
||||
this.revealPublishBlockIds = null;
|
||||
if (!options.preloadOnly && !hasReveal) this.lastDrawSignature = drawSignature;
|
||||
if (options.preloadOnly) this.currentSpread = previousSpread;
|
||||
if (phase !== 'prepare' && !hasReveal) this.lastDrawSignature = drawSignature;
|
||||
if (phase === 'prepare') this.currentSpread = previousSpread;
|
||||
return published;
|
||||
}
|
||||
|
||||
getDrawPhase(options = {}) {
|
||||
if (options.phase === 'prepare' || options.phase === 'activate') return options.phase;
|
||||
return 'activate';
|
||||
}
|
||||
|
||||
getDrawSignature(spread = null, sides = []) {
|
||||
const source = spread || {};
|
||||
return sides.map(side => {
|
||||
@@ -644,6 +649,7 @@ class BookTextureRendererModule extends BaseModule {
|
||||
return {
|
||||
blockIds: Array.from(byBlock.keys()),
|
||||
durationMs: sideRegions.reduce((maxDuration, region) => Math.max(maxDuration, region.timing.delay + region.timing.duration), 0),
|
||||
pageFlipAfterReveal: this.shouldFlipAfterSideReveal(side),
|
||||
baseCanvas: null,
|
||||
lineRects: sideRegions.map(region => ({
|
||||
blockId: region.blockId,
|
||||
@@ -660,6 +666,19 @@ class BookTextureRendererModule extends BaseModule {
|
||||
};
|
||||
}
|
||||
|
||||
shouldFlipAfterSideReveal(side) {
|
||||
if (side !== 'right') return false;
|
||||
const meta = this.currentSpread?.pageMeta?.right || null;
|
||||
if (!meta || meta.section !== 'body' || meta.kind === 'blank' || meta.kind === 'title') return false;
|
||||
const rightLines = Array.isArray(this.currentSpread?.right) ? this.currentSpread.right : [];
|
||||
const maxLine = rightLines.reduce((max, line) => Math.max(
|
||||
max,
|
||||
Number(line?.pageLine || 0) + Math.max(1, Number(line?.lineCount || 1))
|
||||
), 0);
|
||||
const expectedLines = Math.max(1, Number(meta.linesPerPage || 25));
|
||||
return maxLine >= expectedLines;
|
||||
}
|
||||
|
||||
collectRevealRegionCandidates() {
|
||||
const candidates = [];
|
||||
const sourceSpreads = Array.isArray(this.pagination?.spreads) && this.pagination.spreads.length
|
||||
@@ -879,14 +898,16 @@ class BookTextureRendererModule extends BaseModule {
|
||||
if (blockId == null || !Array.isArray(detail.wordTimings)) return;
|
||||
const id = String(blockId);
|
||||
const wordTimings = detail.wordTimings;
|
||||
const preloadOnly = Boolean(detail.preloadOnly || options.preloadOnly);
|
||||
const phase = detail.phase === 'prepare' || options.phase === 'prepare'
|
||||
? 'prepare'
|
||||
: 'activate';
|
||||
this.markPipelineTiming('prepareRevealBlock:start', {
|
||||
blockId: id,
|
||||
wordTimingCount: wordTimings.length,
|
||||
preloadOnly
|
||||
phase
|
||||
});
|
||||
if (!preloadOnly && this.preparedRevealCache.has(id)) {
|
||||
const cached = this.preparedRevealCache.get(id);
|
||||
if (phase === 'activate' && this.pageCache?.hasPreparedRevealPlan?.(id)) {
|
||||
const cached = this.pageCache.takePreparedRevealPlan(id);
|
||||
this.activeAnimations.set(id, this.createAnimationState(blockId, wordTimings, detail));
|
||||
this.pendingRevealBlockIds.delete(id);
|
||||
this.publishPreparedReveal(cached);
|
||||
@@ -903,10 +924,10 @@ class BookTextureRendererModule extends BaseModule {
|
||||
this.revealPublishBlockIds = new Set([id]);
|
||||
const spread = detail.spread || this.currentSpread || this.pagination?.getCurrentSpread?.();
|
||||
const sides = ['left', 'right'];
|
||||
const published = this.drawSpread(spread, sides, { preloadOnly });
|
||||
if (!preloadOnly) this.preloadAdditionalRevealSpreads(id, spread);
|
||||
if (preloadOnly && published) {
|
||||
this.preparedRevealCache.set(id, {
|
||||
const published = this.drawSpread(spread, sides, { phase });
|
||||
if (phase !== 'prepare') this.preloadAdditionalRevealSpreads(id, spread);
|
||||
if (phase === 'prepare' && published) {
|
||||
this.pageCache?.rememberPreparedRevealPlan?.(id, {
|
||||
...published,
|
||||
blockId,
|
||||
wordTimings,
|
||||
@@ -916,7 +937,7 @@ class BookTextureRendererModule extends BaseModule {
|
||||
this.markPipelineTiming('prepareRevealBlock:end', {
|
||||
blockId: id,
|
||||
wordTimingCount: wordTimings.length,
|
||||
preloadOnly
|
||||
phase
|
||||
});
|
||||
}
|
||||
|
||||
@@ -927,7 +948,7 @@ class BookTextureRendererModule extends BaseModule {
|
||||
spreads.forEach((spread) => {
|
||||
if (!spread || Number(spread.index) === primaryIndex) return;
|
||||
if (!this.spreadContainsBlock(spread, blockId)) return;
|
||||
this.drawSpread(spread, ['left', 'right'], { preloadOnly: true });
|
||||
this.drawSpread(spread, ['left', 'right'], { phase: 'prepare' });
|
||||
});
|
||||
}
|
||||
|
||||
@@ -941,7 +962,7 @@ class BookTextureRendererModule extends BaseModule {
|
||||
|
||||
hasPreparedRevealBlock(blockId) {
|
||||
const id = String(blockId ?? '');
|
||||
return Boolean(id && this.preparedRevealCache.has(id));
|
||||
return Boolean(id && this.pageCache?.hasPreparedRevealPlan?.(id));
|
||||
}
|
||||
|
||||
publishPreparedReveal(prepared) {
|
||||
@@ -951,14 +972,14 @@ class BookTextureRendererModule extends BaseModule {
|
||||
sides: prepared.sides || [],
|
||||
hasReveal: Boolean(prepared.reveal && Object.keys(prepared.reveal).length)
|
||||
});
|
||||
document.dispatchEvent(new CustomEvent('webgl-book:page-canvases', {
|
||||
document.dispatchEvent(new CustomEvent('webgl-book:page-texture-records', {
|
||||
detail: {
|
||||
metrics: prepared.metrics,
|
||||
hitMaps: prepared.hitMaps || this.hitMaps,
|
||||
left: prepared.left || null,
|
||||
right: prepared.right || null,
|
||||
records: prepared.records || this.buildPageTextureRecords(prepared.sides || ['left', 'right'], prepared),
|
||||
reveal: prepared.reveal || {},
|
||||
pageMeta: prepared.pageMeta || {},
|
||||
phase: 'activate',
|
||||
preparedFromCache: true
|
||||
}
|
||||
}));
|
||||
@@ -1095,6 +1116,7 @@ class BookTextureRendererModule extends BaseModule {
|
||||
|
||||
publishSpread(sides = null, options = {}) {
|
||||
const sidesToPublish = Array.isArray(sides) && sides.length ? sides : ['left', 'right'];
|
||||
const phase = this.getDrawPhase(options);
|
||||
const regionCounts = {
|
||||
left: 0,
|
||||
right: 0
|
||||
@@ -1103,142 +1125,96 @@ class BookTextureRendererModule extends BaseModule {
|
||||
metrics: this.metrics,
|
||||
hitMaps: this.hitMaps,
|
||||
sides: sidesToPublish,
|
||||
pageMeta: this.buildPublishPageMeta(sidesToPublish)
|
||||
pageMeta: this.buildPublishPageMeta(sidesToPublish),
|
||||
phase
|
||||
};
|
||||
if (options.preloadOnly) detail.preloadOnly = true;
|
||||
if (sidesToPublish.includes('left')) {
|
||||
detail.left = options.preloadOnly ? this.cloneCanvas(this.canvases.left) : this.canvases.left;
|
||||
detail.left = phase === 'prepare' ? this.cloneCanvas(this.canvases.left) : this.canvases.left;
|
||||
}
|
||||
if (sidesToPublish.includes('right')) {
|
||||
detail.right = options.preloadOnly ? this.cloneCanvas(this.canvases.right) : this.canvases.right;
|
||||
detail.right = phase === 'prepare' ? this.cloneCanvas(this.canvases.right) : this.canvases.right;
|
||||
}
|
||||
const reveal = {};
|
||||
sidesToPublish.forEach((side) => {
|
||||
const sideReveal = this.buildRevealRegions(side);
|
||||
if (!sideReveal) return;
|
||||
sideReveal.baseCanvas = options.preloadOnly
|
||||
sideReveal.baseCanvas = phase === 'prepare'
|
||||
? this.cloneCanvas(this.revealBaseCanvases?.[side])
|
||||
: this.revealBaseCanvases?.[side] || null;
|
||||
regionCounts[side] = sideReveal.lineRects.length;
|
||||
reveal[side] = sideReveal;
|
||||
});
|
||||
if (Object.keys(reveal).length) detail.reveal = reveal;
|
||||
detail.records = this.buildPageTextureRecords(sidesToPublish, detail);
|
||||
this.cachePublishedPages(sidesToPublish, detail);
|
||||
this.markPipelineTiming('publishSpread', {
|
||||
sides: sidesToPublish,
|
||||
hasReveal: Object.keys(reveal).length > 0,
|
||||
regionCounts,
|
||||
preloadOnly: Boolean(options.preloadOnly)
|
||||
phase
|
||||
});
|
||||
document.dispatchEvent(new CustomEvent('webgl-book:page-canvases', {
|
||||
document.dispatchEvent(new CustomEvent('webgl-book:page-texture-records', {
|
||||
detail
|
||||
}));
|
||||
return detail;
|
||||
}
|
||||
|
||||
buildPageTextureRecords(sides = [], detail = {}) {
|
||||
return sides.map((side) => ({
|
||||
side,
|
||||
phase: detail.phase || 'activate',
|
||||
canvas: detail[side] || null,
|
||||
pageMeta: detail.pageMeta?.[side] || null,
|
||||
reveal: detail.reveal?.[side] || null,
|
||||
state: {
|
||||
canvasReady: Boolean(detail[side]),
|
||||
vramReady: detail.phase === 'prepare',
|
||||
visible: detail.phase !== 'prepare'
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
buildPublishPageMeta(sides = []) {
|
||||
const baseMeta = this.currentSpread?.pageMeta || {};
|
||||
const spreadIndex = Math.max(0, Math.round(Number(this.currentSpread?.index || 0)));
|
||||
return sides.reduce((meta, side) => {
|
||||
const source = baseMeta[side] || null;
|
||||
if (!source) {
|
||||
meta[side] = null;
|
||||
return meta;
|
||||
}
|
||||
const pageIndex = side === 'left' ? spreadIndex * 2 : spreadIndex * 2 + 1;
|
||||
const source = baseMeta[side] || {
|
||||
kind: 'blank',
|
||||
section: pageIndex < 3 ? 'frontmatter' : 'body',
|
||||
pageIndex,
|
||||
pageNumber: null,
|
||||
omitPageNumber: true
|
||||
};
|
||||
const lines = Array.isArray(this.currentSpread?.[side]) ? this.currentSpread[side] : [];
|
||||
const maxBlockId = lines.reduce((max, line) => Math.max(max, Number(line?.blockId || 0)), 0);
|
||||
const lineCount = lines.length;
|
||||
const pageIndex = Number(source.pageIndex);
|
||||
const key = Number.isFinite(pageIndex) ? pageIndex : side;
|
||||
const normalizedPageIndex = Number(source.pageIndex);
|
||||
const key = Number.isFinite(normalizedPageIndex) ? normalizedPageIndex : side;
|
||||
const nextVersion = Math.max(1, Number(this.pageContentVersions.get(key) || 0) + 1);
|
||||
this.pageContentVersions.set(key, nextVersion);
|
||||
meta[side] = {
|
||||
...source,
|
||||
pageIndex: Number.isFinite(normalizedPageIndex) ? normalizedPageIndex : pageIndex,
|
||||
contentVersion: nextVersion,
|
||||
completenessScore: (maxBlockId * 1000) + lineCount,
|
||||
maxBlockId,
|
||||
lineCount
|
||||
};
|
||||
return meta;
|
||||
}, {
|
||||
left: Object.prototype.hasOwnProperty.call(baseMeta, 'left') ? baseMeta.left : null,
|
||||
right: Object.prototype.hasOwnProperty.call(baseMeta, 'right') ? baseMeta.right : null
|
||||
});
|
||||
}, {});
|
||||
}
|
||||
|
||||
cachePublishedPages(sides = [], detail = {}) {
|
||||
if (!this.pageCache || typeof this.pageCache.cachePageCanvas !== 'function') return;
|
||||
if (!this.pageCache || typeof this.pageCache.storePageCanvas !== 'function') return;
|
||||
sides.forEach((side) => {
|
||||
const canvas = detail[side];
|
||||
const pageMeta = detail.pageMeta?.[side] || null;
|
||||
if (!canvas || !pageMeta || !Number.isFinite(Number(pageMeta.pageIndex))) return;
|
||||
this.schedulePageCacheWrite(pageMeta, canvas);
|
||||
this.pageCache.storePageCanvas(pageMeta, canvas, { persist: true, resident: true });
|
||||
});
|
||||
}
|
||||
|
||||
schedulePageCacheWrite(pageMeta, canvas) {
|
||||
const frozenCanvas = this.cloneCanvas(canvas);
|
||||
const key = this.getPageCacheWriteKey(pageMeta, frozenCanvas);
|
||||
const pending = this.pendingPageCacheWrites.get(key);
|
||||
if (pending && this.isOlderPageMeta(pageMeta, pending.pageMeta)) return pending.promise;
|
||||
const previousWrite = pending?.promise || Promise.resolve();
|
||||
const write = previousWrite.catch(() => false).then(() => this.pageCache?.cachePageCanvas?.(pageMeta, frozenCanvas))
|
||||
.then((stored) => {
|
||||
if (!stored) {
|
||||
document.dispatchEvent(new CustomEvent('webgl-book:page-cache-problem', {
|
||||
detail: {
|
||||
type: 'db-write-failed',
|
||||
pageIndex: pageMeta?.pageIndex ?? null,
|
||||
key
|
||||
}
|
||||
}));
|
||||
}
|
||||
return stored;
|
||||
})
|
||||
.catch((error) => {
|
||||
document.dispatchEvent(new CustomEvent('webgl-book:page-cache-problem', {
|
||||
detail: {
|
||||
type: 'db-write-error',
|
||||
pageIndex: pageMeta?.pageIndex ?? null,
|
||||
key,
|
||||
message: error?.message || String(error)
|
||||
}
|
||||
}));
|
||||
return false;
|
||||
})
|
||||
.finally(() => {
|
||||
if (this.pendingPageCacheWrites.get(key)?.promise === write) {
|
||||
this.pendingPageCacheWrites.delete(key);
|
||||
}
|
||||
});
|
||||
this.pendingPageCacheWrites.set(key, {
|
||||
promise: write,
|
||||
pageMeta: { ...(pageMeta || {}) }
|
||||
});
|
||||
return write;
|
||||
}
|
||||
|
||||
isOlderPageMeta(incoming = {}, existing = null) {
|
||||
if (!existing) return false;
|
||||
const incomingCompleteness = Math.max(0, Number(incoming?.completenessScore || 0));
|
||||
const existingCompleteness = Math.max(0, Number(existing?.completenessScore || 0));
|
||||
if (incomingCompleteness < existingCompleteness) return true;
|
||||
if (incomingCompleteness > existingCompleteness) return false;
|
||||
const incomingVersion = Math.max(0, Number(incoming?.contentVersion || 0));
|
||||
const existingVersion = Math.max(0, Number(existing?.contentVersion || 0));
|
||||
return incomingVersion > 0 && existingVersion > incomingVersion;
|
||||
}
|
||||
|
||||
getPageCacheWriteKey(pageMeta = {}, canvas = null) {
|
||||
if (this.pageCache && typeof this.pageCache.makePageKey === 'function') {
|
||||
return this.pageCache.makePageKey({
|
||||
...pageMeta,
|
||||
width: canvas?.width ?? pageMeta.width,
|
||||
height: canvas?.height ?? pageMeta.height
|
||||
});
|
||||
}
|
||||
return `${pageMeta.cacheKey || window.MODULE_CACHE_BUSTER || 'dev'}:page:${pageMeta.pageIndex}:${canvas?.width || pageMeta.width}x${canvas?.height || pageMeta.height}`;
|
||||
}
|
||||
|
||||
getPageCanvas(side) {
|
||||
return this.canvases[side] || null;
|
||||
}
|
||||
|
||||
@@ -937,8 +937,8 @@ class SentenceQueueModule extends BaseModule {
|
||||
cueTimings: sentence.animation?.cueTimings || [],
|
||||
totalDuration: sentence.animation?.totalDuration || 0,
|
||||
spread,
|
||||
preloadOnly: true
|
||||
}, { preloadOnly: true });
|
||||
phase: 'prepare'
|
||||
}, { phase: 'prepare' });
|
||||
sentence.webglBookPresentation = {
|
||||
prepared: true,
|
||||
blockId,
|
||||
|
||||
@@ -1081,10 +1081,10 @@ class UIDisplayHandlerModule extends BaseModule {
|
||||
cueTimings: sentence.animation?.cueTimings || [],
|
||||
totalDuration: sentence.animation?.totalDuration || 0,
|
||||
spread: previewSpread,
|
||||
preloadOnly: true
|
||||
phase: 'prepare'
|
||||
};
|
||||
if (previewSpread && typeof bookTextureRenderer.prepareRevealBlock === 'function') {
|
||||
bookTextureRenderer.prepareRevealBlock(previewRevealDetail, { preloadOnly: true });
|
||||
bookTextureRenderer.prepareRevealBlock(previewRevealDetail, { phase: 'prepare' });
|
||||
}
|
||||
if (Number(previewSpread?.index || 0) > currentSpreadIndex) {
|
||||
const flipped = await this.waitForWebGLPageFlip({
|
||||
|
||||
+244
-220
@@ -213,6 +213,7 @@ let activeFlips = [];
|
||||
let pendingPageFlips = 0;
|
||||
const pendingRevealStartBlockIds = new Set();
|
||||
const activeRevealBlockStarts = new Map();
|
||||
let lastFlipTexturePreflight = null;
|
||||
|
||||
const paperColor = new THREE.Color(0xece4ca);
|
||||
const inkColor = '#1a1009';
|
||||
@@ -246,19 +247,24 @@ function createPageCanvasTexture(sourceCanvas) {
|
||||
}
|
||||
|
||||
function getBlankPageTexture() {
|
||||
if (blankPageTexture) return blankPageTexture;
|
||||
blankPageTexture = createPageCanvasTexture(createPageCanvas('blank'));
|
||||
return blankPageTexture;
|
||||
return pageTextureStore?.getBlankTexture?.() || createPageCanvasTexture(createPageCanvas('blank'));
|
||||
}
|
||||
|
||||
const preparedPageTextures = {
|
||||
left: new Map(),
|
||||
right: new Map()
|
||||
};
|
||||
const residentPageTextures = new Map();
|
||||
const maxResidentPageTextures = 192;
|
||||
let blankPageTexture = null;
|
||||
const pageCacheProblemLog = [];
|
||||
const pageTextureStore = window.moduleRegistry?.getModule?.('webgl-page-cache') || window.WebGLPageCache || null;
|
||||
pageTextureStore?.configureTextureRuntime?.({
|
||||
THREE,
|
||||
renderer,
|
||||
configureTexture: configurePageCanvasTexture,
|
||||
createBlankCanvas: () => createPageCanvas('blank'),
|
||||
maxResidentTextureCount: maxResidentPageTextures,
|
||||
maxPreparedTextureCount: 128
|
||||
});
|
||||
pageTextureStore?.registerVisibleTexture?.('left', leftTexture, leftCanvas);
|
||||
pageTextureStore?.registerVisibleTexture?.('right', rightTexture, rightCanvas);
|
||||
await reportLabStep(50, 'Initializing page texture store VRAM window');
|
||||
pageTextureStore?.getBlankTexture?.();
|
||||
await prewarmNavigationTextureWindow('loader-prime', { recordMiss: false });
|
||||
let currentPageMeta = {
|
||||
left: null,
|
||||
right: null
|
||||
@@ -589,14 +595,18 @@ window.BookLabDebug = {
|
||||
};
|
||||
},
|
||||
getRuntimeInvariants() {
|
||||
const textureStoreState = pageTextureStore?.getRuntimeState?.() || {};
|
||||
return {
|
||||
targetFrameDurationMs,
|
||||
residentPageTextureCount: residentPageTextures.size,
|
||||
residentPageTextureCount: textureStoreState.residentTextureCount || 0,
|
||||
maxResidentPageTextures,
|
||||
pageCacheProblemCount: pageCacheProblemLog.length,
|
||||
pageCacheProblemCount: textureStoreState.problemCount || 0,
|
||||
preparedPageTextureCount: textureStoreState.preparedTextureCount || 0,
|
||||
singlePageTextureStore: true,
|
||||
flipFrontBackShareMaterial: materials.flipPageSurface === materials.flipPageBackSurface,
|
||||
mirrorRefreshesEveryFrame: true,
|
||||
mirrorRefreshesWhenStaticDirty: true
|
||||
mirrorRefreshesWhenStaticDirty: true,
|
||||
lastFlipTexturePreflight
|
||||
};
|
||||
},
|
||||
projectPointerToPage(clientX, clientY) {
|
||||
@@ -610,7 +620,7 @@ window.BookLabDebug = {
|
||||
};
|
||||
|
||||
window.addEventListener('resize', resize);
|
||||
document.addEventListener('webgl-book:page-canvases', handlePageCanvases);
|
||||
document.addEventListener('webgl-book:page-texture-records', handlePageTextureRecords);
|
||||
document.addEventListener('webgl-book:page-reveal-start', (event) => {
|
||||
startPageRevealForBlock(event.detail?.blockId);
|
||||
});
|
||||
@@ -621,7 +631,7 @@ document.addEventListener('webgl-book:reveal-committed', (event) => {
|
||||
handleRevealCommittedForPageFlip(event.detail || {});
|
||||
});
|
||||
document.addEventListener('webgl-book:page-cache-problem', (event) => {
|
||||
recordPageCacheProblem(event.detail || {});
|
||||
pageTextureStore?.recordProblem?.(event.detail || {});
|
||||
});
|
||||
document.addEventListener('book-pagination:spread-updated', (event) => {
|
||||
const detail = event.detail || {};
|
||||
@@ -630,7 +640,7 @@ document.addEventListener('book-pagination:spread-updated', (event) => {
|
||||
const latestRenderedBlockId = Math.max(0, Number(detail.latestRenderedBlockId || 0));
|
||||
if (
|
||||
latestBlockId > latestRenderedBlockId
|
||||
&& detail.allowFutureUnrendered !== true
|
||||
&& detail.visibility !== 'future-ready'
|
||||
&& activeFlips.length === 0
|
||||
&& incomingSpreadIndex > Math.max(0, Number(bookPaginationState.spreadIndex || 0))
|
||||
) {
|
||||
@@ -2028,35 +2038,34 @@ function syncBottomNavigation() {
|
||||
bottomNavigation.endButton.disabled = activeFlips.length > 0 || currentPage >= navigableLimit;
|
||||
}
|
||||
|
||||
function handlePageCanvases(event) {
|
||||
const detail = event.detail || {};
|
||||
function handlePageTextureRecords(event) {
|
||||
const detail = normalizePageTextureRecordDetail(event.detail || {});
|
||||
if (detail.pageMeta) {
|
||||
const hasLeftMeta = Object.prototype.hasOwnProperty.call(detail.pageMeta, 'left');
|
||||
const hasRightMeta = Object.prototype.hasOwnProperty.call(detail.pageMeta, 'right');
|
||||
currentPageMeta = {
|
||||
left: hasLeftMeta ? detail.pageMeta.left : currentPageMeta.left || null,
|
||||
right: hasRightMeta ? detail.pageMeta.right : currentPageMeta.right || null
|
||||
};
|
||||
currentPageMeta = normalizePageMetaPair(detail.pageMeta, currentPageMeta);
|
||||
}
|
||||
markPageTextureTiming('handlePageCanvases:start', {
|
||||
markPageTextureTiming('handlePageTextureRecords:start', {
|
||||
hasLeft: Boolean(detail.left),
|
||||
hasRight: Boolean(detail.right),
|
||||
revealSides: Object.keys(detail.reveal || {}),
|
||||
preloadOnly: Boolean(detail.preloadOnly),
|
||||
phase: detail.phase || 'activate',
|
||||
pageMeta: currentPageMeta
|
||||
});
|
||||
const leftReveal = attachRevealPageMeta(detail.reveal?.left, detail.pageMeta?.left || currentPageMeta.left || null);
|
||||
const rightReveal = attachRevealPageMeta(detail.reveal?.right, detail.pageMeta?.right || currentPageMeta.right || null);
|
||||
if (detail.preloadOnly) {
|
||||
const leftReveal = attachRevealPageMeta(detail.reveal?.left, currentPageMeta.left || null);
|
||||
const rightReveal = attachRevealPageMeta(detail.reveal?.right, currentPageMeta.right || null);
|
||||
if (detail.phase === 'prepare') {
|
||||
if (detail.left) {
|
||||
const texture = preloadPageTexture('left', detail.left, leftReveal, detail.pageMeta?.left || null);
|
||||
rememberResidentPageTexture(detail.pageMeta?.left || null, texture, detail.left);
|
||||
const texture = preloadPageTexture('left', detail.left, leftReveal, currentPageMeta.left);
|
||||
pageTextureStore?.rememberResidentTexture?.(currentPageMeta.left, texture, detail.left, true);
|
||||
} else if (currentPageMeta.left?.kind === 'blank') {
|
||||
pageTextureStore?.rememberResidentTexture?.(currentPageMeta.left, getBlankPageTexture(), null, false);
|
||||
}
|
||||
if (detail.right) {
|
||||
const texture = preloadPageTexture('right', detail.right, rightReveal, detail.pageMeta?.right || null);
|
||||
rememberResidentPageTexture(detail.pageMeta?.right || null, texture, detail.right);
|
||||
const texture = preloadPageTexture('right', detail.right, rightReveal, currentPageMeta.right);
|
||||
pageTextureStore?.rememberResidentTexture?.(currentPageMeta.right, texture, detail.right, true);
|
||||
} else if (currentPageMeta.right?.kind === 'blank') {
|
||||
pageTextureStore?.rememberResidentTexture?.(currentPageMeta.right, getBlankPageTexture(), null, false);
|
||||
}
|
||||
markPageTextureTiming('handlePageCanvases:preloadOnly:end');
|
||||
markPageTextureTiming('handlePageTextureRecords:prepare:end');
|
||||
return;
|
||||
}
|
||||
if (detail.left) {
|
||||
@@ -2073,13 +2082,110 @@ function handlePageCanvases(event) {
|
||||
uploadPageTextureDirect('right', detail.right, currentPageMeta.right);
|
||||
}
|
||||
}
|
||||
if (!detail.left && currentPageMeta.left?.kind === 'blank') {
|
||||
applyExplicitBlankPageTexture('left', currentPageMeta.left, 'page-texture-records');
|
||||
}
|
||||
if (!detail.right && currentPageMeta.right?.kind === 'blank') {
|
||||
applyExplicitBlankPageTexture('right', currentPageMeta.right, 'page-texture-records');
|
||||
}
|
||||
markStaticSceneBuffersDirty();
|
||||
document.documentElement.dataset.webglPageTextureMetrics = JSON.stringify({
|
||||
width: leftCanvas.width,
|
||||
height: leftCanvas.height,
|
||||
source: 'book-texture-renderer'
|
||||
});
|
||||
markPageTextureTiming('handlePageCanvases:end');
|
||||
markPageTextureTiming('handlePageTextureRecords:end');
|
||||
prewarmNavigationTextureWindow('page-texture-records').catch((error) => {
|
||||
pageTextureStore?.recordProblem?.({
|
||||
type: 'navigation-window-prewarm-error',
|
||||
message: error?.message || String(error)
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function normalizePageTextureRecordDetail(detail = {}) {
|
||||
if (!Array.isArray(detail.records) || detail.records.length === 0) {
|
||||
return {
|
||||
...detail,
|
||||
phase: detail.phase === 'prepare' ? 'prepare' : 'activate'
|
||||
};
|
||||
}
|
||||
return detail.records.reduce((normalized, record) => {
|
||||
const side = record?.side === 'right' ? 'right' : 'left';
|
||||
normalized[side] = record.canvas || normalized[side] || null;
|
||||
normalized.pageMeta[side] = record.pageMeta || detail.pageMeta?.[side] || normalized.pageMeta[side] || null;
|
||||
if (record.reveal) normalized.reveal[side] = record.reveal;
|
||||
return normalized;
|
||||
}, {
|
||||
metrics: detail.metrics,
|
||||
hitMaps: detail.hitMaps || {},
|
||||
sides: detail.records.map(record => record?.side).filter(Boolean),
|
||||
records: detail.records,
|
||||
reveal: {},
|
||||
pageMeta: {},
|
||||
phase: detail.phase === 'prepare' ? 'prepare' : 'activate',
|
||||
preparedFromCache: detail.preparedFromCache === true
|
||||
});
|
||||
}
|
||||
|
||||
function normalizePageMetaPair(pageMeta = {}, previousMeta = currentPageMeta) {
|
||||
const spreadIndex = getSpreadIndexFromPageMeta(pageMeta)
|
||||
?? getSpreadIndexFromPageMeta(previousMeta)
|
||||
?? Math.max(0, Math.round(Number(bookPaginationState.spreadIndex || 0)));
|
||||
const pageIndices = spreadPageIndices(spreadIndex);
|
||||
return {
|
||||
left: normalizePageSideMeta(pageMeta.left, pageIndices.left, 'left'),
|
||||
right: normalizePageSideMeta(pageMeta.right, pageIndices.right, 'right')
|
||||
};
|
||||
}
|
||||
|
||||
function getSpreadIndexFromPageMeta(pageMeta = {}) {
|
||||
const leftIndex = Number(pageMeta?.left?.pageIndex);
|
||||
if (Number.isFinite(leftIndex)) return Math.floor(Math.max(0, leftIndex) / 2);
|
||||
const rightIndex = Number(pageMeta?.right?.pageIndex);
|
||||
if (Number.isFinite(rightIndex)) return Math.floor(Math.max(0, rightIndex) / 2);
|
||||
return null;
|
||||
}
|
||||
|
||||
function normalizePageSideMeta(meta = null, pageIndex = 0, side = 'left') {
|
||||
const index = Math.max(0, Math.round(Number(meta?.pageIndex ?? pageIndex)));
|
||||
if (!meta || meta.kind === 'blank') return makeBlankPageMeta(index, meta?.section || (index < 3 ? 'frontmatter' : 'body'));
|
||||
return {
|
||||
...meta,
|
||||
pageIndex: index,
|
||||
side
|
||||
};
|
||||
}
|
||||
|
||||
function makeBlankPageMeta(pageIndex = 0, section = 'body') {
|
||||
return {
|
||||
kind: 'blank',
|
||||
section,
|
||||
pageIndex: Math.max(0, Math.round(Number(pageIndex || 0))),
|
||||
pageNumber: null,
|
||||
omitPageNumber: true,
|
||||
lineCount: 0,
|
||||
maxBlockId: 0,
|
||||
completenessScore: 0,
|
||||
side: Math.max(0, Math.round(Number(pageIndex || 0))) % 2 === 0 ? 'left' : 'right'
|
||||
};
|
||||
}
|
||||
|
||||
function applyExplicitBlankPageTexture(side, pageMeta = null, reason = 'blank-page') {
|
||||
const material = side === 'left' ? materials.leftPage : materials.rightPage;
|
||||
const blankTexture = getBlankPageTexture();
|
||||
clearPageReveal(side, reason);
|
||||
if (material.map !== blankTexture) {
|
||||
material.map = blankTexture;
|
||||
material.needsUpdate = true;
|
||||
}
|
||||
pageTextureStore?.rememberResidentTexture?.(pageMeta || makeBlankPageMeta(side === 'left' ? 0 : 1), blankTexture, null, false);
|
||||
markPageTextureTiming('explicitBlankTexture', {
|
||||
side,
|
||||
pageIndex: pageMeta?.pageIndex ?? null,
|
||||
reason
|
||||
});
|
||||
return blankTexture;
|
||||
}
|
||||
|
||||
function attachRevealPageMeta(revealDetail = null, pageMeta = null) {
|
||||
@@ -2099,30 +2205,15 @@ function getRevealCacheKey(revealDetail = {}) {
|
||||
|
||||
function preloadPageTexture(side, sourceCanvas, revealDetail = {}, pageMeta = null) {
|
||||
if (!sourceCanvas) return null;
|
||||
const texture = createPageCanvasTexture(sourceCanvas);
|
||||
const baseTexture = revealDetail?.baseCanvas ? createPageCanvasTexture(revealDetail.baseCanvas) : null;
|
||||
const key = getRevealCacheKey({ ...(revealDetail || {}), pageMeta: revealDetail?.pageMeta || pageMeta || null });
|
||||
markPageTextureTiming('preloadTexture:start', {
|
||||
side,
|
||||
key,
|
||||
width: sourceCanvas.width,
|
||||
height: sourceCanvas.height,
|
||||
hasBaseTexture: Boolean(baseTexture)
|
||||
hasBaseTexture: Boolean(revealDetail?.baseCanvas)
|
||||
});
|
||||
preparedPageTextures[side].set(key, {
|
||||
texture,
|
||||
baseTexture,
|
||||
sourceCanvas,
|
||||
revealDetail,
|
||||
uploadedAt: performance.now()
|
||||
});
|
||||
if (preparedPageTextures[side].size > 128) {
|
||||
const oldestKey = preparedPageTextures[side].keys().next().value;
|
||||
const oldest = preparedPageTextures[side].get(oldestKey);
|
||||
oldest?.texture?.dispose?.();
|
||||
oldest?.baseTexture?.dispose?.();
|
||||
preparedPageTextures[side].delete(oldestKey);
|
||||
}
|
||||
const texture = pageTextureStore?.preparePageTexture?.(side, key, pageMeta, sourceCanvas, revealDetail) || null;
|
||||
markPageTextureTiming('preloadTexture:end', { side, key });
|
||||
return texture;
|
||||
}
|
||||
@@ -2142,54 +2233,6 @@ function setPageFlipActiveFlag() {
|
||||
}
|
||||
}
|
||||
|
||||
function recordPageCacheProblem(detail = {}) {
|
||||
const entry = {
|
||||
...detail,
|
||||
at: performance.now()
|
||||
};
|
||||
pageCacheProblemLog.push(entry);
|
||||
if (pageCacheProblemLog.length > 80) pageCacheProblemLog.splice(0, pageCacheProblemLog.length - 80);
|
||||
document.documentElement.dataset.webglPageCacheProblems = JSON.stringify(pageCacheProblemLog);
|
||||
console.warn('WebGL page cache problem', entry);
|
||||
}
|
||||
|
||||
function rememberResidentPageTexture(pageMeta = null, texture = null, sourceCanvas = null, ownsTexture = true) {
|
||||
const pageIndex = Number(pageMeta?.pageIndex);
|
||||
if (!texture || !Number.isFinite(pageIndex) || pageIndex < 0) return null;
|
||||
const key = makeResidentPageTextureKey(pageMeta);
|
||||
const existing = residentPageTextures.get(key);
|
||||
if (isOlderPageTextureMeta(pageMeta, existing?.pageMeta)) return existing?.texture || null;
|
||||
if (existing?.ownsTexture && existing.texture && existing.texture !== texture) existing.texture.dispose?.();
|
||||
residentPageTextures.set(key, {
|
||||
texture,
|
||||
sourceCanvas: sourceCanvas || existing?.sourceCanvas || null,
|
||||
lastUsedAt: performance.now(),
|
||||
ownsTexture,
|
||||
pageMeta: {
|
||||
...(existing?.pageMeta || {}),
|
||||
...(pageMeta || {})
|
||||
}
|
||||
});
|
||||
while (residentPageTextures.size > maxResidentPageTextures) {
|
||||
const oldestKey = residentPageTextures.keys().next().value;
|
||||
const oldest = residentPageTextures.get(oldestKey);
|
||||
if (oldest?.ownsTexture) oldest.texture?.dispose?.();
|
||||
residentPageTextures.delete(oldestKey);
|
||||
}
|
||||
return texture;
|
||||
}
|
||||
|
||||
function isOlderPageTextureMeta(incoming = {}, existing = null) {
|
||||
if (!existing) return false;
|
||||
const incomingCompleteness = Math.max(0, Number(incoming?.completenessScore || 0));
|
||||
const existingCompleteness = Math.max(0, Number(existing?.completenessScore || 0));
|
||||
if (incomingCompleteness < existingCompleteness) return true;
|
||||
if (incomingCompleteness > existingCompleteness) return false;
|
||||
const incomingVersion = Math.max(0, Number(incoming?.contentVersion || 0));
|
||||
const existingVersion = Math.max(0, Number(existing?.contentVersion || 0));
|
||||
return incomingVersion > 0 && existingVersion > incomingVersion;
|
||||
}
|
||||
|
||||
function makePageMetaForCache(pageIndex) {
|
||||
const index = Math.max(0, Math.round(Number(pageIndex || 0)));
|
||||
const paginationMeta = getPaginationPageMeta(index) || {};
|
||||
@@ -2201,16 +2244,6 @@ function makePageMetaForCache(pageIndex) {
|
||||
};
|
||||
}
|
||||
|
||||
function makeResidentPageTextureKey(pageMetaOrIndex = {}) {
|
||||
const pageMeta = typeof pageMetaOrIndex === 'number'
|
||||
? makePageMetaForCache(pageMetaOrIndex)
|
||||
: pageMetaOrIndex || {};
|
||||
const pageIndex = Math.max(0, Math.round(Number(pageMeta.pageIndex || 0)));
|
||||
const kind = String(pageMeta.kind || 'content');
|
||||
const section = String(pageMeta.section || 'body');
|
||||
return `${pageIndex}:${kind}:${section}`;
|
||||
}
|
||||
|
||||
function spreadPageIndices(spreadIndex) {
|
||||
const spread = Math.max(0, Math.round(Number(spreadIndex || 0)));
|
||||
return {
|
||||
@@ -2219,67 +2252,6 @@ function spreadPageIndices(spreadIndex) {
|
||||
};
|
||||
}
|
||||
|
||||
function getResidentPageTexture(pageIndex) {
|
||||
const key = makeResidentPageTextureKey(pageIndex);
|
||||
const resident = residentPageTextures.get(key);
|
||||
if (!resident) return null;
|
||||
resident.lastUsedAt = performance.now();
|
||||
residentPageTextures.delete(key);
|
||||
residentPageTextures.set(key, resident);
|
||||
return resident.texture || null;
|
||||
}
|
||||
|
||||
function getResidentPageTextureForMeta(pageMeta = null) {
|
||||
const pageIndex = Number(pageMeta?.pageIndex);
|
||||
if (!Number.isFinite(pageIndex)) return null;
|
||||
const key = makeResidentPageTextureKey(pageMeta);
|
||||
const resident = residentPageTextures.get(key);
|
||||
if (!resident || isOlderPageTextureMeta(pageMeta, resident.pageMeta)) return null;
|
||||
return getResidentPageTexture(pageIndex);
|
||||
}
|
||||
|
||||
async function preloadCachedPageTexture(pageIndex) {
|
||||
const meta = makePageMetaForCache(pageIndex);
|
||||
const residentKey = makeResidentPageTextureKey(meta);
|
||||
if (residentPageTextures.has(residentKey)) {
|
||||
getResidentPageTexture(meta.pageIndex);
|
||||
return residentPageTextures.get(residentKey)?.texture || null;
|
||||
}
|
||||
const cache = window.WebGLPageCache || window.moduleRegistry?.getModule?.('webgl-page-cache') || null;
|
||||
const sourceCanvas = await cache?.getPageCanvas?.(meta);
|
||||
if (!sourceCanvas) {
|
||||
const pageMeta = getPaginationPageMeta(meta.pageIndex);
|
||||
if (pageMeta?.kind === 'blank') {
|
||||
const blankTexture = getBlankPageTexture();
|
||||
rememberResidentPageTexture({ ...meta, ...pageMeta }, blankTexture, null, false);
|
||||
return blankTexture;
|
||||
}
|
||||
recordPageCacheProblem({
|
||||
type: 'db-cache-miss',
|
||||
pageIndex: meta.pageIndex,
|
||||
width: meta.width,
|
||||
height: meta.height
|
||||
});
|
||||
return null;
|
||||
}
|
||||
const texture = createPageCanvasTexture(sourceCanvas);
|
||||
const cachedMeta = sourceCanvas.__webglPageCacheMeta || meta;
|
||||
residentPageTextures.set(residentKey, {
|
||||
texture,
|
||||
sourceCanvas,
|
||||
lastUsedAt: performance.now(),
|
||||
ownsTexture: true,
|
||||
pageMeta: cachedMeta
|
||||
});
|
||||
while (residentPageTextures.size > maxResidentPageTextures) {
|
||||
const oldestKey = residentPageTextures.keys().next().value;
|
||||
const oldest = residentPageTextures.get(oldestKey);
|
||||
if (oldest?.ownsTexture) oldest.texture?.dispose?.();
|
||||
residentPageTextures.delete(oldestKey);
|
||||
}
|
||||
return texture;
|
||||
}
|
||||
|
||||
function getPaginationPageMeta(pageIndex) {
|
||||
const index = Math.max(0, Math.round(Number(pageIndex || 0)));
|
||||
const spreadIndex = Math.floor(index / 2);
|
||||
@@ -2294,27 +2266,47 @@ function getPaginationPageMeta(pageIndex) {
|
||||
}
|
||||
|
||||
async function prewarmSpreadTextures(spreadIndex) {
|
||||
const indices = spreadPageIndices(spreadIndex);
|
||||
const [left, right] = await Promise.all([
|
||||
preloadCachedPageTexture(indices.left),
|
||||
preloadCachedPageTexture(indices.right)
|
||||
]);
|
||||
return {
|
||||
return pageTextureStore?.prewarmSpreadTextures?.(spreadIndex, makePageMetaForCache) || {
|
||||
spreadIndex: Math.max(0, Math.round(Number(spreadIndex || 0))),
|
||||
left,
|
||||
right
|
||||
left: null,
|
||||
right: null
|
||||
};
|
||||
}
|
||||
|
||||
async function prewarmNavigationTextureWindow(reason = 'navigation-window', options = {}) {
|
||||
const currentSpread = Math.max(0, Math.round(Number(bookPaginationState.spreadIndex || 0)));
|
||||
const endSpread = Math.max(
|
||||
0,
|
||||
Math.round(Number(bookPaginationState.spreadCount || 1)) - 1,
|
||||
pageToSpreadIndex(maxVisitedPagePosition)
|
||||
);
|
||||
markPageTextureTiming('textureStorePrewarm:start', {
|
||||
reason,
|
||||
currentSpread,
|
||||
endSpread
|
||||
});
|
||||
const result = await pageTextureStore?.prewarmNavigationWindow?.({
|
||||
currentSpread,
|
||||
targetSpread: options.targetSpread,
|
||||
endSpread,
|
||||
getPageMetaForIndex: makePageMetaForCache,
|
||||
recordMiss: options.recordMiss !== false
|
||||
});
|
||||
markPageTextureTiming('textureStorePrewarm:end', {
|
||||
reason,
|
||||
spreadCount: result ? Object.keys(result).length : 0
|
||||
});
|
||||
return result || {};
|
||||
}
|
||||
|
||||
async function prewarmFlipTextures(direction, targetSpread = null) {
|
||||
const currentSpread = Math.max(0, Number(bookPaginationState.spreadIndex || 0));
|
||||
const nextSpread = Number.isFinite(Number(targetSpread))
|
||||
? Math.max(0, Math.round(Number(targetSpread)))
|
||||
: Math.max(0, currentSpread + Math.sign(Number(direction || 0)));
|
||||
const [current, next] = await Promise.all([
|
||||
prewarmSpreadTextures(currentSpread),
|
||||
prewarmSpreadTextures(nextSpread)
|
||||
]);
|
||||
const windowMap = await prewarmNavigationTextureWindow('flip-prewarm', { targetSpread: nextSpread });
|
||||
const current = windowMap?.[currentSpread] || await prewarmSpreadTextures(currentSpread);
|
||||
const next = windowMap?.[nextSpread] || await prewarmSpreadTextures(nextSpread);
|
||||
return {
|
||||
current,
|
||||
next
|
||||
@@ -2323,19 +2315,22 @@ async function prewarmFlipTextures(direction, targetSpread = null) {
|
||||
|
||||
function takePreparedPageTexture(side, revealDetail = {}) {
|
||||
const key = getRevealCacheKey(revealDetail);
|
||||
const prepared = preparedPageTextures[side].get(key);
|
||||
const prepared = pageTextureStore?.takePreparedPageTexture?.(side, key) || null;
|
||||
if (!prepared) return null;
|
||||
preparedPageTextures[side].delete(key);
|
||||
markPageTextureTiming('preloadTexture:activate', { side, key });
|
||||
return prepared;
|
||||
}
|
||||
|
||||
function uploadPageTextureDirect(side, sourceCanvas, pageMeta = null) {
|
||||
if (pageMeta?.kind === 'blank') {
|
||||
applyExplicitBlankPageTexture(side, pageMeta, 'direct-upload');
|
||||
return;
|
||||
}
|
||||
const texture = side === 'left' ? leftTexture : rightTexture;
|
||||
const material = side === 'left' ? materials.leftPage : materials.rightPage;
|
||||
const shouldUseResidentTexture = pageMeta?.kind !== 'title';
|
||||
const residentTexture = shouldUseResidentTexture && Number.isFinite(Number(pageMeta?.pageIndex))
|
||||
? getResidentPageTextureForMeta(pageMeta)
|
||||
? pageTextureStore?.getResidentTextureForMeta?.(pageMeta)
|
||||
: null;
|
||||
markPageTextureTiming('directUpload:start', {
|
||||
side,
|
||||
@@ -2356,7 +2351,7 @@ function uploadPageTextureDirect(side, sourceCanvas, pageMeta = null) {
|
||||
material.needsUpdate = true;
|
||||
}
|
||||
bindPageTextureSource(side, texture, sourceCanvas);
|
||||
rememberResidentPageTexture(pageMeta, texture, sourceCanvas, false);
|
||||
pageTextureStore?.rememberResidentTexture?.(pageMeta, texture, sourceCanvas, false);
|
||||
markPageTextureTiming('directUpload:end', { side });
|
||||
}
|
||||
|
||||
@@ -2381,7 +2376,7 @@ function beginPageReveal(side, sourceCanvas, revealDetail = {}) {
|
||||
}
|
||||
bindPageTextureSource(side, texture, sourceCanvas);
|
||||
}
|
||||
const baseTexture = prepared?.baseTexture || (revealDetail?.baseCanvas ? createPageCanvasTexture(revealDetail.baseCanvas) : null);
|
||||
const baseTexture = prepared?.baseTexture || (revealDetail?.baseCanvas ? pageTextureStore?.createTextureFromCanvas?.(revealDetail.baseCanvas) : null);
|
||||
|
||||
const revealBlockIds = Array.isArray(revealDetail.blockIds) ? revealDetail.blockIds.map(value => String(value)) : [];
|
||||
const activeStartedAt = revealBlockIds
|
||||
@@ -2397,6 +2392,7 @@ function beginPageReveal(side, sourceCanvas, revealDetail = {}) {
|
||||
durationMs: Math.max(1, Number(revealDetail.durationMs || 1)),
|
||||
blockIds: revealBlockIds,
|
||||
baseTexture,
|
||||
pageFlipAfterReveal: revealDetail.pageFlipAfterReveal === true,
|
||||
fastForwarding: false,
|
||||
fastForwardStartedAt: null,
|
||||
fastForwardStartElapsedMs: 0,
|
||||
@@ -2408,12 +2404,12 @@ function beginPageReveal(side, sourceCanvas, revealDetail = {}) {
|
||||
if (shader?.uniforms?.bookRevealElapsedMs) {
|
||||
shader.uniforms.bookRevealElapsedMs.value = pageRevealState[side].visualElapsedMs;
|
||||
}
|
||||
if (side === 'right' && isRightBodyPageComplete()) {
|
||||
if (side === 'right' && revealDetail.pageFlipAfterReveal === true) {
|
||||
const targetSpread = Math.max(0, Math.round(Number(bookPaginationState.spreadIndex || 0)) + 1);
|
||||
prewarmFlipTextures(1, targetSpread).then(() => {
|
||||
markPageTextureTiming('rightPageReveal:flip-prewarm-ready', { targetSpread });
|
||||
}).catch((error) => {
|
||||
recordPageCacheProblem({
|
||||
pageTextureStore?.recordProblem?.({
|
||||
type: 'right-page-flip-prewarm-error',
|
||||
targetSpread,
|
||||
message: error?.message || String(error)
|
||||
@@ -2618,7 +2614,8 @@ function updatePageRevealAnimations(now) {
|
||||
document.dispatchEvent(new CustomEvent('webgl-book:reveal-committed', {
|
||||
detail: {
|
||||
side,
|
||||
blockIds: state.blockIds
|
||||
blockIds: state.blockIds,
|
||||
pageFlipAfterReveal: state.pageFlipAfterReveal === true
|
||||
}
|
||||
}));
|
||||
});
|
||||
@@ -2632,8 +2629,11 @@ function bindPageTextureSource(side, texture, sourceCanvas) {
|
||||
width: nextCanvas?.width || 0,
|
||||
height: nextCanvas?.height || 0
|
||||
});
|
||||
texture.image = sourceCanvas || fallbackCanvas;
|
||||
texture.needsUpdate = true;
|
||||
const boundTexture = pageTextureStore?.bindVisibleTextureSource?.(side, sourceCanvas) || null;
|
||||
if (!boundTexture) {
|
||||
texture.image = nextCanvas;
|
||||
texture.needsUpdate = true;
|
||||
}
|
||||
updatePageTextureDebugState(side, nextCanvas, sourceCanvas, true);
|
||||
markPageTextureTiming('bindPageTextureSource:end', { side });
|
||||
}
|
||||
@@ -2841,20 +2841,27 @@ function createPageFlip(direction, startTime, duration) {
|
||||
|
||||
function prepareStaticPageForFlip(flip, prewarm = null) {
|
||||
if (!flip) return false;
|
||||
const sourceMaterial = flip.sourcePageSide === 'left' ? materials.leftPage : materials.rightPage;
|
||||
const sourceTexture = sourceMaterial?.map || (flip.sourcePageSide === 'left' ? leftTexture : rightTexture);
|
||||
const sourceSide = flip.direction > 0 ? 'right' : 'left';
|
||||
const sourceTexture = resolveCurrentFlipSourceTexture(sourceSide);
|
||||
const sourcePageMeta = currentPageMeta?.[sourceSide] || getPaginationPageMeta(spreadPageIndices(bookPaginationState.spreadIndex)[sourceSide]) || null;
|
||||
const targetSpread = Number.isFinite(Number(flip.targetSpread))
|
||||
? Math.max(0, Math.round(Number(flip.targetSpread)))
|
||||
: Math.max(0, Math.round(Number(bookPaginationState.spreadIndex || 0)) + Math.sign(Number(flip.direction || 0)));
|
||||
const targetPages = spreadPageIndices(targetSpread);
|
||||
const targetBackPageIndex = flip.direction > 0 ? targetPages.left : targetPages.right;
|
||||
const targetBackSide = flip.direction > 0 ? 'left' : 'right';
|
||||
const targetBackPageIndex = targetPages[targetBackSide];
|
||||
const targetBackPageMeta = getPaginationPageMeta(targetBackPageIndex) || makeBlankPageMeta(targetBackPageIndex);
|
||||
const prewarmedBackTexture = flip.direction > 0 ? prewarm?.next?.left : prewarm?.next?.right;
|
||||
const residentBackTexture = prewarmedBackTexture || getResidentPageTexture(targetBackPageIndex);
|
||||
const requiresWrittenTexture = targetBackPageIndex <= Math.max(2, Number(bookPaginationState.writtenPageLimit || 0));
|
||||
if (!residentBackTexture && requiresWrittenTexture) {
|
||||
recordPageCacheProblem({
|
||||
type: 'flip-back-texture-missing',
|
||||
const backTexture = resolveFlipBackTexture(targetBackPageMeta, prewarmedBackTexture);
|
||||
const requiresWrittenTexture = targetBackPageMeta.kind !== 'blank'
|
||||
&& targetBackPageIndex <= Math.max(2, Number(bookPaginationState.writtenPageLimit || 0));
|
||||
if (!sourceTexture || (!backTexture && requiresWrittenTexture)) {
|
||||
pageTextureStore?.recordProblem?.({
|
||||
type: !sourceTexture ? 'flip-source-texture-missing' : 'flip-back-texture-missing',
|
||||
sourceSide,
|
||||
sourcePageIndex: sourcePageMeta?.pageIndex ?? null,
|
||||
targetBackPageIndex,
|
||||
targetBackKind: targetBackPageMeta.kind,
|
||||
targetSpread,
|
||||
direction: flip.direction,
|
||||
prewarmedCurrent: Boolean(prewarm?.current),
|
||||
@@ -2862,9 +2869,8 @@ function prepareStaticPageForFlip(flip, prewarm = null) {
|
||||
});
|
||||
return false;
|
||||
}
|
||||
const backTexture = residentBackTexture || getBlankPageTexture();
|
||||
materials.flipPageSurface.map = sourceTexture;
|
||||
materials.flipPageBackSurface.map = backTexture;
|
||||
materials.flipPageBackSurface.map = backTexture || getBlankPageTexture();
|
||||
materials.flipPageSurface.normalMap = materials.pageSurface.normalMap;
|
||||
materials.flipPageBackSurface.normalMap = materials.pageSurface.normalMap;
|
||||
materials.flipPageSurface.roughnessMap = materials.pageSurface.roughnessMap;
|
||||
@@ -2872,8 +2878,24 @@ function prepareStaticPageForFlip(flip, prewarm = null) {
|
||||
materials.flipPageSurface.needsUpdate = true;
|
||||
materials.flipPageBackSurface.needsUpdate = true;
|
||||
flip.sourceTexture = sourceTexture;
|
||||
flip.backTexture = backTexture;
|
||||
flip.sourcePageMeta = sourcePageMeta ? { ...sourcePageMeta } : null;
|
||||
flip.backTexture = backTexture || getBlankPageTexture();
|
||||
flip.backPageMeta = targetBackPageMeta ? { ...targetBackPageMeta } : null;
|
||||
flip.targetBackPageIndex = targetBackPageIndex;
|
||||
flip.sourcePageSide = sourceSide;
|
||||
lastFlipTexturePreflight = {
|
||||
direction: flip.direction,
|
||||
sourceSide,
|
||||
sourcePageIndex: sourcePageMeta?.pageIndex ?? null,
|
||||
sourceKind: sourcePageMeta?.kind || 'content',
|
||||
targetSpread,
|
||||
targetBackSide,
|
||||
targetBackPageIndex,
|
||||
targetBackKind: targetBackPageMeta.kind,
|
||||
hasSourceTexture: Boolean(sourceTexture),
|
||||
hasBackTexture: Boolean(backTexture || getBlankPageTexture()),
|
||||
sourceTextureMatchesBackTexture: sourceTexture === (backTexture || getBlankPageTexture())
|
||||
};
|
||||
if (flip.direction > 0) {
|
||||
const blankTexture = getBlankPageTexture();
|
||||
if (blankTexture && materials.rightPage.map !== blankTexture) {
|
||||
@@ -2890,15 +2912,27 @@ function prepareStaticPageForFlip(flip, prewarm = null) {
|
||||
}
|
||||
}
|
||||
markPageTextureTiming('flipTexturePreflight:ready', {
|
||||
direction: flip.direction,
|
||||
sourceSide: flip.sourcePageSide,
|
||||
targetSpread,
|
||||
targetBackPageIndex,
|
||||
usedResidentBackTexture: Boolean(residentBackTexture)
|
||||
...lastFlipTexturePreflight,
|
||||
usedResidentBackTexture: Boolean(backTexture && backTexture !== getBlankPageTexture())
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
function resolveCurrentFlipSourceTexture(side) {
|
||||
const pageMeta = currentPageMeta?.[side] || null;
|
||||
if (pageMeta?.kind === 'blank') return getBlankPageTexture();
|
||||
const resident = pageTextureStore?.getResidentTextureForMeta?.(pageMeta);
|
||||
if (resident) return resident;
|
||||
const material = side === 'left' ? materials.leftPage : materials.rightPage;
|
||||
return material?.map || null;
|
||||
}
|
||||
|
||||
function resolveFlipBackTexture(pageMeta = null, prewarmedTexture = null) {
|
||||
if (pageMeta?.kind === 'blank') return getBlankPageTexture();
|
||||
if (prewarmedTexture) return prewarmedTexture;
|
||||
return pageTextureStore?.getResidentTextureForMeta?.(pageMeta);
|
||||
}
|
||||
|
||||
function canPageFlip(direction) {
|
||||
if (!currentProceduralBookModel) return false;
|
||||
const currentPage = getCurrentPagePosition();
|
||||
@@ -2908,7 +2942,7 @@ function canPageFlip(direction) {
|
||||
}
|
||||
|
||||
function handleRevealCommittedForPageFlip(detail = {}) {
|
||||
if (detail.side !== 'right' || !isRightBodyPageComplete()) return;
|
||||
if (detail.side !== 'right' || detail.pageFlipAfterReveal !== true) return;
|
||||
if (activeFlips.length > 0 || pendingRightPageFlip) return;
|
||||
if (isChoiceAwaitingPlayer()) return;
|
||||
const autoplayFlip = isTtsPlaybackActive();
|
||||
@@ -2937,16 +2971,6 @@ async function tryStartPendingRightPageFlip(reason = 'pending', options = {}) {
|
||||
return flipped;
|
||||
}
|
||||
|
||||
function isRightBodyPageComplete() {
|
||||
const meta = currentPageMeta?.right || null;
|
||||
if (!meta || meta.section !== 'body' || meta.kind === 'blank' || meta.kind === 'title') return false;
|
||||
const rendererDebug = window.BookTextureRenderer?.currentSpread || null;
|
||||
const rightLines = Array.isArray(rendererDebug?.right) ? rendererDebug.right : [];
|
||||
const maxLine = rightLines.reduce((max, line) => Math.max(max, Number(line?.pageLine || 0) + Math.max(1, Number(line?.lineCount || 1))), 0);
|
||||
const expectedLines = Math.max(1, Number(meta.linesPerPage || 25));
|
||||
return maxLine >= expectedLines;
|
||||
}
|
||||
|
||||
function isChoiceAwaitingPlayer() {
|
||||
return document.documentElement.dataset.choiceAwaiting === 'true'
|
||||
|| document.body?.dataset?.choiceAwaiting === 'true'
|
||||
|
||||
@@ -18,16 +18,63 @@ class WebGLPageCacheModule extends BaseModule {
|
||||
this.maxCacheSizeBytes = 5 * 1024 * 1024 * 1024;
|
||||
this.memoryCanvasCache = new Map();
|
||||
this.maxMemoryCanvasCount = 256;
|
||||
this.textureRuntime = null;
|
||||
this.residentTextures = new Map();
|
||||
this.maxResidentTextureCount = 192;
|
||||
this.preparedTextures = {
|
||||
left: new Map(),
|
||||
right: new Map()
|
||||
};
|
||||
this.preparedRevealPlans = new Map();
|
||||
this.visibleTextures = {
|
||||
left: null,
|
||||
right: null
|
||||
};
|
||||
this.visibleFallbackCanvases = {
|
||||
left: null,
|
||||
right: null
|
||||
};
|
||||
this.maxPreparedTextureCount = 128;
|
||||
this.blankTexture = null;
|
||||
this.problemLog = [];
|
||||
this.pendingPageWrites = new Map();
|
||||
|
||||
this.bindMethods([
|
||||
'initialize',
|
||||
'openDB',
|
||||
'configureTextureRuntime',
|
||||
'cachePageCanvas',
|
||||
'getPageCanvas',
|
||||
'putPageCanvas',
|
||||
'storePageCanvas',
|
||||
'preparePageTexture',
|
||||
'takePreparedPageTexture',
|
||||
'rememberPreparedRevealPlan',
|
||||
'takePreparedRevealPlan',
|
||||
'hasPreparedRevealPlan',
|
||||
'registerVisibleTexture',
|
||||
'bindVisibleTextureSource',
|
||||
'getVisibleTexture',
|
||||
'rememberResidentTexture',
|
||||
'getResidentTexture',
|
||||
'getResidentTextureForMeta',
|
||||
'ensurePageTexture',
|
||||
'prewarmPageTexture',
|
||||
'prewarmSpreadTextures',
|
||||
'prewarmNavigationWindow',
|
||||
'getBlankTexture',
|
||||
'createTextureFromCanvas',
|
||||
'disposeTextureRecord',
|
||||
'makePageKey',
|
||||
'getPageWriteKey',
|
||||
'makeResidentKey',
|
||||
'cloneCanvas',
|
||||
'canvasToBlob',
|
||||
'blobToCanvas',
|
||||
'isOlderPageEntry',
|
||||
'isOlderPageMeta',
|
||||
'recordProblem',
|
||||
'getRuntimeState',
|
||||
'manageCacheSize',
|
||||
'calculateTotalCacheSize',
|
||||
'deleteEntry',
|
||||
@@ -53,6 +100,25 @@ class WebGLPageCacheModule extends BaseModule {
|
||||
}
|
||||
}
|
||||
|
||||
configureTextureRuntime({
|
||||
THREE = null,
|
||||
renderer = null,
|
||||
configureTexture = null,
|
||||
createBlankCanvas = null,
|
||||
maxResidentTextureCount = this.maxResidentTextureCount,
|
||||
maxPreparedTextureCount = this.maxPreparedTextureCount
|
||||
} = {}) {
|
||||
this.textureRuntime = {
|
||||
THREE,
|
||||
renderer,
|
||||
configureTexture,
|
||||
createBlankCanvas
|
||||
};
|
||||
this.maxResidentTextureCount = Math.max(1, Math.round(Number(maxResidentTextureCount || this.maxResidentTextureCount)));
|
||||
this.maxPreparedTextureCount = Math.max(1, Math.round(Number(maxPreparedTextureCount || this.maxPreparedTextureCount)));
|
||||
return this.getRuntimeState();
|
||||
}
|
||||
|
||||
openDB() {
|
||||
if (this.db) return Promise.resolve(this.db);
|
||||
return new Promise((resolve, reject) => {
|
||||
@@ -93,6 +159,303 @@ class WebGLPageCacheModule extends BaseModule {
|
||||
return `${cacheKey}:page:${safePage}:${safeKind}:${safeSection}:${safeWidth}x${safeHeight}`;
|
||||
}
|
||||
|
||||
getPageWriteKey(pageMeta = {}, canvas = null) {
|
||||
return this.makePageKey({
|
||||
...pageMeta,
|
||||
width: canvas?.width ?? pageMeta.width,
|
||||
height: canvas?.height ?? pageMeta.height
|
||||
});
|
||||
}
|
||||
|
||||
makeResidentKey(pageMetaOrIndex = {}) {
|
||||
const pageMeta = typeof pageMetaOrIndex === 'number'
|
||||
? { pageIndex: pageMetaOrIndex }
|
||||
: pageMetaOrIndex || {};
|
||||
const pageIndex = Math.max(0, Math.round(Number(pageMeta.pageIndex || 0)));
|
||||
const kind = String(pageMeta.kind || 'content').replace(/[^a-z0-9_-]/gi, '');
|
||||
const section = String(pageMeta.section || 'body').replace(/[^a-z0-9_-]/gi, '');
|
||||
return `${pageIndex}:${kind}:${section}`;
|
||||
}
|
||||
|
||||
createTextureFromCanvas(canvas = null) {
|
||||
const runtime = this.textureRuntime || {};
|
||||
if (!canvas || !runtime.THREE?.CanvasTexture) return null;
|
||||
const texture = new runtime.THREE.CanvasTexture(canvas);
|
||||
if (typeof runtime.configureTexture === 'function') runtime.configureTexture(texture);
|
||||
texture.needsUpdate = true;
|
||||
if (typeof runtime.renderer?.initTexture === 'function') {
|
||||
runtime.renderer.initTexture(texture);
|
||||
texture.needsUpdate = false;
|
||||
}
|
||||
return texture;
|
||||
}
|
||||
|
||||
getBlankTexture() {
|
||||
if (this.blankTexture) return this.blankTexture;
|
||||
const canvas = this.textureRuntime?.createBlankCanvas?.();
|
||||
this.blankTexture = this.createTextureFromCanvas(canvas);
|
||||
return this.blankTexture;
|
||||
}
|
||||
|
||||
async putPageCanvas(pageMeta = {}, canvas = null, options = {}) {
|
||||
const texture = options.resident === false
|
||||
? null
|
||||
: this.rememberResidentTexture(pageMeta, this.createTextureFromCanvas(canvas), canvas, true);
|
||||
if (options.persist !== false) {
|
||||
const stored = await this.cachePageCanvas(pageMeta, canvas);
|
||||
return texture || stored;
|
||||
}
|
||||
return texture;
|
||||
}
|
||||
|
||||
storePageCanvas(pageMeta = {}, canvas = null, options = {}) {
|
||||
if (!canvas || !pageMeta || !Number.isFinite(Number(pageMeta.pageIndex))) return Promise.resolve(false);
|
||||
const frozenCanvas = this.cloneCanvas(canvas);
|
||||
const key = this.getPageWriteKey(pageMeta, frozenCanvas);
|
||||
const pending = this.pendingPageWrites.get(key);
|
||||
if (pending && this.isOlderPageMeta(pageMeta, pending.pageMeta)) return pending.promise;
|
||||
const previousWrite = pending?.promise || Promise.resolve();
|
||||
const write = previousWrite.catch(() => false)
|
||||
.then(() => this.putPageCanvas(pageMeta, frozenCanvas, {
|
||||
persist: options.persist !== false,
|
||||
resident: options.resident !== false
|
||||
}))
|
||||
.then((stored) => {
|
||||
if (!stored) {
|
||||
this.recordProblem({
|
||||
type: 'db-write-failed',
|
||||
pageIndex: pageMeta?.pageIndex ?? null,
|
||||
key
|
||||
});
|
||||
}
|
||||
return stored;
|
||||
})
|
||||
.catch((error) => {
|
||||
this.recordProblem({
|
||||
type: 'db-write-error',
|
||||
pageIndex: pageMeta?.pageIndex ?? null,
|
||||
key,
|
||||
message: error?.message || String(error)
|
||||
});
|
||||
return false;
|
||||
})
|
||||
.finally(() => {
|
||||
if (this.pendingPageWrites.get(key)?.promise === write) {
|
||||
this.pendingPageWrites.delete(key);
|
||||
}
|
||||
});
|
||||
this.pendingPageWrites.set(key, {
|
||||
promise: write,
|
||||
pageMeta: { ...(pageMeta || {}) }
|
||||
});
|
||||
return write;
|
||||
}
|
||||
|
||||
cloneCanvas(canvas = null) {
|
||||
if (!canvas) return null;
|
||||
const clone = document.createElement('canvas');
|
||||
clone.width = canvas.width;
|
||||
clone.height = canvas.height;
|
||||
const context = clone.getContext('2d');
|
||||
if (context) context.drawImage(canvas, 0, 0);
|
||||
return clone;
|
||||
}
|
||||
|
||||
preparePageTexture(side = 'left', key = '', pageMeta = {}, canvas = null, revealDetail = {}) {
|
||||
if (!canvas || !key) return null;
|
||||
const normalizedSide = side === 'right' ? 'right' : 'left';
|
||||
const texture = this.createTextureFromCanvas(canvas);
|
||||
const baseTexture = revealDetail?.baseCanvas ? this.createTextureFromCanvas(revealDetail.baseCanvas) : null;
|
||||
this.preparedTextures[normalizedSide].set(key, {
|
||||
texture,
|
||||
baseTexture,
|
||||
sourceCanvas: canvas,
|
||||
revealDetail,
|
||||
pageMeta: { ...(pageMeta || {}) },
|
||||
uploadedAt: performance.now()
|
||||
});
|
||||
this.rememberResidentTexture(pageMeta, texture, canvas, false);
|
||||
while (this.preparedTextures[normalizedSide].size > this.maxPreparedTextureCount) {
|
||||
const oldestKey = this.preparedTextures[normalizedSide].keys().next().value;
|
||||
const oldest = this.preparedTextures[normalizedSide].get(oldestKey);
|
||||
this.disposeTextureRecord(oldest);
|
||||
this.preparedTextures[normalizedSide].delete(oldestKey);
|
||||
}
|
||||
return texture;
|
||||
}
|
||||
|
||||
takePreparedPageTexture(side = 'left', key = '') {
|
||||
const normalizedSide = side === 'right' ? 'right' : 'left';
|
||||
const prepared = this.preparedTextures[normalizedSide].get(key);
|
||||
if (!prepared) return null;
|
||||
this.preparedTextures[normalizedSide].delete(key);
|
||||
return prepared;
|
||||
}
|
||||
|
||||
rememberPreparedRevealPlan(blockId = '', prepared = null) {
|
||||
const id = String(blockId ?? '');
|
||||
if (!id || !prepared) return null;
|
||||
this.preparedRevealPlans.set(id, {
|
||||
...prepared,
|
||||
storedAt: performance.now()
|
||||
});
|
||||
while (this.preparedRevealPlans.size > this.maxPreparedTextureCount) {
|
||||
const oldestKey = this.preparedRevealPlans.keys().next().value;
|
||||
this.preparedRevealPlans.delete(oldestKey);
|
||||
}
|
||||
return prepared;
|
||||
}
|
||||
|
||||
takePreparedRevealPlan(blockId = '') {
|
||||
const id = String(blockId ?? '');
|
||||
const prepared = this.preparedRevealPlans.get(id);
|
||||
if (!prepared) return null;
|
||||
this.preparedRevealPlans.delete(id);
|
||||
return prepared;
|
||||
}
|
||||
|
||||
hasPreparedRevealPlan(blockId = '') {
|
||||
const id = String(blockId ?? '');
|
||||
return Boolean(id && this.preparedRevealPlans.has(id));
|
||||
}
|
||||
|
||||
registerVisibleTexture(side = 'left', texture = null, fallbackCanvas = null) {
|
||||
const normalizedSide = side === 'right' ? 'right' : 'left';
|
||||
this.visibleTextures[normalizedSide] = texture || null;
|
||||
this.visibleFallbackCanvases[normalizedSide] = fallbackCanvas || null;
|
||||
return texture || null;
|
||||
}
|
||||
|
||||
bindVisibleTextureSource(side = 'left', sourceCanvas = null) {
|
||||
const normalizedSide = side === 'right' ? 'right' : 'left';
|
||||
const texture = this.visibleTextures[normalizedSide];
|
||||
const canvas = sourceCanvas || this.visibleFallbackCanvases[normalizedSide] || null;
|
||||
if (!texture || !canvas) return null;
|
||||
texture.image = canvas;
|
||||
texture.needsUpdate = true;
|
||||
return texture;
|
||||
}
|
||||
|
||||
getVisibleTexture(side = 'left') {
|
||||
return this.visibleTextures[side === 'right' ? 'right' : 'left'] || null;
|
||||
}
|
||||
|
||||
rememberResidentTexture(pageMeta = {}, texture = null, sourceCanvas = null, ownsTexture = true) {
|
||||
const pageIndex = Number(pageMeta?.pageIndex);
|
||||
if (!texture || !Number.isFinite(pageIndex) || pageIndex < 0) return null;
|
||||
const key = this.makeResidentKey(pageMeta);
|
||||
const existing = this.residentTextures.get(key);
|
||||
if (this.isOlderPageMeta(pageMeta, existing?.pageMeta)) return existing?.texture || null;
|
||||
if (existing?.ownsTexture && existing.texture && existing.texture !== texture) existing.texture.dispose?.();
|
||||
this.residentTextures.set(key, {
|
||||
texture,
|
||||
sourceCanvas: sourceCanvas || existing?.sourceCanvas || null,
|
||||
lastUsedAt: performance.now(),
|
||||
ownsTexture,
|
||||
pageMeta: {
|
||||
...(existing?.pageMeta || {}),
|
||||
...(pageMeta || {})
|
||||
}
|
||||
});
|
||||
while (this.residentTextures.size > this.maxResidentTextureCount) {
|
||||
const oldestKey = this.residentTextures.keys().next().value;
|
||||
const oldest = this.residentTextures.get(oldestKey);
|
||||
if (oldest?.ownsTexture) oldest.texture?.dispose?.();
|
||||
this.residentTextures.delete(oldestKey);
|
||||
}
|
||||
return texture;
|
||||
}
|
||||
|
||||
getResidentTexture(pageMetaOrIndex = {}) {
|
||||
const key = this.makeResidentKey(pageMetaOrIndex);
|
||||
const resident = this.residentTextures.get(key);
|
||||
if (!resident) return null;
|
||||
resident.lastUsedAt = performance.now();
|
||||
this.residentTextures.delete(key);
|
||||
this.residentTextures.set(key, resident);
|
||||
return resident.texture || null;
|
||||
}
|
||||
|
||||
getResidentTextureForMeta(pageMeta = {}) {
|
||||
const pageIndex = Number(pageMeta?.pageIndex);
|
||||
if (!Number.isFinite(pageIndex)) return null;
|
||||
const key = this.makeResidentKey(pageMeta);
|
||||
const resident = this.residentTextures.get(key);
|
||||
if (!resident || this.isOlderPageMeta(pageMeta, resident.pageMeta)) return null;
|
||||
return this.getResidentTexture(pageMeta);
|
||||
}
|
||||
|
||||
async ensurePageTexture(pageMeta = {}, options = {}) {
|
||||
if (pageMeta?.kind === 'blank') {
|
||||
return this.rememberResidentTexture(pageMeta, this.getBlankTexture(), null, false);
|
||||
}
|
||||
const resident = this.getResidentTextureForMeta(pageMeta);
|
||||
if (resident) return resident;
|
||||
if (options.canvas) return this.putPageCanvas(pageMeta, options.canvas, {
|
||||
persist: options.persist !== false,
|
||||
resident: true
|
||||
});
|
||||
const sourceCanvas = await this.getPageCanvas(pageMeta);
|
||||
if (!sourceCanvas) {
|
||||
if (options.recordMiss !== false) {
|
||||
this.recordProblem({
|
||||
type: 'db-cache-miss',
|
||||
pageIndex: pageMeta?.pageIndex ?? null,
|
||||
width: pageMeta?.width ?? null,
|
||||
height: pageMeta?.height ?? null
|
||||
});
|
||||
}
|
||||
return null;
|
||||
}
|
||||
const cachedMeta = sourceCanvas.__webglPageCacheMeta || pageMeta;
|
||||
return this.rememberResidentTexture(cachedMeta, this.createTextureFromCanvas(sourceCanvas), sourceCanvas, true);
|
||||
}
|
||||
|
||||
async prewarmPageTexture(pageMeta = {}, options = {}) {
|
||||
return this.ensurePageTexture(pageMeta, {
|
||||
recordMiss: options.recordMiss !== false && pageMeta?.kind !== 'blank'
|
||||
});
|
||||
}
|
||||
|
||||
async prewarmSpreadTextures(spreadIndex = 0, getPageMetaForIndex = null, options = {}) {
|
||||
const spread = Math.max(0, Math.round(Number(spreadIndex || 0)));
|
||||
const leftIndex = spread * 2;
|
||||
const rightIndex = leftIndex + 1;
|
||||
const leftMeta = getPageMetaForIndex?.(leftIndex) || { pageIndex: leftIndex, kind: 'blank', section: leftIndex < 3 ? 'frontmatter' : 'body' };
|
||||
const rightMeta = getPageMetaForIndex?.(rightIndex) || { pageIndex: rightIndex, kind: 'blank', section: rightIndex < 3 ? 'frontmatter' : 'body' };
|
||||
const [left, right] = await Promise.all([
|
||||
this.prewarmPageTexture(leftMeta, options),
|
||||
this.prewarmPageTexture(rightMeta, options)
|
||||
]);
|
||||
return { spreadIndex: spread, left, right };
|
||||
}
|
||||
|
||||
async prewarmNavigationWindow({
|
||||
currentSpread = 0,
|
||||
targetSpread = null,
|
||||
endSpread = 0,
|
||||
getPageMetaForIndex = null,
|
||||
recordMiss = true
|
||||
} = {}) {
|
||||
const current = Math.max(0, Math.round(Number(currentSpread || 0)));
|
||||
const end = Math.max(0, Math.round(Number(endSpread || 0)));
|
||||
const spreads = new Set([0, end, current, Math.max(0, current - 1), current + 1]);
|
||||
const explicitTarget = Number.isFinite(Number(targetSpread)) ? Math.max(0, Math.round(Number(targetSpread))) : null;
|
||||
if (explicitTarget !== null) spreads.add(explicitTarget);
|
||||
const upperBound = Math.max(end, current + 1, explicitTarget ?? 0);
|
||||
const bounded = Array.from(spreads).filter(value => value >= 0 && value <= upperBound);
|
||||
const results = await Promise.all(bounded.map(spread => this.prewarmSpreadTextures(spread, getPageMetaForIndex, { recordMiss })));
|
||||
return results.reduce((map, spread) => {
|
||||
map[spread.spreadIndex] = spread;
|
||||
return map;
|
||||
}, {});
|
||||
}
|
||||
|
||||
disposeTextureRecord(record = null) {
|
||||
record?.texture?.dispose?.();
|
||||
record?.baseTexture?.dispose?.();
|
||||
}
|
||||
|
||||
async cachePageCanvas(pageMeta = {}, canvas = null) {
|
||||
if (!canvas || !this.db || this.cacheStatus !== 'ready') return false;
|
||||
const pageIndex = Number(pageMeta.pageIndex);
|
||||
@@ -200,6 +563,43 @@ class WebGLPageCacheModule extends BaseModule {
|
||||
return incomingVersion > 0 && existingVersion > incomingVersion;
|
||||
}
|
||||
|
||||
isOlderPageMeta(incoming = {}, existing = null) {
|
||||
if (!existing) return false;
|
||||
const incomingCompleteness = Math.max(0, Number(incoming?.completenessScore || 0));
|
||||
const existingCompleteness = Math.max(0, Number(existing?.completenessScore || 0));
|
||||
if (incomingCompleteness < existingCompleteness) return true;
|
||||
if (incomingCompleteness > existingCompleteness) return false;
|
||||
const incomingVersion = Math.max(0, Number(incoming?.contentVersion || 0));
|
||||
const existingVersion = Math.max(0, Number(existing?.contentVersion || 0));
|
||||
return incomingVersion > 0 && existingVersion > incomingVersion;
|
||||
}
|
||||
|
||||
recordProblem(detail = {}) {
|
||||
const entry = {
|
||||
...detail,
|
||||
at: performance.now()
|
||||
};
|
||||
this.problemLog.push(entry);
|
||||
if (this.problemLog.length > 80) this.problemLog.splice(0, this.problemLog.length - 80);
|
||||
document.documentElement.dataset.webglPageCacheProblems = JSON.stringify(this.problemLog);
|
||||
console.warn('WebGL page texture store problem', entry);
|
||||
return entry;
|
||||
}
|
||||
|
||||
getRuntimeState() {
|
||||
return {
|
||||
cacheStatus: this.cacheStatus,
|
||||
residentTextureCount: this.residentTextures.size,
|
||||
maxResidentTextureCount: this.maxResidentTextureCount,
|
||||
preparedTextureCount: this.preparedTextures.left.size + this.preparedTextures.right.size,
|
||||
preparedRevealPlanCount: this.preparedRevealPlans.size,
|
||||
pendingPageWriteCount: this.pendingPageWrites.size,
|
||||
problemCount: this.problemLog.length,
|
||||
hasRuntime: Boolean(this.textureRuntime?.THREE && this.textureRuntime?.renderer),
|
||||
hasBlankTexture: Boolean(this.blankTexture)
|
||||
};
|
||||
}
|
||||
|
||||
canvasToBlob(canvas) {
|
||||
return new Promise((resolve) => {
|
||||
if (typeof canvas.toBlob !== 'function') {
|
||||
|
||||
@@ -128,7 +128,7 @@ const checks = [
|
||||
['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 source canvases directly instead of copying whole page textures', /bindPageTextureSource/.test(source) && /texture\.image = sourceCanvas/.test(source) && !/drawCanvasPageTexture/.test(methodBody(source, 'uploadPageTextureDirect')) && !/drawCanvasPageTexture/.test(methodBody(source, 'beginPageReveal'))],
|
||||
['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)],
|
||||
@@ -141,16 +141,16 @@ const checks = [
|
||||
['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 caches preload-only reveal canvases for later reuse', /preparedRevealCache/.test(textureRendererSource) && /preloadOnly/.test(textureRendererSource) && /publishPreparedReveal/.test(textureRendererSource) && /reusedPreparedCanvas/.test(textureRendererSource) && /hasPreparedRevealBlock/.test(textureRendererSource)],
|
||||
['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 persists frozen completed page canvases immediately and without duplicate work', /webgl-page-cache/.test(textureRendererSource) && /cachePublishedPages/.test(textureRendererSource) && /schedulePageCacheWrite/.test(textureRendererSource) && /pendingPageCacheWrites/.test(textureRendererSource) && /const frozenCanvas = this\.cloneCanvas\(canvas\)/.test(textureRendererSource) && !/requestIdleCallback/.test(methodBody(textureRendererSource, 'schedulePageCacheWrite')) && /cachePageCanvas/.test(textureRendererSource)],
|
||||
['webgl cache is non-optional with a 5gb persistent budget and large memory cache', /maxCacheSizeBytes = 5 \* 1024 \* 1024 \* 1024/.test(webglPageCacheSource) && /maxMemoryCanvasCount = 256/.test(webglPageCacheSource) && /persistent page caching is in a problem state/.test(webglPageCacheSource) && !/if \(this\.memoryCanvasCache\.has\(key\)\) return true/.test(webglPageCacheSource)],
|
||||
['webgl lab prewarms cached page textures into generous vram before flips', /residentPageTextures/.test(source) && /const maxResidentPageTextures = 192/.test(source) && /preloadCachedPageTexture/.test(source) && /prewarmFlipTextures/.test(source) && /await prewarmFlipTextures\(direction, targetSpread\)/.test(source) && /getResidentPageTexture\(targetBackPageIndex\)/.test(source)],
|
||||
['webgl lab records cache misses as problem states', /pageCacheProblemLog/.test(source) && /recordPageCacheProblem/.test(source) && /db-cache-miss/.test(source) && /webglPageCacheProblems/.test(source)],
|
||||
['webgl lab makes preload-only page canvases resident by explicit page metadata', /rememberResidentPageTexture/.test(source) && /attachRevealPageMeta/.test(source) && /rememberResidentPageTexture\(detail\.pageMeta\?\.left \|\| null, texture, detail\.left\)/.test(source) && /rememberResidentPageTexture\(detail\.pageMeta\?\.right \|\| null, texture, detail\.right\)/.test(source)],
|
||||
['webgl lab keeps current visible page textures resident without disposing shared maps', /rememberResidentPageTexture\(pageMeta, texture, sourceCanvas, false\)/.test(source) && /ownsTexture/.test(source) && /if \(oldest\?\.ownsTexture\) oldest\.texture\?\.dispose\?\.\(\)/.test(source)],
|
||||
['webgl lab reuses current-enough resident cached page textures for direct stack switches', /uploadPageTextureDirect\(side, sourceCanvas, pageMeta = null\)/.test(source) && /getResidentPageTextureForMeta\(pageMeta\)/.test(source) && /usedResidentTexture/.test(source) && /uploadPageTextureDirect\('left', detail\.left, currentPageMeta\.left\)/.test(source) && /uploadPageTextureDirect\('right', detail\.right, currentPageMeta\.right\)/.test(source)],
|
||||
['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?.(currentPageMeta.left, texture, detail.left, true)') && source.includes('pageTextureStore?.rememberResidentTexture?.(currentPageMeta.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, currentPageMeta\.left\)/.test(source) && /uploadPageTextureDirect\('right', detail\.right, currentPageMeta\.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)],
|
||||
@@ -159,7 +159,7 @@ const checks = [
|
||||
['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 waits for page flip before activating future spread', /sentence\.webglBookPresentation\?\.spread/.test(uiDisplayHandlerSource) && /preparePendingBlock\(sentence, \{\s*activate: false,\s*publish: false,\s*includeUnrenderedHistory: true\s*\}/.test(uiDisplayHandlerSource) && /waitForWebGLPageFlip/.test(uiDisplayHandlerSource) && /targetSpread: previewSpread\.index/.test(uiDisplayHandlerSource) && /webgl-book:request-page-flip/.test(uiDisplayHandlerSource) && /const targetSpread = Number\.isFinite\(Number\(detail\.targetSpread\)\)/.test(source) && /startPageFlip\(direction, \{[\s\S]*targetSpread/.test(source)],
|
||||
['texture renderer paints inline bold and italic styles', /getInlineStyleState/.test(textureRendererSource) && /updateInlineStyleState/.test(textureRendererSource) && /getCanvasFont/.test(textureRendererSource) && /segment\?\.style/.test(textureRendererSource)],
|
||||
['webgl lab can preload page textures without swapping visible page material', /preparedPageTextures/.test(source) && /preloadPageTexture/.test(source) && /renderer\.initTexture\(texture\)/.test(source) && /takePreparedPageTexture/.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)],
|
||||
@@ -175,20 +175,22 @@ const checks = [
|
||||
['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', allowFutureUnrendered: true \}\);/.test(bookPaginationSource)],
|
||||
['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 renderer draws title page and page numbers from versioned page metadata', /drawTitlePage/.test(textureRendererSource) && /game_title/.test(textureRendererSource) && /drawPageNumber/.test(textureRendererSource) && /pageMeta: this\.buildPublishPageMeta\(sidesToPublish\)/.test(textureRendererSource)],
|
||||
['texture renderer uses plural page margin metrics for page numbers', /this\.metrics\.margins\.bottom/.test(textureRendererSource) && !/this\.metrics\.margin\.bottom/.test(textureRendererSource)],
|
||||
['webgl flip borrows resident page texture and blanks right stack before forward animation', /prepareStaticPageForFlip/.test(source) && /materials\.flipPageSurface\.map = sourceTexture/.test(source) && /materials\.rightPage\.map = blankTexture/.test(source) && /webgl-book:page-flip-near-end/.test(source)],
|
||||
['webgl flip never falls back to the opposite visible stack for target back texture', /const prewarmedBackTexture = flip\.direction > 0 \? prewarm\?\.next\?\.left : prewarm\?\.next\?\.right/.test(source) && /const residentBackTexture = prewarmedBackTexture \|\| getResidentPageTexture\(targetBackPageIndex\)/.test(source) && /const backTexture = residentBackTexture \|\| getBlankPageTexture\(\)/.test(source) && !/oppositeMaterial\?\.map/.test(methodBody(source, 'prepareStaticPageForFlip'))],
|
||||
['webgl page canvas metadata accepts explicit blank sides instead of retaining stale pages', /hasLeftMeta/.test(source) && /hasRightMeta/.test(source) && /Object\.prototype\.hasOwnProperty\.call\(detail\.pageMeta, 'right'\)/.test(source)],
|
||||
['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 = 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 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 queues newer same-page cache writes instead of dropping them', /isOlderPageMeta/.test(textureRendererSource) && /const previousWrite = pending\?\.promise \|\| Promise\.resolve\(\)/.test(textureRendererSource) && /pendingPageCacheWrites\.set\(key, \{[\s\S]*pageMeta: \{ \.\.\.\(pageMeta \|\| \{\}\) \}/.test(textureRendererSource)],
|
||||
['webgl resident page texture cache rejects older page versions before direct reuse', /isOlderPageTextureMeta/.test(source) && /getResidentPageTextureForMeta/.test(source) && /getResidentPageTextureForMeta\(pageMeta\)/.test(source)],
|
||||
['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 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/.test(source) && /flip\.sourceTexture = sourceTexture/.test(source) && /flip\.backTexture = backTexture/.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 animated page back face uses destination-side page UV orientation', /pageUvForSide\(targetSide, u, v\)/.test(source) && /pageUvForSide\(sourceSide, u, v\)/.test(source) && /side < 0 \? 1 - u : u/.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 live mirror and static heavy refresh', /const targetFrameDurationMs = 1000 \/ 60/.test(source) && /this\.targetFrameDurationMs = 1000 \/ 60/.test(textureRendererSource) && /requestAnimationFrame\(animate\)/.test(source) && /const refreshStaticSceneBuffers = staticSceneBuffersDirty \|\| activeFlips\.length > 0/.test(source) && /updateTableReflection\(\);/.test(source) && /mirrorRefreshesEveryFrame: true/.test(source) && !/setTimeout\(animate/.test(source)],
|
||||
@@ -203,14 +205,14 @@ const checks = [
|
||||
['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 completion arms a durable autoplay-targeted flip without bypassing choices', /handleRevealCommittedForPageFlip/.test(source) && /tryStartPendingRightPageFlip/.test(source) && /pendingRightPageFlipAutoplay/.test(source) && /const targetSpread = Math\.max\(0, Math\.round\(Number\(bookPaginationState\.spreadIndex \|\| 0\)\) \+ 1\)/.test(source) && /force: options\.force === true \|\| pendingRightPageFlipAutoplay/.test(source) && /isChoiceAwaitingPlayer/.test(source) && /pendingRightPageFlip = true/.test(source)],
|
||||
['webgl right-page reveal records arm a durable autoplay-targeted flip without bypassing choices', /handleRevealCommittedForPageFlip/.test(source) && /pageFlipAfterReveal/.test(textureRendererSource) && /tryStartPendingRightPageFlip/.test(source) && /pendingRightPageFlipAutoplay/.test(source) && /const targetSpread = Math\.max\(0, Math\.round\(Number\(bookPaginationState\.spreadIndex \|\| 0\)\) \+ 1\)/.test(source) && /force: options\.force === true \|\| pendingRightPageFlipAutoplay/.test(source) && /isChoiceAwaitingPlayer/.test(source) && /pendingRightPageFlip = true/.test(source)],
|
||||
['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)],
|
||||
['texture renderer preloads every spread touched by an active reveal block', /preloadAdditionalRevealSpreads/.test(textureRendererSource) && /spreadContainsBlock/.test(textureRendererSource) && /this\.drawSpread\(spread, \['left', 'right'\], \{ preloadOnly: true \}\)/.test(textureRendererSource) && /this\.activeAnimations\.has\(id\)/.test(textureRendererSource)],
|
||||
['webgl visible spread state ignores future unrendered prepare publishes before flip', /spreadUpdate:deferred-future-unrendered/.test(source) && /incomingSpreadIndex > Math\.max\(0, Number\(bookPaginationState\.spreadIndex/.test(source) && /this\.drawSpread\(this\.currentSpread, \['left', 'right'\], \{ preloadOnly: true \}\)/.test(textureRendererSource)],
|
||||
['3D overflow reveal preloads target spread before forced page flip', /previewRevealDetail/.test(uiDisplayHandlerSource) && /preloadOnly: true/.test(uiDisplayHandlerSource) && /bookTextureRenderer\.prepareRevealBlock\(previewRevealDetail, \{ preloadOnly: true \}\)/.test(uiDisplayHandlerSource) && /await this\.waitForWebGLPageFlip/.test(uiDisplayHandlerSource)],
|
||||
['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) && /this\.activeAnimations\.has\(id\)/.test(textureRendererSource)],
|
||||
['webgl visible spread state ignores future prepared publishes before flip', /spreadUpdate:deferred-future-unrendered/.test(source) && /incomingSpreadIndex > Math\.max\(0, Number\(bookPaginationState\.spreadIndex/.test(source) && /this\.drawSpread\(this\.currentSpread, \['left', 'right'\], \{ phase: 'prepare' \}\)/.test(textureRendererSource)],
|
||||
['3D overflow reveal preloads target spread before forced page flip', /previewRevealDetail/.test(uiDisplayHandlerSource) && /phase: 'prepare'/.test(uiDisplayHandlerSource) && /bookTextureRenderer\.prepareRevealBlock\(previewRevealDetail, \{ phase: 'prepare' \}\)/.test(uiDisplayHandlerSource) && /await this\.waitForWebGLPageFlip/.test(uiDisplayHandlerSource)],
|
||||
['webgl navigation buttons use visited page limit instead of future prepared pages', /maxVisitedPagePosition/.test(source) && /navigateToPagePosition\(maxVisitedPagePosition\)/.test(source) && /const navigableLimit = Math\.min\(maxVisitedPagePosition, writableLimit\)/.test(source) && !/navigateToPagePosition\(bookPaginationState\.writtenPageLimit\)/.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 back textures before animation starts', /prepareStaticPageForFlip\(flip, prewarm = null\)/.test(source) && /flip-back-texture-missing/.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 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)]
|
||||
|
||||
Reference in New Issue
Block a user