Add the Variable node and show the variable name in node titles
Get/Set variable nodes now title themselves "Get <name>" / "Set <name>", and
the new Variable node just "<name>"; the title is the same folded or open,
and a collapsed node shrinks to it. The redundant "Variable: <name>" row is
gone unless the variable was deleted ("<name>?" is the only hint then).
The Variable node sets its value when an exec flow arrives and exposes the
value at any time. Its output is read when a downstream node pulls it, not
frozen at the write, so count = count + 1 is one Variable node plus an Add.
Get variable keeps reading once per pulse: switching it would change the
behaviour of existing games.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
5f8a308d62
commit
e9fa592078
19 files changed
+354
-38
No files matched your search
@@ -4,7 +4,7 @@ import { CollapseNodesCommand } from '../../src/core/commands/graphCommands';
|
||||
import type { BehaviorAsset } from '../../src/core/model/Asset';
|
||||
import { findAsset } from '../../src/core/model/Project';
|
||||
import { deserializeProject, serializeProject } from '../../src/core/store/serialize';
|
||||
import { foldMarkKey } from '../../src/editor/graph/folding';
|
||||
import { collapsedNodeWidth, foldMarkKey } from '../../src/editor/graph/folding';
|
||||
import { graphEditor, makeBehavior, makeProject, roundTrip } from '../helpers';
|
||||
|
||||
function threeNodes() {
|
||||
@@ -61,6 +61,16 @@ describe('collapsing nodes', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('the width of a collapsed node', () => {
|
||||
it('fits the title plus its chrome, rounded up so the title is not cut', () => {
|
||||
expect(collapsedNodeWidth(40.2, 31, 176)).toBe(72);
|
||||
});
|
||||
|
||||
it('never grows past an open node – a long title is cut instead', () => {
|
||||
expect(collapsedNodeWidth(300, 31, 176)).toBe(176);
|
||||
});
|
||||
});
|
||||
|
||||
describe('hidden links at a collapsed node', () => {
|
||||
it('share one mark per side, because every port sits on the same point', () => {
|
||||
const a = foldMarkKey({ node: 'n', port: 'x' }, false, true);
|
||||
|
||||
@@ -150,6 +150,22 @@ const CASES: Record<string, Case[]> = {
|
||||
after: (ctx) => expect(ctx.variables).toEqual({ speed: 100 }),
|
||||
},
|
||||
],
|
||||
// Reading without a flow needs the interpreter: see variable-node.test.ts.
|
||||
Variable: [
|
||||
{
|
||||
name: 'writes its variable when the flow arrives, keeping the declared type',
|
||||
inputs: { value: '250' },
|
||||
params: { variable: 'speed' },
|
||||
next: 'exec',
|
||||
after: (ctx) => expect(ctx.variables.speed).toBe(250),
|
||||
},
|
||||
{
|
||||
name: 'no variable chosen writes nothing',
|
||||
inputs: { value: 5 },
|
||||
next: 'exec',
|
||||
after: (ctx) => expect(ctx.variables).toEqual({ speed: 100 }),
|
||||
},
|
||||
],
|
||||
|
||||
// references and properties
|
||||
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { RenameVariableCommand } from '../../src/core/commands/variableCommands';
|
||||
import { GraphInterpreter, resolveDef } from '../../src/core/graph';
|
||||
import type { BehaviorAsset } from '../../src/core/model/Asset';
|
||||
import { behaviorLookup } from '../../src/core/model/behaviorPrograms';
|
||||
import { fakeContext, graphEditor, makeBehavior, makeProject } from '../helpers';
|
||||
|
||||
function setup() {
|
||||
const fixture = makeProject();
|
||||
const behavior = makeBehavior(fixture, 'B', ['count', 'copy']);
|
||||
return { fixture, behavior, edit: graphEditor(fixture, behavior) };
|
||||
}
|
||||
|
||||
function run(behavior: BehaviorAsset, fixture: ReturnType<typeof makeProject>, count = 5) {
|
||||
const ctx = fakeContext({ variables: { count, copy: 0 } });
|
||||
new GraphInterpreter(behavior.graph, behaviorLookup(fixture.project)).fire('OnStart', ctx);
|
||||
return ctx.variables;
|
||||
}
|
||||
|
||||
const titleOf = (behavior: BehaviorAsset, index: number) =>
|
||||
resolveDef(behavior.graph.nodes[index]!, behavior.graph, () => null)!.label;
|
||||
|
||||
describe('the Variable node', () => {
|
||||
it('can be read without a flow ever reaching it', () => {
|
||||
const { fixture, behavior, edit } = setup();
|
||||
const start = edit.addNode('OnStart');
|
||||
const variable = edit.addNode('Variable', { variable: 'count' });
|
||||
const copy = edit.addNode('SetVariable', { variable: 'copy' });
|
||||
edit.wire([
|
||||
[start, 'exec', copy, 'exec'],
|
||||
[variable, 'value', copy, 'value'],
|
||||
]);
|
||||
expect(run(behavior, fixture)).toEqual({ count: 5, copy: 5 });
|
||||
});
|
||||
|
||||
it('writes its input when the flow arrives, unconnected as well as through an edge', () => {
|
||||
const { fixture, behavior, edit } = setup();
|
||||
const start = edit.addNode('OnStart');
|
||||
const inline = edit.addNode('Variable', { variable: 'count', value: 7 });
|
||||
const number = edit.addNode('Number', { value: 3 });
|
||||
const wired = edit.addNode('Variable', { variable: 'copy' });
|
||||
edit.wire([
|
||||
[start, 'exec', inline, 'exec'],
|
||||
[inline, 'exec', wired, 'exec'],
|
||||
[number, 'value', wired, 'value'],
|
||||
]);
|
||||
expect(run(behavior, fixture)).toEqual({ count: 7, copy: 3 });
|
||||
});
|
||||
|
||||
it('counts up through its own output: count = count + 1', () => {
|
||||
const { fixture, behavior, edit } = setup();
|
||||
const start = edit.addNode('OnStart');
|
||||
const variable = edit.addNode('Variable', { variable: 'count' });
|
||||
const add = edit.addNode('Add', { b: 1 });
|
||||
edit.wire([
|
||||
[start, 'exec', variable, 'exec'],
|
||||
[variable, 'value', add, 'a'],
|
||||
[add, 'result', variable, 'value'],
|
||||
]);
|
||||
expect(run(behavior, fixture).count).toBe(6);
|
||||
});
|
||||
|
||||
it('is read at the moment it is asked, so it sees a later write by another node', () => {
|
||||
const { fixture, behavior, edit } = setup();
|
||||
const start = edit.addNode('OnStart');
|
||||
const variable = edit.addNode('Variable', { variable: 'count', value: 7 });
|
||||
const overwrite = edit.addNode('SetVariable', { variable: 'count', value: 9 });
|
||||
const copy = edit.addNode('SetVariable', { variable: 'copy' });
|
||||
edit.wire([
|
||||
[start, 'exec', variable, 'exec'],
|
||||
[variable, 'exec', overwrite, 'exec'],
|
||||
[overwrite, 'exec', copy, 'exec'],
|
||||
[variable, 'value', copy, 'value'],
|
||||
]);
|
||||
expect(run(behavior, fixture)).toEqual({ count: 9, copy: 9 });
|
||||
});
|
||||
});
|
||||
|
||||
describe('variable node titles', () => {
|
||||
it('name the picked variable, and keep the plain title until one is picked', () => {
|
||||
const { behavior, edit } = setup();
|
||||
edit.addNode('GetVariable', { variable: 'count' });
|
||||
edit.addNode('SetVariable', { variable: 'count' });
|
||||
edit.addNode('Variable', { variable: 'count' });
|
||||
edit.addNode('GetVariable');
|
||||
edit.addNode('Variable');
|
||||
expect([0, 1, 2, 3, 4].map((i) => titleOf(behavior, i))).toEqual([
|
||||
'Get count',
|
||||
'Set count',
|
||||
'count',
|
||||
'Get variable',
|
||||
'Variable',
|
||||
]);
|
||||
});
|
||||
|
||||
it('follow a renamed variable', () => {
|
||||
const { fixture, behavior, edit } = setup();
|
||||
edit.addNode('Variable', { variable: 'count' });
|
||||
fixture.stack.execute(new RenameVariableCommand(behavior.id, 'count', 'score'));
|
||||
expect(titleOf(behavior, 0)).toBe('score');
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user