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.
|
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
|
## Current Implementation Snapshot
|
||||||
|
|
||||||
This section records the current state after the procedural book integration work.
|
This section records the current state after the procedural book integration work.
|
||||||
|
|
||||||
- The active standalone scene is `public/webgl-book-lab.html`.
|
- The active integrated scene is loaded through the game at `http://localhost:3001/`.
|
||||||
- The intended local test URL is `http://localhost:3001/webgl-book-lab.html`.
|
- `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 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 procedural book model lives in `public/js/procedural-book-model.js`.
|
||||||
- The WebGL lab integration lives in `public/js/webgl-book-lab.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`.
|
- `PAGE_SPLINE_LENGTH` must match `PAGE_WIDTH`.
|
||||||
- Cover width is derived from page width plus cover overhang.
|
- 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 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.
|
- Slow page flips and fast 10-page transitions are implemented.
|
||||||
- Fast transitions run overlapping flip animations before shifting the book by one bundle.
|
- 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.
|
- 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.
|
- 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.
|
- 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
|
## Non-Negotiable Workflow Rules
|
||||||
|
|
||||||
- Do not continue visual coding without a concrete plan for the current sprint.
|
- 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.pages = this.buildPages([]);
|
||||||
this.spreads = this.buildSpreadsFromPages(this.pages);
|
this.spreads = this.buildSpreadsFromPages(this.pages);
|
||||||
this.currentSpreadIndex = 0;
|
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');
|
this.reportProgress(100, 'Book pagination ready');
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -146,7 +146,7 @@ class BookPaginationModule extends BaseModule {
|
|||||||
: renderedSpreadIndex >= 0
|
: renderedSpreadIndex >= 0
|
||||||
? renderedSpreadIndex
|
? renderedSpreadIndex
|
||||||
: Math.max(0, Math.min(this.currentSpreadIndex, Math.max(0, this.spreads.length - 1)));
|
: 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) {
|
getContinuationBlockId(latestBlockId = 0, latestRenderedBlockId = 0) {
|
||||||
@@ -187,7 +187,7 @@ class BookPaginationModule extends BaseModule {
|
|||||||
spreadIndex: cached.targetSpread?.index ?? this.currentSpreadIndex,
|
spreadIndex: cached.targetSpread?.index ?? this.currentSpreadIndex,
|
||||||
latestBlockId: pendingBlockId,
|
latestBlockId: pendingBlockId,
|
||||||
latestRenderedBlockId,
|
latestRenderedBlockId,
|
||||||
preloadOnly: false,
|
phase: 'activate',
|
||||||
reusedPreparedPagination: true
|
reusedPreparedPagination: true
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
@@ -240,7 +240,7 @@ class BookPaginationModule extends BaseModule {
|
|||||||
spreadIndex: targetSpread?.index ?? this.currentSpreadIndex,
|
spreadIndex: targetSpread?.index ?? this.currentSpreadIndex,
|
||||||
latestBlockId: pendingBlockId,
|
latestBlockId: pendingBlockId,
|
||||||
latestRenderedBlockId,
|
latestRenderedBlockId,
|
||||||
preloadOnly: options.activate === false
|
phase: options.activate === false ? 'prepare' : 'activate'
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
return targetSpread || (options.activate === false ? preparedSpreads[0] : this.getCurrentSpread());
|
return targetSpread || (options.activate === false ? preparedSpreads[0] : this.getCurrentSpread());
|
||||||
@@ -371,7 +371,8 @@ class BookPaginationModule extends BaseModule {
|
|||||||
buildSpreadsFromPages(pages = []) {
|
buildSpreadsFromPages(pages = []) {
|
||||||
const spreads = [];
|
const spreads = [];
|
||||||
const linesPerPage = this.getLinesPerPage();
|
const linesPerPage = this.getLinesPerPage();
|
||||||
pages.forEach((page, pageIndex) => {
|
const normalizedPages = this.normalizePagesForSpreads(pages);
|
||||||
|
normalizedPages.forEach((page, pageIndex) => {
|
||||||
const spreadIndex = Math.floor(pageIndex / 2);
|
const spreadIndex = Math.floor(pageIndex / 2);
|
||||||
const side = pageIndex % 2 === 0 ? 'left' : 'right';
|
const side = pageIndex % 2 === 0 ? 'left' : 'right';
|
||||||
if (!spreads[spreadIndex]) {
|
if (!spreads[spreadIndex]) {
|
||||||
@@ -398,6 +399,22 @@ class BookPaginationModule extends BaseModule {
|
|||||||
return spreads.filter(Boolean);
|
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 = {}) {
|
applyPageReserveDirective(block = {}) {
|
||||||
const directive = block?.metadata?.pageReserve || block?.pageReserve || null;
|
const directive = block?.metadata?.pageReserve || block?.pageReserve || null;
|
||||||
const blockId = Number(block?.blockId || block?.metadata?.blockId || 0);
|
const blockId = Number(block?.blockId || block?.metadata?.blockId || 0);
|
||||||
@@ -967,7 +984,7 @@ class BookPaginationModule extends BaseModule {
|
|||||||
|
|
||||||
setCurrentSpread(index = 0) {
|
setCurrentSpread(index = 0) {
|
||||||
this.currentSpreadIndex = Math.max(0, Math.min(Math.round(Number(index || 0)), Math.max(0, this.spreads.length - 1)));
|
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;
|
return this.currentSpreadIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -982,7 +999,7 @@ class BookPaginationModule extends BaseModule {
|
|||||||
latestBlockId: this.latestBlockId,
|
latestBlockId: this.latestBlockId,
|
||||||
latestRenderedBlockId: this.latestRenderedBlockId,
|
latestRenderedBlockId: this.latestRenderedBlockId,
|
||||||
reason: options.reason || 'publish',
|
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.activeAnimations = new Map();
|
||||||
this.revealedBlockIds = new Set();
|
this.revealedBlockIds = new Set();
|
||||||
this.pendingRevealBlockIds = new Set();
|
this.pendingRevealBlockIds = new Set();
|
||||||
this.preparedRevealCache = new Map();
|
|
||||||
this.revealBaseCanvases = null;
|
this.revealBaseCanvases = null;
|
||||||
this.revealPublishBlockIds = null;
|
this.revealPublishBlockIds = null;
|
||||||
this.lastDrawSignature = null;
|
this.lastDrawSignature = null;
|
||||||
@@ -39,7 +38,6 @@ class BookTextureRendererModule extends BaseModule {
|
|||||||
this.targetFrameDurationMs = 1000 / 60;
|
this.targetFrameDurationMs = 1000 / 60;
|
||||||
this.pipelineTimings = [];
|
this.pipelineTimings = [];
|
||||||
this.imageCache = new Map();
|
this.imageCache = new Map();
|
||||||
this.pendingPageCacheWrites = new Map();
|
|
||||||
this.pageContentVersions = new Map();
|
this.pageContentVersions = new Map();
|
||||||
|
|
||||||
this.bindMethods([
|
this.bindMethods([
|
||||||
@@ -63,6 +61,7 @@ class BookTextureRendererModule extends BaseModule {
|
|||||||
'drawLine',
|
'drawLine',
|
||||||
'drawWord',
|
'drawWord',
|
||||||
'buildRevealRegions',
|
'buildRevealRegions',
|
||||||
|
'shouldFlipAfterSideReveal',
|
||||||
'collectRevealRegionCandidates',
|
'collectRevealRegionCandidates',
|
||||||
'createRevealRegionForLine',
|
'createRevealRegionForLine',
|
||||||
'assignRevealTiming',
|
'assignRevealTiming',
|
||||||
@@ -82,6 +81,7 @@ class BookTextureRendererModule extends BaseModule {
|
|||||||
'spreadContainsBlock',
|
'spreadContainsBlock',
|
||||||
'hasPreparedRevealBlock',
|
'hasPreparedRevealBlock',
|
||||||
'createAnimationState',
|
'createAnimationState',
|
||||||
|
'getDrawPhase',
|
||||||
'publishPreparedReveal',
|
'publishPreparedReveal',
|
||||||
'startPreparedRevealAnimation',
|
'startPreparedRevealAnimation',
|
||||||
'fastForwardAnimations',
|
'fastForwardAnimations',
|
||||||
@@ -92,10 +92,8 @@ class BookTextureRendererModule extends BaseModule {
|
|||||||
'requestAnimationFrame',
|
'requestAnimationFrame',
|
||||||
'tickAnimations',
|
'tickAnimations',
|
||||||
'publishSpread',
|
'publishSpread',
|
||||||
|
'buildPageTextureRecords',
|
||||||
'cachePublishedPages',
|
'cachePublishedPages',
|
||||||
'getPageCacheWriteKey',
|
|
||||||
'isOlderPageMeta',
|
|
||||||
'schedulePageCacheWrite',
|
|
||||||
'getPageCanvas',
|
'getPageCanvas',
|
||||||
'getHitMap',
|
'getHitMap',
|
||||||
'handlePageCountChanged'
|
'handlePageCountChanged'
|
||||||
@@ -120,11 +118,12 @@ class BookTextureRendererModule extends BaseModule {
|
|||||||
const spreadIndex = Math.max(0, Number(event.detail?.spreadIndex ?? spread?.index ?? 0));
|
const spreadIndex = Math.max(0, Number(event.detail?.spreadIndex ?? spread?.index ?? 0));
|
||||||
const latestBlockId = event.detail?.latestBlockId;
|
const latestBlockId = event.detail?.latestBlockId;
|
||||||
const latestRenderedBlockId = Math.max(0, Number(event.detail?.latestRenderedBlockId || 0));
|
const latestRenderedBlockId = Math.max(0, Number(event.detail?.latestRenderedBlockId || 0));
|
||||||
|
const visibility = event.detail?.visibility || 'current';
|
||||||
this.currentSpread = spread || { left: [], right: [] };
|
this.currentSpread = spread || { left: [], right: [] };
|
||||||
if (latestBlockId && Number(latestBlockId) > latestRenderedBlockId) {
|
if (latestBlockId && Number(latestBlockId) > latestRenderedBlockId) {
|
||||||
this.markPendingReveal(latestBlockId);
|
this.markPendingReveal(latestBlockId);
|
||||||
const id = String(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']);
|
this.drawSpread(this.stripUnrenderedLines(this.currentSpread, latestRenderedBlockId), ['left', 'right']);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -132,8 +131,8 @@ class BookTextureRendererModule extends BaseModule {
|
|||||||
this.revealPublishBlockIds = new Set([id]);
|
this.revealPublishBlockIds = new Set([id]);
|
||||||
const visibleSpread = Math.max(0, Number(window.BookLabDebug?.getBookState?.().spreadIndex || 0));
|
const visibleSpread = Math.max(0, Number(window.BookLabDebug?.getBookState?.().spreadIndex || 0));
|
||||||
const flipActive = document.documentElement.dataset.webglPageFlipActive === 'true';
|
const flipActive = document.documentElement.dataset.webglPageFlipActive === 'true';
|
||||||
if (!flipActive && event.detail?.allowFutureUnrendered !== true && spreadIndex > visibleSpread) {
|
if (!flipActive && visibility !== 'future-ready' && spreadIndex > visibleSpread) {
|
||||||
this.drawSpread(this.currentSpread, ['left', 'right'], { preloadOnly: true });
|
this.drawSpread(this.currentSpread, ['left', 'right'], { phase: 'prepare' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.drawSpread(this.currentSpread, ['left', 'right']);
|
this.drawSpread(this.currentSpread, ['left', 'right']);
|
||||||
@@ -226,20 +225,21 @@ class BookTextureRendererModule extends BaseModule {
|
|||||||
this.currentSpread = spread || { left: [], right: [] };
|
this.currentSpread = spread || { left: [], right: [] };
|
||||||
const sidesToDraw = Array.isArray(sides) && sides.length ? sides : ['left', 'right'];
|
const sidesToDraw = Array.isArray(sides) && sides.length ? sides : ['left', 'right'];
|
||||||
const hasReveal = this.revealPublishBlockIds && this.revealPublishBlockIds.size > 0;
|
const hasReveal = this.revealPublishBlockIds && this.revealPublishBlockIds.size > 0;
|
||||||
|
const phase = this.getDrawPhase(options);
|
||||||
const drawSignature = this.getDrawSignature(this.currentSpread, sidesToDraw);
|
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();
|
const now = performance.now();
|
||||||
if (now - this.lastDrawSkipLoggedAt > 1000) {
|
if (now - this.lastDrawSkipLoggedAt > 1000) {
|
||||||
this.lastDrawSkipLoggedAt = now;
|
this.lastDrawSkipLoggedAt = now;
|
||||||
this.markPipelineTiming('drawSpread:skip', { sides: sidesToDraw });
|
this.markPipelineTiming('drawSpread:skip', { sides: sidesToDraw });
|
||||||
}
|
}
|
||||||
if (options.preloadOnly) this.currentSpread = previousSpread;
|
if (phase === 'prepare') this.currentSpread = previousSpread;
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
this.markPipelineTiming('drawSpread:start', {
|
this.markPipelineTiming('drawSpread:start', {
|
||||||
sides: sidesToDraw,
|
sides: sidesToDraw,
|
||||||
revealBlockIds: this.revealPublishBlockIds ? Array.from(this.revealPublishBlockIds) : [],
|
revealBlockIds: this.revealPublishBlockIds ? Array.from(this.revealPublishBlockIds) : [],
|
||||||
preloadOnly: Boolean(options.preloadOnly)
|
phase
|
||||||
});
|
});
|
||||||
this.revealBaseCanvases = { left: null, right: null };
|
this.revealBaseCanvases = { left: null, right: null };
|
||||||
sidesToDraw.forEach((side) => {
|
sidesToDraw.forEach((side) => {
|
||||||
@@ -253,15 +253,20 @@ class BookTextureRendererModule extends BaseModule {
|
|||||||
const published = this.publishSpread(sidesToDraw, options);
|
const published = this.publishSpread(sidesToDraw, options);
|
||||||
this.markPipelineTiming('drawSpread:end', {
|
this.markPipelineTiming('drawSpread:end', {
|
||||||
sides: sidesToDraw,
|
sides: sidesToDraw,
|
||||||
preloadOnly: Boolean(options.preloadOnly)
|
phase
|
||||||
});
|
});
|
||||||
this.revealBaseCanvases = null;
|
this.revealBaseCanvases = null;
|
||||||
this.revealPublishBlockIds = null;
|
this.revealPublishBlockIds = null;
|
||||||
if (!options.preloadOnly && !hasReveal) this.lastDrawSignature = drawSignature;
|
if (phase !== 'prepare' && !hasReveal) this.lastDrawSignature = drawSignature;
|
||||||
if (options.preloadOnly) this.currentSpread = previousSpread;
|
if (phase === 'prepare') this.currentSpread = previousSpread;
|
||||||
return published;
|
return published;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getDrawPhase(options = {}) {
|
||||||
|
if (options.phase === 'prepare' || options.phase === 'activate') return options.phase;
|
||||||
|
return 'activate';
|
||||||
|
}
|
||||||
|
|
||||||
getDrawSignature(spread = null, sides = []) {
|
getDrawSignature(spread = null, sides = []) {
|
||||||
const source = spread || {};
|
const source = spread || {};
|
||||||
return sides.map(side => {
|
return sides.map(side => {
|
||||||
@@ -644,6 +649,7 @@ class BookTextureRendererModule extends BaseModule {
|
|||||||
return {
|
return {
|
||||||
blockIds: Array.from(byBlock.keys()),
|
blockIds: Array.from(byBlock.keys()),
|
||||||
durationMs: sideRegions.reduce((maxDuration, region) => Math.max(maxDuration, region.timing.delay + region.timing.duration), 0),
|
durationMs: sideRegions.reduce((maxDuration, region) => Math.max(maxDuration, region.timing.delay + region.timing.duration), 0),
|
||||||
|
pageFlipAfterReveal: this.shouldFlipAfterSideReveal(side),
|
||||||
baseCanvas: null,
|
baseCanvas: null,
|
||||||
lineRects: sideRegions.map(region => ({
|
lineRects: sideRegions.map(region => ({
|
||||||
blockId: region.blockId,
|
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() {
|
collectRevealRegionCandidates() {
|
||||||
const candidates = [];
|
const candidates = [];
|
||||||
const sourceSpreads = Array.isArray(this.pagination?.spreads) && this.pagination.spreads.length
|
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;
|
if (blockId == null || !Array.isArray(detail.wordTimings)) return;
|
||||||
const id = String(blockId);
|
const id = String(blockId);
|
||||||
const wordTimings = detail.wordTimings;
|
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', {
|
this.markPipelineTiming('prepareRevealBlock:start', {
|
||||||
blockId: id,
|
blockId: id,
|
||||||
wordTimingCount: wordTimings.length,
|
wordTimingCount: wordTimings.length,
|
||||||
preloadOnly
|
phase
|
||||||
});
|
});
|
||||||
if (!preloadOnly && this.preparedRevealCache.has(id)) {
|
if (phase === 'activate' && this.pageCache?.hasPreparedRevealPlan?.(id)) {
|
||||||
const cached = this.preparedRevealCache.get(id);
|
const cached = this.pageCache.takePreparedRevealPlan(id);
|
||||||
this.activeAnimations.set(id, this.createAnimationState(blockId, wordTimings, detail));
|
this.activeAnimations.set(id, this.createAnimationState(blockId, wordTimings, detail));
|
||||||
this.pendingRevealBlockIds.delete(id);
|
this.pendingRevealBlockIds.delete(id);
|
||||||
this.publishPreparedReveal(cached);
|
this.publishPreparedReveal(cached);
|
||||||
@@ -903,10 +924,10 @@ class BookTextureRendererModule extends BaseModule {
|
|||||||
this.revealPublishBlockIds = new Set([id]);
|
this.revealPublishBlockIds = new Set([id]);
|
||||||
const spread = detail.spread || this.currentSpread || this.pagination?.getCurrentSpread?.();
|
const spread = detail.spread || this.currentSpread || this.pagination?.getCurrentSpread?.();
|
||||||
const sides = ['left', 'right'];
|
const sides = ['left', 'right'];
|
||||||
const published = this.drawSpread(spread, sides, { preloadOnly });
|
const published = this.drawSpread(spread, sides, { phase });
|
||||||
if (!preloadOnly) this.preloadAdditionalRevealSpreads(id, spread);
|
if (phase !== 'prepare') this.preloadAdditionalRevealSpreads(id, spread);
|
||||||
if (preloadOnly && published) {
|
if (phase === 'prepare' && published) {
|
||||||
this.preparedRevealCache.set(id, {
|
this.pageCache?.rememberPreparedRevealPlan?.(id, {
|
||||||
...published,
|
...published,
|
||||||
blockId,
|
blockId,
|
||||||
wordTimings,
|
wordTimings,
|
||||||
@@ -916,7 +937,7 @@ class BookTextureRendererModule extends BaseModule {
|
|||||||
this.markPipelineTiming('prepareRevealBlock:end', {
|
this.markPipelineTiming('prepareRevealBlock:end', {
|
||||||
blockId: id,
|
blockId: id,
|
||||||
wordTimingCount: wordTimings.length,
|
wordTimingCount: wordTimings.length,
|
||||||
preloadOnly
|
phase
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -927,7 +948,7 @@ class BookTextureRendererModule extends BaseModule {
|
|||||||
spreads.forEach((spread) => {
|
spreads.forEach((spread) => {
|
||||||
if (!spread || Number(spread.index) === primaryIndex) return;
|
if (!spread || Number(spread.index) === primaryIndex) return;
|
||||||
if (!this.spreadContainsBlock(spread, blockId)) 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) {
|
hasPreparedRevealBlock(blockId) {
|
||||||
const id = String(blockId ?? '');
|
const id = String(blockId ?? '');
|
||||||
return Boolean(id && this.preparedRevealCache.has(id));
|
return Boolean(id && this.pageCache?.hasPreparedRevealPlan?.(id));
|
||||||
}
|
}
|
||||||
|
|
||||||
publishPreparedReveal(prepared) {
|
publishPreparedReveal(prepared) {
|
||||||
@@ -951,14 +972,14 @@ class BookTextureRendererModule extends BaseModule {
|
|||||||
sides: prepared.sides || [],
|
sides: prepared.sides || [],
|
||||||
hasReveal: Boolean(prepared.reveal && Object.keys(prepared.reveal).length)
|
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: {
|
detail: {
|
||||||
metrics: prepared.metrics,
|
metrics: prepared.metrics,
|
||||||
hitMaps: prepared.hitMaps || this.hitMaps,
|
hitMaps: prepared.hitMaps || this.hitMaps,
|
||||||
left: prepared.left || null,
|
records: prepared.records || this.buildPageTextureRecords(prepared.sides || ['left', 'right'], prepared),
|
||||||
right: prepared.right || null,
|
|
||||||
reveal: prepared.reveal || {},
|
reveal: prepared.reveal || {},
|
||||||
pageMeta: prepared.pageMeta || {},
|
pageMeta: prepared.pageMeta || {},
|
||||||
|
phase: 'activate',
|
||||||
preparedFromCache: true
|
preparedFromCache: true
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
@@ -1095,6 +1116,7 @@ class BookTextureRendererModule extends BaseModule {
|
|||||||
|
|
||||||
publishSpread(sides = null, options = {}) {
|
publishSpread(sides = null, options = {}) {
|
||||||
const sidesToPublish = Array.isArray(sides) && sides.length ? sides : ['left', 'right'];
|
const sidesToPublish = Array.isArray(sides) && sides.length ? sides : ['left', 'right'];
|
||||||
|
const phase = this.getDrawPhase(options);
|
||||||
const regionCounts = {
|
const regionCounts = {
|
||||||
left: 0,
|
left: 0,
|
||||||
right: 0
|
right: 0
|
||||||
@@ -1103,142 +1125,96 @@ class BookTextureRendererModule extends BaseModule {
|
|||||||
metrics: this.metrics,
|
metrics: this.metrics,
|
||||||
hitMaps: this.hitMaps,
|
hitMaps: this.hitMaps,
|
||||||
sides: sidesToPublish,
|
sides: sidesToPublish,
|
||||||
pageMeta: this.buildPublishPageMeta(sidesToPublish)
|
pageMeta: this.buildPublishPageMeta(sidesToPublish),
|
||||||
|
phase
|
||||||
};
|
};
|
||||||
if (options.preloadOnly) detail.preloadOnly = true;
|
|
||||||
if (sidesToPublish.includes('left')) {
|
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')) {
|
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 = {};
|
const reveal = {};
|
||||||
sidesToPublish.forEach((side) => {
|
sidesToPublish.forEach((side) => {
|
||||||
const sideReveal = this.buildRevealRegions(side);
|
const sideReveal = this.buildRevealRegions(side);
|
||||||
if (!sideReveal) return;
|
if (!sideReveal) return;
|
||||||
sideReveal.baseCanvas = options.preloadOnly
|
sideReveal.baseCanvas = phase === 'prepare'
|
||||||
? this.cloneCanvas(this.revealBaseCanvases?.[side])
|
? this.cloneCanvas(this.revealBaseCanvases?.[side])
|
||||||
: this.revealBaseCanvases?.[side] || null;
|
: this.revealBaseCanvases?.[side] || null;
|
||||||
regionCounts[side] = sideReveal.lineRects.length;
|
regionCounts[side] = sideReveal.lineRects.length;
|
||||||
reveal[side] = sideReveal;
|
reveal[side] = sideReveal;
|
||||||
});
|
});
|
||||||
if (Object.keys(reveal).length) detail.reveal = reveal;
|
if (Object.keys(reveal).length) detail.reveal = reveal;
|
||||||
|
detail.records = this.buildPageTextureRecords(sidesToPublish, detail);
|
||||||
this.cachePublishedPages(sidesToPublish, detail);
|
this.cachePublishedPages(sidesToPublish, detail);
|
||||||
this.markPipelineTiming('publishSpread', {
|
this.markPipelineTiming('publishSpread', {
|
||||||
sides: sidesToPublish,
|
sides: sidesToPublish,
|
||||||
hasReveal: Object.keys(reveal).length > 0,
|
hasReveal: Object.keys(reveal).length > 0,
|
||||||
regionCounts,
|
regionCounts,
|
||||||
preloadOnly: Boolean(options.preloadOnly)
|
phase
|
||||||
});
|
});
|
||||||
document.dispatchEvent(new CustomEvent('webgl-book:page-canvases', {
|
document.dispatchEvent(new CustomEvent('webgl-book:page-texture-records', {
|
||||||
detail
|
detail
|
||||||
}));
|
}));
|
||||||
return 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 = []) {
|
buildPublishPageMeta(sides = []) {
|
||||||
const baseMeta = this.currentSpread?.pageMeta || {};
|
const baseMeta = this.currentSpread?.pageMeta || {};
|
||||||
|
const spreadIndex = Math.max(0, Math.round(Number(this.currentSpread?.index || 0)));
|
||||||
return sides.reduce((meta, side) => {
|
return sides.reduce((meta, side) => {
|
||||||
const source = baseMeta[side] || null;
|
const pageIndex = side === 'left' ? spreadIndex * 2 : spreadIndex * 2 + 1;
|
||||||
if (!source) {
|
const source = baseMeta[side] || {
|
||||||
meta[side] = null;
|
kind: 'blank',
|
||||||
return meta;
|
section: pageIndex < 3 ? 'frontmatter' : 'body',
|
||||||
}
|
pageIndex,
|
||||||
|
pageNumber: null,
|
||||||
|
omitPageNumber: true
|
||||||
|
};
|
||||||
const lines = Array.isArray(this.currentSpread?.[side]) ? this.currentSpread[side] : [];
|
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 maxBlockId = lines.reduce((max, line) => Math.max(max, Number(line?.blockId || 0)), 0);
|
||||||
const lineCount = lines.length;
|
const lineCount = lines.length;
|
||||||
const pageIndex = Number(source.pageIndex);
|
const normalizedPageIndex = Number(source.pageIndex);
|
||||||
const key = Number.isFinite(pageIndex) ? pageIndex : side;
|
const key = Number.isFinite(normalizedPageIndex) ? normalizedPageIndex : side;
|
||||||
const nextVersion = Math.max(1, Number(this.pageContentVersions.get(key) || 0) + 1);
|
const nextVersion = Math.max(1, Number(this.pageContentVersions.get(key) || 0) + 1);
|
||||||
this.pageContentVersions.set(key, nextVersion);
|
this.pageContentVersions.set(key, nextVersion);
|
||||||
meta[side] = {
|
meta[side] = {
|
||||||
...source,
|
...source,
|
||||||
|
pageIndex: Number.isFinite(normalizedPageIndex) ? normalizedPageIndex : pageIndex,
|
||||||
contentVersion: nextVersion,
|
contentVersion: nextVersion,
|
||||||
completenessScore: (maxBlockId * 1000) + lineCount,
|
completenessScore: (maxBlockId * 1000) + lineCount,
|
||||||
maxBlockId,
|
maxBlockId,
|
||||||
lineCount
|
lineCount
|
||||||
};
|
};
|
||||||
return meta;
|
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 = {}) {
|
cachePublishedPages(sides = [], detail = {}) {
|
||||||
if (!this.pageCache || typeof this.pageCache.cachePageCanvas !== 'function') return;
|
if (!this.pageCache || typeof this.pageCache.storePageCanvas !== 'function') return;
|
||||||
sides.forEach((side) => {
|
sides.forEach((side) => {
|
||||||
const canvas = detail[side];
|
const canvas = detail[side];
|
||||||
const pageMeta = detail.pageMeta?.[side] || null;
|
const pageMeta = detail.pageMeta?.[side] || null;
|
||||||
if (!canvas || !pageMeta || !Number.isFinite(Number(pageMeta.pageIndex))) return;
|
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) {
|
getPageCanvas(side) {
|
||||||
return this.canvases[side] || null;
|
return this.canvases[side] || null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -937,8 +937,8 @@ class SentenceQueueModule extends BaseModule {
|
|||||||
cueTimings: sentence.animation?.cueTimings || [],
|
cueTimings: sentence.animation?.cueTimings || [],
|
||||||
totalDuration: sentence.animation?.totalDuration || 0,
|
totalDuration: sentence.animation?.totalDuration || 0,
|
||||||
spread,
|
spread,
|
||||||
preloadOnly: true
|
phase: 'prepare'
|
||||||
}, { preloadOnly: true });
|
}, { phase: 'prepare' });
|
||||||
sentence.webglBookPresentation = {
|
sentence.webglBookPresentation = {
|
||||||
prepared: true,
|
prepared: true,
|
||||||
blockId,
|
blockId,
|
||||||
|
|||||||
@@ -1081,10 +1081,10 @@ class UIDisplayHandlerModule extends BaseModule {
|
|||||||
cueTimings: sentence.animation?.cueTimings || [],
|
cueTimings: sentence.animation?.cueTimings || [],
|
||||||
totalDuration: sentence.animation?.totalDuration || 0,
|
totalDuration: sentence.animation?.totalDuration || 0,
|
||||||
spread: previewSpread,
|
spread: previewSpread,
|
||||||
preloadOnly: true
|
phase: 'prepare'
|
||||||
};
|
};
|
||||||
if (previewSpread && typeof bookTextureRenderer.prepareRevealBlock === 'function') {
|
if (previewSpread && typeof bookTextureRenderer.prepareRevealBlock === 'function') {
|
||||||
bookTextureRenderer.prepareRevealBlock(previewRevealDetail, { preloadOnly: true });
|
bookTextureRenderer.prepareRevealBlock(previewRevealDetail, { phase: 'prepare' });
|
||||||
}
|
}
|
||||||
if (Number(previewSpread?.index || 0) > currentSpreadIndex) {
|
if (Number(previewSpread?.index || 0) > currentSpreadIndex) {
|
||||||
const flipped = await this.waitForWebGLPageFlip({
|
const flipped = await this.waitForWebGLPageFlip({
|
||||||
|
|||||||
+244
-220
@@ -213,6 +213,7 @@ let activeFlips = [];
|
|||||||
let pendingPageFlips = 0;
|
let pendingPageFlips = 0;
|
||||||
const pendingRevealStartBlockIds = new Set();
|
const pendingRevealStartBlockIds = new Set();
|
||||||
const activeRevealBlockStarts = new Map();
|
const activeRevealBlockStarts = new Map();
|
||||||
|
let lastFlipTexturePreflight = null;
|
||||||
|
|
||||||
const paperColor = new THREE.Color(0xece4ca);
|
const paperColor = new THREE.Color(0xece4ca);
|
||||||
const inkColor = '#1a1009';
|
const inkColor = '#1a1009';
|
||||||
@@ -246,19 +247,24 @@ function createPageCanvasTexture(sourceCanvas) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getBlankPageTexture() {
|
function getBlankPageTexture() {
|
||||||
if (blankPageTexture) return blankPageTexture;
|
return pageTextureStore?.getBlankTexture?.() || createPageCanvasTexture(createPageCanvas('blank'));
|
||||||
blankPageTexture = createPageCanvasTexture(createPageCanvas('blank'));
|
|
||||||
return blankPageTexture;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const preparedPageTextures = {
|
|
||||||
left: new Map(),
|
|
||||||
right: new Map()
|
|
||||||
};
|
|
||||||
const residentPageTextures = new Map();
|
|
||||||
const maxResidentPageTextures = 192;
|
const maxResidentPageTextures = 192;
|
||||||
let blankPageTexture = null;
|
const pageTextureStore = window.moduleRegistry?.getModule?.('webgl-page-cache') || window.WebGLPageCache || null;
|
||||||
const pageCacheProblemLog = [];
|
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 = {
|
let currentPageMeta = {
|
||||||
left: null,
|
left: null,
|
||||||
right: null
|
right: null
|
||||||
@@ -589,14 +595,18 @@ window.BookLabDebug = {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
getRuntimeInvariants() {
|
getRuntimeInvariants() {
|
||||||
|
const textureStoreState = pageTextureStore?.getRuntimeState?.() || {};
|
||||||
return {
|
return {
|
||||||
targetFrameDurationMs,
|
targetFrameDurationMs,
|
||||||
residentPageTextureCount: residentPageTextures.size,
|
residentPageTextureCount: textureStoreState.residentTextureCount || 0,
|
||||||
maxResidentPageTextures,
|
maxResidentPageTextures,
|
||||||
pageCacheProblemCount: pageCacheProblemLog.length,
|
pageCacheProblemCount: textureStoreState.problemCount || 0,
|
||||||
|
preparedPageTextureCount: textureStoreState.preparedTextureCount || 0,
|
||||||
|
singlePageTextureStore: true,
|
||||||
flipFrontBackShareMaterial: materials.flipPageSurface === materials.flipPageBackSurface,
|
flipFrontBackShareMaterial: materials.flipPageSurface === materials.flipPageBackSurface,
|
||||||
mirrorRefreshesEveryFrame: true,
|
mirrorRefreshesEveryFrame: true,
|
||||||
mirrorRefreshesWhenStaticDirty: true
|
mirrorRefreshesWhenStaticDirty: true,
|
||||||
|
lastFlipTexturePreflight
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
projectPointerToPage(clientX, clientY) {
|
projectPointerToPage(clientX, clientY) {
|
||||||
@@ -610,7 +620,7 @@ window.BookLabDebug = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
window.addEventListener('resize', resize);
|
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) => {
|
document.addEventListener('webgl-book:page-reveal-start', (event) => {
|
||||||
startPageRevealForBlock(event.detail?.blockId);
|
startPageRevealForBlock(event.detail?.blockId);
|
||||||
});
|
});
|
||||||
@@ -621,7 +631,7 @@ document.addEventListener('webgl-book:reveal-committed', (event) => {
|
|||||||
handleRevealCommittedForPageFlip(event.detail || {});
|
handleRevealCommittedForPageFlip(event.detail || {});
|
||||||
});
|
});
|
||||||
document.addEventListener('webgl-book:page-cache-problem', (event) => {
|
document.addEventListener('webgl-book:page-cache-problem', (event) => {
|
||||||
recordPageCacheProblem(event.detail || {});
|
pageTextureStore?.recordProblem?.(event.detail || {});
|
||||||
});
|
});
|
||||||
document.addEventListener('book-pagination:spread-updated', (event) => {
|
document.addEventListener('book-pagination:spread-updated', (event) => {
|
||||||
const detail = event.detail || {};
|
const detail = event.detail || {};
|
||||||
@@ -630,7 +640,7 @@ document.addEventListener('book-pagination:spread-updated', (event) => {
|
|||||||
const latestRenderedBlockId = Math.max(0, Number(detail.latestRenderedBlockId || 0));
|
const latestRenderedBlockId = Math.max(0, Number(detail.latestRenderedBlockId || 0));
|
||||||
if (
|
if (
|
||||||
latestBlockId > latestRenderedBlockId
|
latestBlockId > latestRenderedBlockId
|
||||||
&& detail.allowFutureUnrendered !== true
|
&& detail.visibility !== 'future-ready'
|
||||||
&& activeFlips.length === 0
|
&& activeFlips.length === 0
|
||||||
&& incomingSpreadIndex > Math.max(0, Number(bookPaginationState.spreadIndex || 0))
|
&& incomingSpreadIndex > Math.max(0, Number(bookPaginationState.spreadIndex || 0))
|
||||||
) {
|
) {
|
||||||
@@ -2028,35 +2038,34 @@ function syncBottomNavigation() {
|
|||||||
bottomNavigation.endButton.disabled = activeFlips.length > 0 || currentPage >= navigableLimit;
|
bottomNavigation.endButton.disabled = activeFlips.length > 0 || currentPage >= navigableLimit;
|
||||||
}
|
}
|
||||||
|
|
||||||
function handlePageCanvases(event) {
|
function handlePageTextureRecords(event) {
|
||||||
const detail = event.detail || {};
|
const detail = normalizePageTextureRecordDetail(event.detail || {});
|
||||||
if (detail.pageMeta) {
|
if (detail.pageMeta) {
|
||||||
const hasLeftMeta = Object.prototype.hasOwnProperty.call(detail.pageMeta, 'left');
|
currentPageMeta = normalizePageMetaPair(detail.pageMeta, currentPageMeta);
|
||||||
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
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
markPageTextureTiming('handlePageCanvases:start', {
|
markPageTextureTiming('handlePageTextureRecords:start', {
|
||||||
hasLeft: Boolean(detail.left),
|
hasLeft: Boolean(detail.left),
|
||||||
hasRight: Boolean(detail.right),
|
hasRight: Boolean(detail.right),
|
||||||
revealSides: Object.keys(detail.reveal || {}),
|
revealSides: Object.keys(detail.reveal || {}),
|
||||||
preloadOnly: Boolean(detail.preloadOnly),
|
phase: detail.phase || 'activate',
|
||||||
pageMeta: currentPageMeta
|
pageMeta: currentPageMeta
|
||||||
});
|
});
|
||||||
const leftReveal = attachRevealPageMeta(detail.reveal?.left, detail.pageMeta?.left || currentPageMeta.left || null);
|
const leftReveal = attachRevealPageMeta(detail.reveal?.left, currentPageMeta.left || null);
|
||||||
const rightReveal = attachRevealPageMeta(detail.reveal?.right, detail.pageMeta?.right || currentPageMeta.right || null);
|
const rightReveal = attachRevealPageMeta(detail.reveal?.right, currentPageMeta.right || null);
|
||||||
if (detail.preloadOnly) {
|
if (detail.phase === 'prepare') {
|
||||||
if (detail.left) {
|
if (detail.left) {
|
||||||
const texture = preloadPageTexture('left', detail.left, leftReveal, detail.pageMeta?.left || null);
|
const texture = preloadPageTexture('left', detail.left, leftReveal, currentPageMeta.left);
|
||||||
rememberResidentPageTexture(detail.pageMeta?.left || null, texture, detail.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) {
|
if (detail.right) {
|
||||||
const texture = preloadPageTexture('right', detail.right, rightReveal, detail.pageMeta?.right || null);
|
const texture = preloadPageTexture('right', detail.right, rightReveal, currentPageMeta.right);
|
||||||
rememberResidentPageTexture(detail.pageMeta?.right || null, texture, detail.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;
|
return;
|
||||||
}
|
}
|
||||||
if (detail.left) {
|
if (detail.left) {
|
||||||
@@ -2073,13 +2082,110 @@ function handlePageCanvases(event) {
|
|||||||
uploadPageTextureDirect('right', detail.right, currentPageMeta.right);
|
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();
|
markStaticSceneBuffersDirty();
|
||||||
document.documentElement.dataset.webglPageTextureMetrics = JSON.stringify({
|
document.documentElement.dataset.webglPageTextureMetrics = JSON.stringify({
|
||||||
width: leftCanvas.width,
|
width: leftCanvas.width,
|
||||||
height: leftCanvas.height,
|
height: leftCanvas.height,
|
||||||
source: 'book-texture-renderer'
|
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) {
|
function attachRevealPageMeta(revealDetail = null, pageMeta = null) {
|
||||||
@@ -2099,30 +2205,15 @@ function getRevealCacheKey(revealDetail = {}) {
|
|||||||
|
|
||||||
function preloadPageTexture(side, sourceCanvas, revealDetail = {}, pageMeta = null) {
|
function preloadPageTexture(side, sourceCanvas, revealDetail = {}, pageMeta = null) {
|
||||||
if (!sourceCanvas) return 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 });
|
const key = getRevealCacheKey({ ...(revealDetail || {}), pageMeta: revealDetail?.pageMeta || pageMeta || null });
|
||||||
markPageTextureTiming('preloadTexture:start', {
|
markPageTextureTiming('preloadTexture:start', {
|
||||||
side,
|
side,
|
||||||
key,
|
key,
|
||||||
width: sourceCanvas.width,
|
width: sourceCanvas.width,
|
||||||
height: sourceCanvas.height,
|
height: sourceCanvas.height,
|
||||||
hasBaseTexture: Boolean(baseTexture)
|
hasBaseTexture: Boolean(revealDetail?.baseCanvas)
|
||||||
});
|
});
|
||||||
preparedPageTextures[side].set(key, {
|
const texture = pageTextureStore?.preparePageTexture?.(side, key, pageMeta, sourceCanvas, revealDetail) || null;
|
||||||
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);
|
|
||||||
}
|
|
||||||
markPageTextureTiming('preloadTexture:end', { side, key });
|
markPageTextureTiming('preloadTexture:end', { side, key });
|
||||||
return texture;
|
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) {
|
function makePageMetaForCache(pageIndex) {
|
||||||
const index = Math.max(0, Math.round(Number(pageIndex || 0)));
|
const index = Math.max(0, Math.round(Number(pageIndex || 0)));
|
||||||
const paginationMeta = getPaginationPageMeta(index) || {};
|
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) {
|
function spreadPageIndices(spreadIndex) {
|
||||||
const spread = Math.max(0, Math.round(Number(spreadIndex || 0)));
|
const spread = Math.max(0, Math.round(Number(spreadIndex || 0)));
|
||||||
return {
|
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) {
|
function getPaginationPageMeta(pageIndex) {
|
||||||
const index = Math.max(0, Math.round(Number(pageIndex || 0)));
|
const index = Math.max(0, Math.round(Number(pageIndex || 0)));
|
||||||
const spreadIndex = Math.floor(index / 2);
|
const spreadIndex = Math.floor(index / 2);
|
||||||
@@ -2294,27 +2266,47 @@ function getPaginationPageMeta(pageIndex) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function prewarmSpreadTextures(spreadIndex) {
|
async function prewarmSpreadTextures(spreadIndex) {
|
||||||
const indices = spreadPageIndices(spreadIndex);
|
return pageTextureStore?.prewarmSpreadTextures?.(spreadIndex, makePageMetaForCache) || {
|
||||||
const [left, right] = await Promise.all([
|
|
||||||
preloadCachedPageTexture(indices.left),
|
|
||||||
preloadCachedPageTexture(indices.right)
|
|
||||||
]);
|
|
||||||
return {
|
|
||||||
spreadIndex: Math.max(0, Math.round(Number(spreadIndex || 0))),
|
spreadIndex: Math.max(0, Math.round(Number(spreadIndex || 0))),
|
||||||
left,
|
left: null,
|
||||||
right
|
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) {
|
async function prewarmFlipTextures(direction, targetSpread = null) {
|
||||||
const currentSpread = Math.max(0, Number(bookPaginationState.spreadIndex || 0));
|
const currentSpread = Math.max(0, Number(bookPaginationState.spreadIndex || 0));
|
||||||
const nextSpread = Number.isFinite(Number(targetSpread))
|
const nextSpread = Number.isFinite(Number(targetSpread))
|
||||||
? Math.max(0, Math.round(Number(targetSpread)))
|
? Math.max(0, Math.round(Number(targetSpread)))
|
||||||
: Math.max(0, currentSpread + Math.sign(Number(direction || 0)));
|
: Math.max(0, currentSpread + Math.sign(Number(direction || 0)));
|
||||||
const [current, next] = await Promise.all([
|
const windowMap = await prewarmNavigationTextureWindow('flip-prewarm', { targetSpread: nextSpread });
|
||||||
prewarmSpreadTextures(currentSpread),
|
const current = windowMap?.[currentSpread] || await prewarmSpreadTextures(currentSpread);
|
||||||
prewarmSpreadTextures(nextSpread)
|
const next = windowMap?.[nextSpread] || await prewarmSpreadTextures(nextSpread);
|
||||||
]);
|
|
||||||
return {
|
return {
|
||||||
current,
|
current,
|
||||||
next
|
next
|
||||||
@@ -2323,19 +2315,22 @@ async function prewarmFlipTextures(direction, targetSpread = null) {
|
|||||||
|
|
||||||
function takePreparedPageTexture(side, revealDetail = {}) {
|
function takePreparedPageTexture(side, revealDetail = {}) {
|
||||||
const key = getRevealCacheKey(revealDetail);
|
const key = getRevealCacheKey(revealDetail);
|
||||||
const prepared = preparedPageTextures[side].get(key);
|
const prepared = pageTextureStore?.takePreparedPageTexture?.(side, key) || null;
|
||||||
if (!prepared) return null;
|
if (!prepared) return null;
|
||||||
preparedPageTextures[side].delete(key);
|
|
||||||
markPageTextureTiming('preloadTexture:activate', { side, key });
|
markPageTextureTiming('preloadTexture:activate', { side, key });
|
||||||
return prepared;
|
return prepared;
|
||||||
}
|
}
|
||||||
|
|
||||||
function uploadPageTextureDirect(side, sourceCanvas, pageMeta = null) {
|
function uploadPageTextureDirect(side, sourceCanvas, pageMeta = null) {
|
||||||
|
if (pageMeta?.kind === 'blank') {
|
||||||
|
applyExplicitBlankPageTexture(side, pageMeta, 'direct-upload');
|
||||||
|
return;
|
||||||
|
}
|
||||||
const texture = side === 'left' ? leftTexture : rightTexture;
|
const texture = side === 'left' ? leftTexture : rightTexture;
|
||||||
const material = side === 'left' ? materials.leftPage : materials.rightPage;
|
const material = side === 'left' ? materials.leftPage : materials.rightPage;
|
||||||
const shouldUseResidentTexture = pageMeta?.kind !== 'title';
|
const shouldUseResidentTexture = pageMeta?.kind !== 'title';
|
||||||
const residentTexture = shouldUseResidentTexture && Number.isFinite(Number(pageMeta?.pageIndex))
|
const residentTexture = shouldUseResidentTexture && Number.isFinite(Number(pageMeta?.pageIndex))
|
||||||
? getResidentPageTextureForMeta(pageMeta)
|
? pageTextureStore?.getResidentTextureForMeta?.(pageMeta)
|
||||||
: null;
|
: null;
|
||||||
markPageTextureTiming('directUpload:start', {
|
markPageTextureTiming('directUpload:start', {
|
||||||
side,
|
side,
|
||||||
@@ -2356,7 +2351,7 @@ function uploadPageTextureDirect(side, sourceCanvas, pageMeta = null) {
|
|||||||
material.needsUpdate = true;
|
material.needsUpdate = true;
|
||||||
}
|
}
|
||||||
bindPageTextureSource(side, texture, sourceCanvas);
|
bindPageTextureSource(side, texture, sourceCanvas);
|
||||||
rememberResidentPageTexture(pageMeta, texture, sourceCanvas, false);
|
pageTextureStore?.rememberResidentTexture?.(pageMeta, texture, sourceCanvas, false);
|
||||||
markPageTextureTiming('directUpload:end', { side });
|
markPageTextureTiming('directUpload:end', { side });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2381,7 +2376,7 @@ function beginPageReveal(side, sourceCanvas, revealDetail = {}) {
|
|||||||
}
|
}
|
||||||
bindPageTextureSource(side, texture, sourceCanvas);
|
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 revealBlockIds = Array.isArray(revealDetail.blockIds) ? revealDetail.blockIds.map(value => String(value)) : [];
|
||||||
const activeStartedAt = revealBlockIds
|
const activeStartedAt = revealBlockIds
|
||||||
@@ -2397,6 +2392,7 @@ function beginPageReveal(side, sourceCanvas, revealDetail = {}) {
|
|||||||
durationMs: Math.max(1, Number(revealDetail.durationMs || 1)),
|
durationMs: Math.max(1, Number(revealDetail.durationMs || 1)),
|
||||||
blockIds: revealBlockIds,
|
blockIds: revealBlockIds,
|
||||||
baseTexture,
|
baseTexture,
|
||||||
|
pageFlipAfterReveal: revealDetail.pageFlipAfterReveal === true,
|
||||||
fastForwarding: false,
|
fastForwarding: false,
|
||||||
fastForwardStartedAt: null,
|
fastForwardStartedAt: null,
|
||||||
fastForwardStartElapsedMs: 0,
|
fastForwardStartElapsedMs: 0,
|
||||||
@@ -2408,12 +2404,12 @@ function beginPageReveal(side, sourceCanvas, revealDetail = {}) {
|
|||||||
if (shader?.uniforms?.bookRevealElapsedMs) {
|
if (shader?.uniforms?.bookRevealElapsedMs) {
|
||||||
shader.uniforms.bookRevealElapsedMs.value = pageRevealState[side].visualElapsedMs;
|
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);
|
const targetSpread = Math.max(0, Math.round(Number(bookPaginationState.spreadIndex || 0)) + 1);
|
||||||
prewarmFlipTextures(1, targetSpread).then(() => {
|
prewarmFlipTextures(1, targetSpread).then(() => {
|
||||||
markPageTextureTiming('rightPageReveal:flip-prewarm-ready', { targetSpread });
|
markPageTextureTiming('rightPageReveal:flip-prewarm-ready', { targetSpread });
|
||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
recordPageCacheProblem({
|
pageTextureStore?.recordProblem?.({
|
||||||
type: 'right-page-flip-prewarm-error',
|
type: 'right-page-flip-prewarm-error',
|
||||||
targetSpread,
|
targetSpread,
|
||||||
message: error?.message || String(error)
|
message: error?.message || String(error)
|
||||||
@@ -2618,7 +2614,8 @@ function updatePageRevealAnimations(now) {
|
|||||||
document.dispatchEvent(new CustomEvent('webgl-book:reveal-committed', {
|
document.dispatchEvent(new CustomEvent('webgl-book:reveal-committed', {
|
||||||
detail: {
|
detail: {
|
||||||
side,
|
side,
|
||||||
blockIds: state.blockIds
|
blockIds: state.blockIds,
|
||||||
|
pageFlipAfterReveal: state.pageFlipAfterReveal === true
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
@@ -2632,8 +2629,11 @@ function bindPageTextureSource(side, texture, sourceCanvas) {
|
|||||||
width: nextCanvas?.width || 0,
|
width: nextCanvas?.width || 0,
|
||||||
height: nextCanvas?.height || 0
|
height: nextCanvas?.height || 0
|
||||||
});
|
});
|
||||||
texture.image = sourceCanvas || fallbackCanvas;
|
const boundTexture = pageTextureStore?.bindVisibleTextureSource?.(side, sourceCanvas) || null;
|
||||||
texture.needsUpdate = true;
|
if (!boundTexture) {
|
||||||
|
texture.image = nextCanvas;
|
||||||
|
texture.needsUpdate = true;
|
||||||
|
}
|
||||||
updatePageTextureDebugState(side, nextCanvas, sourceCanvas, true);
|
updatePageTextureDebugState(side, nextCanvas, sourceCanvas, true);
|
||||||
markPageTextureTiming('bindPageTextureSource:end', { side });
|
markPageTextureTiming('bindPageTextureSource:end', { side });
|
||||||
}
|
}
|
||||||
@@ -2841,20 +2841,27 @@ function createPageFlip(direction, startTime, duration) {
|
|||||||
|
|
||||||
function prepareStaticPageForFlip(flip, prewarm = null) {
|
function prepareStaticPageForFlip(flip, prewarm = null) {
|
||||||
if (!flip) return false;
|
if (!flip) return false;
|
||||||
const sourceMaterial = flip.sourcePageSide === 'left' ? materials.leftPage : materials.rightPage;
|
const sourceSide = flip.direction > 0 ? 'right' : 'left';
|
||||||
const sourceTexture = sourceMaterial?.map || (flip.sourcePageSide === 'left' ? leftTexture : rightTexture);
|
const sourceTexture = resolveCurrentFlipSourceTexture(sourceSide);
|
||||||
|
const sourcePageMeta = currentPageMeta?.[sourceSide] || getPaginationPageMeta(spreadPageIndices(bookPaginationState.spreadIndex)[sourceSide]) || null;
|
||||||
const targetSpread = Number.isFinite(Number(flip.targetSpread))
|
const targetSpread = Number.isFinite(Number(flip.targetSpread))
|
||||||
? Math.max(0, Math.round(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)));
|
: Math.max(0, Math.round(Number(bookPaginationState.spreadIndex || 0)) + Math.sign(Number(flip.direction || 0)));
|
||||||
const targetPages = spreadPageIndices(targetSpread);
|
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 prewarmedBackTexture = flip.direction > 0 ? prewarm?.next?.left : prewarm?.next?.right;
|
||||||
const residentBackTexture = prewarmedBackTexture || getResidentPageTexture(targetBackPageIndex);
|
const backTexture = resolveFlipBackTexture(targetBackPageMeta, prewarmedBackTexture);
|
||||||
const requiresWrittenTexture = targetBackPageIndex <= Math.max(2, Number(bookPaginationState.writtenPageLimit || 0));
|
const requiresWrittenTexture = targetBackPageMeta.kind !== 'blank'
|
||||||
if (!residentBackTexture && requiresWrittenTexture) {
|
&& targetBackPageIndex <= Math.max(2, Number(bookPaginationState.writtenPageLimit || 0));
|
||||||
recordPageCacheProblem({
|
if (!sourceTexture || (!backTexture && requiresWrittenTexture)) {
|
||||||
type: 'flip-back-texture-missing',
|
pageTextureStore?.recordProblem?.({
|
||||||
|
type: !sourceTexture ? 'flip-source-texture-missing' : 'flip-back-texture-missing',
|
||||||
|
sourceSide,
|
||||||
|
sourcePageIndex: sourcePageMeta?.pageIndex ?? null,
|
||||||
targetBackPageIndex,
|
targetBackPageIndex,
|
||||||
|
targetBackKind: targetBackPageMeta.kind,
|
||||||
targetSpread,
|
targetSpread,
|
||||||
direction: flip.direction,
|
direction: flip.direction,
|
||||||
prewarmedCurrent: Boolean(prewarm?.current),
|
prewarmedCurrent: Boolean(prewarm?.current),
|
||||||
@@ -2862,9 +2869,8 @@ function prepareStaticPageForFlip(flip, prewarm = null) {
|
|||||||
});
|
});
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const backTexture = residentBackTexture || getBlankPageTexture();
|
|
||||||
materials.flipPageSurface.map = sourceTexture;
|
materials.flipPageSurface.map = sourceTexture;
|
||||||
materials.flipPageBackSurface.map = backTexture;
|
materials.flipPageBackSurface.map = backTexture || getBlankPageTexture();
|
||||||
materials.flipPageSurface.normalMap = materials.pageSurface.normalMap;
|
materials.flipPageSurface.normalMap = materials.pageSurface.normalMap;
|
||||||
materials.flipPageBackSurface.normalMap = materials.pageSurface.normalMap;
|
materials.flipPageBackSurface.normalMap = materials.pageSurface.normalMap;
|
||||||
materials.flipPageSurface.roughnessMap = materials.pageSurface.roughnessMap;
|
materials.flipPageSurface.roughnessMap = materials.pageSurface.roughnessMap;
|
||||||
@@ -2872,8 +2878,24 @@ function prepareStaticPageForFlip(flip, prewarm = null) {
|
|||||||
materials.flipPageSurface.needsUpdate = true;
|
materials.flipPageSurface.needsUpdate = true;
|
||||||
materials.flipPageBackSurface.needsUpdate = true;
|
materials.flipPageBackSurface.needsUpdate = true;
|
||||||
flip.sourceTexture = sourceTexture;
|
flip.sourceTexture = sourceTexture;
|
||||||
flip.backTexture = backTexture;
|
flip.sourcePageMeta = sourcePageMeta ? { ...sourcePageMeta } : null;
|
||||||
|
flip.backTexture = backTexture || getBlankPageTexture();
|
||||||
|
flip.backPageMeta = targetBackPageMeta ? { ...targetBackPageMeta } : null;
|
||||||
flip.targetBackPageIndex = targetBackPageIndex;
|
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) {
|
if (flip.direction > 0) {
|
||||||
const blankTexture = getBlankPageTexture();
|
const blankTexture = getBlankPageTexture();
|
||||||
if (blankTexture && materials.rightPage.map !== blankTexture) {
|
if (blankTexture && materials.rightPage.map !== blankTexture) {
|
||||||
@@ -2890,15 +2912,27 @@ function prepareStaticPageForFlip(flip, prewarm = null) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
markPageTextureTiming('flipTexturePreflight:ready', {
|
markPageTextureTiming('flipTexturePreflight:ready', {
|
||||||
direction: flip.direction,
|
...lastFlipTexturePreflight,
|
||||||
sourceSide: flip.sourcePageSide,
|
usedResidentBackTexture: Boolean(backTexture && backTexture !== getBlankPageTexture())
|
||||||
targetSpread,
|
|
||||||
targetBackPageIndex,
|
|
||||||
usedResidentBackTexture: Boolean(residentBackTexture)
|
|
||||||
});
|
});
|
||||||
return true;
|
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) {
|
function canPageFlip(direction) {
|
||||||
if (!currentProceduralBookModel) return false;
|
if (!currentProceduralBookModel) return false;
|
||||||
const currentPage = getCurrentPagePosition();
|
const currentPage = getCurrentPagePosition();
|
||||||
@@ -2908,7 +2942,7 @@ function canPageFlip(direction) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleRevealCommittedForPageFlip(detail = {}) {
|
function handleRevealCommittedForPageFlip(detail = {}) {
|
||||||
if (detail.side !== 'right' || !isRightBodyPageComplete()) return;
|
if (detail.side !== 'right' || detail.pageFlipAfterReveal !== true) return;
|
||||||
if (activeFlips.length > 0 || pendingRightPageFlip) return;
|
if (activeFlips.length > 0 || pendingRightPageFlip) return;
|
||||||
if (isChoiceAwaitingPlayer()) return;
|
if (isChoiceAwaitingPlayer()) return;
|
||||||
const autoplayFlip = isTtsPlaybackActive();
|
const autoplayFlip = isTtsPlaybackActive();
|
||||||
@@ -2937,16 +2971,6 @@ async function tryStartPendingRightPageFlip(reason = 'pending', options = {}) {
|
|||||||
return flipped;
|
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() {
|
function isChoiceAwaitingPlayer() {
|
||||||
return document.documentElement.dataset.choiceAwaiting === 'true'
|
return document.documentElement.dataset.choiceAwaiting === 'true'
|
||||||
|| document.body?.dataset?.choiceAwaiting === 'true'
|
|| document.body?.dataset?.choiceAwaiting === 'true'
|
||||||
|
|||||||
@@ -18,16 +18,63 @@ class WebGLPageCacheModule extends BaseModule {
|
|||||||
this.maxCacheSizeBytes = 5 * 1024 * 1024 * 1024;
|
this.maxCacheSizeBytes = 5 * 1024 * 1024 * 1024;
|
||||||
this.memoryCanvasCache = new Map();
|
this.memoryCanvasCache = new Map();
|
||||||
this.maxMemoryCanvasCount = 256;
|
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([
|
this.bindMethods([
|
||||||
'initialize',
|
'initialize',
|
||||||
'openDB',
|
'openDB',
|
||||||
|
'configureTextureRuntime',
|
||||||
'cachePageCanvas',
|
'cachePageCanvas',
|
||||||
'getPageCanvas',
|
'getPageCanvas',
|
||||||
|
'putPageCanvas',
|
||||||
|
'storePageCanvas',
|
||||||
|
'preparePageTexture',
|
||||||
|
'takePreparedPageTexture',
|
||||||
|
'rememberPreparedRevealPlan',
|
||||||
|
'takePreparedRevealPlan',
|
||||||
|
'hasPreparedRevealPlan',
|
||||||
|
'registerVisibleTexture',
|
||||||
|
'bindVisibleTextureSource',
|
||||||
|
'getVisibleTexture',
|
||||||
|
'rememberResidentTexture',
|
||||||
|
'getResidentTexture',
|
||||||
|
'getResidentTextureForMeta',
|
||||||
|
'ensurePageTexture',
|
||||||
|
'prewarmPageTexture',
|
||||||
|
'prewarmSpreadTextures',
|
||||||
|
'prewarmNavigationWindow',
|
||||||
|
'getBlankTexture',
|
||||||
|
'createTextureFromCanvas',
|
||||||
|
'disposeTextureRecord',
|
||||||
'makePageKey',
|
'makePageKey',
|
||||||
|
'getPageWriteKey',
|
||||||
|
'makeResidentKey',
|
||||||
|
'cloneCanvas',
|
||||||
'canvasToBlob',
|
'canvasToBlob',
|
||||||
'blobToCanvas',
|
'blobToCanvas',
|
||||||
'isOlderPageEntry',
|
'isOlderPageEntry',
|
||||||
|
'isOlderPageMeta',
|
||||||
|
'recordProblem',
|
||||||
|
'getRuntimeState',
|
||||||
'manageCacheSize',
|
'manageCacheSize',
|
||||||
'calculateTotalCacheSize',
|
'calculateTotalCacheSize',
|
||||||
'deleteEntry',
|
'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() {
|
openDB() {
|
||||||
if (this.db) return Promise.resolve(this.db);
|
if (this.db) return Promise.resolve(this.db);
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
@@ -93,6 +159,303 @@ class WebGLPageCacheModule extends BaseModule {
|
|||||||
return `${cacheKey}:page:${safePage}:${safeKind}:${safeSection}:${safeWidth}x${safeHeight}`;
|
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) {
|
async cachePageCanvas(pageMeta = {}, canvas = null) {
|
||||||
if (!canvas || !this.db || this.cacheStatus !== 'ready') return false;
|
if (!canvas || !this.db || this.cacheStatus !== 'ready') return false;
|
||||||
const pageIndex = Number(pageMeta.pageIndex);
|
const pageIndex = Number(pageMeta.pageIndex);
|
||||||
@@ -200,6 +563,43 @@ class WebGLPageCacheModule extends BaseModule {
|
|||||||
return incomingVersion > 0 && existingVersion > incomingVersion;
|
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) {
|
canvasToBlob(canvas) {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
if (typeof canvas.toBlob !== 'function') {
|
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 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 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 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)],
|
['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 exposes reveal pipeline diagnostics', /pipelineTimings/.test(textureRendererSource) && /markPipelineTiming/.test(textureRendererSource) && /webglTexturePipeline/.test(textureRendererSource)],
|
||||||
['texture renderer records prepare draw publish and start reveal timing', /markPipelineTiming\('prepareRevealBlock:start'/.test(textureRendererSource) && /markPipelineTiming\('drawSpread:start'/.test(textureRendererSource) && /markPipelineTiming\('publishSpread'/.test(textureRendererSource) && /markPipelineTiming\('startPreparedRevealAnimation'/.test(textureRendererSource)],
|
['texture renderer 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)],
|
['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 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)],
|
['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 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)],
|
['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)],
|
['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 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 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 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 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 lab records cache misses as problem states', /pageCacheProblemLog/.test(source) && /recordPageCacheProblem/.test(source) && /db-cache-miss/.test(source) && /webglPageCacheProblems/.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', /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 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 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 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 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)],
|
['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 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)],
|
['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)],
|
['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)],
|
['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)],
|
['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)],
|
['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 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 against a base-page texture instead of flat color blocks', /bookRevealBaseMap/.test(source) && /bookRevealUseBaseMap/.test(source) && /revealBaseColor/.test(source) && /baseCanvas/.test(textureRendererSource)],
|
||||||
['webgl reveal shader masks antialiased ink and uses smooth line-dominant scan', /smoothstep\(0\.52, 0\.9, luminance\)/.test(source) && /local\.x \* 0\.96/.test(source) && /bookRevealSoftness = \{ value: 0\.025 \}/.test(source)],
|
['webgl reveal 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 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 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)],
|
['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)],
|
['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)],
|
['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 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)],
|
['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 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', /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 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 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 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 publishes both spread sides for reveal preparation and fallback start', /this\.drawSpread\(this\.currentSpread \|\| this\.pagination\?\.getCurrentSpread\?\.\(\), \['left', 'right'\]\)/.test(textureRendererSource) && /const sides = \['left', 'right'\]/.test(textureRendererSource) && /pageMeta: prepared\.pageMeta \|\| \{\}/.test(textureRendererSource)],
|
||||||
['texture renderer marks page canvases with content versions before cache writes', /pageContentVersions/.test(textureRendererSource) && /buildPublishPageMeta/.test(textureRendererSource) && /completenessScore: \(maxBlockId \* 1000\) \+ lineCount/.test(textureRendererSource)],
|
['texture renderer marks page canvases with content versions before cache writes', /pageContentVersions/.test(textureRendererSource) && /buildPublishPageMeta/.test(textureRendererSource) && /completenessScore: \(maxBlockId \* 1000\) \+ lineCount/.test(textureRendererSource)],
|
||||||
['texture 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)],
|
['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 resident page texture cache rejects older page versions before direct reuse', /isOlderPageTextureMeta/.test(source) && /getResidentPageTextureForMeta/.test(source) && /getResidentPageTextureForMeta\(pageMeta\)/.test(source)],
|
['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 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 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 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)],
|
['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)],
|
['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)],
|
['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'))],
|
['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)],
|
['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)],
|
['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 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)],
|
['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) && /preloadOnly: true/.test(uiDisplayHandlerSource) && /bookTextureRenderer\.prepareRevealBlock\(previewRevealDetail, \{ preloadOnly: true \}\)/.test(uiDisplayHandlerSource) && /await this\.waitForWebGLPageFlip/.test(uiDisplayHandlerSource)],
|
['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 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 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)],
|
['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)],
|
['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)]
|
['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