0.10.7: fire scripts at play-start + right-click duplicate script frame

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
thandClaude Opus 4.7 committed 2026-04-22 19:39:26 +02:00
1 parent 0bbef66192
commit 34d1a89d56
6 files changed
+313 -15

No files matched your search

+49 -2
View File
@@ -94,6 +94,10 @@ interface State {
selectedClipInstanceId: string | null
playhead: number
playing: boolean
/** True between `play()` and the next `tick()`. Lets the first tick
* after a play-start fire scripts whose time === current playhead
* (e.g. a frame-0 script). Cleared by tick / pause / exitClip. */
playStartPending: boolean
activeClipId: string | null
/** When set, the Stage + Timeline are inside a Button-asset edit session. */
activeButtonAssetId: string | null
@@ -395,6 +399,7 @@ export const useSceneStore = defineStore('scene', {
selectedClipInstanceId: null,
playhead: 0,
playing: false,
playStartPending: false,
activeClipId: null,
activeButtonAssetId: null,
activeButtonEditMode: null,
@@ -1600,6 +1605,37 @@ export const useSceneStore = defineStore('scene', {
frame.time = Math.max(0, Math.min(this.scene.duration, time))
this.scene.scriptFrames?.sort((a, b) => a.time - b.time)
},
duplicateScriptFrame(frameId: string): string | null {
const frames = this.scene.scriptFrames ?? []
const source = frames.find((f) => f.id === frameId)
if (!source) return null
const sameTrack = frames.filter((f) => f.trackId === source.trackId)
const occupied = (t: number) =>
sameTrack.some((f) => Math.abs(f.time - t) < KEYFRAME_EPSILON)
const fps = this.scene.fps > 0 ? this.scene.fps : 60
const offset = Math.max(3 / fps, 0.1)
const duration = this.scene.duration
// Walk forward looking for the first free slot; if the track is
// packed to the end, fall back to stepping backward before giving up.
let target = source.time + offset
while (occupied(target) && target < duration) target += offset
if (target > duration || occupied(target)) {
target = source.time - offset
while (occupied(target) && target > 0) target -= offset
if (target < 0) target = 0
if (occupied(target)) return null
}
const copy: ScriptFrame = {
id: uid(),
trackId: source.trackId,
time: target,
actions: source.actions.map((a) => ({ ...a })),
}
if (source.label) copy.label = source.label
;(this.scene.scriptFrames ||= []).push(copy)
this.scene.scriptFrames.sort((a, b) => a.time - b.time)
return copy.id
},
// -------- Audio tracks, clips & assets -----------------------------
@@ -2578,9 +2614,11 @@ export const useSceneStore = defineStore('scene', {
play() {
if (this.playing) return
this.playing = true
this.playStartPending = true
},
pause() {
this.playing = false
this.playStartPending = false
},
togglePlay() {
if (this.playing) this.pause()
@@ -2607,8 +2645,17 @@ export const useSceneStore = defineStore('scene', {
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)
// is naturally preserved. The first tick after play() widens the
// left bound to [prev, landed] so scripts at the starting frame
// (e.g. the very first time slot) also fire.
const includeStart = this.playStartPending
this.playStartPending = false
const frames = scriptFramesInRange(
this.scene.scriptFrames,
prev,
landed,
{ includeStart },
)
if (frames.length) {
const rt = this.actionRuntime()
for (const f of frames) {