A single node now lists any number of variables. Every entry is an output that reads the current value whenever it is pulled; an entry switched to Set is also an input and is written when the flow arrives. Exec pins only exist while at least one entry writes, and all inputs are read before any write, so one node can swap two variables. Get/Set variable, Get/Set variables and Variable are gone. Old project files are converted on load: each old node becomes a Variables node with the same variables, edges and typed values. A written port without a typed value now shows the zero of its kind on the node body, matching what the inspector shows and what the run writes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
219 lines
8.5 KiB
JavaScript
219 lines
8.5 KiB
JavaScript
import { edge, node, port, variable, written } from '../lib/builders.mjs';
|
||
import { MAKE_OBJECT } from './lib/scaffold.mjs';
|
||
|
||
/**
|
||
* Get properties / Set properties: several properties of an object in one node, picked the way
|
||
* the Variables node picks its variables. A built-in property keeps its own field and default; a name the
|
||
* list does not know is typed. The step brings its own box and behavior and removes them again.
|
||
*/
|
||
|
||
/** `On update → Set properties{width 40, color red} on the owner`, then its width read back into `seen`. */
|
||
const logicGraph = {
|
||
nodes: [
|
||
node('pp-update', 'OnUpdate', 0, 0),
|
||
node('pp-owner', 'Owner', 0, 240),
|
||
node('pp-set', 'SetProperties', 260, 0, { width: 40, color: 0xff0000 }, [
|
||
port('width', 'number'),
|
||
port('color', 'number'),
|
||
]),
|
||
node('pp-get', 'GetProperties', 260, 300, {}, [port('width', 'number')]),
|
||
node('pp-store', 'Variables', 560, 0, {}, [written('seen', 'number')]),
|
||
node('pp-single', 'SetProperty', 560, 300, { property: 'width' }),
|
||
],
|
||
edges: [
|
||
edge('pp-e1', 'pp-update', 'exec', 'pp-set', 'exec'),
|
||
edge('pp-e2', 'pp-owner', 'object', 'pp-set', 'object'),
|
||
edge('pp-e3', 'pp-set', 'exec', 'pp-store', 'exec'),
|
||
edge('pp-e4', 'pp-owner', 'object', 'pp-get', 'object'),
|
||
edge('pp-e5', 'pp-get', 'width', 'pp-store', 'seen'),
|
||
],
|
||
};
|
||
|
||
export async function propertyNodes(t) {
|
||
const { check, sleep, evaluate, json, shot, click, doubleClick, locate, find, undo, entity, asset } = t;
|
||
const { mutate, stop, typeText, press } = t;
|
||
const portsOf = async (nodeId) =>
|
||
((await asset('pp-logic')).graph.nodes.find((n) => n.id === nodeId).ports ?? []).map(
|
||
(p) => `${p.key}:${p.kind}`,
|
||
);
|
||
const select = async (nodeId) => {
|
||
await evaluate(
|
||
`__gm.state.select({ kind: 'node', assetId: 'pp-logic', nodeId: ${JSON.stringify(nodeId)}, nodeIds: [${JSON.stringify(nodeId)}] })`,
|
||
);
|
||
await sleep(300);
|
||
};
|
||
/** The inspector is drawn over the graph, so the last match is the row rather than the port. */
|
||
const clickLast = async (text) => {
|
||
const p = await locate(text, true);
|
||
await click(p.x, p.y);
|
||
await sleep(200);
|
||
};
|
||
const addProperty = async (search) => {
|
||
await clickLast('+ Property');
|
||
await typeText(search);
|
||
await sleep(150);
|
||
await press('Enter');
|
||
await sleep(300);
|
||
};
|
||
/** The name field, not the node's port or the unconnected-input row below it that say the same. */
|
||
const rename = async (from, to) => {
|
||
const canvas = await json('__gm.graph.canvasRect()');
|
||
const hits = await json(`__gm.locateAll(${JSON.stringify(from)})`);
|
||
const field = hits.find((p) => p.x > canvas.x + canvas.w);
|
||
await doubleClick(field.x, field.y);
|
||
await typeText(to);
|
||
await press('Enter');
|
||
await sleep(300);
|
||
};
|
||
|
||
// 24a. A box wearing a behavior that reshapes it through one node.
|
||
await mutate(`
|
||
const p = __gm.state.project;
|
||
const scene = __gm.state.activeScene;
|
||
const a = structuredClone(p.assets.find((x) => x.id === 'mover'));
|
||
Object.assign(a, {
|
||
id: 'pp-logic',
|
||
name: 'Reshape',
|
||
props: { enabled: true, runInEditor: false, seen: 0 },
|
||
variables: ${JSON.stringify([variable('seen', 'number', 0)])},
|
||
graph: ${JSON.stringify(logicGraph)},
|
||
});
|
||
p.assets.unshift(a);
|
||
${MAKE_OBJECT}
|
||
scene.objects.push(make('pp-box', 'crate', '', 900, 'Reshaped box'), make('pp-brain', 'pp-logic', 'pp-box', 900, 'Reshape'));
|
||
`);
|
||
await sleep(400);
|
||
|
||
// 24b. The run: one node writes both properties, another reads one of them back.
|
||
await t.play();
|
||
await sleep(400);
|
||
const box = (await entity('pp-box'))?.props;
|
||
const seen = (await entity('pp-brain'))?.props.seen;
|
||
await stop();
|
||
await sleep(300);
|
||
check(
|
||
'one node writes several properties, another reads them back',
|
||
box?.width === 40 && box?.color === 0xff0000 && seen === 40,
|
||
JSON.stringify({ width: box?.width, color: box?.color, seen }),
|
||
);
|
||
|
||
// 24c. A typed-in value reads in the words of its property: a color as its hex.
|
||
await select('pp-set');
|
||
check(
|
||
'the node shows a typed-in color as a color',
|
||
(await find('color #ff0000')) !== null,
|
||
JSON.stringify(await json(`__gm.graph.nodeLabel('pp-set')`)),
|
||
);
|
||
await shot('01-property-ports');
|
||
|
||
// 24d. The list names the type each property belongs to; a key several types share is there once
|
||
// per type, and taken for all of them once picked.
|
||
await clickLast('+ Property');
|
||
const offered = await json('__gm.overlayTexts()');
|
||
await press('Escape');
|
||
await sleep(200);
|
||
check(
|
||
'the list names each property with its type',
|
||
['Object enabled', 'Sprite height', 'Camera zoom', 'Text align'].every((l) => offered.includes(l)) &&
|
||
!offered.includes('height'),
|
||
offered.join(' | '),
|
||
);
|
||
|
||
// 24e. Picked from the built-in properties, and an unconnected one starts at that property's
|
||
// default – a height of 100, not a bare 0 that would flatten the box.
|
||
await addProperty('height');
|
||
const grown = await portsOf('pp-set');
|
||
check(
|
||
'picking a property adds it as a port',
|
||
grown.join(',') === 'width:number,color:number,height:number',
|
||
grown.join(','),
|
||
);
|
||
check(
|
||
"and its typed-in value starts at the property's own default",
|
||
(await find('height 100')) !== null,
|
||
JSON.stringify(await find('height 0')),
|
||
);
|
||
await shot('02-property-added');
|
||
await undo();
|
||
await sleep(300);
|
||
check('undo takes it back', (await portsOf('pp-set')).length === 2);
|
||
|
||
// 24f. A name the list does not know – a variable of another behavior's object – is typed, with
|
||
// a kind of its own. Typing a built-in name turns it into that property, kind and all.
|
||
await addProperty('custom text');
|
||
check('a custom port is named and typed by hand', (await portsOf('pp-set'))[2] === 'property1:text');
|
||
await rename('property1', 'speed');
|
||
check(
|
||
'and renamed by typing',
|
||
(await portsOf('pp-set'))[2] === 'speed:text',
|
||
(await portsOf('pp-set')).join(),
|
||
);
|
||
await rename('speed', 'radius');
|
||
check(
|
||
'typing a built-in name makes it that property, with its kind',
|
||
(await portsOf('pp-set'))[2] === 'radius:number',
|
||
(await portsOf('pp-set')).join(),
|
||
);
|
||
await shot('03-custom-renamed');
|
||
|
||
await addProperty('custom number');
|
||
await evaluate("__gm.state.notice = ''");
|
||
await rename('property1', 'object');
|
||
const notice = await json('__gm.state.notice');
|
||
check(
|
||
'the object input keeps its key: a port cannot take it',
|
||
(await portsOf('pp-set'))[3] === 'property1:number' && notice.includes('"object" is taken'),
|
||
JSON.stringify({ ports: await portsOf('pp-set'), notice }),
|
||
);
|
||
|
||
// 24g. Get properties grows outputs from the same list.
|
||
await select('pp-get');
|
||
await addProperty('color');
|
||
check(
|
||
'Get properties picks from the same list, as outputs',
|
||
(await portsOf('pp-get')).join(',') === 'width:number,color:number' &&
|
||
Boolean(await json(`__gm.graph.portAt('pp-get', 'color', true)`)),
|
||
(await portsOf('pp-get')).join(','),
|
||
);
|
||
|
||
// 24h. The single Set property names its property by text; the button beside it suggests the
|
||
// same list, and picking an entry writes its key.
|
||
await select('pp-single');
|
||
const param = async () =>
|
||
(await asset('pp-logic')).graph.nodes.find((n) => n.id === 'pp-single').params.property;
|
||
const field = await json(`__gm.inspectorField('Property', true)`);
|
||
await click(2 * field.x - field.left - 14, field.y);
|
||
await sleep(200);
|
||
const suggested = await json('__gm.overlayTexts()');
|
||
check(
|
||
'the property setting suggests the built-in properties, the current one marked',
|
||
suggested.includes('● Sprite width') && suggested.includes('Text align'),
|
||
suggested.join(' | '),
|
||
);
|
||
await shot('04-property-suggestions');
|
||
await typeText('text align');
|
||
await sleep(150);
|
||
await press('Enter');
|
||
await sleep(300);
|
||
check(
|
||
'picking a suggestion writes its key',
|
||
(await param()) === 'align' && (await find('Property: align')) !== null,
|
||
JSON.stringify(await param()),
|
||
);
|
||
await undo();
|
||
await sleep(300);
|
||
check('undo takes it back', (await param()) === 'width', JSON.stringify(await param()));
|
||
|
||
await mutate(`
|
||
const p = __gm.state.project;
|
||
p.assets = p.assets.filter((a) => a.id !== 'pp-logic');
|
||
const scene = __gm.state.activeScene;
|
||
scene.objects = scene.objects.filter((o) => o.id !== 'pp-box' && o.id !== 'pp-brain');
|
||
__gm.state.select(null);
|
||
__gm.state.preview();
|
||
`);
|
||
await sleep(300);
|
||
const left = await json('__gm.state.project.assets.filter((a) => a.id === "pp-logic").length');
|
||
check('the step leaves the fixture as it found it', left === 0, String(left));
|
||
}
|