Files
feuerwerk/src/core/graph/NodeRegistry.ts
T
rootandClaude Fable 5.1 11c900c998 Drop the per-list item declaration; every item carries its own kind
A list no longer says what its items are. ListDef.items, ListDef.fields,
ListFieldDef.items and SetListItemsCommand are gone; the single seam is
src/core/properties/listItems.ts. JSON already tells number, text, boolean
and struct apart, so only two kinds need a tag: {"$color": n} and the new
{"$texture": id}. Field names cannot start with "$", so a tag never
collides with a struct field. On a data link a texture item arrives as
its bare id; the graph has no images.

List tab: the mark in front of each row is the kind picker, "+ Item"
asks for the kind, both are ordinary value edits (one undo step). A list
inside a struct field only offers the flat kinds because the depth rule
forbids a struct item there (ListAccess.kinds); the menu used to offer
it and silently do nothing.

Legacy lists that declared items: 'color' are tagged on load.

Actor demo: the four walk animations and four standing frames live in
two list variables of texture items indexed by facing (Item at), so the
graph can be rebuilt in the editor. The stand frames get their grid ids
back; 44 nodes instead of 46.

Also: the user's edits to on-start-during-play.md and the new note
reseed-because-of-list.md.

Verified: check.sh 794 tests, full e2e 631 checks, smokes 24/19/20.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-12 13:35:02 +02:00

168 lines
6.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type { PropertyBag, PropertyValue } from '../model/Properties';
import type { PropertyDef } from '../properties/PropertySchema';
import type { DeclaredPort, Graph, GraphNode, PortType } from './Graph';
import type { BehaviorLookup } from './lookup';
import type { NodeContext } from './NodeContext';
import { asBoolean, asList, asNumber, asString, type Value } from './values';
export interface PortDef {
key: string;
label: string;
type: PortType;
/** Inline value used while the input is unconnected and the node has no own value (default 0/''/false). */
default?: PropertyValue;
/** The property this port carries; its typed-in value is edited as that property (color, enum, …). */
field?: PropertyDef;
}
export type NodeCategory = 'event' | 'flow' | 'data' | 'variable' | 'action';
/**
* How a loop node asks for one of its exec outputs to be run repeatedly: `port` carries the body,
* `index` is the number output seeded with the round number.
*/
export interface RepeatResult {
port: string;
index: string;
times: number;
}
/** How a node asks for another behavior's graph to be run in place, `inputs` seeding its start node. */
export interface CallResult {
behaviorId: string;
inputs: Record<string, Value>;
}
export interface RunResult {
/** Cached for the rest of the pulse. */
outputs?: Record<string, Value>;
/** Exec output(s) to continue along; omitted = the node ends the flow. */
next?: string | string[];
/** Runs `port` that many times before continuing along `next`. */
repeat?: RepeatResult;
/** Runs another behavior before continuing along `next`; its results join `outputs`. */
call?: CallResult;
}
/** What a node may compute its ports from: itself, the graph it sits in, and the behaviors around it. */
export interface PortSource {
node: GraphNode;
graph: Graph;
behaviors: BehaviorLookup;
}
/** Ports a single node has on top of the fixed ones of its type, and a label naming what it does. */
export interface DerivedShape {
label?: string;
inputs?: PortDef[];
outputs?: PortDef[];
}
/** Palette, port rendering, connection validation and inspector fields are all derived from this. */
export interface NodeDef {
type: string;
label: string;
category: NodeCategory;
description: string;
inputs: PortDef[];
outputs: PortDef[];
/** Node settings that are not ports (key name, operator, variable). */
params: PropertyDef[];
/**
* Settings that say nothing at this value set, so the node body leaves them off; the inspector
* still offers them. Without it a switched-off repeat is two rows of "0" on every key node.
*/
hiddenParams?(params: PropertyBag): string[];
/**
* Which side the ports this node declares for itself (`GraphNode.ports`) appear on. Setting it
* is what gives a node the port editor in the inspector. `both`: every port is an output, and an
* input as well where it is marked `write`.
*/
declaresPorts?: 'inputs' | 'outputs' | 'both';
/** How a declared port becomes a port; without it, named after its key and typed by its kind. */
declaredPort?(port: DeclaredPort, node: GraphNode): PortDef;
/**
* Ports this node computes beyond the fixed and the declared ones – how many outputs a Sequence
* 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. `input` pulls one input on demand:
* pulling all of them would loop through an edge that feeds the node's own output back in.
*/
read?(ctx: NodeContext, params: PropertyBag, input: (key: string) => Value): Record<string, Value>;
/**
* 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.
*/
run?(ctx: NodeContext, inputs: Record<string, Value>, params: PropertyBag, def: NodeDef): RunResult;
}
const defs = new Map<string, NodeDef>();
export function registerNode(def: NodeDef): NodeDef {
if (defs.has(def.type)) throw new Error(`Node type "${def.type}" registered twice`);
defs.set(def.type, def);
return def;
}
export function nodeDef(type: string): NodeDef | undefined {
return defs.get(type);
}
export function allNodeDefs(): NodeDef[] {
return [...defs.values()];
}
export function isEventNode(def: NodeDef): boolean {
return def.category === 'event';
}
export function hasExecInput(def: NodeDef): boolean {
return def.inputs.some((p) => p.type === 'exec');
}
/**
* Data inputs that may carry an inline value while unconnected. An object is a live handle and a
* struct is assembled from its parts, so neither has a value that could be typed on one line.
*/
export function inlineInputs(def: NodeDef): PortDef[] {
return def.inputs.filter(isInlinePort);
}
function isInlinePort(port: PortDef): boolean {
return port.type !== 'exec' && port.type !== 'object' && port.type !== 'struct';
}
/** Untyped, an input writes the zero of its kind; as its default the node body shows it, not just the name. */
export function withInlineDefault(port: PortDef): PortDef {
return isInlinePort(port) ? { ...port, default: inlineInputDef(port).default } : port;
}
export function inlineInputDef(port: PortDef): PropertyDef {
if (port.field) return { ...port.field, key: port.key, label: port.label };
switch (port.type) {
case 'boolean':
return { key: port.key, label: port.label, kind: 'boolean', default: asBoolean(port.default ?? false) };
case 'string':
case 'any':
return { key: port.key, label: port.label, kind: 'text', default: asString(port.default ?? '') };
case 'list':
return { key: port.key, label: port.label, kind: 'list', default: asList(port.default) };
default:
return {
key: port.key,
label: port.label,
kind: 'number',
default: asNumber(port.default ?? 0),
step: 1,
};
}
}
/** The value an unconnected input carries: the node's own inline value, else the port default. */
export function inlineValue(node: { params: PropertyBag }, port: PortDef): Value {
return node.params[port.key] ?? port.default ?? null;
}