diff --git a/README.md b/README.md index 0b4ed385..22479b09 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/src/core/graph/GraphInterpreter.ts b/src/core/graph/GraphInterpreter.ts index 73dba273..e14a9f7d 100644 --- a/src/core/graph/GraphInterpreter.ts +++ b/src/core/graph/GraphInterpreter.ts @@ -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) diff --git a/src/core/graph/NodeRegistry.ts b/src/core/graph/NodeRegistry.ts index 010e9a53..7eb5cce6 100644 --- a/src/core/graph/NodeRegistry.ts +++ b/src/core/graph/NodeRegistry.ts @@ -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; /** * 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. diff --git a/src/core/graph/nodes/index.ts b/src/core/graph/nodes/index.ts index 56ca86c1..3ee31a8c 100644 --- a/src/core/graph/nodes/index.ts +++ b/src/core/graph/nodes/index.ts @@ -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, diff --git a/src/core/graph/nodes/shared.ts b/src/core/graph/nodes/shared.ts index 58b8a00a..00712090 100644 --- a/src/core/graph/nodes/shared.ts +++ b/src/core/graph/nodes/shared.ts @@ -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 => ({ + value: ctx.variables[variableKey(params)] ?? null, +}); + +export function writeVariable( + ctx: NodeContext, + inputs: Record, + 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: '' }; diff --git a/src/core/graph/nodes/variable/GetVariable.ts b/src/core/graph/nodes/variable/GetVariable.ts index a9f4b971..2e72e33e 100644 --- a/src/core/graph/nodes/variable/GetVariable.ts +++ b/src/core/graph/nodes/variable/GetVariable.ts @@ -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) }), }; diff --git a/src/core/graph/nodes/variable/SetVariable.ts b/src/core/graph/nodes/variable/SetVariable.ts index 10f3f5c3..a8f72b65 100644 --- a/src/core/graph/nodes/variable/SetVariable.ts +++ b/src/core/graph/nodes/variable/SetVariable.ts @@ -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, }; diff --git a/src/core/graph/nodes/variable/Variable.ts b/src/core/graph/nodes/variable/Variable.ts new file mode 100644 index 00000000..b7881137 --- /dev/null +++ b/src/core/graph/nodes/variable/Variable.ts @@ -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, +}; diff --git a/src/editor/debugHook.ts b/src/editor/debugHook.ts index 5e252458..5f231caf 100644 --- a/src/editor/debugHook.ts +++ b/src/editor/debugHook.ts @@ -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; diff --git a/src/editor/graph/NodeView.ts b/src/editor/graph/NodeView.ts index a80c9629..85c5c921 100644 --- a/src/editor/graph/NodeView.ts +++ b/src/editor/graph/NodeView.ts @@ -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; diff --git a/src/editor/graph/folding.ts b/src/editor/graph/folding.ts index bc6d8175..bb119d92 100644 --- a/src/editor/graph/folding.ts +++ b/src/editor/graph/folding.ts @@ -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 ''; diff --git a/tests/core/collapsed-node.test.ts b/tests/core/collapsed-node.test.ts index 7c320211..179c892a 100644 --- a/tests/core/collapsed-node.test.ts +++ b/tests/core/collapsed-node.test.ts @@ -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); diff --git a/tests/core/node-catalog.test.ts b/tests/core/node-catalog.test.ts index bf3f501a..3d5d043b 100644 --- a/tests/core/node-catalog.test.ts +++ b/tests/core/node-catalog.test.ts @@ -150,6 +150,22 @@ const CASES: Record = { 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 diff --git a/tests/core/variable-node.test.ts b/tests/core/variable-node.test.ts new file mode 100644 index 00000000..65dcab8a --- /dev/null +++ b/tests/core/variable-node.test.ts @@ -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, 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'); + }); +}); diff --git a/todo-notes/done/graph-node-collapse-width.md b/todo-notes/done/graph-node-collapse-width.md new file mode 100644 index 00000000..415c6396 --- /dev/null +++ b/todo-notes/done/graph-node-collapse-width.md @@ -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") diff --git a/todo-notes/done/merge-get-set-variable.md b/todo-notes/done/merge-get-set-variable.md new file mode 100644 index 00000000..f546bc10 --- /dev/null +++ b/todo-notes/done/merge-get-set-variable.md @@ -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). diff --git a/todo-notes/graph-node-collapse-width.md b/todo-notes/graph-node-collapse-width.md deleted file mode 100644 index 19f83ab1..00000000 --- a/todo-notes/graph-node-collapse-width.md +++ /dev/null @@ -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? diff --git a/tools/e2e/06-graph.mjs b/tools/e2e/06-graph.mjs index 87899a31..d0582a73 100644 --- a/tools/e2e/06-graph.mjs +++ b/tools/e2e/06-graph.mjs @@ -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')`); diff --git a/tools/e2e/23-variable-nodes.mjs b/tools/e2e/23-variable-nodes.mjs index ac9ff326..617ff2ef 100644 --- a/tools/e2e/23-variable-nodes.mjs +++ b/tools/e2e/23-variable-nodes.mjs @@ -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');