A composition could be placed and played, but not filled. Its tab puts the picture at the playhead above a timeline of tracks, so a cut or a cutscene is laid out by dragging assets onto tracks. - The timeline takes the song tab's geometry with frames for ticks: ruler in seconds, scrubbing, play and pause, + Track. Zoom and pan glide through viewTween like the other tabs. - A placeable asset dropped on a track becomes an item, 3 s long or as long as its sprite's video. Dropped on an item it lands behind. - Items move without overlapping. The right edge stops at the next item. The left edge trims into a video and not before its start. - An animation dropped on a track becomes a narrower clip bar above the item it plays on. The asset panel now starts a drag for animations too. - The picture is a scene view over compositionHolder. A click picks an item or its child, a drag moves the real object. It takes no assets, since an object there would have no item. - The hierarchy lists the items alive at the playhead. Reparenting is off there, items stay at the top level. - The inspector shows a TIMING block on an item (start, length, offset for a video, Fit to media) and edits tracks and clips. - Delete, the context menu and Remove item take item and object in one undo step. - fitMedia: an override that gives an item another video sets its length in the same undo step, capped at the next item. Dragging an edge or typing a length turns it off. - A composition inside a composition is refused. Tests: - Unit composition-edit (new): 19 tests. Four counter-checks each turned their test red. The "back to 3 s" test got a step in between after one stayed green. - e2e 69 (new): 11 checks, 9 own. The hierarchy check reads the rows shown, after a counter-check on the model's rows stayed green. - Gate on melektaus-atx: CHECK EXIT 0, 302 files, 4662 tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
215 lines
8.4 KiB
TypeScript
215 lines
8.4 KiB
TypeScript
/** Lookups that throw a `CommandError` when the target does not exist. */
|
||
import { findNode, type Graph, type GraphEdge, type GraphNode } from '../graph/Graph';
|
||
import { type NodeDef, nodeDef } from '../graph/NodeRegistry';
|
||
import type {
|
||
Asset,
|
||
CompositionAsset,
|
||
CurveAsset,
|
||
PatternAsset,
|
||
SongAsset,
|
||
TileAsset,
|
||
BehaviorAsset,
|
||
StateMachineAsset,
|
||
TilemapAsset,
|
||
TilemapLayer,
|
||
VariableOwnerAsset,
|
||
VariablesAsset,
|
||
VectorArtAsset,
|
||
VectorItem,
|
||
VectorShape,
|
||
} from '../model/Asset';
|
||
import { findLayer } from '../model/tilemap';
|
||
import { findShape } from '../model/vectorart';
|
||
import { findVectorItem } from '../model/vectorTree';
|
||
import { findObject, type ObjectTree } from '../model/hierarchy';
|
||
import { isTemplateContent, objectRef, type ObjectRef } from '../model/instance';
|
||
import {
|
||
findAsset,
|
||
findAssetOf,
|
||
findGraphAsset,
|
||
findObjectHolder,
|
||
type ObjectHolder,
|
||
type Project,
|
||
} from '../model/Project';
|
||
import type { SceneObject } from '../model/SceneObject';
|
||
import { type PropertyDef, schemaForAsset, schemaForObject } from '../properties/PropertySchema';
|
||
import { CommandError } from './Command';
|
||
|
||
export function requireAsset(project: Project, assetId: string): Asset {
|
||
const asset = findAsset(project, assetId);
|
||
if (!asset) throw new CommandError(`Asset ${assetId} not found`);
|
||
return asset;
|
||
}
|
||
|
||
export function requireHolder(project: Project, holderId: string): ObjectHolder {
|
||
const holder = findObjectHolder(project, holderId);
|
||
if (!holder) throw new CommandError(`Scene or container ${holderId} not found`);
|
||
return holder;
|
||
}
|
||
|
||
export function requireObject(project: Project, holderId: string, objectId: string): SceneObject {
|
||
const object = findObject(requireHolder(project, holderId), objectId);
|
||
if (!object) throw new CommandError(`Scene object ${objectId} not found`);
|
||
return object;
|
||
}
|
||
|
||
/**
|
||
* The object a value edit works on, whether it is stored in the holder or resolved from a
|
||
* container template – the writes then land in the instance's edits.
|
||
*/
|
||
export function requireObjectRef(project: Project, holderId: string, objectId: string): ObjectRef {
|
||
const ref = objectRef(project, requireHolder(project, holderId), objectId);
|
||
if (!ref) throw new CommandError(`Scene object ${objectId} not found`);
|
||
return ref;
|
||
}
|
||
|
||
/**
|
||
* Rejects an object that only exists because a container template says so. Its structure – what
|
||
* exists, what it is parented to – is the template's business, not the instance's.
|
||
*/
|
||
function requireStructural(project: Project, holder: ObjectTree, objectId: string): void {
|
||
if (!isTemplateContent(holder, objectId)) return;
|
||
// Not resolvable at all is a plain "not found", which the caller's own lookup words better.
|
||
const object = objectRef(project, holder, objectId)?.object;
|
||
if (!object) return;
|
||
const name = object.name || 'That object';
|
||
throw new CommandError(`${name} comes from a container template; open the container to change it`);
|
||
}
|
||
|
||
/** A stored object the caller may restructure: template content is rejected before the lookup. */
|
||
export function requireStructuralObject(project: Project, holder: ObjectTree, objectId: string): SceneObject {
|
||
requireStructural(project, holder, objectId);
|
||
const object = findObject(holder, objectId);
|
||
if (!object) throw new CommandError(`Scene object ${objectId} not found`);
|
||
return object;
|
||
}
|
||
|
||
export function requireBehavior(project: Project, assetId: string): BehaviorAsset {
|
||
const asset = findAssetOf(project, assetId, 'behavior');
|
||
if (!asset) throw new CommandError(`Behavior ${assetId} not found`);
|
||
return asset;
|
||
}
|
||
|
||
/**
|
||
* An asset that declares variables: a behavior, a Variables asset shared by the whole run, or a
|
||
* Struct asset, whose fields are declared the same way (#253).
|
||
*/
|
||
export function requireVariableOwner(project: Project, assetId: string): VariableOwnerAsset {
|
||
const asset = findAsset(project, assetId);
|
||
if (asset?.type !== 'behavior' && asset?.type !== 'variables' && asset?.type !== 'struct')
|
||
throw new CommandError(`Variable owner ${assetId} not found`);
|
||
return asset;
|
||
}
|
||
|
||
export function requireTile(project: Project, assetId: string): TileAsset {
|
||
const asset = findAssetOf(project, assetId, 'tile');
|
||
if (!asset) throw new CommandError(`Tile ${assetId} not found`);
|
||
return asset;
|
||
}
|
||
|
||
export function requireTilemap(project: Project, assetId: string): TilemapAsset {
|
||
const asset = findAssetOf(project, assetId, 'tilemap');
|
||
if (!asset) throw new CommandError(`Tilemap ${assetId} not found`);
|
||
return asset;
|
||
}
|
||
|
||
export function requireTilemapLayer(project: Project, assetId: string, layerId: string): TilemapLayer {
|
||
const layer = findLayer(requireTilemap(project, assetId), layerId);
|
||
if (!layer) throw new CommandError(`Layer ${layerId} not found`);
|
||
return layer;
|
||
}
|
||
|
||
export function requireVectorArt(project: Project, assetId: string): VectorArtAsset {
|
||
const asset = findAssetOf(project, assetId, 'vectorart');
|
||
if (!asset) throw new CommandError(`Vector art ${assetId} not found`);
|
||
return asset;
|
||
}
|
||
|
||
export function requireVectorShape(project: Project, assetId: string, shapeId: string): VectorShape {
|
||
const shape = findShape(requireVectorArt(project, assetId), shapeId);
|
||
if (!shape) throw new CommandError(`Shape ${shapeId} not found`);
|
||
return shape;
|
||
}
|
||
|
||
/** A shape or a group of the drawing (#190). */
|
||
export function requireVectorItem(project: Project, assetId: string, itemId: string): VectorItem {
|
||
const item = findVectorItem(requireVectorArt(project, assetId), itemId);
|
||
if (!item) throw new CommandError(`Shape ${itemId} not found`);
|
||
return item;
|
||
}
|
||
|
||
export function requireCurve(project: Project, assetId: string): CurveAsset {
|
||
const asset = findAssetOf(project, assetId, 'curve');
|
||
if (!asset) throw new CommandError(`Curve ${assetId} not found`);
|
||
return asset;
|
||
}
|
||
|
||
export function requirePattern(project: Project, assetId: string): PatternAsset {
|
||
const asset = findAssetOf(project, assetId, 'pattern');
|
||
if (!asset) throw new CommandError(`Pattern ${assetId} not found`);
|
||
return asset;
|
||
}
|
||
|
||
export function requireSong(project: Project, assetId: string): SongAsset {
|
||
const asset = findAssetOf(project, assetId, 'song');
|
||
if (!asset) throw new CommandError(`Song ${assetId} not found`);
|
||
return asset;
|
||
}
|
||
|
||
export function requireComposition(project: Project, assetId: string): CompositionAsset {
|
||
const asset = findAssetOf(project, assetId, 'composition');
|
||
if (!asset) throw new CommandError(`Composition ${assetId} not found`);
|
||
return asset;
|
||
}
|
||
|
||
export function requireVariablesAsset(project: Project, assetId: string): VariablesAsset {
|
||
const asset = findAssetOf(project, assetId, 'variables');
|
||
if (!asset) throw new CommandError(`Variables ${assetId} not found`);
|
||
return asset;
|
||
}
|
||
|
||
export function requireStateMachine(project: Project, assetId: string): StateMachineAsset {
|
||
const asset = findAssetOf(project, assetId, 'statemachine');
|
||
if (!asset) throw new CommandError(`State machine ${assetId} not found`);
|
||
return asset;
|
||
}
|
||
|
||
/** The graph of a behavior or of a state machine – what every graph command edits. */
|
||
export function requireGraph(project: Project, assetId: string): Graph {
|
||
const asset = findGraphAsset(project, assetId);
|
||
if (!asset) throw new CommandError(`Behavior or state machine ${assetId} not found`);
|
||
return asset.graph;
|
||
}
|
||
|
||
export function requireNode(project: Project, assetId: string, nodeId: string): GraphNode {
|
||
const node = findNode(requireGraph(project, assetId), nodeId);
|
||
if (!node) throw new CommandError(`Node ${nodeId} not found`);
|
||
return node;
|
||
}
|
||
|
||
export function requireEdge(project: Project, assetId: string, edgeId: string): GraphEdge {
|
||
const edge = requireGraph(project, assetId).edges.find((e) => e.id === edgeId);
|
||
if (!edge) throw new CommandError(`Edge ${edgeId} not found`);
|
||
return edge;
|
||
}
|
||
|
||
/** A `CommandError`, so an unknown type becomes a notice instead of a crash. */
|
||
export function requireNodeDef(type: string): NodeDef {
|
||
const def = nodeDef(type);
|
||
if (!def) throw new CommandError(`Unknown node type "${type}"`);
|
||
return def;
|
||
}
|
||
|
||
/** The row `key` names on the asset – or, given the placed object, on that object (its shader's rows). */
|
||
export function requirePropDef(
|
||
project: Project,
|
||
asset: Asset,
|
||
key: string,
|
||
object?: SceneObject,
|
||
): PropertyDef {
|
||
const schema = object ? schemaForObject(project, asset, object) : schemaForAsset(project, asset);
|
||
const def = schema.find((d) => d.key === key);
|
||
if (!def) throw new CommandError(`Property "${key}" is not defined for ${asset.type}`);
|
||
return def;
|
||
}
|