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>
263 lines
8.7 KiB
TypeScript
263 lines
8.7 KiB
TypeScript
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,
|
||
},
|
||
];
|