macro-clips slice 5: action evaluator + forward-only script frame firing
- new src/lib/actions.ts:
- ActionRuntime interface (fps, range, markers, setPlayhead/play/pause,
setButtonState/setButtonDisabled, warn)
- evalAction dispatches each action kind, with clamping to [0, duration]
for time-based jumps and a warning path for unknown markers and for
script actions (DSL lands in slice 8)
- scriptFramesInRange helper returns frames in the open-closed interval
(prev, next], sorted
- store wires it up:
- tick captures prevPlayhead, advances, then fires script frames that
fall in (prev, landed]. Scrubs via setPlayhead never touch this path
so the forward-only guarantee falls out of the architecture.
- actionRuntime() builds an ActionRuntime bound to this store
- setButtonState is a no-op in the editor for now (waits for the
button-runtime slice); setButtonDisabled goes to the store action,
rewindToLoop/play/pause/jumps mutate the store directly
- tests: 10 evaluator tests against a mock ActionRuntime covering all
action kinds + marker map + scriptFramesInRange; 3 integration tests
for tick firing (ordering, no-fire-on-scrub, firing-at-playbackOut)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
b9d1ca16f7
commit
8817a73c3d
3 files changed
+320
-3
No files matched your search
@@ -0,0 +1,85 @@
|
||||
import type { Action, ButtonState, Marker } from '@/types/scene'
|
||||
|
||||
export interface ActionRuntime {
|
||||
fps: number
|
||||
playbackIn: number
|
||||
playbackOut: number
|
||||
duration: number
|
||||
markers: Record<string, number>
|
||||
setPlayhead(t: number): void
|
||||
play(): void
|
||||
pause(): void
|
||||
setButtonState(instanceId: string, state: ButtonState): void
|
||||
setButtonDisabled(instanceId: string, disabled: boolean): void
|
||||
warn(msg: string): void
|
||||
}
|
||||
|
||||
export function buildMarkerMap(
|
||||
markers: Marker[] | undefined,
|
||||
): Record<string, number> {
|
||||
const out: Record<string, number> = {}
|
||||
for (const m of markers ?? []) out[m.name] = m.time
|
||||
return out
|
||||
}
|
||||
|
||||
export function evalAction(action: Action, rt: ActionRuntime): void {
|
||||
switch (action.type) {
|
||||
case 'play':
|
||||
rt.play()
|
||||
return
|
||||
case 'pause':
|
||||
rt.pause()
|
||||
return
|
||||
case 'rewindToLoop':
|
||||
rt.setPlayhead(rt.playbackIn)
|
||||
return
|
||||
case 'jumpTo':
|
||||
rt.setPlayhead(clamp(action.time, 0, rt.duration))
|
||||
return
|
||||
case 'jumpToFrame': {
|
||||
const fps = rt.fps || 60
|
||||
rt.setPlayhead(clamp(action.frame / fps, 0, rt.duration))
|
||||
return
|
||||
}
|
||||
case 'jumpToMarker': {
|
||||
const t = rt.markers[action.name]
|
||||
if (t == null) {
|
||||
rt.warn(`Unknown marker: ${action.name}`)
|
||||
return
|
||||
}
|
||||
rt.setPlayhead(clamp(t, 0, rt.duration))
|
||||
return
|
||||
}
|
||||
case 'setButtonState':
|
||||
rt.setButtonState(action.instanceId, action.state)
|
||||
return
|
||||
case 'setButtonDisabled':
|
||||
rt.setButtonDisabled(action.instanceId, action.disabled)
|
||||
return
|
||||
case 'script':
|
||||
rt.warn('Script DSL is not implemented yet')
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
function clamp(n: number, lo: number, hi: number): number {
|
||||
return Math.max(lo, Math.min(hi, n))
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the script frames whose time sits in the open-closed interval
|
||||
* `(prev, next]`, sorted by time ascending. Forward-only — callers must
|
||||
* pass `next >= prev`, and the caller is responsible for suppressing
|
||||
* firing across loop wraps or scrubs.
|
||||
*/
|
||||
export function scriptFramesInRange<
|
||||
F extends { time: number; actions: unknown[] },
|
||||
>(frames: F[] | undefined, prev: number, next: number): F[] {
|
||||
if (!frames || !frames.length || next <= prev) return []
|
||||
const out: F[] = []
|
||||
for (const f of frames) {
|
||||
if (f.time > prev && f.time <= next) out.push(f)
|
||||
}
|
||||
out.sort((a, b) => a.time - b.time)
|
||||
return out
|
||||
}
|
||||
Reference in new issue
Block a user