Stabilize WebGL book pagination restore
This commit is contained in:
+112
-27
@@ -204,6 +204,7 @@ let bookPaginationState = {
|
||||
spreadCount: 1,
|
||||
writtenPageLimit: 0
|
||||
};
|
||||
let maxVisitedPagePosition = 0;
|
||||
const normalFlipDuration = 900;
|
||||
const fastFlipDuration = 520;
|
||||
const fastFlipCount = 10;
|
||||
@@ -364,7 +365,7 @@ const materials = {
|
||||
emissive: 0x100d08,
|
||||
emissiveIntensity: 0.004,
|
||||
envMapIntensity: 0.01,
|
||||
side: THREE.DoubleSide
|
||||
side: THREE.FrontSide
|
||||
}),
|
||||
leftPage: new THREE.MeshStandardMaterial({
|
||||
color: 0xffffff,
|
||||
@@ -414,7 +415,7 @@ const materials = {
|
||||
};
|
||||
materials.flipPageBackSurface = materials.flipPageSurface.clone();
|
||||
materials.flipPageBackSurface.map = getBlankPageTexture();
|
||||
materials.flipPageBackSurface.side = THREE.DoubleSide;
|
||||
materials.flipPageBackSurface.side = THREE.FrontSide;
|
||||
materials.flipPageEdge = materials.pageSurface.clone();
|
||||
materials.flipPageEdge.map = paperTextures.edge;
|
||||
materials.flipPageEdge.normalMap = paperTextures.normal;
|
||||
@@ -531,6 +532,7 @@ window.BookLabDebug = {
|
||||
pageReserve,
|
||||
progress: readingProgress,
|
||||
pagePosition: getCurrentPagePosition(),
|
||||
maxVisitedPagePosition,
|
||||
spreadIndex: bookPaginationState.spreadIndex,
|
||||
writtenPageLimit: bookPaginationState.writtenPageLimit
|
||||
};
|
||||
@@ -541,10 +543,17 @@ window.BookLabDebug = {
|
||||
spreadCount: Math.max(1, Number(state.spreadCount ?? bookPaginationState.spreadCount ?? 1)),
|
||||
writtenPageLimit: Math.max(0, Number(state.writtenPageLimit ?? bookPaginationState.writtenPageLimit ?? 0))
|
||||
};
|
||||
maxVisitedPagePosition = Math.max(maxVisitedPagePosition, getCurrentPagePosition());
|
||||
growBookIfWritableLimitReached();
|
||||
syncBookControls();
|
||||
return this.getBookState();
|
||||
},
|
||||
setMaxVisitedPagePosition(value) {
|
||||
const page = Math.max(0, Math.round(Number(value || 0)));
|
||||
maxVisitedPagePosition = Math.max(maxVisitedPagePosition, page);
|
||||
syncBookControls();
|
||||
return maxVisitedPagePosition;
|
||||
},
|
||||
navigateToPagePosition(value) {
|
||||
return navigateToPagePosition(value);
|
||||
},
|
||||
@@ -616,9 +625,26 @@ document.addEventListener('webgl-book:page-cache-problem', (event) => {
|
||||
});
|
||||
document.addEventListener('book-pagination:spread-updated', (event) => {
|
||||
const detail = event.detail || {};
|
||||
const incomingSpreadIndex = Math.max(0, Number(detail.spreadIndex || 0));
|
||||
const latestBlockId = Math.max(0, Number(detail.latestBlockId || 0));
|
||||
const latestRenderedBlockId = Math.max(0, Number(detail.latestRenderedBlockId || 0));
|
||||
if (
|
||||
latestBlockId > latestRenderedBlockId
|
||||
&& detail.allowFutureUnrendered !== true
|
||||
&& activeFlips.length === 0
|
||||
&& incomingSpreadIndex > Math.max(0, Number(bookPaginationState.spreadIndex || 0))
|
||||
) {
|
||||
markPageTextureTiming('spreadUpdate:deferred-future-unrendered', {
|
||||
incomingSpreadIndex,
|
||||
visibleSpreadIndex: bookPaginationState.spreadIndex,
|
||||
latestBlockId,
|
||||
latestRenderedBlockId
|
||||
});
|
||||
return;
|
||||
}
|
||||
const previousPageCount = bookPageCount;
|
||||
bookPaginationState = {
|
||||
spreadIndex: Math.max(0, Number(detail.spreadIndex || 0)),
|
||||
spreadIndex: incomingSpreadIndex,
|
||||
spreadCount: Math.max(1, Number(detail.spreadCount || 1)),
|
||||
writtenPageLimit: Math.max(0, Number(detail.writtenPageLimit || 0))
|
||||
};
|
||||
@@ -1920,10 +1946,10 @@ function ensureBottomNavigation() {
|
||||
startButton.addEventListener('click', () => navigateToPagePosition(0));
|
||||
backButton.addEventListener('click', () => navigateByPageDelta(-1));
|
||||
forwardButton.addEventListener('click', () => navigateByPageDelta(1));
|
||||
endButton.addEventListener('click', () => navigateToPagePosition(bookPaginationState.writtenPageLimit));
|
||||
endButton.addEventListener('click', () => navigateToPagePosition(maxVisitedPagePosition));
|
||||
slider.addEventListener('input', () => {
|
||||
const requested = Number(slider.value);
|
||||
const clamped = Math.min(requested, Math.max(0, bookPaginationState.writtenPageLimit || 0), getWritablePageLimit());
|
||||
const clamped = Math.min(requested, maxVisitedPagePosition, getWritablePageLimit());
|
||||
if (requested !== clamped) slider.value = String(clamped);
|
||||
pageLabel.textContent = `${appInitialState.t?.('webgl.page') || 'Page'} ${clamped}`;
|
||||
});
|
||||
@@ -1952,8 +1978,7 @@ function navigateByPageDelta(delta) {
|
||||
|
||||
function navigateToPagePosition(pagePosition) {
|
||||
const writableLimit = getWritablePageLimit();
|
||||
const writtenLimit = Math.max(0, bookPaginationState.writtenPageLimit || 0);
|
||||
const targetPage = THREE.MathUtils.clamp(Math.round(Number(pagePosition || 0)), 0, Math.min(writableLimit, writtenLimit));
|
||||
const targetPage = THREE.MathUtils.clamp(Math.round(Number(pagePosition || 0)), 0, Math.min(writableLimit, maxVisitedPagePosition));
|
||||
const currentPage = getCurrentPagePosition();
|
||||
if (targetPage === currentPage) {
|
||||
syncBookControls();
|
||||
@@ -1984,9 +2009,8 @@ function syncBookControls() {
|
||||
function syncBottomNavigation() {
|
||||
if (!bottomNavigation) return;
|
||||
const currentPage = getCurrentPagePosition();
|
||||
const writtenLimit = Math.max(0, bookPaginationState.writtenPageLimit || 0);
|
||||
const writableLimit = getWritablePageLimit();
|
||||
const navigableLimit = Math.min(writtenLimit, writableLimit);
|
||||
const navigableLimit = Math.min(maxVisitedPagePosition, writableLimit);
|
||||
const reservedStart = Math.max(0, writableLimit);
|
||||
bottomNavigation.slider.max = String(Math.max(0, bookPageCount));
|
||||
bottomNavigation.slider.value = String(Math.min(currentPage, navigableLimit));
|
||||
@@ -1994,7 +2018,7 @@ function syncBottomNavigation() {
|
||||
bottomNavigation.maxLabel.textContent = String(bookPageCount);
|
||||
bottomNavigation.pageLabel.textContent = `${appInitialState.t?.('webgl.page') || 'Page'} ${Math.min(currentPage, navigableLimit)}`;
|
||||
bottomNavigation.root.style.setProperty('--book-nav-position', `${bookPageCount > 0 ? currentPage / bookPageCount : 0}`);
|
||||
bottomNavigation.root.style.setProperty('--book-nav-written', `${bookPageCount > 0 ? writtenLimit / bookPageCount : 0}`);
|
||||
bottomNavigation.root.style.setProperty('--book-nav-written', `${bookPageCount > 0 ? maxVisitedPagePosition / bookPageCount : 0}`);
|
||||
bottomNavigation.root.style.setProperty('--book-nav-reserve-start', `${bookPageCount > 0 ? reservedStart / bookPageCount : 1}`);
|
||||
bottomNavigation.root.dataset.bookSize = String(bookPageCount);
|
||||
bottomNavigation.root.dataset.pageReserve = String(pageReserve);
|
||||
@@ -2132,7 +2156,7 @@ function recordPageCacheProblem(detail = {}) {
|
||||
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 = makePageMetaForCache(pageIndex).pageIndex;
|
||||
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?.();
|
||||
@@ -2167,13 +2191,26 @@ function isOlderPageTextureMeta(incoming = {}, existing = null) {
|
||||
}
|
||||
|
||||
function makePageMetaForCache(pageIndex) {
|
||||
const index = Math.max(0, Math.round(Number(pageIndex || 0)));
|
||||
const paginationMeta = getPaginationPageMeta(index) || {};
|
||||
return {
|
||||
pageIndex: Math.max(0, Math.round(Number(pageIndex || 0))),
|
||||
...paginationMeta,
|
||||
pageIndex: index,
|
||||
width: pageTextureWidth,
|
||||
height: leftCanvas?.height || Math.round(pageTextureWidth * PROCEDURAL_BOOK.PAGE_DEPTH / PROCEDURAL_BOOK.PAGE_WIDTH)
|
||||
};
|
||||
}
|
||||
|
||||
function makeResidentPageTextureKey(pageMetaOrIndex = {}) {
|
||||
const pageMeta = typeof pageMetaOrIndex === 'number'
|
||||
? makePageMetaForCache(pageMetaOrIndex)
|
||||
: pageMetaOrIndex || {};
|
||||
const pageIndex = Math.max(0, Math.round(Number(pageMeta.pageIndex || 0)));
|
||||
const kind = String(pageMeta.kind || 'content');
|
||||
const section = String(pageMeta.section || 'body');
|
||||
return `${pageIndex}:${kind}:${section}`;
|
||||
}
|
||||
|
||||
function spreadPageIndices(spreadIndex) {
|
||||
const spread = Math.max(0, Math.round(Number(spreadIndex || 0)));
|
||||
return {
|
||||
@@ -2183,7 +2220,7 @@ function spreadPageIndices(spreadIndex) {
|
||||
}
|
||||
|
||||
function getResidentPageTexture(pageIndex) {
|
||||
const key = makePageMetaForCache(pageIndex).pageIndex;
|
||||
const key = makeResidentPageTextureKey(pageIndex);
|
||||
const resident = residentPageTextures.get(key);
|
||||
if (!resident) return null;
|
||||
resident.lastUsedAt = performance.now();
|
||||
@@ -2195,7 +2232,7 @@ function getResidentPageTexture(pageIndex) {
|
||||
function getResidentPageTextureForMeta(pageMeta = null) {
|
||||
const pageIndex = Number(pageMeta?.pageIndex);
|
||||
if (!Number.isFinite(pageIndex)) return null;
|
||||
const key = makePageMetaForCache(pageIndex).pageIndex;
|
||||
const key = makeResidentPageTextureKey(pageMeta);
|
||||
const resident = residentPageTextures.get(key);
|
||||
if (!resident || isOlderPageTextureMeta(pageMeta, resident.pageMeta)) return null;
|
||||
return getResidentPageTexture(pageIndex);
|
||||
@@ -2203,13 +2240,20 @@ function getResidentPageTextureForMeta(pageMeta = null) {
|
||||
|
||||
async function preloadCachedPageTexture(pageIndex) {
|
||||
const meta = makePageMetaForCache(pageIndex);
|
||||
if (residentPageTextures.has(meta.pageIndex)) {
|
||||
const residentKey = makeResidentPageTextureKey(meta);
|
||||
if (residentPageTextures.has(residentKey)) {
|
||||
getResidentPageTexture(meta.pageIndex);
|
||||
return residentPageTextures.get(meta.pageIndex)?.texture || null;
|
||||
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,
|
||||
@@ -2220,7 +2264,7 @@ async function preloadCachedPageTexture(pageIndex) {
|
||||
}
|
||||
const texture = createPageCanvasTexture(sourceCanvas);
|
||||
const cachedMeta = sourceCanvas.__webglPageCacheMeta || meta;
|
||||
residentPageTextures.set(meta.pageIndex, {
|
||||
residentPageTextures.set(residentKey, {
|
||||
texture,
|
||||
sourceCanvas,
|
||||
lastUsedAt: performance.now(),
|
||||
@@ -2236,6 +2280,19 @@ async function preloadCachedPageTexture(pageIndex) {
|
||||
return texture;
|
||||
}
|
||||
|
||||
function getPaginationPageMeta(pageIndex) {
|
||||
const index = Math.max(0, Math.round(Number(pageIndex || 0)));
|
||||
const spreadIndex = Math.floor(index / 2);
|
||||
const side = index % 2 === 0 ? 'left' : 'right';
|
||||
const pagination = window.moduleRegistry?.getModule?.('book-pagination') || null;
|
||||
const spread = typeof pagination?.getSpread === 'function'
|
||||
? pagination.getSpread(spreadIndex)
|
||||
: Array.isArray(pagination?.spreads)
|
||||
? pagination.spreads[spreadIndex]
|
||||
: null;
|
||||
return spread?.pageMeta?.[side] || null;
|
||||
}
|
||||
|
||||
async function prewarmSpreadTextures(spreadIndex) {
|
||||
const indices = spreadPageIndices(spreadIndex);
|
||||
const [left, right] = await Promise.all([
|
||||
@@ -2276,7 +2333,8 @@ function takePreparedPageTexture(side, revealDetail = {}) {
|
||||
function uploadPageTextureDirect(side, sourceCanvas, pageMeta = null) {
|
||||
const texture = side === 'left' ? leftTexture : rightTexture;
|
||||
const material = side === 'left' ? materials.leftPage : materials.rightPage;
|
||||
const residentTexture = Number.isFinite(Number(pageMeta?.pageIndex))
|
||||
const shouldUseResidentTexture = pageMeta?.kind !== 'title';
|
||||
const residentTexture = shouldUseResidentTexture && Number.isFinite(Number(pageMeta?.pageIndex))
|
||||
? getResidentPageTextureForMeta(pageMeta)
|
||||
: null;
|
||||
markPageTextureTiming('directUpload:start', {
|
||||
@@ -2703,6 +2761,13 @@ function startPageFlipPrepared(direction, options = {}) {
|
||||
delete document.documentElement.dataset.webglPendingPageFlip;
|
||||
activeFlips.push(flip);
|
||||
setPageFlipActiveFlag();
|
||||
document.dispatchEvent(new CustomEvent('webgl-book:page-flip-started', {
|
||||
detail: {
|
||||
direction: flip.direction,
|
||||
sourceSide: flip.sourcePageSide || (flip.direction > 0 ? 'right' : 'left'),
|
||||
targetSpread: Number.isFinite(Number(flip.targetSpread)) ? Math.max(0, Math.round(Number(flip.targetSpread))) : null
|
||||
}
|
||||
}));
|
||||
syncBookControls();
|
||||
updateActiveFlips(flip.startTime);
|
||||
return true;
|
||||
@@ -2723,6 +2788,7 @@ function startFastPageFlipPrepared(direction, options = {}) {
|
||||
if (activeFlips.length || !currentProceduralBookModel || !canPageFlip(direction)) return false;
|
||||
const firstFlip = createPageFlip(direction, performance.now(), fastFlipDuration);
|
||||
if (!firstFlip) return false;
|
||||
firstFlip.targetSpread = Number.isFinite(Number(options.targetSpread)) ? Math.max(0, Math.round(Number(options.targetSpread))) : null;
|
||||
if (!prepareStaticPageForFlip(firstFlip, options.prewarm || null)) return false;
|
||||
const startTime = firstFlip.startTime;
|
||||
const interval = fastFlipDuration / fastFlipOverlap;
|
||||
@@ -2740,6 +2806,14 @@ function startFastPageFlipPrepared(direction, options = {}) {
|
||||
});
|
||||
}
|
||||
setPageFlipActiveFlag();
|
||||
document.dispatchEvent(new CustomEvent('webgl-book:page-flip-started', {
|
||||
detail: {
|
||||
direction: firstFlip.direction,
|
||||
sourceSide: firstFlip.sourcePageSide || (firstFlip.direction > 0 ? 'right' : 'left'),
|
||||
targetSpread: Number.isFinite(Number(firstFlip.targetSpread)) ? Math.max(0, Math.round(Number(firstFlip.targetSpread))) : null,
|
||||
fast: true
|
||||
}
|
||||
}));
|
||||
syncBookControls();
|
||||
updateActiveFlips(startTime);
|
||||
return true;
|
||||
@@ -2774,7 +2848,8 @@ function prepareStaticPageForFlip(flip, prewarm = null) {
|
||||
: Math.max(0, Math.round(Number(bookPaginationState.spreadIndex || 0)) + Math.sign(Number(flip.direction || 0)));
|
||||
const targetPages = spreadPageIndices(targetSpread);
|
||||
const targetBackPageIndex = flip.direction > 0 ? targetPages.left : targetPages.right;
|
||||
const residentBackTexture = getResidentPageTexture(targetBackPageIndex);
|
||||
const prewarmedBackTexture = flip.direction > 0 ? prewarm?.next?.left : prewarm?.next?.right;
|
||||
const residentBackTexture = prewarmedBackTexture || getResidentPageTexture(targetBackPageIndex);
|
||||
const requiresWrittenTexture = targetBackPageIndex <= Math.max(2, Number(bookPaginationState.writtenPageLimit || 0));
|
||||
if (!residentBackTexture && requiresWrittenTexture) {
|
||||
recordPageCacheProblem({
|
||||
@@ -2827,7 +2902,7 @@ function prepareStaticPageForFlip(flip, prewarm = null) {
|
||||
function canPageFlip(direction) {
|
||||
if (!currentProceduralBookModel) return false;
|
||||
const currentPage = getCurrentPagePosition();
|
||||
const maxNavigablePage = Math.min(Math.max(0, bookPaginationState.writtenPageLimit || 0), getWritablePageLimit());
|
||||
const maxNavigablePage = Math.min(maxVisitedPagePosition, getWritablePageLimit());
|
||||
if (direction > 0) return currentPage < maxNavigablePage;
|
||||
return currentPage > 0;
|
||||
}
|
||||
@@ -3031,7 +3106,7 @@ function lineYAtX(points, x) {
|
||||
|
||||
function setActivePageGeometry(flip, surface) {
|
||||
if (!flip.mesh) {
|
||||
const geometry = createFlippingPageGeometry(surface);
|
||||
const geometry = createFlippingPageGeometry(surface, flip.direction);
|
||||
flip.mesh = new THREE.Mesh(geometry, [
|
||||
materials.flipPageSurface,
|
||||
materials.flipPageBackSurface,
|
||||
@@ -3045,13 +3120,13 @@ function setActivePageGeometry(flip, surface) {
|
||||
return;
|
||||
}
|
||||
if (!updateFlippingPageGeometry(flip.mesh.geometry, surface)) {
|
||||
const geometry = createFlippingPageGeometry(surface);
|
||||
const geometry = createFlippingPageGeometry(surface, flip.direction);
|
||||
flip.mesh.geometry.dispose();
|
||||
flip.mesh.geometry = geometry;
|
||||
}
|
||||
}
|
||||
|
||||
function createFlippingPageGeometry(surface) {
|
||||
function createFlippingPageGeometry(surface, direction = 1) {
|
||||
const positions = [];
|
||||
const uvs = [];
|
||||
const indices = [];
|
||||
@@ -3063,10 +3138,12 @@ function createFlippingPageGeometry(surface) {
|
||||
const pageThickness = Math.max(0.0008, Number(PROCEDURAL_BOOK.SHEET_THICKNESS_MODEL || 0.001));
|
||||
const widthSegments = surface.length - 1;
|
||||
const depthSegments = surface[0].length - 1;
|
||||
const push = (point, yOffset, u, v) => {
|
||||
const sourceSide = direction > 0 ? 1 : -1;
|
||||
const targetSide = -sourceSide;
|
||||
const push = (point, yOffset, uv) => {
|
||||
const index = positions.length / 3;
|
||||
positions.push(point.x, point.y + yOffset, point.z);
|
||||
uvs.push(u, v);
|
||||
uvs.push(uv.x, uv.y);
|
||||
return index;
|
||||
};
|
||||
|
||||
@@ -3076,8 +3153,8 @@ function createFlippingPageGeometry(surface) {
|
||||
const u = widthSegments <= 0 ? 0 : widthIndex / widthSegments;
|
||||
rowPoints.forEach((point, depthIndex) => {
|
||||
const v = depthSegments <= 0 ? 0 : depthIndex / depthSegments;
|
||||
topRow.push(push(point, pageThickness, u, v));
|
||||
bottomRow.push(push(point, 0, u, 1 - v));
|
||||
topRow.push(push(point, pageThickness, pageUvForSide(sourceSide, u, v)));
|
||||
bottomRow.push(push(point, 0, pageUvForSide(targetSide, u, v)));
|
||||
});
|
||||
topGrid.push(topRow);
|
||||
bottomGrid.push(bottomRow);
|
||||
@@ -3123,6 +3200,13 @@ function createFlippingPageGeometry(surface) {
|
||||
}
|
||||
}
|
||||
|
||||
function pageUvForSide(side, u, v) {
|
||||
return {
|
||||
x: side < 0 ? 1 - u : u,
|
||||
y: 1 - v
|
||||
};
|
||||
}
|
||||
|
||||
function updateFlippingPageGeometry(geometry, surface) {
|
||||
const position = geometry?.getAttribute?.('position');
|
||||
if (!position || !surface?.length || !surface[0]?.length) return false;
|
||||
@@ -3160,6 +3244,7 @@ function finishActiveFlip(flip) {
|
||||
...bookPaginationState,
|
||||
spreadIndex: Math.max(0, Math.round(Number(flip.targetSpread)))
|
||||
};
|
||||
maxVisitedPagePosition = Math.max(maxVisitedPagePosition, getCurrentPagePosition());
|
||||
syncReadingProgressToCurrentPage();
|
||||
}
|
||||
document.dispatchEvent(new CustomEvent('webgl-book:page-flip-finished', {
|
||||
|
||||
Reference in New Issue
Block a user