/** * The Animation tab's preview (#99): the root with every row's value at the playhead written over * a copy of it, drawn like a scene and never written back. */ import { describe, expect, it } from 'vitest'; import { AddAnimationTrackCommand, SetAnimationKeyCommand, SetAnimationRootCommand, SetTrackRelativeCommand, } from '../../src/core/commands/animationCommands'; import { CreateAssetCommand } from '../../src/core/commands/assetCommands'; import { CreateContainerCommand } from '../../src/core/commands/containerCommands'; import { RemoveSceneObjectsCommand } from '../../src/core/commands/sceneObjectCommands'; import { PREVIEW_ROOT_ID, previewHolder, previewObjectId } from '../../src/core/model/animationPreview'; import type { AnimationAsset, AnimationKey, ContainerAsset } from '../../src/core/model/Asset'; import { instanceChildId, resolveObjects } from '../../src/core/model/instance'; import { findAsset, findAssetOf } from '../../src/core/model/Project'; import { resolveSceneObjectProps } from '../../src/core/properties/resolve'; import { collectHolderItems } from '../../src/viewport/items'; import { makeSpriteScene, place, type SceneFixture } from '../helpers'; function addAnimation(f: SceneFixture, root: string, props = {}): AnimationAsset { const create = new CreateAssetCommand('animation', 'Walk', props); f.stack.execute(create); f.stack.execute(new SetAnimationRootCommand(create.asset.id, root)); return findAssetOf(f.project, create.asset.id, 'animation')!; } function addRow( f: SceneFixture, animation: AnimationAsset, property: string, pairs: [number, AnimationKey['value']][], target = '', ): string { const row = new AddAnimationTrackCommand(animation.id, target, property); f.stack.execute(row); for (const [frame, value] of pairs) f.stack.execute(new SetAnimationKeyCommand(animation.id, row.trackId, frame, value)); return row.trackId; } const current = (f: SceneFixture, id: string) => findAssetOf(f.project, id, 'animation')!; /** The objects the preview would draw, by id – resolved like the viewport resolves them. */ function drawn(f: SceneFixture, animation: AnimationAsset, frame: number) { const holder = previewHolder(f.project, animation, frame)!; return new Map(resolveObjects(f.project, holder).map((o) => [o.id, o])); } describe('the preview of an animation over a sprite', () => { it('places the root at the origin and writes a row of the root onto that object', () => { const f = makeSpriteScene(); const walk = addAnimation(f, f.spriteId); addRow(f, walk, 'x', [ [0, 0], [10, 100], ]); const root = drawn(f, current(f, walk.id), 5).get(PREVIEW_ROOT_ID)!; expect(root.assetId).toBe(f.spriteId); expect(root.position).toEqual({ x: 50, y: 0, z: 0 }); expect(previewObjectId(findAsset(f.project, f.spriteId), '')).toBe(PREVIEW_ROOT_ID); }); it('holds the first value before the first key and the last one after the last', () => { const f = makeSpriteScene(); const walk = addAnimation(f, f.spriteId); addRow(f, walk, 'rotation', [ [2, 10], [4, 30], ]); const at = (frame: number) => drawn(f, current(f, walk.id), frame).get(PREVIEW_ROOT_ID)!.overrides.rotation; expect(at(0)).toBe(10); expect(at(3)).toBe(20); expect(at(9)).toBe(30); }); it('draws the values through the viewport like any placed object', () => { const f = makeSpriteScene(); const walk = addAnimation(f, f.spriteId); addRow(f, walk, 'y', [[0, 40]]); const holder = previewHolder(f.project, current(f, walk.id), 0); const { items } = collectHolderItems(f.project, holder, 0); expect(items.map((i) => [i.id, i.type, i.y])).toEqual([[PREVIEW_ROOT_ID, 'sprite', 40]]); }); it('never writes into the document', () => { const f = makeSpriteScene(); const walk = addAnimation(f, f.spriteId); addRow(f, walk, 'x', [[0, 77]]); const before = JSON.stringify(f.project); previewHolder(f.project, current(f, walk.id), 0); expect(JSON.stringify(f.project)).toBe(before); }); it('shows the starting values once a once-reset pass is over, and the last ones for once', () => { const f = makeSpriteScene(); const reset = addAnimation(f, f.spriteId, { playback: 'once-reset' }); addRow(f, reset, 'x', [ [0, 0], [9, 90], ]); expect(drawn(f, current(f, reset.id), 9).get(PREVIEW_ROOT_ID)!.position.x).toBe(90); expect(drawn(f, current(f, reset.id), 10).get(PREVIEW_ROOT_ID)!.position.x).toBe(0); const hold = addAnimation(f, f.spriteId, { playback: 'once' }); addRow(f, hold, 'x', [ [0, 0], [9, 90], ]); expect(drawn(f, current(f, hold.id), 10).get(PREVIEW_ROOT_ID)!.position.x).toBe(90); }); }); describe('the preview of an animation over a container', () => { /** A container packed around one placed sprite; `at` is where the template keeps it. */ function withChild() { const f = makeSpriteScene(); const inner = place(f, f.spriteId, { x: 10, y: 20 }); const wrap = new CreateContainerCommand(f.sceneId, [inner]); f.stack.execute(wrap); const container = findAsset(f.project, wrap.assetId) as ContainerAsset; const template = container.objects[0]!; return { f, container, arm: template.id, at: { ...template.position } }; } it('reaches a child through the placed root, under the id a placed container gives it', () => { const { f, container, arm, at } = withChild(); const walk = addAnimation(f, container.id); addRow(f, walk, 'x', [[0, 55]], arm); addRow(f, walk, 'rotation', [[0, 45]]); const objects = drawn(f, current(f, walk.id), 0); const childId = instanceChildId(PREVIEW_ROOT_ID, arm); expect(previewObjectId(findAsset(f.project, container.id), arm)).toBe(childId); expect(objects.get(childId)!.position).toEqual({ ...at, x: 55 }); expect(objects.get(PREVIEW_ROOT_ID)!.overrides.rotation).toBe(45); }); it('adds a relative row to what the child holds in the template', () => { const { f, container, arm, at } = withChild(); const walk = addAnimation(f, container.id); const row = addRow(f, walk, 'x', [[0, 5]], arm); f.stack.execute(new SetTrackRelativeCommand(walk.id, row, true)); const child = drawn(f, current(f, walk.id), 0).get(instanceChildId(PREVIEW_ROOT_ID, arm))!; expect(child.position.x).toBe(at.x + 5); }); it('skips a row whose target is gone, and keeps the rest', () => { const f = makeSpriteScene(); const wrap = new CreateContainerCommand(f.sceneId, [ place(f, f.spriteId, { x: 10, y: 20 }), place(f, f.spriteId, { x: 0, y: 0 }), ]); f.stack.execute(wrap); const [arm, other] = (findAsset(f.project, wrap.assetId) as ContainerAsset).objects.map((o) => o.id); const walk = addAnimation(f, wrap.assetId); addRow(f, walk, 'x', [[0, 55]], arm); addRow(f, walk, 'x', [[0, 66]], other); // The first template object goes: its row stays and aims at nothing. f.stack.execute(new RemoveSceneObjectsCommand(wrap.assetId, [arm!])); const objects = drawn(f, current(f, walk.id), 0); expect(objects.has(instanceChildId(PREVIEW_ROOT_ID, arm!))).toBe(false); expect(objects.get(instanceChildId(PREVIEW_ROOT_ID, other!))!.position.x).toBe(66); }); }); describe('the preview of an animation over a scene', () => { it('is the scene itself, with the rows written onto copies of its objects', () => { const f = makeSpriteScene(); const box = place(f, f.spriteId, { x: 1, y: 2 }); const walk = addAnimation(f, f.sceneId); addRow(f, walk, 'y', [[0, 200]], box); addRow(f, walk, 'color', [[0, 0xff0000]], box); const holder = previewHolder(f.project, current(f, walk.id), 0)!; expect(holder.id).toBe(f.sceneId); expect(previewObjectId(findAsset(f.project, f.sceneId), box)).toBe(box); const object = holder.objects.find((o) => o.id === box)!; expect(object.position.y).toBe(200); expect(resolveSceneObjectProps(f.project, findAsset(f.project, f.spriteId)!, object).color).toBe( 0xff0000, ); expect(f.sceneOf().objects.find((o) => o.id === box)!.position.y).toBe(2); }); });