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
@@ -49,20 +49,32 @@ export type PropertyDef =
|
||||
* list, so one `list` port fits them all. There is no `object` item kind: a list is stored in the
|
||||
* project file, and an object is a live handle that no reload survives.
|
||||
*/
|
||||
| { key: string; label: string; kind: 'list'; default: ListValue; items: ListItemKind }
|
||||
| {
|
||||
key: string;
|
||||
label: string;
|
||||
kind: 'list';
|
||||
default: ListValue;
|
||||
items: ListItemKind;
|
||||
/** The shape of every item, and only for `items: 'struct'` – a list of structs is homogeneous. */
|
||||
fields?: readonly StructFieldDef[];
|
||||
}
|
||||
/**
|
||||
* `fields` is the declaration; the value carries exactly those keys, which is what lets a struct
|
||||
* keep its shape while single fields are written (`mergeStruct`).
|
||||
*/
|
||||
| { key: string; label: string; kind: 'struct'; default: StructValue; fields: readonly StructFieldDef[] };
|
||||
|
||||
export const LIST_ITEM_KINDS = ['number', 'text', 'boolean', 'color'] as const;
|
||||
/** What a struct field holds. No `struct`: that is the rule that bounds the nesting depth. */
|
||||
export const FIELD_KINDS = ['number', 'text', 'boolean', 'color'] as const;
|
||||
export type FieldKind = (typeof FIELD_KINDS)[number];
|
||||
|
||||
/** What a list holds – a field's kinds plus `struct`, so a list of bundles is one declaration. */
|
||||
export const LIST_ITEM_KINDS = [...FIELD_KINDS, 'struct'] as const;
|
||||
export type ListItemKind = (typeof LIST_ITEM_KINDS)[number];
|
||||
|
||||
/** A field holds what a list item holds: a field that were itself a struct is what `FieldValue` rules out. */
|
||||
export interface StructFieldDef {
|
||||
key: string;
|
||||
kind: ListItemKind;
|
||||
kind: FieldKind;
|
||||
}
|
||||
|
||||
export type StructDef = Extract<PropertyDef, { kind: 'struct' }>;
|
||||
@@ -200,14 +212,19 @@ export function variableDef(
|
||||
return { key, label: key, kind, default: Boolean(defaultValue ?? false) };
|
||||
case 'text':
|
||||
return { key, label: key, kind, default: defaultValue === undefined ? '' : textOf(defaultValue) };
|
||||
case 'list':
|
||||
case 'list': {
|
||||
const item = items ?? 'number';
|
||||
const shape = item === 'struct' ? (fields ?? []) : undefined;
|
||||
const raw = isList(defaultValue) ? defaultValue : [];
|
||||
return {
|
||||
key,
|
||||
label: key,
|
||||
kind,
|
||||
default: isList(defaultValue) ? defaultValue : [],
|
||||
items: items ?? 'number',
|
||||
default: raw.map((entry) => coerceListItem(entry, item, shape)),
|
||||
items: item,
|
||||
...(shape ? { fields: shape } : {}),
|
||||
};
|
||||
}
|
||||
case 'struct': {
|
||||
const declared = fields ?? [];
|
||||
return { key, label: key, kind, fields: declared, default: structOf(declared, defaultValue) };
|
||||
@@ -216,14 +233,14 @@ export function variableDef(
|
||||
}
|
||||
|
||||
/** The empty value of a kind, so a field that is not there yet still has one. */
|
||||
const EMPTY_FIELD: Record<ListItemKind, number | string | boolean> = {
|
||||
const EMPTY_FIELD: Record<FieldKind, number | string | boolean> = {
|
||||
number: 0,
|
||||
text: '',
|
||||
boolean: false,
|
||||
color: 0,
|
||||
};
|
||||
|
||||
export function emptyField(kind: ListItemKind): number | string | boolean {
|
||||
export function emptyField(kind: FieldKind): number | string | boolean {
|
||||
return EMPTY_FIELD[kind];
|
||||
}
|
||||
|
||||
@@ -236,7 +253,7 @@ export function structOf(fields: readonly StructFieldDef[], from: PropertyValue
|
||||
const value: Record<string, FieldValue> = {};
|
||||
for (const field of fields)
|
||||
value[field.key] =
|
||||
field.key in source ? coerceListItem(source[field.key]!, field.kind) : emptyField(field.kind);
|
||||
field.key in source ? coerceField(source[field.key]!, field.kind) : emptyField(field.kind);
|
||||
return value;
|
||||
}
|
||||
|
||||
@@ -266,14 +283,24 @@ export function coerceValue(def: PropertyDef, value: PropertyValue): PropertyVal
|
||||
return def.options.some((o) => o.value === s) ? s : def.default;
|
||||
}
|
||||
case 'list':
|
||||
return isList(value) ? value.map((item) => coerceListItem(item, def.items)) : def.default;
|
||||
return isList(value) ? value.map((item) => coerceListItem(item, def.items, def.fields)) : def.default;
|
||||
case 'struct':
|
||||
return structOf(def.fields, value);
|
||||
}
|
||||
}
|
||||
|
||||
export function coerceListItem(value: PropertyValue, items: ListItemKind): number | string | boolean {
|
||||
if (items === 'text') return isList(value) ? value.join(', ') : textOf(value);
|
||||
/** `fields` is only read for `items: 'struct'`, where it is the shape every item is forced into. */
|
||||
export function coerceListItem(
|
||||
value: PropertyValue,
|
||||
items: ListItemKind,
|
||||
fields?: readonly StructFieldDef[],
|
||||
): number | string | boolean | StructValue {
|
||||
return items === 'struct' ? structOf(fields ?? [], value) : coerceField(value, items);
|
||||
}
|
||||
|
||||
/** The primitive half of `coerceListItem`, split off because a struct field is never a struct. */
|
||||
export function coerceField(value: PropertyValue, items: FieldKind): number | string | boolean {
|
||||
if (items === 'text') return textOf(value);
|
||||
if (items === 'boolean') {
|
||||
if (typeof value === 'boolean') return value;
|
||||
// A list is typed as text, so the spelling has to count: Boolean('false') would be true.
|
||||
|
||||
Reference in new issue
Block a user