preview.ts

examples/fxedit/preview.ts · raw · open the example · all examples · site home

Exports: upstreamOfdepthsoutputNodesflowRootsexposePortprobeGraph

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
// What flows where: depths for the flow preview, the nodes an output depends
// on, and probe graphs, which show one port's value as the splats' color.
import {
  boundaryNode, GROUP_OUTPUT, groupOf, splitPort, withGroups, type Graph, type GraphLevel, type NodeLookup,
} from "./fx/index.js";

// Nodes feeding `ids`, themselves included
export function upstreamOf(level: GraphLevel, ids: string[]) {
  const into = new Map<string, string[]>();
  for (const link of level.links) {
    const to = splitPort(link.to)[0];
    into.set(to, [...(into.get(to) ?? []), splitPort(link.from)[0]]);
  }
  const seen = new Set<string>();
  const stack = [...ids];
  while (stack.length > 0) {
    const id = stack.pop()!;
    if (!seen.has(id) && level.nodes[id]) {
      seen.add(id);
      stack.push(...(into.get(id) ?? []));
    }
  }
  return seen;
}

// Longest chain of links from a source to each node (0 for nodes nothing
// feeds); a cycle stops counting where it closes
export function depths(level: GraphLevel) {
  const into = new Map<string, string[]>();
  for (const link of level.links) {
    const to = splitPort(link.to)[0];
    into.set(to, [...(into.get(to) ?? []), splitPort(link.from)[0]]);
  }
  const depth = new Map<string, number>();
  const visiting = new Set<string>();
  const visit = (id: string): number => {
    const known = depth.get(id);
    if (known !== undefined) {
      return known;
    }
    if (visiting.has(id)) {
      return 0;
    }
    visiting.add(id);
    const d = Math.max(-1, ...(into.get(id) ?? []).filter((from) => level.nodes[from]).map(visit)) + 1;
    visiting.delete(id);
    depth.set(id, d);
    return d;
  };
  Object.keys(level.nodes).forEach(visit);
  return depth;
}

export function outputNodes(level: GraphLevel, registry: NodeLookup) {
  return Object.keys(level.nodes).filter((id) => registry.get(level.nodes[id].type)?.output);
}

// Where values end up at a level: the output node, or a group's outputs
export function flowRoots(level: GraphLevel, registry: NodeLookup) {
  return Object.keys(level.nodes).filter((id) => level.nodes[id].type === GROUP_OUTPUT || registry.get(level.nodes[id].type)?.output);
}

// A port inside a group (reached through `path`, instance ids from the top)
// wired out through every group on the way as a new output, so a probe can
// read it at the top. The copy of the graph and the port there.
export function exposePort(graph: Graph, registry: NodeLookup, path: string[], port: string): { graph: Graph; port: string } | undefined {
  if (path.length === 0) {
    return { graph, port };
  }
  const copy = structuredClone(graph);
  const names: string[] = [];
  let level: GraphLevel = copy;
  for (const id of path) {
    const name = groupOf(level.nodes[id]?.type ?? "");
    const group = name !== undefined ? copy.groups?.[name] : undefined;
    if (!group || name === undefined) {
      return undefined;
    }
    names.push(name);
    level = group;
  }
  const [node, output] = splitPort(port);
  const type = withGroups(registry, copy.groups, names[names.length - 1]).get(level.nodes[node]?.type ?? "")?.outputs[output];
  if (!type) {
    return undefined;
  }
  let current = port;
  for (let i = path.length - 1; i >= 0; i -= 1) {
    const group = copy.groups![names[i]];
    let name = "__probe";
    for (let n = 2; group.outputs[name]; n += 1) {
      name = `__probe${n}`;
    }
    group.outputs[name] = type;
    group.links.push({ from: current, to: `${boundaryNode(group, GROUP_OUTPUT)}.${name}` });
    current = `${path[i]}.${name}`;
  }
  return { graph: copy, port: current };
}

// Probe viewer: floats through a blue-purple-orange ramp (period 2, so
// -1 and 1 match), vectors from [-1, 1] to [0, 1], bools green (true) or
// red, colors as they are
const RAMP = { a: [0.5, 0.5, 0.5], b: [0.5, 0.5, 0.5], c: [0.5, 0.5, 0.5], d: [0.5, 0.25, 0] };

// `graph` with `port` ("node.output" at the top) driving the output's rgb
// instead; offset, scale and keep stay (a probed bool shows every splat)
export function probeGraph(graph: Graph, registry: NodeLookup, port: string): Graph | undefined {
  const lookup = withGroups(registry, graph.groups);
  const [node, output] = splitPort(port);
  const type = graph.nodes[node] && lookup.get(graph.nodes[node].type)?.outputs[output];
  const outs = outputNodes(graph, lookup);
  if (!type || outs.length !== 1) {
    return undefined;
  }
  const out = outs[0];
  const probe = structuredClone(graph);
  probe.name = `${graph.name ?? "graph"} probe ${port}`;
  delete probe.params;
  probe.links = probe.links.filter((link) => link.to !== `${out}.rgb` && !(type === "bool" && link.to === `${out}.keep`));
  const link = (from: string, to: string) => probe.links.push({ from, to });
  switch (type) {
    case "float":
      probe.nodes.__probe = { type: "color.cosinePalette", inputs: structuredClone(RAMP) };
      link(port, "__probe.t");
      break;
    case "bool":
      probe.nodes.__probePick = { type: "math.select", inputs: { a: 0, b: 1 } };
      probe.nodes.__probe = { type: "color.tint", inputs: { rgb: [1, 0.12, 0.18], color: [0.2, 1, 0.4], intensity: 1 } };
      link(port, "__probePick.condition");
      link("__probePick.value", "__probe.amount");
      break;
    case "vec3":
      probe.nodes.__probeHalf = { type: "math.vec3Scale", inputs: { s: 0.5 } };
      probe.nodes.__probe = { type: "math.vec3Add", inputs: { b: [0.5, 0.5, 0.5] } };
      link(port, "__probeHalf.v");
      link("__probeHalf.value", "__probe.a");
      break;
    case "color":
      link(port, `${out}.rgb`);
      return probe;
    default:
      return undefined;
  }
  link("__probe.value", `${out}.rgb`);
  return probe;
}