The Outputs setting is gone. A Sequence now keeps one free output below the last connected one: nothing plugged in is one output, connecting the spare grows a new one, unplugging the bottom edge shrinks it back. It counts the last *connected* port rather than the number of edges, so unplugging a middle one leaves a gap instead of silently dropping an edge. Removing the setting also removes a bug it caused. Typing in the Outputs field did nothing and the arrow keys did nothing, while dragging the value worked: onInput fires per keystroke, the preview writes into node.params, that emits 'project', and Inspector.refreshValues() sees the node's signature change -- the port list is part of it -- so it rebuilds and destroys the row being typed in. A drag survives because it keeps receiving globalpointermove on the old widget. refreshValues() has no focus guard where refreshLive() does; the trap is still there for the next typed field that changes a node's shape. Ports that depend on the edges around a node mean derivePorts needs the graph, not just the params, so resolveDef and portSignature take it too. And a node whose ports are derived cannot know how many it has from its type, so run is handed the definition resolved for it.
163 lines
7.0 KiB
TypeScript
163 lines
7.0 KiB
TypeScript
/** Ports a node works out for itself (`NodeDef.derivePorts`): from its settings, or from its own edges. */
|
|
import { describe, expect, it } from 'vitest';
|
|
import { AddPortCommand } from '../../src/core/commands/portCommands';
|
|
import { DisconnectCommand, SetNodeParamCommand } from '../../src/core/commands/graphCommands';
|
|
import {
|
|
BEHAVIOR_END,
|
|
BEHAVIOR_START,
|
|
type GraphNode,
|
|
GraphInterpreter,
|
|
inlineInputs,
|
|
requireNodeDef,
|
|
resolveDef,
|
|
} from '../../src/core/graph';
|
|
import { behaviorLookup } from '../../src/core/model/behaviorPrograms';
|
|
import type { BehaviorAsset } from '../../src/core/model/Asset';
|
|
import { fakeContext, graphEditor, makeBehavior, makeProject, type ProjectFixture } from '../helpers';
|
|
|
|
function sequenceFixture() {
|
|
const fixture = makeProject();
|
|
const behavior = makeBehavior(fixture, 'B');
|
|
const edit = graphEditor(fixture, behavior);
|
|
return { ...fixture, behavior, edit };
|
|
}
|
|
|
|
const portsOf = (behavior: BehaviorAsset, node: GraphNode, fixture: ProjectFixture) =>
|
|
resolveDef(node, behavior.graph, behaviorLookup(fixture.project))!.outputs.map((p) => p.key);
|
|
|
|
const edgeKeys = (behavior: BehaviorAsset) =>
|
|
behavior.graph.edges.map((e) => `${e.from.port}→${e.to.port}`).sort();
|
|
|
|
describe('a Sequence sized by what is plugged into it', () => {
|
|
it('starts with one output and grows a spare as each last one is connected', () => {
|
|
const fixture = sequenceFixture();
|
|
const { behavior, edit } = fixture;
|
|
const node = edit.addNode('Sequence');
|
|
expect(portsOf(behavior, node, fixture)).toEqual(['a']);
|
|
|
|
edit.connect(node, 'a', edit.addNode('Log'), 'exec');
|
|
expect(portsOf(behavior, node, fixture)).toEqual(['a', 'b']);
|
|
edit.connect(node, 'b', edit.addNode('Log'), 'exec');
|
|
expect(portsOf(behavior, node, fixture)).toEqual(['a', 'b', 'c']);
|
|
});
|
|
|
|
it('shrinks again when the bottom edge goes, and never below the last connected one', () => {
|
|
const fixture = sequenceFixture();
|
|
const { stack, behavior, edit } = fixture;
|
|
const node = edit.addNode('Sequence');
|
|
edit.wire(['a', 'b'].map((port) => [node, port, edit.addNode('Log'), 'exec'] as const));
|
|
expect(portsOf(behavior, node, fixture)).toEqual(['a', 'b', 'c']);
|
|
|
|
stack.undo();
|
|
expect(portsOf(behavior, node, fixture)).toEqual(['a', 'b']);
|
|
stack.undo();
|
|
expect(portsOf(behavior, node, fixture)).toEqual(['a']);
|
|
});
|
|
|
|
it('keeps a gap rather than dropping an edge: the last connected port sets the size', () => {
|
|
const fixture = sequenceFixture();
|
|
const { behavior, edit } = fixture;
|
|
const node = edit.addNode('Sequence');
|
|
edit.wire(['a', 'b', 'c'].map((port) => [node, port, edit.addNode('Log'), 'exec'] as const));
|
|
const middle = behavior.graph.edges.find((e) => e.from.port === 'b')!;
|
|
fixture.stack.execute(new DisconnectCommand(behavior.id, middle.id));
|
|
|
|
expect(portsOf(behavior, node, fixture)).toEqual(['a', 'b', 'c', 'd']);
|
|
expect(edgeKeys(behavior)).toEqual(['a→exec', 'c→exec']);
|
|
});
|
|
|
|
it('runs the outputs it has, in order', () => {
|
|
const fixture = sequenceFixture();
|
|
const { behavior, edit } = fixture;
|
|
const node = edit.addNode('Sequence');
|
|
const onStart = edit.addNode('OnStart');
|
|
edit.connect(onStart, 'exec', node, 'exec');
|
|
for (const port of ['a', 'b'])
|
|
edit.connect(node, port, edit.addNode('Log', { message: port.toUpperCase() }), 'exec');
|
|
|
|
const ctx = fakeContext();
|
|
new GraphInterpreter(behavior.graph, behaviorLookup(fixture.project)).fire('OnStart', ctx);
|
|
expect(ctx.logs).toEqual(['A', 'B']);
|
|
});
|
|
});
|
|
|
|
/**
|
|
* The point of a struct: a bundle crosses a behavior boundary on ONE edge. Tetris carries the same
|
|
* state today as eight parallel ports wired by hand, which is the pain this replaces.
|
|
*/
|
|
describe('a struct travelling through a nested behavior', () => {
|
|
it('goes in as one port and comes back out as its parts', () => {
|
|
const fixture = makeProject();
|
|
|
|
const callee = makeBehavior(fixture, 'Sum piece');
|
|
const inner = graphEditor(fixture, callee);
|
|
const start = inner.addNode(BEHAVIOR_START);
|
|
const end = inner.addNode(BEHAVIOR_END);
|
|
fixture.stack.execute(new AddPortCommand(callee.id, start.id, { key: 'piece', kind: 'struct' }));
|
|
fixture.stack.execute(new AddPortCommand(callee.id, end.id, { key: 'total', kind: 'number' }));
|
|
const expand = inner.addNode('ExpandStruct');
|
|
for (const key of ['x', 'y'])
|
|
fixture.stack.execute(new AddPortCommand(callee.id, expand.id, { key, kind: 'number' }));
|
|
const add = inner.addNode('Add');
|
|
inner.wire([
|
|
[start, 'exec', end, 'exec'],
|
|
[start, 'piece', expand, 'value'],
|
|
[expand, 'x', add, 'a'],
|
|
[expand, 'y', add, 'b'],
|
|
[add, 'result', end, 'total'],
|
|
]);
|
|
|
|
const caller = makeBehavior(fixture, 'Caller', ['result']);
|
|
const outer = graphEditor(fixture, caller);
|
|
const pack = outer.addNode('PackStruct', { x: 30, y: 12 });
|
|
for (const key of ['x', 'y'])
|
|
fixture.stack.execute(new AddPortCommand(caller.id, pack.id, { key, kind: 'number' }));
|
|
const onStart = outer.addNode('OnStart');
|
|
const call = outer.addNode('RunBehavior', { behavior: callee.id });
|
|
const set = outer.addNode('SetVariable', { variable: 'result' });
|
|
outer.wire([
|
|
[onStart, 'exec', call, 'exec'],
|
|
[pack, 'value', call, 'piece'],
|
|
[call, 'exec', set, 'exec'],
|
|
[call, 'total', set, 'value'],
|
|
]);
|
|
|
|
// One edge into the call, where two numbers would need two.
|
|
expect(caller.graph.edges.filter((e) => e.to.node === call.id && e.to.port !== 'exec')).toHaveLength(1);
|
|
const ctx = fakeContext({ variables: { result: 0 } });
|
|
new GraphInterpreter(caller.graph, behaviorLookup(fixture.project)).fire('OnStart', ctx);
|
|
expect(ctx.variables.result).toBe(42);
|
|
});
|
|
});
|
|
|
|
describe('a struct input', () => {
|
|
it('is offered no inline field, because a bundle cannot be typed on one line', () => {
|
|
const def = requireNodeDef('ExpandStruct');
|
|
expect(def.inputs.map((p) => p.type)).toEqual(['struct']);
|
|
expect(inlineInputs(def)).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe('a Run behavior node whose behavior changes', () => {
|
|
it('drops the edges of the ports the new behavior does not have', () => {
|
|
const fixture = makeProject();
|
|
const called = makeBehavior(fixture, 'Called');
|
|
const other = makeBehavior(fixture, 'Other');
|
|
const start = graphEditor(fixture, called).addNode(BEHAVIOR_START);
|
|
fixture.stack.execute(new AddPortCommand(called.id, start.id, { key: 'power', kind: 'number' }));
|
|
|
|
const caller = makeBehavior(fixture, 'Caller');
|
|
const edit = graphEditor(fixture, caller);
|
|
const call = edit.addNode('RunBehavior', { behavior: called.id });
|
|
const number = edit.addNode('Number', { value: 3 });
|
|
edit.connect(number, 'value', call, 'power');
|
|
expect(edgeKeys(caller)).toEqual(['value→power']);
|
|
|
|
fixture.stack.execute(new SetNodeParamCommand(caller.id, call.id, 'behavior', other.id));
|
|
expect(edgeKeys(caller)).toEqual([]);
|
|
fixture.stack.undo();
|
|
expect(edgeKeys(caller)).toEqual(['value→power']);
|
|
expect(start.ports).toEqual([{ key: 'power', kind: 'number' }]);
|
|
});
|
|
});
|