Wait for a composition's videos and copy and paste its items (#417)
A composition lost the first frames of a video item. Its clock ran while the player was still loading, in the tab and in the game. The game only waited for videos once, when it started. A composition that a behavior starts later spawned its items after that wait. - The view now records which video sprites it left out for lack of a first picture, one entry per object. - The composition tab holds its playhead while the preview misses a video, for at most VIDEO_WAIT_MS (3 s) like the game. - CompositionSystem holds a composition's clock while one of its live items misses its video. The rest of the game keeps running. - The editor play and the published game pass their view to the run as RunServices.pictures. - VIDEO_WAIT_MS moves to core/model/video.ts, because the runtime does not import from viewport/. - Videos loaded ahead for coming items do not hold the clock. Items can now be copied and pasted inside the composition tab. - Ctrl+C keeps the timing of the copied items next to the objects. - Ctrl+V pastes objects and items in one undo step. The earliest item lands at the frame under the pointer, on the track there. - Without the pointer over the tracks it lands at the playhead, on the selected track or on the top track of the selected items. - Several items keep their distances and tracks to each other. - The same copy pastes into a scene as plain objects. Objects copied from a scene do not paste into a composition, they have no timing. Tests: - Unit composition-edit, clipboard, composition-play: 3 new for pasting, 1 new for the clock waiting in the game - e2e 69i (play from the end waits for the video), 69j (paste under the pointer and at the playhead), 68d (a composition started from On start waits for its video) - each new check is red without its fix - README: 2036 editor checks Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
c4650e43f0
commit
6e6b6a4d9a
20 files changed
+421
-19
No files matched your search
@@ -13,7 +13,7 @@ import { curveLookup } from '../../core/model/curve';
|
||||
import { contentId, placedWithContent } from '../../core/model/instance';
|
||||
import { findAssetOf } from '../../core/model/Project';
|
||||
import type { PropertyValue } from '../../core/model/Properties';
|
||||
import { findVideo } from '../../core/model/video';
|
||||
import { findVideo, VIDEO_WAIT_MS } from '../../core/model/video';
|
||||
import { VIDEO_CLOCK_KEY, VIDEO_TIME_KEY, videoDrivenOf } from '../../core/properties/videos';
|
||||
import type { Entity } from '../Entity';
|
||||
import type { World } from '../World';
|
||||
@@ -38,6 +38,14 @@ interface Run {
|
||||
id: number;
|
||||
/** By item id. An item a behavior destroyed stays here until it ends, so it is not built again. */
|
||||
live: Map<string, Spawned>;
|
||||
/** Seconds the clock has stood so far for a video without its first picture, see `waits`. */
|
||||
waited: number;
|
||||
}
|
||||
|
||||
/** What the view that shows a run knows of its driven videos (#404). */
|
||||
export interface VideoPictures {
|
||||
/** Whether it left the sprite `objectId` out, its video still without a first picture. */
|
||||
missing(objectId: string): boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -46,12 +54,18 @@ interface Run {
|
||||
* videos. Runs before the animations and the behaviors, so a behavior writes last and hears this
|
||||
* frame's `finished`. The editor run plays none: it writes into the saved scene, where the objects of
|
||||
* a cut would stay behind.
|
||||
*
|
||||
* While the view still waits for the first picture of a video an item brings in, the composition's
|
||||
* clock stands, so the video starts at its first frame and not where the clock has got to meanwhile.
|
||||
*/
|
||||
export class CompositionSystem implements System {
|
||||
finished: CompositionFinish[] = [];
|
||||
private readonly runs = new Map<Entity, Run>();
|
||||
|
||||
constructor(private readonly plays = true) {}
|
||||
constructor(
|
||||
private readonly plays = true,
|
||||
private readonly pictures: VideoPictures | null = null,
|
||||
) {}
|
||||
|
||||
update(world: World, dt: number): void {
|
||||
this.finished = [];
|
||||
@@ -82,8 +96,8 @@ export class CompositionSystem implements System {
|
||||
return;
|
||||
}
|
||||
run = this.runs.get(placement);
|
||||
if (!run) this.runs.set(placement, (run = { id: player.run, live: new Map() }));
|
||||
if (player.shown && isHandleEnabled(placement)) player.time += dt;
|
||||
if (!run) this.runs.set(placement, (run = { id: player.run, live: new Map(), waited: 0 }));
|
||||
if (player.shown && isHandleEnabled(placement) && !this.waits(run, dt)) player.time += dt;
|
||||
player.shown = true;
|
||||
const length = compositionLength(world.project, comp);
|
||||
let frame = compositionFrameAt(comp, player.time);
|
||||
@@ -99,6 +113,20 @@ export class CompositionSystem implements System {
|
||||
this.show(world, comp, placement, run, frame);
|
||||
}
|
||||
|
||||
/** Whether a video of a live item still has no picture; at most `VIDEO_WAIT_MS` at a stretch. */
|
||||
private waits(run: Run, dt: number): boolean {
|
||||
const pictures = this.pictures;
|
||||
const missing =
|
||||
pictures !== null &&
|
||||
[...run.live.values()].some(({ videos }) => videos.some((v) => pictures.missing(v.objectId)));
|
||||
if (!missing) {
|
||||
run.waited = 0;
|
||||
return false;
|
||||
}
|
||||
run.waited += dt;
|
||||
return run.waited * 1000 < VIDEO_WAIT_MS;
|
||||
}
|
||||
|
||||
/** Takes every item of the placement's run away. */
|
||||
private end(world: World, placement: Entity): void {
|
||||
const run = this.runs.get(placement);
|
||||
|
||||
Reference in new issue
Block a user