Let the agent use the graph, scene and project commands (#429)
The agent knew 23 of the editor's commands. It could not point a Variables node at a variable, group nodes, edit ports, transitions, containers, colliders or the project settings. Stage 5a brings the catalog to 85 commands in five areas and keeps every answer short. - The catalog lists names and descriptions only. With a query (an area or command names) it adds the arguments. All schemas at once would pass the 60 000 character limit of one answer. - add_port points a Variables node at a variable. The port takes the variable's kind, and an unknown name is refused with the list of the ones that exist. - add_group and add_comment take nodes instead of coordinates. They follow the nodes after the arrange step, in the same undo step. - The rule for which list a port picks from moved into portChoices.ts. Inspector and agent now share it. - The frame of a group around nodes moved into annotationDrag.ts, so the button and the agent get the same frame. - list_assets returns the project settings. get_asset shows the content of placed containers with the ids overrides need. get_graph shows declared ports and frame colors. - A new node is placed at the height of its port, in the free spot whose links cross the fewest others. A source goes left of the node it feeds. Before, new nodes always went right of the rest. - Arrange breaks a tie in crossings by putting the flow on top, and exec links pull harder when a column is sorted. - A comment over a group keeps clear of the group's title bar. - The agent's instructions and lint_graph ask to look at crossings too and to call arrange_graph when there are more than needed. Left out on purpose: file assets (no upload), paste from the clipboard, internal wrapper commands and switching plugins on. Found on the way: - Variable commands on a Struct asset did not run through WithStructTypes. New fields did not reach variables of that type. - A query "variables" also matched set_variables_scope. A word that names an area now means only that area. Tests: - agent-catalog (new, 10), agent-edit, graph-arrange (3 new) and relay green. 11 bends of the code, all red. - e2e 71 on atx: 22/22, the new check sees port speed and a group around all 4 drawn nodes. - Live on a test project: a speed variable of 300 moved the player to x 150.03 in 0.5 s. A re-placed Variables node landed below Axis input with 4 crossings instead of 7. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
1943539a7a
commit
0d8fd6ff52
26 files changed
+2585
-672
No files matched your search
@@ -0,0 +1,215 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { findAnnotation, VARIABLES } from '../../src/core/graph';
|
||||
import { findAsset, findGraphAsset } from '../../src/core/model/Project';
|
||||
import { runTool } from '../../src/editor/agent/agentTools';
|
||||
import { normalizeBox } from '../../src/core/commands/annotationCommands';
|
||||
import { groupBoxAround, noteBoxAbove } from '../../src/editor/graph/annotationDrag';
|
||||
import { lintLayout } from '../../src/editor/graph/arrange';
|
||||
import { layoutOf } from '../../src/editor/graph/graphLayout';
|
||||
import { makeBehavior, makeSpriteScene, type SceneFixture } from '../helpers';
|
||||
import { hostFor, text } from './agentTestHost';
|
||||
|
||||
/** The commands of #429 stage 5a: what an agent needs past nodes and edges to build a game. */
|
||||
|
||||
const run = (fixture: SceneFixture, steps: object[]) =>
|
||||
runTool('run_commands', { steps }, hostFor(fixture).host);
|
||||
|
||||
const graphOf = (fixture: SceneFixture, assetId: string) => findGraphAsset(fixture.project, assetId)!.graph;
|
||||
|
||||
describe('variables in a graph', () => {
|
||||
it('a Variables node reads a variable once add_port names it, in the kind of the variable', async () => {
|
||||
const fixture = makeSpriteScene();
|
||||
const behavior = makeBehavior(fixture, 'Walk', ['speed']);
|
||||
const result = await run(fixture, [
|
||||
{ command: 'add_node', args: { asset: behavior.id, node: 'Variables' }, as: 'vars' },
|
||||
{ command: 'add_port', args: { asset: behavior.id, node: '$vars', key: 'speed' } },
|
||||
{ command: 'set_port_write', args: { asset: behavior.id, node: '$vars', key: 'speed', write: true } },
|
||||
]);
|
||||
expect(result.isError).toBeUndefined();
|
||||
const node = graphOf(fixture, behavior.id).nodes.find((n) => n.type === VARIABLES)!;
|
||||
expect(node.ports).toEqual([{ key: 'speed', kind: 'number', write: true }]);
|
||||
|
||||
const read = (await runTool('get_graph', { asset: behavior.id }, hostFor(fixture).host))
|
||||
.structuredContent as { nodes: { ports?: unknown; outputs: { key: string }[] }[] };
|
||||
expect(read.nodes[0]!.ports).toEqual([{ key: 'speed', kind: 'number', write: true }]);
|
||||
expect(read.nodes[0]!.outputs.map((port) => port.key)).toContain('speed');
|
||||
});
|
||||
|
||||
it('refuses a port naming no variable, and lists the ones there are', async () => {
|
||||
const fixture = makeSpriteScene();
|
||||
const behavior = makeBehavior(fixture, 'Walk', ['speed', 'jump']);
|
||||
const result = await run(fixture, [
|
||||
{ command: 'add_node', args: { asset: behavior.id, node: 'Variables' }, as: 'vars' },
|
||||
{ command: 'add_port', args: { asset: behavior.id, node: '$vars', key: 'sped' } },
|
||||
]);
|
||||
expect(result.isError).toBe(true);
|
||||
expect(text(result)).toContain(
|
||||
'Step 2 (add_port): "sped" is none of what this node can name: speed, jump',
|
||||
);
|
||||
expect(graphOf(fixture, behavior.id).nodes).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('a port named freely needs a kind', async () => {
|
||||
const fixture = makeSpriteScene();
|
||||
const behavior = makeBehavior(fixture, 'Point');
|
||||
const steps = (kind?: string) => [
|
||||
{ command: 'add_node', args: { asset: behavior.id, node: 'PackStruct' }, as: 'pack' },
|
||||
{
|
||||
command: 'add_port',
|
||||
args: { asset: behavior.id, node: '$pack', key: 'x', ...(kind ? { kind } : {}) },
|
||||
},
|
||||
];
|
||||
expect(text(await run(fixture, steps()))).toContain('Step 2 (add_port): Give the port a kind');
|
||||
expect((await run(fixture, steps('number'))).isError).toBeUndefined();
|
||||
expect(graphOf(fixture, behavior.id).nodes[0]!.ports).toEqual([{ key: 'x', kind: 'number' }]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('groups and comments', () => {
|
||||
it('a group made in the call that adds its nodes fits them where the call arranged them', async () => {
|
||||
const fixture = makeSpriteScene();
|
||||
const behavior = makeBehavior(fixture, 'Walk');
|
||||
const link = (from: string, port: string, to: string, into = port) => ({
|
||||
command: 'connect',
|
||||
args: { asset: behavior.id, from: { node: from, port }, to: { node: to, port: into } },
|
||||
});
|
||||
// An Owner waits right of the others until arranging puts it before the Move: the group has to follow.
|
||||
const result = await run(fixture, [
|
||||
{ command: 'add_node', args: { asset: behavior.id, node: 'OnKeyHeld' }, as: 'key' },
|
||||
{ command: 'add_node', args: { asset: behavior.id, node: 'Move' }, as: 'move' },
|
||||
{ command: 'add_node', args: { asset: behavior.id, node: 'Owner' }, as: 'owner' },
|
||||
link('$key', 'exec', '$move'),
|
||||
link('$owner', 'object', '$move'),
|
||||
{
|
||||
command: 'add_group',
|
||||
args: { asset: behavior.id, nodes: ['$key', '$move', '$owner'], title: 'Walk' },
|
||||
},
|
||||
{ command: 'add_comment', args: { asset: behavior.id, nodes: ['$key'], text: 'Held key moves it.' } },
|
||||
]);
|
||||
expect(result.isError).toBeUndefined();
|
||||
const graph = graphOf(fixture, behavior.id);
|
||||
const layout = layoutOf(fixture.project, behavior.id)!;
|
||||
const owner = graph.nodes.find((node) => node.type === 'Owner')!;
|
||||
const move = graph.nodes.find((node) => node.type === 'Move')!;
|
||||
expect(owner.x).toBeLessThan(move.x);
|
||||
|
||||
expect(graph.groups!.map(({ x, y, w, h, text: title }) => ({ x, y, w, h, title }))).toEqual([
|
||||
{ ...normalizeBox(groupBoxAround(layout.nodes)!), title: 'Walk' },
|
||||
]);
|
||||
const key = layout.nodes.find((node) => node.id === graph.nodes[0]!.id)!;
|
||||
const { x, y, w, h } = graph.notes![0]!;
|
||||
expect({ x, y, w, h }).toEqual(normalizeBox(noteBoxAbove([key])!));
|
||||
expect(lintLayout(layout).overlaps).toBe(0);
|
||||
|
||||
await runTool('undo', {}, hostFor(fixture).host);
|
||||
expect(graphOf(fixture, behavior.id).groups ?? []).toHaveLength(0);
|
||||
expect(graphOf(fixture, behavior.id).nodes).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('edit_annotation changes title, color and box; remove_annotation leaves the nodes', async () => {
|
||||
const fixture = makeSpriteScene();
|
||||
const behavior = makeBehavior(fixture, 'Walk');
|
||||
await run(fixture, [
|
||||
{ command: 'add_node', args: { asset: behavior.id, node: 'Move' }, as: 'move' },
|
||||
{ command: 'add_group', args: { asset: behavior.id, nodes: ['$move'], title: 'A', color: '#ff8800' } },
|
||||
]);
|
||||
const id = graphOf(fixture, behavior.id).groups![0]!.id;
|
||||
expect(graphOf(fixture, behavior.id).groups![0]!.color).toBe(0xff8800);
|
||||
await run(fixture, [
|
||||
{ command: 'edit_annotation', args: { asset: behavior.id, id, text: 'B', color: '', w: 400 } },
|
||||
]);
|
||||
const group = findAnnotation(graphOf(fixture, behavior.id), id)!.annotation;
|
||||
expect([group.text, group.color, group.w]).toEqual(['B', undefined, 400]);
|
||||
await run(fixture, [{ command: 'remove_annotation', args: { asset: behavior.id, id } }]);
|
||||
expect(graphOf(fixture, behavior.id).groups).toEqual([]);
|
||||
expect(graphOf(fixture, behavior.id).nodes).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('struct types', () => {
|
||||
it('a field added to a Struct asset reaches the variables of that type', async () => {
|
||||
const fixture = makeSpriteScene();
|
||||
const behavior = makeBehavior(fixture, 'Walk');
|
||||
const result = await run(fixture, [
|
||||
{ command: 'create_asset', args: { type: 'struct', name: 'Point' }, as: 'point' },
|
||||
{ command: 'add_variable', args: { asset: '$point', key: 'x', kind: 'number' } },
|
||||
{ command: 'add_variable', args: { asset: behavior.id, key: 'at', kind: 'struct' } },
|
||||
{ command: 'set_struct_type', args: { asset: behavior.id, key: 'at', type: '$point' } },
|
||||
{ command: 'add_variable', args: { asset: '$point', key: 'y', kind: 'number' } },
|
||||
]);
|
||||
expect(result.isError).toBeUndefined();
|
||||
const at = findAsset(fixture.project, behavior.id)!;
|
||||
const def = 'variables' in at ? at.variables.find((v) => v.key === 'at') : undefined;
|
||||
expect(def && 'fields' in def ? def.fields.map((field) => field.key) : []).toEqual(['x', 'y']);
|
||||
});
|
||||
|
||||
it('make_struct_type hands later steps the Struct asset it made', async () => {
|
||||
const fixture = makeSpriteScene();
|
||||
const behavior = makeBehavior(fixture, 'Walk');
|
||||
const result = await run(fixture, [
|
||||
{ command: 'add_variable', args: { asset: behavior.id, key: 'at', kind: 'struct' } },
|
||||
{ command: 'add_struct_field', args: { asset: behavior.id, key: 'at', field: 'x', kind: 'number' } },
|
||||
{ command: 'make_struct_type', args: { asset: behavior.id, key: 'at' }, as: 'type' },
|
||||
{ command: 'rename_asset', args: { asset: '$type', name: 'Spot' } },
|
||||
]);
|
||||
expect(result.isError).toBeUndefined();
|
||||
expect(fixture.project.assets.find((asset) => asset.type === 'struct')?.name).toBe('Spot');
|
||||
});
|
||||
});
|
||||
|
||||
describe('objects', () => {
|
||||
it('a container made of objects lists its content by the ids an override takes', async () => {
|
||||
const fixture = makeSpriteScene();
|
||||
const made = await run(fixture, [
|
||||
{ command: 'place_asset', args: { in: fixture.sceneId, asset: fixture.spriteId, x: 0, y: 0 }, as: 'a' },
|
||||
{
|
||||
command: 'place_asset',
|
||||
args: { in: fixture.sceneId, asset: fixture.spriteId, x: 64, y: 0 },
|
||||
as: 'b',
|
||||
},
|
||||
{ command: 'add_collider', args: { in: fixture.sceneId, object: '$a' } },
|
||||
{ command: 'create_container', args: { in: fixture.sceneId, objects: ['$a', '$b'] } },
|
||||
]);
|
||||
expect(made.isError).toBeUndefined();
|
||||
const read = (await runTool('get_asset', { asset: fixture.sceneId }, hostFor(fixture).host))
|
||||
.structuredContent as { objects: { id: string; fromContainer?: true }[] };
|
||||
const content = read.objects.filter((object) => object.fromContainer);
|
||||
// Both sprites, and the collider under the first.
|
||||
expect(content).toHaveLength(3);
|
||||
expect(content.every((object) => object.id.includes('~'))).toBe(true);
|
||||
|
||||
const set = await run(fixture, [
|
||||
{
|
||||
command: 'set_override',
|
||||
args: { in: fixture.sceneId, object: content[0]!.id, key: 'width', value: 10 },
|
||||
},
|
||||
]);
|
||||
expect(set.isError).toBeUndefined();
|
||||
expect(fixture.object(content[0]!.id).overrides.width).toBe(10);
|
||||
});
|
||||
|
||||
it('groups top-level objects and names the group it made', async () => {
|
||||
const fixture = makeSpriteScene();
|
||||
const result = await run(fixture, [
|
||||
{ command: 'place_asset', args: { in: fixture.sceneId, asset: fixture.spriteId, x: 0, y: 0 }, as: 'a' },
|
||||
{ command: 'group_objects', args: { in: fixture.sceneId, objects: ['$a'], name: 'Enemies' }, as: 'g' },
|
||||
{ command: 'rename_object_group', args: { in: fixture.sceneId, group: '$g', name: 'Foes' } },
|
||||
]);
|
||||
expect(result.isError).toBeUndefined();
|
||||
expect(fixture.sceneOf().groups?.map((group) => group.name)).toEqual(['Foes']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('the project', () => {
|
||||
it('list_assets gives the settings that the project commands change', async () => {
|
||||
const fixture = makeSpriteScene();
|
||||
await run(fixture, [
|
||||
{ command: 'set_layer_names', args: { list: 'collisionLayers', names: ['Default', 'Player'] } },
|
||||
{ command: 'set_play_from', args: { from: 'active' } },
|
||||
]);
|
||||
const listed = (await runTool('list_assets', {}, hostFor(fixture).host)).structuredContent as {
|
||||
project: { collisionLayers: string[]; playFrom: string };
|
||||
};
|
||||
expect(listed.project).toMatchObject({ collisionLayers: ['Default', 'Player'], playFrom: 'active' });
|
||||
});
|
||||
});
|
||||
@@ -1,60 +1,17 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { type Command, CommandError } from '../../src/core/commands/Command';
|
||||
import { findGraphAsset } from '../../src/core/model/Project';
|
||||
import { argProblem } from '../../src/editor/agent/argCheck';
|
||||
import { runTool, type ToolHost, type ToolResult } from '../../src/editor/agent/agentTools';
|
||||
import type { AgentView } from '../../src/editor/agent/agentView';
|
||||
import { COMMANDS, type StepTarget } from '../../src/editor/agent/commandCatalog';
|
||||
import { runTool } from '../../src/editor/agent/agentTools';
|
||||
import { COMMANDS } from '../../src/editor/agent/commandCatalog';
|
||||
import { NODE_STEP } from '../../src/editor/agent/editPlan';
|
||||
import { COLUMN_GAP, type GraphLint } from '../../src/editor/graph/arrange';
|
||||
import { NODE_WIDTH } from '../../src/editor/graph/nodeBox';
|
||||
import TOOLS from '../../src/editor/agent/tools.json';
|
||||
import { makeBehavior, makeSpriteScene, type SceneFixture } from '../helpers';
|
||||
import { hostFor, text } from './agentTestHost';
|
||||
|
||||
/** What an agent changes and reads through the tools (#429, stage 2). */
|
||||
|
||||
function hostFor(fixture: SceneFixture, deleteAnswer: string | null = null, follows = false) {
|
||||
const asked: string[][] = [];
|
||||
const shown: StepTarget[] = [];
|
||||
const host: ToolHost = {
|
||||
view: () => ({ play: 'editing' }) as AgentView,
|
||||
screenshot: () => Promise.resolve({ data: '', width: 0, height: 0 }),
|
||||
project: () => fixture.project,
|
||||
execute: (command: Command) => {
|
||||
try {
|
||||
fixture.stack.execute(command);
|
||||
return null;
|
||||
} catch (error) {
|
||||
if (error instanceof CommandError) return error.message;
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
confirmDeletes: (ids) => {
|
||||
asked.push([...ids]);
|
||||
return Promise.resolve(deleteAnswer);
|
||||
},
|
||||
open: () => null,
|
||||
select: () => null,
|
||||
play: () => null,
|
||||
undo: () => {
|
||||
const label = fixture.stack.undoLabel;
|
||||
if (label !== null) fixture.stack.undo();
|
||||
return label;
|
||||
},
|
||||
redo: () => {
|
||||
const label = fixture.stack.redoLabel;
|
||||
if (label !== null) fixture.stack.redo();
|
||||
return label;
|
||||
},
|
||||
follows: () => follows,
|
||||
show: (focus) => shown.push(focus),
|
||||
};
|
||||
return { host, asked, shown };
|
||||
}
|
||||
|
||||
const text = (result: ToolResult) =>
|
||||
result.content.map((item) => (item.type === 'text' ? item.text : '')).join('\n');
|
||||
|
||||
/** An object that walks left while the left arrow is held: behavior, placement, three nodes, three edges. */
|
||||
function walkLeft(fixture: SceneFixture) {
|
||||
return [
|
||||
@@ -470,12 +427,32 @@ describe('the catalog', () => {
|
||||
expect(command.args).toMatchObject({ type: 'object', additionalProperties: false });
|
||||
expect(argProblem(command.args, {})).toMatch(/is missing|^null$/);
|
||||
}
|
||||
const fixture = makeSpriteScene();
|
||||
const listed = (await runTool('catalog', { topic: 'commands' }, hostFor(fixture).host))
|
||||
.structuredContent as {
|
||||
commands: { name: string }[];
|
||||
const { host } = hostFor(makeSpriteScene());
|
||||
const listed = (await runTool('catalog', { topic: 'commands' }, host)).structuredContent as {
|
||||
commands: { areas: { area: string; commands: { name: string; args?: unknown }[] }[] };
|
||||
};
|
||||
expect(listed.commands.map((command) => command.name)).toEqual(names);
|
||||
const areas = listed.commands.areas;
|
||||
expect(areas.flatMap((area) => area.commands.map((command) => command.name))).toEqual(names);
|
||||
expect(areas.flatMap((area) => area.commands).some((command) => 'args' in command)).toBe(false);
|
||||
// Each area in full fits one answer, which is cut beyond 60 000 characters.
|
||||
for (const { area, commands } of areas) {
|
||||
const full = await runTool('catalog', { topic: 'commands', query: area }, host);
|
||||
expect(full.isError).toBeUndefined();
|
||||
expect(text(full).length).toBeLessThan(60_000);
|
||||
const rows = (full.structuredContent as { commands: { name: string; args: unknown }[] }).commands;
|
||||
expect(rows.map((row) => row.name)).toEqual(commands.map((command) => command.name));
|
||||
expect(rows.every((row) => row.args !== undefined)).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it('a query of command names answers just those in full, and an unknown one says so', async () => {
|
||||
const { host } = hostFor(makeSpriteScene());
|
||||
const found = await runTool('catalog', { topic: 'commands', query: 'add_port, add_group' }, host);
|
||||
const rows = (found.structuredContent as { commands: { area: string; name: string }[] }).commands;
|
||||
expect(rows.map((row) => `${row.area}/${row.name}`)).toEqual(['graphs/add_group', 'graphs/add_port']);
|
||||
const none = await runTool('catalog', { topic: 'commands', query: 'teleport' }, host);
|
||||
expect(none.isError).toBe(true);
|
||||
expect(text(none)).toContain('No area or command matches "teleport"');
|
||||
});
|
||||
|
||||
it('the tools are the ones the relay lists, each with a schema', () => {
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import { type Command, CommandError } from '../../src/core/commands/Command';
|
||||
import type { ToolHost, ToolResult } from '../../src/editor/agent/agentTools';
|
||||
import type { AgentView } from '../../src/editor/agent/agentView';
|
||||
import type { StepTarget } from '../../src/editor/agent/commandCatalog';
|
||||
import type { SceneFixture } from '../helpers';
|
||||
|
||||
/** The editor behind the agent's tools, as the tests drive it: the fixture's project and undo stack. */
|
||||
export function hostFor(fixture: SceneFixture, deleteAnswer: string | null = null, follows = false) {
|
||||
const asked: string[][] = [];
|
||||
const shown: StepTarget[] = [];
|
||||
const host: ToolHost = {
|
||||
view: () => ({ play: 'editing' }) as AgentView,
|
||||
screenshot: () => Promise.resolve({ data: '', width: 0, height: 0 }),
|
||||
project: () => fixture.project,
|
||||
execute: (command: Command) => {
|
||||
try {
|
||||
fixture.stack.execute(command);
|
||||
return null;
|
||||
} catch (error) {
|
||||
if (error instanceof CommandError) return error.message;
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
confirmDeletes: (ids) => {
|
||||
asked.push([...ids]);
|
||||
return Promise.resolve(deleteAnswer);
|
||||
},
|
||||
open: () => null,
|
||||
select: () => null,
|
||||
play: () => null,
|
||||
undo: () => {
|
||||
const label = fixture.stack.undoLabel;
|
||||
if (label !== null) fixture.stack.undo();
|
||||
return label;
|
||||
},
|
||||
redo: () => {
|
||||
const label = fixture.stack.redoLabel;
|
||||
if (label !== null) fixture.stack.redo();
|
||||
return label;
|
||||
},
|
||||
follows: () => follows,
|
||||
show: (focus) => shown.push(focus),
|
||||
};
|
||||
return { host, asked, shown };
|
||||
}
|
||||
|
||||
export const text = (result: ToolResult) =>
|
||||
result.content.map((item) => (item.type === 'text' ? item.text : '')).join('\n');
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
type LayoutGraph,
|
||||
type LayoutNode,
|
||||
lintLayout,
|
||||
ROW_GAP,
|
||||
} from '../../src/editor/graph/arrange';
|
||||
import { layoutOf } from '../../src/editor/graph/graphLayout';
|
||||
import { makeBehavior, makeProject } from '../helpers';
|
||||
@@ -151,11 +152,74 @@ describe('arrange', () => {
|
||||
const clear = n.x >= box.x + box.w || n.x + n.w <= box.x || n.y >= box.y + box.h || n.y + n.h <= box.y;
|
||||
expect(clear).toBe(true);
|
||||
}
|
||||
// Right of everything, level with the node they hang on.
|
||||
expect(Math.min(...result.nodes.filter((n) => n.id !== 'kept').map((n) => n.x))).toBeGreaterThan(
|
||||
box.x + box.w,
|
||||
);
|
||||
expect(Math.min(...result.nodes.filter((n) => n.id !== 'kept').map((n) => n.y))).toBe(0);
|
||||
// Right of the node they hang on, a column away, out of the group's way above or below it.
|
||||
const newA = result.nodes.find((n) => n.id === 'new-a')!;
|
||||
expect(newA.x).toBe(kept.x + kept.w + COLUMN_GAP);
|
||||
expect(lintLayout(result).backwards).toBe(0);
|
||||
});
|
||||
|
||||
/** The walking graph of #429: an update and the arrow-key axis moving the owner. */
|
||||
function walking(): LayoutGraph {
|
||||
return {
|
||||
nodes: [
|
||||
node('owner', [], ['object'], 0, 0),
|
||||
node('update', [], ['exec', 'dt'], 0, 80),
|
||||
node('axis', [], ['x', 'y', 'active'], 0, 180),
|
||||
node('move', ['exec', 'object', 'dx', 'dy', 'scale', 'dt'], ['exec'], 248, 67),
|
||||
],
|
||||
edges: [
|
||||
edge('update', 'exec', 'move', 'exec'),
|
||||
edge('update', 'dt', 'move', 'dt'),
|
||||
edge('owner', 'object', 'move', 'object'),
|
||||
edge('axis', 'x', 'move', 'dx'),
|
||||
edge('axis', 'y', 'move', 'dy'),
|
||||
],
|
||||
boxes: [],
|
||||
};
|
||||
}
|
||||
|
||||
it('puts a new node handing a value to one that stays left of it, where its link crosses least', () => {
|
||||
// The live case of #429: a Variables node added to the walking graph, feeding the Move's scale.
|
||||
// Above the Owner is nearer, with the axis this tall, but its link would cross the flow, the
|
||||
// object and dx and dy there.
|
||||
const graph = walking();
|
||||
graph.nodes[2] = node('axis', [], ['x', 'y', 'active', 'a', 'b', 'c', 'd', 'e'], 0, 180);
|
||||
graph.nodes.push(node('vars', [], ['speed'], 508, 0));
|
||||
graph.edges.push(edge('vars', 'speed', 'move', 'scale'));
|
||||
const result = placed(graph, new Set(['vars']));
|
||||
const vars = result.nodes.find((n) => n.id === 'vars')!;
|
||||
const axis = result.nodes.find((n) => n.id === 'axis')!;
|
||||
expect(vars.x + vars.w + COLUMN_GAP).toBe(248);
|
||||
const lint = lintLayout(result);
|
||||
expect(lint).toMatchObject({ overlaps: 0, backwards: 0, crossings: 4 });
|
||||
// Under the axis, as near the Move's scale as the column leaves room.
|
||||
expect(vars.y).toBeGreaterThanOrEqual(axis.y + axis.h + ROW_GAP);
|
||||
expect(vars.y).toBeLessThan(axis.y + axis.h + 2 * ROW_GAP);
|
||||
});
|
||||
|
||||
it('lines the new node’s port up with the one it feeds where there is room', () => {
|
||||
const graph: LayoutGraph = {
|
||||
nodes: [
|
||||
node('move', ['exec', 'object', 'dx', 'dy', 'scale'], ['exec'], 248, 0),
|
||||
node('vars', [], ['speed'], 508, 0),
|
||||
],
|
||||
edges: [edge('vars', 'speed', 'move', 'scale')],
|
||||
boxes: [],
|
||||
};
|
||||
const result = placed(graph, new Set(['vars']));
|
||||
const [move, vars] = result.nodes;
|
||||
expect(vars!.y + vars!.outputs.speed!).toBe(move!.y + move!.inputs.scale!);
|
||||
});
|
||||
|
||||
it('puts the node giving the flow on top when two orders cross as often', () => {
|
||||
// The Owner stood first; giving it the top or the update crosses three links either way.
|
||||
const result = placed(walking());
|
||||
const ids = result.nodes
|
||||
.filter((n) => n.id !== 'move')
|
||||
.sort((a, b) => a.y - b.y)
|
||||
.map((n) => n.id);
|
||||
expect(ids[0]).toBe('update');
|
||||
expect(lintLayout(result).crossings).toBe(3);
|
||||
});
|
||||
|
||||
it('arranges nodes inside the group that holds them where they are, rather than outside it', () => {
|
||||
|
||||
@@ -8,7 +8,7 @@ import { CreateAssetCommand } from '../../src/core/commands/assetCommands';
|
||||
import { AddNodeCommand } from '../../src/core/commands/graphCommands';
|
||||
import { AddVariableCommand } from '../../src/core/commands/variableCommands';
|
||||
import { VARIABLES, VARIABLES_PARAM } from '../../src/core/graph';
|
||||
import { variableChoices } from '../../src/editor/inspector/portSection';
|
||||
import { variableChoices } from '../../src/editor/graph/portChoices';
|
||||
import { makeBehavior, makeProject } from '../helpers';
|
||||
|
||||
describe('the variables a node offers', () => {
|
||||
|
||||
Reference in new issue
Block a user