Checkpoint before line-grid scrolling refactor

This commit is contained in:
2026-05-16 13:44:03 +02:00
parent 42582352d6
commit fe33e4f0ab
25 changed files with 1989 additions and 840 deletions
+34 -11
View File
@@ -23,6 +23,7 @@ class SentenceQueueModule extends BaseModule {
this.autoplay = true;
this.inputMode = 'text';
this.lastContinueAt = 0;
this.pauseBeforeNextReason = null;
// Bind methods
this.bindMethods([
@@ -30,6 +31,7 @@ class SentenceQueueModule extends BaseModule {
'addSentence',
'processNextSentence',
'setOnSentenceReady',
'pauseBeforeNext',
'completeSentence',
'getCacheKey',
'getPreparedSentence',
@@ -105,6 +107,10 @@ class SentenceQueueModule extends BaseModule {
this.onSentenceReadyCallback = callback;
}
}
pauseBeforeNext(reason = 'manual-pause') {
this.pauseBeforeNextReason = reason;
}
/**
* Add a sentence to the queue
@@ -139,6 +145,12 @@ class SentenceQueueModule extends BaseModule {
const item = this.sentenceQueue[0];
try {
if (this.pauseBeforeNextReason) {
const reason = this.pauseBeforeNextReason;
this.pauseBeforeNextReason = null;
await this.waitForManualContinue(reason);
}
const sentence = await this.getPreparedSentence(item);
// Prefetch far enough ahead that media pauses do not block TTS
@@ -294,6 +306,8 @@ class SentenceQueueModule extends BaseModule {
kind: metadata.type,
text: text || '',
turnId: metadata.turnId ?? null,
blockId: metadata.blockId ?? null,
gameId: metadata.gameId ?? null,
status: 'ready',
metadata: imageLayout ? { ...metadata, imageLayout } : metadata,
tts: { duration: 0, provider: null, audioData: null, play: null, stop: null, enabled: false },
@@ -325,6 +339,8 @@ class SentenceQueueModule extends BaseModule {
kind: metadata.type === 'heading' ? 'heading' : 'paragraph',
text,
turnId: metadata.turnId ?? null,
blockId: metadata.blockId ?? null,
gameId: metadata.gameId ?? null,
paragraphIndex: metadata.paragraphIndex ?? null,
isFirstParagraphInChapter: Boolean(metadata.isFirstParagraphInChapter),
role: metadata.role || (metadata.type === 'heading' ? 'chapter-heading' : 'body'),
@@ -746,20 +762,27 @@ class SentenceQueueModule extends BaseModule {
probe.remove();
const pageWidth = storyElement.clientWidth;
const size = String(metadata.size || 'landscape').toLowerCase();
const aspect = size === 'portrait' ? (9 / 16) : size === 'square' ? 1 : (16 / 9);
const imageGap = lineHeight * 0.9;
const maxWidth = size === 'portrait' ? pageWidth * 0.5 : pageWidth;
const naturalHeight = maxWidth / aspect;
const requestedSize = String(metadata.size || 'landscape').toLowerCase();
const size = requestedSize === 'widescreen' ? 'landscape' : requestedSize;
const isPortrait = size === 'portrait';
const aspect = isPortrait ? (9 / 16) : size === 'square' ? 1 : (16 / 9);
const imageGap = lineHeight;
const maxOuterWidth = isPortrait ? pageWidth * 0.5 : pageWidth;
const maxImageWidth = isPortrait
? Math.max(lineHeight * 4, maxOuterWidth - imageGap)
: maxOuterWidth;
const naturalHeight = maxImageWidth / aspect;
const imageLineCount = Math.max(1, Math.floor(naturalHeight / lineHeight));
const height = imageLineCount * lineHeight;
const width = Math.min(maxWidth, height * aspect);
const verticalMargin = lineHeight / 2;
const lineCount = imageLineCount + 1;
const verticalMargin = isPortrait ? lineHeight / 2 : 0;
const lineCount = isPortrait ? imageLineCount + 1 : imageLineCount;
const height = isPortrait
? Math.max(lineHeight, (lineCount * lineHeight) - (verticalMargin * 2))
: imageLineCount * lineHeight;
const width = Math.min(maxImageWidth, height * aspect);
if (size === 'portrait') {
if (isPortrait) {
this.activeImageWrap = {
lines: lineCount,
lines: lineCount + 1,
width: width + imageGap,
imageWidth: width,
gap: imageGap,