Stabilize WebGL flip reveal handoff
This commit is contained in:
@@ -108,10 +108,10 @@ class BookPlaybackTimelineModule extends BaseModule {
|
||||
await this.timeStage('activate', segment, () => this.activatePreparedSegment(segment, sentence));
|
||||
|
||||
sentence.webglRevealController = () => this.startRevealForSegment(segment);
|
||||
const visualPromise = this.waitForVisualCompletion(segment);
|
||||
const playbackPromise = this.timeStage('playback', segment, () => {
|
||||
return this.playbackCoordinator?.play?.(sentence) || Promise.resolve();
|
||||
});
|
||||
const visualPromise = this.waitForVisualCompletion(segment);
|
||||
await Promise.all([playbackPromise, visualPromise]);
|
||||
|
||||
this.recordDiagnostic('segment-play:end', segment);
|
||||
@@ -172,8 +172,14 @@ class BookPlaybackTimelineModule extends BaseModule {
|
||||
requiresRightFlip: revealSides.includes('right') && this.requiresRightPageFlipAfterReveal(previewSpread),
|
||||
preparedTexturePlan: texturePlan,
|
||||
preparedAt: performance.now(),
|
||||
revealStartedAt: null,
|
||||
revealStartedPromise: null,
|
||||
resolveRevealStarted: null,
|
||||
status: 'prepared'
|
||||
};
|
||||
segment.revealStartedPromise = new Promise(resolve => {
|
||||
segment.resolveRevealStarted = resolve;
|
||||
});
|
||||
|
||||
this.applyTexturePlan(texturePlan, segment, 'prepare');
|
||||
await this.timeStage('texture-prewarm', segment, () => this.prewarmSegmentTextures(segment));
|
||||
@@ -303,6 +309,11 @@ class BookPlaybackTimelineModule extends BaseModule {
|
||||
throw new Error('BookPlaybackTimeline: WebGL book lab cannot start prepared reveals explicitly');
|
||||
}
|
||||
window.BookLabDebug.startRevealForBlock(segment.blockId);
|
||||
segment.revealStartedAt = performance.now();
|
||||
if (typeof segment.resolveRevealStarted === 'function') {
|
||||
segment.resolveRevealStarted(segment.revealStartedAt);
|
||||
segment.resolveRevealStarted = null;
|
||||
}
|
||||
this.markBenchmark('reveal-start', segment);
|
||||
this.recordDiagnostic('reveal-started', segment);
|
||||
return true;
|
||||
@@ -337,7 +348,7 @@ class BookPlaybackTimelineModule extends BaseModule {
|
||||
this.recordDiagnostic('visual-completion:no-right-flip-wait', segment);
|
||||
return;
|
||||
}
|
||||
const committed = await this.timeStage('wait-right-reveal-commit', segment, () => this.waitForRevealCommit(segment));
|
||||
const committed = await this.timeStage('wait-right-reveal-commit', segment, () => this.waitForPlannedRightReveal(segment));
|
||||
if (!committed || this.isChoiceAwaitingPlayer()) return;
|
||||
await this.timeStage('right-page-flip', segment, () => this.requestPageFlip(1, {
|
||||
reason: 'timeline-right-page-filled',
|
||||
@@ -346,6 +357,29 @@ class BookPlaybackTimelineModule extends BaseModule {
|
||||
}));
|
||||
}
|
||||
|
||||
async waitForPlannedRightReveal(segment = {}) {
|
||||
const startedAt = Number(segment.revealStartedAt)
|
||||
|| await (segment.revealStartedPromise || Promise.resolve(performance.now()));
|
||||
const duration = this.getRightRevealDurationMs(segment);
|
||||
const elapsed = Math.max(0, performance.now() - Number(startedAt || performance.now()));
|
||||
const remaining = Math.max(0, duration - elapsed);
|
||||
const planned = new Promise(resolve => {
|
||||
setTimeout(() => resolve(true), remaining);
|
||||
});
|
||||
return Promise.race([
|
||||
planned,
|
||||
this.waitForRevealCommit(segment)
|
||||
]);
|
||||
}
|
||||
|
||||
getRightRevealDurationMs(segment = {}) {
|
||||
const duration = Number(segment.activeTexturePlan?.reveal?.right?.durationMs
|
||||
?? segment.preparedTexturePlan?.reveal?.right?.durationMs
|
||||
?? 0);
|
||||
if (Number.isFinite(duration) && duration > 0) return duration;
|
||||
return Math.max(1, Number(segment.sentence?.animation?.totalDuration || 1));
|
||||
}
|
||||
|
||||
waitForRevealCommit(segment = {}) {
|
||||
const blockId = String(segment.blockId ?? '');
|
||||
if (!blockId) return Promise.resolve(false);
|
||||
|
||||
@@ -148,6 +148,9 @@ class BookTextureRendererModule extends BaseModule {
|
||||
this.drawSpread(this.currentSpread);
|
||||
});
|
||||
this.addEventListener(document, 'book-texture:fast-forward', this.fastForwardAnimations);
|
||||
this.addEventListener(document, 'webgl-book:reveal-committed', (event) => {
|
||||
this.completeRevealBlockIds(event.detail?.blockIds || []);
|
||||
});
|
||||
this.addEventListener(document, 'ui:command', (event) => {
|
||||
if (event.detail?.type === 'continue') this.fastForwardAnimations();
|
||||
});
|
||||
@@ -1081,6 +1084,18 @@ class BookTextureRendererModule extends BaseModule {
|
||||
}
|
||||
}
|
||||
|
||||
completeRevealBlockIds(blockIds = []) {
|
||||
const ids = Array.isArray(blockIds) ? blockIds : [];
|
||||
ids.forEach((blockId) => {
|
||||
const id = String(blockId ?? '');
|
||||
if (!id) return;
|
||||
const animation = this.activeAnimations.get(id);
|
||||
if (animation) animation.completed = true;
|
||||
this.revealedBlockIds.add(id);
|
||||
this.pendingRevealBlockIds.delete(id);
|
||||
});
|
||||
}
|
||||
|
||||
stopAnimations() {
|
||||
this.activeAnimations.clear();
|
||||
this.pendingRevealBlockIds.clear();
|
||||
|
||||
@@ -76,7 +76,7 @@ class GameLoopModule extends BaseModule {
|
||||
return true;
|
||||
}
|
||||
|
||||
start() {
|
||||
async start() {
|
||||
console.log("GameLoop: Starting game sequence...");
|
||||
|
||||
try {
|
||||
@@ -87,12 +87,14 @@ class GameLoopModule extends BaseModule {
|
||||
console.log("GameLoop: Setting up socket listeners and connecting...");
|
||||
|
||||
// Set up socket event listeners and connect
|
||||
this.setupSocketEventListeners();
|
||||
const connected = await this.setupSocketEventListeners();
|
||||
|
||||
// Set the game loop as running
|
||||
this.isRunning = true;
|
||||
return connected;
|
||||
} catch (error) {
|
||||
console.error("Error starting game loop:", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,7 +135,7 @@ class GameLoopModule extends BaseModule {
|
||||
|
||||
if (!socketClient) {
|
||||
console.error("Socket client module not found");
|
||||
return;
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
|
||||
// Connect UI controller to socket client for command handling
|
||||
@@ -181,12 +183,13 @@ class GameLoopModule extends BaseModule {
|
||||
});
|
||||
|
||||
// Connect to the socket server
|
||||
socketClient.connect().then(success => {
|
||||
return socketClient.connect().then(success => {
|
||||
if (success) {
|
||||
console.log("GameLoop: Socket connection established successfully.");
|
||||
} else {
|
||||
console.error("GameLoop: Failed to connect to socket server");
|
||||
}
|
||||
return success;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+7
-7
@@ -24,7 +24,7 @@ const ModuleState = {
|
||||
ERROR: 'ERROR'
|
||||
};
|
||||
|
||||
const MODULE_CACHE_BUSTER = '20260610-book-timeline-j';
|
||||
const MODULE_CACHE_BUSTER = '20260610-book-timeline-l';
|
||||
window.MODULE_CACHE_BUSTER = MODULE_CACHE_BUSTER;
|
||||
|
||||
/**
|
||||
@@ -830,17 +830,17 @@ const ModuleLoader = (function() {
|
||||
async function completeFinalization() {
|
||||
isLoadingComplete = true;
|
||||
|
||||
// Call the start method on the game loop module directly
|
||||
// Ensure the game loop module was found during initialization
|
||||
// Call the start method on the game loop module directly.
|
||||
// Starting before hiding the overlay lets socket connection and
|
||||
// save/resume state settle as part of the loader handoff.
|
||||
if (gameLoopModule && typeof gameLoopModule.start === 'function') {
|
||||
// Hide the overlay first, then start the game loop
|
||||
await hideOverlay();
|
||||
console.log("Loader: Overlay hidden, starting Game Loop.");
|
||||
try {
|
||||
gameLoopModule.start();
|
||||
console.log("Loader: Starting Game Loop before hiding overlay.");
|
||||
await gameLoopModule.start();
|
||||
} catch (error) {
|
||||
console.error("Error starting Game Loop:", error);
|
||||
}
|
||||
await hideOverlay();
|
||||
} else {
|
||||
console.error("Loader: Game Loop module not found or start method missing.");
|
||||
// Hide overlay anyway, but log error
|
||||
|
||||
+111
-25
@@ -4,7 +4,7 @@ import { RenderPass } from 'https://esm.sh/three@0.165.0/examples/jsm/postproces
|
||||
import { SSAOPass } from 'https://esm.sh/three@0.165.0/examples/jsm/postprocessing/SSAOPass.js';
|
||||
import { SMAAPass } from 'https://esm.sh/three@0.165.0/examples/jsm/postprocessing/SMAAPass.js';
|
||||
import { OutputPass } from 'https://esm.sh/three@0.165.0/examples/jsm/postprocessing/OutputPass.js';
|
||||
import { PROCEDURAL_BOOK, createProceduralBookModel, snapProceduralPageCount } from './procedural-book-model.js?v=20260610-book-timeline-j';
|
||||
import { PROCEDURAL_BOOK, createProceduralBookModel, snapProceduralPageCount } from './procedural-book-model.js?v=20260610-book-timeline-l';
|
||||
|
||||
const canvas = document.getElementById('scene');
|
||||
canvas.style.cursor = 'grab';
|
||||
@@ -430,6 +430,12 @@ materials.flipPageEdge.map = paperTextures.edge;
|
||||
materials.flipPageEdge.normalMap = paperTextures.normal;
|
||||
materials.flipPageEdge.roughnessMap = paperTextures.roughness;
|
||||
materials.flipPageEdge.side = THREE.DoubleSide;
|
||||
materials.flipPageSurface.userData.bookPageReveal = {
|
||||
side: 'flipFront'
|
||||
};
|
||||
materials.flipPageBackSurface.userData.bookPageReveal = {
|
||||
side: 'flipBack'
|
||||
};
|
||||
materials.leftPage.userData.bookPageReveal = {
|
||||
side: 'left'
|
||||
};
|
||||
@@ -441,6 +447,8 @@ materials.headband.userData.isHeadband = true;
|
||||
configureHardcoverPaperMaterial(materials.pageBlock);
|
||||
configureHardcoverPaperMaterial(materials.pageEdge, { useEdgeMap: true });
|
||||
configureHardcoverPaperMaterial(materials.pageSurface);
|
||||
configureHardcoverPaperMaterial(materials.flipPageSurface);
|
||||
configureHardcoverPaperMaterial(materials.flipPageBackSurface);
|
||||
configureHardcoverPaperMaterial(materials.leftPage);
|
||||
configureHardcoverPaperMaterial(materials.rightPage);
|
||||
|
||||
@@ -2072,30 +2080,36 @@ function syncBottomNavigation() {
|
||||
|
||||
function handlePageTextureRecords(event) {
|
||||
const detail = normalizePageTextureRecordDetail(event.detail || {});
|
||||
if (detail.pageMeta) {
|
||||
currentPageMeta = normalizePageMetaPair(detail.pageMeta, currentPageMeta);
|
||||
const incomingPageMeta = detail.pageMeta
|
||||
? normalizePageMetaPair(detail.pageMeta, currentPageMeta)
|
||||
: currentPageMeta;
|
||||
const effectivePageMeta = detail.phase === 'prepare'
|
||||
? incomingPageMeta
|
||||
: incomingPageMeta;
|
||||
if (detail.phase !== 'prepare' && detail.pageMeta) {
|
||||
currentPageMeta = incomingPageMeta;
|
||||
}
|
||||
markPageTextureTiming('handlePageTextureRecords:start', {
|
||||
hasLeft: Boolean(detail.left),
|
||||
hasRight: Boolean(detail.right),
|
||||
revealSides: Object.keys(detail.reveal || {}),
|
||||
phase: detail.phase || 'activate',
|
||||
pageMeta: currentPageMeta
|
||||
pageMeta: effectivePageMeta
|
||||
});
|
||||
const leftReveal = attachRevealPageMeta(detail.reveal?.left, currentPageMeta.left || null);
|
||||
const rightReveal = attachRevealPageMeta(detail.reveal?.right, currentPageMeta.right || null);
|
||||
const leftReveal = attachRevealPageMeta(detail.reveal?.left, effectivePageMeta.left || null);
|
||||
const rightReveal = attachRevealPageMeta(detail.reveal?.right, effectivePageMeta.right || null);
|
||||
if (detail.phase === 'prepare') {
|
||||
if (detail.left) {
|
||||
const texture = preloadPageTexture('left', detail.left, leftReveal, currentPageMeta.left);
|
||||
pageTextureStore?.rememberResidentTexture?.(currentPageMeta.left, texture, detail.left, true);
|
||||
} else if (currentPageMeta.left?.kind === 'blank') {
|
||||
pageTextureStore?.rememberResidentTexture?.(currentPageMeta.left, getBlankPageTexture(), null, false);
|
||||
const texture = preloadPageTexture('left', detail.left, leftReveal, effectivePageMeta.left);
|
||||
pageTextureStore?.rememberResidentTexture?.(effectivePageMeta.left, texture, detail.left, true);
|
||||
} else if (effectivePageMeta.left?.kind === 'blank') {
|
||||
pageTextureStore?.rememberResidentTexture?.(effectivePageMeta.left, getBlankPageTexture(), null, false);
|
||||
}
|
||||
if (detail.right) {
|
||||
const texture = preloadPageTexture('right', detail.right, rightReveal, currentPageMeta.right);
|
||||
pageTextureStore?.rememberResidentTexture?.(currentPageMeta.right, texture, detail.right, true);
|
||||
} else if (currentPageMeta.right?.kind === 'blank') {
|
||||
pageTextureStore?.rememberResidentTexture?.(currentPageMeta.right, getBlankPageTexture(), null, false);
|
||||
const texture = preloadPageTexture('right', detail.right, rightReveal, effectivePageMeta.right);
|
||||
pageTextureStore?.rememberResidentTexture?.(effectivePageMeta.right, texture, detail.right, true);
|
||||
} else if (effectivePageMeta.right?.kind === 'blank') {
|
||||
pageTextureStore?.rememberResidentTexture?.(effectivePageMeta.right, getBlankPageTexture(), null, false);
|
||||
}
|
||||
markPageTextureTiming('handlePageTextureRecords:prepare:end');
|
||||
return;
|
||||
@@ -2104,21 +2118,21 @@ function handlePageTextureRecords(event) {
|
||||
if (leftReveal) {
|
||||
beginPageReveal('left', detail.left, leftReveal);
|
||||
} else {
|
||||
uploadPageTextureDirect('left', detail.left, currentPageMeta.left);
|
||||
uploadPageTextureDirect('left', detail.left, effectivePageMeta.left);
|
||||
}
|
||||
}
|
||||
if (detail.right) {
|
||||
if (rightReveal) {
|
||||
beginPageReveal('right', detail.right, rightReveal);
|
||||
} else {
|
||||
uploadPageTextureDirect('right', detail.right, currentPageMeta.right);
|
||||
uploadPageTextureDirect('right', detail.right, effectivePageMeta.right);
|
||||
}
|
||||
}
|
||||
if (!detail.left && currentPageMeta.left?.kind === 'blank') {
|
||||
applyExplicitBlankPageTexture('left', currentPageMeta.left, 'page-texture-records');
|
||||
if (!detail.left && effectivePageMeta.left?.kind === 'blank') {
|
||||
applyExplicitBlankPageTexture('left', effectivePageMeta.left, 'page-texture-records');
|
||||
}
|
||||
if (!detail.right && currentPageMeta.right?.kind === 'blank') {
|
||||
applyExplicitBlankPageTexture('right', currentPageMeta.right, 'page-texture-records');
|
||||
if (!detail.right && effectivePageMeta.right?.kind === 'blank') {
|
||||
applyExplicitBlankPageTexture('right', effectivePageMeta.right, 'page-texture-records');
|
||||
}
|
||||
markStaticSceneBuffersDirty();
|
||||
document.documentElement.dataset.webglPageTextureMetrics = JSON.stringify({
|
||||
@@ -2427,6 +2441,7 @@ function beginPageReveal(side, sourceCanvas, revealDetail = {}) {
|
||||
});
|
||||
if (prepared?.texture) {
|
||||
material.map = prepared.texture;
|
||||
material.needsUpdate = true;
|
||||
} else {
|
||||
if (material.map !== texture) {
|
||||
material.map = texture;
|
||||
@@ -2549,10 +2564,44 @@ function applyPageRevealRegions(shader, regions = []) {
|
||||
}
|
||||
|
||||
function getPageRevealShader(side) {
|
||||
const material = side === 'left' ? materials.leftPage : materials.rightPage;
|
||||
const material = side === 'left'
|
||||
? materials.leftPage
|
||||
: side === 'right'
|
||||
? materials.rightPage
|
||||
: side === 'flipFront'
|
||||
? materials.flipPageSurface
|
||||
: side === 'flipBack'
|
||||
? materials.flipPageBackSurface
|
||||
: null;
|
||||
return material?.userData?.bookRevealShader || null;
|
||||
}
|
||||
|
||||
function syncFlipRevealShaderFromSource(sourceSide, targetMaterial = materials.flipPageSurface) {
|
||||
if (!sourceSide || !targetMaterial?.userData) return false;
|
||||
const sourceState = pageRevealState[sourceSide];
|
||||
const sourceShader = getPageRevealShader(sourceSide);
|
||||
const targetShader = targetMaterial.userData.bookRevealShader || null;
|
||||
if (!sourceState || !sourceShader?.uniforms || !targetShader?.uniforms) return false;
|
||||
const sourceUniforms = sourceShader.uniforms;
|
||||
const targetUniforms = targetShader.uniforms;
|
||||
targetUniforms.bookRevealActive.value = sourceUniforms.bookRevealActive?.value || 0;
|
||||
targetUniforms.bookRevealElapsedMs.value = sourceUniforms.bookRevealElapsedMs?.value || sourceState.visualElapsedMs || 0;
|
||||
targetUniforms.bookRevealRegionCount.value = sourceUniforms.bookRevealRegionCount?.value || 0;
|
||||
if (targetUniforms.bookRevealBaseMap) targetUniforms.bookRevealBaseMap.value = sourceUniforms.bookRevealBaseMap?.value || sourceState.baseTexture || targetMaterial.map;
|
||||
if (targetUniforms.bookRevealUseBaseMap) targetUniforms.bookRevealUseBaseMap.value = sourceUniforms.bookRevealUseBaseMap?.value || 0;
|
||||
const sourceRects = sourceUniforms.bookRevealRegionRects?.value || [];
|
||||
const targetRects = targetUniforms.bookRevealRegionRects?.value || [];
|
||||
const sourceTimings = sourceUniforms.bookRevealRegionTimings?.value || [];
|
||||
const targetTimings = targetUniforms.bookRevealRegionTimings?.value || [];
|
||||
for (let index = 0; index < Math.min(sourceRects.length, targetRects.length); index += 1) {
|
||||
targetRects[index].copy(sourceRects[index]);
|
||||
}
|
||||
for (let index = 0; index < Math.min(sourceTimings.length, targetTimings.length); index += 1) {
|
||||
targetTimings[index].copy(sourceTimings[index]);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function getRevealDebugState() {
|
||||
return ['left', 'right'].reduce((state, side) => {
|
||||
const shader = getPageRevealShader(side);
|
||||
@@ -2699,6 +2748,12 @@ function updatePageRevealAnimations(now) {
|
||||
}
|
||||
const progress = THREE.MathUtils.clamp(state.visualElapsedMs / state.durationMs, 0, 1);
|
||||
shader.uniforms.bookRevealElapsedMs.value = state.visualElapsedMs;
|
||||
if (materials.flipPageSurface.userData.sourceRevealSide === side) {
|
||||
syncFlipRevealShaderFromSource(side, materials.flipPageSurface);
|
||||
}
|
||||
if (materials.flipPageBackSurface.userData.sourceRevealSide === side) {
|
||||
syncFlipRevealShaderFromSource(side, materials.flipPageBackSurface);
|
||||
}
|
||||
if (progress < 1) return;
|
||||
|
||||
clearPageReveal(side, 'duration-complete');
|
||||
@@ -2913,8 +2968,8 @@ function startFastPageFlipPrepared(direction, options = {}) {
|
||||
function createPageFlip(direction, startTime, duration) {
|
||||
const sourceSide = direction > 0 ? 1 : -1;
|
||||
const sourcePageSide = direction > 0 ? 'right' : 'left';
|
||||
const sourceLine = topVisibleLine(sourceSide);
|
||||
const destinationLine = topVisibleLine(-sourceSide);
|
||||
const sourceLine = normalizeFlipLineToVisiblePage(topVisibleLine(sourceSide), sourceSide);
|
||||
const destinationLine = normalizeFlipLineToVisiblePage(topVisibleLine(-sourceSide), -sourceSide);
|
||||
if (!sourceLine || !destinationLine) return null;
|
||||
return {
|
||||
direction,
|
||||
@@ -2930,6 +2985,29 @@ function createPageFlip(direction, startTime, duration) {
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeFlipLineToVisiblePage(line, side) {
|
||||
if (!line || !currentProceduralBookModel) return line;
|
||||
const points = Array.isArray(line.points) ? line.points : [];
|
||||
if (points.length < 2) return line;
|
||||
const pageStartX = side * Math.max(0, Number(currentProceduralBookModel.spineHalf || 0));
|
||||
const endpoint = points[points.length - 1];
|
||||
const sourceStart = points[0];
|
||||
const sourceSpan = Math.max(0.0001, side * (endpoint.x - sourceStart.x));
|
||||
const normalizedPoints = points.map((point) => {
|
||||
const u = THREE.MathUtils.clamp(side * (point.x - sourceStart.x) / sourceSpan, 0, 1);
|
||||
return {
|
||||
x: THREE.MathUtils.lerp(pageStartX, endpoint.x, u),
|
||||
y: point.y
|
||||
};
|
||||
});
|
||||
return {
|
||||
...line,
|
||||
anchor: normalizedPoints[0],
|
||||
points: normalizedPoints,
|
||||
endpoint: normalizedPoints[normalizedPoints.length - 1]
|
||||
};
|
||||
}
|
||||
|
||||
function prepareStaticPageForFlip(flip, prewarm = null) {
|
||||
if (!flip) return false;
|
||||
const sourceSide = flip.direction > 0 ? 'right' : 'left';
|
||||
@@ -2962,6 +3040,8 @@ function prepareStaticPageForFlip(flip, prewarm = null) {
|
||||
}
|
||||
materials.flipPageSurface.map = sourceTexture;
|
||||
materials.flipPageBackSurface.map = backTexture || getBlankPageTexture();
|
||||
materials.flipPageSurface.userData.sourceRevealSide = pageRevealState[sourceSide] ? sourceSide : null;
|
||||
materials.flipPageBackSurface.userData.sourceRevealSide = null;
|
||||
materials.flipPageSurface.normalMap = materials.pageSurface.normalMap;
|
||||
materials.flipPageBackSurface.normalMap = materials.pageSurface.normalMap;
|
||||
materials.flipPageSurface.roughnessMap = materials.pageSurface.roughnessMap;
|
||||
@@ -3006,15 +3086,17 @@ function prepareStaticPageForFlip(flip, prewarm = null) {
|
||||
...lastFlipTexturePreflight,
|
||||
usedResidentBackTexture: Boolean(backTexture && backTexture !== getBlankPageTexture())
|
||||
});
|
||||
syncFlipRevealShaderFromSource(sourceSide, materials.flipPageSurface);
|
||||
return true;
|
||||
}
|
||||
|
||||
function resolveCurrentFlipSourceTexture(side) {
|
||||
const pageMeta = currentPageMeta?.[side] || null;
|
||||
if (pageMeta?.kind === 'blank') return getBlankPageTexture();
|
||||
const material = side === 'left' ? materials.leftPage : materials.rightPage;
|
||||
if (pageRevealState[side]) return material?.map || null;
|
||||
const resident = pageTextureStore?.getResidentTextureForMeta?.(pageMeta);
|
||||
if (resident) return resident;
|
||||
const material = side === 'left' ? materials.leftPage : materials.rightPage;
|
||||
return material?.map || null;
|
||||
}
|
||||
|
||||
@@ -3366,8 +3448,10 @@ function createFlippingPageGeometry(surface, direction = 1) {
|
||||
}
|
||||
|
||||
function pageUvForSide(side, u, v) {
|
||||
const inset = THREE.MathUtils.clamp(Number(PROCEDURAL_BOOK.PAGE_TEXTURE_FORE_EDGE_INSET_RATIO || 0), 0, 0.35);
|
||||
const pageU = THREE.MathUtils.clamp(u / Math.max(0.0001, 1 - inset), 0, 1);
|
||||
return {
|
||||
x: side < 0 ? 1 - u : u,
|
||||
x: side < 0 ? 1 - pageU : pageU,
|
||||
y: v
|
||||
};
|
||||
}
|
||||
@@ -3462,6 +3546,8 @@ function removeFlipMesh(flip) {
|
||||
book.remove(flip.mesh);
|
||||
flip.mesh.geometry.dispose();
|
||||
flip.mesh = null;
|
||||
materials.flipPageSurface.userData.sourceRevealSide = null;
|
||||
materials.flipPageBackSurface.userData.sourceRevealSide = null;
|
||||
}
|
||||
|
||||
function easeInOutCubic(t) {
|
||||
|
||||
@@ -150,9 +150,9 @@ const checks = [
|
||||
['webgl texture store is non-optional with db memory cache prepared textures and vram cache', /maxCacheSizeBytes = 5 \* 1024 \* 1024 \* 1024/.test(webglPageCacheSource) && /maxMemoryCanvasCount = 256/.test(webglPageCacheSource) && /residentTextures = new Map/.test(webglPageCacheSource) && /preparedTextures = \{/.test(webglPageCacheSource) && /persistent page caching is in a problem state/.test(webglPageCacheSource) && !/if \(this\.memoryCanvasCache\.has\(key\)\) return true/.test(webglPageCacheSource)],
|
||||
['webgl lab prewarms navigation texture window through single store before flips', /const maxResidentPageTextures = 192/.test(source) && /configureTextureRuntime/.test(source) && /prewarmNavigationTextureWindow/.test(source) && /await prewarmFlipTextures\(direction, targetSpread\)/.test(source) && /resolveFlipBackTexture\(targetBackPageMeta, prewarmedBackTexture\)/.test(source) && !/const residentPageTextures = new Map/.test(source)],
|
||||
['webgl texture store records cache misses as problem states', /problemLog/.test(webglPageCacheSource) && /recordProblem/.test(webglPageCacheSource) && /db-cache-miss/.test(webglPageCacheSource) && /webglPageCacheProblems/.test(webglPageCacheSource)],
|
||||
['webgl lab makes preload-only page canvases resident by explicit page metadata through store', /pageTextureStore\?\.preparePageTexture/.test(source) && /attachRevealPageMeta/.test(source) && source.includes('pageTextureStore?.rememberResidentTexture?.(currentPageMeta.left, texture, detail.left, true)') && source.includes('pageTextureStore?.rememberResidentTexture?.(currentPageMeta.right, texture, detail.right, true)')],
|
||||
['webgl lab makes preload-only page canvases resident by explicit page metadata through store', /pageTextureStore\?\.preparePageTexture/.test(source) && /attachRevealPageMeta/.test(source) && source.includes('pageTextureStore?.rememberResidentTexture?.(effectivePageMeta.left, texture, detail.left, true)') && source.includes('pageTextureStore?.rememberResidentTexture?.(effectivePageMeta.right, texture, detail.right, true)')],
|
||||
['webgl texture store keeps current visible page textures resident without disposing shared maps', /rememberResidentTexture\(pageMeta = \{\}, texture = null, sourceCanvas = null, ownsTexture = true\)/.test(webglPageCacheSource) && /ownsTexture/.test(webglPageCacheSource) && /if \(oldest\?\.ownsTexture\) oldest\.texture\?\.dispose\?\.\(\)/.test(webglPageCacheSource)],
|
||||
['webgl lab reuses current-enough resident cached page textures via single store for direct stack switches', /uploadPageTextureDirect\(side, sourceCanvas, pageMeta = null\)/.test(source) && source.includes('pageTextureStore?.getResidentTextureForMeta?.(pageMeta)') && /usedResidentTexture/.test(source) && /uploadPageTextureDirect\('left', detail\.left, currentPageMeta\.left\)/.test(source) && /uploadPageTextureDirect\('right', detail\.right, currentPageMeta\.right\)/.test(source) && !/function getResidentPageTextureForMeta/.test(source)],
|
||||
['webgl 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, effectivePageMeta\.left\)/.test(source) && /uploadPageTextureDirect\('right', detail\.right, effectivePageMeta\.right\)/.test(source) && !/function getResidentPageTextureForMeta/.test(source)],
|
||||
['webgl page cache preserves explicit cache keys across writes and reads', /cacheKey: pageMeta\.cacheKey/.test(webglPageCacheSource) && /makePageKey\(pageMeta\)/.test(webglPageCacheSource)],
|
||||
['webgl page cache rejects older page versions for the same page key', /isOlderPageEntry/.test(webglPageCacheSource) && /contentVersion/.test(webglPageCacheSource) && /completenessScore/.test(webglPageCacheSource) && /if \(this\.isOlderPageEntry\(pageMeta, oldEntry\)\) return true/.test(webglPageCacheSource)],
|
||||
['targeted page flips commit target spread before emitting finished event', /bookPaginationState = \{[\s\S]*spreadIndex: Math\.max\(0, Math\.round\(Number\(flip\.targetSpread\)\)\)[\s\S]*document\.dispatchEvent\(new CustomEvent\('webgl-book:page-flip-finished'/.test(source) && /targetSpread: Number\.isFinite\(Number\(flip\.targetSpread\)\)/.test(source)],
|
||||
@@ -193,7 +193,9 @@ const checks = [
|
||||
['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) && /const topMaterialIndex = direction > 0 \? 1 : 0/.test(source) && /const bottomMaterialIndex = direction > 0 \? 0 : 1/.test(source) && /geometry\.addGroup\(0, topIndices\.length, topMaterialIndex\)/.test(source) && /geometry\.addGroup\(topIndices\.length, bottomIndices\.length, bottomMaterialIndex\)/.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 maps source and destination textures to direction-aware physical sides', /const topPageSide = direction > 0 \? targetSide : sourceSide/.test(source) && /const bottomPageSide = direction > 0 \? sourceSide : targetSide/.test(source) && /topRow\.push\(push\(point, pageThickness, pageUvForSide\(topPageSide, u, v\)\)\)/.test(source) && /bottomRow\.push\(push\(point, 0, pageUvForSide\(bottomPageSide, u, v\)\)\)/.test(source) && /side < 0 \? 1 - u : u/.test(source) && /y: v/.test(source)],
|
||||
['webgl animated page maps source and destination textures to direction-aware physical sides', /const topPageSide = direction > 0 \? targetSide : sourceSide/.test(source) && /const bottomPageSide = direction > 0 \? sourceSide : targetSide/.test(source) && /topRow\.push\(push\(point, pageThickness, pageUvForSide\(topPageSide, u, v\)\)\)/.test(source) && /bottomRow\.push\(push\(point, 0, pageUvForSide\(bottomPageSide, u, v\)\)\)/.test(source) && /side < 0 \? 1 - pageU : pageU/.test(source) && /y: v/.test(source)],
|
||||
['webgl animated page UVs use the same fore-edge inset as the visible stack cap', /PAGE_TEXTURE_FORE_EDGE_INSET_RATIO/.test(source) && /const pageU = THREE\.MathUtils\.clamp\(u \/ Math\.max\(0\.0001, 1 - inset\), 0, 1\)/.test(source)],
|
||||
['webgl flip geometry samples the same visible page plane as the static stack', /normalizeFlipLineToVisiblePage/.test(source) && /currentProceduralBookModel\.spineHalf/.test(source) && /const pageStartX = side \* Math\.max/.test(source) && /normalizeFlipLineToVisiblePage\(topVisibleLine\(sourceSide\), sourceSide\)/.test(source)],
|
||||
['webgl flip prewarm prepares current and target spread texture records before cache lookup', /prepareSpreadTextureRecordsForFlip\(currentSpread\)/.test(source) && /prepareSpreadTextureRecordsForFlip\(nextSpread\)/.test(source) && /function prepareSpreadTextureRecordsForFlip/.test(source) && /spreadTextureRecordsReady\(spread\)/.test(source) && /window\.BookTextureRenderer\.drawSpread\(spread, \['left', 'right'\], \{[\s\S]*phase: 'prepare'/.test(source)],
|
||||
['webgl animated page reuses geometry buffers during flips', /function updateFlippingPageGeometry/.test(source) && /position\.needsUpdate = true/.test(source) && /updateFlippingPageGeometry\(flip\.mesh\.geometry, surface\)/.test(source) && !/flip\.mesh\.geometry\.dispose\(\);\s*flip\.mesh\.geometry = geometry;/.test(methodBody(source, 'setActivePageGeometry'))],
|
||||
['webgl scene targets 60fps with browser-frame scheduling live mirror and static heavy refresh', /const targetFrameDurationMs = 1000 \/ 60/.test(source) && /this\.targetFrameDurationMs = 1000 \/ 60/.test(textureRendererSource) && /requestAnimationFrame\(animate\)/.test(source) && /const refreshStaticSceneBuffers = staticSceneBuffersDirty \|\| activeFlips\.length > 0/.test(source) && /updateTableReflection\(\);/.test(source) && /mirrorRefreshesEveryFrame: true/.test(source) && !/setTimeout\(animate/.test(source)],
|
||||
@@ -213,6 +215,10 @@ const checks = [
|
||||
['webgl line reveal timing uses TTS word spans instead of stretching split page fragments', /lineWordCount/.test(bookPaginationSource) && /blockWordStart/.test(textureRendererSource) && /blockWordCount/.test(textureRendererSource) && /getLineTimingFromWords/.test(textureRendererSource) && /const canUseLineWordSpans/.test(textureRendererSource)],
|
||||
['webgl flip completion defers book rebuild out of the final animation frame', /scheduledBookRebuildFrame/.test(source) && /function scheduleBookRebuild/.test(source) && /syncReadingProgressToCurrentPage\(\{[\s\S]*rebuild: 'defer'[\s\S]*reason: 'page-flip-finished'/.test(source)],
|
||||
['webgl ordinary flip near-end uses resident target textures instead of renderer redraw', /applyResidentSpreadTextures\(targetSpread, 'page-flip-near-end'\)/.test(source) && /function applyResidentSpreadTextures/.test(source) && /residentSpreadTextures:applied/.test(source) && /spreadUpdate:skip-during-flip/.test(textureRendererSource)],
|
||||
['webgl autoplay flip source prefers currently revealing visible material over resident cache', /if \(pageRevealState\[side\]\) return material\?\.map \|\| null/.test(source) && /materials\.flipPageSurface\.userData\.sourceRevealSide = pageRevealState\[sourceSide\] \? sourceSide : null/.test(source)],
|
||||
['webgl flipping page materials mirror active reveal shader uniforms', /materials\.flipPageSurface\.userData\.bookPageReveal/.test(source) && /syncFlipRevealShaderFromSource/.test(source) && /bookRevealRegionRects/.test(source) && /materials\.flipPageSurface\.userData\.sourceRevealSide === side/.test(source)],
|
||||
['webgl prepared texture records do not mutate the visible page metadata', /const incomingPageMeta = detail\.pageMeta/.test(source) && /if \(detail\.phase !== 'prepare' && detail\.pageMeta\) \{[\s\S]*currentPageMeta = incomingPageMeta/.test(source) && /pageMeta: effectivePageMeta/.test(source)],
|
||||
['texture renderer marks committed reveal blocks complete so pauses cannot replay them', /webgl-book:reveal-committed/.test(textureRendererSource) && /completeRevealBlockIds/.test(textureRendererSource) && /this\.pendingRevealBlockIds\.delete\(id\)/.test(textureRendererSource)],
|
||||
['webgl timeline recalculates placeholder zero-duration reveal timings from TTS duration', /existingTimings/.test(bookPlaybackTimelineSource) && /existingDuration/.test(bookPlaybackTimelineSource) && /ttsDuration/.test(bookPlaybackTimelineSource) && /existingTimings\.length > 0 && \(existingDuration > 0 \|\| ttsDuration <= 0\)/.test(bookPlaybackTimelineSource)],
|
||||
['webgl playback coordinator rejects placeholder zero-duration reveal timings', /timingDuration/.test(playbackCoordinatorSource) && /ttsDuration/.test(playbackCoordinatorSource) && /timingDuration <= 0 && ttsDuration > 0/.test(playbackCoordinatorSource) && /sentence\.animation = \{[\s\S]*wordTimings,[\s\S]*totalDuration: calculated\.totalDuration/.test(playbackCoordinatorSource)],
|
||||
['texture renderer derives reveal regions from the just-drawn preview spread before committed pagination', /if \(this\.currentSpread\) sourceSpreads\.push\(this\.currentSpread\)/.test(textureRendererSource) && /this\.pagination\.spreads\.forEach/.test(textureRendererSource) && /Number\(spread\.index\) === Number\(this\.currentSpread\.index\)/.test(textureRendererSource)],
|
||||
@@ -229,6 +235,7 @@ const checks = [
|
||||
['webgl lab delegates right-page reveal commits to timeline owner', /BookPlaybackTimeline\?\.ownsPageFlipCommit === true/.test(source) && /handleRevealCommittedForPageFlip/.test(source)],
|
||||
['webgl reveal clock explicitly freezes during physical flips', /pageRevealFreezeAt/.test(source) && /state\.startedAt \+= frozenMs/.test(source) && /activeRevealBlockStarts\.set\(blockId, Number\(value\) \+ frozenMs\)/.test(source)],
|
||||
['book playback timeline waits for right reveal only when current block is on right page', /getBlockRevealSides/.test(bookPlaybackTimelineSource) && /revealSides\.includes\('right'\) && this\.requiresRightPageFlipAfterReveal/.test(bookPlaybackTimelineSource) && /visual-completion:no-right-flip-wait/.test(bookPlaybackTimelineSource)],
|
||||
['book playback timeline flips at planned right-page fragment time instead of full TTS completion', /waitForPlannedRightReveal/.test(bookPlaybackTimelineSource) && /getRightRevealDurationMs/.test(bookPlaybackTimelineSource) && /segment\.revealStartedPromise/.test(bookPlaybackTimelineSource) && /Promise\.race\(\[[\s\S]*this\.waitForRevealCommit\(segment\)/.test(bookPlaybackTimelineSource)],
|
||||
['book playback timeline exposes reveal lifecycle benchmark entries', /benchmarkEntries/.test(bookPlaybackTimelineSource) && /markBenchmark\('reveal-start'/.test(bookPlaybackTimelineSource) && /markBenchmark\('reveal-committed'/.test(bookPlaybackTimelineSource) && /webglBookBenchmark/.test(bookPlaybackTimelineSource)],
|
||||
['webgl scene records reveal start and slow-frame benchmark diagnostics', /revealState:created/.test(source) && /revealStart:applied/.test(source) && /slowFrameLog/.test(source) && /getBenchmarkState/.test(source) && /webglSlowFrames/.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)],
|
||||
|
||||
Reference in New Issue
Block a user