Two compilers that walk a graph the same way walk it through one emitter (#85)
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>
This commit is contained in:
1 parent
bf0be66c9d
commit
d3563897d8
16 files changed
+376
-379
No files matched your search
@@ -1,5 +1,6 @@
|
||||
import { edgesInto, findNode, type Graph, type GraphNode } from '../graph/Graph';
|
||||
import { nodeDef, type PortDef } from '../graph/NodeRegistry';
|
||||
import { graphEmitter } from '../graph/emitGraph';
|
||||
import type { Graph } from '../graph/Graph';
|
||||
import { nodeDef } from '../graph/NodeRegistry';
|
||||
import type { FlatDef } from '../properties/PropertySchema';
|
||||
import {
|
||||
isParticlePropertyNode,
|
||||
@@ -13,6 +14,7 @@ import {
|
||||
PARTICLE_OUTPUTS,
|
||||
PARTICLE_TIME,
|
||||
type ParticleEnv,
|
||||
type ParticleNodeDef,
|
||||
type ParticleExpr,
|
||||
type ParticleOutputKey,
|
||||
} from './nodes';
|
||||
@@ -44,79 +46,40 @@ export interface ParticlesBuild {
|
||||
* particle. There is no interpreter and no eval – the compiler composes closures.
|
||||
*/
|
||||
export function compileParticles(graph: Graph): ParticlesBuild {
|
||||
const problems: Record<string, string> = {};
|
||||
const problem = (node: GraphNode, text: string) => {
|
||||
if (!(node.id in problems)) problems[node.id] = text;
|
||||
};
|
||||
|
||||
const names = new Set<string>();
|
||||
for (const node of graph.nodes) {
|
||||
if (!isParticlePropertyNode(node)) continue;
|
||||
const name = propertyNameOf(node);
|
||||
if (!isValidParticlePropertyName(name))
|
||||
problem(node, 'Name must be one word: letters, digits, _ – and not one of the source’s own rows');
|
||||
else if (names.has(name)) problem(node, `Another property is named "${name}"`);
|
||||
names.add(name);
|
||||
}
|
||||
|
||||
const params = particleParamDefs(graph);
|
||||
const paramIndex = (name: string) => params.findIndex((row) => row.key === name);
|
||||
|
||||
const ops: ParticleExpr[] = [];
|
||||
const emitted = new Map<string, Record<string, ParticleExpr>>();
|
||||
const visiting = new Set<string>();
|
||||
|
||||
const emitNode = (node: GraphNode): Record<string, ParticleExpr> | 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 || !isParticleNodeDef(def)) {
|
||||
problem(node, 'Not a particle node');
|
||||
return null;
|
||||
}
|
||||
visiting.add(node.id);
|
||||
const inputs: Record<string, ParticleExpr> = {};
|
||||
let complete = true;
|
||||
for (const port of def.inputs) {
|
||||
const expr = input(node, port);
|
||||
if (expr) inputs[port.key] = expr;
|
||||
else complete = false;
|
||||
}
|
||||
visiting.delete(node.id);
|
||||
if (!complete) return null;
|
||||
const outputs: Record<string, ParticleExpr> = {};
|
||||
for (const [key, expr] of Object.entries(
|
||||
def.emit(inputs, node.params, { nodeId: node.id, paramIndex }),
|
||||
)) {
|
||||
const slot = ops.length;
|
||||
ops.push(expr);
|
||||
outputs[key] = (_env, locals) => locals[slot]!;
|
||||
}
|
||||
emitted.set(node.id, outputs);
|
||||
return outputs;
|
||||
};
|
||||
const walk = graphEmitter<ParticleNodeDef, ParticleExpr>(graph, {
|
||||
isDef: isParticleNodeDef,
|
||||
noun: 'particle',
|
||||
emit: (def, inputs, node) => {
|
||||
const outputs: Record<string, ParticleExpr> = {};
|
||||
for (const [key, expr] of Object.entries(
|
||||
def.emit(inputs, node.params, { nodeId: node.id, paramIndex }),
|
||||
)) {
|
||||
const slot = ops.length;
|
||||
ops.push(expr);
|
||||
outputs[key] = (_env, locals) => locals[slot]!;
|
||||
}
|
||||
return outputs;
|
||||
},
|
||||
unfed: (node, port, def) => {
|
||||
if (port.field) {
|
||||
const typed = Number(node.params[port.key] ?? port.default ?? 0);
|
||||
const value = Number.isFinite(typed) ? typed : 0;
|
||||
return () => value;
|
||||
}
|
||||
return def?.unfed?.[port.key] ?? (() => 0);
|
||||
},
|
||||
});
|
||||
const { problems } = walk;
|
||||
|
||||
/** What feeds an input: the edge's source, the typed-in value, or what the node says (`unfed`). */
|
||||
const input = (node: GraphNode, port: PortDef): ParticleExpr | null => {
|
||||
const edge = edgesInto(graph, node.id, port.key)[0];
|
||||
if (edge) {
|
||||
const source = findNode(graph, edge.from.node);
|
||||
const outputs = source && emitNode(source);
|
||||
return outputs?.[edge.from.port] ?? null;
|
||||
}
|
||||
if (port.field) {
|
||||
const typed = Number(node.params[port.key] ?? port.default ?? 0);
|
||||
const value = Number.isFinite(typed) ? typed : 0;
|
||||
return () => value;
|
||||
}
|
||||
const def = nodeDef(node.type);
|
||||
const unfed = def && isParticleNodeDef(def) ? def.unfed?.[port.key] : undefined;
|
||||
return unfed ?? (() => 0);
|
||||
};
|
||||
walk.checkNames(graph.nodes.filter(isParticlePropertyNode), propertyNameOf, (name) =>
|
||||
isValidParticlePropertyName(name)
|
||||
? ''
|
||||
: 'Name must be one word: letters, digits, _ – and not one of the source’s own rows',
|
||||
);
|
||||
|
||||
const output = graph.nodes.find((n) => n.type === PARTICLE_OUTPUT);
|
||||
if (!output) return { compiled: null, problems, reason: 'No Output node' };
|
||||
@@ -124,17 +87,17 @@ export function compileParticles(graph: Graph): ParticlesBuild {
|
||||
const reads: ParticleExpr[] = [];
|
||||
for (const key of PARTICLE_OUTPUTS) {
|
||||
const port = outputDef.inputs.find((p) => p.key === key)!;
|
||||
const expr = input(output, port);
|
||||
const expr = walk.input(output, port);
|
||||
if (!expr) break;
|
||||
reads.push(expr);
|
||||
}
|
||||
|
||||
const first = graph.nodes.find((n) => n.id in problems);
|
||||
if (first || reads.length !== PARTICLE_OUTPUTS.length)
|
||||
return { compiled: null, problems, reason: first ? problems[first.id]! : 'Cannot compile' };
|
||||
const failed = walk.firstProblem();
|
||||
if (failed !== null || reads.length !== PARTICLE_OUTPUTS.length)
|
||||
return { compiled: null, problems, reason: failed ?? 'Cannot compile' };
|
||||
|
||||
const locals = new Float64Array(ops.length);
|
||||
const time = graph.nodes.some((n) => n.type === PARTICLE_TIME && emitted.has(n.id));
|
||||
const time = graph.nodes.some((n) => n.type === PARTICLE_TIME && walk.reached(n));
|
||||
const compiled: CompiledParticles = {
|
||||
params,
|
||||
time,
|
||||
|
||||
Reference in new issue
Block a user