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

+1
View File
@@ -23,6 +23,7 @@ export const ASSET_GROUPS: Record<AssetType, { name: string; category: AssetCate
pixelart: { name: 'Pixel art', category: 'Visuals' },
palette: { name: 'Palette', category: 'Visuals' },
animation: { name: 'Animation', category: 'Visuals' },
composition: { name: 'Composition', category: 'Visuals' },
tile: { name: 'Tile', category: 'Visuals' },
tilemap: { name: 'Tilemap', category: 'Visuals' },
shader: { name: 'Shader', category: 'Visuals' },
+2
View File
@@ -9,6 +9,7 @@ import bus from './icons/bus.svg?raw';
import camera from './icons/camera.svg?raw';
import clip from './icons/clip.svg?raw';
import collider from './icons/collider.svg?raw';
import composition from './icons/composition.svg?raw';
import container from './icons/container.svg?raw';
import curve from './icons/curve.svg?raw';
import font from './icons/font.svg?raw';
@@ -48,6 +49,7 @@ export const ASSET_ICON_SVG: Record<AssetType, string> = {
camera,
clip,
collider,
composition,
container,
curve,
font,
+9
View File
@@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-8 -8 16 16" width="16" height="16">
<!-- A cut on two tracks: items in lanes, and the playhead running through a gap between them. -->
<rect x="-7" y="-4" width="5.75" height="3.5" rx="0.6" fill="currentColor" />
<rect x="1.25" y="-4" width="5.75" height="3.5" rx="0.6" fill="currentColor" />
<rect x="-7" y="1.5" width="5.75" height="3.5" rx="0.6" fill="currentColor" fill-opacity="0.6" />
<rect x="1.25" y="1.5" width="4" height="3.5" rx="0.6" fill="currentColor" fill-opacity="0.6" />
<path d="M0 -5.5 V7" fill="none" stroke="currentColor" stroke-width="1" />
<path d="M-1.75 -7.5 H1.75 L0 -5.25 Z" fill="currentColor" />
</svg>

After

Width:  |  Height:  |  Size: 696 B