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 | (() => Record); /** 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; /** * 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): 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, 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): 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: [] });