Files
feuerwerk/src/editor/agent/catalog/animation.ts
T
adminandClaude Opus 5.5 2099511695
feuerwerk / gate (push) Successful in 6m3s
feuerwerk / suite (push) Successful in 20m14s
Let the agent set automation lanes of a song track (#337)
Stage 4 of #337. Lanes could only be drawn in the song tab. Now the
agent can set them in one step and sees which parameters a lane of a
track can move.

- New command set_song_lane. The target is named as it is stored:
  volume, pan or param:<name>. All points come at once, each with
  tick, value and ease, from the same eases as animation keys,
  curve assets included.
- Points on one tick keep the order they come in, which makes a jump.
  points [] removes a volume or pan lane, remove a parameter lane.
- It refuses with a clear message: a value outside the range, an
  unknown ease, a missing curve asset, a range for volume or pan, and
  a new lane for a parameter nothing on the track has. The message
  then names the parameters there are.
- Without min and max a new parameter lane gets the range the dialog
  in the song tab offers.
- get_asset names under params of each song track the parameters a
  lane can move, with their default. The lanes were already in it.

Live in the user's project: get_asset named both parameters of their
two lanes (frequency, env) with defaults 20000 and 0.5. Playing to
3 s and 4.2 s, both tracks were heard, the audio clock running.

Tests:
- agent-music 13/13 (2 new), agent and MCP tests 129/129
- 6 counter-probes, all compile, all red
- gate on atx: CHECK EXIT 0, 330 files, 5082 tests

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 10:54:51 +02:00

263 lines
8.7 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.
import {
AddAnimationTrackCommand,
KeyStraightRunCommand,
MoveAnimationKeysCommand,
RemoveAnimationKeysCommand,
RemoveAnimationTracksCommand,
RetargetAnimationTrackCommand,
SetAnimationKeyCommand,
SetAnimationRootCommand,
SetKeyInterpolationCommand,
SetTrackRelativeCommand,
ShiftAnimationFramesCommand,
} from '../../../core/commands/animationCommands';
import { SetCurvePointsCommand } from '../../../core/commands/curveCommands';
import {
ANIMATION_ROOT_TYPES,
EASING_DIRECTIONS,
EASING_SHAPES,
easingValue,
type KeyRef,
} from '../../../core/model/animation';
import type { AnimationKey, KeyInterpolation, VectorPoint } from '../../../core/model/Asset';
import type { PropertyValue } from '../../../core/model/Properties';
import type { ArgSchema } from '../argCheck';
import {
anyValue,
ASSET,
type Args,
type CatalogCommand,
flag,
id,
ids,
listOf,
number,
numberOf,
object,
oneOf,
text,
theTab,
whole,
} from './shared';
const TRACK = id('Id of the row, as get_asset lists it under tracks.');
const TARGET: ArgSchema = {
type: 'string',
description: 'What the row animates: "" for the root itself, or an id get_asset lists under targets.',
};
const PROPERTY = id('Key of the property, as get_asset lists it for the target.');
const FRAME = whole('A frame, from 0.', 0);
const KEYS: ArgSchema = {
type: 'array',
minItems: 1,
items: object({ track: TRACK, frame: FRAME }, ['track', 'frame']),
description: 'Keys by their row and frame.',
};
const keyRefs = (args: Args): KeyRef[] =>
(args.keys as { track: string; frame: number }[]).map((key) => ({ trackId: key.track, frame: key.frame }));
const KEY: ArgSchema = object({ frame: FRAME, value: anyValue }, ['frame', 'value']);
const keyOf = (args: Args, name: string): AnimationKey => {
const key = args[name] as { frame: number; value: PropertyValue };
return { frame: key.frame, value: key.value };
};
/** Every easing a key can name, but the curve ones: those take the id of a Curve asset. */
export const EASINGS = EASING_SHAPES.flatMap(({ value }) =>
value === 'linear' || value === 'step'
? [value]
: value === 'curve'
? []
: EASING_DIRECTIONS.map((direction) => easingValue(value, direction.value)),
);
const HANDLE = object({ x: number('x'), y: number('y') }, ['x', 'y']);
const POINT: ArgSchema = object(
{
x: number('Share of the way, 0 to 1.'),
y: number('Value; 0 to 1 is start to end, outside overshoots.'),
in: {
...HANDLE,
description: 'Bezier handle towards the point before, as an offset; leave out for a corner.',
},
out: { ...HANDLE, description: 'Bezier handle towards the point after, as an offset.' },
mode: oneOf(['corner', 'smooth', 'symmetric'], 'How the handles move together when dragged.'),
},
['x', 'y'],
);
/** Timelines of Animation assets and the points of Curve assets. */
export const ANIMATION_COMMANDS: readonly CatalogCommand[] = [
{
name: 'set_animation_root',
description:
`Sets what an animation animates: a ${ANIMATION_ROOT_TYPES.join(', ')} asset, or "" for nothing. ` +
'Its rows reach the root and the objects inside it; get_asset lists them under targets.',
args: object({ asset: ASSET, root: { type: 'string', description: 'Id of the asset, or "".' } }, [
'asset',
'root',
]),
build: (args) => ({ command: new SetAnimationRootCommand(text(args, 'asset'), text(args, 'root')) }),
shows: theTab,
},
{
name: 'add_animation_track',
description: 'Adds a row to the timeline for one property of the root or of an object inside it.',
args: object({ asset: ASSET, target: TARGET, property: PROPERTY }, ['asset', 'target', 'property']),
build: (args) => {
const command = new AddAnimationTrackCommand(
text(args, 'asset'),
text(args, 'target'),
text(args, 'property'),
);
return { command, made: { id: command.trackId } };
},
shows: theTab,
},
{
name: 'remove_animation_tracks',
description: 'Removes rows with all their keys.',
args: object({ asset: ASSET, tracks: ids('Ids of the rows.') }, ['asset', 'tracks']),
build: (args) => ({
command: new RemoveAnimationTracksCommand(text(args, 'asset'), listOf(args, 'tracks')),
}),
shows: theTab,
},
{
name: 'retarget_animation_track',
description: 'Aims a row at another property, of the same kind, and keeps its keys.',
args: object({ asset: ASSET, track: TRACK, target: TARGET, property: PROPERTY }, [
'asset',
'track',
'target',
'property',
]),
build: (args) => ({
command: new RetargetAnimationTrackCommand(
text(args, 'asset'),
text(args, 'track'),
text(args, 'target'),
text(args, 'property'),
),
}),
shows: theTab,
},
{
name: 'set_track_relative',
description:
'Makes a number row relative (its keys add to the value the object had when the animation started) or absolute.',
args: object({ asset: ASSET, track: TRACK, relative: flag('Relative.') }, ['asset', 'track', 'relative']),
build: (args) => ({
command: new SetTrackRelativeCommand(
text(args, 'asset'),
text(args, 'track'),
args.relative as boolean,
),
}),
shows: theTab,
},
{
name: 'set_animation_key',
description:
'Sets the key of a row on a frame, replacing one there. Without a value it takes the value the row has on ' +
'that frame. The last key of all rows decides how long the animation runs; fps and playback are properties.',
args: object({ asset: ASSET, track: TRACK, frame: FRAME, value: anyValue }, ['asset', 'track', 'frame']),
build: (args) => ({
command: new SetAnimationKeyCommand(
text(args, 'asset'),
text(args, 'track'),
numberOf(args, 'frame'),
args.value as PropertyValue | undefined,
),
}),
shows: theTab,
},
{
name: 'set_key_easing',
description:
'How keys blend into the next one (number and color rows only): ' +
`${EASINGS.join(', ')}, or "curve:<id of a Curve asset>".`,
args: object({ asset: ASSET, keys: KEYS, easing: id('The easing.') }, ['asset', 'keys', 'easing']),
build: (args) => ({
command: new SetKeyInterpolationCommand(
text(args, 'asset'),
keyRefs(args),
text(args, 'easing') as KeyInterpolation,
),
}),
shows: theTab,
},
{
name: 'remove_animation_keys',
description: 'Removes keys.',
args: object({ asset: ASSET, keys: KEYS }, ['asset', 'keys']),
build: (args) => ({ command: new RemoveAnimationKeysCommand(text(args, 'asset'), keyRefs(args)) }),
shows: theTab,
},
{
name: 'move_animation_keys',
description:
'Moves keys by whole frames together; one landing on a key that stays replaces it. With copy the keys stay ' +
'and copies move.',
args: object(
{ asset: ASSET, keys: KEYS, by: whole('Frames, negative is earlier.'), copy: flag('Copy.') },
['asset', 'keys', 'by'],
),
build: (args) => ({
command: new MoveAnimationKeysCommand(
text(args, 'asset'),
keyRefs(args),
numberOf(args, 'by'),
args.copy === true,
),
}),
shows: theTab,
},
{
name: 'shift_animation_frames',
description:
'Inserts frames at a frame (count above 0), moving every later key, or removes them (below 0) with the keys on them.',
args: object({ asset: ASSET, at: FRAME, count: whole('How many frames.') }, ['asset', 'at', 'count']),
build: (args) => ({
command: new ShiftAnimationFramesCommand(
text(args, 'asset'),
numberOf(args, 'at'),
numberOf(args, 'count'),
),
}),
shows: theTab,
},
{
name: 'key_straight_run',
description:
'Keys a number row in a straight line from one key to another: both set, the first linear, the keys between ' +
'them removed – a driven video at 1x.',
args: object({ asset: ASSET, track: TRACK, from: KEY, to: KEY }, ['asset', 'track', 'from', 'to']),
build: (args) => ({
command: new KeyStraightRunCommand(
text(args, 'asset'),
text(args, 'track'),
keyOf(args, 'from'),
keyOf(args, 'to'),
),
}),
shows: theTab,
},
{
name: 'set_curve_points',
description:
'Sets all points of a Curve asset, which an easing or a Lerp node follows from (0, 0) to (1, 1). The first ' +
'point is moved to x 0, the last to x 1.',
args: object(
{ asset: ASSET, points: { type: 'array', minItems: 2, items: POINT, description: 'Left to right.' } },
['asset', 'points'],
),
build: (args) => ({
command: new SetCurvePointsCommand(text(args, 'asset'), args.points as VectorPoint[]),
}),
shows: theTab,
},
];