Files
feuerwerk/src/core/model/animationPreview.ts
T
adminandClaude Opus 5.5 73e675835e Add the composition asset with its tracks, items and clips (#417)
A composition is a timeline of scene objects for cuts and cutscenes.
This first step brings the model and the pure evaluation behind it.
The tab and placing it in a scene follow in the next steps.

- CompositionAsset holds objects, tracks, items and clips. Each item
  owns exactly one object with overrides and container edits.
- The composition is a third object holder next to scene and
  container. Delete cascade, undo, usages and references work with it.
- Duplicating gives the copy new object ids and moves the item ids
  along with them.
- compositionAt() tells which items live at a frame. The top track is
  drawn in front.
- A clip acts on the item of its own track that it overlaps. After its
  end it holds the last value until the item ends. The animation's
  play mode is ignored, a clip is one pass.
- Clip targets are found by template path, else by name path, like
  the runtime's animation system.
- The root sprite of an item plays its video driven at offset plus
  time. Children follow unless they are already driven by hand.
- fittedLength() gives the video or clip length minus the offset, or
  3 s without media.
- fps (60) and loop live in their own properties file. Keeping them in
  PropertySchema would close an import cycle.
- The type is listed under Visuals with its own icon. It is not
  placeable yet.

Tests: the gate passed with 300 files and 4625 tests. That is 25 new
tests in tests/core/composition.test.ts and 2 icon checks that run
for every icon. Four counter-checks (track order, overlap rule, child
videos, ids on duplicate) each turned exactly their test red. e2e 04
passed with 67 checks and now counts 33 asset types.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 06:36:30 +02:00

113 lines
4.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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,
animatedDefOf,
frameCount,
isPositionRow,
PLACED_ROOT_ID,
placedRoot,
playbackOf,
trackWriteAt,
} from './animation';
import type { AnimationAsset, AnimationTrack, Asset } from './Asset';
import { type CurveLookup, curveLookup } from './curve';
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);
const at: ReadAt = { frame, reverse, curves: curveLookup(project) };
for (const track of reset ? [] : animation.tracks) {
if (root.type === 'scene' && track.target === '') writeScene(holder, track, at);
else writeObjectRow(project, holder, previewObjectId(root, track.target), track, at);
}
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)] };
}
/** Where on the timeline a row is read, and the Curve assets its keys may blend along. */
export interface ReadAt {
frame: number;
reverse: boolean;
curves: CurveLookup;
}
/** A row on the scene itself: its value goes into the copy's own properties. */
function writeScene(holder: ObjectHolder, track: AnimationTrack, at: ReadAt): void {
const def = animatableDefs('scene').find((d) => d.key === track.property);
if (!def) return;
const base = holder.props[track.property] ?? def.default;
const value = trackWriteAt(track, def, at.frame, base, at.reverse, at.curves);
if (value !== undefined) holder.props[track.property] = value;
}
/** A row on the object `id` of the holder, stored there or resolved from a container – also a composition's (#417). */
export function writeObjectRow(
project: Project,
holder: ObjectHolder,
id: string,
track: AnimationTrack,
at: ReadAt,
): 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 def = animatedDefOf(project, asset, props, track.property);
if (!def) return;
const key = track.property;
if (isPositionRow(asset.type, key)) {
const value = trackWriteAt(track, def, at.frame, ref.object.position[key], at.reverse, at.curves);
if (value !== undefined) ref.setPosition({ ...ref.object.position, [key]: Number(value) });
return;
}
const value = trackWriteAt(track, def, at.frame, props[key], at.reverse, at.curves);
if (value !== undefined) ref.setOverride(key, value);
}