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:
1 parent
597367c804
commit
73e675835e
14 files changed
+1031
-12
No files matched your search
@@ -36,6 +36,7 @@ import type {
|
||||
AnimationTrack,
|
||||
Asset,
|
||||
BehaviorAsset,
|
||||
CompositionAsset,
|
||||
ContainerAsset,
|
||||
PictureCut,
|
||||
PixelArtAsset,
|
||||
@@ -72,7 +73,8 @@ import { isSoundFile } from '../model/sound';
|
||||
import { isVideoFile } from '../model/video';
|
||||
import { emptyEdit, type InstanceEdit, isEmptyEdit } from '../model/instance';
|
||||
import { type PropertyBag, type PropertyValue, tagOf, type Vec2 } from '../model/Properties';
|
||||
import { PROJECT_FORMAT_VERSION, type Project } from '../model/Project';
|
||||
import { type ObjectHolder, PROJECT_FORMAT_VERSION, type Project } from '../model/Project';
|
||||
import { heldCompositionClips, heldCompositionItems, heldCompositionTracks } from '../model/composition';
|
||||
import { readScope, readSharedScope } from '../model/Scope';
|
||||
import type { ObjectGroup, SceneObject } from '../model/SceneObject';
|
||||
import {
|
||||
@@ -205,6 +207,24 @@ export function deserializeProject(raw: unknown, fallbackId = ''): Project {
|
||||
};
|
||||
normalizeGroups(holder, a.groups);
|
||||
assets.push(holder);
|
||||
} else if (type === 'composition') {
|
||||
// One object per item, all at the top level; an object no item spawns is dropped with it.
|
||||
const objects = normalizeObjects(a.objects, schemaOf).map((o) => ({ ...o, parentId: '' }));
|
||||
const tracks = heldCompositionTracks(a.tracks);
|
||||
const items = heldCompositionItems(a.items, tracks, objects);
|
||||
const spawned = new Set(items.map((i) => i.id));
|
||||
const composition: CompositionAsset = {
|
||||
id,
|
||||
type,
|
||||
name,
|
||||
props,
|
||||
objects: objects.filter((o) => spawned.has(o.id)),
|
||||
tracks,
|
||||
items,
|
||||
clips: heldCompositionClips(a.clips, tracks),
|
||||
};
|
||||
normalizeGroups(composition, a.groups);
|
||||
assets.push(composition);
|
||||
} else if (type === 'behavior') {
|
||||
const declared = variables.get(id) ?? [];
|
||||
const behavior: BehaviorAsset = {
|
||||
@@ -447,7 +467,7 @@ function normalizeObjects(
|
||||
* Hierarchy folders (#241): only those with a member are kept, and a member is a top-level object
|
||||
* naming one of them – anything else loses its `groupId`.
|
||||
*/
|
||||
function normalizeGroups(holder: SceneAsset | ContainerAsset, raw: unknown): void {
|
||||
function normalizeGroups(holder: ObjectHolder, raw: unknown): void {
|
||||
const groups = new Map<string, ObjectGroup>();
|
||||
for (const item of Array.isArray(raw) ? raw : []) {
|
||||
const g = asRecord(item);
|
||||
|
||||
Reference in new issue
Block a user