Files
feuerwerk/src/core/graph/NodeRegistry.ts
T
rootandClaude Fable 5.1 42cafa7543
game-maker / suite (push) Failing after 3m59s
Shader assets: graph-built shaders, the Shader tab and sprite rows from the graph (#62)
A shader is an asset carrying a node graph (Texture color, Output, Property,
Color, Add/Subtract/Multiply/Divide, Mix, Luminance) that compiles to the GLSL
fragment of a Pixi filter. The Shader tab reuses the Behavior canvas; a sprite
wears a shader through its Shader picker, and the rows below it are the
graph's Property nodes. Property schema, resolution and deviation take the
project because a sprite's rows now live in another asset. The built-in
registry of the prototype is gone; "Adjust colors" ships as a graph in the
fixture and the three example games.

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

170 lines
6.5 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 { isKindWith, plainKindOf } from '../properties/kinds';
import { flatDef, type PropertyDef } from '../properties/PropertySchema';
import type { DeclaredPort, Graph, GraphNode, PortType } from './Graph';
import type { GraphLookup } from './lookup';
import type { NodeContext } from './NodeContext';
import { asList, 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;
}
/**
* `state` and `shader` are the categories no behavior graph offers: their nodes are the states of
* a state machine and the GLSL of a shader asset.
*/
export type NodeCategory = 'event' | 'flow' | 'data' | 'variable' | 'action' | 'state' | 'shader';
/**
* 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 project around it. */
export interface PortSource {
node: GraphNode;
graph: Graph;
lookup: GraphLookup;
}
/** 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. `entry` is the exec input
* the pulse arrived through – what a node with one exec input per meaning reads.
*/
run?(
ctx: NodeContext,
inputs: Record<string, Value>,
params: PropertyBag,
def: NodeDef,
entry: string,
): 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; a
* shader colour has one only where the node gives it a colour field.
*/
export function inlineInputs(def: NodeDef): PortDef[] {
return def.inputs.filter(isInlinePort);
}
function isInlinePort(port: PortDef): boolean {
if (port.type === 'vec3') return port.field !== undefined;
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 };
const kind = port.type === 'any' ? 'text' : plainKindOf(port.type);
if (kind === 'list') return { key: port.key, label: port.label, kind, default: asList(port.default) };
const flat = kind !== undefined && isKindWith('field', kind) ? kind : 'number';
const def = flatDef(port.key, flat, { label: port.label, value: port.default });
return def.kind === 'number' ? { ...def, step: 1 } : def;
}
/** 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;
}