Rework WebGL book playback to single ownership and fix flip/reveal pipeline

Establish book-playback-timeline as the sole playback owner driving the
scene through formal webgl-book:* events (not the BookLabDebug surface),
with a single reveal clock in the scene render loop and webgl-page-cache as
the only texture cache. Remove the legacy dual playback path and the
ownsPageFlipCommit gating.

Fixes:
- Flip page detached/folded at the spine: restore the raw page-cap line for
  flip geometry (matches the prototype/pre-regression), removing
  normalizeFlipLineToVisiblePage which moved the pivot off the spine arc.
- Flip textures: distance-based UVs (no horizontal compression),
  direction-aware face material (source on the camera-facing side), source
  meta derived from the visible spread (manual flips), prewarm shape fix.
- Reveal: flash removed on the static page and the flip back surface;
  spanning blocks rebuild the reveal plan at activate and continue the
  reveal on the next spread after the fill flip.
- Cache staleness is contentVersion-primary; nav clamps to spreadCount.

Docs updated to describe the intended single-owner architecture. Regression
checks updated to match.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-17 15:29:50 +02:00
parent c19ebe3089
commit 8bb18fa201
9 changed files with 439 additions and 438 deletions
+14 -10
View File
@@ -17,6 +17,10 @@ class WebGLBookSceneModule extends BaseModule {
this.gameConfig = null;
this.mode = '2d';
this.is3dSupported = false;
// Production control surface + visible-spread accessor for the dynamically
// imported webgl-book-lab. Populated by the lab once the scene is built.
this.sceneControl = null;
this.getVisibleSpreadIndex = null;
this.labImportPromise = null;
this.textureRefreshTimer = null;
this.textureRefreshAnimationId = null;
@@ -330,7 +334,7 @@ class WebGLBookSceneModule extends BaseModule {
this.persistenceManager?.updatePreference?.('webgl', 'pageReserve', value);
this.preferenceWriteGuard = false;
},
getBookState: () => window.BookLabDebug?.getBookState?.() || {
getBookState: () => this.sceneControl?.getBookState?.() || {
pageCount: this.persistenceManager?.getPreference?.('webgl', 'bookPageCount', DEFAULT_BOOK_PAGE_COUNT) ?? DEFAULT_BOOK_PAGE_COUNT,
pageReserve: this.persistenceManager?.getPreference?.('webgl', 'pageReserve', DEFAULT_PAGE_RESERVE) ?? DEFAULT_PAGE_RESERVE,
progress: this.persistenceManager?.getPreference?.('webgl', 'bookProgress', DEFAULT_BOOK_PROGRESS) ?? DEFAULT_BOOK_PROGRESS
@@ -341,19 +345,19 @@ class WebGLBookSceneModule extends BaseModule {
const progress = Number(state.progress);
if (Number.isFinite(pageCount)) {
this.persistenceManager?.updatePreference?.('webgl', 'bookPageCount', pageCount);
window.BookLabDebug?.setBookPageCount?.(pageCount);
this.sceneControl?.setBookPageCount?.(pageCount);
}
if (Number.isFinite(pageReserve)) {
this.persistenceManager?.updatePreference?.('webgl', 'pageReserve', pageReserve);
window.BookLabDebug?.setPageReserve?.(pageReserve);
this.sceneControl?.setPageReserve?.(pageReserve);
}
if (Number.isFinite(progress)) {
this.persistenceManager?.updatePreference?.('webgl', 'bookProgress', progress);
window.BookLabDebug?.setReadingProgress?.(progress);
this.sceneControl?.setReadingProgress?.(progress);
}
const maxVisitedPagePosition = Number(state.maxVisitedPagePosition ?? state.pagePosition);
if (Number.isFinite(maxVisitedPagePosition)) {
window.BookLabDebug?.setMaxVisitedPagePosition?.(maxVisitedPagePosition);
this.sceneControl?.setMaxVisitedPagePosition?.(maxVisitedPagePosition);
}
}
};
@@ -456,7 +460,7 @@ class WebGLBookSceneModule extends BaseModule {
}
projectCanvasEventTarget(event) {
const projection = window.BookLabDebug?.projectPointerToPage?.(event.clientX, event.clientY);
const projection = this.sceneControl?.projectPointerToPage?.(event.clientX, event.clientY);
if (!projection) {
document.documentElement.dataset.webglLastProjection = JSON.stringify({
hit: false,
@@ -531,11 +535,11 @@ class WebGLBookSceneModule extends BaseModule {
this.initializeScene();
}
} else if (key === 'bookProgress' && !this.preferenceWriteGuard) {
window.BookLabDebug?.setReadingProgress?.(value);
this.sceneControl?.setReadingProgress?.(value);
} else if (key === 'bookPageCount' && !this.preferenceWriteGuard) {
window.BookLabDebug?.setBookPageCount?.(value);
this.sceneControl?.setBookPageCount?.(value);
} else if (key === 'pageReserve' && !this.preferenceWriteGuard) {
window.BookLabDebug?.setPageReserve?.(value);
this.sceneControl?.setPageReserve?.(value);
}
}
@@ -556,7 +560,7 @@ class WebGLBookSceneModule extends BaseModule {
triggerTextureRefresh() {
clearTimeout(this.textureRefreshTimer);
this.textureRefreshTimer = setTimeout(() => {
window.BookLabDebug?.redrawPageTextures?.();
this.sceneControl?.redrawPageTextures?.();
}, 60);
}