Checkpoint Eibenreith ink architecture

This commit is contained in:
2026-05-24 09:09:41 +02:00
parent beac5a2be3
commit d42540f29d
35 changed files with 12015 additions and 54 deletions
+92 -11
View File
@@ -38,6 +38,7 @@ class UIDisplayHandlerModule extends BaseModule {
this.scrollTargetLine = null;
this.scrollRequestId = 0;
this.renderWindowToken = 0;
this.displayGeneration = 0;
this.wheelLineAccumulator = 0;
this.viewportLineCount = 1;
this.lineHeightPx = 24;
@@ -124,6 +125,7 @@ class UIDisplayHandlerModule extends BaseModule {
'rerenderStory',
'clear',
'scheduleRerender',
'isDisplayGenerationCurrent',
'measureText',
'loadCSS',
'showChoices',
@@ -955,15 +957,35 @@ class UIDisplayHandlerModule extends BaseModule {
return null;
}
const generation = this.displayGeneration;
const sentenceGameId = sentence.gameId || null;
const isCurrent = () => this.isDisplayGenerationCurrent(generation, sentenceGameId);
try {
await this.ensureLiveTailWindow();
if (!isCurrent()) return null;
await this.scrollTo(this.getLiveEndLine(), { mode: 'enter-live-tail', smooth: false });
if (!isCurrent()) return null;
this.rebuildLayoutExclusions(this.renderedItems);
this.layoutFlowLine = this.getFlowLineFromItems(this.renderedItems);
const element = await this.renderStoryBlock(sentence, { animate: true, playback: true, placement: 'append' });
const element = await this.renderStoryBlock(sentence, {
animate: true,
playback: true,
placement: 'append',
token: this.renderWindowToken,
generation
});
if (!element) return null;
if (!isCurrent()) {
element.remove();
return null;
}
sentence.element = element;
await this.scrollTo(this.getLiveEndLine(), { mode: 'append-live' });
if (!isCurrent()) {
element.remove();
return null;
}
if (sentence.kind === 'image') {
this.revealImageBlock(element);
@@ -972,6 +994,7 @@ class UIDisplayHandlerModule extends BaseModule {
} else if (sentence.kind === 'music') {
console.log('UIDisplayHandler: Music block started', sentence.metadata || {});
}
if (!isCurrent()) return null;
this.dispatchDeferredTagsForBlock(sentence);
@@ -990,34 +1013,51 @@ class UIDisplayHandlerModule extends BaseModule {
async rerenderStory() {
if (!this.paragraphContainer || this.renderedItems.length === 0) return;
console.log('UIDisplayHandler: Re-typesetting story after page resize');
const generation = this.displayGeneration;
const activeLine = this.getCurrentScrollLine();
await this.renderHistoryWindow([...this.renderedItems]);
await this.renderHistoryWindow([...this.renderedItems], { generation });
if (!this.isDisplayGenerationCurrent(generation)) return;
await this.scrollTo(activeLine, { mode: 'rerender-preserve', smooth: false });
}
isDisplayGenerationCurrent(generation = this.displayGeneration, gameId = null) {
if (generation !== this.displayGeneration) return false;
if (gameId && this.storyHistory?.currentGameId && this.storyHistory.currentGameId !== gameId) {
return false;
}
return true;
}
async restoreFromHistory(saveRecord = {}) {
if (!this.paragraphContainer || !this.storyHistory || !saveRecord?.gameId) return;
const generation = this.displayGeneration;
const latestRenderedBlockId = Math.max(0, Number(saveRecord.latestRenderedBlockId || 0));
if (!this.storyHistory.renderedLineCount) {
await this.storyHistory.getRenderedLineCount(saveRecord.gameId, latestRenderedBlockId);
if (!this.isDisplayGenerationCurrent(generation, saveRecord.gameId)) return;
}
const targetLine = Math.max(0, latestRenderedBlockId > 0 ? this.storyHistory.renderedLineCount - 1 : 0);
if (latestRenderedBlockId > 0) {
const targetBlock = await this.storyHistory.findBlockForLine(saveRecord.gameId, targetLine, latestRenderedBlockId);
if (!this.isDisplayGenerationCurrent(generation, saveRecord.gameId)) return;
const targetBlockId = Math.max(1, Number(targetBlock?.blockId || latestRenderedBlockId));
await this.renderWindowForBounds({
start: Math.max(1, targetBlockId - this.historyBufferBlocks),
end: Math.min(latestRenderedBlockId, targetBlockId + this.historyBufferBlocks),
targetBlockId,
windowOriginLine: this.getTopLineForActiveLine(targetLine)
windowOriginLine: this.getTopLineForActiveLine(targetLine),
gameId: saveRecord.gameId,
generation
});
} else {
await this.renderHistoryWindow([], { windowOriginLine: 0 });
await this.renderHistoryWindow([], { windowOriginLine: 0, generation });
}
if (!this.isDisplayGenerationCurrent(generation, saveRecord.gameId)) return;
await this.scrollTo(targetLine, {
mode: 'restore-bottom',
smooth: false
});
if (!this.isDisplayGenerationCurrent(generation, saveRecord.gameId)) return;
this.updateStoryScrollbar({ latestBlockId: saveRecord.latestBlockId || latestRenderedBlockId || 1 });
}
@@ -1038,10 +1078,12 @@ class UIDisplayHandlerModule extends BaseModule {
targetContainer = this.paragraphContainer,
renderedItemsTarget = this.renderedItems,
token = null,
recordMetrics = true
recordMetrics = true,
generation = this.displayGeneration
} = options;
if (!item || !this.paragraphContainer) return null;
const renderable = await this.prepareRenderableBlock(item);
if (!this.isDisplayGenerationCurrent(generation, item.gameId || null)) return null;
if (token != null && token !== this.renderWindowToken) return null;
if (!renderable) return null;
@@ -1110,6 +1152,10 @@ class UIDisplayHandlerModule extends BaseModule {
if (recordMetrics && item.blockId != null) {
const updated = await this.recordRenderedMetrics(item.blockId, element, renderable.lineCount, renderable.lineStart);
if (!this.isDisplayGenerationCurrent(generation, item.gameId || null)) {
element?.remove();
return null;
}
if (token != null && token !== this.renderWindowToken) {
element?.remove();
return null;
@@ -1130,6 +1176,8 @@ class UIDisplayHandlerModule extends BaseModule {
async renderHistoryWindow(blocks = [], options = {}) {
if (!this.paragraphContainer) return;
const generation = options.generation ?? this.displayGeneration;
if (!this.isDisplayGenerationCurrent(generation, options.gameId || blocks[0]?.gameId || null)) return;
const token = ++this.renderWindowToken;
const orderedBlocks = Array.isArray(blocks)
? [...blocks].sort((left, right) => Number(left?.blockId || 0) - Number(right?.blockId || 0))
@@ -1152,12 +1200,15 @@ class UIDisplayHandlerModule extends BaseModule {
placement: 'append',
targetContainer: fragment,
renderedItemsTarget: nextRenderedItems,
token
token,
generation
});
if (token !== this.renderWindowToken) return;
if (!this.isDisplayGenerationCurrent(generation, item.gameId || options.gameId || null)) return;
}
if (token !== this.renderWindowToken) return;
if (!this.isDisplayGenerationCurrent(generation, options.gameId || orderedBlocks[0]?.gameId || null)) return;
this.paragraphContainer.replaceChildren(fragment);
this.renderedItems = nextRenderedItems;
this.historyWindowStartId = orderedBlocks[0]?.blockId || 1;
@@ -1263,9 +1314,11 @@ class UIDisplayHandlerModule extends BaseModule {
async reflowTextBlocksForActiveExclusions(token = this.renderWindowToken) {
if (!this.layoutExclusions.length || !this.paragraphContainer) return;
const generation = this.displayGeneration;
const candidates = this.renderedItems.filter(item => this.blockIntersectsExclusions(item));
for (const item of candidates) {
if (token !== this.renderWindowToken) return;
if (!this.isDisplayGenerationCurrent(generation, item.gameId || null)) return;
const oldElement = this.paragraphContainer.querySelector(`[data-story-block-id="${CSS.escape(String(item.blockId))}"]`);
if (!oldElement) continue;
const previousFlowLine = this.layoutFlowLine;
@@ -1274,6 +1327,7 @@ class UIDisplayHandlerModule extends BaseModule {
this.layoutFlowLine = previousFlowLine;
this.layoutExclusions = previousExclusions;
if (token !== this.renderWindowToken) return;
if (!this.isDisplayGenerationCurrent(generation, item.gameId || null)) return;
renderable.layout.lineCount = Math.max(1, Number(item.lineCount ?? item.metadata?.lineCount ?? renderable.lineCount));
renderable.lineCount = renderable.layout.lineCount;
const replacement = this.layoutRenderer.renderParagraph(renderable.layout, { id: renderable.id });
@@ -1294,6 +1348,9 @@ class UIDisplayHandlerModule extends BaseModule {
async renderIncrementalWindow(bounds = {}, requestId = null) {
if (!this.storyHistory || !this.paragraphContainer) return;
const generation = bounds.generation ?? this.displayGeneration;
const gameId = bounds.gameId || this.storyHistory.currentGameId;
if (!this.isDisplayGenerationCurrent(generation, gameId)) return;
if (!this.renderedItems.length) {
await this.renderWindowForBounds(bounds, requestId);
return;
@@ -1310,15 +1367,17 @@ class UIDisplayHandlerModule extends BaseModule {
const missingBeforeEnd = Math.min(end, (this.historyWindowStartId || start) - 1);
const missingAfterStart = Math.max(start, (this.historyWindowEndId || 0) + 1);
const missingBeforeBlocks = start <= missingBeforeEnd
? await this.storyHistory.getBlocksRange(this.storyHistory.currentGameId, start, missingBeforeEnd)
? await this.storyHistory.getBlocksRange(gameId, start, missingBeforeEnd)
: [];
if (requestId != null && requestId !== this.scrollRequestId) return;
if (token !== this.renderWindowToken) return;
if (!this.isDisplayGenerationCurrent(generation, gameId)) return;
const missingAfterBlocks = missingAfterStart <= end
? await this.storyHistory.getBlocksRange(this.storyHistory.currentGameId, missingAfterStart, end)
? await this.storyHistory.getBlocksRange(gameId, missingAfterStart, end)
: [];
if (requestId != null && requestId !== this.scrollRequestId) return;
if (token !== this.renderWindowToken) return;
if (!this.isDisplayGenerationCurrent(generation, gameId)) return;
const uniqueBeforeBlocks = missingBeforeBlocks.filter(block => {
const id = Number(block?.blockId || 0);
@@ -1355,6 +1414,7 @@ class UIDisplayHandlerModule extends BaseModule {
targetContainer: beforeFragment,
renderedItemsTarget: beforeItems,
token,
generation,
recordMetrics: false
});
if (token !== this.renderWindowToken || (requestId != null && requestId !== this.scrollRequestId)) {
@@ -1378,6 +1438,7 @@ class UIDisplayHandlerModule extends BaseModule {
targetContainer: afterFragment,
renderedItemsTarget: afterItems,
token,
generation,
recordMetrics: false
});
if (token !== this.renderWindowToken || (requestId != null && requestId !== this.scrollRequestId)) {
@@ -1402,6 +1463,7 @@ class UIDisplayHandlerModule extends BaseModule {
this.rebuildLayoutExclusions(this.renderedItems);
await this.reflowTextBlocksForActiveExclusions(token);
if (token !== this.renderWindowToken) return;
if (!this.isDisplayGenerationCurrent(generation, gameId)) return;
this.setVirtualPadding();
this.updateStoryScrollbar();
}
@@ -2033,6 +2095,8 @@ class UIDisplayHandlerModule extends BaseModule {
async ensureLiveTailWindow() {
if (!this.storyHistory || !this.paragraphContainer) return;
const generation = this.displayGeneration;
const gameId = this.storyHistory.currentGameId;
const latestRendered = Math.max(0, Number(this.storyHistory.latestRenderedBlockId || 0));
if (latestRendered > 0) {
const start = Math.max(1, latestRendered - (this.visibleBlockLimit - 2));
@@ -2043,8 +2107,11 @@ class UIDisplayHandlerModule extends BaseModule {
start,
end,
targetBlockId: latestRendered,
windowOriginLine: this.getTopLineForActiveLine(liveEndLine)
windowOriginLine: this.getTopLineForActiveLine(liveEndLine),
gameId,
generation
});
if (!this.isDisplayGenerationCurrent(generation, gameId)) return;
}
} else if (this.renderedItems.length) {
this.paragraphContainer.innerHTML = '';
@@ -2104,11 +2171,20 @@ class UIDisplayHandlerModule extends BaseModule {
}
async renderWindowForBounds(bounds = {}, requestId = null) {
if (!this.storyHistory) return;
const generation = bounds.generation ?? this.displayGeneration;
const gameId = bounds.gameId || this.storyHistory.currentGameId;
if (!this.isDisplayGenerationCurrent(generation, gameId)) return;
const start = Math.max(1, Number(bounds.start || 1));
const end = Math.max(start, Number(bounds.end || start));
const blocks = await this.storyHistory.getBlocksRange(this.storyHistory.currentGameId, start, end);
const blocks = await this.storyHistory.getBlocksRange(gameId, start, end);
if (requestId != null && requestId !== this.scrollRequestId) return;
await this.renderHistoryWindow(blocks, { windowOriginLine: bounds.windowOriginLine });
if (!this.isDisplayGenerationCurrent(generation, gameId)) return;
await this.renderHistoryWindow(blocks, {
windowOriginLine: bounds.windowOriginLine,
gameId,
generation
});
}
focusTurn(turnId) {
@@ -2335,6 +2411,7 @@ class UIDisplayHandlerModule extends BaseModule {
}
clear() {
this.displayGeneration += 1;
this.renderWindowToken += 1;
this.scrollRequestId += 1;
if (this.scrollAnimationFrameId != null) {
@@ -2370,6 +2447,10 @@ class UIDisplayHandlerModule extends BaseModule {
this.historyWindowStartId = 1;
this.historyWindowEndId = 0;
this.storyTopLine = 0;
this.scrollTargetLine = null;
this.wheelLineAccumulator = 0;
this.draggingStoryScrollbar = false;
this.scrollbarPreviewLine = null;
this.activeCenterBlockId = null;
this.layoutFlowLine = 0;
this.layoutExclusions = [];