Stage 5c of the MCP work. The agent could place sprites and edit graphs, but it could not draw: pixel art, palettes and vector art had no commands in its catalog. Two new areas bring 29 commands, so the catalog now holds 144. - pixelart (13): paint, layers (add, copy, remove, change, move, merge down), frames (add, also as a copy, remove, move), canvas size with an anchor, palette colors set and removed. - paint_pixels edits one layer on one frame. Its steps run in order: rows (a small picture as characters, a space keeps the pixel), pixels, line, rect, ellipse, fill, clear. An index the palette does not have, a pixel off the canvas and an unknown character are refused. - vectorart (16): shapes from rings or as rect and ellipse, outline, style (fill, opacity, gradient or image fill, stroke), move, scale, rotate and flip, copy, remove, combine, rename, order, move into groups, group, layers. - get_asset gives the palette of pixel art as colors. Up to 16384 pixels over all layers and frames each layer comes as rows, above that only the number of painted pixels. Palette and vector colors come as #rrggbb. - Merge down, canvas size and removing a color are now builders in pixelActions.ts. The panel and the agent share the same code. - Color and point helpers moved into catalog/shared.ts, and graphs.ts uses them too. Found while testing: - The ellipse tests only checked the four extreme points, which stay right even with wrong handle lengths. The test now checks that every point of the drawn curve lies on the ellipse. Tests: - agent-drawing (new, 9) and the other agent, pixel and relay tests: 133/133. - 7 mutations, all red in the matching test. - e2e 71 on melektaus-atx: 24/24. New check: the agent's pixel art opens in its tab, reads back as rows, and its cel is on the server after saving (status 200). - Live in a test project: an 8x8 sprite and a copied second frame read back as painted, a rotated square and a radial gradient disc with their corners where computed. - Gate on melektaus-atx: CHECK EXIT 0, 322 files, 4978 tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
141 lines
5.5 KiB
TypeScript
141 lines
5.5 KiB
TypeScript
import { type Command, CommandError } from '../../../core/commands/Command';
|
||
import type { AnnotationKind } from '../../../core/graph';
|
||
import type { Project } from '../../../core/model/Project';
|
||
import { parseHex } from '../../../ui/color';
|
||
import type { MeasureNode } from '../../graph/graphLayout';
|
||
import type { ArgSchema } from '../argCheck';
|
||
|
||
/** What a command is built from: the project as it stands before the edit, and where new nodes go. */
|
||
export interface BuildContext {
|
||
project: Project;
|
||
/** A free place for a node the agent gave none, right of what the graph and this edit hold. */
|
||
spot(assetId: string): { x: number; y: number };
|
||
/** How the nodes of a graph are measured for arranging. */
|
||
measure(assetId: string): MeasureNode;
|
||
}
|
||
|
||
/** The command, and the ids of what it makes – what later steps of the same edit can name. */
|
||
export interface Built {
|
||
command: Command;
|
||
/** A function for what is only known once the command has run. */
|
||
made?: Record<string, string> | (() => Record<string, string>);
|
||
/** Nodes it added without a place: they are arranged when the edit's steps have run. */
|
||
unplaced?: { asset: string; nodes: string[] };
|
||
/** A group or comment it put at nodes: fitted to them again once the edit has arranged its nodes. */
|
||
frames?: Frame;
|
||
}
|
||
|
||
export interface Frame {
|
||
asset: string;
|
||
kind: AnnotationKind;
|
||
id: string;
|
||
nodes: readonly string[];
|
||
}
|
||
|
||
export type Args = Record<string, unknown>;
|
||
|
||
/**
|
||
* What a step worked on, for the editor to show the user: an asset (in the inspector, or with `open`
|
||
* in its tab, there with one of its texture frames picked), objects of a holder, nodes of a graph.
|
||
*/
|
||
export type StepTarget =
|
||
| { kind: 'asset'; asset: string; open?: true; frame?: string }
|
||
| { kind: 'objects'; in: string; ids: string[] }
|
||
| { kind: 'nodes'; asset: string; ids: string[] };
|
||
|
||
/** One command the agent can run (#429): what it does, its arguments, and how it is made. */
|
||
export interface CatalogCommand {
|
||
name: string;
|
||
description: string;
|
||
args: ArgSchema;
|
||
/** Deletes something the user has to agree to first – decision 4 of #429. */
|
||
confirms?: true;
|
||
build(args: Args, context: BuildContext): Built;
|
||
/** What it worked on, from its arguments and what it made; none for a step there is nothing to show of. */
|
||
shows?(args: Args, made: Record<string, string>): StepTarget;
|
||
}
|
||
|
||
export const id = (description: string): ArgSchema => ({ type: 'string', minLength: 1, description });
|
||
export const number = (description: string): ArgSchema => ({ type: 'number', description });
|
||
export const whole = (description: string, minimum?: number): ArgSchema => ({
|
||
type: 'integer',
|
||
description,
|
||
...(minimum === undefined ? {} : { minimum }),
|
||
});
|
||
export const flag = (description: string): ArgSchema => ({ type: 'boolean', description });
|
||
export const ids = (description: string): ArgSchema => ({
|
||
type: 'array',
|
||
items: { type: 'string', minLength: 1 },
|
||
minItems: 1,
|
||
description,
|
||
});
|
||
export const oneOf = (values: readonly string[], description: string): ArgSchema => ({
|
||
type: 'string',
|
||
enum: values,
|
||
description,
|
||
});
|
||
export const anyValue: ArgSchema = {
|
||
description: 'Any JSON value; it is checked against the kind of the property.',
|
||
};
|
||
export const object = (properties: Record<string, ArgSchema>, required: string[]): ArgSchema => ({
|
||
type: 'object',
|
||
properties,
|
||
required,
|
||
additionalProperties: false,
|
||
});
|
||
|
||
export const color = (description: string): ArgSchema => ({
|
||
type: 'string',
|
||
description: `${description} As "#rrggbb".`,
|
||
});
|
||
export const xy = (description: string): ArgSchema => ({
|
||
...object({ x: number('x'), y: number('y') }, ['x', 'y']),
|
||
description,
|
||
});
|
||
|
||
export const ASSET = id('Id of the asset.');
|
||
export const HOLDER = id('Id of the scene or container the object is in.');
|
||
export const OBJECT = id('Id of the placed object.');
|
||
export const GRAPH = id('Id of the asset holding the graph (behavior, state machine, shader, ...).');
|
||
export const NODE = id('Id of the node.');
|
||
export const PATH: ArgSchema = {
|
||
type: 'array',
|
||
items: { type: 'string', minLength: 1 },
|
||
description:
|
||
'Field keys leading into a struct variable, outermost first; leave out for the variable itself.',
|
||
};
|
||
|
||
export const text = (args: Args, key: string): string => args[key] as string;
|
||
export const numberOf = (args: Args, key: string): number => args[key] as number;
|
||
export const listOf = (args: Args, key: string): string[] => args[key] as string[];
|
||
|
||
/** A color the agent wrote as "#rrggbb", refused in words when it is none. */
|
||
export function hexColor(value: string): number {
|
||
const parsed = parseHex(value);
|
||
if (parsed === null) throw new CommandError(`"${value}" is no color – write "#rrggbb"`);
|
||
return parsed;
|
||
}
|
||
|
||
export const theAsset = (args: Args): StepTarget => ({ kind: 'asset', asset: text(args, 'asset') });
|
||
export const theTab = (args: Args): StepTarget => ({ kind: 'asset', asset: text(args, 'asset'), open: true });
|
||
export const madeAsset = (_args: Args, made: Record<string, string>): StepTarget => ({
|
||
kind: 'asset',
|
||
asset: made.id!,
|
||
});
|
||
export const theObject = (args: Args): StepTarget => ({
|
||
kind: 'objects',
|
||
in: text(args, 'in'),
|
||
ids: [text(args, 'object')],
|
||
});
|
||
export const theObjects = (args: Args): StepTarget => ({
|
||
kind: 'objects',
|
||
in: text(args, 'in'),
|
||
ids: listOf(args, 'objects'),
|
||
});
|
||
export const theNode = (args: Args): StepTarget => ({
|
||
kind: 'nodes',
|
||
asset: text(args, 'asset'),
|
||
ids: [text(args, 'node')],
|
||
});
|
||
export const theGraph = (args: Args): StepTarget => ({ kind: 'nodes', asset: text(args, 'asset'), ids: [] });
|