@field/cascade 0.8.0 → 0.10.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 +17 -1
- package/CHANGELOG.md +214 -0
- package/DESIGN.md +14 -0
- package/README.md +41 -25
- package/dist/assets/CodeEditor-DSZBpzdy.js +623 -0
- package/dist/assets/CodePanel-sY6sZa7B.js +2 -0
- package/dist/assets/{CustomNodeDialog-BQbEhIwL.js → CustomNodeDialog-BRKnqZhp.js} +1 -1
- package/dist/assets/{ExportDialog-CjTbbTZJ.js → ExportDialog-C7dVGua4.js} +2 -2
- package/dist/assets/OpenGraphDialog-ZsgL-iwr.js +1 -0
- package/dist/assets/ProjectSettings-Cw17nNe3.js +2 -0
- package/dist/assets/SettingsDialog-p25h3tj0.js +1 -0
- package/dist/assets/{VersionHistoryDialog-D6VBzSQ0.js → VersionHistoryDialog-DzFLUotB.js} +2 -2
- package/dist/assets/{core-BInLG4gw.js → core-BICrEt0y.js} +8 -8
- package/dist/assets/{cssMode-DfR2qmYG.js → cssMode-D7TXw57I.js} +1 -1
- package/dist/assets/definitions-DUww2Rt-.js +1 -0
- package/dist/assets/{editor.api-Ck8_vgQV.js → editor.api-DvkCO3ko.js} +1 -1
- package/dist/assets/{freemarker2-fVn10Wn3.js → freemarker2-BA2lxsF1.js} +1 -1
- package/dist/assets/geo-C2RHOVHC.js +1 -0
- package/dist/assets/{handlebars-BCSchLT2.js → handlebars-BcDiyPd8.js} +1 -1
- package/dist/assets/{html-CVpSv_k9.js → html-C7O4TyhX.js} +1 -1
- package/dist/assets/{htmlMode-Da4ESTv0.js → htmlMode-WaDGF3Mj.js} +1 -1
- package/dist/assets/{image-BLn--oSF.js → image-DR4oeyJB.js} +3 -3
- package/dist/assets/index-7vae6YFC.css +1 -0
- package/dist/assets/index-DyvDC6pS.js +219 -0
- package/dist/assets/index-client-DqYmKstU.js +1 -0
- package/dist/assets/{javascript-BpX0Gvcf.js → javascript-CpIEK7tz.js} +1 -1
- package/dist/assets/{jsonMode-CsLqaou7.js → jsonMode-B2FltW0f.js} +1 -1
- package/dist/assets/legacy-CTEeXeqF.js +2 -0
- package/dist/assets/{liquid-efUz51Wf.js → liquid-CN6FFHZE.js} +1 -1
- package/dist/assets/{lspLanguageFeatures-DCi5-K0n.js → lspLanguageFeatures-QhV9XtLp.js} +1 -1
- package/dist/assets/math-DaDEgWKt.js +1 -0
- package/dist/assets/{mdx-DP_sjGVy.js → mdx-DE89pafV.js} +1 -1
- package/dist/assets/pop-pLH7L0PW.js +1 -0
- package/dist/assets/{python-aAuP3LUi.js → python-ByTllTB9.js} +1 -1
- package/dist/assets/{razor-7M6sMmC5.js → razor-USYlRRCE.js} +1 -1
- package/dist/assets/studioScenes-8sq9FfFg.js +1 -0
- package/dist/assets/{tsMode-Bf4CnoYK.js → tsMode-BXpN8iUz.js} +1 -1
- package/dist/assets/{typescript-ddhYBGyT.js → typescript-DInBjGQQ.js} +1 -1
- package/dist/assets/{workers-X5la8DDT.js → workers-C7Ye5B7D.js} +1 -1
- package/dist/assets/{xml-BzbXa3Ps.js → xml-U0Uocqyo.js} +1 -1
- package/dist/assets/{yaml-CXs-VzxL.js → yaml-CBw_PVYf.js} +1 -1
- package/dist/cli/index.js +19978 -3423
- package/dist/cli/runtime/stage.ts +70 -10
- package/dist/contracts/bundle.d.ts +75 -0
- package/dist/contracts/bundle.js +112 -0
- package/dist/contracts/definition.d.ts +67 -4
- package/dist/contracts/document.d.ts +29 -0
- package/dist/contracts/editor.d.ts +172 -0
- package/dist/contracts/editor.js +2 -0
- package/dist/contracts/index.d.ts +2 -0
- package/dist/contracts/index.js +2 -0
- package/dist/contracts/validate.js +59 -3
- package/dist/contracts/values.d.ts +3 -3
- package/dist/contracts/values.js +1 -0
- package/dist/index.html +4 -4
- package/dist/player/app.js +17 -5
- package/dist/player/editor.js +1894 -0
- package/dist/player/editorEngine.js +477 -0
- package/dist/runtime/builtins/core/bundle.d.ts +70 -0
- package/dist/runtime/builtins/core/bundle.js +81 -0
- package/dist/runtime/builtins/core/index.d.ts +3 -2
- package/dist/runtime/builtins/core/index.js +6 -2
- package/dist/runtime/builtins/core/routing.d.ts +27 -0
- package/dist/runtime/builtins/core/routing.js +28 -0
- package/dist/runtime/edit/document.d.ts +94 -0
- package/dist/runtime/edit/document.js +194 -0
- package/dist/runtime/edit/graphEditor.d.ts +223 -0
- package/dist/runtime/edit/graphEditor.js +893 -0
- package/dist/runtime/edit/index.d.ts +12 -0
- package/dist/runtime/edit/index.js +6 -0
- package/dist/runtime/edit/layout.d.ts +50 -0
- package/dist/runtime/edit/layout.js +126 -0
- package/dist/runtime/edit/operations.d.ts +65 -0
- package/dist/runtime/edit/operations.js +57 -0
- package/dist/runtime/edit/parameters.d.ts +75 -0
- package/dist/runtime/edit/parameters.js +284 -0
- package/dist/runtime/edit/read.d.ts +35 -0
- package/dist/runtime/edit/read.js +65 -0
- package/dist/runtime/editor/index.d.ts +104 -0
- package/dist/runtime/editor/index.js +439 -0
- package/dist/runtime/expressions/bundlePaths.d.ts +37 -0
- package/dist/runtime/expressions/bundlePaths.js +55 -0
- package/dist/runtime/expressions/index.d.ts +2 -0
- package/dist/runtime/expressions/index.js +1 -0
- package/dist/runtime/index.d.ts +2 -0
- package/dist/runtime/index.js +2 -0
- package/dist/runtime/params/animator.d.ts +5 -0
- package/dist/runtime/params/animator.js +16 -0
- package/dist/runtime/runtime.js +238 -12
- package/dist/runtime/seed.d.ts +29 -0
- package/dist/runtime/seed.js +52 -0
- package/dist/runtime/types.d.ts +12 -0
- package/dist/stage.js +40 -9
- package/dist/studio/panel.d.ts +2 -0
- package/doc/CLI_REFERENCE.md +100 -0
- package/doc/EDITOR_EXPORT.md +226 -0
- package/doc/HEADLESS_GPU.md +27 -0
- package/doc/MCP.md +53 -0
- package/doc/NODE_REFERENCE.md +66 -0
- package/doc/PROJECT_AUTHORING.md +137 -0
- package/package.json +15 -6
- package/dist/assets/CodeEditor-BEk-kIBd.js +0 -623
- package/dist/assets/CodePanel-CeSdjXkg.js +0 -2
- package/dist/assets/OpenGraphDialog-DCwH9itM.js +0 -1
- package/dist/assets/ProjectSettings-Dg7M5xt9.js +0 -2
- package/dist/assets/SettingsDialog-e7GgsjB9.js +0 -1
- package/dist/assets/definitions-DZpOjWKR.js +0 -1
- package/dist/assets/geo-D2VPjAdt.js +0 -1
- package/dist/assets/index-CMiBdEuT.js +0 -212
- package/dist/assets/index-DMIuml9a.css +0 -1
- package/dist/assets/index-client-BI2H82FJ.js +0 -1
- package/dist/assets/legacy-ZA7textG.js +0 -2
- package/dist/assets/math-B6hpxPJO.js +0 -1
- package/dist/assets/pop-CYUIjT6x.js +0 -1
- package/dist/assets/projectSettingsApi-C1WvIwfP.js +0 -1
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bundle field references in expressions.
|
|
3
|
+
*
|
|
4
|
+
* `ch("format.dpi")` reads field `dpi` of the bundle wired into this node's
|
|
5
|
+
* `format` input. The path names an input, not another node's output, on
|
|
6
|
+
* purpose: the wire is the dependency, so the scheduler orders the producer
|
|
7
|
+
* first and an edit upstream reaches this parameter. Reading another node's
|
|
8
|
+
* output by absolute path would be a dependency no wire records, cooked in
|
|
9
|
+
* whatever order the graph happened to be in.
|
|
10
|
+
*
|
|
11
|
+
* Port and parameter names are identifiers, so a dot can only mean a field.
|
|
12
|
+
*/
|
|
13
|
+
export interface BundleFieldReference {
|
|
14
|
+
/** The input port, on the node whose parameter this is. */
|
|
15
|
+
readonly port: string;
|
|
16
|
+
readonly field: string;
|
|
17
|
+
/** The path as written. */
|
|
18
|
+
readonly path: string;
|
|
19
|
+
}
|
|
20
|
+
/** `format.dpi` or `./format.dpi` as port and field; null for anything else. */
|
|
21
|
+
export declare function parseBundleFieldPath(path: string): {
|
|
22
|
+
port: string;
|
|
23
|
+
field: string;
|
|
24
|
+
} | null;
|
|
25
|
+
/** Every relative bundle field an expression reads, in source order. */
|
|
26
|
+
export declare function bundleFieldReferences(expression: string): BundleFieldReference[];
|
|
27
|
+
/**
|
|
28
|
+
* An absolute or parent path whose last segment looks like `port.field`:
|
|
29
|
+
* an attempt to read another node's bundle without a wire. Returned so a
|
|
30
|
+
* checker can say what to do instead.
|
|
31
|
+
*/
|
|
32
|
+
export declare function remoteBundlePath(path: string): {
|
|
33
|
+
node: string;
|
|
34
|
+
port: string;
|
|
35
|
+
field: string;
|
|
36
|
+
} | null;
|
|
37
|
+
//# sourceMappingURL=bundlePaths.d.ts.map
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bundle field references in expressions.
|
|
3
|
+
*
|
|
4
|
+
* `ch("format.dpi")` reads field `dpi` of the bundle wired into this node's
|
|
5
|
+
* `format` input. The path names an input, not another node's output, on
|
|
6
|
+
* purpose: the wire is the dependency, so the scheduler orders the producer
|
|
7
|
+
* first and an edit upstream reaches this parameter. Reading another node's
|
|
8
|
+
* output by absolute path would be a dependency no wire records, cooked in
|
|
9
|
+
* whatever order the graph happened to be in.
|
|
10
|
+
*
|
|
11
|
+
* Port and parameter names are identifiers, so a dot can only mean a field.
|
|
12
|
+
*/
|
|
13
|
+
const REFERENCE = /\b(?:ch|chs|chv)\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
|
|
14
|
+
/** `format.dpi` or `./format.dpi` as port and field; null for anything else. */
|
|
15
|
+
export function parseBundleFieldPath(path) {
|
|
16
|
+
const local = path.startsWith("./") ? path.slice(2) : path;
|
|
17
|
+
if (local.includes("/"))
|
|
18
|
+
return null;
|
|
19
|
+
const dot = local.indexOf(".");
|
|
20
|
+
if (dot <= 0 || dot === local.length - 1)
|
|
21
|
+
return null;
|
|
22
|
+
return { port: local.slice(0, dot), field: local.slice(dot + 1) };
|
|
23
|
+
}
|
|
24
|
+
/** Every relative bundle field an expression reads, in source order. */
|
|
25
|
+
export function bundleFieldReferences(expression) {
|
|
26
|
+
const references = [];
|
|
27
|
+
REFERENCE.lastIndex = 0;
|
|
28
|
+
let match;
|
|
29
|
+
while ((match = REFERENCE.exec(expression)) !== null) {
|
|
30
|
+
const parsed = parseBundleFieldPath(match[1]);
|
|
31
|
+
if (parsed)
|
|
32
|
+
references.push({ ...parsed, path: match[1] });
|
|
33
|
+
}
|
|
34
|
+
return references;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* An absolute or parent path whose last segment looks like `port.field`:
|
|
38
|
+
* an attempt to read another node's bundle without a wire. Returned so a
|
|
39
|
+
* checker can say what to do instead.
|
|
40
|
+
*/
|
|
41
|
+
export function remoteBundlePath(path) {
|
|
42
|
+
const local = path.startsWith("./") ? path.slice(2) : path;
|
|
43
|
+
const slash = local.lastIndexOf("/");
|
|
44
|
+
if (slash === -1)
|
|
45
|
+
return null;
|
|
46
|
+
const last = local.slice(slash + 1);
|
|
47
|
+
const dot = last.indexOf(".");
|
|
48
|
+
if (dot <= 0 || dot === last.length - 1)
|
|
49
|
+
return null;
|
|
50
|
+
const node = local.slice(0, slash).split("/").filter((part) => part && part !== "." && part !== "..").pop();
|
|
51
|
+
if (!node)
|
|
52
|
+
return null;
|
|
53
|
+
return { node, port: last.slice(0, dot), field: last.slice(dot + 1) };
|
|
54
|
+
}
|
|
55
|
+
//# sourceMappingURL=bundlePaths.js.map
|
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
* evaluate parameters with the same engine.
|
|
7
7
|
*/
|
|
8
8
|
export { ExpressionEvaluator } from "./evaluator.js";
|
|
9
|
+
export { bundleFieldReferences, parseBundleFieldPath, remoteBundlePath } from "./bundlePaths.js";
|
|
10
|
+
export type { BundleFieldReference } from "./bundlePaths.js";
|
|
9
11
|
export { clamp, fit, fit01, lerp, noise, padzero, random, smooth } from "./math.js";
|
|
10
12
|
export { TIME_IDENTIFIERS, TIME_TOKENS, hasTimeReference, preprocessExpression } from "./preprocess.js";
|
|
11
13
|
export { DEFAULT_FPS, MIN_FRAME, TimeState } from "./time.js";
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* evaluate parameters with the same engine.
|
|
7
7
|
*/
|
|
8
8
|
export { ExpressionEvaluator } from "./evaluator.js";
|
|
9
|
+
export { bundleFieldReferences, parseBundleFieldPath, remoteBundlePath } from "./bundlePaths.js";
|
|
9
10
|
export { clamp, fit, fit01, lerp, noise, padzero, random, smooth } from "./math.js";
|
|
10
11
|
export { TIME_IDENTIFIERS, TIME_TOKENS, hasTimeReference, preprocessExpression } from "./preprocess.js";
|
|
11
12
|
export { DEFAULT_FPS, MIN_FRAME, TimeState } from "./time.js";
|
package/dist/runtime/index.d.ts
CHANGED
|
@@ -7,6 +7,8 @@ export * from "./geometry/index.js";
|
|
|
7
7
|
export * from "./gpu.js";
|
|
8
8
|
export * from "./params/index.js";
|
|
9
9
|
export * from "./scene/index.js";
|
|
10
|
+
export * from "./seed.js";
|
|
10
11
|
export { createRuntime, PREFLIGHT_WARNING_CODES } from "./runtime.js";
|
|
11
12
|
export type * from "./types.js";
|
|
13
|
+
export * from "./edit/index.js";
|
|
12
14
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/runtime/index.js
CHANGED
|
@@ -7,5 +7,7 @@ export * from "./geometry/index.js";
|
|
|
7
7
|
export * from "./gpu.js";
|
|
8
8
|
export * from "./params/index.js";
|
|
9
9
|
export * from "./scene/index.js";
|
|
10
|
+
export * from "./seed.js";
|
|
10
11
|
export { createRuntime, PREFLIGHT_WARNING_CODES } from "./runtime.js";
|
|
12
|
+
export * from "./edit/index.js";
|
|
11
13
|
//# sourceMappingURL=index.js.map
|
|
@@ -33,6 +33,9 @@ export interface PropAnimatorTopology<TNode extends AnimatableNode> {
|
|
|
33
33
|
node(id: string): TNode | undefined;
|
|
34
34
|
/** The node feeding `node`'s data input at `index`, for `opinput()`. */
|
|
35
35
|
inputNode(node: TNode, index: number): TNode | null;
|
|
36
|
+
/** The value wired into `node`'s input `name`, for bundle field reads
|
|
37
|
+
* (`ch("format.dpi")`). Optional: a host without it reads none. */
|
|
38
|
+
inputValue?(node: TNode, name: string): unknown;
|
|
36
39
|
/** A failed expression. The parameter has already fallen back to its stored
|
|
37
40
|
* value; this is so the host can say so rather than render a wrong number
|
|
38
41
|
* in silence. */
|
|
@@ -50,6 +53,8 @@ export declare class PropAnimator<TNode extends AnimatableNode> {
|
|
|
50
53
|
setFps(fps: number): void;
|
|
51
54
|
/** Write every bound parameter's value at the current frame into `props`. */
|
|
52
55
|
resolveAll(): void;
|
|
56
|
+
/** Re-resolve one node's bound parameters, e.g. once its inputs exist. */
|
|
57
|
+
resolveNode(node: TNode): void;
|
|
53
58
|
/**
|
|
54
59
|
* One parameter, at the current frame.
|
|
55
60
|
*
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
* deterministic.
|
|
20
20
|
*/
|
|
21
21
|
import { ExpressionEvaluator } from "../expressions/evaluator.js";
|
|
22
|
+
import { parseBundleFieldPath } from "../expressions/bundlePaths.js";
|
|
22
23
|
import { isBoundProp, resolvePropBinding } from "./resolve.js";
|
|
23
24
|
export class PropAnimator {
|
|
24
25
|
topology;
|
|
@@ -45,6 +46,11 @@ export class PropAnimator {
|
|
|
45
46
|
for (const name of Object.keys(node.bindings))
|
|
46
47
|
this.resolve(node, name);
|
|
47
48
|
}
|
|
49
|
+
/** Re-resolve one node's bound parameters, e.g. once its inputs exist. */
|
|
50
|
+
resolveNode(node) {
|
|
51
|
+
for (const name of Object.keys(node.bindings))
|
|
52
|
+
this.resolve(node, name);
|
|
53
|
+
}
|
|
48
54
|
/**
|
|
49
55
|
* One parameter, at the current frame.
|
|
50
56
|
*
|
|
@@ -99,6 +105,16 @@ export class PropAnimator {
|
|
|
99
105
|
// like it works until somebody keys the parameter it points at.
|
|
100
106
|
if (owner.bindings[name])
|
|
101
107
|
return { value: this.resolve(owner, name) };
|
|
108
|
+
// `format.dpi`: a field of the bundle wired into input `format`.
|
|
109
|
+
const field = parseBundleFieldPath(name);
|
|
110
|
+
if (field && this.topology.inputValue) {
|
|
111
|
+
const bundle = this.topology.inputValue(owner, field.port);
|
|
112
|
+
if (bundle === null || typeof bundle !== "object")
|
|
113
|
+
return null;
|
|
114
|
+
if (!Object.prototype.hasOwnProperty.call(bundle, field.field))
|
|
115
|
+
return null;
|
|
116
|
+
return { value: bundle[field.field] };
|
|
117
|
+
}
|
|
102
118
|
if (name in owner.props)
|
|
103
119
|
return { value: owner.props[name] };
|
|
104
120
|
if (name in owner.inputs)
|
package/dist/runtime/runtime.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import { canConnectTypes, validateNodeDefinition, } from "@field/cascade/contracts";
|
|
1
|
+
import { bundleFieldType, bundleSchemasCompatible, bundleValueProblem, canConnectTypes, isBundleValue, selectBundleFields, suggestName, unknownFieldMessage, validateNodeDefinition, } from "@field/cascade/contracts";
|
|
2
2
|
import { CascadeRuntimeError } from "./error.js";
|
|
3
3
|
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
|
+
import { bundleFieldReferences, parseBundleFieldPath, remoteBundlePath, } from "./expressions/bundlePaths.js";
|
|
6
7
|
import { DEFAULT_FPS, MIN_FRAME } from "./expressions/time.js";
|
|
7
8
|
import { isGpuHost } from "./gpu.js";
|
|
9
|
+
import { DEFAULT_GRAPH_SEED, graphSeed, nodeSeed, seededRandom } from "./seed.js";
|
|
8
10
|
/**
|
|
9
11
|
* Preflight codes that describe something other than "this graph cannot run
|
|
10
12
|
* here", so `run` proceeds and a checker reports them as warnings.
|
|
@@ -164,7 +166,11 @@ class Runtime {
|
|
|
164
166
|
const connections = (document.connections ?? []).map((connection, index) => materializeConnection(connection, index, byId));
|
|
165
167
|
validateCardinality(byId, connections);
|
|
166
168
|
validateTopology(nodes, connections);
|
|
167
|
-
const graph = new Graph(this.host, nodes, connections, () => this.graphs.delete(graph), document.annotations);
|
|
169
|
+
const graph = new Graph(this.host, nodes, connections, () => this.graphs.delete(graph), document.annotations, graphSeed(document));
|
|
170
|
+
// The graph's own frame rate, saved with it; a run's `fps` still overrides.
|
|
171
|
+
const fps = document.timeline?.fps;
|
|
172
|
+
if (typeof fps === "number" && Number.isFinite(fps) && fps > 0)
|
|
173
|
+
graph.setFps(fps);
|
|
168
174
|
this.graphs.add(graph);
|
|
169
175
|
return graph;
|
|
170
176
|
}
|
|
@@ -181,6 +187,7 @@ class Graph {
|
|
|
181
187
|
nodes;
|
|
182
188
|
connections;
|
|
183
189
|
onDispose;
|
|
190
|
+
seed;
|
|
184
191
|
graphState = "ready";
|
|
185
192
|
byId;
|
|
186
193
|
dataConnectionsByInput;
|
|
@@ -206,11 +213,14 @@ class Graph {
|
|
|
206
213
|
animator;
|
|
207
214
|
frame = MIN_FRAME;
|
|
208
215
|
fps = DEFAULT_FPS;
|
|
209
|
-
constructor(host, nodes, connections, onDispose, annotations = []
|
|
216
|
+
constructor(host, nodes, connections, onDispose, annotations = [],
|
|
217
|
+
/** The document's seed; each node's `context.seed` derives from it. */
|
|
218
|
+
seed = DEFAULT_GRAPH_SEED) {
|
|
210
219
|
this.host = host;
|
|
211
220
|
this.nodes = nodes;
|
|
212
221
|
this.connections = connections;
|
|
213
222
|
this.onDispose = onDispose;
|
|
223
|
+
this.seed = seed;
|
|
214
224
|
this.byId = new Map(nodes.map((node) => [node.id, node]));
|
|
215
225
|
const byInput = new Map();
|
|
216
226
|
const byNode = new Map();
|
|
@@ -251,6 +261,7 @@ class Graph {
|
|
|
251
261
|
const source = feeding[index]?.source.nodeId;
|
|
252
262
|
return (source ? this.byId.get(source) : undefined) ?? null;
|
|
253
263
|
},
|
|
264
|
+
inputValue: (node, name) => this.dataInputValue(node, name),
|
|
254
265
|
// A failed expression has already fallen back to the stored value, so it
|
|
255
266
|
// is a diagnostic rather than a stopped run — but it is never silent: a
|
|
256
267
|
// parameter that quietly reverts is the hardest fault to see in a
|
|
@@ -447,7 +458,7 @@ class Graph {
|
|
|
447
458
|
// still receives a plain number and never reads a clock.
|
|
448
459
|
else
|
|
449
460
|
this.animator.resolveAll();
|
|
450
|
-
return this.trackRun(options.target ?? { kind: "all" }, options.signal, options.runId);
|
|
461
|
+
return this.trackRun(options.target ?? { kind: "all" }, options.signal, options.runId, undefined, options.reuse === true || options.reuse === "strict" ? options.reuse : false);
|
|
451
462
|
}
|
|
452
463
|
trigger(nodeId, inputName, payload, options = {}) {
|
|
453
464
|
const node = this.requireNode(nodeId);
|
|
@@ -486,8 +497,8 @@ class Graph {
|
|
|
486
497
|
this.disposal ??= this.performDispose();
|
|
487
498
|
return this.disposal;
|
|
488
499
|
}
|
|
489
|
-
trackRun(target, signal, runId, injection) {
|
|
490
|
-
const running = this.executeRun(target, signal, runId, injection);
|
|
500
|
+
trackRun(target, signal, runId, injection, reuse = false) {
|
|
501
|
+
const running = this.executeRun(target, signal, runId, injection, reuse);
|
|
491
502
|
this.activeRun = running;
|
|
492
503
|
const clear = () => {
|
|
493
504
|
if (this.activeRun === running)
|
|
@@ -510,7 +521,7 @@ class Graph {
|
|
|
510
521
|
this.listeners.clear();
|
|
511
522
|
this.onDispose();
|
|
512
523
|
}
|
|
513
|
-
async executeRun(target, externalSignal, requestedId, injection) {
|
|
524
|
+
async executeRun(target, externalSignal, requestedId, injection, reuse = false) {
|
|
514
525
|
this.requireReady();
|
|
515
526
|
const runId = requestedId ?? `run-${this.runCounter++}`;
|
|
516
527
|
const signal = new MutableAbortSignal((diagnostic) => this.host.report(diagnostic));
|
|
@@ -565,10 +576,20 @@ class Graph {
|
|
|
565
576
|
*/
|
|
566
577
|
const insideFeedback = this.feedbackDescendants();
|
|
567
578
|
const ordered = topological(this.nodes, this.connections).filter((node) => initial.includes(node) && !insideFeedback.has(node.id));
|
|
579
|
+
/** Nodes that cooked in this run, for `reuse: "strict"`. */
|
|
580
|
+
const cooked = new Set();
|
|
568
581
|
for (const node of ordered) {
|
|
569
582
|
if (signal.aborted)
|
|
570
583
|
break;
|
|
584
|
+
if (reuse && this.reusable(node) && !(reuse === "strict" && this.feedsFrom(node, cooked))) {
|
|
585
|
+
this.emit({ type: "node:start", runId, nodeId: node.id });
|
|
586
|
+
this.emit({ type: "node:complete", runId, nodeId: node.id });
|
|
587
|
+
continue;
|
|
588
|
+
}
|
|
571
589
|
await this.executeNode(node, runId, signal, {}, enqueue);
|
|
590
|
+
cooked.add(node.id);
|
|
591
|
+
if (!signal.aborted)
|
|
592
|
+
node.lastCook = this.cookKey(node);
|
|
572
593
|
}
|
|
573
594
|
if (injection && !signal.aborted)
|
|
574
595
|
enqueue(injection.nodeId, injection.inputName, injection.payload, {
|
|
@@ -645,14 +666,23 @@ class Graph {
|
|
|
645
666
|
return [name, Object.freeze(values)];
|
|
646
667
|
}
|
|
647
668
|
const connection = connections[0];
|
|
669
|
+
const value = connection
|
|
670
|
+
? this.requireNode(connection.source.nodeId).outputs.get(connection.source.outputName)
|
|
671
|
+
: node.inputs[name];
|
|
672
|
+
// A consumer that declares the fields it reads sees only those, so
|
|
673
|
+
// the declaration is the dependency rather than a comment about it.
|
|
648
674
|
return [
|
|
649
675
|
name,
|
|
650
|
-
|
|
651
|
-
?
|
|
652
|
-
:
|
|
676
|
+
definition.type === "bundle" && definition.fields
|
|
677
|
+
? selectBundleFields(value, definition.fields)
|
|
678
|
+
: value,
|
|
653
679
|
];
|
|
654
680
|
}));
|
|
655
681
|
Object.freeze(inputs);
|
|
682
|
+
// A parameter whose expression reads a wired bundle field resolves now,
|
|
683
|
+
// when the bundle has been cooked, not at the start of the run.
|
|
684
|
+
if (readsBundleFields(node))
|
|
685
|
+
this.animator.resolveNode(node);
|
|
656
686
|
if (this.isRemote(node)) {
|
|
657
687
|
const bridge = this.host.capabilities.serverBridge;
|
|
658
688
|
if (!bridge)
|
|
@@ -680,7 +710,7 @@ class Graph {
|
|
|
680
710
|
const definition = node.definition.outputs?.[name];
|
|
681
711
|
if (!definition || definition.kind !== "data")
|
|
682
712
|
continue;
|
|
683
|
-
const value =
|
|
713
|
+
const value = outputValue(response.outputs[name], definition, `${node.id}.${name}`);
|
|
684
714
|
node.outputs.set(name, value);
|
|
685
715
|
this.emit({
|
|
686
716
|
type: "node:output",
|
|
@@ -709,7 +739,7 @@ class Graph {
|
|
|
709
739
|
}),
|
|
710
740
|
}
|
|
711
741
|
: {
|
|
712
|
-
set: (value) => pendingData.set(name,
|
|
742
|
+
set: (value) => pendingData.set(name, outputValue(value, definition, `${node.id}.${name}`)),
|
|
713
743
|
},
|
|
714
744
|
]));
|
|
715
745
|
const capabilityEntries = [];
|
|
@@ -733,6 +763,7 @@ class Graph {
|
|
|
733
763
|
const progress = {
|
|
734
764
|
report: (event) => this.emit({ type: "node:progress", runId, nodeId: node.id, ...event }),
|
|
735
765
|
};
|
|
766
|
+
const seed = nodeSeed(this.seed, node.id);
|
|
736
767
|
await node.execute({
|
|
737
768
|
nodeId: node.id,
|
|
738
769
|
inputs,
|
|
@@ -741,6 +772,9 @@ class Graph {
|
|
|
741
772
|
capabilities,
|
|
742
773
|
signal,
|
|
743
774
|
progress,
|
|
775
|
+
seed,
|
|
776
|
+
// Fresh per cook: a node that draws N numbers draws the same N each time.
|
|
777
|
+
random: seededRandom(seed),
|
|
744
778
|
});
|
|
745
779
|
if (signal.aborted)
|
|
746
780
|
return;
|
|
@@ -831,6 +865,79 @@ class Graph {
|
|
|
831
865
|
if (!this.host.capabilities[capability])
|
|
832
866
|
push("runtime/missing-capability", node.id, `Missing capability ${capability} for ${node.moduleId} (node ${node.id}) — the ${this.host.environment} host ${this.hostProvides()}`);
|
|
833
867
|
}
|
|
868
|
+
diagnostics.push(...this.bundleDiagnostics(nodes));
|
|
869
|
+
return diagnostics;
|
|
870
|
+
}
|
|
871
|
+
/**
|
|
872
|
+
* Bundle fields read by name, checked against what the producer declares:
|
|
873
|
+
* a consumer's declared `fields`, an expression's `ch("format.dpi")`, and a
|
|
874
|
+
* Pick's `field`. A producer that declares no fields (Pack, a subnet Input,
|
|
875
|
+
* a project node that left them out) cannot be checked and is not. Reading
|
|
876
|
+
* another node's bundle by absolute path is refused with what to do instead.
|
|
877
|
+
*/
|
|
878
|
+
bundleDiagnostics(nodes) {
|
|
879
|
+
const diagnostics = [];
|
|
880
|
+
const push = (nodeId, code, message, at = nodeId) => diagnostics.push({ phase: "preflight", code, path: at, message });
|
|
881
|
+
const producerFields = (node, input) => {
|
|
882
|
+
const connection = this.dataConnectionsByInput.get(`${node.id}\0${input}`)?.[0];
|
|
883
|
+
if (!connection)
|
|
884
|
+
return null;
|
|
885
|
+
const output = this.byId.get(connection.source.nodeId)?.definition.outputs?.[connection.source.outputName];
|
|
886
|
+
if (output?.kind !== "data" || output.type !== "bundle" || !output.fields)
|
|
887
|
+
return null;
|
|
888
|
+
return Object.keys(output.fields);
|
|
889
|
+
};
|
|
890
|
+
for (const node of nodes) {
|
|
891
|
+
for (const [name, input] of Object.entries(node.definition.inputs ?? {})) {
|
|
892
|
+
if (input.kind !== "data" || input.type !== "bundle" || !input.fields)
|
|
893
|
+
continue;
|
|
894
|
+
const known = producerFields(node, name);
|
|
895
|
+
if (!known)
|
|
896
|
+
continue;
|
|
897
|
+
for (const field of input.fields)
|
|
898
|
+
if (!known.includes(field))
|
|
899
|
+
push(node.id, "runtime/bundle-unknown-field", unknownFieldMessage(field, known, `${node.id}.${name} (declared fields)`), `${node.id}.${name}`);
|
|
900
|
+
}
|
|
901
|
+
if (node.moduleId === "cascade.core.Pick") {
|
|
902
|
+
const field = String(node.props.field ?? "").trim();
|
|
903
|
+
const known = producerFields(node, "bundle");
|
|
904
|
+
if (field && known && !known.includes(field))
|
|
905
|
+
push(node.id, "runtime/bundle-unknown-field", unknownFieldMessage(field, known, `${node.id}.field`), `${node.id}.field`);
|
|
906
|
+
}
|
|
907
|
+
for (const [prop, binding] of Object.entries(node.bindings)) {
|
|
908
|
+
if (!binding.expression)
|
|
909
|
+
continue;
|
|
910
|
+
for (const path of expressionPaths(binding.expression)) {
|
|
911
|
+
const remote = remoteBundlePath(path);
|
|
912
|
+
if (remote) {
|
|
913
|
+
const owner = this.byId.get(remote.node);
|
|
914
|
+
const output = owner?.definition.outputs?.[remote.port];
|
|
915
|
+
if (output?.kind === "data" && output.type === "bundle")
|
|
916
|
+
push(node.id, "runtime/bundle-remote-read", `${node.id}.${prop} reads "${path}", another node's bundle, without a wire. `
|
|
917
|
+
+ `Wire ${remote.node}.${remote.port} into a bundle input on ${node.id} and read "${remote.port}.${remote.field}" instead`, `${node.id}.${prop}`);
|
|
918
|
+
continue;
|
|
919
|
+
}
|
|
920
|
+
const reference = parseBundleFieldPath(path);
|
|
921
|
+
if (!reference)
|
|
922
|
+
continue;
|
|
923
|
+
const input = node.definition.inputs?.[reference.port];
|
|
924
|
+
if (!input) {
|
|
925
|
+
const ports = Object.entries(node.definition.inputs ?? {})
|
|
926
|
+
.filter(([, item]) => item.kind === "data" && item.type === "bundle")
|
|
927
|
+
.map(([portName]) => portName);
|
|
928
|
+
const suggestion = suggestName(reference.port, ports);
|
|
929
|
+
push(node.id, "runtime/bundle-unknown-port", `${node.id}.${prop} reads "${path}", but ${node.moduleId} has no input "${reference.port}"`
|
|
930
|
+
+ (suggestion ? ` — did you mean "${suggestion}.${reference.field}"?` : ""), `${node.id}.${prop}`);
|
|
931
|
+
continue;
|
|
932
|
+
}
|
|
933
|
+
if (input.kind !== "data" || input.type !== "bundle")
|
|
934
|
+
continue;
|
|
935
|
+
const known = producerFields(node, reference.port);
|
|
936
|
+
if (known && !known.includes(reference.field))
|
|
937
|
+
push(node.id, "runtime/bundle-unknown-field", unknownFieldMessage(reference.field, known, `${node.id}.${prop}`), `${node.id}.${prop}`);
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
}
|
|
834
941
|
return diagnostics;
|
|
835
942
|
}
|
|
836
943
|
/**
|
|
@@ -1012,6 +1119,66 @@ class Graph {
|
|
|
1012
1119
|
* per step. Computed once per run rather than per step: the hierarchy cannot
|
|
1013
1120
|
* change mid-run, and this is walked for every node in the order.
|
|
1014
1121
|
*/
|
|
1122
|
+
/**
|
|
1123
|
+
* Early cutoff for `run({ reuse: true })`: a node whose wired inputs are the
|
|
1124
|
+
* very same values and whose resolved props are equal keeps its outputs.
|
|
1125
|
+
*
|
|
1126
|
+
* Sound because a definition-v1 node may observe only its inputs, its props
|
|
1127
|
+
* and declared capabilities, and time reaches it through props. Upstream
|
|
1128
|
+
* reuse keeps output identity, so one unchanged stage skips everything below
|
|
1129
|
+
* it that is unchanged too. Trigger-driven, feedback and remote nodes always
|
|
1130
|
+
* cook. Off by default: a host opts in when it re-runs a graph on every edit.
|
|
1131
|
+
*/
|
|
1132
|
+
/** Whether any data input of `node` (or, for a subnet, any inner output)
|
|
1133
|
+
* comes from a node in `cooked`. */
|
|
1134
|
+
feedsFrom(node, cooked) {
|
|
1135
|
+
for (const name of Object.keys(node.definition.inputs ?? {})) {
|
|
1136
|
+
for (const connection of this.dataConnectionsByInput.get(`${node.id}\0${name}`) ?? [])
|
|
1137
|
+
if (cooked.has(connection.source.nodeId))
|
|
1138
|
+
return true;
|
|
1139
|
+
}
|
|
1140
|
+
if (node.moduleId === "cascade.core.Input" && node.parent && cooked.size)
|
|
1141
|
+
return true;
|
|
1142
|
+
if (node.moduleId === "cascade.core.Subnet")
|
|
1143
|
+
for (const child of this.nodes)
|
|
1144
|
+
if (child.parent === node.id && cooked.has(child.id))
|
|
1145
|
+
return true;
|
|
1146
|
+
return false;
|
|
1147
|
+
}
|
|
1148
|
+
reusable(node) {
|
|
1149
|
+
const last = node.lastCook;
|
|
1150
|
+
if (!last || this.isRemote(node) || node.definition.container === "feedback")
|
|
1151
|
+
return false;
|
|
1152
|
+
if (Object.values(node.definition.inputs ?? {}).some((input) => input.kind === "trigger"))
|
|
1153
|
+
return false;
|
|
1154
|
+
if (Object.values(node.definition.outputs ?? {}).some((output) => output.kind === "trigger"))
|
|
1155
|
+
return false;
|
|
1156
|
+
const next = this.cookKey(node);
|
|
1157
|
+
return sameValues(last.inputs, next.inputs, Object.is) && sameValues(last.props, next.props, sameJson);
|
|
1158
|
+
}
|
|
1159
|
+
cookKey(node) {
|
|
1160
|
+
const inputs = [];
|
|
1161
|
+
for (const [name, definition] of Object.entries(node.definition.inputs ?? {})) {
|
|
1162
|
+
if (definition.kind !== "data")
|
|
1163
|
+
continue;
|
|
1164
|
+
if (node.moduleId === "cascade.core.Input" && name === "value" && node.parent) {
|
|
1165
|
+
const parent = this.requireNode(node.parent);
|
|
1166
|
+
inputs.push(this.dataInputValue(parent, `input_${integerProp(node.props.inputIndex)}`));
|
|
1167
|
+
continue;
|
|
1168
|
+
}
|
|
1169
|
+
const connections = this.dataConnectionsByInput.get(`${node.id}\0${name}`) ?? [];
|
|
1170
|
+
if (!connections.length)
|
|
1171
|
+
inputs.push(node.inputs[name]);
|
|
1172
|
+
for (const connection of connections)
|
|
1173
|
+
inputs.push(this.requireNode(connection.source.nodeId).outputs.get(connection.source.outputName));
|
|
1174
|
+
}
|
|
1175
|
+
// A subnet's outputs are its inner Output nodes' values, which are inputs in all but name.
|
|
1176
|
+
if (node.moduleId === "cascade.core.Subnet")
|
|
1177
|
+
for (const child of this.nodes)
|
|
1178
|
+
if (child.parent === node.id && child.moduleId === "cascade.core.Output")
|
|
1179
|
+
inputs.push(child.outputs.get("output"));
|
|
1180
|
+
return { inputs, props: Object.keys(node.definition.props ?? {}).map((name) => node.props[name]) };
|
|
1181
|
+
}
|
|
1015
1182
|
feedbackDescendants() {
|
|
1016
1183
|
const containers = new Set(this.nodes
|
|
1017
1184
|
.filter((node) => node.definition.container === "feedback")
|
|
@@ -1480,6 +1647,12 @@ function materializeConnection(connection, index, nodes) {
|
|
|
1480
1647
|
input.kind === "data" &&
|
|
1481
1648
|
!canConnectTypes(output.type, input.type))
|
|
1482
1649
|
misuse("runtime/connection-type", `Connection ${index} has incompatible types ${output.type} and ${input.type}`);
|
|
1650
|
+
if (output.kind === "data" &&
|
|
1651
|
+
input.kind === "data" &&
|
|
1652
|
+
output.type === "bundle" &&
|
|
1653
|
+
input.type === "bundle" &&
|
|
1654
|
+
!bundleSchemasCompatible(output.schema, input.schema))
|
|
1655
|
+
misuse("runtime/connection-schema", `Connection ${index} wires a ${output.schema} bundle into an input that takes ${input.schema}`);
|
|
1483
1656
|
return Object.freeze({
|
|
1484
1657
|
source: Object.freeze(endpoints.source),
|
|
1485
1658
|
target: Object.freeze(endpoints.target),
|
|
@@ -1766,6 +1939,42 @@ const TUPLE_LENGTHS = {
|
|
|
1766
1939
|
mat4: 16,
|
|
1767
1940
|
color: 4,
|
|
1768
1941
|
};
|
|
1942
|
+
/**
|
|
1943
|
+
* An output value, checked against its declaration. A bundle with declared
|
|
1944
|
+
* fields may only set those, and each one must be a valid value of its type.
|
|
1945
|
+
*/
|
|
1946
|
+
function outputValue(value, definition, path) {
|
|
1947
|
+
if (definition.type !== "bundle" || !definition.fields)
|
|
1948
|
+
return runtimeValue(value, definition.type, false, path);
|
|
1949
|
+
const fields = definition.fields;
|
|
1950
|
+
const problem = bundleValueProblem(value, fields);
|
|
1951
|
+
if (problem)
|
|
1952
|
+
misuse("runtime/value-type", `${path} ${problem}`);
|
|
1953
|
+
if (isBundleValue(value))
|
|
1954
|
+
for (const [field, item] of Object.entries(value))
|
|
1955
|
+
runtimeValue(item, bundleFieldType(fields[field]), true, `${path}.${field}`);
|
|
1956
|
+
return runtimeValue(value, "bundle", false, path);
|
|
1957
|
+
}
|
|
1958
|
+
/** Every path an expression names through ch/chs/chv. */
|
|
1959
|
+
function expressionPaths(expression) {
|
|
1960
|
+
const paths = [];
|
|
1961
|
+
const pattern = /\b(?:ch|chs|chv)\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
|
|
1962
|
+
let match;
|
|
1963
|
+
while ((match = pattern.exec(expression)) !== null)
|
|
1964
|
+
paths.push(match[1]);
|
|
1965
|
+
return paths;
|
|
1966
|
+
}
|
|
1967
|
+
/** Whether any of a node's bound parameters reads a wired bundle field. */
|
|
1968
|
+
function readsBundleFields(node) {
|
|
1969
|
+
for (const binding of Object.values(node.bindings))
|
|
1970
|
+
if (binding.expression)
|
|
1971
|
+
for (const reference of bundleFieldReferences(binding.expression)) {
|
|
1972
|
+
const input = node.definition.inputs?.[reference.port];
|
|
1973
|
+
if (input?.kind === "data" && input.type === "bundle")
|
|
1974
|
+
return true;
|
|
1975
|
+
}
|
|
1976
|
+
return false;
|
|
1977
|
+
}
|
|
1769
1978
|
function runtimeValue(value, type, allowUndefined, path) {
|
|
1770
1979
|
if (value === undefined) {
|
|
1771
1980
|
if (allowUndefined)
|
|
@@ -1783,7 +1992,9 @@ function runtimeValue(value, type, allowUndefined, path) {
|
|
|
1783
1992
|
value.every((item) => type.endsWith("i")
|
|
1784
1993
|
? typeof item === "number" && Number.isInteger(item)
|
|
1785
1994
|
: typeof item === "number" && Number.isFinite(item))) ||
|
|
1995
|
+
(type === "bundle" && isBundleValue(value)) ||
|
|
1786
1996
|
(!tupleLength &&
|
|
1997
|
+
type !== "bundle" &&
|
|
1787
1998
|
type !== "float" &&
|
|
1788
1999
|
type !== "int" &&
|
|
1789
2000
|
type !== "bool" &&
|
|
@@ -1792,4 +2003,19 @@ function runtimeValue(value, type, allowUndefined, path) {
|
|
|
1792
2003
|
misuse("runtime/value-type", `${path} is not a valid ${type}`);
|
|
1793
2004
|
return snapshot(value);
|
|
1794
2005
|
}
|
|
2006
|
+
function sameValues(a, b, equal) {
|
|
2007
|
+
return a.length === b.length && a.every((value, index) => equal(value, b[index]));
|
|
2008
|
+
}
|
|
2009
|
+
function sameJson(a, b) {
|
|
2010
|
+
if (Object.is(a, b))
|
|
2011
|
+
return true;
|
|
2012
|
+
if (typeof a !== "object" || typeof b !== "object" || a === null || b === null)
|
|
2013
|
+
return false;
|
|
2014
|
+
try {
|
|
2015
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
2016
|
+
}
|
|
2017
|
+
catch {
|
|
2018
|
+
return false;
|
|
2019
|
+
}
|
|
2020
|
+
}
|
|
1795
2021
|
//# sourceMappingURL=runtime.js.map
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Seeds: one number per graph, one derived number per node, the same in every
|
|
3
|
+
* host.
|
|
4
|
+
*
|
|
5
|
+
* A node that wants randomness gets `context.seed` and `context.random` from
|
|
6
|
+
* whichever host cooks it — the headless runtime here, Studio through its
|
|
7
|
+
* definition adapter — and both compute them with these functions. So the
|
|
8
|
+
* same graph draws the same dots headless and in Studio, and changing the
|
|
9
|
+
* graph's `seed` reshuffles every node at once while each keeps its own
|
|
10
|
+
* stream (doc/PRODUCTION_READINESS.md, item 5).
|
|
11
|
+
*
|
|
12
|
+
* The node id is part of the derivation, not its position: ids are stable
|
|
13
|
+
* across edits, so adding a node never reshuffles its neighbours.
|
|
14
|
+
*/
|
|
15
|
+
/** The graph seed when a document does not state one. */
|
|
16
|
+
export declare const DEFAULT_GRAPH_SEED = 0;
|
|
17
|
+
/** A document's `seed`, as an unsigned 32-bit integer; the default when absent
|
|
18
|
+
* or not an integer. */
|
|
19
|
+
export declare function graphSeed(document: Readonly<{
|
|
20
|
+
seed?: unknown;
|
|
21
|
+
}> | null | undefined): number;
|
|
22
|
+
/**
|
|
23
|
+
* A node's seed: FNV-1a over `<graph seed>:<node id>`, finished with
|
|
24
|
+
* MurmurHash3's avalanche so neighbouring ids give unrelated seeds.
|
|
25
|
+
*/
|
|
26
|
+
export declare function nodeSeed(seedOfGraph: number, nodeId: string): number;
|
|
27
|
+
/** Mulberry32: a small, fast stream of floats in [0, 1) from one seed. */
|
|
28
|
+
export declare function seededRandom(seed: number): () => number;
|
|
29
|
+
//# sourceMappingURL=seed.d.ts.map
|