/** * What the Animation tab shows above the timeline (#99): the root asset with every row's value at * the playhead written over it. Nothing here touches the document – the holder returned is built * fresh on every call from copies, and the preview draws it the way the Scene tab draws a scene. */ import { animatableDefs, animatedKindOf, frameCount, isPositionRow, PLACED_ROOT_ID, placedRoot, playbackOf, trackWriteAt, } from './animation'; import type { AnimationAsset, AnimationTrack, Asset } from './Asset'; import { instanceChildId, objectRef } from './instance'; import { findAsset, type ObjectHolder, type Project } from './Project'; import { resolveSceneObjectProps } from '../properties/resolve'; /** * The holder the preview draws at `frame`: a copy of the scene for a scene root, otherwise a holder * of one object placing the root at the origin, its content resolved from the template like any * placed container's. A row reaches an object stored in the holder directly and one resolved from a * container through its instance's edits – the same route an edit in the Scene tab takes, so a row * aimed into a placed container needs nothing of its own (#109). `undefined` without a root. Past * the end of a `once-reset` pass nothing is written: the player has put the starting values back. * `reverse` is ping-pong's way back, as `playheadAt` reports it. */ export function previewHolder( project: Project, animation: AnimationAsset, frame: number, reverse = false, ): ObjectHolder | undefined { const root = findAsset(project, animation.root); if (!root) return undefined; const reset = playbackOf(animation) === 'once-reset' && frame >= frameCount(animation); const holder = root.type === 'scene' ? sceneCopy(root) : placedHolder(root); for (const track of reset ? [] : animation.tracks) { if (root.type === 'scene' && track.target === '') writeScene(holder, track, frame, reverse); else writeObject(project, holder, previewObjectId(root, track.target), track, frame, reverse); } return holder; } /** The id the preview draws a row's target under – for a check that wants to look at it. */ export function previewObjectId(root: Asset | undefined, target: string): string { if (root?.type === 'scene') return target; return target === '' ? PLACED_ROOT_ID : instanceChildId(PLACED_ROOT_ID, target); } /** Deep enough that nothing a row writes – a position, an override, an instance edit – is the document's. */ function sceneCopy(scene: ObjectHolder): ObjectHolder { return { ...scene, props: { ...scene.props }, objects: scene.objects.map((o) => ({ ...o, position: { ...o.position }, overrides: { ...o.overrides }, instanceEdits: structuredClone(o.instanceEdits), })), }; } function placedHolder(root: Asset): ObjectHolder { return { id: PLACED_ROOT_ID, type: 'container', name: root.name, props: {}, objects: [placedRoot(root)] }; } /** A row on the scene itself: its value goes into the copy's own properties. */ function writeScene(holder: ObjectHolder, track: AnimationTrack, frame: number, reverse: boolean): void { const def = animatableDefs('scene').find((d) => d.key === track.property); if (!def) return; const value = trackWriteAt(track, def.kind, frame, holder.props[track.property] ?? def.default, reverse); if (value !== undefined) holder.props[track.property] = value; } function writeObject( project: Project, holder: ObjectHolder, id: string, track: AnimationTrack, frame: number, reverse: boolean, ): void { const ref = objectRef(project, holder, id); const asset = ref && findAsset(project, ref.object.assetId); if (!ref || !asset) return; // Resolved, so a shader worn through an override or an instance edit brings its parameters (#104). const props = resolveSceneObjectProps(project, asset, ref.object); const kind = animatedKindOf(project, asset, props, track.property); if (!kind) return; const key = track.property; if (isPositionRow(asset.type, key)) { const value = trackWriteAt(track, kind, frame, ref.object.position[key], reverse); if (value !== undefined) ref.setPosition({ ...ref.object.position, [key]: Number(value) }); return; } const value = trackWriteAt(track, kind, frame, props[key], reverse); if (value !== undefined) ref.setOverride(key, value); }