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
@@ -99,8 +99,12 @@ shown on the node and editable in the inspector.
|
||||
|
||||
- **Variables** are the behavior's own property schema (`BehaviorAsset.variables`): they
|
||||
appear in the inspector, can be overridden per placed instance, and each running instance
|
||||
gets its own mutable copy. `Get variable` / `Set variable` read and write them, and their plurals
|
||||
`Get variables` / `Set variables` do the same for several at once: their settings are a *list* of
|
||||
gets its own mutable copy. `Get variable` / `Set variable` read and write them, and are titled
|
||||
by the variable they name (*Get speed*, *Set speed*). `Variable` is both in one node, titled by the
|
||||
variable alone: its output can be read at any time – no flow has to reach it – and is read at the
|
||||
moment it is asked, so a read after a write sees the new value; an arriving flow writes its input.
|
||||
`count = count + 1` is one `Variable` node feeding its own output through an `Add` back into its
|
||||
input. Their plurals `Get variables` / `Set variables` do the same for several at once: their settings are a *list* of
|
||||
variables, one port per entry, so reading a handful is one node instead of a column of five. Those
|
||||
ports are picked from the behavior's variables rather than named, they take their type from the
|
||||
declaration, and renaming a variable carries them (and their edges) along. A variable is a
|
||||
@@ -149,7 +153,7 @@ shown on the node and editable in the inspector.
|
||||
- **Node set** (`src/core/graph/nodes/`, one file per node): events `On start`, `On update`, `On key down/up/held`,
|
||||
`On timer`, `On collision enter/stay/exit`, `On click` (the mouse pressed on the owner – anywhere when it has none), `Start` · flow `Branch`, `Sequence`, `Repeat`, `Run behavior`, `End` · data `Self`, `Owner`, `Parent`, `Object by name`, `Name of`,
|
||||
`Is asset` (is this object an instance of asset X – how you tell what you collided with), `Get/Set
|
||||
variable`, `Get/Set variables`, `Get/Set property`, `Get position` (local and world), `Is enabled`, literals, `Add/Subtract/Multiply/Divide`,
|
||||
variable`, `Variable`, `Get/Set variables`, `Get/Set property`, `Get position` (local and world), `Is enabled`, literals, `Add/Subtract/Multiply/Divide`,
|
||||
`Min/Max/Abs/Sqrt/Clamp`, `Floor/Ceil/Round`, `Modulo`, `Random`, `Time`, `To text`, `To number`, `Join text`,
|
||||
`Substring`, `Find text`, `Equals`,
|
||||
lists `List` (a constant), `Fill` (one value repeated – how an empty board is made), `Count`,
|
||||
@@ -268,7 +272,8 @@ shown on the node and editable in the inspector.
|
||||
- **A node can be collapsed** to its title bar: the chevron in front of the title (it leaves the
|
||||
selection alone, like the hierarchy's), or right-click → *Collapse node* / *Expand node*, which
|
||||
acts on the whole selection as one undo step. Every link of one side then meets at a single
|
||||
point on the title bar, and its hidden links share one mark per side. A collapsed node has no
|
||||
point on the title bar, and its hidden links share one mark per side. It narrows to its title
|
||||
(never wider than an open node, where a long title is cut). A collapsed node has no
|
||||
ports to grab, so a link dropped on it goes nowhere – expand it to wire it. Like hiding a link,
|
||||
this is drawing only: `collapsed` on the node is saved with the project.
|
||||
- **Header bar** (the same one the Texture and Container tabs have): the behavior picker,
|
||||
@@ -644,7 +649,7 @@ npm run check # typecheck + lint + format:check + tests, what CI would ru
|
||||
# code is lost (tail's is reported), so a red run reads as green
|
||||
npm run build # writes dist/ and regenerates index.html
|
||||
export GM_REMOTE=user@host # optional: run the browser elsewhere; see below
|
||||
node tools/seed-e2e.mjs && node tools/e2e.mjs # 537 editor checks, screenshots in /tmp/gm-e2e-*.png
|
||||
node tools/seed-e2e.mjs && node tools/e2e.mjs # 542 editor checks, screenshots in /tmp/gm-e2e-*.png
|
||||
GM_STEPS=20 node tools/e2e.mjs # or 1-12, or 6,20 – a subset while working on one step
|
||||
./e2e.sh start [GM_STEPS] # the same run detached, see below
|
||||
./e2e.sh status # live progress, or the last run's result
|
||||
|
||||
@@ -96,7 +96,7 @@ export class GraphInterpreter {
|
||||
const inputs = this.collectInputs(node, ctx);
|
||||
const result = def.run(ctx, inputs, node.params, def);
|
||||
const returned = result.call ? this.call(result.call, ctx, depth) : {};
|
||||
this.frame.outputCache.set(node.id, { ...result.outputs, ...returned });
|
||||
if (!def.read) this.frame.outputCache.set(node.id, { ...result.outputs, ...returned });
|
||||
this.trace?.onNode(node.id);
|
||||
if (result.repeat) this.loop(node, result.repeat, ctx, depth);
|
||||
const next = result.next === undefined ? [] : Array.isArray(result.next) ? result.next : [result.next];
|
||||
@@ -167,6 +167,10 @@ export class GraphInterpreter {
|
||||
if (!source) return null;
|
||||
const def = this.def(source);
|
||||
if (!def) return null;
|
||||
if (def.read) {
|
||||
this.trace?.onNode(nodeId);
|
||||
return def.read(ctx, source.params)[port] ?? null;
|
||||
}
|
||||
// Exec nodes that have not run in this pulse have no value yet.
|
||||
if (isEventNode(def) || hasExecInput(def) || !def.run) return null;
|
||||
if (++this.steps > MAX_STEPS_PER_FIRE)
|
||||
|
||||
@@ -81,6 +81,11 @@ export interface NodeDef {
|
||||
* offers, which values the behavior a call node names takes. `resolveDef` applies it.
|
||||
*/
|
||||
derivePorts?(source: PortSource): DerivedShape;
|
||||
/**
|
||||
* Outputs readable at any moment, even while the node's exec input has not fired in this pulse.
|
||||
* Never cached, so a read after a write sees the new value.
|
||||
*/
|
||||
read?(ctx: NodeContext, params: PropertyBag): Record<string, Value>;
|
||||
/**
|
||||
* Event nodes have no `run` – the interpreter seeds their outputs. `def` is resolved for the node at
|
||||
* hand, the only way a node with derived ports knows how many it has.
|
||||
|
||||
@@ -85,6 +85,7 @@ import { GetVariable } from './variable/GetVariable';
|
||||
import { GetVariables } from './variable/GetVariables';
|
||||
import { SetVariable } from './variable/SetVariable';
|
||||
import { SetVariables } from './variable/SetVariables';
|
||||
import { Variable } from './variable/Variable';
|
||||
|
||||
/**
|
||||
* Importing this module registers the built-in nodes. The palette sorts by category and label, so
|
||||
@@ -110,6 +111,7 @@ export const BUILT_IN_NODES: readonly NodeDef[] = [
|
||||
RunBehavior,
|
||||
BehaviorEnd,
|
||||
// variables
|
||||
Variable,
|
||||
GetVariable,
|
||||
GetVariables,
|
||||
SetVariable,
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import type { PropertyValue } from '../../model/Properties';
|
||||
import { type PropertyBag, type PropertyValue, textOf } from '../../model/Properties';
|
||||
import type { PropertyDef } from '../../properties/PropertySchema';
|
||||
import type { NodeDef, PortDef } from '../NodeRegistry';
|
||||
import { asBoolean, asNumber } from '../values';
|
||||
import type { NodeContext } from '../NodeContext';
|
||||
import type { DerivedShape, NodeDef, PortDef, PortSource, RunResult } from '../NodeRegistry';
|
||||
import { asBoolean, asNumber, type Value, writeTyped } from '../values';
|
||||
|
||||
export const exec = (key = 'exec', label = ''): PortDef => ({ key, label, type: 'exec' });
|
||||
export const num = (key: string, label = key): PortDef => ({ key, label, type: 'number' });
|
||||
@@ -33,6 +34,31 @@ export const variableParam: PropertyDef = {
|
||||
options: [],
|
||||
};
|
||||
|
||||
const variableKey = (params: PropertyBag) => textOf(params.variable ?? '');
|
||||
|
||||
export const readVariable = (ctx: NodeContext, params: PropertyBag): Record<string, Value> => ({
|
||||
value: ctx.variables[variableKey(params)] ?? null,
|
||||
});
|
||||
|
||||
export function writeVariable(
|
||||
ctx: NodeContext,
|
||||
inputs: Record<string, Value>,
|
||||
params: PropertyBag,
|
||||
): RunResult {
|
||||
const key = variableKey(params);
|
||||
if (key) writeTyped(ctx.variables, key, inputs.value);
|
||||
return { next: 'exec' };
|
||||
}
|
||||
|
||||
/** Once a variable is picked, the title names it: "Get speed", or just "speed". */
|
||||
export const variableTitle =
|
||||
(verb: string) =>
|
||||
({ node }: PortSource): DerivedShape => {
|
||||
const name = variableKey(node.params);
|
||||
if (!name) return {};
|
||||
return { label: verb ? `${verb} ${name}` : name };
|
||||
};
|
||||
|
||||
/** No `assetTypes`: any placeable asset is a valid answer here. */
|
||||
export const assetParam: PropertyDef = { key: 'asset', label: 'Asset', kind: 'asset', default: '' };
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { NodeDef } from '../../NodeRegistry';
|
||||
import { any, variableParam } from '../shared';
|
||||
import { any, readVariable, variableParam, variableTitle } from '../shared';
|
||||
|
||||
import { textOf } from '../../../model/Properties';
|
||||
export const GetVariable: NodeDef = {
|
||||
type: 'GetVariable',
|
||||
label: 'Get variable',
|
||||
@@ -10,7 +9,6 @@ export const GetVariable: NodeDef = {
|
||||
inputs: [],
|
||||
outputs: [any('value')],
|
||||
params: [variableParam],
|
||||
run: (ctx, _inputs, params) => ({
|
||||
outputs: { value: ctx.variables[textOf(params.variable ?? '')] ?? null },
|
||||
}),
|
||||
derivePorts: variableTitle('Get'),
|
||||
run: (ctx, _inputs, params) => ({ outputs: readVariable(ctx, params) }),
|
||||
};
|
||||
@@ -1,8 +1,6 @@
|
||||
import type { NodeDef } from '../../NodeRegistry';
|
||||
import { writeTyped } from '../../values';
|
||||
import { any, exec, variableParam } from '../shared';
|
||||
import { any, exec, variableParam, variableTitle, writeVariable } from '../shared';
|
||||
|
||||
import { textOf } from '../../../model/Properties';
|
||||
export const SetVariable: NodeDef = {
|
||||
type: 'SetVariable',
|
||||
label: 'Set variable',
|
||||
@@ -11,9 +9,6 @@ export const SetVariable: NodeDef = {
|
||||
inputs: [exec(), any('value')],
|
||||
outputs: [exec()],
|
||||
params: [variableParam],
|
||||
run: (ctx, inputs, params) => {
|
||||
const key = textOf(params.variable ?? '');
|
||||
if (key) writeTyped(ctx.variables, key, inputs.value);
|
||||
return { next: 'exec' };
|
||||
},
|
||||
derivePorts: variableTitle('Set'),
|
||||
run: writeVariable,
|
||||
};
|
||||
@@ -0,0 +1,18 @@
|
||||
import type { NodeDef } from '../../NodeRegistry';
|
||||
import { any, exec, readVariable, variableParam, variableTitle, writeVariable } from '../shared';
|
||||
|
||||
/** Get and Set in one node: the output can be read at any time, an arriving flow writes the input. */
|
||||
export const Variable: NodeDef = {
|
||||
type: 'Variable',
|
||||
label: 'Variable',
|
||||
category: 'variable',
|
||||
description:
|
||||
"Reads one of this behavior's variables at any time, and writes it when the flow arrives. " +
|
||||
'A read after the write sees the new value.',
|
||||
inputs: [exec(), any('value')],
|
||||
outputs: [exec(), any('value')],
|
||||
params: [variableParam],
|
||||
derivePorts: variableTitle(''),
|
||||
read: readVariable,
|
||||
run: writeVariable,
|
||||
};
|
||||
@@ -56,10 +56,10 @@ export function installDebugHook(app: Application, state: EditorState, editor: E
|
||||
* *minimum* width, so a label wider than the column pushes its control right – which is why a
|
||||
* fixed offset from the label's own text is the wrong way to click one.
|
||||
*/
|
||||
const fieldOf = (label: string): { x: number; y: number; left: number } | null => {
|
||||
const fieldOf = (label: string, last = false): { x: number; y: number; left: number } | null => {
|
||||
let found: Text | null = null;
|
||||
const walk = (node: Container): void => {
|
||||
if (found !== null || !node.visible) return;
|
||||
if ((found !== null && !last) || !node.visible) return;
|
||||
if (node instanceof Text && node.text === label) {
|
||||
found = node;
|
||||
return;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Graphics, Rectangle, Text } from 'pixi.js';
|
||||
import { CanvasTextMetrics, Graphics, Rectangle, Text } from 'pixi.js';
|
||||
import {
|
||||
asString,
|
||||
edgesInto,
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
inlineValue,
|
||||
type NodeCategory,
|
||||
type NodeDef,
|
||||
nodeDef,
|
||||
type PortDef,
|
||||
type PortType,
|
||||
type Value,
|
||||
@@ -19,6 +20,7 @@ import { assetRefLabel } from '../../core/properties/assetRef';
|
||||
import type { PropertyDef } from '../../core/properties/PropertySchema';
|
||||
import { formatNumber, Theme, textStyle, type TextStyleToken, truncate, Widget } from '../../ui';
|
||||
import { keyLabel } from '../inspector/fieldFactory';
|
||||
import { collapsedNodeWidth } from './folding';
|
||||
|
||||
/** Node geometry in graph units; exported because edge anchors must land exactly on these ports. */
|
||||
export const NODE_WIDTH = 176;
|
||||
@@ -30,6 +32,9 @@ export const PORT_HIT_RADIUS = 9;
|
||||
const PAD = 6;
|
||||
/** Room in front of the title for the collapse chevron, as wide as the hierarchy's. */
|
||||
const CHEVRON_SLOT = 14;
|
||||
const TITLE_X = PAD + CHEVRON_SLOT;
|
||||
/** Right of a collapsed title: the side's link dot sits centred on the edge. */
|
||||
const COLLAPSED_TITLE_END = PAD + PORT_RADIUS;
|
||||
|
||||
export function categoryColor(category: NodeCategory): number {
|
||||
const c = Theme.colors;
|
||||
@@ -126,15 +131,17 @@ export class NodeView extends Widget {
|
||||
this.def = def;
|
||||
this.problem = problem;
|
||||
const hidden = new Set(this.def.hiddenParams?.(node.params) ?? []);
|
||||
const title = this.def.label;
|
||||
const derived = title !== nodeDef(node.type)?.label;
|
||||
this.paramLines = this.def.params
|
||||
.filter((p) => !hidden.has(p.key))
|
||||
.map((p) => ({
|
||||
label: p.label,
|
||||
value: formatParam(p, node.params[p.key] ?? p.default, asset, project),
|
||||
}))
|
||||
// A setting the title already states is not worth a row: a nested-behavior node is titled
|
||||
// with the behavior it runs, so "Behavior: Double" under "Double" says nothing new.
|
||||
.filter((p) => p.value !== this.def.label)
|
||||
// A title made from a setting states it ("Behavior: Double" under "Double", "Variable: speed"
|
||||
// under "Get speed"). A dangling reference keeps its row: "speed?" is not in the title.
|
||||
.filter((p) => !derived || (p.value !== title && !title.endsWith(` ${p.value}`)))
|
||||
.map((p) => `${p.label}: ${p.value}`);
|
||||
this.connected = new Set(
|
||||
inlineInputs(this.def)
|
||||
@@ -146,17 +153,23 @@ export class NodeView extends Widget {
|
||||
outputs: graph.edges.some((e) => e.from.node === node.id),
|
||||
};
|
||||
const height = this.collapsed ? TITLE_HEIGHT : this.headerHeight() + this.rowCount() * ROW_HEIGHT + PAD;
|
||||
// Sized before the texts: output labels are placed against the right edge.
|
||||
this.resize(this.collapsed ? this.collapsedWidth() : NODE_WIDTH, height);
|
||||
this.rebuildTexts();
|
||||
this.resize(NODE_WIDTH, height);
|
||||
this.position.set(node.x, node.y);
|
||||
}
|
||||
|
||||
private collapsedWidth(): number {
|
||||
const title = CanvasTextMetrics.measureText(this.def.label, textStyle('accent')).width;
|
||||
return collapsedNodeWidth(title, TITLE_X + COLLAPSED_TITLE_END, NODE_WIDTH);
|
||||
}
|
||||
|
||||
get collapsed(): boolean {
|
||||
return this.node.collapsed === true;
|
||||
}
|
||||
|
||||
chevronAt(localX: number, localY: number): boolean {
|
||||
return localY >= 0 && localY < TITLE_HEIGHT && localX >= PAD - 2 && localX < PAD + CHEVRON_SLOT;
|
||||
return localY >= 0 && localY < TITLE_HEIGHT && localX >= PAD - 2 && localX < TITLE_X;
|
||||
}
|
||||
|
||||
chevronGlobal(): { x: number; y: number } {
|
||||
@@ -233,10 +246,10 @@ export class NodeView extends Widget {
|
||||
add(
|
||||
this.def.label,
|
||||
'accent',
|
||||
PAD + CHEVRON_SLOT,
|
||||
TITLE_X,
|
||||
(TITLE_HEIGHT - 16) / 2,
|
||||
'left',
|
||||
NODE_WIDTH - PAD * 2 - CHEVRON_SLOT,
|
||||
this.w - TITLE_X - (this.collapsed ? COLLAPSED_TITLE_END : PAD),
|
||||
);
|
||||
if (this.collapsed) return;
|
||||
let y = TITLE_HEIGHT + PAD;
|
||||
|
||||
@@ -21,6 +21,11 @@ export function foldMarkKey(
|
||||
return `${side.node}\0${collapsed ? '' : side.port}\0${isOutput ? 'out' : 'in'}`;
|
||||
}
|
||||
|
||||
/** A collapsed node is as wide as its title needs, but never wider than an open one. */
|
||||
export function collapsedNodeWidth(titleWidth: number, chrome: number, openWidth: number): number {
|
||||
return Math.min(openWidth, Math.ceil(titleWidth + chrome));
|
||||
}
|
||||
|
||||
/** One name plus a count: three names do not fit, and the count is what says "there is more here". */
|
||||
export function foldMarkLabel(names: readonly string[]): string {
|
||||
if (names.length === 0) return '';
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,14 @@
|
||||
man kann ja nun graph nodes zusammenklappen.
|
||||
hier soll auch die breite veringert werden auf die titeltext breite wenn möglich?
|
||||
|
||||
---
|
||||
|
||||
EDIT:
|
||||
|
||||
speziell für "Get variable" node, "Set variable" node und der zukünftige "Variable" node:
|
||||
Es soll in der titelbar vom node der variablennamen (falls gesetzt!) angezeigt werden (egal ob zusammengeklappt oder nicht, ist unabhängig davon)
|
||||
|
||||
hier die beispiele:
|
||||
- "Get dummyX" (beim node "Get variable")
|
||||
- "Set dummyX" (beim node "Set variable")
|
||||
- "dummyX" (beim node "Variable")
|
||||
@@ -0,0 +1,9 @@
|
||||
Kann man einen quasi 2 in 1 node machen "Variable" - dieser soll Get/Set gleichzeichtig können
|
||||
- kommt ein exec flow rein, dann wird gesetzt (vom input port - wert oder edge)
|
||||
- get gibt es quasi immer, natürlich der neue wert falls er gesetzt wird
|
||||
|
||||
---
|
||||
|
||||
Umgesetzt: der Ausgang liest die Variable im Moment des Abrufs, nicht beim Schreiben.
|
||||
`Get variable` liest dagegen weiter einmal pro Puls — auf dieselbe Technik umstellen hiesse,
|
||||
bestehende Spiele im Verhalten zu aendern (Tetris liest das Brett deshalb zweimal).
|
||||
@@ -1,2 +0,0 @@
|
||||
man kann ja nun graph nodes zusammenklappen.
|
||||
hier soll auch die breite veringert werden auf die titeltext breite wenn möglich?
|
||||
+13
-2
@@ -780,13 +780,24 @@ export async function graph(t) {
|
||||
await click(chevron.x, chevron.y);
|
||||
await sleep(200);
|
||||
const shut = await json(`__gm.graph.nodeRect('n-move')`);
|
||||
const zoom = openRect.w / 176;
|
||||
check(
|
||||
'the chevron collapses the node to its title bar, and leaves the selection alone',
|
||||
(await nodeOf('n-move')).collapsed === true &&
|
||||
Math.abs(shut.h / shut.w - 24 / 176) < 0.01 &&
|
||||
Math.abs(shut.h - 24 * zoom) < 1 &&
|
||||
(await json('__gm.graph.selected()')).length === 0,
|
||||
JSON.stringify({ openRect, shut, collapsed: (await nodeOf('n-move')).collapsed }),
|
||||
);
|
||||
// Title starts 20 units in and ends 11 before the edge; `locate` matches exactly, so a cut title is null.
|
||||
const titleText = await json(`__gm.locate('Move')`);
|
||||
check(
|
||||
'a collapsed node narrows to its whole title, keeping its left edge under the chevron',
|
||||
titleText !== null &&
|
||||
Math.abs(shut.x - openRect.x) < 1 &&
|
||||
shut.w < openRect.w &&
|
||||
Math.abs(shut.w - (2 * (titleText.x - shut.x) - 9 * zoom)) < 2,
|
||||
JSON.stringify({ openRect, shut, titleText }),
|
||||
);
|
||||
const title = await json(`__gm.graph.nodeAt('n-move')`);
|
||||
const anchorOf = (port, out) => json(`__gm.graph.portAt('n-move', '${port}', ${out})`);
|
||||
const inputs = [
|
||||
@@ -894,7 +905,7 @@ export async function graph(t) {
|
||||
const reloaded = await json(`__gm.graph.nodeRect('n-move')`);
|
||||
check(
|
||||
'and a reloaded project draws it collapsed again',
|
||||
(await nodeOf('n-move')).collapsed === true && Math.abs(reloaded.h / reloaded.w - 24 / 176) < 0.01,
|
||||
(await nodeOf('n-move')).collapsed === true && Math.abs(reloaded.h / reloaded.w - shut.h / shut.w) < 0.01,
|
||||
JSON.stringify(reloaded),
|
||||
);
|
||||
const reopen = await json(`__gm.graph.chevronAt('n-move')`);
|
||||
|
||||
@@ -35,7 +35,22 @@ const logicGraph = {
|
||||
};
|
||||
|
||||
export async function variableNodes(t) {
|
||||
const { check, sleep, evaluate, json, shot, click, locate, undo, entity, asset, mutate, stop } = t;
|
||||
const {
|
||||
check,
|
||||
sleep,
|
||||
evaluate,
|
||||
json,
|
||||
shot,
|
||||
click,
|
||||
locate,
|
||||
undo,
|
||||
entity,
|
||||
asset,
|
||||
mutate,
|
||||
stop,
|
||||
typeText,
|
||||
press,
|
||||
} = t;
|
||||
const portAt = (nodeId, key, isOutput) =>
|
||||
json(`__gm.graph.portAt(${JSON.stringify(nodeId)}, ${JSON.stringify(key)}, ${isOutput})`);
|
||||
const portsOf = async (nodeId) =>
|
||||
@@ -216,6 +231,76 @@ export async function variableNodes(t) {
|
||||
JSON.stringify(helper),
|
||||
);
|
||||
|
||||
// 23i. The Variable node: picked from the palette, titled by the variable picked in the inspector,
|
||||
// and in the run `vz = vz + 1` once a frame – its own output feeding its input via Add.
|
||||
const nodeIds = async () => (await asset('vp-logic')).graph.nodes.map((n) => n.id);
|
||||
const idsBefore = await nodeIds();
|
||||
const centre = await json('__gm.graph.center()');
|
||||
// Below the Owner node; centre + (250, 250) would land on the Pack.
|
||||
await click(centre.x - 250, centre.y + 250, 'right');
|
||||
await sleep(150);
|
||||
// Its description is the one that says "any time"; "variable" would put Get variable first.
|
||||
await typeText('any time');
|
||||
await sleep(200);
|
||||
await press('Enter');
|
||||
await sleep(300);
|
||||
const added = (await asset('vp-logic')).graph.nodes.find((n) => !idsBefore.includes(n.id));
|
||||
check('the palette offers the Variable node', added?.type === 'Variable', JSON.stringify(added));
|
||||
const variableId = added?.id ?? '';
|
||||
const titleOf = (id) => json(`__gm.graph.nodeLabel(${JSON.stringify(id)})`);
|
||||
const untitled = await titleOf(variableId);
|
||||
// The node's own title says "Variable" too, and the canvas is drawn first.
|
||||
const picker = await json(`__gm.inspectorField('Variable', true)`);
|
||||
await click(picker.x, picker.y);
|
||||
await sleep(200);
|
||||
await clickLast('vz');
|
||||
await sleep(300);
|
||||
check(
|
||||
'picking its variable makes that the title, and drops the row that would repeat it',
|
||||
untitled === 'Variable' &&
|
||||
(await titleOf(variableId)) === 'vz' &&
|
||||
(await json(`__gm.graph.nodeSettings(${JSON.stringify(variableId)})`)).length === 0,
|
||||
JSON.stringify({ untitled, titled: await titleOf(variableId), picker }),
|
||||
);
|
||||
await shot('05-variable-node');
|
||||
|
||||
await mutate(`
|
||||
const g = __gm.state.project.assets.find((x) => x.id === 'vp-logic').graph;
|
||||
g.nodes.push(
|
||||
${JSON.stringify(node('vp-add', 'Add', 800, 200, { b: 1 }))},
|
||||
${JSON.stringify(node('vp-set-vx', 'SetVariable', 800, 400, { variable: 'vx' }))},
|
||||
${JSON.stringify(node('vp-set-gone', 'SetVariable', 800, 500, { variable: 'gone' }))},
|
||||
);
|
||||
g.edges.push(
|
||||
${JSON.stringify(edge('vp-e7', 'vp-move', 'exec', variableId, 'exec'))},
|
||||
${JSON.stringify(edge('vp-e8', variableId, 'value', 'vp-add', 'a'))},
|
||||
${JSON.stringify(edge('vp-e9', 'vp-add', 'result', variableId, 'value'))},
|
||||
);
|
||||
`);
|
||||
await sleep(300);
|
||||
const settingsOf = (id) => json(`__gm.graph.nodeSettings(${JSON.stringify(id)})`);
|
||||
const named = { title: await titleOf('vp-set-vx'), rows: await settingsOf('vp-set-vx') };
|
||||
const gone = { title: await titleOf('vp-set-gone'), rows: await settingsOf('vp-set-gone') };
|
||||
check(
|
||||
'Set variable names its variable too, and a deleted one keeps the row that marks it',
|
||||
named.title === 'Set vx' &&
|
||||
named.rows.length === 0 &&
|
||||
gone.title === 'Set gone' &&
|
||||
JSON.stringify(gone.rows) === '["Variable: gone?"]',
|
||||
JSON.stringify({ named, gone }),
|
||||
);
|
||||
|
||||
await t.play();
|
||||
await sleep(1000);
|
||||
const counted = (await entity('vp-brain'))?.props.vz;
|
||||
await stop();
|
||||
await sleep(300);
|
||||
check(
|
||||
'in the run it reads through its own output and writes the sum back, so vz climbs',
|
||||
typeof counted === 'number' && counted > 20,
|
||||
`vz = ${counted} after ~1 s`,
|
||||
);
|
||||
|
||||
await mutate(`
|
||||
const p = __gm.state.project;
|
||||
p.assets = p.assets.filter((a) => a.id !== 'vp-logic');
|
||||
|
||||
Reference in new issue
Block a user