Files
wehub-resource-sync c48612c494
CI / E2E Tests (push) Has been cancelled
CI / Lint, Typecheck & Unit Tests (push) Has been cancelled
Docs Build / Build docs site (push) Has been cancelled
Publish @openmaic packages / Build, validate & publish (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:03:23 +08:00

74 lines
2.4 KiB
TypeScript

/**
* Playback cursor — resolve the current action from a `(sceneIndex, actionIndex)`
* cursor over a scene list.
*
* Moved verbatim from the app's `lib/playback/engine-cursor.ts` so the app
* runtime and the video exporter walk scenes identically. Pure — types come
* from `@openmaic/dsl`, no runtime dependencies.
*/
import type { Action, SceneCore } from '@openmaic/dsl';
/**
* Synthetic dwell beat yielded for a scene that carries no actions. It is an
* empty-text speech, so the engine routes it through the same reading-timer
* dwell a blank speech clip already uses — the slide shows for a short beat
* instead of being skipped (which would make it vanish from playback). A scene
* with `actions: []` thus behaves exactly like one carrying a single blank
* speech clip.
*/
export const EMPTY_SCENE_DWELL: Action = {
id: '__empty_scene_dwell__',
type: 'speech',
text: '',
} as Action;
export interface CursorResult {
action: Action;
sceneId: string;
/** The (possibly advanced) scene cursor the engine should adopt. */
sceneIndex: number;
/** The (possibly advanced) action cursor the engine should adopt. */
actionIndex: number;
}
/**
* Resolve the current playback action from a `(sceneIndex, actionIndex)` cursor,
* advancing past scenes whose actions are exhausted. A scene with no actions
* yields one {@link EMPTY_SCENE_DWELL} beat (when its action cursor is still 0)
* rather than being skipped. Returns `null` once every scene is consumed.
*
* Pure: it does not mutate inputs; the caller adopts the returned cursor. Typed
* against {@link SceneCore} (only `id` + `actions` are read), so an app-widened
* `Scene` (extra content kinds) is accepted without casting.
*/
export function resolvePlaybackCursor(
scenes: SceneCore[],
sceneIndex: number,
actionIndex: number,
): CursorResult | null {
let si = sceneIndex;
let ai = actionIndex;
while (si < scenes.length) {
const actions = scenes[si].actions ?? [];
if (actions.length === 0) {
if (ai === 0) {
return {
action: EMPTY_SCENE_DWELL,
sceneId: scenes[si].id,
sceneIndex: si,
actionIndex: ai,
};
}
si++;
ai = 0;
continue;
}
if (ai < actions.length) {
return { action: actions[ai], sceneId: scenes[si].id, sceneIndex: si, actionIndex: ai };
}
si++;
ai = 0;
}
return null;
}