@field/cascade 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +14 -9
- package/ARCHITECTURE.md +22 -6
- package/CHANGELOG.md +69 -1
- package/CONTRIBUTING.md +71 -0
- package/DESIGN.md +19 -13
- package/README.md +42 -11
- package/dist/assets/{CodeEditor-BADFbTJS.js → CodeEditor-BNOJIDzL.js} +4 -4
- package/dist/assets/{CodePanel-faK6PRfz.js → CodePanel-2wp2G0Fj.js} +2 -2
- package/dist/assets/{CustomNodeDialog-DhlKuviy.js → CustomNodeDialog-DNqaVBqA.js} +1 -1
- package/dist/assets/{ProjectSettings-D6a6kjqR.js → ProjectSettings-DcdYF2E1.js} +1 -1
- package/dist/assets/{SettingsDialog-DJZdIvLC.js → SettingsDialog-D2xVvyiR.js} +1 -1
- package/dist/assets/{VersionHistoryDialog-C1X3HOhc.js → VersionHistoryDialog-DW288ykF.js} +1 -1
- package/dist/assets/{core-CiuiuWZi.js → core-B1y38i95.js} +1 -1
- package/dist/assets/{cssMode-D-NwZSMJ.js → cssMode-BLucnaln.js} +1 -1
- package/dist/assets/{editor.api-DT3WEvBd.js → editor.api-B3Et3Pph.js} +1 -1
- package/dist/assets/{freemarker2-eKNYQaiz.js → freemarker2-DVlPoano.js} +1 -1
- package/dist/assets/geo-DhyFdBXX.js +1 -0
- package/dist/assets/{handlebars-C2w1ggK2.js → handlebars-B5FZBAMR.js} +1 -1
- package/dist/assets/{html-DQb41jyo.js → html-FodD8KbR.js} +1 -1
- package/dist/assets/{htmlMode-IhNlh6Lv.js → htmlMode-CsG57VCD.js} +1 -1
- package/dist/assets/{image-DMCXBY-_.js → image-B3F-nq25.js} +1 -1
- package/dist/assets/index-CC6YQw2n.js +212 -0
- package/dist/assets/index-Y2ock1cw.css +1 -0
- package/dist/assets/{javascript-BIT6jMLC.js → javascript-B3gonF_H.js} +1 -1
- package/dist/assets/{jsonMode-CNfyB1ro.js → jsonMode-Y3w1ahZg.js} +1 -1
- package/dist/assets/{liquid-CmSmJmOG.js → liquid-CTpWXIYO.js} +1 -1
- package/dist/assets/{lspLanguageFeatures-BqMm3uMP.js → lspLanguageFeatures-CODBdKEZ.js} +1 -1
- package/dist/assets/{mdx-6r6YgOFw.js → mdx-CDAQJZIz.js} +1 -1
- package/dist/assets/{python-x1H0TQuI.js → python-LOIlxJU-.js} +1 -1
- package/dist/assets/{razor-Dzl1vZFm.js → razor-CllPQSHo.js} +1 -1
- package/dist/assets/{tsMode-n6DMMe3a.js → tsMode-CO28ut8t.js} +1 -1
- package/dist/assets/{typescript-ziEz65xN.js → typescript-ruU9wKQ_.js} +1 -1
- package/dist/assets/{workers-D-qUdp-r.js → workers-Bc1Ywp4-.js} +1 -1
- package/dist/assets/{xml-DMZjt5lE.js → xml-DoXP2Dh1.js} +1 -1
- package/dist/assets/{yaml-DFe5qZSO.js → yaml-D9YPyGiJ.js} +1 -1
- package/dist/cli/index.js +1240 -158
- package/dist/cli/runtime/gpu.ts +147 -0
- package/dist/contracts/camera.d.ts +70 -0
- package/dist/contracts/camera.js +27 -0
- package/dist/contracts/capabilities.d.ts +69 -6
- package/dist/contracts/definition.d.ts +61 -2
- package/dist/contracts/index.d.ts +2 -0
- package/dist/contracts/index.js +2 -0
- package/dist/contracts/schema.js +33 -7
- package/dist/contracts/series.d.ts +75 -0
- package/dist/contracts/series.js +2 -0
- package/dist/contracts/validate.js +29 -4
- package/dist/contracts/values.d.ts +3 -2
- package/dist/contracts/values.js +1 -0
- package/dist/gpu.d.ts +88 -0
- package/dist/gpu.js +113 -0
- package/dist/index.html +2 -2
- package/dist/runtime/builtins/core/camera.d.ts +252 -0
- package/dist/runtime/builtins/core/camera.js +149 -0
- package/dist/runtime/builtins/core/index.d.ts +2 -1
- package/dist/runtime/builtins/core/index.js +3 -1
- package/dist/runtime/camera/index.d.ts +60 -0
- package/dist/runtime/camera/index.js +234 -0
- package/dist/runtime/definition/architecture.js +36 -1
- package/dist/runtime/gpu.d.ts +33 -0
- package/dist/runtime/gpu.js +218 -0
- package/dist/runtime/index.d.ts +3 -0
- package/dist/runtime/index.js +3 -0
- package/dist/runtime/runtime.d.ts +23 -3
- package/dist/runtime/runtime.js +181 -30
- package/dist/runtime/series/index.d.ts +41 -0
- package/dist/runtime/series/index.js +167 -0
- package/dist/runtime/types.d.ts +3 -3
- package/doc/HEADLESS_GPU.md +160 -0
- package/doc/NODE_REFERENCE.md +36 -0
- package/doc/PROJECT_AUTHORING.md +106 -20
- package/package.json +14 -2
- package/dist/assets/geo-DUvVbLnx.js +0 -1
- package/dist/assets/index-BDV5xGBZ.css +0 -1
- package/dist/assets/index-HTCN44kj.js +0 -212
package/dist/runtime/runtime.js
CHANGED
|
@@ -4,6 +4,7 @@ import { builtinNodeRegistrations } from "./builtins/index.js";
|
|
|
4
4
|
import { deserializeChannel, isEmptyChannel } from "./animation/channel.js";
|
|
5
5
|
import { PropAnimator, isBoundProp } from "./params/index.js";
|
|
6
6
|
import { DEFAULT_FPS, MIN_FRAME } from "./expressions/time.js";
|
|
7
|
+
import { isGpuHost } from "./gpu.js";
|
|
7
8
|
/**
|
|
8
9
|
* Preflight codes that describe something other than "this graph cannot run
|
|
9
10
|
* here", so `run` proceeds and a checker reports them as warnings.
|
|
@@ -21,13 +22,47 @@ import { DEFAULT_FPS, MIN_FRAME } from "./expressions/time.js";
|
|
|
21
22
|
* leftover `params` array runs perfectly well on its defaults, which is
|
|
22
23
|
* precisely the problem.
|
|
23
24
|
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
25
|
+
* It was briefly an error, and Marcus reverted that on 2026-09-08 with the
|
|
26
|
+
* evidence that decides it: while converting sixty-two nodes across four
|
|
27
|
+
* sketches, every one of them was a mixed graph for most of the day, and as an
|
|
28
|
+
* error an unconverted or half-converted sketch **stops validating at all** —
|
|
29
|
+
* which is the one thing you need while converting it. The warning is the
|
|
30
|
+
* conversion checklist; as an error it is a locked door on the room you are
|
|
31
|
+
* working in.
|
|
32
|
+
*
|
|
33
|
+
* The fair case for the error was that the warning never fired, and that was
|
|
34
|
+
* true: `check` bailed on dynamic modules before preflight ran. The fix for
|
|
35
|
+
* that was the reporting *order*, not the severity.
|
|
36
|
+
*
|
|
37
|
+
* This set lives here rather than in the CLI because both paths read it, and a
|
|
38
|
+
* duplicated copy is what let `validate` and `check` pass graphs `run` then
|
|
39
|
+
* rejected.
|
|
40
|
+
*
|
|
41
|
+
* **It classifies a static check, and it is not the run gate.** Those are
|
|
42
|
+
* different questions and conflating them was a real bug of mine, caught by
|
|
43
|
+
* Codex's own comment on 2026-09-08: *execution is stricter.* A server run must
|
|
44
|
+
* still refuse a browser node — you cannot cook WebGPU headlessly, so tolerating
|
|
45
|
+
* it means executing and failing on an undefined capability instead of saying
|
|
46
|
+
* why. What a static check downgrades is the *checker's reach*; what a run
|
|
47
|
+
* tolerates is much narrower. See `RUN_TOLERATED_CODES`.
|
|
27
48
|
*/
|
|
28
49
|
export const PREFLIGHT_WARNING_CODES = new Set([
|
|
29
50
|
"runtime/preflight-environment",
|
|
30
51
|
"runtime/stray-params",
|
|
52
|
+
"runtime/unknown-props",
|
|
53
|
+
]);
|
|
54
|
+
/**
|
|
55
|
+
* The only preflight code that does not stop a run.
|
|
56
|
+
*
|
|
57
|
+
* A stranded `params` value is a fact about the document: the graph cooks
|
|
58
|
+
* perfectly well on its defaults, which is exactly the problem, and refusing to
|
|
59
|
+
* run is the worse answer. Everything else in the preflight is a reason this
|
|
60
|
+
* host cannot execute this graph — an environment mismatch and a missing
|
|
61
|
+
* capability both mean the cook would fail partway with a worse message.
|
|
62
|
+
*/
|
|
63
|
+
const RUN_TOLERATED_CODES = new Set([
|
|
64
|
+
"runtime/stray-params",
|
|
65
|
+
"runtime/unknown-props",
|
|
31
66
|
]);
|
|
32
67
|
class MutableAbortSignal {
|
|
33
68
|
report;
|
|
@@ -148,6 +183,10 @@ class Graph {
|
|
|
148
183
|
onDispose;
|
|
149
184
|
graphState = "ready";
|
|
150
185
|
byId;
|
|
186
|
+
dataConnectionsByInput;
|
|
187
|
+
dataConnectionsByNode;
|
|
188
|
+
connectedInputs;
|
|
189
|
+
connectedOutputs;
|
|
151
190
|
listeners = new Set();
|
|
152
191
|
diagnostics = [];
|
|
153
192
|
active;
|
|
@@ -173,6 +212,34 @@ class Graph {
|
|
|
173
212
|
this.connections = connections;
|
|
174
213
|
this.onDispose = onDispose;
|
|
175
214
|
this.byId = new Map(nodes.map((node) => [node.id, node]));
|
|
215
|
+
const byInput = new Map();
|
|
216
|
+
const byNode = new Map();
|
|
217
|
+
const connectedInputs = new Set();
|
|
218
|
+
const connectedOutputs = new Set();
|
|
219
|
+
for (const connection of connections) {
|
|
220
|
+
connectedInputs.add(`${connection.target.nodeId}\0${connection.target.inputName}`);
|
|
221
|
+
connectedOutputs.add(`${connection.source.nodeId}\0${connection.source.outputName}`);
|
|
222
|
+
if (connection.kind !== "data")
|
|
223
|
+
continue;
|
|
224
|
+
const inputKey = `${connection.target.nodeId}\0${connection.target.inputName}`;
|
|
225
|
+
const inputConnections = byInput.get(inputKey) ?? [];
|
|
226
|
+
inputConnections.push(connection);
|
|
227
|
+
byInput.set(inputKey, inputConnections);
|
|
228
|
+
const nodeConnections = byNode.get(connection.target.nodeId) ?? [];
|
|
229
|
+
nodeConnections.push(connection);
|
|
230
|
+
byNode.set(connection.target.nodeId, nodeConnections);
|
|
231
|
+
}
|
|
232
|
+
this.dataConnectionsByInput = new Map([...byInput].map(([key, inputConnections]) => [
|
|
233
|
+
key,
|
|
234
|
+
Object.freeze(inputConnections.sort((first, second) => (first.variadicIndex ?? first.order) -
|
|
235
|
+
(second.variadicIndex ?? second.order))),
|
|
236
|
+
]));
|
|
237
|
+
this.dataConnectionsByNode = new Map([...byNode].map(([key, nodeConnections]) => [
|
|
238
|
+
key,
|
|
239
|
+
Object.freeze(nodeConnections.sort((first, second) => first.order - second.order)),
|
|
240
|
+
]));
|
|
241
|
+
this.connectedInputs = connectedInputs;
|
|
242
|
+
this.connectedOutputs = connectedOutputs;
|
|
176
243
|
this.graphInputs = graphBoundaries(nodes, "cascade.core.Input", "inputName");
|
|
177
244
|
this.graphOutputs = graphBoundaries(nodes, "cascade.core.Output", "outputName");
|
|
178
245
|
this.authoredAnnotations = Object.freeze(annotations.map((annotation) => snapshot(annotation)));
|
|
@@ -180,10 +247,7 @@ class Graph {
|
|
|
180
247
|
nodes,
|
|
181
248
|
node: (id) => this.byId.get(id),
|
|
182
249
|
inputNode: (node, index) => {
|
|
183
|
-
const feeding = this.
|
|
184
|
-
.filter((connection) => connection.kind === "data" &&
|
|
185
|
-
connection.target.nodeId === node.id)
|
|
186
|
-
.sort((a, b) => a.order - b.order);
|
|
250
|
+
const feeding = this.dataConnectionsByNode.get(node.id) ?? [];
|
|
187
251
|
const source = feeding[index]?.source.nodeId;
|
|
188
252
|
return (source ? this.byId.get(source) : undefined) ?? null;
|
|
189
253
|
},
|
|
@@ -228,8 +292,7 @@ class Graph {
|
|
|
228
292
|
{
|
|
229
293
|
kind: definition.kind,
|
|
230
294
|
type: definition.kind === "trigger" ? "trigger" : definition.type,
|
|
231
|
-
connected: this.
|
|
232
|
-
connection.target.inputName === name),
|
|
295
|
+
connected: this.connectedInputs.has(`${node.id}\0${name}`),
|
|
233
296
|
...(definition.kind === "data"
|
|
234
297
|
? { value: snapshot(node.inputs[name]) }
|
|
235
298
|
: {}),
|
|
@@ -240,8 +303,7 @@ class Graph {
|
|
|
240
303
|
{
|
|
241
304
|
kind: definition.kind,
|
|
242
305
|
type: definition.kind === "trigger" ? "trigger" : definition.type,
|
|
243
|
-
connected: this.
|
|
244
|
-
connection.source.outputName === name),
|
|
306
|
+
connected: this.connectedOutputs.has(`${node.id}\0${name}`),
|
|
245
307
|
...(definition.kind === "data" && node.outputs.has(name)
|
|
246
308
|
? { value: snapshot(node.outputs.get(name)) }
|
|
247
309
|
: {}),
|
|
@@ -549,10 +611,7 @@ class Graph {
|
|
|
549
611
|
const index = integerProp(node.props.inputIndex);
|
|
550
612
|
return [name, this.dataInputValue(parent, `input_${index}`)];
|
|
551
613
|
}
|
|
552
|
-
const connections = this.
|
|
553
|
-
item.target.nodeId === node.id &&
|
|
554
|
-
item.target.inputName === name).sort((first, second) => (first.variadicIndex ?? first.order) -
|
|
555
|
-
(second.variadicIndex ?? second.order));
|
|
614
|
+
const connections = this.dataConnectionsByInput.get(`${node.id}\0${name}`) ?? [];
|
|
556
615
|
if (definition.variadic) {
|
|
557
616
|
const values = connections.map((connection) => this.requireNode(connection.source.nodeId).outputs.get(connection.source.outputName));
|
|
558
617
|
if (!values.length && node.inputs[name] !== undefined)
|
|
@@ -627,12 +686,23 @@ class Graph {
|
|
|
627
686
|
set: (value) => pendingData.set(name, runtimeValue(value, definition.type, false, `${node.id}.${name}`)),
|
|
628
687
|
},
|
|
629
688
|
]));
|
|
630
|
-
const
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
689
|
+
const capabilityEntries = [];
|
|
690
|
+
for (const name of node.definition.capabilities ?? []) {
|
|
691
|
+
const installed = this.host.capabilities[name];
|
|
692
|
+
if (name === "gpu" && isGpuHost(installed)) {
|
|
693
|
+
await installed.ensure();
|
|
694
|
+
capabilityEntries.push([name, installed.forNode(node.id)]);
|
|
695
|
+
}
|
|
696
|
+
else {
|
|
697
|
+
capabilityEntries.push([
|
|
698
|
+
name,
|
|
699
|
+
name === "shell"
|
|
700
|
+
? bindShellToRun(this.host.capabilities.shell, signal)
|
|
701
|
+
: installed,
|
|
702
|
+
]);
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
const capabilities = Object.freeze(Object.fromEntries(capabilityEntries));
|
|
636
706
|
const props = Object.freeze({ ...node.props });
|
|
637
707
|
const progress = {
|
|
638
708
|
report: (event) => this.emit({ type: "node:progress", runId, nodeId: node.id, ...event }),
|
|
@@ -695,6 +765,8 @@ class Graph {
|
|
|
695
765
|
const diagnostics = [];
|
|
696
766
|
const push = (code, nodeId, message) => diagnostics.push({ phase: "preflight", code, path: nodeId, message });
|
|
697
767
|
for (const node of nodes) {
|
|
768
|
+
if (node.unknownProps.length)
|
|
769
|
+
push("runtime/unknown-props", node.id, `${node.moduleId} stores ${node.unknownProps.join(", ")} under "props", and its definition declares no such prop — ${node.unknownProps.length === 1 ? "that value was" : "those values were"} dropped on load. A prop renamed or removed leaves its value behind; a name that never existed is a typo`);
|
|
698
770
|
if (node.strayParams.length)
|
|
699
771
|
push("runtime/stray-params", node.id, `${node.moduleId} stores ${node.strayParams.join(", ")} under "params", which this runtime does not read — move ${node.strayParams.length === 1 ? "it" : "them"} to "props" or "inputs", whichever the definition declares, or the ${node.strayParams.length === 1 ? "value is" : "values are"} silently the default`);
|
|
700
772
|
const remote = this.isRemote(node);
|
|
@@ -740,7 +812,7 @@ class Graph {
|
|
|
740
812
|
: `provides no capabilities`;
|
|
741
813
|
}
|
|
742
814
|
assertPreflight(nodes) {
|
|
743
|
-
const [first] = this.preflightDiagnostics(nodes).filter((diagnostic) => !
|
|
815
|
+
const [first] = this.preflightDiagnostics(nodes).filter((diagnostic) => !RUN_TOLERATED_CODES.has(diagnostic.code));
|
|
744
816
|
if (first)
|
|
745
817
|
throw new CascadeRuntimeError(first.code, [first]);
|
|
746
818
|
}
|
|
@@ -754,7 +826,8 @@ class Graph {
|
|
|
754
826
|
}
|
|
755
827
|
runSelection(target) {
|
|
756
828
|
if (target.kind === "all") {
|
|
757
|
-
const
|
|
829
|
+
const gatedDataDescendants = this.gatedDataDescendants();
|
|
830
|
+
const initial = this.nodes.filter((node) => !isGated(node) && !gatedDataDescendants.has(node.id));
|
|
758
831
|
const ids = new Set(initial.map((node) => node.id));
|
|
759
832
|
let changed = true;
|
|
760
833
|
while (changed) {
|
|
@@ -799,8 +872,25 @@ class Graph {
|
|
|
799
872
|
}
|
|
800
873
|
return this.nodes.filter((node) => ids.has(node.id));
|
|
801
874
|
}
|
|
802
|
-
|
|
803
|
-
|
|
875
|
+
gatedDataDescendants() {
|
|
876
|
+
const outgoing = new Map();
|
|
877
|
+
for (const dependency of dataDependencies(this.nodes, this.connections)) {
|
|
878
|
+
const targets = outgoing.get(dependency.sourceId) ?? [];
|
|
879
|
+
targets.push(dependency.targetId);
|
|
880
|
+
outgoing.set(dependency.sourceId, targets);
|
|
881
|
+
}
|
|
882
|
+
const queue = this.nodes.filter(isGated).map((node) => node.id);
|
|
883
|
+
const descendants = new Set(queue);
|
|
884
|
+
let queueIndex = 0;
|
|
885
|
+
while (queueIndex < queue.length) {
|
|
886
|
+
for (const targetId of outgoing.get(queue[queueIndex++]) ?? []) {
|
|
887
|
+
if (descendants.has(targetId))
|
|
888
|
+
continue;
|
|
889
|
+
descendants.add(targetId);
|
|
890
|
+
queue.push(targetId);
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
return descendants;
|
|
804
894
|
}
|
|
805
895
|
triggerDataAncestors(nodeId) {
|
|
806
896
|
const dependencies = this.dataClosure(nodeId).filter((node) => node.id !== nodeId);
|
|
@@ -948,8 +1038,41 @@ function materialize(authored, registration) {
|
|
|
948
1038
|
}
|
|
949
1039
|
for (const [name, definition] of Object.entries(registration.definition.props ?? {}))
|
|
950
1040
|
props[name] = runtimeValue(definition.default, definition.type, false, `${authored.id}.${name}`);
|
|
1041
|
+
/**
|
|
1042
|
+
* `props` is a map and `params` was a list, and a migration that changes the
|
|
1043
|
+
* key without changing the shape leaves an array here.
|
|
1044
|
+
*
|
|
1045
|
+
* Fatal, and the reason is what it costs: `Object.entries` of an array gives
|
|
1046
|
+
* keys `"0"`, `"1"`, which match no declared prop, so **every authored value
|
|
1047
|
+
* is dropped and every signal stays green.** Measured on 2026-09-08 —
|
|
1048
|
+
* preflight empty, the prop back at its default, `cascade run` reporting
|
|
1049
|
+
* `completed`. `cloud-plots` rendered a whole picture against no moment and
|
|
1050
|
+
* was reported as working on the strength of an exit code.
|
|
1051
|
+
*
|
|
1052
|
+
* An error rather than a warning, unlike a leftover under `params`: that is a
|
|
1053
|
+
* real state an unconverted sketch is legitimately in and must keep
|
|
1054
|
+
* validating, whereas `props` as an array is never valid — the schema
|
|
1055
|
+
* declares a record — and nothing passes through it on the way to being
|
|
1056
|
+
* right. Same reasoning as `definition/re-exported`.
|
|
1057
|
+
*/
|
|
1058
|
+
if (Array.isArray(authored.props))
|
|
1059
|
+
misuse("runtime/props-not-a-map", `Node ${authored.id} stores props as a list. "props" is a map of name to value; "params" was the list. Every value in it is being dropped, silently, and the graph would run on its defaults`);
|
|
951
1060
|
const bindings = {};
|
|
952
|
-
|
|
1061
|
+
// A declared default expression, applied only where the document stored
|
|
1062
|
+
// nothing at all for the prop. The loop below then runs over what *was*
|
|
1063
|
+
// stored, so an authored value or an authored expression overwrites this —
|
|
1064
|
+
// which is the whole contract: a default expression is a default, and a file
|
|
1065
|
+
// that saved a plain number keeps its number rather than quietly animating.
|
|
1066
|
+
const storedProps = authored.props ?? {};
|
|
1067
|
+
for (const [name, definition] of Object.entries(registration.definition.props ?? {})) {
|
|
1068
|
+
if (!definition.expression || name in storedProps)
|
|
1069
|
+
continue;
|
|
1070
|
+
bindings[name] = {
|
|
1071
|
+
value: props[name],
|
|
1072
|
+
expression: definition.expression,
|
|
1073
|
+
};
|
|
1074
|
+
}
|
|
1075
|
+
for (const [name, saved] of Object.entries(storedProps)) {
|
|
953
1076
|
const definition = registration.definition.props?.[name];
|
|
954
1077
|
if (!definition)
|
|
955
1078
|
continue;
|
|
@@ -961,8 +1084,12 @@ function materialize(authored, registration) {
|
|
|
961
1084
|
: null;
|
|
962
1085
|
const value = runtimeValue(object ? object.value : saved, definition.type, false, `${authored.id}.${name}`);
|
|
963
1086
|
props[name] = value;
|
|
964
|
-
|
|
1087
|
+
// A stored plain value is an author's decision and beats the declared
|
|
1088
|
+
// default expression, so any binding seeded above has to go.
|
|
1089
|
+
if (!object) {
|
|
1090
|
+
delete bindings[name];
|
|
965
1091
|
continue;
|
|
1092
|
+
}
|
|
966
1093
|
const channel = deserializeChannel(object.channel);
|
|
967
1094
|
const expression = typeof object.expression === "string" && object.expression
|
|
968
1095
|
? object.expression
|
|
@@ -987,6 +1114,7 @@ function materialize(authored, registration) {
|
|
|
987
1114
|
props,
|
|
988
1115
|
bindings,
|
|
989
1116
|
strayParams: strayParams(authored, registration),
|
|
1117
|
+
unknownProps: unknownProps(authored, registration),
|
|
990
1118
|
outputs: new Map(),
|
|
991
1119
|
};
|
|
992
1120
|
}
|
|
@@ -1011,6 +1139,27 @@ function materialize(authored, registration) {
|
|
|
1011
1139
|
* naming nothing is a different fault, and reporting it would make this fire on
|
|
1012
1140
|
* documents where no value was lost.
|
|
1013
1141
|
*/
|
|
1142
|
+
/**
|
|
1143
|
+
* Names stored under `props` that the definition does not declare.
|
|
1144
|
+
*
|
|
1145
|
+
* The mirror of `strayParams`, and the gap that one had: it asks *is anything
|
|
1146
|
+
* still under the old key*, which is blind to *did what arrived under the new
|
|
1147
|
+
* key arrive in the right form*. One is a leftover, the other is a botched
|
|
1148
|
+
* move, and the second is worse because it looks finished.
|
|
1149
|
+
*
|
|
1150
|
+
* A warning rather than an error, because unlike an array container this is a
|
|
1151
|
+
* state a real document reaches honestly: a prop renamed or removed from a
|
|
1152
|
+
* definition leaves its value behind, and the graph runs on the defaults. Same
|
|
1153
|
+
* class as a leftover under `params`, and the same severity for the same
|
|
1154
|
+
* reason — a sketch mid-conversion must keep validating.
|
|
1155
|
+
*/
|
|
1156
|
+
function unknownProps(authored, registration) {
|
|
1157
|
+
const props = authored.props;
|
|
1158
|
+
if (!props || Array.isArray(props))
|
|
1159
|
+
return [];
|
|
1160
|
+
const declared = registration.definition.props ?? {};
|
|
1161
|
+
return Object.keys(props).filter((name) => !(name in declared));
|
|
1162
|
+
}
|
|
1014
1163
|
function strayParams(authored, registration) {
|
|
1015
1164
|
const definition = registration.definition;
|
|
1016
1165
|
const declared = new Set([
|
|
@@ -1265,6 +1414,7 @@ function validateTopology(nodes, connections) {
|
|
|
1265
1414
|
topological(nodes, connections);
|
|
1266
1415
|
}
|
|
1267
1416
|
function topological(nodes, connections) {
|
|
1417
|
+
const byId = new Map(nodes.map((node) => [node.id, node]));
|
|
1268
1418
|
const indegree = new Map(nodes.map((node) => [node.id, 0]));
|
|
1269
1419
|
const outgoing = new Map(nodes.map((node) => [node.id, []]));
|
|
1270
1420
|
for (const dependency of graphDependencies(nodes, connections)) {
|
|
@@ -1273,13 +1423,14 @@ function topological(nodes, connections) {
|
|
|
1273
1423
|
}
|
|
1274
1424
|
const queue = nodes.filter((node) => indegree.get(node.id) === 0);
|
|
1275
1425
|
const result = [];
|
|
1276
|
-
|
|
1277
|
-
|
|
1426
|
+
let queueIndex = 0;
|
|
1427
|
+
while (queueIndex < queue.length) {
|
|
1428
|
+
const node = queue[queueIndex++];
|
|
1278
1429
|
result.push(node);
|
|
1279
1430
|
for (const id of outgoing.get(node.id)) {
|
|
1280
1431
|
indegree.set(id, indegree.get(id) - 1);
|
|
1281
1432
|
if (indegree.get(id) === 0)
|
|
1282
|
-
queue.push(
|
|
1433
|
+
queue.push(byId.get(id));
|
|
1283
1434
|
}
|
|
1284
1435
|
}
|
|
1285
1436
|
if (result.length !== nodes.length)
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { CascadeAbortSignal, CascadePreset, CascadeSeries, SeriesInstanceResult } from "@field/cascade/contracts";
|
|
2
|
+
import type { LoadedCascadeGraph } from "../types.js";
|
|
3
|
+
/**
|
|
4
|
+
* The graph's current values for those keys, as a preset.
|
|
5
|
+
*
|
|
6
|
+
* Read through `inspect()` rather than from the nodes directly, so the values
|
|
7
|
+
* are the same snapshots every other reader gets. A key the graph does not
|
|
8
|
+
* have is left out rather than defaulted: `applyPreset` is the thing that
|
|
9
|
+
* decides an unknown key is a misuse, and it should keep deciding that.
|
|
10
|
+
*/
|
|
11
|
+
export declare function seriesBaseline(graph: LoadedCascadeGraph, series: CascadeSeries): CascadePreset;
|
|
12
|
+
export interface RunSeriesOptions {
|
|
13
|
+
readonly signal?: CascadeAbortSignal;
|
|
14
|
+
readonly frame?: number;
|
|
15
|
+
readonly fps?: number;
|
|
16
|
+
/**
|
|
17
|
+
* The instance to cook first, by id.
|
|
18
|
+
*
|
|
19
|
+
* *"Making changes to the underlying network re-renders the current selected
|
|
20
|
+
* item first then the entire series."* The order of the results is always the
|
|
21
|
+
* series' own order whatever this says — only the order of the **work**
|
|
22
|
+
* changes, because the caller is watching one instance and waiting for a
|
|
23
|
+
* list would defeat the point.
|
|
24
|
+
*/
|
|
25
|
+
readonly first?: string;
|
|
26
|
+
/** Called as each instance finishes, in cook order rather than list order,
|
|
27
|
+
* so a gallery can fill in as results arrive instead of after all of them. */
|
|
28
|
+
readonly onInstance?: (result: SeriesInstanceResult) => void;
|
|
29
|
+
}
|
|
30
|
+
export interface SeriesRunResult {
|
|
31
|
+
/** In the series' own order, whatever order the work ran in. */
|
|
32
|
+
readonly instances: readonly SeriesInstanceResult[];
|
|
33
|
+
/** By record id, which is the address everything downstream uses. */
|
|
34
|
+
readonly byId: ReadonlyMap<string, SeriesInstanceResult>;
|
|
35
|
+
readonly status: "completed" | "cancelled" | "failed";
|
|
36
|
+
}
|
|
37
|
+
/** Ids must be unique, or an instance has no address at all — and every
|
|
38
|
+
* judgement, override and cache key in the plan hangs off that address. */
|
|
39
|
+
export declare function validateSeries(series: CascadeSeries): void;
|
|
40
|
+
export declare function runSeries(graph: LoadedCascadeGraph, series: CascadeSeries, options?: RunSeriesOptions): Promise<SeriesRunResult>;
|
|
41
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cooking a graph once per parameter set.
|
|
3
|
+
*
|
|
4
|
+
* Step 1 of `PLAN series.md`, and headless on purpose: it is the foundation the
|
|
5
|
+
* gallery and the scheduler stand on, and it is verifiable without a picture.
|
|
6
|
+
*
|
|
7
|
+
* **The whole difficulty is that a parameter set is sparse.** `applyPreset`
|
|
8
|
+
* mutates the runtime, and instance 3 overriding `seed` while instance 4 does
|
|
9
|
+
* not means instance 4 inherits instance 3's seed unless something puts it
|
|
10
|
+
* back. That is accidental feedback — in the one mechanism specified as having
|
|
11
|
+
* none — and it would show up as a series where the pictures are subtly wrong
|
|
12
|
+
* in an order-dependent way, which is close to the worst failure this system
|
|
13
|
+
* could have: re-run it and the same instance renders differently, so it reads
|
|
14
|
+
* as a flaky renderer rather than as a bug.
|
|
15
|
+
*
|
|
16
|
+
* So every instance is cooked from the same baseline. The baseline is itself a
|
|
17
|
+
* `CascadePreset`, holding the graph's current value for **every key any
|
|
18
|
+
* instance touches** — the union, computed once — which means restoring is the
|
|
19
|
+
* same validated code path as applying, rather than a second one that can
|
|
20
|
+
* disagree with it.
|
|
21
|
+
*/
|
|
22
|
+
/** Every `node.input` and `node.prop` any instance in the series overrides. */
|
|
23
|
+
function overriddenKeys(series) {
|
|
24
|
+
const keys = new Map();
|
|
25
|
+
for (const instance of series.instances) {
|
|
26
|
+
for (const [nodeId, values] of Object.entries(instance.parameters.nodes)) {
|
|
27
|
+
let entry = keys.get(nodeId);
|
|
28
|
+
if (!entry) {
|
|
29
|
+
entry = { inputs: new Set(), props: new Set() };
|
|
30
|
+
keys.set(nodeId, entry);
|
|
31
|
+
}
|
|
32
|
+
for (const name of Object.keys(values.inputs ?? {}))
|
|
33
|
+
entry.inputs.add(name);
|
|
34
|
+
for (const name of Object.keys(values.props ?? {}))
|
|
35
|
+
entry.props.add(name);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return keys;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* The graph's current values for those keys, as a preset.
|
|
42
|
+
*
|
|
43
|
+
* Read through `inspect()` rather than from the nodes directly, so the values
|
|
44
|
+
* are the same snapshots every other reader gets. A key the graph does not
|
|
45
|
+
* have is left out rather than defaulted: `applyPreset` is the thing that
|
|
46
|
+
* decides an unknown key is a misuse, and it should keep deciding that.
|
|
47
|
+
*/
|
|
48
|
+
export function seriesBaseline(graph, series) {
|
|
49
|
+
const wanted = overriddenKeys(series);
|
|
50
|
+
const inspection = graph.inspect();
|
|
51
|
+
const nodes = {};
|
|
52
|
+
for (const node of inspection.nodes) {
|
|
53
|
+
const entry = wanted.get(node.id);
|
|
54
|
+
if (!entry)
|
|
55
|
+
continue;
|
|
56
|
+
const inputs = {};
|
|
57
|
+
const props = {};
|
|
58
|
+
for (const name of entry.inputs) {
|
|
59
|
+
if (node.inputs[name] !== undefined)
|
|
60
|
+
inputs[name] = node.inputs[name].value;
|
|
61
|
+
}
|
|
62
|
+
for (const name of entry.props) {
|
|
63
|
+
if (node.props[name] !== undefined)
|
|
64
|
+
props[name] = node.props[name].value;
|
|
65
|
+
}
|
|
66
|
+
if (Object.keys(inputs).length > 0 || Object.keys(props).length > 0) {
|
|
67
|
+
nodes[node.id] = {
|
|
68
|
+
...(Object.keys(inputs).length > 0 ? { inputs } : {}),
|
|
69
|
+
...(Object.keys(props).length > 0 ? { props } : {}),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return { version: 1, nodes };
|
|
74
|
+
}
|
|
75
|
+
/** Ids must be unique, or an instance has no address at all — and every
|
|
76
|
+
* judgement, override and cache key in the plan hangs off that address. */
|
|
77
|
+
export function validateSeries(series) {
|
|
78
|
+
if (series.version !== 1) {
|
|
79
|
+
throw new Error("Series version must be 1");
|
|
80
|
+
}
|
|
81
|
+
const seen = new Set();
|
|
82
|
+
for (const instance of series.instances) {
|
|
83
|
+
if (!instance.id)
|
|
84
|
+
throw new Error("Every series instance needs an id");
|
|
85
|
+
if (seen.has(instance.id)) {
|
|
86
|
+
throw new Error(`Duplicate series instance id ${instance.id}`);
|
|
87
|
+
}
|
|
88
|
+
seen.add(instance.id);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
function collect(graph, collection) {
|
|
92
|
+
return "graphOutput" in collection
|
|
93
|
+
? graph.getGraphOutput(collection.graphOutput)
|
|
94
|
+
: graph.getOutput(collection.nodeId, collection.output);
|
|
95
|
+
}
|
|
96
|
+
/** The cook order: the selected instance, then the rest in the series' order. */
|
|
97
|
+
function cookOrder(series, first) {
|
|
98
|
+
const order = series.instances.map((_, index) => index);
|
|
99
|
+
if (first === undefined)
|
|
100
|
+
return order;
|
|
101
|
+
const selected = series.instances.findIndex(instance => instance.id === first);
|
|
102
|
+
if (selected < 0)
|
|
103
|
+
return order;
|
|
104
|
+
return [selected, ...order.filter(index => index !== selected)];
|
|
105
|
+
}
|
|
106
|
+
export async function runSeries(graph, series, options = {}) {
|
|
107
|
+
validateSeries(series);
|
|
108
|
+
// Before the first `applyPreset` and not after, or the baseline is already
|
|
109
|
+
// instance one's.
|
|
110
|
+
const baseline = seriesBaseline(graph, series);
|
|
111
|
+
const results = new Array(series.instances.length);
|
|
112
|
+
let cancelled = false;
|
|
113
|
+
try {
|
|
114
|
+
for (const index of cookOrder(series, options.first)) {
|
|
115
|
+
if (options.signal?.aborted) {
|
|
116
|
+
cancelled = true;
|
|
117
|
+
break;
|
|
118
|
+
}
|
|
119
|
+
const instance = series.instances[index];
|
|
120
|
+
// Baseline first, every time. A sparse set does not clear what the
|
|
121
|
+
// previous instance changed, and inheriting it is accidental feedback.
|
|
122
|
+
await graph.applyPreset(baseline);
|
|
123
|
+
await graph.applyPreset(instance.parameters);
|
|
124
|
+
const run = await graph.run({
|
|
125
|
+
...(options.signal ? { signal: options.signal } : {}),
|
|
126
|
+
...(options.frame === undefined ? {} : { frame: options.frame }),
|
|
127
|
+
...(options.fps === undefined ? {} : { fps: options.fps }),
|
|
128
|
+
});
|
|
129
|
+
const result = {
|
|
130
|
+
id: instance.id,
|
|
131
|
+
index,
|
|
132
|
+
status: run.status,
|
|
133
|
+
// A run that did not complete has no value worth collecting, and
|
|
134
|
+
// reading the port anyway would hand back the previous instance's
|
|
135
|
+
// picture under this instance's id.
|
|
136
|
+
value: run.status === "completed" ? collect(graph, series.collect) : undefined,
|
|
137
|
+
diagnostics: run.diagnostics,
|
|
138
|
+
durationMs: run.timings.durationMs,
|
|
139
|
+
};
|
|
140
|
+
results[index] = result;
|
|
141
|
+
options.onInstance?.(result);
|
|
142
|
+
if (run.status === "cancelled") {
|
|
143
|
+
cancelled = true;
|
|
144
|
+
break;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
finally {
|
|
149
|
+
// Leave the graph as it was found. A series is a way of looking at a graph,
|
|
150
|
+
// not an edit to it, and a Studio session that ran one and was left on the
|
|
151
|
+
// last instance's parameters would be indistinguishable from one the author
|
|
152
|
+
// had changed by hand.
|
|
153
|
+
await graph.applyPreset(baseline);
|
|
154
|
+
}
|
|
155
|
+
const completed = results.filter((result) => Boolean(result));
|
|
156
|
+
const status = cancelled
|
|
157
|
+
? "cancelled"
|
|
158
|
+
: completed.some(result => result.status === "failed")
|
|
159
|
+
? "failed"
|
|
160
|
+
: "completed";
|
|
161
|
+
return {
|
|
162
|
+
instances: completed,
|
|
163
|
+
byId: new Map(completed.map(result => [result.id, result])),
|
|
164
|
+
status,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
//# sourceMappingURL=index.js.map
|
package/dist/runtime/types.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AssetCapability, AssetRef, CascadeAbortSignal, CascadeDocument, CascadePreset, Diagnostic, FileCapability, ImageRef, JsonValue, MediaCapability, NodeDefinition, NodeExecute, ProgressReporter, PythonCapability, SerializableValue, ShellCapability, TextureHandle,
|
|
1
|
+
import type { AssetCapability, AssetRef, CascadeAbortSignal, CascadeDocument, CascadePreset, Diagnostic, FileCapability, ImageRef, JsonValue, MediaCapability, NodeDefinition, NodeExecute, ProgressReporter, PythonCapability, SerializableValue, ShellCapability, TextureHandle, GpuCapability } from "@field/cascade/contracts";
|
|
2
2
|
export type { CascadeDocument, CascadeDocumentAnnotation, CascadeDocumentConnection, CascadeDocumentNode, CascadePreset, } from "@field/cascade/contracts";
|
|
3
3
|
export interface DefinitionNodeRegistration<D extends NodeDefinition = NodeDefinition> {
|
|
4
4
|
readonly kind: "definition-v1";
|
|
@@ -31,7 +31,7 @@ export interface RuntimeCapabilities {
|
|
|
31
31
|
readonly assets?: AssetCapability;
|
|
32
32
|
readonly media?: MediaCapability;
|
|
33
33
|
readonly python?: PythonCapability;
|
|
34
|
-
readonly
|
|
34
|
+
readonly gpu?: GpuCapability;
|
|
35
35
|
readonly shell?: ShellCapability;
|
|
36
36
|
readonly serverBridge?: ServerBridgeCapability;
|
|
37
37
|
}
|
|
@@ -246,7 +246,7 @@ export interface LoadedCascadeGraph {
|
|
|
246
246
|
getOutputs(nodeId: string): ReadonlyMap<string, unknown>;
|
|
247
247
|
dispose(): Promise<void>;
|
|
248
248
|
}
|
|
249
|
-
export interface NodeRuntimeHostOptions extends
|
|
249
|
+
export interface NodeRuntimeHostOptions extends RuntimeCapabilities {
|
|
250
250
|
readonly modules: RuntimeHost["modules"];
|
|
251
251
|
readonly legacyShell?: LegacyShellBindingFactory & Readonly<{
|
|
252
252
|
kind: "server-direct";
|