Replace the property nodes with one Properties node
Get/Set property, Get/Set properties, Set enabled and Set text are gone. A single Properties node takes a fixed "object" input; every chosen property is an output that reads the current value, and one switched to Set is also an input written when the flow arrives. Exec pins only exist while at least one entry writes. The node carries a type (Sprite, Text, Collider, Camera, Container, Behavior, Any) that decides what "+ Property" offers: the fields of that type, the variables of the chosen behavior, or for Any a kind to pick and a name to type. Each port takes the default and field of its type, so a Camera width starts at 640. Switching the type keeps the ports and marks those the new type lacks. At runtime the type is only an editor aid; the node writes to whatever object it gets. Old project files are converted on load. The type is inferred from the keys: the first object type that has them all, else the one behavior that declares them all, else Any. enabled belongs to every object, so a converted Set enabled lands on Any. The jump-run and tetris seeders use Properties throughout; draw-piece switches its cell on in the same node that sets the colour. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
7c63c884ef
commit
58f19a3451
36 files changed
+1040
-719
No files matched your search
+110
-88
@@ -1,31 +1,30 @@
|
||||
import { edge, node, port, variable, written } from '../lib/builders.mjs';
|
||||
import { edge, node, propertiesOf, 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.
|
||||
* Properties: properties of the object handed in, each read at any time and written where it is
|
||||
* switched to Set. Its type decides what `+ Property` offers. 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`. */
|
||||
/** `On update → Properties{width 40, color red} on the owner`, whose width is then read 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-set', 'Properties', 260, 0, { ...propertiesOf('sprite'), width: 40, color: 0xff0000 }, [
|
||||
written('width', 'number'),
|
||||
written('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' }),
|
||||
node('pp-any', 'Properties', 260, 420),
|
||||
node('pp-beh', 'Properties', 560, 420, propertiesOf('behavior')),
|
||||
],
|
||||
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'),
|
||||
edge('pp-e4', 'pp-set', 'width', 'pp-store', 'seen'),
|
||||
],
|
||||
};
|
||||
|
||||
@@ -34,8 +33,9 @@ export async function propertyNodes(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}`,
|
||||
(p) => `${p.key}:${p.kind}${p.write ? ':set' : ''}`,
|
||||
);
|
||||
const title = (nodeId) => json(`__gm.graph.nodeLabel(${JSON.stringify(nodeId)})`);
|
||||
const select = async (nodeId) => {
|
||||
await evaluate(
|
||||
`__gm.state.select({ kind: 'node', assetId: 'pp-logic', nodeId: ${JSON.stringify(nodeId)}, nodeIds: [${JSON.stringify(nodeId)}] })`,
|
||||
@@ -48,6 +48,13 @@ export async function propertyNodes(t) {
|
||||
await click(p.x, p.y);
|
||||
await sleep(200);
|
||||
};
|
||||
const offered = async () => {
|
||||
await clickLast('+ Property');
|
||||
const texts = await json('__gm.overlayTexts()');
|
||||
await press('Escape');
|
||||
await sleep(200);
|
||||
return texts;
|
||||
};
|
||||
const addProperty = async (search) => {
|
||||
await clickLast('+ Property');
|
||||
await typeText(search);
|
||||
@@ -55,6 +62,13 @@ export async function propertyNodes(t) {
|
||||
await press('Enter');
|
||||
await sleep(300);
|
||||
};
|
||||
const choose = async (setting, option) => {
|
||||
const field = await json(`__gm.inspectorField(${JSON.stringify(setting)}, true)`);
|
||||
await click(field.x, field.y);
|
||||
await sleep(200);
|
||||
await clickLast(option);
|
||||
await sleep(200);
|
||||
};
|
||||
/** 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()');
|
||||
@@ -84,7 +98,7 @@ export async function propertyNodes(t) {
|
||||
`);
|
||||
await sleep(400);
|
||||
|
||||
// 24b. The run: one node writes both properties, another reads one of them back.
|
||||
// 24b. The run: one node writes both properties, and its own output reads the width back after.
|
||||
await t.play();
|
||||
await sleep(400);
|
||||
const box = (await entity('pp-box'))?.props;
|
||||
@@ -92,117 +106,125 @@ export async function propertyNodes(t) {
|
||||
await stop();
|
||||
await sleep(300);
|
||||
check(
|
||||
'one node writes several properties, another reads them back',
|
||||
'one node writes several properties and 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.
|
||||
// 24c. Named after its type; the type is chosen above the list it decides.
|
||||
await select('pp-set');
|
||||
const typeRow = await json(`__gm.inspectorField('Type', true)`);
|
||||
const header = await locate('PROPERTIES', true);
|
||||
check(
|
||||
'the node is named after its type, chosen above its properties',
|
||||
(await title('pp-set')) === 'Sprite properties' && typeRow && typeRow.y < header.y,
|
||||
JSON.stringify({ title: await title('pp-set'), type: typeRow?.y, header: header.y }),
|
||||
);
|
||||
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')`)),
|
||||
JSON.stringify(await json(`__gm.graph.nodeInputs('pp-set')`)),
|
||||
);
|
||||
await shot('01-property-ports');
|
||||
await shot('01-sprite-properties');
|
||||
|
||||
// 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);
|
||||
// 24d. A Sprite offers the fields of a sprite, and nothing of the other types.
|
||||
const sprite = await offered();
|
||||
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(' | '),
|
||||
"a Sprite offers a sprite's properties",
|
||||
['enabled', 'width', 'height', 'color', 'texture'].every((k) => sprite.includes(k)) &&
|
||||
!sprite.includes('radius') &&
|
||||
!sprite.includes('zoom'),
|
||||
sprite.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.
|
||||
// 24e. Picked, then switched to Set: its typed-in value starts at the property's own 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',
|
||||
'picking a property adds it as an output',
|
||||
grown.join(',') === 'width:number:set,color:number:set,height:number',
|
||||
grown.join(','),
|
||||
);
|
||||
const toggle = await json(`__gm.inspectorField('Set', true)`);
|
||||
await click(toggle.x, toggle.y);
|
||||
await sleep(300);
|
||||
const inputs = await json(`__gm.graph.nodeInputs('pp-set')`);
|
||||
check(
|
||||
"and its typed-in value starts at the property's own default",
|
||||
(await find('height 100')) !== null,
|
||||
JSON.stringify(await find('height 0')),
|
||||
"switched to Set, its typed-in value starts at the property's own default",
|
||||
inputs.includes('height 100'),
|
||||
JSON.stringify(inputs),
|
||||
);
|
||||
await shot('02-property-added');
|
||||
await shot('02-height-set');
|
||||
await undo();
|
||||
await undo();
|
||||
await sleep(300);
|
||||
check('undo takes it back', (await portsOf('pp-set')).length === 2);
|
||||
check('undo takes both back', (await portsOf('pp-set')).length === 2, (await portsOf('pp-set')).join());
|
||||
|
||||
// 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');
|
||||
// 24f. Another type offers its own fields; a port it does not have is marked, not dropped.
|
||||
await choose('Type', 'Camera');
|
||||
const camera = await offered();
|
||||
check(
|
||||
'and renamed by typing',
|
||||
(await portsOf('pp-set'))[2] === 'speed:text',
|
||||
'switching to Camera renames the node and offers the fields of a camera',
|
||||
(await title('pp-set')) === 'Camera properties' && camera.includes('zoom') && !camera.includes('texture'),
|
||||
JSON.stringify({ title: await title('pp-set'), offered: camera }),
|
||||
);
|
||||
check(
|
||||
'a port the new type does not have stays, marked',
|
||||
(await portsOf('pp-set')).length === 2 && (await find('"color" is not a property of a Camera.')) !== null,
|
||||
(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 shot('03-camera');
|
||||
await undo();
|
||||
await sleep(300);
|
||||
check('undo switches back', (await title('pp-set')) === 'Sprite properties', await title('pp-set'));
|
||||
|
||||
await addProperty('custom number');
|
||||
// 24g. Any names its ports by hand, and the kind is picked when adding one. A name that happens
|
||||
// to be a property's stays of the kind picked.
|
||||
await select('pp-any');
|
||||
const kinds = await offered();
|
||||
check(
|
||||
'Any offers the kinds to pick from, not property names',
|
||||
['Number', 'Text', 'Boolean', 'List', 'Struct'].every((k) => kinds.includes(k)) &&
|
||||
!kinds.includes('width'),
|
||||
kinds.join(' | '),
|
||||
);
|
||||
await addProperty('text');
|
||||
check('a port is added of the kind picked', (await portsOf('pp-any'))[0] === 'property1:text');
|
||||
await rename('property1', 'width');
|
||||
check(
|
||||
'and renamed by typing, keeping its kind',
|
||||
(await portsOf('pp-any'))[0] === 'width:text',
|
||||
(await portsOf('pp-any')).join(),
|
||||
);
|
||||
await evaluate("__gm.state.notice = ''");
|
||||
await rename('property1', 'object');
|
||||
await rename('width', '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 }),
|
||||
(await portsOf('pp-any'))[0] === 'width:text' && notice.includes('"object" is taken'),
|
||||
JSON.stringify({ ports: await portsOf('pp-any'), notice }),
|
||||
);
|
||||
await shot('04-any');
|
||||
|
||||
// 24g. Get properties grows outputs from the same list.
|
||||
await select('pp-get');
|
||||
await addProperty('color');
|
||||
// 24h. A Behavior offers the variables of the behavior picked.
|
||||
await select('pp-beh');
|
||||
const before = await offered();
|
||||
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(','),
|
||||
'a Behavior offers nothing until one is picked',
|
||||
before.length === 0 && (await find('Pick a behavior first.')) !== null,
|
||||
JSON.stringify(before),
|
||||
);
|
||||
|
||||
// 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()');
|
||||
await choose('Behavior', 'Reshape (behavior)');
|
||||
const vars = await offered();
|
||||
check(
|
||||
'the property setting suggests the built-in properties, the current one marked',
|
||||
suggested.includes('● Sprite width') && suggested.includes('Text align'),
|
||||
suggested.join(' | '),
|
||||
'picking the behavior offers its variables and names the node after it',
|
||||
vars.includes('seen') && vars.includes('enabled') && (await title('pp-beh')) === 'Reshape properties',
|
||||
JSON.stringify({ title: await title('pp-beh'), offered: vars }),
|
||||
);
|
||||
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 addProperty('seen');
|
||||
check('a variable is picked as a port', (await portsOf('pp-beh')).join() === 'seen:number');
|
||||
await shot('05-behavior');
|
||||
|
||||
await mutate(`
|
||||
const p = __gm.state.project;
|
||||
|
||||
Reference in new issue
Block a user