Drag clips of videos and sounds straight onto a track (#425)
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>
This commit is contained in:
1 parent
0701fe3856
commit
406839baea
20 files changed
+469
-94
No files matched your search
@@ -472,8 +472,8 @@ describe('items and their objects', () => {
|
||||
const cut = makeCut();
|
||||
const screen = new CreateAssetCommand('sprite', 'Screen', { texture: cut.videoId });
|
||||
cut.stack.execute(screen);
|
||||
expect(droppedLength(cut.project, cut.comp(), screen.asset)).toBe(300);
|
||||
expect(droppedLength(cut.project, cut.comp(), findAsset(cut.project, cut.boxId)!)).toBe(180);
|
||||
expect(droppedLength(cut.project, cut.comp(), screen.asset.id)).toBe(300);
|
||||
expect(droppedLength(cut.project, cut.comp(), cut.boxId)).toBe(180);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -11,19 +11,22 @@ import {
|
||||
DuplicateAssetCommand,
|
||||
MakeProjectAssetCommand,
|
||||
} from '../../src/core/commands/assetCommands';
|
||||
import { AddClipCommand } from '../../src/core/commands/clipCommands';
|
||||
import type { Command } from '../../src/core/commands/Command';
|
||||
import { pasteCompositionItems, removeFromComposition } from '../../src/core/commands/compositionCommands';
|
||||
import {
|
||||
addPictureItem,
|
||||
droppedPicture,
|
||||
KeepCompositionSpritesCommand,
|
||||
wornPicture,
|
||||
} from '../../src/core/commands/compositionSprites';
|
||||
import { PasteObjectsCommand } from '../../src/core/commands/pasteCommands';
|
||||
import { copyObjects } from '../../src/core/model/clipboard';
|
||||
import { isCompositionSprite } from '../../src/core/model/composition';
|
||||
import { isCompositionSprite, itemVideo } from '../../src/core/model/composition';
|
||||
import { droppedLength } from '../../src/core/model/compositionEdit';
|
||||
import { findAsset, findAssetOf } from '../../src/core/model/Project';
|
||||
import { assetTitle, listedAssets } from '../../src/core/music/embedded';
|
||||
import { VIDEO_END_KEY, VIDEO_START_KEY } from '../../src/core/properties/videos';
|
||||
import { deserializeProject, serializeProject } from '../../src/core/store/serialize';
|
||||
import { makeProject } from '../helpers';
|
||||
|
||||
@@ -45,8 +48,8 @@ function makeFilm() {
|
||||
const comp = (id = film.asset.id) => findAssetOf(f.project, id, 'composition')!;
|
||||
const sprites = () => f.project.assets.filter(isCompositionSprite);
|
||||
/** The video laid on the first track of `compId` at `start`, run through the stack as the timeline does. */
|
||||
const drop = (start: number, compId = film.asset.id) => {
|
||||
const { command, objectId } = addPictureItem(f.project, compId, video.asset.id, 'Intro', {
|
||||
const drop = (start: number, compId = film.asset.id, dropped = video.asset.id) => {
|
||||
const { command, objectId } = addPictureItem(f.project, compId, droppedPicture(f.project, dropped)!, {
|
||||
track: comp(compId).tracks[0]!.id,
|
||||
start,
|
||||
length: 150,
|
||||
@@ -92,7 +95,7 @@ describe('a picture laid on a track', () => {
|
||||
const video = findAsset(film.project, film.videoId)!;
|
||||
expect(wornPicture(video)).toBe(film.videoId);
|
||||
expect(wornPicture(findAsset(film.project, film.sceneId)!)).toBeNull();
|
||||
expect(droppedLength(film.project, film.comp(), video)).toBe(300);
|
||||
expect(droppedLength(film.project, film.comp(), video.id)).toBe(300);
|
||||
});
|
||||
|
||||
it('wears a sprite that "Make project asset" lists like any other, and it stays when the item goes', () => {
|
||||
@@ -133,6 +136,55 @@ describe('a picture laid on a track', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('a clip of a video laid on a track (#425)', () => {
|
||||
const withClip = () => {
|
||||
const film = makeFilm();
|
||||
const add = new AddClipCommand(film.videoId, { [VIDEO_START_KEY]: 1, [VIDEO_END_KEY]: 3 });
|
||||
film.run(add);
|
||||
return { ...film, clipId: add.clipId };
|
||||
};
|
||||
const objectOf = (film: ReturnType<typeof withClip>, id: string) =>
|
||||
film.comp().objects.find((o) => o.id === id)!;
|
||||
|
||||
it('wears the sprite of its video with the clip as the texture, as long as the clip', () => {
|
||||
const film = withClip();
|
||||
expect(droppedPicture(film.project, film.clipId)).toEqual({
|
||||
picture: film.videoId,
|
||||
clip: film.clipId,
|
||||
name: 'Intro',
|
||||
});
|
||||
expect(droppedLength(film.project, film.comp(), film.clipId)).toBe(120);
|
||||
const id = film.drop(0, film.filmId, film.clipId);
|
||||
const [sprite] = film.sprites();
|
||||
expect(sprite!.props).toMatchObject({ texture: film.videoId, width: 320, height: 180 });
|
||||
expect(objectOf(film, id)).toMatchObject({ assetId: sprite!.id, overrides: { texture: film.clipId } });
|
||||
const item = film.comp().items.find((i) => i.id === id)!;
|
||||
expect(itemVideo(film.project, film.comp(), item)?.clip?.id).toBe(film.clipId);
|
||||
});
|
||||
|
||||
it('shares the sprite with the whole video, and one undo takes a first item with its sprite', () => {
|
||||
const film = withClip();
|
||||
const whole = film.drop(0);
|
||||
const cut = film.drop(200, film.filmId, film.clipId);
|
||||
expect(film.sprites()).toHaveLength(1);
|
||||
expect(film.wornBy(film.comp(), cut)).toBe(film.wornBy(film.comp(), whole));
|
||||
expect(objectOf(film, whole).overrides).toEqual({});
|
||||
film.stack.undo();
|
||||
expect(film.comp().objects.map((o) => o.id)).toEqual([whole]);
|
||||
film.stack.undo();
|
||||
film.drop(0, film.filmId, film.clipId);
|
||||
film.stack.undo();
|
||||
expect(film.sprites()).toHaveLength(0);
|
||||
expect(film.comp().objects).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('is no picture for a sound or a scene', () => {
|
||||
const film = withClip();
|
||||
expect(droppedPicture(film.project, film.sceneId)).toBeNull();
|
||||
expect(droppedPicture(film.project, 'nothing')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('a copied item', () => {
|
||||
it('pasted after a cut wears its sprite again', () => {
|
||||
const film = makeFilm();
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
/** Which asset a reference field points at – what "Open texture" and the inspector link open (#221). */
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { CreateAssetCommand, CreateTextureCommand } from '../../src/core/commands/assetCommands';
|
||||
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, referencesOf } from '../../src/core/properties/references';
|
||||
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';
|
||||
@@ -20,6 +26,33 @@ const SHADER: PropertyDef = {
|
||||
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 });
|
||||
@@ -69,6 +102,23 @@ describe('referencedAssetId', () => {
|
||||
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', () => {
|
||||
|
||||
Reference in new issue
Block a user