Fix WebGL reveal timing and flip texture readiness
This commit is contained in:
@@ -353,6 +353,7 @@ class BookPaginationModule extends BaseModule {
|
|||||||
lineHeightPx: layout.lineHeightPx,
|
lineHeightPx: layout.lineHeightPx,
|
||||||
fontStyle: layout.fontStyle,
|
fontStyle: layout.fontStyle,
|
||||||
blockWordStart: blockWordCursor,
|
blockWordStart: blockWordCursor,
|
||||||
|
lineWordCount,
|
||||||
dropCapText: layoutLineIndex === 0 ? layout.dropCapText : '',
|
dropCapText: layoutLineIndex === 0 ? layout.dropCapText : '',
|
||||||
smallCaps: Boolean(layout.dropCap && layoutLineIndex === 0)
|
smallCaps: Boolean(layout.dropCap && layoutLineIndex === 0)
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -208,9 +208,17 @@ class BookPlaybackTimelineModule extends BaseModule {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ensureAnimationTimings(sentence = {}) {
|
ensureAnimationTimings(sentence = {}) {
|
||||||
if (Array.isArray(sentence.animation?.wordTimings) && sentence.animation.wordTimings.length > 0) return;
|
const existingTimings = Array.isArray(sentence.animation?.wordTimings)
|
||||||
|
? sentence.animation.wordTimings
|
||||||
|
: [];
|
||||||
|
const existingDuration = existingTimings.reduce((max, timing) => Math.max(
|
||||||
|
max,
|
||||||
|
Number(timing?.delay || 0) + Number(timing?.duration || 0)
|
||||||
|
), Number(sentence.animation?.totalDuration || 0));
|
||||||
|
const ttsDuration = Number(sentence.tts?.duration || 0);
|
||||||
|
if (existingTimings.length > 0 && (existingDuration > 0 || ttsDuration <= 0)) return;
|
||||||
const words = String(sentence.layoutText || sentence.text || '').match(/\S+/g) || [];
|
const words = String(sentence.layoutText || sentence.text || '').match(/\S+/g) || [];
|
||||||
sentence.animation = this.calculateAnimationTiming(words, sentence.tts?.duration || 0, sentence.cueMarkers || []);
|
sentence.animation = this.calculateAnimationTiming(words, ttsDuration, sentence.cueMarkers || []);
|
||||||
}
|
}
|
||||||
|
|
||||||
calculateAnimationTiming(words = [], totalDuration = 0, cueMarkers = []) {
|
calculateAnimationTiming(words = [], totalDuration = 0, cueMarkers = []) {
|
||||||
|
|||||||
@@ -119,6 +119,13 @@ class BookTextureRendererModule extends BaseModule {
|
|||||||
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';
|
const visibility = event.detail?.visibility || 'current';
|
||||||
this.currentSpread = spread || { left: [], right: [] };
|
this.currentSpread = spread || { left: [], right: [] };
|
||||||
|
if (document.documentElement.dataset.webglPageFlipActive === 'true' && this.activeAnimations.size === 0) {
|
||||||
|
this.markPipelineTiming('spreadUpdate:skip-during-flip', {
|
||||||
|
spreadIndex,
|
||||||
|
visibility
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (latestBlockId && Number(latestBlockId) > latestRenderedBlockId) {
|
if (latestBlockId && Number(latestBlockId) > latestRenderedBlockId) {
|
||||||
this.markPendingReveal(latestBlockId);
|
this.markPendingReveal(latestBlockId);
|
||||||
const id = String(latestBlockId);
|
const id = String(latestBlockId);
|
||||||
@@ -674,9 +681,16 @@ class BookTextureRendererModule extends BaseModule {
|
|||||||
|
|
||||||
collectRevealRegionCandidates() {
|
collectRevealRegionCandidates() {
|
||||||
const candidates = [];
|
const candidates = [];
|
||||||
const sourceSpreads = Array.isArray(this.pagination?.spreads) && this.pagination.spreads.length
|
const sourceSpreads = [];
|
||||||
? this.pagination.spreads
|
if (this.currentSpread) sourceSpreads.push(this.currentSpread);
|
||||||
: [this.currentSpread || { index: 0, left: [], right: [] }];
|
if (Array.isArray(this.pagination?.spreads)) {
|
||||||
|
this.pagination.spreads.forEach((spread) => {
|
||||||
|
if (!spread) return;
|
||||||
|
if (this.currentSpread && Number(spread.index) === Number(this.currentSpread.index)) return;
|
||||||
|
sourceSpreads.push(spread);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (!sourceSpreads.length) sourceSpreads.push({ index: 0, left: [], right: [] });
|
||||||
sourceSpreads.forEach((spread) => {
|
sourceSpreads.forEach((spread) => {
|
||||||
['left', 'right'].forEach((side) => {
|
['left', 'right'].forEach((side) => {
|
||||||
const spreadLines = Array.isArray(spread?.[side]) ? spread[side] : [];
|
const spreadLines = Array.isArray(spread?.[side]) ? spread[side] : [];
|
||||||
@@ -707,7 +721,20 @@ class BookTextureRendererModule extends BaseModule {
|
|||||||
const fixedRegions = sortedRegions.filter(region => region.fixedDurationMs > 0);
|
const fixedRegions = sortedRegions.filter(region => region.fixedDurationMs > 0);
|
||||||
let fallbackDelay = 0;
|
let fallbackDelay = 0;
|
||||||
const totalArea = textRegions.reduce((sum, region) => sum + Math.max(1, region.area), 0);
|
const totalArea = textRegions.reduce((sum, region) => sum + Math.max(1, region.area), 0);
|
||||||
|
const wordTimings = Array.isArray(animation.wordTimings) ? animation.wordTimings : [];
|
||||||
|
const canUseLineWordSpans = wordTimings.length > 0
|
||||||
|
&& textRegions.every(region => Number.isFinite(Number(region.blockWordStart)) && Number(region.blockWordCount) > 0);
|
||||||
|
|
||||||
|
if (canUseLineWordSpans) {
|
||||||
|
textRegions.forEach((region) => {
|
||||||
|
const timing = this.getLineTimingFromWords(region, wordTimings);
|
||||||
|
timedRegions.push({
|
||||||
|
...region,
|
||||||
|
timing
|
||||||
|
});
|
||||||
|
fallbackDelay = Math.max(fallbackDelay, timing.delay + timing.duration);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
textRegions.forEach((region) => {
|
textRegions.forEach((region) => {
|
||||||
const duration = totalArea > 0
|
const duration = totalArea > 0
|
||||||
? Math.max(1, totalDuration * (Math.max(1, region.area) / totalArea))
|
? Math.max(1, totalDuration * (Math.max(1, region.area) / totalArea))
|
||||||
@@ -718,6 +745,7 @@ class BookTextureRendererModule extends BaseModule {
|
|||||||
});
|
});
|
||||||
fallbackDelay += duration;
|
fallbackDelay += duration;
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
fixedRegions.forEach((region) => {
|
fixedRegions.forEach((region) => {
|
||||||
timedRegions.push({
|
timedRegions.push({
|
||||||
@@ -738,6 +766,23 @@ class BookTextureRendererModule extends BaseModule {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getLineTimingFromWords(region = {}, wordTimings = []) {
|
||||||
|
const start = Math.max(0, Math.floor(Number(region.blockWordStart || 0)));
|
||||||
|
const count = Math.max(1, Math.floor(Number(region.blockWordCount || 1)));
|
||||||
|
const first = wordTimings[Math.min(start, wordTimings.length - 1)] || { delay: 0, duration: 1 };
|
||||||
|
const lastIndex = Math.min(wordTimings.length - 1, start + count - 1);
|
||||||
|
const last = wordTimings[lastIndex] || first;
|
||||||
|
const delay = Math.max(0, Number(first.delay || 0));
|
||||||
|
const end = Math.max(
|
||||||
|
delay + 1,
|
||||||
|
Number(last.delay || 0) + Math.max(1, Number(last.duration || 1))
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
delay,
|
||||||
|
duration: Math.max(1, end - delay)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
createRevealRegionForLine(side, lineRecord = {}, spreadIndex = null) {
|
createRevealRegionForLine(side, lineRecord = {}, spreadIndex = null) {
|
||||||
const blockId = String(lineRecord?.blockId ?? '');
|
const blockId = String(lineRecord?.blockId ?? '');
|
||||||
if (!blockId || !this.revealPublishBlockIds.has(blockId)) return null;
|
if (!blockId || !this.revealPublishBlockIds.has(blockId)) return null;
|
||||||
@@ -770,6 +815,8 @@ class BookTextureRendererModule extends BaseModule {
|
|||||||
spreadIndex: Math.max(0, Number((spreadIndex ?? Math.floor(Number(lineRecord.pageIndex || 0) / 2)) || 0)),
|
spreadIndex: Math.max(0, Number((spreadIndex ?? Math.floor(Number(lineRecord.pageIndex || 0) / 2)) || 0)),
|
||||||
blockId,
|
blockId,
|
||||||
lineIndex: Number(lineRecord.lineIndex ?? lineRecord.pageLine ?? 0),
|
lineIndex: Number(lineRecord.lineIndex ?? lineRecord.pageLine ?? 0),
|
||||||
|
blockWordStart: Number(lineRecord.blockWordStart ?? 0),
|
||||||
|
blockWordCount: Number(lineRecord.lineWordCount ?? 0),
|
||||||
fixedDurationMs,
|
fixedDurationMs,
|
||||||
area: rectWidth * rectHeight,
|
area: rectWidth * rectHeight,
|
||||||
pixelRect: { x: left, y: top, right, bottom },
|
pixelRect: { x: left, y: top, right, bottom },
|
||||||
|
|||||||
+1
-1
@@ -24,7 +24,7 @@ const ModuleState = {
|
|||||||
ERROR: 'ERROR'
|
ERROR: 'ERROR'
|
||||||
};
|
};
|
||||||
|
|
||||||
const MODULE_CACHE_BUSTER = '20260610-book-timeline-d';
|
const MODULE_CACHE_BUSTER = '20260610-book-timeline-j';
|
||||||
window.MODULE_CACHE_BUSTER = MODULE_CACHE_BUSTER;
|
window.MODULE_CACHE_BUSTER = MODULE_CACHE_BUSTER;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -315,11 +315,22 @@ class PlaybackCoordinatorModule extends BaseModule {
|
|||||||
let cueTimings = Array.isArray(sentence.animation?.cueTimings)
|
let cueTimings = Array.isArray(sentence.animation?.cueTimings)
|
||||||
? sentence.animation.cueTimings
|
? sentence.animation.cueTimings
|
||||||
: [];
|
: [];
|
||||||
if (wordTimings.length === 0) {
|
const timingDuration = wordTimings.reduce((max, timing) => Math.max(
|
||||||
|
max,
|
||||||
|
Number(timing?.delay || 0) + Number(timing?.duration || 0)
|
||||||
|
), Number(sentence.animation?.totalDuration || 0));
|
||||||
|
const ttsDuration = Number(sentence.tts?.duration || sentence.animation?.totalDuration || 0);
|
||||||
|
if (wordTimings.length === 0 || (timingDuration <= 0 && ttsDuration > 0)) {
|
||||||
const words = String(sentence.text || '').match(/\S+/g) || [];
|
const words = String(sentence.text || '').match(/\S+/g) || [];
|
||||||
const calculated = this.calculateWordTimings(words, sentence.tts?.duration || sentence.animation?.totalDuration || 0);
|
const calculated = this.calculateWordTimings(words, ttsDuration);
|
||||||
wordTimings = calculated.wordTimings;
|
wordTimings = calculated.wordTimings;
|
||||||
cueTimings = [];
|
cueTimings = [];
|
||||||
|
sentence.animation = {
|
||||||
|
...(sentence.animation || {}),
|
||||||
|
wordTimings,
|
||||||
|
cueTimings,
|
||||||
|
totalDuration: calculated.totalDuration
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof sentence.webglRevealController !== 'function') {
|
if (typeof sentence.webglRevealController !== 'function') {
|
||||||
|
|||||||
+117
-17
@@ -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 { 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 { 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 { 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-d';
|
import { PROCEDURAL_BOOK, createProceduralBookModel, snapProceduralPageCount } from './procedural-book-model.js?v=20260610-book-timeline-j';
|
||||||
|
|
||||||
const canvas = document.getElementById('scene');
|
const canvas = document.getElementById('scene');
|
||||||
canvas.style.cursor = 'grab';
|
canvas.style.cursor = 'grab';
|
||||||
@@ -278,6 +278,7 @@ const pageRevealState = {
|
|||||||
};
|
};
|
||||||
let pageRevealFreezeAt = null;
|
let pageRevealFreezeAt = null;
|
||||||
const pageRevealClearLog = [];
|
const pageRevealClearLog = [];
|
||||||
|
let scheduledBookRebuildFrame = null;
|
||||||
await reportLabStep(52, 'Generating leather texture set');
|
await reportLabStep(52, 'Generating leather texture set');
|
||||||
const leatherTextures = createLeatherTextures();
|
const leatherTextures = createLeatherTextures();
|
||||||
await reportLabStep(56, 'Generating spine cloth texture set');
|
await reportLabStep(56, 'Generating spine cloth texture set');
|
||||||
@@ -1842,13 +1843,34 @@ function getCurrentPagePosition() {
|
|||||||
return spreadIndexToPagePosition(bookPaginationState.spreadIndex);
|
return spreadIndexToPagePosition(bookPaginationState.spreadIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
function syncReadingProgressToCurrentPage() {
|
function scheduleBookRebuild(reason = 'scheduled') {
|
||||||
const nextProgress = THREE.MathUtils.clamp(getCurrentPagePosition() / Math.max(1, bookPageCount), 0, 1);
|
if (scheduledBookRebuildFrame !== null) return;
|
||||||
if (Math.abs(nextProgress - readingProgress) < 0.0001) return;
|
const scheduler = typeof window.requestIdleCallback === 'function'
|
||||||
readingProgress = nextProgress;
|
? (callback) => window.requestIdleCallback(callback, { timeout: 180 })
|
||||||
|
: requestAnimationFrame;
|
||||||
|
scheduledBookRebuildFrame = scheduler(() => {
|
||||||
|
scheduledBookRebuildFrame = null;
|
||||||
|
markPageTextureTiming('bookRebuild:deferred', { reason });
|
||||||
buildBook();
|
buildBook();
|
||||||
|
syncBookControls();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncReadingProgressToCurrentPage(options = {}) {
|
||||||
|
const nextProgress = THREE.MathUtils.clamp(getCurrentPagePosition() / Math.max(1, bookPageCount), 0, 1);
|
||||||
|
const changed = Math.abs(nextProgress - readingProgress) >= 0.0001;
|
||||||
|
if (changed) {
|
||||||
|
readingProgress = nextProgress;
|
||||||
window.WebGLBookPreferenceBridge?.updateProgress?.(readingProgress);
|
window.WebGLBookPreferenceBridge?.updateProgress?.(readingProgress);
|
||||||
}
|
}
|
||||||
|
if (!changed && options.rebuild !== 'defer') return;
|
||||||
|
if (options.rebuild === 'defer') {
|
||||||
|
scheduleBookRebuild(options.reason || 'reading-progress-sync');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (options.rebuild === false) return;
|
||||||
|
buildBook();
|
||||||
|
}
|
||||||
|
|
||||||
function growBookIfWritableLimitReached() {
|
function growBookIfWritableLimitReached() {
|
||||||
const writtenLimit = Math.max(0, bookPaginationState.writtenPageLimit || 0);
|
const writtenLimit = Math.max(0, bookPaginationState.writtenPageLimit || 0);
|
||||||
@@ -2266,15 +2288,20 @@ 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);
|
||||||
const side = index % 2 === 0 ? 'left' : 'right';
|
const side = index % 2 === 0 ? 'left' : 'right';
|
||||||
const pagination = window.moduleRegistry?.getModule?.('book-pagination') || null;
|
const spread = getPaginationSpread(spreadIndex);
|
||||||
const spread = typeof pagination?.getSpread === 'function'
|
|
||||||
? pagination.getSpread(spreadIndex)
|
|
||||||
: Array.isArray(pagination?.spreads)
|
|
||||||
? pagination.spreads[spreadIndex]
|
|
||||||
: null;
|
|
||||||
return spread?.pageMeta?.[side] || null;
|
return spread?.pageMeta?.[side] || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getPaginationSpread(spreadIndex) {
|
||||||
|
const index = Math.max(0, Math.round(Number(spreadIndex || 0)));
|
||||||
|
const pagination = window.moduleRegistry?.getModule?.('book-pagination') || null;
|
||||||
|
return typeof pagination?.getSpread === 'function'
|
||||||
|
? pagination.getSpread(index)
|
||||||
|
: Array.isArray(pagination?.spreads)
|
||||||
|
? pagination.spreads[index]
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
async function prewarmSpreadTextures(spreadIndex) {
|
async function prewarmSpreadTextures(spreadIndex) {
|
||||||
return pageTextureStore?.prewarmSpreadTextures?.(spreadIndex, makePageMetaForCache) || {
|
return pageTextureStore?.prewarmSpreadTextures?.(spreadIndex, makePageMetaForCache) || {
|
||||||
spreadIndex: Math.max(0, Math.round(Number(spreadIndex || 0))),
|
spreadIndex: Math.max(0, Math.round(Number(spreadIndex || 0))),
|
||||||
@@ -2314,6 +2341,8 @@ async function prewarmFlipTextures(direction, targetSpread = null) {
|
|||||||
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)));
|
||||||
|
prepareSpreadTextureRecordsForFlip(currentSpread);
|
||||||
|
prepareSpreadTextureRecordsForFlip(nextSpread);
|
||||||
const windowMap = await prewarmNavigationTextureWindow('flip-prewarm', { targetSpread: nextSpread });
|
const windowMap = await prewarmNavigationTextureWindow('flip-prewarm', { targetSpread: nextSpread });
|
||||||
const current = windowMap?.[currentSpread] || await prewarmSpreadTextures(currentSpread);
|
const current = windowMap?.[currentSpread] || await prewarmSpreadTextures(currentSpread);
|
||||||
const next = windowMap?.[nextSpread] || await prewarmSpreadTextures(nextSpread);
|
const next = windowMap?.[nextSpread] || await prewarmSpreadTextures(nextSpread);
|
||||||
@@ -2323,6 +2352,25 @@ async function prewarmFlipTextures(direction, targetSpread = null) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function prepareSpreadTextureRecordsForFlip(spreadIndex) {
|
||||||
|
const spread = getPaginationSpread(spreadIndex);
|
||||||
|
if (!spread || typeof window.BookTextureRenderer?.drawSpread !== 'function') return false;
|
||||||
|
if (spreadTextureRecordsReady(spread)) return true;
|
||||||
|
window.BookTextureRenderer.drawSpread(spread, ['left', 'right'], {
|
||||||
|
phase: 'prepare'
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function spreadTextureRecordsReady(spread = null) {
|
||||||
|
if (!spread?.pageMeta || !pageTextureStore) return false;
|
||||||
|
return ['left', 'right'].every((side) => {
|
||||||
|
const meta = spread.pageMeta?.[side] || null;
|
||||||
|
if (!meta || meta.kind === 'blank') return true;
|
||||||
|
return Boolean(pageTextureStore.getResidentTextureForMeta?.(meta));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function takePreparedPageTexture(side, revealDetail = {}) {
|
function takePreparedPageTexture(side, revealDetail = {}) {
|
||||||
const key = getRevealCacheKey(revealDetail);
|
const key = getRevealCacheKey(revealDetail);
|
||||||
const prepared = pageTextureStore?.takePreparedPageTexture?.(side, key) || null;
|
const prepared = pageTextureStore?.takePreparedPageTexture?.(side, key) || null;
|
||||||
@@ -3047,6 +3095,9 @@ function updateActiveFlips(now) {
|
|||||||
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)))
|
||||||
: null;
|
: null;
|
||||||
|
if (targetSpread !== null && !hasActivePageReveal()) {
|
||||||
|
applyResidentSpreadTextures(targetSpread, 'page-flip-near-end');
|
||||||
|
}
|
||||||
document.dispatchEvent(new CustomEvent('webgl-book:page-flip-near-end', {
|
document.dispatchEvent(new CustomEvent('webgl-book:page-flip-near-end', {
|
||||||
detail: {
|
detail: {
|
||||||
direction: flip.direction,
|
direction: flip.direction,
|
||||||
@@ -3060,6 +3111,48 @@ function updateActiveFlips(now) {
|
|||||||
completed.forEach((flip) => finishActiveFlip(flip));
|
completed.forEach((flip) => finishActiveFlip(flip));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function hasActivePageReveal() {
|
||||||
|
return ['left', 'right'].some((side) => {
|
||||||
|
const state = pageRevealState[side];
|
||||||
|
if (!state) return false;
|
||||||
|
if (state.startedAt != null) return true;
|
||||||
|
return Array.isArray(state.blockIds) && state.blockIds.length > 0;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyResidentSpreadTextures(spreadIndex, reason = 'resident-spread') {
|
||||||
|
const pageIndices = spreadPageIndices(spreadIndex);
|
||||||
|
['left', 'right'].forEach((side) => {
|
||||||
|
const pageIndex = pageIndices[side];
|
||||||
|
const pageMeta = getPaginationPageMeta(pageIndex) || makeBlankPageMeta(pageIndex);
|
||||||
|
const texture = pageMeta.kind === 'blank'
|
||||||
|
? getBlankPageTexture()
|
||||||
|
: pageTextureStore?.getResidentTextureForMeta?.(pageMeta);
|
||||||
|
if (!texture) {
|
||||||
|
pageTextureStore?.recordProblem?.({
|
||||||
|
type: 'resident-spread-texture-missing',
|
||||||
|
reason,
|
||||||
|
side,
|
||||||
|
spreadIndex,
|
||||||
|
pageIndex,
|
||||||
|
pageKind: pageMeta.kind
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const material = side === 'left' ? materials.leftPage : materials.rightPage;
|
||||||
|
clearPageReveal(side, reason);
|
||||||
|
if (material.map !== texture) {
|
||||||
|
material.map = texture;
|
||||||
|
material.needsUpdate = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
markStaticSceneBuffersDirty();
|
||||||
|
markPageTextureTiming('residentSpreadTextures:applied', {
|
||||||
|
reason,
|
||||||
|
spreadIndex
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function buildFlippingPageSurface(sourceLine, destinationLine, direction, t, pageOffset = 0) {
|
function buildFlippingPageSurface(sourceLine, destinationLine, direction, t, pageOffset = 0) {
|
||||||
const widthSegments = sourceLine.points.length - 1;
|
const widthSegments = sourceLine.points.length - 1;
|
||||||
const depthSegments = 18;
|
const depthSegments = 18;
|
||||||
@@ -3208,6 +3301,10 @@ function createFlippingPageGeometry(surface, direction = 1) {
|
|||||||
const depthSegments = surface[0].length - 1;
|
const depthSegments = surface[0].length - 1;
|
||||||
const sourceSide = direction > 0 ? 1 : -1;
|
const sourceSide = direction > 0 ? 1 : -1;
|
||||||
const targetSide = -sourceSide;
|
const targetSide = -sourceSide;
|
||||||
|
const topPageSide = direction > 0 ? targetSide : sourceSide;
|
||||||
|
const bottomPageSide = direction > 0 ? sourceSide : targetSide;
|
||||||
|
const topMaterialIndex = direction > 0 ? 1 : 0;
|
||||||
|
const bottomMaterialIndex = direction > 0 ? 0 : 1;
|
||||||
const push = (point, yOffset, uv) => {
|
const push = (point, yOffset, uv) => {
|
||||||
const index = positions.length / 3;
|
const index = positions.length / 3;
|
||||||
positions.push(point.x, point.y + yOffset, point.z);
|
positions.push(point.x, point.y + yOffset, point.z);
|
||||||
@@ -3221,8 +3318,8 @@ function createFlippingPageGeometry(surface, direction = 1) {
|
|||||||
const u = widthSegments <= 0 ? 0 : widthIndex / widthSegments;
|
const u = widthSegments <= 0 ? 0 : widthIndex / widthSegments;
|
||||||
rowPoints.forEach((point, depthIndex) => {
|
rowPoints.forEach((point, depthIndex) => {
|
||||||
const v = depthSegments <= 0 ? 0 : depthIndex / depthSegments;
|
const v = depthSegments <= 0 ? 0 : depthIndex / depthSegments;
|
||||||
topRow.push(push(point, pageThickness, pageUvForSide(sourceSide, u, v)));
|
topRow.push(push(point, pageThickness, pageUvForSide(topPageSide, u, v)));
|
||||||
bottomRow.push(push(point, 0, pageUvForSide(targetSide, u, v)));
|
bottomRow.push(push(point, 0, pageUvForSide(bottomPageSide, u, v)));
|
||||||
});
|
});
|
||||||
topGrid.push(topRow);
|
topGrid.push(topRow);
|
||||||
bottomGrid.push(bottomRow);
|
bottomGrid.push(bottomRow);
|
||||||
@@ -3257,8 +3354,8 @@ function createFlippingPageGeometry(surface, direction = 1) {
|
|||||||
geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
|
geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
|
||||||
geometry.setAttribute('uv', new THREE.Float32BufferAttribute(uvs, 2));
|
geometry.setAttribute('uv', new THREE.Float32BufferAttribute(uvs, 2));
|
||||||
geometry.clearGroups();
|
geometry.clearGroups();
|
||||||
geometry.addGroup(0, topIndices.length, 0);
|
geometry.addGroup(0, topIndices.length, topMaterialIndex);
|
||||||
geometry.addGroup(topIndices.length, bottomIndices.length, 1);
|
geometry.addGroup(topIndices.length, bottomIndices.length, bottomMaterialIndex);
|
||||||
geometry.addGroup(topIndices.length + bottomIndices.length, wallIndices.length, 2);
|
geometry.addGroup(topIndices.length + bottomIndices.length, wallIndices.length, 2);
|
||||||
geometry.computeVertexNormals();
|
geometry.computeVertexNormals();
|
||||||
return geometry;
|
return geometry;
|
||||||
@@ -3271,7 +3368,7 @@ function createFlippingPageGeometry(surface, direction = 1) {
|
|||||||
function pageUvForSide(side, u, v) {
|
function pageUvForSide(side, u, v) {
|
||||||
return {
|
return {
|
||||||
x: side < 0 ? 1 - u : u,
|
x: side < 0 ? 1 - u : u,
|
||||||
y: 1 - v
|
y: v
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3313,7 +3410,10 @@ function finishActiveFlip(flip) {
|
|||||||
spreadIndex: Math.max(0, Math.round(Number(flip.targetSpread)))
|
spreadIndex: Math.max(0, Math.round(Number(flip.targetSpread)))
|
||||||
};
|
};
|
||||||
maxVisitedPagePosition = Math.max(maxVisitedPagePosition, getCurrentPagePosition());
|
maxVisitedPagePosition = Math.max(maxVisitedPagePosition, getCurrentPagePosition());
|
||||||
syncReadingProgressToCurrentPage();
|
syncReadingProgressToCurrentPage({
|
||||||
|
rebuild: 'defer',
|
||||||
|
reason: 'page-flip-finished'
|
||||||
|
});
|
||||||
}
|
}
|
||||||
document.dispatchEvent(new CustomEvent('webgl-book:page-flip-finished', {
|
document.dispatchEvent(new CustomEvent('webgl-book:page-flip-finished', {
|
||||||
detail: {
|
detail: {
|
||||||
|
|||||||
@@ -190,10 +190,11 @@ const checks = [
|
|||||||
['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 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)],
|
['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 texture store resident cache reuses newest page version for older readiness requests', /isOlderPageMeta/.test(webglPageCacheSource) && /getResidentTextureForMeta/.test(webglPageCacheSource) && /if \(!resident\) return null/.test(webglPageCacheSource) && !/if \(!resident \|\| this\.isOlderPageMeta\(pageMeta, resident\.pageMeta\)\) return null/.test(webglPageCacheSource)],
|
['webgl texture store resident cache reuses newest page version for older readiness requests', /isOlderPageMeta/.test(webglPageCacheSource) && /getResidentTextureForMeta/.test(webglPageCacheSource) && /if \(!resident\) return null/.test(webglPageCacheSource) && !/if \(!resident \|\| this\.isOlderPageMeta\(pageMeta, resident\.pageMeta\)\) return null/.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) && /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 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 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 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 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 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)],
|
||||||
['webgl scene lowers mirror target and caps table film maps to 2k', /const reflectionPixelRatio = 1/.test(source) && /const tableReflectionBaseWidth = 2048/.test(source) && /const tableReflectionBaseHeight = 1152/.test(source) && /tableDustTexture = loadUtilityTexture\('\/assets\/webgl\/table_dust_4k\.png', \{ maxSize: 2048 \}\)/.test(source) && /tableGreaseTexture = loadUtilityTexture\('\/assets\/webgl\/table_grease_4k\.png', \{ maxSize: 2048 \}\)/.test(source)],
|
['webgl scene lowers mirror target and caps table film maps to 2k', /const reflectionPixelRatio = 1/.test(source) && /const tableReflectionBaseWidth = 2048/.test(source) && /const tableReflectionBaseHeight = 1152/.test(source) && /tableDustTexture = loadUtilityTexture\('\/assets\/webgl\/table_dust_4k\.png', \{ maxSize: 2048 \}\)/.test(source) && /tableGreaseTexture = loadUtilityTexture\('\/assets\/webgl\/table_grease_4k\.png', \{ maxSize: 2048 \}\)/.test(source)],
|
||||||
@@ -209,6 +210,12 @@ const checks = [
|
|||||||
['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 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 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)],
|
||||||
|
['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 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)],
|
||||||
['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)],
|
['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 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)],
|
['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', /createRevealDetail\(sentence, previewSpread, 'prepare'\)/.test(bookPlaybackTimelineSource) && /this\.textureRenderer\.prepareRevealBlock\(revealDetail, \{[\s\S]*phase: 'prepare'[\s\S]*publishEvent: false/.test(bookPlaybackTimelineSource) && /this\.prewarmSegmentTextures\(segment\)/.test(bookPlaybackTimelineSource) && /this\.assertSegmentReady\(segment, 'prepare'\)/.test(bookPlaybackTimelineSource) && /reason: 'timeline-preplay-spread-transition'/.test(bookPlaybackTimelineSource)],
|
['3D overflow reveal preloads target spread before forced page flip', /createRevealDetail\(sentence, previewSpread, 'prepare'\)/.test(bookPlaybackTimelineSource) && /this\.textureRenderer\.prepareRevealBlock\(revealDetail, \{[\s\S]*phase: 'prepare'[\s\S]*publishEvent: false/.test(bookPlaybackTimelineSource) && /this\.prewarmSegmentTextures\(segment\)/.test(bookPlaybackTimelineSource) && /this\.assertSegmentReady\(segment, 'prepare'\)/.test(bookPlaybackTimelineSource) && /reason: 'timeline-preplay-spread-transition'/.test(bookPlaybackTimelineSource)],
|
||||||
|
|||||||
Reference in New Issue
Block a user