Refactoring round two: the particle and shader compilers share emitGraph.ts (cycle detection, duplicate names, emit order) and keep only their rules. FontLibrary and TextureLibrary sit on FileLibrary (base URL, loading/failed bookkeeping, listeners); SoundLibrary keeps its own contract. The three Create*Command share ImportFileCommand, the three renames share RenameCommand - class names stay. bandBounds() in framing.ts and portNameField() in portSection.ts replace in-file clones, and three comments that told history now state the rule. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
107 lines
3.9 KiB
TypeScript
107 lines
3.9 KiB
TypeScript
import { edgesInto, findNode, type Graph, type GraphNode } from './Graph';
|
|
import { nodeDef, type NodeDef, type PortDef } from './NodeRegistry';
|
|
|
|
export interface EmitRules<D extends NodeDef, T> {
|
|
isDef: (def: NodeDef) => def is D;
|
|
/** Names the node kind in "Not a … node". */
|
|
noun: string;
|
|
/** A node's outputs from its inputs; called once per node, in dependency order. */
|
|
emit: (def: D, inputs: Record<string, T>, node: GraphNode) => Record<string, T>;
|
|
/** An input without an edge: the typed-in value or what the node says it means unfed. */
|
|
unfed: (node: GraphNode, port: PortDef, def: D | undefined) => T | null;
|
|
/** An edge's value on its way into `port`; null (after reporting a problem) when it cannot go in. */
|
|
conform?: (node: GraphNode, port: PortDef, value: T) => T | null;
|
|
}
|
|
|
|
export interface GraphEmitter<T> {
|
|
/** Per node id, the first thing that keeps the graph from compiling. */
|
|
readonly problems: Record<string, string>;
|
|
problem(node: GraphNode, text: string): void;
|
|
/** What feeds an input, emitting whatever it depends on first. */
|
|
input(node: GraphNode, port: PortDef): T | null;
|
|
/** Whether the walk reached the node. */
|
|
reached(node: GraphNode): boolean;
|
|
/** Reports property names that are invalid (`invalid` returns the text) or taken twice. */
|
|
checkNames(
|
|
nodes: GraphNode[],
|
|
nameOf: (node: GraphNode) => string,
|
|
invalid: (name: string) => string,
|
|
): void;
|
|
/** The problem of the first node in graph order, or null when there is none. */
|
|
firstProblem(): string | null;
|
|
}
|
|
|
|
/**
|
|
* The walk shared by the graph compilers: every node reachable from where `input` is first asked
|
|
* is emitted once, before anything that reads it, and a node feeding itself is a problem.
|
|
*/
|
|
export function graphEmitter<D extends NodeDef, T>(graph: Graph, rules: EmitRules<D, T>): GraphEmitter<T> {
|
|
const problems: Record<string, string> = {};
|
|
const emitted = new Map<string, Record<string, T>>();
|
|
const visiting = new Set<string>();
|
|
|
|
const problem = (node: GraphNode, text: string) => {
|
|
if (!(node.id in problems)) problems[node.id] = text;
|
|
};
|
|
|
|
const emitNode = (node: GraphNode): Record<string, T> | null => {
|
|
const done = emitted.get(node.id);
|
|
if (done) return done;
|
|
if (visiting.has(node.id)) {
|
|
problem(node, 'Feeds itself');
|
|
return null;
|
|
}
|
|
const def = nodeDef(node.type);
|
|
if (!def || !rules.isDef(def)) {
|
|
problem(node, `Not a ${rules.noun} node`);
|
|
return null;
|
|
}
|
|
visiting.add(node.id);
|
|
const inputs: Record<string, T> = {};
|
|
let complete = true;
|
|
for (const port of def.inputs) {
|
|
const value = input(node, port);
|
|
if (value) inputs[port.key] = value;
|
|
else complete = false;
|
|
}
|
|
visiting.delete(node.id);
|
|
if (!complete) return null;
|
|
const outputs = rules.emit(def, inputs, node);
|
|
emitted.set(node.id, outputs);
|
|
return outputs;
|
|
};
|
|
|
|
const input = (node: GraphNode, port: PortDef): T | null => {
|
|
const edge = edgesInto(graph, node.id, port.key)[0];
|
|
if (edge) {
|
|
const source = findNode(graph, edge.from.node);
|
|
const value = (source && emitNode(source))?.[edge.from.port];
|
|
if (!value) return null;
|
|
return rules.conform ? rules.conform(node, port, value) : value;
|
|
}
|
|
const def = nodeDef(node.type);
|
|
return rules.unfed(node, port, def && rules.isDef(def) ? def : undefined);
|
|
};
|
|
|
|
return {
|
|
problems,
|
|
problem,
|
|
input,
|
|
reached: (node) => emitted.has(node.id),
|
|
checkNames(nodes, nameOf, invalid) {
|
|
const names = new Set<string>();
|
|
for (const node of nodes) {
|
|
const name = nameOf(node);
|
|
const text = invalid(name);
|
|
if (text) problem(node, text);
|
|
else if (names.has(name)) problem(node, `Another property is named "${name}"`);
|
|
names.add(name);
|
|
}
|
|
},
|
|
firstProblem() {
|
|
const first = graph.nodes.find((n) => n.id in problems);
|
|
return first ? problems[first.id]! : null;
|
|
},
|
|
};
|
|
}
|