A clip of a video could not be laid on a composition track at all, and neither could a clip of a sound, although #418 and the wiki said so. The only place a drag starts is the asset panel, and it listed assets, not clips. The texture label of an item playing a video clip was dead too, because the reference lookup did not know video clips. - The asset panel gives a sound or video with clips a chevron of its own. It unfolds one row per clip and starts closed. - A clip row drags like an asset. One click shows the clip in the inspector without leaving the tab (`EditorState.glance`). A double click opens its sound or video with the clip picked. - A video clip dropped on a composition track becomes an item as long as the clip. It wears the sprite of its whole video, so a composition keeps one sprite per video, and the clip is a texture override on the object. Sprite, item and override are one undo step (`droppedPicture`). - A sound clip becomes an audio item as long as the clip. The song tab takes sound clips on the same way, through `findSound`. - `referencedTarget` lets a reference label link to the clip itself. One click pings the clip row, a double click opens the asset with the clip picked. The Plays row of an audio item reads "Sound / Clip". - `clipOwner` in `sound.ts` finds the sound or video a clip id belongs to. `activateAssetOrClip` uses it for the double clicks. Found on the way: the Blip of the e2e seed is only 0.25 s long, so a clip of 0.2 to 0.7 s on it is cut to one frame. 69s takes the Drone (0.75 s) instead. Tests: - tsc, eslint, Prettier clean - Unit on stein2 (composition, references, sound, video, editor): 94 files, 1238 tests green - e2e 69 on melektaus-atx: ALL PASSED, 25 checks, 2 of them new (69s) - Gate on melektaus-atx with the fix for 68k on top: CHECK EXIT 0, 313 files, 4841 tests - Counter checks, all red: no clip rows in e2e 69 (no drag starts), no clip override in the unit test, the link aimed at the asset instead of the clip - Not covered by its own e2e check: sound clips on the song tab Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
140 lines
5.2 KiB
TypeScript
140 lines
5.2 KiB
TypeScript
/** Which asset a reference field points at – what "Open texture" and the inspector link open (#221). */
|
||
import { describe, expect, it } from 'vitest';
|
||
import {
|
||
CreateAssetCommand,
|
||
CreateSoundCommand,
|
||
CreateTextureCommand,
|
||
CreateVideoCommand,
|
||
} from '../../src/core/commands/assetCommands';
|
||
import { AddClipCommand } from '../../src/core/commands/clipCommands';
|
||
import { AddFrameCommand } from '../../src/core/commands/frameCommands';
|
||
import type { PixelArtAsset } from '../../src/core/model/Asset';
|
||
import { findAsset } from '../../src/core/model/Project';
|
||
import { type PropertyDef, schemaForAsset } from '../../src/core/properties/PropertySchema';
|
||
import { referencedAssetId, referencedTarget, referencesOf } from '../../src/core/properties/references';
|
||
import { resolveAssetProps } from '../../src/core/properties/resolve';
|
||
import { SHADER_KEY } from '../../src/core/properties/shaders';
|
||
import { makeSpriteScene } from '../helpers';
|
||
|
||
const FILE = `${'ab'.repeat(32)}.png`;
|
||
const TEXTURE: PropertyDef = { key: 'texture', label: 'Texture', kind: 'texture', default: '' };
|
||
const SHADER: PropertyDef = {
|
||
key: SHADER_KEY,
|
||
label: 'Shader',
|
||
kind: 'asset',
|
||
default: '',
|
||
assetTypes: ['shader'],
|
||
};
|
||
|
||
const SOUND: PropertyDef = { key: 'sound', label: 'Sound', kind: 'sound', default: '' };
|
||
|
||
function withClips() {
|
||
const f = setup();
|
||
const video = new CreateVideoCommand('Film', `${'cd'.repeat(32)}.mp4`, {
|
||
width: 320,
|
||
height: 180,
|
||
duration: 4,
|
||
fps: 30,
|
||
audio: false,
|
||
});
|
||
const sound = new CreateSoundCommand('Kick', `${'ef'.repeat(32)}.ogg`);
|
||
f.stack.execute(video);
|
||
f.stack.execute(sound);
|
||
const videoClip = new AddClipCommand(video.asset.id, { start: 1, end: 2 });
|
||
const soundClip = new AddClipCommand(sound.asset.id, { start: 0.1, end: 0.2 });
|
||
f.stack.execute(videoClip);
|
||
f.stack.execute(soundClip);
|
||
return {
|
||
...f,
|
||
videoId: video.asset.id,
|
||
soundId: sound.asset.id,
|
||
videoClip: videoClip.clipId,
|
||
soundClip: soundClip.clipId,
|
||
};
|
||
}
|
||
|
||
function setup() {
|
||
const fixture = makeSpriteScene();
|
||
const texture = new CreateTextureCommand('Sheet', { file: FILE, width: 64, height: 32 });
|
||
fixture.stack.execute(texture);
|
||
const frame = new AddFrameCommand(texture.asset.id, { x: 0, y: 0, w: 16, h: 16 }, 'Idle');
|
||
fixture.stack.execute(frame);
|
||
const shader = new CreateAssetCommand('shader', 'Glow');
|
||
fixture.stack.execute(shader);
|
||
const art = new CreateAssetCommand('pixelart', 'Art');
|
||
fixture.stack.execute(art);
|
||
return {
|
||
...fixture,
|
||
textureId: texture.asset.id,
|
||
frameId: frame.frameId,
|
||
shaderId: shader.asset.id,
|
||
art: art.asset as PixelArtAsset,
|
||
};
|
||
}
|
||
|
||
describe('referencedAssetId', () => {
|
||
it('is the asset a reference names', () => {
|
||
const f = setup();
|
||
expect(referencedAssetId(f.project, TEXTURE, f.textureId)).toBe(f.textureId);
|
||
expect(referencedAssetId(f.project, SHADER, f.shaderId)).toBe(f.shaderId);
|
||
});
|
||
|
||
it('is the sheet for a frame of a texture or of pixel art', () => {
|
||
const f = setup();
|
||
expect(referencedAssetId(f.project, TEXTURE, f.frameId)).toBe(f.textureId);
|
||
f.art.frames.push({ id: 'second', cels: {} });
|
||
expect(referencedAssetId(f.project, TEXTURE, 'second')).toBe(f.art.id);
|
||
});
|
||
|
||
it('is nothing for an empty or dangling reference', () => {
|
||
const f = setup();
|
||
expect(referencedAssetId(f.project, TEXTURE, '')).toBeNull();
|
||
expect(referencedAssetId(f.project, SHADER, 'gone')).toBeNull();
|
||
});
|
||
|
||
it('only resolves frames in a texture field', () => {
|
||
const f = setup();
|
||
expect(referencedAssetId(f.project, SHADER, f.frameId)).toBeNull();
|
||
});
|
||
|
||
it('is nothing for a field that names no asset, whatever it holds', () => {
|
||
const f = setup();
|
||
const text: PropertyDef = { key: 'name', label: 'Name', kind: 'text', default: '' };
|
||
expect(referencedAssetId(f.project, text, f.textureId)).toBeNull();
|
||
});
|
||
|
||
it('is the video for a clip of one in a texture field (#425)', () => {
|
||
const f = withClips();
|
||
expect(referencedAssetId(f.project, TEXTURE, f.videoClip)).toBe(f.videoId);
|
||
expect(referencedAssetId(f.project, SOUND, f.soundClip)).toBe(f.soundId);
|
||
});
|
||
});
|
||
|
||
describe('referencedTarget (#425)', () => {
|
||
it('is the clip itself for a clip of a video or a sound, the asset for anything else', () => {
|
||
const f = withClips();
|
||
expect(referencedTarget(f.project, TEXTURE, f.videoClip)).toBe(f.videoClip);
|
||
expect(referencedTarget(f.project, SOUND, f.soundClip)).toBe(f.soundClip);
|
||
expect(referencedTarget(f.project, TEXTURE, f.videoId)).toBe(f.videoId);
|
||
expect(referencedTarget(f.project, TEXTURE, f.frameId)).toBe(f.textureId);
|
||
expect(referencedTarget(f.project, SHADER, f.videoClip)).toBeNull();
|
||
});
|
||
});
|
||
|
||
describe('referencesOf', () => {
|
||
it("lists a sprite asset's texture and shader in schema order, and nothing unset", () => {
|
||
const f = setup();
|
||
const sprite = findAsset(f.project, f.spriteId)!;
|
||
const refs = () =>
|
||
referencesOf(f.project, schemaForAsset(f.project, sprite), resolveAssetProps(f.project, sprite));
|
||
sprite.props.texture = '';
|
||
expect(refs()).toEqual([]);
|
||
sprite.props[SHADER_KEY] = f.shaderId;
|
||
sprite.props.texture = f.frameId;
|
||
expect(refs()).toEqual([
|
||
{ label: 'Texture', assetId: f.textureId },
|
||
{ label: 'Shader', assetId: f.shaderId },
|
||
]);
|
||
});
|
||
});
|