0.9.0: scenes — multi-page projects with right-side drawer

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 07:17:06 +02:00
1 parent 517705bf55
commit b57994c754
12 files changed
+1193 -176

No files matched your search

+369 -110
View File
@@ -16,7 +16,9 @@ import type {
Layer,
Marker,
MaskMode,
Project,
Scene,
ScenePage,
ScriptFrame,
ScriptTrack,
TrackGroup,
@@ -64,7 +66,15 @@ function snapToButtonStateTime(t: number): number {
export type ButtonEditMode = 'states' | 'hit'
interface State {
scene: Scene
/** The persisted top-level container. Holds shared fields (canvas,
* duration, fps, in/out, asset library, clip library) plus the list
* of scene pages and which one is active. The `scene` getter assembles
* a Scene-shaped merged view (shared fields + active page's arrays)
* so existing read sites — `store.scene.canvas`, `store.scene.layers`,
* `store.scene.assets`, … — keep working unchanged. Mutations go
* through actions that target either `this.project.X` (shared) or
* `this.activeScene.X` (per-page) directly. */
project: Project
/** Primary (anchor) selection — drives the Properties panel and
* any code that expects a single focused layer. Always matches
* the last entry of `selectedLayerIds`. */
@@ -184,32 +194,33 @@ function findLayerAnywhere(scene: Scene, layerId: string): Layer | null {
return null
}
function normalizeScene(scene: Scene): Scene {
scene.clips ||= []
scene.clipTracks ||= []
scene.clipInstances ||= []
scene.groups ||= []
scene.scriptTracks ||= []
scene.scriptFrames ||= []
scene.markers ||= []
if (!scene.canvas.background) scene.canvas.background = '#ffffff'
/** Per-page housekeeping: ensure all arrays exist, clean up orphan
* childIds in groups, dedupe, build trackOrder for any pristine track,
* and migrate pre-`colorOverridden` color flags. */
function normalizeScenePage(page: ScenePage): ScenePage {
page.layers ||= []
page.clipInstances ||= []
page.clipTracks ||= []
page.scriptTracks ||= []
page.scriptFrames ||= []
page.markers ||= []
page.groups ||= []
page.trackOrder ||= []
const known = new Set<string>()
for (const l of scene.layers) known.add(l.id)
for (const t of scene.clipTracks) known.add(t.id)
for (const g of scene.groups) known.add(g.id)
for (const st of scene.scriptTracks) known.add(st.id)
for (const l of page.layers) known.add(l.id)
for (const t of page.clipTracks) known.add(t.id)
for (const g of page.groups) known.add(g.id)
for (const st of page.scriptTracks) known.add(st.id)
// Cleanup: group childIds should only reference existing layers/clipTracks
const layerAndClipIds = new Set<string>()
for (const l of scene.layers) layerAndClipIds.add(l.id)
for (const t of scene.clipTracks) layerAndClipIds.add(t.id)
for (const g of scene.groups) {
for (const l of page.layers) layerAndClipIds.add(l.id)
for (const t of page.clipTracks) layerAndClipIds.add(t.id)
for (const g of page.groups) {
g.childIds = g.childIds.filter((id) => layerAndClipIds.has(id))
}
// Dedupe: any given track can only be in one group
const inGroup = new Set<string>()
for (const g of scene.groups) {
for (const g of page.groups) {
g.childIds = g.childIds.filter((id) => {
if (inGroup.has(id)) return false
inGroup.add(id)
@@ -217,60 +228,147 @@ function normalizeScene(scene: Scene): Scene {
})
}
if (!scene.trackOrder) scene.trackOrder = []
scene.trackOrder = scene.trackOrder.filter((id) => known.has(id))
for (const l of scene.layers) {
if (!inGroup.has(l.id) && !scene.trackOrder.includes(l.id)) {
scene.trackOrder.unshift(l.id)
page.trackOrder = page.trackOrder.filter((id) => known.has(id))
for (const l of page.layers) {
if (!inGroup.has(l.id) && !page.trackOrder.includes(l.id)) {
page.trackOrder.unshift(l.id)
}
}
for (const t of scene.clipTracks) {
if (!inGroup.has(t.id) && !scene.trackOrder.includes(t.id)) {
scene.trackOrder.push(t.id)
for (const t of page.clipTracks) {
if (!inGroup.has(t.id) && !page.trackOrder.includes(t.id)) {
page.trackOrder.push(t.id)
}
}
for (const g of scene.groups) {
if (!scene.trackOrder.includes(g.id)) scene.trackOrder.push(g.id)
for (const g of page.groups) {
if (!page.trackOrder.includes(g.id)) page.trackOrder.push(g.id)
}
for (const st of scene.scriptTracks) {
if (!scene.trackOrder.includes(st.id)) scene.trackOrder.push(st.id)
for (const st of page.scriptTracks) {
if (!page.trackOrder.includes(st.id)) page.trackOrder.push(st.id)
}
// Migration: scenes saved before `colorOverridden` existed must treat
// any track that already carries a `color` as user-set. Without this,
// the first subsequent group-color change would silently overwrite
// pre-existing per-track colors.
for (const l of scene.layers) {
for (const l of page.layers) {
if (l.color != null && l.colorOverridden === undefined) l.colorOverridden = true
}
for (const t of scene.clipTracks) {
for (const t of page.clipTracks) {
if (t.color != null && t.colorOverridden === undefined) t.colorOverridden = true
}
for (const st of scene.scriptTracks) {
for (const st of page.scriptTracks) {
if (st.color != null && st.colorOverridden === undefined) st.colorOverridden = true
}
for (const g of scene.groups) {
for (const g of page.groups) {
if (g.color != null && g.colorOverridden === undefined) g.colorOverridden = true
}
return scene
return page
}
/** Project-level housekeeping. Ensures shared arrays exist and that
* `activeSceneId` actually references one of the pages — falls back to
* the first page (or a freshly minted one if none exist). */
function normalizeProject(project: Project): Project {
project.assets ||= []
project.clips ||= []
project.scenes ||= []
if (!project.canvas.background) project.canvas.background = '#ffffff'
if (project.scenes.length === 0) {
project.scenes.push(createBlankScenePage('Scene 1'))
}
for (const page of project.scenes) normalizeScenePage(page)
if (!project.scenes.find((s) => s.id === project.activeSceneId)) {
project.activeSceneId = project.scenes[0].id
}
return project
}
export function createBlankScene(): Scene {
return normalizeScene({
export function createBlankScenePage(name: string, id?: string): ScenePage {
return {
id: id ?? uid(),
name,
layers: [],
clipInstances: [],
clipTracks: [],
scriptTracks: [],
scriptFrames: [],
markers: [],
groups: [],
trackOrder: [],
}
}
export function createBlankProject(): Project {
const initial = createBlankScenePage('Scene 1')
return normalizeProject({
version: 1,
canvas: { width: 1280, height: 720, background: '#ffffff' },
duration: 10,
fps: 60,
assets: [],
layers: [],
clips: [],
scenes: [initial],
activeSceneId: initial.id,
})
}
/** Wrap an old-format Scene (v0 file: { scene }) into a one-page Project.
* All shared fields lift up to the project; per-page fields stay on the
* one synthesized page. */
export function projectFromLegacyScene(scene: Scene, sceneName = 'Scene 1'): Project {
const page: ScenePage = {
id: uid(),
name: sceneName,
layers: scene.layers ?? [],
clipInstances: scene.clipInstances ?? [],
clipTracks: scene.clipTracks ?? [],
scriptTracks: scene.scriptTracks ?? [],
scriptFrames: scene.scriptFrames ?? [],
markers: scene.markers ?? [],
groups: scene.groups ?? [],
trackOrder: scene.trackOrder ?? [],
backgroundVideo: scene.backgroundVideo,
}
return normalizeProject({
version: 1,
canvas: { ...scene.canvas },
duration: scene.duration,
fps: scene.fps,
inTime: scene.inTime,
outTime: scene.outTime,
assets: scene.assets ?? [],
clips: scene.clips ?? [],
scenes: [page],
activeSceneId: page.id,
})
}
/** Backward-compat shim. Older callers (legacy save format, tests) still
* reach for `createBlankScene()` to seed a fresh editor. Hand back a
* Scene-shaped view of a fresh blank project's first page. */
export function createBlankScene(): Scene {
const project = createBlankProject()
const page = project.scenes[0]
return {
version: 1,
canvas: project.canvas,
duration: project.duration,
fps: project.fps,
inTime: project.inTime,
outTime: project.outTime,
assets: project.assets,
clips: project.clips,
layers: page.layers,
clipInstances: page.clipInstances,
clipTracks: page.clipTracks,
scriptTracks: page.scriptTracks,
scriptFrames: page.scriptFrames,
markers: page.markers,
groups: page.groups,
trackOrder: page.trackOrder,
backgroundVideo: page.backgroundVideo,
}
}
export const useSceneStore = defineStore('scene', {
state: (): State => ({
scene: createBlankScene(),
project: createBlankProject(),
selectedLayerId: null,
selectedLayerIds: [],
selectedClipInstanceId: null,
@@ -285,14 +383,48 @@ export const useSceneStore = defineStore('scene', {
drawingLayerId: null,
}),
getters: {
activeClip(state): Clip | null {
if (!state.activeClipId) return null
const clips = state.scene.clips ?? []
return clips.find((c) => c.id === state.activeClipId) ?? null
/** The active page from `project.scenes`. Use this when you need to
* mutate per-page fields (e.g., reassign `activeScene.layers`). */
activeScene(state): ScenePage {
const p = state.project
return p.scenes.find((s) => s.id === p.activeSceneId) ?? p.scenes[0]
},
activeButtonAsset(state): ButtonAsset | null {
if (!state.activeButtonAssetId) return null
const asset = state.scene.assets.find((a) => a.id === state.activeButtonAssetId)
/** Merged Scene-shaped view: shared project fields combined with the
* active page's arrays. Read-only by convention — array references
* are live (so `store.scene.layers.push(x)` still hits the active
* page) but assigning a primitive (`store.scene.duration = 5`) on
* this view does NOT reach the project. Mutate via store actions
* (setDuration, setFps, …) instead. */
scene(): Scene {
const p = this.project
const page = this.activeScene
return {
version: 1,
canvas: p.canvas,
duration: p.duration,
fps: p.fps,
inTime: p.inTime,
outTime: p.outTime,
assets: p.assets,
clips: p.clips,
layers: page.layers,
clipInstances: page.clipInstances,
clipTracks: page.clipTracks,
scriptTracks: page.scriptTracks,
scriptFrames: page.scriptFrames,
markers: page.markers,
groups: page.groups,
trackOrder: page.trackOrder,
backgroundVideo: page.backgroundVideo,
}
},
activeClip(): Clip | null {
if (!this.activeClipId) return null
return this.project.clips.find((c) => c.id === this.activeClipId) ?? null
},
activeButtonAsset(): ButtonAsset | null {
if (!this.activeButtonAssetId) return null
const asset = this.project.assets.find((a) => a.id === this.activeButtonAssetId)
return asset && asset.kind === 'button' ? asset : null
},
activeLayers(): Layer[] {
@@ -429,8 +561,8 @@ export const useSceneStore = defineStore('scene', {
const layers = this.activeLayers
const idx = layers.findIndex((l) => l.id === id)
if (idx >= 0) layers.splice(idx, 1)
if (!this.activeClipId && this.scene.trackOrder) {
this.scene.trackOrder = this.scene.trackOrder.filter((x) => x !== id)
if (!this.activeClipId) {
this.activeScene.trackOrder = this.activeScene.trackOrder.filter((x) => x !== id)
}
if (this.selectedLayerId === id) {
this.selectedLayerId = null
@@ -500,41 +632,41 @@ export const useSceneStore = defineStore('scene', {
clip.duration = Math.max(0.1, seconds)
if (this.playhead > clip.duration) this.playhead = clip.duration
} else {
this.scene.duration = Math.max(0.1, seconds)
if (this.playhead > this.scene.duration) this.playhead = this.scene.duration
this.project.duration = Math.max(0.1, seconds)
if (this.playhead > this.project.duration) this.playhead = this.project.duration
}
},
setFps(fps: number) {
this.scene.fps = Math.max(1, Math.round(fps))
this.project.fps = Math.max(1, Math.round(fps))
},
setCanvasSize(width: number, height: number) {
this.scene.canvas.width = Math.max(1, Math.round(width))
this.scene.canvas.height = Math.max(1, Math.round(height))
this.project.canvas.width = Math.max(1, Math.round(width))
this.project.canvas.height = Math.max(1, Math.round(height))
},
setCanvasBackground(color: string) {
this.scene.canvas.background = color
this.project.canvas.background = color
},
setBackgroundVideo(video: BackgroundVideo | null) {
if (video && video.src) this.scene.backgroundVideo = { ...video }
else delete this.scene.backgroundVideo
if (video && video.src) this.activeScene.backgroundVideo = { ...video }
else delete this.activeScene.backgroundVideo
},
setPlaybackRange(inTime: number | null, outTime: number | null) {
const dur = this.scene.duration
const dur = this.project.duration
const hasIn = inTime !== null && inTime !== undefined
const hasOut = outTime !== null && outTime !== undefined
if (hasIn) {
const v = Math.max(0, Math.min(dur, inTime!))
if (v === 0 && this.scene.outTime === undefined) delete this.scene.inTime
else this.scene.inTime = v
if (v === 0 && this.project.outTime === undefined) delete this.project.inTime
else this.project.inTime = v
}
if (hasOut) {
const v = Math.max(this.scene.inTime ?? 0, Math.min(dur, outTime!))
if (v === dur && this.scene.inTime === undefined) delete this.scene.outTime
else this.scene.outTime = v
const v = Math.max(this.project.inTime ?? 0, Math.min(dur, outTime!))
if (v === dur && this.project.inTime === undefined) delete this.project.outTime
else this.project.outTime = v
}
// Re-clamp playhead into the new range
const lo = this.scene.inTime ?? 0
const hi = this.scene.outTime ?? dur
const lo = this.project.inTime ?? 0
const hi = this.project.outTime ?? dur
if (this.playhead < lo) this.playhead = lo
else if (this.playhead > hi) this.playhead = hi
},
@@ -1043,7 +1175,7 @@ export const useSceneStore = defineStore('scene', {
if (!asset || asset.kind !== 'polygon') return
if (asset.points.length < 2) {
this.removeLayer(layerId)
this.scene.assets = this.scene.assets.filter((a) => a.id !== assetId)
this.project.assets = this.project.assets.filter((a) => a.id !== assetId)
}
},
cancelDrawing() {
@@ -1053,7 +1185,7 @@ export const useSceneStore = defineStore('scene', {
this.drawingAssetId = null
this.drawingLayerId = null
if (layerId) this.removeLayer(layerId)
if (assetId) this.scene.assets = this.scene.assets.filter((a) => a.id !== assetId)
if (assetId) this.project.assets = this.project.assets.filter((a) => a.id !== assetId)
},
setPolygonVertexType(
assetId: string,
@@ -1361,8 +1493,7 @@ export const useSceneStore = defineStore('scene', {
return m
},
removeMarker(id: string) {
if (!this.scene.markers) return
this.scene.markers = this.scene.markers.filter((m) => m.id !== id)
this.activeScene.markers = this.activeScene.markers.filter((m) => m.id !== id)
},
renameMarker(id: string, name: string): boolean {
const markers = this.scene.markers
@@ -1398,15 +1529,10 @@ export const useSceneStore = defineStore('scene', {
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)
}
const page = this.activeScene
page.scriptTracks = page.scriptTracks.filter((t) => t.id !== id)
page.scriptFrames = page.scriptFrames.filter((f) => f.trackId !== id)
page.trackOrder = page.trackOrder.filter((tid) => tid !== id)
},
setScriptTrackName(id: string, name: string) {
const track = this.scene.scriptTracks?.find((t) => t.id === id)
@@ -1433,8 +1559,7 @@ export const useSceneStore = defineStore('scene', {
return frame
},
removeScriptFrame(id: string) {
if (!this.scene.scriptFrames) return
this.scene.scriptFrames = this.scene.scriptFrames.filter((f) => f.id !== id)
this.activeScene.scriptFrames = this.activeScene.scriptFrames.filter((f) => f.id !== id)
},
setScriptFrameActions(id: string, actions: Action[]) {
const frame = this.scene.scriptFrames?.find((f) => f.id === id)
@@ -1627,17 +1752,155 @@ export const useSceneStore = defineStore('scene', {
layer.separated = false
}
},
loadScene(scene: Scene) {
this.scene = normalizeScene(scene)
/** Replace the entire project. Resets editor context (selection,
* playback, clip / button-edit drill-down). */
loadProject(project: Project) {
this.project = normalizeProject(project)
this.selectedLayerId = null
this.selectedLayerIds = []
this.selectedClipInstanceId = null
this.playhead = 0
this.playing = false
this.activeClipId = null
this.activeButtonAssetId = null
this.activeButtonEditMode = null
this.activeButtonState = null
this.drawingMode = null
this.drawingAssetId = null
this.drawingLayerId = null
},
/** Backward-compat shim: legacy callers (older project files, recents
* stored as Scene, tests) still pass a Scene. Wrap into a one-page
* Project and delegate. */
loadScene(scene: Scene) {
this.loadProject(projectFromLegacyScene(scene))
},
/** Replace the project data only — for use by the undo/redo layer.
* Unlike `loadProject`, transient editor state (playhead, selection,
* activeClipId, activeButtonAssetId, drawing) is left untouched so
* undoing a content change doesn't kick the user out of clip-edit
* or reset where they were scrubbing. The caller is responsible for
* pruning any selection that now points at a vanished id. */
applyProjectSnapshot(project: Project) {
this.project = normalizeProject(project)
},
// -------- Scene pages ----------------------------------------------
/** Append a fresh, empty page and activate it. The default name is
* "Scene N" with N counting up from the current page count. */
addScene(name?: string): ScenePage {
const final = name?.trim() || `Scene ${this.project.scenes.length + 1}`
const page = createBlankScenePage(final)
this.project.scenes.push(page)
this.switchScene(page.id)
return page
},
/** Activate a page by id. Clears any drill-down state (clip-edit,
* button-edit, selections, drawing) so the new page isn't shown
* through the lens of a context that belongs to another page. */
switchScene(id: string): boolean {
if (!this.project.scenes.find((s) => s.id === id)) return false
this.project.activeSceneId = id
this.activeClipId = null
this.activeButtonAssetId = null
this.activeButtonEditMode = null
this.activeButtonState = null
this.selectedLayerId = null
this.selectedLayerIds = []
this.selectedClipInstanceId = null
this.drawingMode = null
this.drawingAssetId = null
this.drawingLayerId = null
this.playing = false
return true
},
renameScene(id: string, name: string): boolean {
const page = this.project.scenes.find((s) => s.id === id)
if (!page) return false
const next = name.trim()
if (!next) return false
page.name = next
return true
},
/** Remove a page. Refuses to delete the last remaining page (a
* project must always have at least one). If the active page is
* removed, activates the neighbour. */
removeScene(id: string): boolean {
const scenes = this.project.scenes
if (scenes.length <= 1) return false
const idx = scenes.findIndex((s) => s.id === id)
if (idx < 0) return false
const wasActive = this.project.activeSceneId === id
scenes.splice(idx, 1)
if (wasActive) {
const neighbour = scenes[Math.min(idx, scenes.length - 1)]
this.switchScene(neighbour.id)
}
return true
},
/** Deep-clone the named page with fresh ids on every layer, keyframe,
* clip-instance, script-frame, marker, group, and trackOrder entry.
* Inserted directly after the original; activated. */
duplicateScene(id: string): ScenePage | null {
const orig = this.project.scenes.find((s) => s.id === id)
if (!orig) return null
const cloned = structuredClone(toRaw(orig)) as ScenePage
// Build a complete id-remap (every layer, group, clipTrack, scriptTrack)
// so trackOrder/group childIds/scriptFrames stay internally consistent.
const remap = new Map<string, string>()
const remapId = (oldId: string) => {
let next = remap.get(oldId)
if (!next) {
next = uid()
remap.set(oldId, next)
}
return next
}
cloned.id = uid()
cloned.name = `${orig.name} copy`
for (const l of cloned.layers) {
l.id = remapId(l.id)
l.keyframes = l.keyframes.map((k) => ({ ...k, id: uid() }))
if (l.positionKeyframes) {
l.positionKeyframes = l.positionKeyframes.map((k) => ({ ...k, id: uid() }))
}
if (l.rotationKeyframes) {
l.rotationKeyframes = l.rotationKeyframes.map((k) => ({ ...k, id: uid() }))
}
}
for (const t of cloned.clipTracks) t.id = remapId(t.id)
for (const st of cloned.scriptTracks) st.id = remapId(st.id)
for (const g of cloned.groups) {
g.id = remapId(g.id)
g.childIds = g.childIds.map((cid) => remap.get(cid) ?? cid)
}
cloned.trackOrder = cloned.trackOrder.map((oid) => remap.get(oid) ?? oid)
cloned.clipInstances = cloned.clipInstances.map((ci) => ({
...ci,
id: uid(),
clipTrackId: remap.get(ci.clipTrackId) ?? ci.clipTrackId,
}))
cloned.scriptFrames = cloned.scriptFrames.map((f) => ({
...f,
id: uid(),
trackId: remap.get(f.trackId) ?? f.trackId,
}))
cloned.markers = cloned.markers.map((m) => ({ ...m, id: uid() }))
const idx = this.project.scenes.findIndex((s) => s.id === id)
this.project.scenes.splice(idx + 1, 0, cloned)
this.switchScene(cloned.id)
return cloned
},
/** Remove an asset from the project library. Layers that referenced
* it remain — they'll just render as nothing. Use this from UI
* components instead of mutating `project.assets` inline. */
removeAsset(id: string) {
this.project.assets = this.project.assets.filter((a) => a.id !== id)
},
addClip(name?: string): Clip {
const clip: Clip = {
id: uid(),
@@ -1702,9 +1965,10 @@ export const useSceneStore = defineStore('scene', {
// group they were part of. Clearing in one pass keeps Pinia's
// reactivity from thrashing on intermediate states.
const idSet = new Set(ids)
this.scene.layers = this.scene.layers.filter((l) => !idSet.has(l.id))
this.scene.trackOrder = order.filter((x) => !idSet.has(x))
for (const g of this.scene.groups ?? []) {
const page = this.activeScene
page.layers = page.layers.filter((l) => !idSet.has(l.id))
page.trackOrder = order.filter((x) => !idSet.has(x))
for (const g of page.groups) {
g.childIds = g.childIds.filter((x) => !idSet.has(x))
}
@@ -1733,16 +1997,17 @@ export const useSceneStore = defineStore('scene', {
return clip.id
},
removeClip(id: string) {
if (this.scene.clips) {
this.scene.clips = this.scene.clips.filter((c) => c.id !== id)
}
if (this.scene.clipInstances) {
this.scene.clipInstances = this.scene.clipInstances.filter((ci) => ci.clipId !== id)
this.project.clips = this.project.clips.filter((c) => c.id !== id)
// Clip-instances of the deleted clip live on every scene page that
// happened to place it — purge from all of them so we don't leave
// dangling references in non-active pages.
for (const page of this.project.scenes) {
page.clipInstances = page.clipInstances.filter((ci) => ci.clipId !== id)
}
if (this.activeClipId === id) {
this.activeClipId = null
this.selectedLayerId = null
this.selectedLayerIds = []
this.selectedLayerIds = []
this.playhead = 0
}
},
@@ -1851,15 +2116,10 @@ export const useSceneStore = defineStore('scene', {
return null
},
removeClipTrack(id: string) {
if (this.scene.clipTracks) {
this.scene.clipTracks = this.scene.clipTracks.filter((t) => t.id !== id)
}
if (this.scene.clipInstances) {
this.scene.clipInstances = this.scene.clipInstances.filter((ci) => ci.clipTrackId !== id)
}
if (this.scene.trackOrder) {
this.scene.trackOrder = this.scene.trackOrder.filter((x) => x !== id)
}
const page = this.activeScene
page.clipTracks = page.clipTracks.filter((t) => t.id !== id)
page.clipInstances = page.clipInstances.filter((ci) => ci.clipTrackId !== id)
page.trackOrder = page.trackOrder.filter((x) => x !== id)
},
addClipInstance(clipId: string, startTime: number, clipTrackId: string): ClipInstance | null {
const clip = this.scene.clips?.find((c) => c.id === clipId)
@@ -1914,8 +2174,7 @@ export const useSceneStore = defineStore('scene', {
inst.startTime = Math.max(0, startTime)
},
removeClipInstance(id: string) {
if (!this.scene.clipInstances) return
this.scene.clipInstances = this.scene.clipInstances.filter((ci) => ci.id !== id)
this.activeScene.clipInstances = this.activeScene.clipInstances.filter((ci) => ci.id !== id)
if (this.selectedClipInstanceId === id) this.selectedClipInstanceId = null
},
/** Update the per-instance x / y / scaleX / scaleY. Missing fields
@@ -2020,10 +2279,10 @@ export const useSceneStore = defineStore('scene', {
removeTrackGroup(id: string) {
const group = this.scene.groups?.find((g) => g.id === id)
if (!group) return
const order = this.scene.trackOrder ?? []
const order = this.activeScene.trackOrder
const idx = order.indexOf(id)
if (idx >= 0) order.splice(idx, 1, ...group.childIds)
this.scene.groups = (this.scene.groups ?? []).filter((g) => g.id !== id)
this.activeScene.groups = this.activeScene.groups.filter((g) => g.id !== id)
},
renameGroup(id: string, name: string) {
const group = this.scene.groups?.find((g) => g.id === id)