@field/cascade 0.3.1 → 0.5.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 +32 -9
- package/ARCHITECTURE.md +75 -13
- package/CHANGELOG.md +98 -1
- package/CONTRIBUTING.md +104 -0
- package/DESIGN.md +24 -15
- package/README.md +121 -14
- package/dist/assets/{CodeEditor-D22fVi-4.js → CodeEditor-dOrU3aQg.js} +7 -7
- package/dist/assets/{CodePanel-DPNEYfcR.js → CodePanel-Bqy85qen.js} +2 -2
- package/dist/assets/{CustomNodeDialog-DeHCAvgb.js → CustomNodeDialog-DkZlOgjs.js} +1 -1
- package/dist/assets/{ProjectSettings-DWpVtWEx.js → ProjectSettings-D192mr6g.js} +2 -2
- package/dist/assets/{SettingsDialog-za00IS3L.js → SettingsDialog-BTeeIE3T.js} +1 -1
- package/dist/assets/{VersionHistoryDialog-DTMGhI0_.js → VersionHistoryDialog-TZQKkn3Y.js} +1 -1
- package/dist/assets/{core-JsyKMO1k.js → core-DwbcGL0o.js} +2 -2
- package/dist/assets/{cssMode-emdRheFK.js → cssMode-Dcjn356x.js} +1 -1
- package/dist/assets/definitions-C_SY6Bhg.js +1 -0
- package/dist/assets/{editor.api-xHX-fMSo.js → editor.api-D7Q83606.js} +1 -1
- package/dist/assets/{freemarker2-C3M8_Gs3.js → freemarker2-DCCiHlV6.js} +1 -1
- package/dist/assets/geo-CT-wtvaE.js +1 -0
- package/dist/assets/{handlebars-CBMurtFk.js → handlebars-BCxC8xu1.js} +1 -1
- package/dist/assets/{html-xbLQwknw.js → html-COwepzSd.js} +1 -1
- package/dist/assets/{htmlMode-2wKdDTon.js → htmlMode-Dpg7H0rx.js} +1 -1
- package/dist/assets/{image-i1j6nWKe.js → image-CMfMMxlC.js} +3 -3
- package/dist/assets/index-De1cIZ9N.js +212 -0
- package/dist/assets/index-ilgbOu6G.css +1 -0
- package/dist/assets/{javascript-DJaOo_2I.js → javascript-CSwWxfLr.js} +1 -1
- package/dist/assets/{jsonMode-Bheas8_0.js → jsonMode-BXRriTpj.js} +1 -1
- package/dist/assets/{liquid-BCj_BGbM.js → liquid-Cmkfyogc.js} +1 -1
- package/dist/assets/{lspLanguageFeatures-BfIsx_xz.js → lspLanguageFeatures-xH9JbZGO.js} +1 -1
- package/dist/assets/{mdx-DwiQDiVO.js → mdx-BzrNjNvK.js} +1 -1
- package/dist/assets/nodeTypeUtils-BDJsc2Ks.js +1 -0
- package/dist/assets/pop-BdKEHslJ.js +1 -0
- package/dist/assets/{python-BjrB5nqj.js → python-D4E9jbx7.js} +1 -1
- package/dist/assets/{razor-CfoBmGDs.js → razor-B3x9cIEM.js} +1 -1
- package/dist/assets/{tsMode-BcEoMSxp.js → tsMode-dhQYPNMZ.js} +1 -1
- package/dist/assets/{typescript-DIg7TEmQ.js → typescript-CITkDxAE.js} +1 -1
- package/dist/assets/{workers-vXYuwtPw.js → workers-BQvF9bbO.js} +1 -1
- package/dist/assets/{xml-DexLQIQy.js → xml-DvHlU3A8.js} +1 -1
- package/dist/assets/{yaml-DUnD_1dE.js → yaml-uznsLgs8.js} +1 -1
- package/dist/cli/index.js +16465 -12096
- package/dist/cli/runtime/gpu.ts +71 -0
- package/dist/cli/runtime/io.ts +31 -25
- package/dist/contracts/camera.d.ts +70 -0
- package/dist/contracts/camera.js +27 -0
- package/dist/contracts/capabilities.d.ts +6 -6
- package/dist/contracts/definition.d.ts +62 -4
- package/dist/contracts/index.d.ts +4 -0
- package/dist/contracts/index.js +4 -0
- package/dist/contracts/particles.d.ts +54 -0
- package/dist/contracts/particles.js +167 -0
- package/dist/contracts/scene.d.ts +129 -0
- package/dist/contracts/scene.js +73 -0
- package/dist/contracts/schema.js +26 -3
- package/dist/contracts/series.d.ts +75 -0
- package/dist/contracts/series.js +2 -0
- package/dist/contracts/validate.js +23 -5
- package/dist/contracts/values.d.ts +4 -2
- package/dist/contracts/values.js +10 -0
- package/dist/gpu.d.ts +17 -0
- package/dist/gpu.js +61 -1
- package/dist/index.html +3 -3
- package/dist/io.d.ts +4 -16
- package/dist/io.js +26 -8
- package/dist/player/app.js +810 -0
- package/dist/player/embed.d.ts +4 -0
- package/dist/player/embed.js +77 -0
- package/dist/player/types.d.ts +35 -0
- 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 +4 -2
- package/dist/runtime/builtins/core/index.js +8 -2
- package/dist/runtime/builtins/core/structural.d.ts +168 -0
- package/dist/runtime/builtins/core/structural.js +175 -0
- package/dist/runtime/builtins/core/time.d.ts +165 -0
- package/dist/runtime/builtins/core/time.js +110 -0
- package/dist/runtime/builtins/geo/definitions.d.ts +247 -0
- package/dist/runtime/builtins/geo/definitions.js +70 -0
- package/dist/runtime/builtins/geo/index.d.ts +2 -1
- package/dist/runtime/builtins/geo/index.js +3 -1
- package/dist/runtime/builtins/geo/render.d.ts +87 -0
- package/dist/runtime/builtins/geo/render.js +87 -0
- package/dist/runtime/builtins/geo/svgExport.d.ts +11 -0
- package/dist/runtime/builtins/geo/svgExport.js +10 -0
- package/dist/runtime/builtins/index.d.ts +5 -2
- package/dist/runtime/builtins/index.js +13 -3
- package/dist/runtime/builtins/pop/definitions.d.ts +2120 -0
- package/dist/runtime/builtins/pop/definitions.js +815 -0
- package/dist/runtime/builtins/pop/index.d.ts +2 -0
- package/dist/runtime/builtins/pop/index.js +2 -0
- package/dist/runtime/builtins/scene/definitions.d.ts +356 -0
- package/dist/runtime/builtins/scene/definitions.js +224 -0
- package/dist/runtime/builtins/scene/index.d.ts +2 -0
- package/dist/runtime/builtins/scene/index.js +2 -0
- 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/geometry/render.d.ts +109 -0
- package/dist/runtime/geometry/render.js +145 -0
- package/dist/runtime/geometry/svg.d.ts +29 -0
- package/dist/runtime/geometry/svg.js +11 -5
- package/dist/runtime/gpu.d.ts +33 -0
- package/dist/runtime/gpu.js +218 -0
- package/dist/runtime/index.d.ts +4 -0
- package/dist/runtime/index.js +4 -0
- package/dist/runtime/pop/cache.d.ts +27 -0
- package/dist/runtime/pop/cache.js +123 -0
- package/dist/runtime/pop/forces.d.ts +202 -0
- package/dist/runtime/pop/forces.js +519 -0
- package/dist/runtime/pop/index.d.ts +5 -0
- package/dist/runtime/pop/index.js +5 -0
- package/dist/runtime/pop/state.d.ts +136 -0
- package/dist/runtime/pop/state.js +283 -0
- package/dist/runtime/pop/trail.d.ts +62 -0
- package/dist/runtime/pop/trail.js +142 -0
- package/dist/runtime/runtime.js +284 -9
- package/dist/runtime/scene/index.d.ts +50 -0
- package/dist/runtime/scene/index.js +94 -0
- package/dist/runtime/series/index.d.ts +41 -0
- package/dist/runtime/series/index.js +167 -0
- package/dist/runtime/types.d.ts +1 -1
- package/dist/studio/panel.d.ts +68 -1
- package/doc/HEADLESS_GPU.md +160 -0
- package/doc/NODE_REFERENCE.md +408 -1
- package/doc/PROJECT_AUTHORING.md +118 -20
- package/doc/WEB_PLAYER.md +131 -0
- package/package.json +25 -3
- package/dist/assets/geo-BAR6lQp5.js +0 -1
- package/dist/assets/index-BDV5xGBZ.css +0 -1
- package/dist/assets/index-CMcH8AgP.js +0 -212
- package/dist/assets/nodeTypeUtils-BBA1JqHx.js +0 -1
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.
|
|
@@ -48,6 +49,7 @@ import { DEFAULT_FPS, MIN_FRAME } from "./expressions/time.js";
|
|
|
48
49
|
export const PREFLIGHT_WARNING_CODES = new Set([
|
|
49
50
|
"runtime/preflight-environment",
|
|
50
51
|
"runtime/stray-params",
|
|
52
|
+
"runtime/unknown-props",
|
|
51
53
|
]);
|
|
52
54
|
/**
|
|
53
55
|
* The only preflight code that does not stop a run.
|
|
@@ -60,6 +62,7 @@ export const PREFLIGHT_WARNING_CODES = new Set([
|
|
|
60
62
|
*/
|
|
61
63
|
const RUN_TOLERATED_CODES = new Set([
|
|
62
64
|
"runtime/stray-params",
|
|
65
|
+
"runtime/unknown-props",
|
|
63
66
|
]);
|
|
64
67
|
class MutableAbortSignal {
|
|
65
68
|
report;
|
|
@@ -553,7 +556,15 @@ class Graph {
|
|
|
553
556
|
queue.push({ nodeId, inputName, event });
|
|
554
557
|
this.emit({ type: "trigger:queued", runId, nodeId, inputName, event });
|
|
555
558
|
};
|
|
556
|
-
|
|
559
|
+
/**
|
|
560
|
+
* Feedback descendants are removed from the main order: their container
|
|
561
|
+
* runs them, once per step. Leaving them here would cook each of them a
|
|
562
|
+
* second time, out of the loop and with whatever state the last step
|
|
563
|
+
* happened to leave behind — which reads as a simulation that is one step
|
|
564
|
+
* wrong rather than as a scheduling bug.
|
|
565
|
+
*/
|
|
566
|
+
const insideFeedback = this.feedbackDescendants();
|
|
567
|
+
const ordered = topological(this.nodes, this.connections).filter((node) => initial.includes(node) && !insideFeedback.has(node.id));
|
|
557
568
|
for (const node of ordered) {
|
|
558
569
|
if (signal.aborted)
|
|
559
570
|
break;
|
|
@@ -598,6 +609,24 @@ class Graph {
|
|
|
598
609
|
}
|
|
599
610
|
async executeNode(node, runId, signal, triggers, enqueue) {
|
|
600
611
|
this.emit({ type: "node:start", runId, nodeId: node.id });
|
|
612
|
+
/**
|
|
613
|
+
* `cascade.core.Previous` reads the carried state off its container, the
|
|
614
|
+
* same way `cascade.core.Input` reads the parent's wired input: a boundary's
|
|
615
|
+
* value comes from the parent's evaluation, not from a wire of its own.
|
|
616
|
+
*
|
|
617
|
+
* Outside a container it falls through to its own `initial`, which is what
|
|
618
|
+
* makes a node dragged out of a loop explicable rather than an error.
|
|
619
|
+
*/
|
|
620
|
+
if (node.moduleId === "cascade.core.Previous") {
|
|
621
|
+
const container = this.enclosingFeedback(node);
|
|
622
|
+
const state = container ? this.feedbackState.get(container) : undefined;
|
|
623
|
+
if (state) {
|
|
624
|
+
node.outputs.set("value", state.value);
|
|
625
|
+
node.outputs.set("step", state.step);
|
|
626
|
+
this.emit({ type: "node:complete", runId, nodeId: node.id });
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
629
|
+
}
|
|
601
630
|
const inputs = Object.fromEntries(Object.entries(node.definition.inputs ?? {}).map(([name, definition]) => {
|
|
602
631
|
if (definition.kind === "trigger")
|
|
603
632
|
return [name, triggers[name]];
|
|
@@ -683,12 +712,23 @@ class Graph {
|
|
|
683
712
|
set: (value) => pendingData.set(name, runtimeValue(value, definition.type, false, `${node.id}.${name}`)),
|
|
684
713
|
},
|
|
685
714
|
]));
|
|
686
|
-
const
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
715
|
+
const capabilityEntries = [];
|
|
716
|
+
for (const name of node.definition.capabilities ?? []) {
|
|
717
|
+
const installed = this.host.capabilities[name];
|
|
718
|
+
if (name === "gpu" && isGpuHost(installed)) {
|
|
719
|
+
await installed.ensure();
|
|
720
|
+
capabilityEntries.push([name, installed.forNode(node.id)]);
|
|
721
|
+
}
|
|
722
|
+
else {
|
|
723
|
+
capabilityEntries.push([
|
|
724
|
+
name,
|
|
725
|
+
name === "shell"
|
|
726
|
+
? bindShellToRun(this.host.capabilities.shell, signal)
|
|
727
|
+
: installed,
|
|
728
|
+
]);
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
const capabilities = Object.freeze(Object.fromEntries(capabilityEntries));
|
|
692
732
|
const props = Object.freeze({ ...node.props });
|
|
693
733
|
const progress = {
|
|
694
734
|
report: (event) => this.emit({ type: "node:progress", runId, nodeId: node.id, ...event }),
|
|
@@ -704,6 +744,15 @@ class Graph {
|
|
|
704
744
|
});
|
|
705
745
|
if (signal.aborted)
|
|
706
746
|
return;
|
|
747
|
+
if (node.definition.container === "feedback") {
|
|
748
|
+
// Resolved here rather than in the node's own execute, because the loop
|
|
749
|
+
// needs the graph and a definition-v1 node is given only its own id.
|
|
750
|
+
const feedback = await this.runFeedback(node, runId, signal, enqueue);
|
|
751
|
+
if (signal.aborted)
|
|
752
|
+
return;
|
|
753
|
+
pendingData.set("result", feedback.result);
|
|
754
|
+
pendingData.set("history", feedback.history);
|
|
755
|
+
}
|
|
707
756
|
if (node.moduleId === "cascade.core.Subnet") {
|
|
708
757
|
for (const child of this.nodes) {
|
|
709
758
|
if (child.parent !== node.id ||
|
|
@@ -751,6 +800,8 @@ class Graph {
|
|
|
751
800
|
const diagnostics = [];
|
|
752
801
|
const push = (code, nodeId, message) => diagnostics.push({ phase: "preflight", code, path: nodeId, message });
|
|
753
802
|
for (const node of nodes) {
|
|
803
|
+
if (node.unknownProps.length)
|
|
804
|
+
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`);
|
|
754
805
|
if (node.strayParams.length)
|
|
755
806
|
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`);
|
|
756
807
|
const remote = this.isRemote(node);
|
|
@@ -942,6 +993,128 @@ class Graph {
|
|
|
942
993
|
isDisposed() {
|
|
943
994
|
return this.graphState === "disposed";
|
|
944
995
|
}
|
|
996
|
+
/**
|
|
997
|
+
* The state a feedback container is carrying, while it is running.
|
|
998
|
+
*
|
|
999
|
+
* Runtime state on the instance rather than on the node, because it is alive
|
|
1000
|
+
* only during one container's loop and a node's `outputs` map is the graph's
|
|
1001
|
+
* result — putting a half-finished step in there would make a partially
|
|
1002
|
+
* simulated value readable by anything that inspects outputs mid-run.
|
|
1003
|
+
*
|
|
1004
|
+
* Keyed by container id so nested feedback works: an outer loop's carried
|
|
1005
|
+
* state survives an inner loop running to completion inside one of its steps.
|
|
1006
|
+
*/
|
|
1007
|
+
feedbackState = new Map();
|
|
1008
|
+
/**
|
|
1009
|
+
* Every node under a feedback container, transitively.
|
|
1010
|
+
*
|
|
1011
|
+
* These are excluded from the main cook order — the container runs them, once
|
|
1012
|
+
* per step. Computed once per run rather than per step: the hierarchy cannot
|
|
1013
|
+
* change mid-run, and this is walked for every node in the order.
|
|
1014
|
+
*/
|
|
1015
|
+
feedbackDescendants() {
|
|
1016
|
+
const containers = new Set(this.nodes
|
|
1017
|
+
.filter((node) => node.definition.container === "feedback")
|
|
1018
|
+
.map((node) => node.id));
|
|
1019
|
+
if (!containers.size)
|
|
1020
|
+
return new Set();
|
|
1021
|
+
const inside = new Set();
|
|
1022
|
+
for (const node of this.nodes) {
|
|
1023
|
+
let parent = node.parent;
|
|
1024
|
+
const seen = new Set();
|
|
1025
|
+
while (parent && !seen.has(parent)) {
|
|
1026
|
+
seen.add(parent);
|
|
1027
|
+
if (containers.has(parent)) {
|
|
1028
|
+
inside.add(node.id);
|
|
1029
|
+
break;
|
|
1030
|
+
}
|
|
1031
|
+
parent = this.byId.get(parent)?.parent;
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
return inside;
|
|
1035
|
+
}
|
|
1036
|
+
/**
|
|
1037
|
+
* Run a feedback container's contents `steps` times and return the last
|
|
1038
|
+
* result.
|
|
1039
|
+
*
|
|
1040
|
+
* The order the children run in is the graph's own topological order
|
|
1041
|
+
* restricted to this container's descendants, which is what makes the forces
|
|
1042
|
+
* inside behave like nodes rather than like a list: wiring one into another
|
|
1043
|
+
* changes the order they apply in.
|
|
1044
|
+
*/
|
|
1045
|
+
async runFeedback(node, runId, signal, enqueue) {
|
|
1046
|
+
const initial = this.dataInputValue(node, "initial");
|
|
1047
|
+
const requested = this.dataInputValue(node, "steps");
|
|
1048
|
+
const steps = Math.max(0, Math.trunc(Number(requested ?? 0)));
|
|
1049
|
+
if (!Number.isFinite(steps))
|
|
1050
|
+
throw runtimeError("runtime/feedback-steps", `${node.id}.steps is not a finite number`);
|
|
1051
|
+
const children = topological(this.nodes, this.connections).filter((child) => this.enclosingFeedback(child) === node.id);
|
|
1052
|
+
// The step's result comes from the Output boundary at index 0, the same
|
|
1053
|
+
// convention a subnet uses. Resolved before the loop so that a container
|
|
1054
|
+
// with no Output fails immediately rather than after simulating everything
|
|
1055
|
+
// and finding nothing to return.
|
|
1056
|
+
const sink = children.find((child) => child.parent === node.id &&
|
|
1057
|
+
child.moduleId === "cascade.core.Output" &&
|
|
1058
|
+
integerProp(child.props.outputIndex) === 0);
|
|
1059
|
+
if (steps > 0 && !sink)
|
|
1060
|
+
throw runtimeError("runtime/feedback-no-output", `${node.id} has no cascade.core.Output inside it, so a step has no result to carry. Add one and wire the step's result into it.`);
|
|
1061
|
+
/**
|
|
1062
|
+
* How many step results to keep for the `history` output.
|
|
1063
|
+
*
|
|
1064
|
+
* Zero by default and read once, before the loop: a simulation that
|
|
1065
|
+
* quietly held every frame of its own geometry would be the difference
|
|
1066
|
+
* between a preview and a tab that runs out of memory, so keeping history
|
|
1067
|
+
* is something a graph asks for and bounds.
|
|
1068
|
+
*/
|
|
1069
|
+
const keep = Math.max(0, integerProp(node.props.history));
|
|
1070
|
+
const history = [];
|
|
1071
|
+
// Zero steps is the initial state, not a failure: frame zero of a
|
|
1072
|
+
// simulation is where it starts, and a graph that throws at the beginning
|
|
1073
|
+
// of the timeline is unusable.
|
|
1074
|
+
let carried = initial;
|
|
1075
|
+
try {
|
|
1076
|
+
for (let step = 0; step < steps; step += 1) {
|
|
1077
|
+
if (signal.aborted)
|
|
1078
|
+
break;
|
|
1079
|
+
this.feedbackState.set(node.id, { value: carried, step });
|
|
1080
|
+
for (const child of children) {
|
|
1081
|
+
if (signal.aborted)
|
|
1082
|
+
break;
|
|
1083
|
+
await this.executeNode(child, runId, signal, {}, enqueue);
|
|
1084
|
+
}
|
|
1085
|
+
if (signal.aborted)
|
|
1086
|
+
break;
|
|
1087
|
+
carried = sink.outputs.get("output");
|
|
1088
|
+
if (keep > 0) {
|
|
1089
|
+
history.push(carried);
|
|
1090
|
+
// Trimmed inside the loop rather than after it, so the peak memory is
|
|
1091
|
+
// the cap and not the step count.
|
|
1092
|
+
if (history.length > keep)
|
|
1093
|
+
history.shift();
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
return Object.freeze({
|
|
1097
|
+
result: carried,
|
|
1098
|
+
history: Object.freeze(history),
|
|
1099
|
+
});
|
|
1100
|
+
}
|
|
1101
|
+
finally {
|
|
1102
|
+
this.feedbackState.delete(node.id);
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
/** The nearest ancestor that is a feedback container, or undefined. */
|
|
1106
|
+
enclosingFeedback(node) {
|
|
1107
|
+
let parent = node.parent;
|
|
1108
|
+
const seen = new Set();
|
|
1109
|
+
while (parent && !seen.has(parent)) {
|
|
1110
|
+
seen.add(parent);
|
|
1111
|
+
const candidate = this.byId.get(parent);
|
|
1112
|
+
if (candidate?.definition.container === "feedback")
|
|
1113
|
+
return candidate.id;
|
|
1114
|
+
parent = candidate?.parent;
|
|
1115
|
+
}
|
|
1116
|
+
return undefined;
|
|
1117
|
+
}
|
|
945
1118
|
requireNode(id) {
|
|
946
1119
|
const node = this.byId.get(id);
|
|
947
1120
|
if (!node)
|
|
@@ -1022,6 +1195,25 @@ function materialize(authored, registration) {
|
|
|
1022
1195
|
}
|
|
1023
1196
|
for (const [name, definition] of Object.entries(registration.definition.props ?? {}))
|
|
1024
1197
|
props[name] = runtimeValue(definition.default, definition.type, false, `${authored.id}.${name}`);
|
|
1198
|
+
/**
|
|
1199
|
+
* `props` is a map and `params` was a list, and a migration that changes the
|
|
1200
|
+
* key without changing the shape leaves an array here.
|
|
1201
|
+
*
|
|
1202
|
+
* Fatal, and the reason is what it costs: `Object.entries` of an array gives
|
|
1203
|
+
* keys `"0"`, `"1"`, which match no declared prop, so **every authored value
|
|
1204
|
+
* is dropped and every signal stays green.** Measured on 2026-09-08 —
|
|
1205
|
+
* preflight empty, the prop back at its default, `cascade run` reporting
|
|
1206
|
+
* `completed`. `cloud-plots` rendered a whole picture against no moment and
|
|
1207
|
+
* was reported as working on the strength of an exit code.
|
|
1208
|
+
*
|
|
1209
|
+
* An error rather than a warning, unlike a leftover under `params`: that is a
|
|
1210
|
+
* real state an unconverted sketch is legitimately in and must keep
|
|
1211
|
+
* validating, whereas `props` as an array is never valid — the schema
|
|
1212
|
+
* declares a record — and nothing passes through it on the way to being
|
|
1213
|
+
* right. Same reasoning as `definition/re-exported`.
|
|
1214
|
+
*/
|
|
1215
|
+
if (Array.isArray(authored.props))
|
|
1216
|
+
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`);
|
|
1025
1217
|
const bindings = {};
|
|
1026
1218
|
// A declared default expression, applied only where the document stored
|
|
1027
1219
|
// nothing at all for the prop. The loop below then runs over what *was*
|
|
@@ -1079,6 +1271,7 @@ function materialize(authored, registration) {
|
|
|
1079
1271
|
props,
|
|
1080
1272
|
bindings,
|
|
1081
1273
|
strayParams: strayParams(authored, registration),
|
|
1274
|
+
unknownProps: unknownProps(authored, registration),
|
|
1082
1275
|
outputs: new Map(),
|
|
1083
1276
|
};
|
|
1084
1277
|
}
|
|
@@ -1103,6 +1296,27 @@ function materialize(authored, registration) {
|
|
|
1103
1296
|
* naming nothing is a different fault, and reporting it would make this fire on
|
|
1104
1297
|
* documents where no value was lost.
|
|
1105
1298
|
*/
|
|
1299
|
+
/**
|
|
1300
|
+
* Names stored under `props` that the definition does not declare.
|
|
1301
|
+
*
|
|
1302
|
+
* The mirror of `strayParams`, and the gap that one had: it asks *is anything
|
|
1303
|
+
* still under the old key*, which is blind to *did what arrived under the new
|
|
1304
|
+
* key arrive in the right form*. One is a leftover, the other is a botched
|
|
1305
|
+
* move, and the second is worse because it looks finished.
|
|
1306
|
+
*
|
|
1307
|
+
* A warning rather than an error, because unlike an array container this is a
|
|
1308
|
+
* state a real document reaches honestly: a prop renamed or removed from a
|
|
1309
|
+
* definition leaves its value behind, and the graph runs on the defaults. Same
|
|
1310
|
+
* class as a leftover under `params`, and the same severity for the same
|
|
1311
|
+
* reason — a sketch mid-conversion must keep validating.
|
|
1312
|
+
*/
|
|
1313
|
+
function unknownProps(authored, registration) {
|
|
1314
|
+
const props = authored.props;
|
|
1315
|
+
if (!props || Array.isArray(props))
|
|
1316
|
+
return [];
|
|
1317
|
+
const declared = registration.definition.props ?? {};
|
|
1318
|
+
return Object.keys(props).filter((name) => !(name in declared));
|
|
1319
|
+
}
|
|
1106
1320
|
function strayParams(authored, registration) {
|
|
1107
1321
|
const definition = registration.definition;
|
|
1108
1322
|
const declared = new Set([
|
|
@@ -1294,8 +1508,9 @@ function validateHierarchy(nodes, byId) {
|
|
|
1294
1508
|
const parent = byId.get(node.parent);
|
|
1295
1509
|
if (!parent)
|
|
1296
1510
|
misuse("runtime/parent-not-found", `Node ${node.id} references missing parent ${node.parent}`);
|
|
1297
|
-
if (parent.definition.container !== "subnet"
|
|
1298
|
-
|
|
1511
|
+
if (parent.definition.container !== "subnet" &&
|
|
1512
|
+
parent.definition.container !== "feedback")
|
|
1513
|
+
misuse("runtime/parent-not-container", `Node ${node.id} parent ${node.parent} is not a container`);
|
|
1299
1514
|
}
|
|
1300
1515
|
for (const node of nodes) {
|
|
1301
1516
|
const visited = new Set([node.id]);
|
|
@@ -1425,6 +1640,66 @@ function dataDependencies(nodes, connections) {
|
|
|
1425
1640
|
dependencies.set(`${dependency.sourceId}\0${dependency.targetId}`, dependency);
|
|
1426
1641
|
}
|
|
1427
1642
|
}
|
|
1643
|
+
/**
|
|
1644
|
+
* Project data dependencies across every Feedback boundary they cross.
|
|
1645
|
+
*
|
|
1646
|
+
* Feedback descendants are removed from the main execution order because
|
|
1647
|
+
* their container runs them. Therefore an outside source wired to a child
|
|
1648
|
+
* must delay every enclosing container, and a child wired to an outside
|
|
1649
|
+
* consumer must make that consumer wait for the container. Nested or sibling
|
|
1650
|
+
* loops need the same projection between their scheduled containers.
|
|
1651
|
+
*
|
|
1652
|
+
* Shared ancestors are excluded: a wire whose endpoints are already inside
|
|
1653
|
+
* the same loop orders the children only. Projecting it onto that shared
|
|
1654
|
+
* container would create a false container-to-itself cycle.
|
|
1655
|
+
*/
|
|
1656
|
+
const parents = new Map(nodes.map((node) => [node.id, node.parent]));
|
|
1657
|
+
const feedbackContainers = new Set(nodes
|
|
1658
|
+
.filter((node) => node.definition.container === "feedback")
|
|
1659
|
+
.map((node) => node.id));
|
|
1660
|
+
if (feedbackContainers.size) {
|
|
1661
|
+
const feedbackLineage = (id) => {
|
|
1662
|
+
const lineage = [];
|
|
1663
|
+
const seen = new Set();
|
|
1664
|
+
let current = id;
|
|
1665
|
+
while (current && !seen.has(current)) {
|
|
1666
|
+
seen.add(current);
|
|
1667
|
+
if (feedbackContainers.has(current))
|
|
1668
|
+
lineage.push(current);
|
|
1669
|
+
current = parents.get(current);
|
|
1670
|
+
}
|
|
1671
|
+
return lineage;
|
|
1672
|
+
};
|
|
1673
|
+
const add = (sourceId, targetId) => {
|
|
1674
|
+
if (sourceId === targetId)
|
|
1675
|
+
return;
|
|
1676
|
+
const dependency = { sourceId, targetId };
|
|
1677
|
+
dependencies.set(`${sourceId}\0${targetId}`, dependency);
|
|
1678
|
+
};
|
|
1679
|
+
for (const connection of connections) {
|
|
1680
|
+
if (connection.kind !== "data")
|
|
1681
|
+
continue;
|
|
1682
|
+
const sourceLineage = feedbackLineage(connection.source.nodeId);
|
|
1683
|
+
const targetLineage = feedbackLineage(connection.target.nodeId);
|
|
1684
|
+
const sourceSet = new Set(sourceLineage);
|
|
1685
|
+
const targetSet = new Set(targetLineage);
|
|
1686
|
+
const crossedFrom = sourceLineage.filter((id) => !targetSet.has(id));
|
|
1687
|
+
const crossedInto = targetLineage.filter((id) => !sourceSet.has(id));
|
|
1688
|
+
if (crossedFrom.length > 0 && crossedInto.length > 0) {
|
|
1689
|
+
for (const sourceId of crossedFrom)
|
|
1690
|
+
for (const targetId of crossedInto)
|
|
1691
|
+
add(sourceId, targetId);
|
|
1692
|
+
}
|
|
1693
|
+
else if (crossedFrom.length > 0) {
|
|
1694
|
+
for (const sourceId of crossedFrom)
|
|
1695
|
+
add(sourceId, connection.target.nodeId);
|
|
1696
|
+
}
|
|
1697
|
+
else {
|
|
1698
|
+
for (const targetId of crossedInto)
|
|
1699
|
+
add(connection.source.nodeId, targetId);
|
|
1700
|
+
}
|
|
1701
|
+
}
|
|
1702
|
+
}
|
|
1428
1703
|
return [...dependencies.values()];
|
|
1429
1704
|
}
|
|
1430
1705
|
function runtimeError(code, message) {
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { Scene } from "@field/cascade/contracts";
|
|
2
|
+
import { type Bounds } from "../geometry/primitives.js";
|
|
3
|
+
export type { Bounds };
|
|
4
|
+
/**
|
|
5
|
+
* Axis-aligned bounds over every geometry in the scene, or `undefined` when
|
|
6
|
+
* there is nothing in it.
|
|
7
|
+
*
|
|
8
|
+
* Always three components, even when every geometry inside is 2D — a 2D
|
|
9
|
+
* geometry's `P` has a `size` of 2 and `geometryBounds` returns two components
|
|
10
|
+
* for it, and a viewport doing arithmetic on `min[2]` needs it to be `0` rather
|
|
11
|
+
* than `undefined`. Padding here rather than at each caller is the whole reason
|
|
12
|
+
* this function exists instead of callers mapping `geometryBounds`.
|
|
13
|
+
*/
|
|
14
|
+
export declare function sceneBounds(scene: Scene): Bounds | undefined;
|
|
15
|
+
export interface SceneDimensionality {
|
|
16
|
+
readonly is2d: boolean;
|
|
17
|
+
/** Why. Shown in the viewport's mode control, so it is never a mystery. */
|
|
18
|
+
readonly reason: "camera" | "normals" | "depth" | "flat" | "empty";
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Is this scene flat enough to look at as a drawing?
|
|
22
|
+
*
|
|
23
|
+
* Marcus, 2026-09-09: *"i want the viewer to detect if our geometry is 2D or 3D
|
|
24
|
+
* and adapt the viewport accordingly … unless our geometry / camera wants a 3D
|
|
25
|
+
* view."*
|
|
26
|
+
*
|
|
27
|
+
* Three conditions, and every one of them is a **fact about the cooked output**
|
|
28
|
+
* rather than a guess about intent, which is the property that makes this
|
|
29
|
+
* safe to act on:
|
|
30
|
+
*
|
|
31
|
+
* 1. A wired camera forces 3D whatever the bounds say. That is the *"unless the
|
|
32
|
+
* camera wants a 3D view"* clause, and it is also the escape hatch for a
|
|
33
|
+
* deliberately flat 3D scene — somebody who wires a camera has said which
|
|
34
|
+
* way they are looking at it.
|
|
35
|
+
* 2. A point `N` attribute forces 3D. Normals only mean something on a surface.
|
|
36
|
+
* 3. Otherwise, flat iff the Z extent is below a relative epsilon.
|
|
37
|
+
*
|
|
38
|
+
* ## This may only ever set the *initial* mode
|
|
39
|
+
*
|
|
40
|
+
* A particle simulation can be planar at frame 1 and gain depth by frame 20.
|
|
41
|
+
* Re-running this on every cook would flip the viewport mid-playback, which is
|
|
42
|
+
* worse than either mode would have been. So the Viewer calls this **once**,
|
|
43
|
+
* when a node has no remembered view, and never again — which is why the
|
|
44
|
+
* per-node view memory is not a separate feature but the thing that makes
|
|
45
|
+
* detect-once implementable at all. Without somewhere to keep the answer the
|
|
46
|
+
* only options are re-detecting, which flips, and never detecting, which is
|
|
47
|
+
* useless.
|
|
48
|
+
*/
|
|
49
|
+
export declare function sceneDimensionality(scene: Scene): SceneDimensionality;
|
|
50
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { geometryBounds } from "../geometry/primitives.js";
|
|
2
|
+
/**
|
|
3
|
+
* Axis-aligned bounds over every geometry in the scene, or `undefined` when
|
|
4
|
+
* there is nothing in it.
|
|
5
|
+
*
|
|
6
|
+
* Always three components, even when every geometry inside is 2D — a 2D
|
|
7
|
+
* geometry's `P` has a `size` of 2 and `geometryBounds` returns two components
|
|
8
|
+
* for it, and a viewport doing arithmetic on `min[2]` needs it to be `0` rather
|
|
9
|
+
* than `undefined`. Padding here rather than at each caller is the whole reason
|
|
10
|
+
* this function exists instead of callers mapping `geometryBounds`.
|
|
11
|
+
*/
|
|
12
|
+
export function sceneBounds(scene) {
|
|
13
|
+
const min = [
|
|
14
|
+
Number.POSITIVE_INFINITY,
|
|
15
|
+
Number.POSITIVE_INFINITY,
|
|
16
|
+
Number.POSITIVE_INFINITY,
|
|
17
|
+
];
|
|
18
|
+
const max = [
|
|
19
|
+
Number.NEGATIVE_INFINITY,
|
|
20
|
+
Number.NEGATIVE_INFINITY,
|
|
21
|
+
Number.NEGATIVE_INFINITY,
|
|
22
|
+
];
|
|
23
|
+
let found = false;
|
|
24
|
+
for (const geometry of scene.geometry) {
|
|
25
|
+
const bounds = geometryBounds(geometry);
|
|
26
|
+
if (bounds === undefined)
|
|
27
|
+
continue;
|
|
28
|
+
found = true;
|
|
29
|
+
for (let axis = 0; axis < 3; axis += 1) {
|
|
30
|
+
const low = bounds.min[axis] ?? 0;
|
|
31
|
+
const high = bounds.max[axis] ?? 0;
|
|
32
|
+
if (low < min[axis])
|
|
33
|
+
min[axis] = low;
|
|
34
|
+
if (high > max[axis])
|
|
35
|
+
max[axis] = high;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return found ? { min, max } : undefined;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Relative epsilon for the flatness test.
|
|
42
|
+
*
|
|
43
|
+
* Absolute would be wrong in both directions: a scene in millimetres is
|
|
44
|
+
* legitimately 0.4 deep, and a scene in metres can carry 0.4 of floating-point
|
|
45
|
+
* noise on a plane that was meant to be flat. So the test is against the scene's
|
|
46
|
+
* own size.
|
|
47
|
+
*/
|
|
48
|
+
const FLATNESS_EPSILON = 1e-6;
|
|
49
|
+
/**
|
|
50
|
+
* Is this scene flat enough to look at as a drawing?
|
|
51
|
+
*
|
|
52
|
+
* Marcus, 2026-09-09: *"i want the viewer to detect if our geometry is 2D or 3D
|
|
53
|
+
* and adapt the viewport accordingly … unless our geometry / camera wants a 3D
|
|
54
|
+
* view."*
|
|
55
|
+
*
|
|
56
|
+
* Three conditions, and every one of them is a **fact about the cooked output**
|
|
57
|
+
* rather than a guess about intent, which is the property that makes this
|
|
58
|
+
* safe to act on:
|
|
59
|
+
*
|
|
60
|
+
* 1. A wired camera forces 3D whatever the bounds say. That is the *"unless the
|
|
61
|
+
* camera wants a 3D view"* clause, and it is also the escape hatch for a
|
|
62
|
+
* deliberately flat 3D scene — somebody who wires a camera has said which
|
|
63
|
+
* way they are looking at it.
|
|
64
|
+
* 2. A point `N` attribute forces 3D. Normals only mean something on a surface.
|
|
65
|
+
* 3. Otherwise, flat iff the Z extent is below a relative epsilon.
|
|
66
|
+
*
|
|
67
|
+
* ## This may only ever set the *initial* mode
|
|
68
|
+
*
|
|
69
|
+
* A particle simulation can be planar at frame 1 and gain depth by frame 20.
|
|
70
|
+
* Re-running this on every cook would flip the viewport mid-playback, which is
|
|
71
|
+
* worse than either mode would have been. So the Viewer calls this **once**,
|
|
72
|
+
* when a node has no remembered view, and never again — which is why the
|
|
73
|
+
* per-node view memory is not a separate feature but the thing that makes
|
|
74
|
+
* detect-once implementable at all. Without somewhere to keep the answer the
|
|
75
|
+
* only options are re-detecting, which flips, and never detecting, which is
|
|
76
|
+
* useless.
|
|
77
|
+
*/
|
|
78
|
+
export function sceneDimensionality(scene) {
|
|
79
|
+
if (scene.camera !== null)
|
|
80
|
+
return { is2d: false, reason: "camera" };
|
|
81
|
+
for (const geometry of scene.geometry)
|
|
82
|
+
if (geometry.point.N !== undefined)
|
|
83
|
+
return { is2d: false, reason: "normals" };
|
|
84
|
+
const bounds = sceneBounds(scene);
|
|
85
|
+
if (bounds === undefined)
|
|
86
|
+
return { is2d: true, reason: "empty" };
|
|
87
|
+
const depth = (bounds.max[2] ?? 0) - (bounds.min[2] ?? 0);
|
|
88
|
+
const size = Math.max((bounds.max[0] ?? 0) - (bounds.min[0] ?? 0), (bounds.max[1] ?? 0) - (bounds.min[1] ?? 0), Math.abs(bounds.max[2] ?? 0), Math.abs(bounds.min[2] ?? 0));
|
|
89
|
+
const tolerance = Math.max(size, 1) * FLATNESS_EPSILON;
|
|
90
|
+
return depth <= tolerance
|
|
91
|
+
? { is2d: true, reason: "flat" }
|
|
92
|
+
: { is2d: false, reason: "depth" };
|
|
93
|
+
}
|
|
94
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -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
|