Let a list hold structs, and read or write variables in bundles
A list item may now be a struct. The depth is bounded in the declaration
rather than by a comment: FieldKind (number|text|boolean|color) says what a
struct field holds, ListItemKind adds struct on top and says what a list
holds, so list -> struct -> primitive is the deepest storable shape and
sameValue stays finite. A struct item renders inline in the List tab, which
is why it needs no path in the slot address.
Four paths silently assumed a list item was a primitive: sameValue compared
items with ===, textOf would have printed [object Object], asListItem turned
a struct into text on Set item and Fill, and Index of used indexOf, which is
reference equality and could never have found a struct.
Tetris now runs on bundles. shapes is 112 {x, y} structs instead of 224 loose
numbers, piece and the candidate are structs, so Next move hands back a whole
candidate and each strand packs only the field it changes, since mergeStruct
leaves the rest standing. Declared behavior ports drop 49 -> 35, the master's
variables 18 -> 13, its graph 84/94 -> 69/76 nodes/edges.
Get variables and Set variables declare their ports from a list of variable
names, the way Pack and Expand already do, so one node can read or write
several at once. Renaming a variable rewires them, because there the port key
is the variable name.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
9d62dfea7b
commit
f60358c951
32 files changed
+1083
-253
No files matched your search
@@ -6,9 +6,20 @@ import { nodeDef, type NodeDef, type PortDef } from './NodeRegistry';
|
||||
export const BEHAVIOR_START = 'BehaviorStart';
|
||||
export const BEHAVIOR_END = 'BehaviorEnd';
|
||||
export const RUN_BEHAVIOR = 'RunBehavior';
|
||||
export const GET_VARIABLES = 'GetVariables';
|
||||
export const SET_VARIABLES = 'SetVariables';
|
||||
|
||||
export const BEHAVIOR_PARAM = 'behavior';
|
||||
|
||||
/**
|
||||
* Nodes whose declared ports are not names of their own but variables of the behavior around them.
|
||||
* Renaming a variable therefore has to reach them, and the inspector picks their ports from a list
|
||||
* instead of letting them be typed.
|
||||
*/
|
||||
export function declaresVariablePorts(type: string): boolean {
|
||||
return type === GET_VARIABLES || type === SET_VARIABLES;
|
||||
}
|
||||
|
||||
/** Identifier-like, and not `exec`: that key belongs to the fixed flow port of every terminal node. */
|
||||
export function isValidPortName(name: string): boolean {
|
||||
return /^[A-Za-z_][A-Za-z0-9_]*$/.test(name) && name !== 'exec';
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { sameValue } from '../../../model/Properties';
|
||||
import type { NodeDef } from '../../NodeRegistry';
|
||||
import { asList, asListItem } from '../../values';
|
||||
import { any, list, num } from '../shared';
|
||||
@@ -10,7 +11,9 @@ export const IndexOf: NodeDef = {
|
||||
inputs: [list('list'), any('value')],
|
||||
outputs: [num('index')],
|
||||
params: [],
|
||||
run: (_ctx, inputs) => ({
|
||||
outputs: { index: asList(inputs.list).indexOf(asListItem(inputs.value)) },
|
||||
}),
|
||||
// Compared by content, not by identity: two equal structs are never the same object.
|
||||
run: (_ctx, inputs) => {
|
||||
const wanted = asListItem(inputs.value);
|
||||
return { outputs: { index: asList(inputs.list).findIndex((item) => sameValue(item, wanted)) } };
|
||||
},
|
||||
};
|
||||
@@ -82,7 +82,9 @@ import { Repeat } from './flow/Repeat';
|
||||
import { RunBehavior } from './flow/RunBehavior';
|
||||
import { Sequence } from './flow/Sequence';
|
||||
import { GetVariable } from './variable/GetVariable';
|
||||
import { GetVariables } from './variable/GetVariables';
|
||||
import { SetVariable } from './variable/SetVariable';
|
||||
import { SetVariables } from './variable/SetVariables';
|
||||
|
||||
/**
|
||||
* Importing this module registers the built-in nodes. The palette sorts by category and label, so
|
||||
@@ -109,7 +111,9 @@ export const BUILT_IN_NODES: readonly NodeDef[] = [
|
||||
BehaviorEnd,
|
||||
// variables
|
||||
GetVariable,
|
||||
GetVariables,
|
||||
SetVariable,
|
||||
SetVariables,
|
||||
// data: references and properties
|
||||
Self,
|
||||
Owner,
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import type { NodeDef } from '../../NodeRegistry';
|
||||
import { GET_VARIABLES } from '../../nested';
|
||||
import type { Value } from '../../values';
|
||||
|
||||
/**
|
||||
* The plural of Get variable: one output per variable picked, so a graph reading a handful of them
|
||||
* is one node instead of a column of five. The ports are declared on the node, the way Expand
|
||||
* declares the fields of a struct.
|
||||
*/
|
||||
export const GetVariables: NodeDef = {
|
||||
type: GET_VARIABLES,
|
||||
label: 'Get variables',
|
||||
category: 'variable',
|
||||
description: "Reads several of this behavior's variables at once, one output per variable.",
|
||||
inputs: [],
|
||||
outputs: [],
|
||||
params: [],
|
||||
declaresPorts: 'outputs',
|
||||
run: (ctx, _inputs, _params, def) => {
|
||||
const outputs: Record<string, Value> = {};
|
||||
for (const port of def.outputs) outputs[port.key] = ctx.variables[port.key] ?? null;
|
||||
return { outputs };
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
import type { NodeDef } from '../../NodeRegistry';
|
||||
import { SET_VARIABLES } from '../../nested';
|
||||
import { writeTyped } from '../../values';
|
||||
import { exec } from '../shared';
|
||||
|
||||
/**
|
||||
* The plural of Set variable: one input per variable picked. An input left unconnected writes the
|
||||
* value typed on the node, exactly as it does on the singular node – the node writes every variable
|
||||
* it lists, every time it runs.
|
||||
*/
|
||||
export const SetVariables: NodeDef = {
|
||||
type: SET_VARIABLES,
|
||||
label: 'Set variables',
|
||||
category: 'variable',
|
||||
description: "Writes several of this behavior's variables at once, one input per variable.",
|
||||
inputs: [exec()],
|
||||
outputs: [exec()],
|
||||
params: [],
|
||||
declaresPorts: 'inputs',
|
||||
run: (ctx, inputs, _params, def) => {
|
||||
for (const port of def.inputs)
|
||||
if (port.type !== 'exec') writeTyped(ctx.variables, port.key, inputs[port.key]);
|
||||
return { next: 'exec' };
|
||||
},
|
||||
};
|
||||
@@ -39,16 +39,21 @@ export function asList(value: Value | undefined): ListValue {
|
||||
return [value];
|
||||
}
|
||||
|
||||
/** A value narrowed to what a list may hold: handles and nested lists have no stored form. */
|
||||
export function asListItem(value: Value | undefined): number | string | boolean {
|
||||
if (typeof value === 'number' || typeof value === 'boolean' || typeof value === 'string') return value;
|
||||
if (value === null || value === undefined) return 0;
|
||||
return asString(value);
|
||||
/** A value narrowed to what a list may hold: a struct stays one, handles and lists flatten. */
|
||||
export function asListItem(value: Value | undefined): number | string | boolean | StructValue {
|
||||
if (isStructValue(value)) return value;
|
||||
return asPrimitive(value);
|
||||
}
|
||||
|
||||
/** A value narrowed to what a struct field may hold: a list stays a list, anything deeper flattens. */
|
||||
export function asField(value: Value | undefined): FieldValue {
|
||||
return isList(value) ? value : asListItem(value);
|
||||
return isList(value) ? value : asPrimitive(value);
|
||||
}
|
||||
|
||||
function asPrimitive(value: Value | undefined): number | string | boolean {
|
||||
if (typeof value === 'number' || typeof value === 'boolean' || typeof value === 'string') return value;
|
||||
if (value === null || value === undefined) return 0;
|
||||
return asString(value);
|
||||
}
|
||||
|
||||
export function asBoolean(value: Value | undefined): boolean {
|
||||
|
||||
Reference in new issue
Block a user