@field/cascade 0.2.22 → 0.3.1
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 +47 -7
- package/ARCHITECTURE.md +30 -2
- package/CHANGELOG.md +107 -0
- package/DESIGN.md +10 -5
- package/README.md +103 -7
- package/dist/assets/CodeEditor-D22fVi-4.js +623 -0
- package/dist/assets/CodePanel-DPNEYfcR.js +2 -0
- package/dist/assets/CustomNodeDialog-DeHCAvgb.js +1 -0
- package/dist/assets/{ExportDialog-DWN4nhXI.js → ExportDialog-BJu2Bpvv.js} +2 -2
- package/dist/assets/ProjectSettings-DWpVtWEx.js +2 -0
- package/dist/assets/SettingsDialog-za00IS3L.js +1 -0
- package/dist/assets/VersionHistoryDialog-DTMGhI0_.js +3 -0
- package/dist/assets/{core-Bji4aZUU.js → core-JsyKMO1k.js} +29 -38
- package/dist/assets/{cssMode-B6Z_Q1Kh.js → cssMode-emdRheFK.js} +1 -1
- package/dist/assets/{editor.api-jMoW_43X.js → editor.api-xHX-fMSo.js} +1 -1
- package/dist/assets/{freemarker2-DvvPV6m5.js → freemarker2-C3M8_Gs3.js} +1 -1
- package/dist/assets/geo-BAR6lQp5.js +1 -0
- package/dist/assets/{handlebars-B8rfuSIa.js → handlebars-CBMurtFk.js} +1 -1
- package/dist/assets/{html-DyHm6JCp.js → html-xbLQwknw.js} +1 -1
- package/dist/assets/{htmlMode-E9XLWE5u.js → htmlMode-2wKdDTon.js} +1 -1
- package/dist/assets/{image-BUcQC7PG.js → image-i1j6nWKe.js} +26 -40
- package/dist/assets/index-BDV5xGBZ.css +1 -0
- package/dist/assets/index-CMcH8AgP.js +212 -0
- package/dist/assets/index-client-BoJgsN9M.js +1 -0
- package/dist/assets/{javascript-C_bM5PCN.js → javascript-DJaOo_2I.js} +1 -1
- package/dist/assets/{jsonMode-BdyVS7G4.js → jsonMode-Bheas8_0.js} +1 -1
- package/dist/assets/{legacy-DIu3XRwl.js → legacy-CAj2_7QE.js} +2 -2
- package/dist/assets/{liquid-CDkn3spA.js → liquid-BCj_BGbM.js} +1 -1
- package/dist/assets/{lspLanguageFeatures-AB5DMAsr.js → lspLanguageFeatures-BfIsx_xz.js} +1 -1
- package/dist/assets/{mdx-BvH_NjDN.js → mdx-DwiQDiVO.js} +1 -1
- package/dist/assets/{nodeTypeUtils-CweA3NHd.js → nodeTypeUtils-BBA1JqHx.js} +1 -1
- package/dist/assets/{python-B7uii4fi.js → python-BjrB5nqj.js} +1 -1
- package/dist/assets/{razor-Z48fWdJc.js → razor-CfoBmGDs.js} +1 -1
- package/dist/assets/{tsMode-BDgacVG4.js → tsMode-BcEoMSxp.js} +1 -1
- package/dist/assets/{typescript-Derb6Oqa.js → typescript-DIg7TEmQ.js} +1 -1
- package/dist/assets/{workers-BtoMX_pm.js → workers-vXYuwtPw.js} +1 -1
- package/dist/assets/{xml-B1CaFSrG.js → xml-DexLQIQy.js} +1 -1
- package/dist/assets/{yaml-HmIeA0KH.js → yaml-DUnD_1dE.js} +1 -1
- package/dist/cli/index.js +16467 -10200
- package/dist/cli/runtime/gpu.ts +76 -0
- package/dist/cli/runtime/io.ts +93 -7
- package/dist/contracts/animation.d.ts +40 -0
- package/dist/contracts/animation.js +14 -0
- package/dist/contracts/capabilities.d.ts +69 -6
- package/dist/contracts/definition.d.ts +60 -2
- package/dist/contracts/document.d.ts +15 -0
- package/dist/contracts/index.d.ts +1 -0
- package/dist/contracts/index.js +1 -0
- package/dist/contracts/schema.js +8 -5
- package/dist/contracts/validate.d.ts +10 -1
- package/dist/contracts/validate.js +20 -3
- package/dist/gpu.d.ts +71 -0
- package/dist/gpu.js +53 -0
- package/dist/graphs/default.cascade +1 -1
- package/dist/index.html +5 -5
- package/dist/io.d.ts +196 -0
- package/dist/io.js +303 -0
- package/dist/net.d.ts +1 -0
- package/dist/net.js +28 -0
- package/dist/runtime/animation/channel.d.ts +108 -0
- package/dist/runtime/animation/channel.js +204 -0
- package/dist/runtime/animation/frameRange.d.ts +78 -0
- package/dist/runtime/animation/frameRange.js +76 -0
- package/dist/runtime/animation/index.d.ts +10 -0
- package/dist/runtime/animation/index.js +8 -0
- package/dist/runtime/builtins/core/random.d.ts +1 -1
- package/dist/runtime/builtins/core/remap.d.ts +1 -1
- package/dist/runtime/builtins/geo/copyToPoints.d.ts +1 -1
- package/dist/runtime/builtins/geo/copyToPoints.js +1 -1
- package/dist/runtime/builtins/geo/generators.d.ts +1 -1
- package/dist/runtime/builtins/geo/merge.d.ts +1 -1
- package/dist/runtime/builtins/geo/svgExport.d.ts +1 -1
- package/dist/runtime/builtins/geo/svgExport.js +1 -1
- package/dist/runtime/builtins/geo/transform.d.ts +1 -1
- package/dist/runtime/builtins/geo/transform.js +1 -1
- package/dist/runtime/definition/architecture.d.ts +1 -1
- package/dist/runtime/definition/extract.d.ts +1 -1
- package/dist/runtime/definition/extract.js +1 -1
- package/dist/runtime/error.d.ts +1 -1
- package/dist/runtime/expressions/evaluator.d.ts +53 -0
- package/dist/runtime/expressions/evaluator.js +193 -0
- package/dist/runtime/expressions/index.d.ts +15 -0
- package/dist/runtime/expressions/index.js +12 -0
- package/dist/runtime/expressions/math.d.ts +75 -0
- package/dist/runtime/expressions/math.js +140 -0
- package/dist/runtime/expressions/preprocess.d.ts +33 -0
- package/dist/runtime/expressions/preprocess.js +102 -0
- package/dist/runtime/expressions/scope.d.ts +27 -0
- package/dist/runtime/expressions/scope.js +10 -0
- package/dist/runtime/expressions/time.d.ts +54 -0
- package/dist/runtime/expressions/time.js +71 -0
- package/dist/runtime/expressions/types.d.ts +66 -0
- package/dist/runtime/expressions/types.js +9 -0
- package/dist/runtime/geometry/attributes.d.ts +2 -2
- package/dist/runtime/geometry/attributes.js +1 -1
- package/dist/runtime/geometry/builder.d.ts +1 -1
- package/dist/runtime/geometry/builder.js +1 -1
- package/dist/runtime/geometry/copy.d.ts +1 -1
- package/dist/runtime/geometry/copy.js +1 -1
- package/dist/runtime/geometry/generate.d.ts +1 -1
- package/dist/runtime/geometry/groups.d.ts +1 -1
- package/dist/runtime/geometry/groups.js +1 -1
- package/dist/runtime/geometry/matrix.d.ts +1 -1
- package/dist/runtime/geometry/merge.d.ts +1 -1
- package/dist/runtime/geometry/merge.js +1 -1
- package/dist/runtime/geometry/primitives.d.ts +1 -1
- package/dist/runtime/geometry/primitives.js +1 -1
- package/dist/runtime/geometry/promote.d.ts +1 -1
- package/dist/runtime/geometry/promote.js +1 -1
- package/dist/runtime/geometry/resample.d.ts +1 -1
- package/dist/runtime/geometry/resample.js +1 -1
- package/dist/runtime/geometry/svg.d.ts +1 -1
- package/dist/runtime/geometry/svg.js +1 -1
- package/dist/runtime/geometry/transform.d.ts +1 -1
- package/dist/runtime/geometry/transform.js +1 -1
- package/dist/runtime/index.d.ts +4 -1
- package/dist/runtime/index.js +4 -1
- package/dist/runtime/params/animator.d.ts +74 -0
- package/dist/runtime/params/animator.js +137 -0
- package/dist/runtime/params/index.d.ts +11 -0
- package/dist/runtime/params/index.js +9 -0
- package/dist/runtime/params/resolve.d.ts +73 -0
- package/dist/runtime/params/resolve.js +62 -0
- package/dist/runtime/runtime.d.ts +42 -0
- package/dist/runtime/runtime.js +351 -29
- package/dist/runtime/types.d.ts +32 -6
- package/dist/shell.d.ts +3 -3
- package/doc/NODE_REFERENCE.md +395 -0
- package/doc/PROJECT_AUTHORING.md +148 -6
- package/package.json +30 -3
- package/dist/assets/CodeEditor-DoaNEUaM.js +0 -641
- package/dist/assets/CodePanel-B0_2XJ-t.js +0 -2
- package/dist/assets/CustomNodeDialog-026lzecD.js +0 -1
- package/dist/assets/ProjectSettings-BzT164Yx.js +0 -2
- package/dist/assets/SettingsDialog-DmHb1V0r.js +0 -1
- package/dist/assets/VersionHistoryDialog-hH6VsXLX.js +0 -3
- package/dist/assets/geo-CXmCtzMM.js +0 -2
- package/dist/assets/index-BXYXSdgh.js +0 -224
- package/dist/assets/index-CrNke7SW.css +0 -1
- package/dist/assets/index-client-DN_FvjN8.js +0 -1
- package/dist/assets/quill-DjZ69wL_.js +0 -121
- package/dist/cli/index.js.map +0 -7
- package/dist/contracts/capabilities.d.ts.map +0 -1
- package/dist/contracts/capabilities.js.map +0 -1
- package/dist/contracts/color.d.ts.map +0 -1
- package/dist/contracts/color.js.map +0 -1
- package/dist/contracts/definition.d.ts.map +0 -1
- package/dist/contracts/definition.js.map +0 -1
- package/dist/contracts/document.d.ts.map +0 -1
- package/dist/contracts/document.js.map +0 -1
- package/dist/contracts/geometry.d.ts.map +0 -1
- package/dist/contracts/geometry.js.map +0 -1
- package/dist/contracts/index.d.ts.map +0 -1
- package/dist/contracts/index.js.map +0 -1
- package/dist/contracts/schema.d.ts.map +0 -1
- package/dist/contracts/schema.js.map +0 -1
- package/dist/contracts/validate.d.ts.map +0 -1
- package/dist/contracts/validate.js.map +0 -1
- package/dist/contracts/values.d.ts.map +0 -1
- package/dist/contracts/values.js.map +0 -1
- package/dist/graphs/default-orig.cascade +0 -128
- package/dist/runtime/browser.d.ts.map +0 -1
- package/dist/runtime/browser.js.map +0 -1
- package/dist/runtime/builtins/core/index.d.ts.map +0 -1
- package/dist/runtime/builtins/core/index.js.map +0 -1
- package/dist/runtime/builtins/core/random.d.ts.map +0 -1
- package/dist/runtime/builtins/core/random.js.map +0 -1
- package/dist/runtime/builtins/core/remap.d.ts.map +0 -1
- package/dist/runtime/builtins/core/remap.js.map +0 -1
- package/dist/runtime/builtins/core/routing.d.ts.map +0 -1
- package/dist/runtime/builtins/core/routing.js.map +0 -1
- package/dist/runtime/builtins/core/structural.d.ts.map +0 -1
- package/dist/runtime/builtins/core/structural.js.map +0 -1
- package/dist/runtime/builtins/geo/copyToPoints.d.ts.map +0 -1
- package/dist/runtime/builtins/geo/copyToPoints.js.map +0 -1
- package/dist/runtime/builtins/geo/definitions.d.ts.map +0 -1
- package/dist/runtime/builtins/geo/definitions.js.map +0 -1
- package/dist/runtime/builtins/geo/generators.d.ts.map +0 -1
- package/dist/runtime/builtins/geo/generators.js.map +0 -1
- package/dist/runtime/builtins/geo/index.d.ts.map +0 -1
- package/dist/runtime/builtins/geo/index.js.map +0 -1
- package/dist/runtime/builtins/geo/merge.d.ts.map +0 -1
- package/dist/runtime/builtins/geo/merge.js.map +0 -1
- package/dist/runtime/builtins/geo/namespace.d.ts.map +0 -1
- package/dist/runtime/builtins/geo/namespace.js.map +0 -1
- package/dist/runtime/builtins/geo/svgExport.d.ts.map +0 -1
- package/dist/runtime/builtins/geo/svgExport.js.map +0 -1
- package/dist/runtime/builtins/geo/transform.d.ts.map +0 -1
- package/dist/runtime/builtins/geo/transform.js.map +0 -1
- package/dist/runtime/builtins/index.d.ts.map +0 -1
- package/dist/runtime/builtins/index.js.map +0 -1
- package/dist/runtime/definition/architecture.d.ts.map +0 -1
- package/dist/runtime/definition/architecture.js.map +0 -1
- package/dist/runtime/definition/extract.d.ts.map +0 -1
- package/dist/runtime/definition/extract.js.map +0 -1
- package/dist/runtime/error.d.ts.map +0 -1
- package/dist/runtime/error.js.map +0 -1
- package/dist/runtime/geometry/attributes.d.ts.map +0 -1
- package/dist/runtime/geometry/attributes.js.map +0 -1
- package/dist/runtime/geometry/builder.d.ts.map +0 -1
- package/dist/runtime/geometry/builder.js.map +0 -1
- package/dist/runtime/geometry/copy.d.ts.map +0 -1
- package/dist/runtime/geometry/copy.js.map +0 -1
- package/dist/runtime/geometry/generate.d.ts.map +0 -1
- package/dist/runtime/geometry/generate.js.map +0 -1
- package/dist/runtime/geometry/groups.d.ts.map +0 -1
- package/dist/runtime/geometry/groups.js.map +0 -1
- package/dist/runtime/geometry/index.d.ts.map +0 -1
- package/dist/runtime/geometry/index.js.map +0 -1
- package/dist/runtime/geometry/matrix.d.ts.map +0 -1
- package/dist/runtime/geometry/matrix.js.map +0 -1
- package/dist/runtime/geometry/merge.d.ts.map +0 -1
- package/dist/runtime/geometry/merge.js.map +0 -1
- package/dist/runtime/geometry/primitives.d.ts.map +0 -1
- package/dist/runtime/geometry/primitives.js.map +0 -1
- package/dist/runtime/geometry/promote.d.ts.map +0 -1
- package/dist/runtime/geometry/promote.js.map +0 -1
- package/dist/runtime/geometry/resample.d.ts.map +0 -1
- package/dist/runtime/geometry/resample.js.map +0 -1
- package/dist/runtime/geometry/svg.d.ts.map +0 -1
- package/dist/runtime/geometry/svg.js.map +0 -1
- package/dist/runtime/geometry/transform.d.ts.map +0 -1
- package/dist/runtime/geometry/transform.js.map +0 -1
- package/dist/runtime/index.d.ts.map +0 -1
- package/dist/runtime/index.js.map +0 -1
- package/dist/runtime/node.d.ts.map +0 -1
- package/dist/runtime/node.js.map +0 -1
- package/dist/runtime/runtime.d.ts.map +0 -1
- package/dist/runtime/runtime.js.map +0 -1
- package/dist/runtime/types.d.ts.map +0 -1
- package/dist/runtime/types.js.map +0 -1
- package/dist/shell.js.map +0 -7
- package/dist/stage.js.map +0 -7
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one place a parameter becomes a value.
|
|
3
|
+
*
|
|
4
|
+
* Three bindings, in one order: a keyframe channel, an expression, the stored
|
|
5
|
+
* value. That order was implemented once, in Studio's `Node.evalParm`, which
|
|
6
|
+
* meant the deterministic runtime had no answer at all — a definition-v1 prop
|
|
7
|
+
* carrying `$T` or a drawn curve rendered as a static number offline. Copying
|
|
8
|
+
* `evalParm` into the runtime would have produced a second answer to the same
|
|
9
|
+
* question, which is exactly the two-parameter-stores mistake this codebase
|
|
10
|
+
* has already paid for once. So the order moved here, into the neutral
|
|
11
|
+
* runtime, and both hosts call it.
|
|
12
|
+
*
|
|
13
|
+
* What it deliberately does NOT own: the expression *engine*. Evaluation is
|
|
14
|
+
* passed in as a callback, because each host has its own scope (Studio's
|
|
15
|
+
* `ExpressionEngine` reaches into the compatibility graph; the runtime binds
|
|
16
|
+
* its own node map) and its own error bookkeeping (Studio stores the message
|
|
17
|
+
* on the prop for the Inspector to show). Keeping evaluation out is also what
|
|
18
|
+
* keeps this module dependent on nothing but the channel sampler — no cycle
|
|
19
|
+
* between `animation` and `expressions`, and no clock anywhere: the frame is a
|
|
20
|
+
* value the caller hands in, which is what keeps a cook deterministic.
|
|
21
|
+
*/
|
|
22
|
+
import { isEmptyChannel, sampleChannel } from "../animation/channel.js";
|
|
23
|
+
/** True when a parameter carries something beyond its stored value, and so has
|
|
24
|
+
* to be resolved per frame rather than read once. */
|
|
25
|
+
export function isBoundProp(binding) {
|
|
26
|
+
if (!binding)
|
|
27
|
+
return false;
|
|
28
|
+
return !!binding.expression || !isEmptyChannel(binding.channel);
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Resolve a parameter at a frame.
|
|
32
|
+
*
|
|
33
|
+
* A parameter may carry both a channel and an expression, and the channel
|
|
34
|
+
* wins. The expression is kept, inert, and comes back when the channel is
|
|
35
|
+
* deleted: silently discarding what an author wrote is the worse of the two
|
|
36
|
+
* failures. `contracts/animation.ts` states the same rule for the document, so
|
|
37
|
+
* the file format and the resolver agree by construction.
|
|
38
|
+
*
|
|
39
|
+
* An empty channel resolves to nothing rather than to zero, so a parameter
|
|
40
|
+
* whose last key was deleted falls through to its expression or its value
|
|
41
|
+
* instead of snapping to the origin.
|
|
42
|
+
*/
|
|
43
|
+
export function resolvePropBinding(binding, context) {
|
|
44
|
+
if (!isEmptyChannel(binding.channel)) {
|
|
45
|
+
const sampled = sampleChannel(binding.channel, context.frame);
|
|
46
|
+
if (sampled !== undefined)
|
|
47
|
+
return { value: sampled, source: "channel" };
|
|
48
|
+
}
|
|
49
|
+
if (binding.expression && context.evaluateExpression) {
|
|
50
|
+
const result = context.evaluateExpression(binding.expression);
|
|
51
|
+
// A failed expression falls back to the stored value and reports why. The
|
|
52
|
+
// alternative — a hole in the middle of a render — is the one outcome
|
|
53
|
+
// nobody can act on.
|
|
54
|
+
if (result?.error) {
|
|
55
|
+
return { value: binding.value, source: "value", error: result.error };
|
|
56
|
+
}
|
|
57
|
+
if (result)
|
|
58
|
+
return { value: result.value, source: "expression" };
|
|
59
|
+
}
|
|
60
|
+
return { value: binding.value, source: "value" };
|
|
61
|
+
}
|
|
62
|
+
//# sourceMappingURL=resolve.js.map
|
|
@@ -1,3 +1,45 @@
|
|
|
1
1
|
import type { CascadeRuntime, CreateRuntimeOptions } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Preflight codes that describe something other than "this graph cannot run
|
|
4
|
+
* here", so `run` proceeds and a checker reports them as warnings.
|
|
5
|
+
*
|
|
6
|
+
* False positives are worse than blindness: a validator that cries wolf gets
|
|
7
|
+
* ignored, and then it is no better than the one that saw nothing. So the line
|
|
8
|
+
* is drawn at what the diagnostic is about.
|
|
9
|
+
*
|
|
10
|
+
* `preflight-environment` is about the checker's reach — a node saying
|
|
11
|
+
* `runsOn: 'browser'` claims nothing about the CLI and is not defective
|
|
12
|
+
* because somebody validated it from a terminal.
|
|
13
|
+
*
|
|
14
|
+
* `stray-params` is about the document — values were lost on the way in, which
|
|
15
|
+
* is worth saying loudly and is not a reason to refuse to run: a graph with a
|
|
16
|
+
* leftover `params` array runs perfectly well on its defaults, which is
|
|
17
|
+
* precisely the problem.
|
|
18
|
+
*
|
|
19
|
+
* It was briefly an error, and Marcus reverted that on 2026-09-08 with the
|
|
20
|
+
* evidence that decides it: while converting sixty-two nodes across four
|
|
21
|
+
* sketches, every one of them was a mixed graph for most of the day, and as an
|
|
22
|
+
* error an unconverted or half-converted sketch **stops validating at all** —
|
|
23
|
+
* which is the one thing you need while converting it. The warning is the
|
|
24
|
+
* conversion checklist; as an error it is a locked door on the room you are
|
|
25
|
+
* working in.
|
|
26
|
+
*
|
|
27
|
+
* The fair case for the error was that the warning never fired, and that was
|
|
28
|
+
* true: `check` bailed on dynamic modules before preflight ran. The fix for
|
|
29
|
+
* that was the reporting *order*, not the severity.
|
|
30
|
+
*
|
|
31
|
+
* This set lives here rather than in the CLI because both paths read it, and a
|
|
32
|
+
* duplicated copy is what let `validate` and `check` pass graphs `run` then
|
|
33
|
+
* rejected.
|
|
34
|
+
*
|
|
35
|
+
* **It classifies a static check, and it is not the run gate.** Those are
|
|
36
|
+
* different questions and conflating them was a real bug of mine, caught by
|
|
37
|
+
* Codex's own comment on 2026-09-08: *execution is stricter.* A server run must
|
|
38
|
+
* still refuse a browser node — you cannot cook WebGPU headlessly, so tolerating
|
|
39
|
+
* it means executing and failing on an undefined capability instead of saying
|
|
40
|
+
* why. What a static check downgrades is the *checker's reach*; what a run
|
|
41
|
+
* tolerates is much narrower. See `RUN_TOLERATED_CODES`.
|
|
42
|
+
*/
|
|
43
|
+
export declare const PREFLIGHT_WARNING_CODES: ReadonlySet<string>;
|
|
2
44
|
export declare function createRuntime(options: CreateRuntimeOptions): CascadeRuntime;
|
|
3
45
|
//# sourceMappingURL=runtime.d.ts.map
|
package/dist/runtime/runtime.js
CHANGED
|
@@ -1,6 +1,66 @@
|
|
|
1
|
-
import { canConnectTypes, validateNodeDefinition, } from "cascade/contracts";
|
|
1
|
+
import { canConnectTypes, validateNodeDefinition, } from "@field/cascade/contracts";
|
|
2
2
|
import { CascadeRuntimeError } from "./error.js";
|
|
3
3
|
import { builtinNodeRegistrations } from "./builtins/index.js";
|
|
4
|
+
import { deserializeChannel, isEmptyChannel } from "./animation/channel.js";
|
|
5
|
+
import { PropAnimator, isBoundProp } from "./params/index.js";
|
|
6
|
+
import { DEFAULT_FPS, MIN_FRAME } from "./expressions/time.js";
|
|
7
|
+
/**
|
|
8
|
+
* Preflight codes that describe something other than "this graph cannot run
|
|
9
|
+
* here", so `run` proceeds and a checker reports them as warnings.
|
|
10
|
+
*
|
|
11
|
+
* False positives are worse than blindness: a validator that cries wolf gets
|
|
12
|
+
* ignored, and then it is no better than the one that saw nothing. So the line
|
|
13
|
+
* is drawn at what the diagnostic is about.
|
|
14
|
+
*
|
|
15
|
+
* `preflight-environment` is about the checker's reach — a node saying
|
|
16
|
+
* `runsOn: 'browser'` claims nothing about the CLI and is not defective
|
|
17
|
+
* because somebody validated it from a terminal.
|
|
18
|
+
*
|
|
19
|
+
* `stray-params` is about the document — values were lost on the way in, which
|
|
20
|
+
* is worth saying loudly and is not a reason to refuse to run: a graph with a
|
|
21
|
+
* leftover `params` array runs perfectly well on its defaults, which is
|
|
22
|
+
* precisely the problem.
|
|
23
|
+
*
|
|
24
|
+
* It was briefly an error, and Marcus reverted that on 2026-09-08 with the
|
|
25
|
+
* evidence that decides it: while converting sixty-two nodes across four
|
|
26
|
+
* sketches, every one of them was a mixed graph for most of the day, and as an
|
|
27
|
+
* error an unconverted or half-converted sketch **stops validating at all** —
|
|
28
|
+
* which is the one thing you need while converting it. The warning is the
|
|
29
|
+
* conversion checklist; as an error it is a locked door on the room you are
|
|
30
|
+
* working in.
|
|
31
|
+
*
|
|
32
|
+
* The fair case for the error was that the warning never fired, and that was
|
|
33
|
+
* true: `check` bailed on dynamic modules before preflight ran. The fix for
|
|
34
|
+
* that was the reporting *order*, not the severity.
|
|
35
|
+
*
|
|
36
|
+
* This set lives here rather than in the CLI because both paths read it, and a
|
|
37
|
+
* duplicated copy is what let `validate` and `check` pass graphs `run` then
|
|
38
|
+
* rejected.
|
|
39
|
+
*
|
|
40
|
+
* **It classifies a static check, and it is not the run gate.** Those are
|
|
41
|
+
* different questions and conflating them was a real bug of mine, caught by
|
|
42
|
+
* Codex's own comment on 2026-09-08: *execution is stricter.* A server run must
|
|
43
|
+
* still refuse a browser node — you cannot cook WebGPU headlessly, so tolerating
|
|
44
|
+
* it means executing and failing on an undefined capability instead of saying
|
|
45
|
+
* why. What a static check downgrades is the *checker's reach*; what a run
|
|
46
|
+
* tolerates is much narrower. See `RUN_TOLERATED_CODES`.
|
|
47
|
+
*/
|
|
48
|
+
export const PREFLIGHT_WARNING_CODES = new Set([
|
|
49
|
+
"runtime/preflight-environment",
|
|
50
|
+
"runtime/stray-params",
|
|
51
|
+
]);
|
|
52
|
+
/**
|
|
53
|
+
* The only preflight code that does not stop a run.
|
|
54
|
+
*
|
|
55
|
+
* A stranded `params` value is a fact about the document: the graph cooks
|
|
56
|
+
* perfectly well on its defaults, which is exactly the problem, and refusing to
|
|
57
|
+
* run is the worse answer. Everything else in the preflight is a reason this
|
|
58
|
+
* host cannot execute this graph — an environment mismatch and a missing
|
|
59
|
+
* capability both mean the cook would fail partway with a worse message.
|
|
60
|
+
*/
|
|
61
|
+
const RUN_TOLERATED_CODES = new Set([
|
|
62
|
+
"runtime/stray-params",
|
|
63
|
+
]);
|
|
4
64
|
class MutableAbortSignal {
|
|
5
65
|
report;
|
|
6
66
|
aborted = false;
|
|
@@ -120,6 +180,10 @@ class Graph {
|
|
|
120
180
|
onDispose;
|
|
121
181
|
graphState = "ready";
|
|
122
182
|
byId;
|
|
183
|
+
dataConnectionsByInput;
|
|
184
|
+
dataConnectionsByNode;
|
|
185
|
+
connectedInputs;
|
|
186
|
+
connectedOutputs;
|
|
123
187
|
listeners = new Set();
|
|
124
188
|
diagnostics = [];
|
|
125
189
|
active;
|
|
@@ -129,19 +193,91 @@ class Graph {
|
|
|
129
193
|
authoredAnnotations;
|
|
130
194
|
graphInputs;
|
|
131
195
|
graphOutputs;
|
|
196
|
+
/**
|
|
197
|
+
* The graph's animated parameters, and the clock they resolve against.
|
|
198
|
+
*
|
|
199
|
+
* The clock is a value the caller sets per run (`run({ frame })`), never one
|
|
200
|
+
* this graph reads: same graph plus same frame gives the same output, which
|
|
201
|
+
* is the only reason a headless render can be trusted.
|
|
202
|
+
*/
|
|
203
|
+
animator;
|
|
204
|
+
frame = MIN_FRAME;
|
|
205
|
+
fps = DEFAULT_FPS;
|
|
132
206
|
constructor(host, nodes, connections, onDispose, annotations = []) {
|
|
133
207
|
this.host = host;
|
|
134
208
|
this.nodes = nodes;
|
|
135
209
|
this.connections = connections;
|
|
136
210
|
this.onDispose = onDispose;
|
|
137
211
|
this.byId = new Map(nodes.map((node) => [node.id, node]));
|
|
212
|
+
const byInput = new Map();
|
|
213
|
+
const byNode = new Map();
|
|
214
|
+
const connectedInputs = new Set();
|
|
215
|
+
const connectedOutputs = new Set();
|
|
216
|
+
for (const connection of connections) {
|
|
217
|
+
connectedInputs.add(`${connection.target.nodeId}\0${connection.target.inputName}`);
|
|
218
|
+
connectedOutputs.add(`${connection.source.nodeId}\0${connection.source.outputName}`);
|
|
219
|
+
if (connection.kind !== "data")
|
|
220
|
+
continue;
|
|
221
|
+
const inputKey = `${connection.target.nodeId}\0${connection.target.inputName}`;
|
|
222
|
+
const inputConnections = byInput.get(inputKey) ?? [];
|
|
223
|
+
inputConnections.push(connection);
|
|
224
|
+
byInput.set(inputKey, inputConnections);
|
|
225
|
+
const nodeConnections = byNode.get(connection.target.nodeId) ?? [];
|
|
226
|
+
nodeConnections.push(connection);
|
|
227
|
+
byNode.set(connection.target.nodeId, nodeConnections);
|
|
228
|
+
}
|
|
229
|
+
this.dataConnectionsByInput = new Map([...byInput].map(([key, inputConnections]) => [
|
|
230
|
+
key,
|
|
231
|
+
Object.freeze(inputConnections.sort((first, second) => (first.variadicIndex ?? first.order) -
|
|
232
|
+
(second.variadicIndex ?? second.order))),
|
|
233
|
+
]));
|
|
234
|
+
this.dataConnectionsByNode = new Map([...byNode].map(([key, nodeConnections]) => [
|
|
235
|
+
key,
|
|
236
|
+
Object.freeze(nodeConnections.sort((first, second) => first.order - second.order)),
|
|
237
|
+
]));
|
|
238
|
+
this.connectedInputs = connectedInputs;
|
|
239
|
+
this.connectedOutputs = connectedOutputs;
|
|
138
240
|
this.graphInputs = graphBoundaries(nodes, "cascade.core.Input", "inputName");
|
|
139
241
|
this.graphOutputs = graphBoundaries(nodes, "cascade.core.Output", "outputName");
|
|
140
242
|
this.authoredAnnotations = Object.freeze(annotations.map((annotation) => snapshot(annotation)));
|
|
243
|
+
this.animator = new PropAnimator({
|
|
244
|
+
nodes,
|
|
245
|
+
node: (id) => this.byId.get(id),
|
|
246
|
+
inputNode: (node, index) => {
|
|
247
|
+
const feeding = this.dataConnectionsByNode.get(node.id) ?? [];
|
|
248
|
+
const source = feeding[index]?.source.nodeId;
|
|
249
|
+
return (source ? this.byId.get(source) : undefined) ?? null;
|
|
250
|
+
},
|
|
251
|
+
// A failed expression has already fallen back to the stored value, so it
|
|
252
|
+
// is a diagnostic rather than a stopped run — but it is never silent: a
|
|
253
|
+
// parameter that quietly reverts is the hardest fault to see in a
|
|
254
|
+
// hundred rendered frames.
|
|
255
|
+
report: (nodeId, propName, error) => this.reportDiagnostic({
|
|
256
|
+
phase: "run",
|
|
257
|
+
code: "runtime/expression-failed",
|
|
258
|
+
message: `${nodeId}.${propName}: ${error}`,
|
|
259
|
+
path: `${nodeId}.${propName}`,
|
|
260
|
+
}),
|
|
261
|
+
});
|
|
262
|
+
this.animator.setFps(this.fps);
|
|
263
|
+
this.animator.setFrame(this.frame);
|
|
264
|
+
// Resolve once at load, so `inspect()` and a preset applied before any run
|
|
265
|
+
// both see the parameter at the graph's own first frame rather than the
|
|
266
|
+
// number that was last saved.
|
|
267
|
+
this.animator.resolveAll();
|
|
141
268
|
}
|
|
142
269
|
get state() {
|
|
143
270
|
return this.graphState;
|
|
144
271
|
}
|
|
272
|
+
/**
|
|
273
|
+
* Everything that would stop this host running the graph, without running
|
|
274
|
+
* it: a node whose environment this host is not, a bridged node that cannot
|
|
275
|
+
* be bridged, a declared capability this host does not install. Empty means
|
|
276
|
+
* the graph clears the same gate `run` puts it through.
|
|
277
|
+
*/
|
|
278
|
+
preflight() {
|
|
279
|
+
return this.preflightDiagnostics(this.nodes);
|
|
280
|
+
}
|
|
145
281
|
inspect() {
|
|
146
282
|
const nodes = this.nodes.map((node) => ({
|
|
147
283
|
id: node.id,
|
|
@@ -153,8 +289,7 @@ class Graph {
|
|
|
153
289
|
{
|
|
154
290
|
kind: definition.kind,
|
|
155
291
|
type: definition.kind === "trigger" ? "trigger" : definition.type,
|
|
156
|
-
connected: this.
|
|
157
|
-
connection.target.inputName === name),
|
|
292
|
+
connected: this.connectedInputs.has(`${node.id}\0${name}`),
|
|
158
293
|
...(definition.kind === "data"
|
|
159
294
|
? { value: snapshot(node.inputs[name]) }
|
|
160
295
|
: {}),
|
|
@@ -165,8 +300,7 @@ class Graph {
|
|
|
165
300
|
{
|
|
166
301
|
kind: definition.kind,
|
|
167
302
|
type: definition.kind === "trigger" ? "trigger" : definition.type,
|
|
168
|
-
connected: this.
|
|
169
|
-
connection.source.outputName === name),
|
|
303
|
+
connected: this.connectedOutputs.has(`${node.id}\0${name}`),
|
|
170
304
|
...(definition.kind === "data" && node.outputs.has(name)
|
|
171
305
|
? { value: snapshot(node.outputs.get(name)) }
|
|
172
306
|
: {}),
|
|
@@ -212,7 +346,26 @@ class Graph {
|
|
|
212
346
|
misuse("runtime/structural-prop", `${nodeId}.${propName} is structural and requires a graph reload`);
|
|
213
347
|
if (!node.definition.props?.[propName])
|
|
214
348
|
misuse("runtime/invalid-prop", `${nodeId}.${propName} is not a prop`);
|
|
215
|
-
|
|
349
|
+
const resolved = runtimeValue(value, node.definition.props[propName].type, false, `${nodeId}.${propName}`);
|
|
350
|
+
node.props[propName] = resolved;
|
|
351
|
+
this.rebind(node, propName, resolved);
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* Keep a bound prop's STORED value in step with a direct write.
|
|
355
|
+
*
|
|
356
|
+
* A channel or an expression still wins on the next resolve — setting a
|
|
357
|
+
* keyed parameter's value does not delete its keys, in Studio or here — but
|
|
358
|
+
* the value written is the one the parameter falls back to, so dropping it
|
|
359
|
+
* would make a later `clearChannel` reinstate a number nobody chose.
|
|
360
|
+
*/
|
|
361
|
+
rebind(node, propName, value) {
|
|
362
|
+
const binding = node.bindings[propName];
|
|
363
|
+
if (binding)
|
|
364
|
+
node.bindings[propName] = { ...binding, value };
|
|
365
|
+
}
|
|
366
|
+
reportDiagnostic(diagnostic) {
|
|
367
|
+
this.diagnostics.push(diagnostic);
|
|
368
|
+
this.emit({ type: "diagnostic", diagnostic });
|
|
216
369
|
}
|
|
217
370
|
async applyPreset(preset) {
|
|
218
371
|
this.requireReady();
|
|
@@ -247,10 +400,50 @@ class Graph {
|
|
|
247
400
|
}
|
|
248
401
|
for (const [node, name, value] of inputDrafts)
|
|
249
402
|
node.inputs[name] = value;
|
|
250
|
-
for (const [node, name, value] of propDrafts)
|
|
403
|
+
for (const [node, name, value] of propDrafts) {
|
|
251
404
|
node.props[name] = value;
|
|
405
|
+
this.rebind(node, name, value);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* The frame this graph is cooking at, and the rate frames are counted in.
|
|
410
|
+
*
|
|
411
|
+
* Separate from `run` so a caller can scrub without cooking — and so that
|
|
412
|
+
* `setFrame` then `inspect()` reports a keyed parameter at that frame, which
|
|
413
|
+
* is what a panel needs and what `run` alone cannot give it.
|
|
414
|
+
*/
|
|
415
|
+
setFrame(frame) {
|
|
416
|
+
if (!Number.isFinite(frame))
|
|
417
|
+
return;
|
|
418
|
+
this.frame = Math.max(MIN_FRAME, frame);
|
|
419
|
+
this.animator.setFrame(this.frame);
|
|
420
|
+
this.animator.resolveAll();
|
|
421
|
+
}
|
|
422
|
+
setFps(fps) {
|
|
423
|
+
if (!Number.isFinite(fps))
|
|
424
|
+
return;
|
|
425
|
+
this.fps = Math.max(1, fps);
|
|
426
|
+
this.animator.setFps(this.fps);
|
|
427
|
+
this.animator.resolveAll();
|
|
428
|
+
}
|
|
429
|
+
getFrame() {
|
|
430
|
+
return this.frame;
|
|
431
|
+
}
|
|
432
|
+
getFps() {
|
|
433
|
+
return this.fps;
|
|
252
434
|
}
|
|
253
435
|
run(options = {}) {
|
|
436
|
+
// The frame belongs to the run, not to the graph's history: a render loop
|
|
437
|
+
// states which frame it wants and gets exactly that, with no dependence on
|
|
438
|
+
// what the previous run left behind.
|
|
439
|
+
if (options.fps !== undefined)
|
|
440
|
+
this.setFps(options.fps);
|
|
441
|
+
if (options.frame !== undefined)
|
|
442
|
+
this.setFrame(options.frame);
|
|
443
|
+
// Every bound parameter, resolved before any node executes — so a node
|
|
444
|
+
// still receives a plain number and never reads a clock.
|
|
445
|
+
else
|
|
446
|
+
this.animator.resolveAll();
|
|
254
447
|
return this.trackRun(options.target ?? { kind: "all" }, options.signal, options.runId);
|
|
255
448
|
}
|
|
256
449
|
trigger(nodeId, inputName, payload, options = {}) {
|
|
@@ -339,7 +532,7 @@ class Graph {
|
|
|
339
532
|
}
|
|
340
533
|
: this.runSelection(target);
|
|
341
534
|
const { reachable, initial } = selection;
|
|
342
|
-
this.
|
|
535
|
+
this.assertPreflight(reachable);
|
|
343
536
|
if (!signal.aborted)
|
|
344
537
|
await Promise.all(reachable
|
|
345
538
|
.filter((node) => !this.isRemote(node))
|
|
@@ -415,10 +608,7 @@ class Graph {
|
|
|
415
608
|
const index = integerProp(node.props.inputIndex);
|
|
416
609
|
return [name, this.dataInputValue(parent, `input_${index}`)];
|
|
417
610
|
}
|
|
418
|
-
const connections = this.
|
|
419
|
-
item.target.nodeId === node.id &&
|
|
420
|
-
item.target.inputName === name).sort((first, second) => (first.variadicIndex ?? first.order) -
|
|
421
|
-
(second.variadicIndex ?? second.order));
|
|
611
|
+
const connections = this.dataConnectionsByInput.get(`${node.id}\0${name}`) ?? [];
|
|
422
612
|
if (definition.variadic) {
|
|
423
613
|
const values = connections.map((connection) => this.requireNode(connection.source.nodeId).outputs.get(connection.source.outputName));
|
|
424
614
|
if (!values.length && node.inputs[name] !== undefined)
|
|
@@ -504,6 +694,7 @@ class Graph {
|
|
|
504
694
|
report: (event) => this.emit({ type: "node:progress", runId, nodeId: node.id, ...event }),
|
|
505
695
|
};
|
|
506
696
|
await node.execute({
|
|
697
|
+
nodeId: node.id,
|
|
507
698
|
inputs,
|
|
508
699
|
outputs,
|
|
509
700
|
props,
|
|
@@ -544,33 +735,70 @@ class Graph {
|
|
|
544
735
|
}
|
|
545
736
|
this.emit({ type: "node:complete", runId, nodeId: node.id });
|
|
546
737
|
}
|
|
547
|
-
|
|
738
|
+
/**
|
|
739
|
+
* Every reason this host cannot run these nodes, collected rather than
|
|
740
|
+
* thrown. `assertPreflight` is the run path and raises the first of them;
|
|
741
|
+
* `preflight()` is the static path and hands the whole list to a caller that
|
|
742
|
+
* is not executing anything.
|
|
743
|
+
*
|
|
744
|
+
* There is deliberately one implementation. `validate` and `check` used to
|
|
745
|
+
* answer "will this graph run?" without ever consulting a host, so a node
|
|
746
|
+
* declaring a capability nobody installs passed both and then failed here —
|
|
747
|
+
* the two commands could not see the fault they exist to report. Any second
|
|
748
|
+
* copy of these rules would drift back into that.
|
|
749
|
+
*/
|
|
750
|
+
preflightDiagnostics(nodes) {
|
|
751
|
+
const diagnostics = [];
|
|
752
|
+
const push = (code, nodeId, message) => diagnostics.push({ phase: "preflight", code, path: nodeId, message });
|
|
548
753
|
for (const node of nodes) {
|
|
754
|
+
if (node.strayParams.length)
|
|
755
|
+
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`);
|
|
549
756
|
const remote = this.isRemote(node);
|
|
550
757
|
const locationWorks = node.definition.runsOn === "portable" ||
|
|
551
758
|
node.definition.runsOn === this.host.environment ||
|
|
552
759
|
remote;
|
|
553
|
-
if (!locationWorks)
|
|
554
|
-
|
|
760
|
+
if (!locationWorks) {
|
|
761
|
+
push("runtime/preflight-environment", node.id, `${node.moduleId} requires ${node.definition.runsOn}`);
|
|
762
|
+
continue;
|
|
763
|
+
}
|
|
555
764
|
if (remote) {
|
|
556
765
|
const hasTriggers = [
|
|
557
766
|
...Object.values(node.definition.inputs ?? {}),
|
|
558
767
|
...Object.values(node.definition.outputs ?? {}),
|
|
559
768
|
].some((port) => port.kind === "trigger");
|
|
560
769
|
if (hasTriggers)
|
|
561
|
-
|
|
770
|
+
push("runtime/preflight-bridge-trigger", node.id, `${node.moduleId} cannot send triggers over the server bridge`);
|
|
562
771
|
const hasTexture = [
|
|
563
772
|
...Object.values(node.definition.inputs ?? {}),
|
|
564
773
|
...Object.values(node.definition.outputs ?? {}),
|
|
565
774
|
].some((port) => port.kind === "data" && port.type === "texture");
|
|
566
775
|
if (hasTexture)
|
|
567
|
-
|
|
776
|
+
push("runtime/preflight-bridge-texture", node.id, `${node.moduleId} cannot send textures over the server bridge`);
|
|
568
777
|
}
|
|
569
778
|
else
|
|
570
779
|
for (const capability of node.definition.capabilities ?? [])
|
|
571
780
|
if (!this.host.capabilities[capability])
|
|
572
|
-
|
|
781
|
+
push("runtime/missing-capability", node.id, `Missing capability ${capability} for ${node.moduleId} (node ${node.id}) — the ${this.host.environment} host ${this.hostProvides()}`);
|
|
573
782
|
}
|
|
783
|
+
return diagnostics;
|
|
784
|
+
}
|
|
785
|
+
/**
|
|
786
|
+
* What the host actually installed, asked of the host rather than written
|
|
787
|
+
* down. `serverBridge` is left out: it is how a node runs somewhere else,
|
|
788
|
+
* not a capability a definition can declare.
|
|
789
|
+
*/
|
|
790
|
+
hostProvides() {
|
|
791
|
+
const supplied = Object.keys(this.host.capabilities)
|
|
792
|
+
.filter((name) => name !== "serverBridge" && !!this.host.capabilities[name])
|
|
793
|
+
.sort();
|
|
794
|
+
return supplied.length
|
|
795
|
+
? `provides ${supplied.join(", ")}`
|
|
796
|
+
: `provides no capabilities`;
|
|
797
|
+
}
|
|
798
|
+
assertPreflight(nodes) {
|
|
799
|
+
const [first] = this.preflightDiagnostics(nodes).filter((diagnostic) => !RUN_TOLERATED_CODES.has(diagnostic.code));
|
|
800
|
+
if (first)
|
|
801
|
+
throw new CascadeRuntimeError(first.code, [first]);
|
|
574
802
|
}
|
|
575
803
|
isRemote(node) {
|
|
576
804
|
return (this.host.environment === "browser" &&
|
|
@@ -582,7 +810,8 @@ class Graph {
|
|
|
582
810
|
}
|
|
583
811
|
runSelection(target) {
|
|
584
812
|
if (target.kind === "all") {
|
|
585
|
-
const
|
|
813
|
+
const gatedDataDescendants = this.gatedDataDescendants();
|
|
814
|
+
const initial = this.nodes.filter((node) => !isGated(node) && !gatedDataDescendants.has(node.id));
|
|
586
815
|
const ids = new Set(initial.map((node) => node.id));
|
|
587
816
|
let changed = true;
|
|
588
817
|
while (changed) {
|
|
@@ -627,8 +856,25 @@ class Graph {
|
|
|
627
856
|
}
|
|
628
857
|
return this.nodes.filter((node) => ids.has(node.id));
|
|
629
858
|
}
|
|
630
|
-
|
|
631
|
-
|
|
859
|
+
gatedDataDescendants() {
|
|
860
|
+
const outgoing = new Map();
|
|
861
|
+
for (const dependency of dataDependencies(this.nodes, this.connections)) {
|
|
862
|
+
const targets = outgoing.get(dependency.sourceId) ?? [];
|
|
863
|
+
targets.push(dependency.targetId);
|
|
864
|
+
outgoing.set(dependency.sourceId, targets);
|
|
865
|
+
}
|
|
866
|
+
const queue = this.nodes.filter(isGated).map((node) => node.id);
|
|
867
|
+
const descendants = new Set(queue);
|
|
868
|
+
let queueIndex = 0;
|
|
869
|
+
while (queueIndex < queue.length) {
|
|
870
|
+
for (const targetId of outgoing.get(queue[queueIndex++]) ?? []) {
|
|
871
|
+
if (descendants.has(targetId))
|
|
872
|
+
continue;
|
|
873
|
+
descendants.add(targetId);
|
|
874
|
+
queue.push(targetId);
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
return descendants;
|
|
632
878
|
}
|
|
633
879
|
triggerDataAncestors(nodeId) {
|
|
634
880
|
const dependencies = this.dataClosure(nodeId).filter((node) => node.id !== nodeId);
|
|
@@ -776,12 +1022,52 @@ function materialize(authored, registration) {
|
|
|
776
1022
|
}
|
|
777
1023
|
for (const [name, definition] of Object.entries(registration.definition.props ?? {}))
|
|
778
1024
|
props[name] = runtimeValue(definition.default, definition.type, false, `${authored.id}.${name}`);
|
|
779
|
-
|
|
1025
|
+
const bindings = {};
|
|
1026
|
+
// A declared default expression, applied only where the document stored
|
|
1027
|
+
// nothing at all for the prop. The loop below then runs over what *was*
|
|
1028
|
+
// stored, so an authored value or an authored expression overwrites this —
|
|
1029
|
+
// which is the whole contract: a default expression is a default, and a file
|
|
1030
|
+
// that saved a plain number keeps its number rather than quietly animating.
|
|
1031
|
+
const storedProps = authored.props ?? {};
|
|
1032
|
+
for (const [name, definition] of Object.entries(registration.definition.props ?? {})) {
|
|
1033
|
+
if (!definition.expression || name in storedProps)
|
|
1034
|
+
continue;
|
|
1035
|
+
bindings[name] = {
|
|
1036
|
+
value: props[name],
|
|
1037
|
+
expression: definition.expression,
|
|
1038
|
+
};
|
|
1039
|
+
}
|
|
1040
|
+
for (const [name, saved] of Object.entries(storedProps)) {
|
|
780
1041
|
const definition = registration.definition.props?.[name];
|
|
781
|
-
if (definition)
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
1042
|
+
if (!definition)
|
|
1043
|
+
continue;
|
|
1044
|
+
// `{ value, expression?, channel? }` — the object form declared in
|
|
1045
|
+
// contracts/animation.ts. A bare value is the same prop with nothing more
|
|
1046
|
+
// to say, which is what every older file on disk contains.
|
|
1047
|
+
const object = saved && typeof saved === "object" && "value" in saved
|
|
1048
|
+
? saved
|
|
1049
|
+
: null;
|
|
1050
|
+
const value = runtimeValue(object ? object.value : saved, definition.type, false, `${authored.id}.${name}`);
|
|
1051
|
+
props[name] = value;
|
|
1052
|
+
// A stored plain value is an author's decision and beats the declared
|
|
1053
|
+
// default expression, so any binding seeded above has to go.
|
|
1054
|
+
if (!object) {
|
|
1055
|
+
delete bindings[name];
|
|
1056
|
+
continue;
|
|
1057
|
+
}
|
|
1058
|
+
const channel = deserializeChannel(object.channel);
|
|
1059
|
+
const expression = typeof object.expression === "string" && object.expression
|
|
1060
|
+
? object.expression
|
|
1061
|
+
: undefined;
|
|
1062
|
+
const binding = {
|
|
1063
|
+
value,
|
|
1064
|
+
...(expression ? { expression } : {}),
|
|
1065
|
+
...(isEmptyChannel(channel) ? {} : { channel }),
|
|
1066
|
+
};
|
|
1067
|
+
// Only a prop that actually carries something, so `animated` stays a real
|
|
1068
|
+
// question and an unanimated graph pays nothing for this.
|
|
1069
|
+
if (isBoundProp(binding))
|
|
1070
|
+
bindings[name] = binding;
|
|
785
1071
|
}
|
|
786
1072
|
return {
|
|
787
1073
|
id: authored.id,
|
|
@@ -791,9 +1077,43 @@ function materialize(authored, registration) {
|
|
|
791
1077
|
definition: registration.definition,
|
|
792
1078
|
inputs,
|
|
793
1079
|
props,
|
|
1080
|
+
bindings,
|
|
1081
|
+
strayParams: strayParams(authored, registration),
|
|
794
1082
|
outputs: new Map(),
|
|
795
1083
|
};
|
|
796
1084
|
}
|
|
1085
|
+
/**
|
|
1086
|
+
* The names a document stored under `params` that this runtime will not read.
|
|
1087
|
+
*
|
|
1088
|
+
* Nothing above consults `params`: the loops that fill `inputs` and `props`
|
|
1089
|
+
* read the definition and then the document's own `inputs` and `props`, so a
|
|
1090
|
+
* value left in `params` is dropped without a word and the parameter keeps its
|
|
1091
|
+
* default. That is not a hypothetical — on `cascade-logo` it put a seed back to
|
|
1092
|
+
* 7 and two rasterizers back to 1024, so three nodes wrote one file and the
|
|
1093
|
+
* drawing changed, with every command still reporting success.
|
|
1094
|
+
*
|
|
1095
|
+
* **Inputs count, not just props.** The first version of this checked props
|
|
1096
|
+
* alone and so was blind to exactly the case the conversions produce most: a
|
|
1097
|
+
* parameter another node drives has to be declared as an `inputs` entry,
|
|
1098
|
+
* because a v1 prop cannot be promoted to a pin. `field-logo`'s `angle` is one,
|
|
1099
|
+
* and a negative control over the pre-migration document named five nodes and
|
|
1100
|
+
* missed that one.
|
|
1101
|
+
*
|
|
1102
|
+
* Only names the definition actually declares are reported. A leftover key
|
|
1103
|
+
* naming nothing is a different fault, and reporting it would make this fire on
|
|
1104
|
+
* documents where no value was lost.
|
|
1105
|
+
*/
|
|
1106
|
+
function strayParams(authored, registration) {
|
|
1107
|
+
const definition = registration.definition;
|
|
1108
|
+
const declared = new Set([
|
|
1109
|
+
...Object.keys(definition.props ?? {}),
|
|
1110
|
+
...Object.keys(definition.inputs ?? {}),
|
|
1111
|
+
]);
|
|
1112
|
+
const names = Array.isArray(authored.params)
|
|
1113
|
+
? authored.params.map((saved) => saved?.name)
|
|
1114
|
+
: Object.keys(authored.params ?? {});
|
|
1115
|
+
return names.filter((name) => typeof name === "string" && declared.has(name));
|
|
1116
|
+
}
|
|
797
1117
|
function specializeCoreRegistration(registration, authored, nodes) {
|
|
798
1118
|
if (registration.moduleId === "cascade.core.Input" ||
|
|
799
1119
|
registration.moduleId === "cascade.core.Output") {
|
|
@@ -1037,6 +1357,7 @@ function validateTopology(nodes, connections) {
|
|
|
1037
1357
|
topological(nodes, connections);
|
|
1038
1358
|
}
|
|
1039
1359
|
function topological(nodes, connections) {
|
|
1360
|
+
const byId = new Map(nodes.map((node) => [node.id, node]));
|
|
1040
1361
|
const indegree = new Map(nodes.map((node) => [node.id, 0]));
|
|
1041
1362
|
const outgoing = new Map(nodes.map((node) => [node.id, []]));
|
|
1042
1363
|
for (const dependency of graphDependencies(nodes, connections)) {
|
|
@@ -1045,13 +1366,14 @@ function topological(nodes, connections) {
|
|
|
1045
1366
|
}
|
|
1046
1367
|
const queue = nodes.filter((node) => indegree.get(node.id) === 0);
|
|
1047
1368
|
const result = [];
|
|
1048
|
-
|
|
1049
|
-
|
|
1369
|
+
let queueIndex = 0;
|
|
1370
|
+
while (queueIndex < queue.length) {
|
|
1371
|
+
const node = queue[queueIndex++];
|
|
1050
1372
|
result.push(node);
|
|
1051
1373
|
for (const id of outgoing.get(node.id)) {
|
|
1052
1374
|
indegree.set(id, indegree.get(id) - 1);
|
|
1053
1375
|
if (indegree.get(id) === 0)
|
|
1054
|
-
queue.push(
|
|
1376
|
+
queue.push(byId.get(id));
|
|
1055
1377
|
}
|
|
1056
1378
|
}
|
|
1057
1379
|
if (result.length !== nodes.length)
|