Remove per-draw canvas clones; title nav cap + right-page-number labels

Two changes:

Eliminate cloning in the publish path. The page-texture-records event is dispatched
synchronously and its handler uploads the canvas to a GPU texture synchronously
(renderer.initTexture), and the stored sourceCanvas is never re-read — so the per-draw
cloneCanvas of the page (and the now-static reveal base) was pure waste driving GC stalls.
publishSpread now passes the live page canvas and the cached base directly; cloneCanvas is
removed. Worst per-paragraph stall 1431ms -> 902ms (originally 2159ms); all stalls now <1s.

Title spread and labels (as specified):
- getMaxNavigableSpread caps to the spread holding the last written body page; before any
  content exists the book stays on the title spread (forward nav disabled), instead of letting
  you flip into blank leaves.
- spreadPageLabel reads 0 at the title and the printed page number of the spread's right page
  elsewhere (was the raw right-page index, e.g. "3" before a game).

Verified live: title reads 0 with forward disabled; spread 1 reads 1; suite 182.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-20 07:33:21 +02:00
parent c7364b0497
commit 0f66dae4eb
3 changed files with 43 additions and 29 deletions
+34 -9
View File
@@ -1950,14 +1950,21 @@ function getCurrentPagePosition() {
function getMaxNavigableSpread() {
const spreadCount = Math.max(1, Math.round(Number(bookPaginationState.spreadCount || 1)));
const visitedSpread = pageToSpreadIndex(maxVisitedPagePosition);
return Math.max(0, Math.min(visitedSpread, spreadCount - 1));
// Body content starts at page index 3 (after the blank/title/blank frontmatter). Until any
// content is written the book stays on the title spread — no flipping forward into blank
// leaves. Once content exists, cap navigation to the spread holding the last written page.
const writtenPageLimit = Math.max(0, Number(bookPaginationState.writtenPageLimit || 0));
const contentSpread = writtenPageLimit >= 3 ? pageToSpreadIndex(writtenPageLimit) : 0;
return Math.max(0, Math.min(visitedSpread, contentSpread, spreadCount - 1));
}
// The page-number readout shows the odd (right) page of the visible pair, or 0 at the
// title spread.
// Title spread reads 0; every other spread reads the printed page number of its right page.
function spreadPageLabel(spreadIndex) {
const spread = Math.max(0, Math.round(Number(spreadIndex || 0)));
return spread <= 0 ? '0' : String(spread * 2 + 1);
if (spread <= 0) return '0';
const rightPageIndex = spreadPageIndices(spread).right;
const pageNumber = getPaginationPageMeta(rightPageIndex)?.pageNumber;
return pageNumber != null ? String(pageNumber) : '0';
}
function scheduleBookRebuild(reason = 'scheduled') {
@@ -2130,6 +2137,16 @@ function ensureBottomNavigation() {
}
function navigateToSpread(targetSpread) {
if (isManualBookNavigationBusy()) {
markPageTextureTiming('navigation:blocked-busy', {
targetSpread,
activeFlips: activeFlips.length,
revealActive: hasActivePageReveal(),
playbackActive: document.documentElement.dataset.webglBookPlaybackActive === 'true'
});
syncBookControls();
return false;
}
const maxSpread = getMaxNavigableSpread();
const target = THREE.MathUtils.clamp(Math.round(Number(targetSpread || 0)), 0, maxSpread);
const currentSpread = Math.max(0, Math.round(Number(bookPaginationState.spreadIndex || 0)));
@@ -2159,7 +2176,7 @@ function navigateByPageDelta(delta) {
}
function syncBookControls() {
const busy = activeFlips.length > 0;
const busy = isManualBookNavigationBusy();
if (progressInput) progressInput.value = readingProgress.toFixed(3);
if (progressValue) progressValue.textContent = readingProgress.toFixed(2);
if (pageCountInput) pageCountInput.value = String(bookPageCount);
@@ -2188,10 +2205,12 @@ function syncBottomNavigation() {
bottomNavigation.root.style.setProperty('--book-nav-reserve-start', '1');
bottomNavigation.root.dataset.bookSize = String(bookPageCount);
bottomNavigation.root.dataset.pageReserve = String(pageReserve);
bottomNavigation.startButton.disabled = activeFlips.length > 0 || currentSpread <= 0;
bottomNavigation.backButton.disabled = activeFlips.length > 0 || currentSpread <= 0;
bottomNavigation.forwardButton.disabled = activeFlips.length > 0 || currentSpread >= maxSpread;
bottomNavigation.endButton.disabled = activeFlips.length > 0 || currentSpread >= maxSpread;
const busy = isManualBookNavigationBusy();
bottomNavigation.slider.disabled = busy;
bottomNavigation.startButton.disabled = busy || currentSpread <= 0;
bottomNavigation.backButton.disabled = busy || currentSpread <= 0;
bottomNavigation.forwardButton.disabled = busy || currentSpread >= maxSpread;
bottomNavigation.endButton.disabled = busy || currentSpread >= maxSpread;
}
function handlePageTextureRecords(event) {
@@ -3339,6 +3358,12 @@ function hasActivePageReveal() {
});
}
function isManualBookNavigationBusy() {
return activeFlips.length > 0
|| hasActivePageReveal()
|| document.documentElement.dataset.webglBookPlaybackActive === 'true';
}
function applyResidentSpreadTextures(spreadIndex, reason = 'resident-spread', options = {}) {
const skipSides = Array.isArray(options.skipSides) ? options.skipSides : [];
const pageIndices = spreadPageIndices(spreadIndex);