Files
feuerwerk/src/core/commands/require.ts
T
adminandClaude Opus 5.5 e435c54afb Edit a composition's tracks, items and clips in its own tab (#417)
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>
2026-10-12 07:47:22 +02:00

215 lines
8.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/** 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;
}