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>
This commit is contained in:
adminandClaude Opus 5.5 committed 2026-10-12 06:36:30 +02:00
1 parent 597367c804
commit 73e675835e
14 files changed
+1031 -12

No files matched your search

+16
View File
@@ -23,6 +23,7 @@ import {
} from './kinds';
import { findAssetOf, type Project } from '../model/Project';
import { ALL_LAYERS, COLLIDES_WITH_KEY, COLLISION_LAYER_KEY } from '../model/collisionLayers';
import { COMPOSITION_FPS_KEY, COMPOSITION_LOOP_KEY, DEFAULT_COMPOSITION_FPS } from './compositions';
import { INHERIT_LAYER, OVERLAY_KEY, RENDER_LAYER_KEY, SEES_KEY } from '../model/renderLayers';
import {
TILE_BOUNCINESS_KEY,
@@ -597,6 +598,21 @@ const ASSET_SCHEMAS: Record<AssetType, PropertySchema> = {
],
},
],
// Tracks, items and clips are intrinsic data on the asset (see CompositionAsset). Whole frames
// per second, so a cut lands on a frame.
composition: [
{
key: COMPOSITION_FPS_KEY,
label: 'Frames per second',
kind: 'number',
default: DEFAULT_COMPOSITION_FPS,
min: 1,
max: 120,
step: 1,
decimals: 0,
},
{ key: COMPOSITION_LOOP_KEY, label: 'Loop', kind: 'boolean', default: false },
],
// The pictures are intrinsic data on the asset (see TileAsset); these three say what the
// block holds and how its steps run. There is no tile size: the block's width in tiles is known
// from the kind, so the picture already says it (see `readTile`). A new tile is a plain one; the