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
+51
-3
@@ -21,6 +21,12 @@ import type {
|
||||
} from '@/types/scene'
|
||||
import { DEFAULT_BASE_PROPS } from '@/types/scene'
|
||||
import { sampleLayer } from '@/lib/interpolate'
|
||||
import {
|
||||
buildMarkerMap,
|
||||
evalAction,
|
||||
scriptFramesInRange,
|
||||
type ActionRuntime,
|
||||
} from '@/lib/actions'
|
||||
|
||||
const uid = () => Math.random().toString(36).slice(2, 10)
|
||||
|
||||
@@ -1464,13 +1470,55 @@ export const useSceneStore = defineStore('scene', {
|
||||
},
|
||||
tick(deltaSeconds: number) {
|
||||
if (!this.playing) return
|
||||
const prev = this.playhead
|
||||
const end = this.playbackOut
|
||||
const next = this.playhead + deltaSeconds
|
||||
if (next >= end) {
|
||||
const nextRaw = prev + deltaSeconds
|
||||
if (nextRaw >= end) {
|
||||
this.playhead = end
|
||||
this.playing = false
|
||||
} else {
|
||||
this.playhead = next
|
||||
this.playhead = nextRaw
|
||||
}
|
||||
const landed = this.playhead
|
||||
// Forward-only firing: script frames whose time sits in (prev, landed].
|
||||
// Scrubs/setPlayhead/loop-wraps never reach this path, so the rule
|
||||
// is naturally preserved.
|
||||
const frames = scriptFramesInRange(this.scene.scriptFrames, prev, landed)
|
||||
if (frames.length) {
|
||||
const rt = this.actionRuntime()
|
||||
for (const f of frames) {
|
||||
for (const action of f.actions) evalAction(action, rt)
|
||||
}
|
||||
}
|
||||
},
|
||||
actionRuntime(): ActionRuntime {
|
||||
const self = this
|
||||
return {
|
||||
fps: self.scene.fps,
|
||||
playbackIn: self.playbackIn,
|
||||
playbackOut: self.playbackOut,
|
||||
duration: self.scene.duration,
|
||||
markers: buildMarkerMap(self.scene.markers),
|
||||
setPlayhead(t: number) {
|
||||
self.setPlayhead(t)
|
||||
},
|
||||
play() {
|
||||
self.play()
|
||||
},
|
||||
pause() {
|
||||
self.pause()
|
||||
},
|
||||
setButtonState() {
|
||||
// No-op in editor for now — runtime-state management lands in
|
||||
// the export-runtime slice together with pointer handling.
|
||||
},
|
||||
setButtonDisabled(instanceId: string, disabled: boolean) {
|
||||
self.setButtonInstanceDisabled(instanceId, disabled)
|
||||
},
|
||||
warn(msg: string) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn('[animatr]', msg)
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
Reference in new issue
Block a user