macro-clips slice 1: types + store for buttons, scripts, markers

- ButtonClip/ButtonInstance, ScriptTrack/ScriptFrame, Marker, Action union
- new optional scene collections (buttonClips, buttonInstances, scriptTracks,
  scriptFrames, markers); normalizeScene initializes them on load
- store actions for all new entities, marker name + button label uniqueness,
  time clamping, cascading deletes (script frames with their track, button
  instances with their clip), script tracks join trackOrder + setTrackLocked
- design doc docs/macro-clips-and-scripts.md
- tests cover add/remove/rename/retime paths, uniqueness, sort order,
  label collisions, and normalization

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
thandClaude Opus 4.7 committed 2026-04-20 20:34:44 +02:00
1 parent d4c4830fe2
commit a75e9151ed
4 files changed
+952

No files matched your search

+193
View File
@@ -1,16 +1,22 @@
import { defineStore } from 'pinia'
import type {
Action,
AnimatableProps,
Asset,
BackgroundVideo,
ButtonClip,
ButtonInstance,
Clip,
ClipInstance,
ClipTrack,
Easing,
Keyframe,
Layer,
Marker,
MaskMode,
Scene,
ScriptFrame,
ScriptTrack,
TrackGroup,
} from '@/types/scene'
import { DEFAULT_BASE_PROPS } from '@/types/scene'
@@ -112,6 +118,11 @@ function normalizeScene(scene: Scene): Scene {
scene.clipTracks ||= []
scene.clipInstances ||= []
scene.groups ||= []
scene.buttonClips ||= []
scene.buttonInstances ||= []
scene.scriptTracks ||= []
scene.scriptFrames ||= []
scene.markers ||= []
if (!scene.canvas.background) scene.canvas.background = '#ffffff'
const known = new Set<string>()
@@ -1001,12 +1012,194 @@ export const useSceneStore = defineStore('scene', {
else delete clipTrack.locked
return
}
const scriptTrack = this.scene.scriptTracks?.find((t) => t.id === trackId)
if (scriptTrack) {
if (locked) scriptTrack.locked = true
else delete scriptTrack.locked
return
}
const group = this.scene.groups?.find((g) => g.id === trackId)
if (group) {
if (locked) group.locked = true
else delete group.locked
}
},
// -------- Markers --------------------------------------------------
addMarker(time: number, name?: string, color?: string | null): Marker {
const t = Math.max(0, Math.min(this.scene.duration, time))
const markers = (this.scene.markers ||= [])
const used = new Set(markers.map((m) => m.name))
let base = (name ?? '').trim() || 'Marker'
let final = base
let i = 2
while (used.has(final)) final = `${base} ${i++}`
const m: Marker = { id: uid(), name: final, time: t }
if (color != null) m.color = color
markers.push(m)
markers.sort((a, b) => a.time - b.time)
return m
},
removeMarker(id: string) {
if (!this.scene.markers) return
this.scene.markers = this.scene.markers.filter((m) => m.id !== id)
},
renameMarker(id: string, name: string): boolean {
const markers = this.scene.markers
if (!markers) return false
const target = markers.find((m) => m.id === id)
if (!target) return false
const next = name.trim()
if (!next) return false
if (next === target.name) return true
if (markers.some((m) => m.id !== id && m.name === next)) return false
target.name = next
return true
},
setMarkerTime(id: string, time: number) {
const marker = this.scene.markers?.find((m) => m.id === id)
if (!marker) return
marker.time = Math.max(0, Math.min(this.scene.duration, time))
this.scene.markers?.sort((a, b) => a.time - b.time)
},
setMarkerColor(id: string, color: string | null) {
const marker = this.scene.markers?.find((m) => m.id === id)
if (!marker) return
if (color == null) delete marker.color
else marker.color = color
},
// -------- Script tracks & frames -----------------------------------
addScriptTrack(name?: string): ScriptTrack {
const track: ScriptTrack = { id: uid(), name }
;(this.scene.scriptTracks ||= []).push(track)
;(this.scene.trackOrder ||= []).push(track.id)
return track
},
removeScriptTrack(id: string) {
if (this.scene.scriptTracks) {
this.scene.scriptTracks = this.scene.scriptTracks.filter((t) => t.id !== id)
}
if (this.scene.scriptFrames) {
this.scene.scriptFrames = this.scene.scriptFrames.filter((f) => f.trackId !== id)
}
if (this.scene.trackOrder) {
this.scene.trackOrder = this.scene.trackOrder.filter((tid) => tid !== id)
}
},
setScriptTrackName(id: string, name: string) {
const track = this.scene.scriptTracks?.find((t) => t.id === id)
if (track) track.name = name.trim() || undefined
},
addScriptFrame(
trackId: string,
time: number,
actions: Action[] = [],
label?: string,
): ScriptFrame | null {
const track = this.scene.scriptTracks?.find((t) => t.id === trackId)
if (!track) return null
const t = Math.max(0, Math.min(this.scene.duration, time))
const frame: ScriptFrame = {
id: uid(),
trackId,
time: t,
actions: [...actions],
}
if (label) frame.label = label
;(this.scene.scriptFrames ||= []).push(frame)
this.scene.scriptFrames.sort((a, b) => a.time - b.time)
return frame
},
removeScriptFrame(id: string) {
if (!this.scene.scriptFrames) return
this.scene.scriptFrames = this.scene.scriptFrames.filter((f) => f.id !== id)
},
setScriptFrameActions(id: string, actions: Action[]) {
const frame = this.scene.scriptFrames?.find((f) => f.id === id)
if (frame) frame.actions = [...actions]
},
setScriptFrameTime(id: string, time: number) {
const frame = this.scene.scriptFrames?.find((f) => f.id === id)
if (!frame) return
frame.time = Math.max(0, Math.min(this.scene.duration, time))
this.scene.scriptFrames?.sort((a, b) => a.time - b.time)
},
// -------- Button clips & instances ---------------------------------
addButtonClip(name?: string): ButtonClip {
const clip: ButtonClip = {
id: uid(),
name: name?.trim() || 'Button',
states: { normal: [], hover: [], pressed: [], disabled: [] },
}
;(this.scene.buttonClips ||= []).push(clip)
return clip
},
removeButtonClip(id: string) {
if (this.scene.buttonClips) {
this.scene.buttonClips = this.scene.buttonClips.filter((c) => c.id !== id)
}
if (this.scene.buttonInstances) {
this.scene.buttonInstances = this.scene.buttonInstances.filter(
(bi) => bi.buttonClipId !== id,
)
}
},
setButtonClipName(id: string, name: string) {
const clip = this.scene.buttonClips?.find((c) => c.id === id)
if (clip) clip.name = name.trim() || clip.name
},
addButtonInstance(
buttonClipId: string,
clipTrackId: string,
startTime: number,
): ButtonInstance | null {
if (!this.scene.buttonClips?.some((c) => c.id === buttonClipId)) return null
if (!this.scene.clipTracks?.some((t) => t.id === clipTrackId)) return null
const inst: ButtonInstance = {
id: uid(),
buttonClipId,
clipTrackId,
startTime: Math.max(0, Math.min(this.scene.duration, startTime)),
}
;(this.scene.buttonInstances ||= []).push(inst)
return inst
},
removeButtonInstance(id: string) {
if (!this.scene.buttonInstances) return
this.scene.buttonInstances = this.scene.buttonInstances.filter((bi) => bi.id !== id)
},
setButtonInstanceLabel(id: string, label: string): boolean {
const instances = this.scene.buttonInstances
if (!instances) return false
const target = instances.find((bi) => bi.id === id)
if (!target) return false
const next = label.trim()
if (!next) {
delete target.label
return true
}
if (next === target.label) return true
if (instances.some((bi) => bi.id !== id && bi.label === next)) return false
target.label = next
return true
},
setButtonInstanceOnClick(id: string, actions: Action[]) {
const target = this.scene.buttonInstances?.find((bi) => bi.id === id)
if (!target) return
if (actions.length) target.onClick = [...actions]
else delete target.onClick
},
setButtonInstanceDisabled(id: string, disabled: boolean) {
const target = this.scene.buttonInstances?.find((bi) => bi.id === id)
if (!target) return
if (disabled) target.disabled = true
else delete target.disabled
},
setLayerSeparated(layerId: string, separated: boolean) {
const layer = this.activeLayers.find((l) => l.id === layerId)
if (!layer) return