Drop a picture or video onto a composition track (#419)
A track item has always carried an object, and an object needs an asset. Instead of objects without one, a dropped picture or video gets a sprite the composition owns, the way a song owns its sounds (#355). To the rest of the code it is a sprite like any other. - The asset list starts a drag for textures, vector art, pixel art and videos. Dropped on a track, they make the sprite and the item in one undo step. A video item is as long as the video and follows it. - One sprite per picture and composition. A second drop of the same video shares it. Once no item wears it, it goes in the same step. - Deleting a composition takes its sprites along. A duplicate gets sprites of its own. - Pasting into a scene or container turns the sprite into a project asset. Pasting into another composition gives that one a copy. The clipboard carries the sprite, so cut and paste loses nothing. - Owned sprites stay out of the asset list and the pickers. The inspector shows them with a dot in front. - "Make project asset" in the item's menu, on the timeline and in the hierarchy, turns the sprite into an ordinary one. - The first track of the Composition tab gets its top line, as the Song tab's has. Found on the way: loading kept an owner only for the asset types a song can hold. A composition's sprite would have lost its owner and turned up in the list after a reload. Tests: - Unit composition-sprites: 11 new. Without the rule 5 of them are red, without the loading fix the loading test is. - e2e 69 on melektaus-atx: ALL PASSED, 15 checks, 69k new - Gate on melektaus-atx: 305 files, 4714 tests Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
f171c77f91
commit
b16fe8c718
18 files changed
+573
-49
No files matched your search
@@ -14,7 +14,10 @@ export interface AssetBase {
|
||||
type: AssetType;
|
||||
name: string;
|
||||
props: PropertyBag;
|
||||
/** The song a sound, pattern or sound FX made in it belongs to (#355); absent on a project asset. */
|
||||
/**
|
||||
* The song a sound, pattern or sound FX made in it belongs to (#355), or the composition a sprite
|
||||
* worn by its items does (#419); absent on a project asset.
|
||||
*/
|
||||
owner?: string;
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,8 @@ import {
|
||||
import { editorTransform, parentTransform, positionOf } from '../properties/transform';
|
||||
import { findObject, subtreeIds, subtreeRoots, treeOrder } from './hierarchy';
|
||||
import { isLiveInstance, isTemplateContent, resolvedHolder, splitInstanceId } from './instance';
|
||||
import type { CompositionItem } from './Asset';
|
||||
import type { CompositionItem, SpriteAsset } from './Asset';
|
||||
import { isCompositionSprite } from './composition';
|
||||
import type { Mat } from './matrix';
|
||||
import type { Position } from './Properties';
|
||||
import type { ObjectHolder, Project } from './Project';
|
||||
@@ -37,6 +38,8 @@ export interface ObjectClip {
|
||||
roots: ClipRoot[];
|
||||
/** Copied out of a composition (#417): the items of the roots that are items, for a paste there. */
|
||||
items?: CompositionItem[];
|
||||
/** The sprites of a composition's own the objects wear (#419), back for a paste after a cut. */
|
||||
sprites?: SpriteAsset[];
|
||||
}
|
||||
|
||||
export interface GraphClip {
|
||||
@@ -80,10 +83,13 @@ export function copyObjects(
|
||||
.filter((o) => members.has(o.id) && !shownByInstance(o.id))
|
||||
.map((o) => ({ ...structuredClone(o), editorOffset: { x: 0, y: 0 } }));
|
||||
const items = holder.type === 'composition' ? holder.items.filter((i) => roots.includes(i.id)) : [];
|
||||
const worn = new Set(objects.map((o) => o.assetId));
|
||||
const sprites = project.assets.filter(isCompositionSprite).filter((a) => worn.has(a.id));
|
||||
return {
|
||||
kind: 'objects',
|
||||
objects,
|
||||
...(items.length > 0 && { items: structuredClone(items) }),
|
||||
...(sprites.length > 0 && { sprites: structuredClone(sprites) }),
|
||||
roots: roots.map((id) => {
|
||||
const object = findObject(view, id)!;
|
||||
return {
|
||||
|
||||
@@ -16,10 +16,12 @@ import { type ReadAt, writeObjectRow } from './animationPreview';
|
||||
import type {
|
||||
AnimationAsset,
|
||||
AnimationTrack,
|
||||
Asset,
|
||||
CompositionAsset,
|
||||
CompositionClip,
|
||||
CompositionItem,
|
||||
CompositionTrack,
|
||||
SpriteAsset,
|
||||
} from './Asset';
|
||||
import { curveLookup } from './curve';
|
||||
import {
|
||||
@@ -47,6 +49,11 @@ export function compositionLoops(comp: CompositionAsset): boolean {
|
||||
return booleanOf(comp.props[COMPOSITION_LOOP_KEY]);
|
||||
}
|
||||
|
||||
/** A sprite a composition keeps to itself (#419), worn by the items a texture or video was laid as. */
|
||||
export function isCompositionSprite(asset: Asset): asset is SpriteAsset {
|
||||
return asset.type === 'sprite' && (asset.owner ?? '') !== '';
|
||||
}
|
||||
|
||||
export function newCompositionTrack(index: number): CompositionTrack {
|
||||
return { id: newId(), name: `Track ${index}` };
|
||||
}
|
||||
|
||||
@@ -235,9 +235,13 @@ export function snappedToMedia(frame: number, media: ItemMedia | null, reach: nu
|
||||
);
|
||||
}
|
||||
|
||||
/** How long an item of `asset` is when it is dropped in: as long as the video its sprite wears, else 3 s. */
|
||||
/**
|
||||
* How long an item of `asset` is when it is dropped in: as long as the video it is (#419) or its
|
||||
* sprite wears, else 3 s.
|
||||
*/
|
||||
export function droppedLength(project: Project, comp: CompositionAsset, asset: Asset): number {
|
||||
const texture = asset.type === 'sprite' ? asset.props.texture : undefined;
|
||||
const texture =
|
||||
asset.type === 'sprite' ? asset.props.texture : asset.type === 'video' ? asset.id : undefined;
|
||||
const video = typeof texture === 'string' ? findVideo(project, texture) : null;
|
||||
const fps = compositionFps(comp);
|
||||
const seconds = video ? videoLengthOf(video) : 0;
|
||||
|
||||
Reference in new issue
Block a user