@field/cascade 0.4.0 → 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.
Files changed (110) hide show
  1. package/AGENTS.md +18 -0
  2. package/ARCHITECTURE.md +53 -7
  3. package/CHANGELOG.md +47 -0
  4. package/CONTRIBUTING.md +33 -0
  5. package/DESIGN.md +9 -6
  6. package/README.md +80 -4
  7. package/dist/assets/{CodeEditor-BNOJIDzL.js → CodeEditor-dOrU3aQg.js} +7 -7
  8. package/dist/assets/{CodePanel-2wp2G0Fj.js → CodePanel-Bqy85qen.js} +2 -2
  9. package/dist/assets/{CustomNodeDialog-DNqaVBqA.js → CustomNodeDialog-DkZlOgjs.js} +1 -1
  10. package/dist/assets/{ProjectSettings-DcdYF2E1.js → ProjectSettings-D192mr6g.js} +2 -2
  11. package/dist/assets/{SettingsDialog-D2xVvyiR.js → SettingsDialog-BTeeIE3T.js} +1 -1
  12. package/dist/assets/{VersionHistoryDialog-DW288ykF.js → VersionHistoryDialog-TZQKkn3Y.js} +1 -1
  13. package/dist/assets/{core-B1y38i95.js → core-DwbcGL0o.js} +2 -2
  14. package/dist/assets/{cssMode-BLucnaln.js → cssMode-Dcjn356x.js} +1 -1
  15. package/dist/assets/definitions-C_SY6Bhg.js +1 -0
  16. package/dist/assets/{editor.api-B3Et3Pph.js → editor.api-D7Q83606.js} +1 -1
  17. package/dist/assets/{freemarker2-DVlPoano.js → freemarker2-DCCiHlV6.js} +1 -1
  18. package/dist/assets/geo-CT-wtvaE.js +1 -0
  19. package/dist/assets/{handlebars-B5FZBAMR.js → handlebars-BCxC8xu1.js} +1 -1
  20. package/dist/assets/{html-FodD8KbR.js → html-COwepzSd.js} +1 -1
  21. package/dist/assets/{htmlMode-CsG57VCD.js → htmlMode-Dpg7H0rx.js} +1 -1
  22. package/dist/assets/{image-B3F-nq25.js → image-CMfMMxlC.js} +3 -3
  23. package/dist/assets/index-De1cIZ9N.js +212 -0
  24. package/dist/assets/{index-Y2ock1cw.css → index-ilgbOu6G.css} +1 -1
  25. package/dist/assets/{javascript-B3gonF_H.js → javascript-CSwWxfLr.js} +1 -1
  26. package/dist/assets/{jsonMode-Y3w1ahZg.js → jsonMode-BXRriTpj.js} +1 -1
  27. package/dist/assets/{liquid-CTpWXIYO.js → liquid-Cmkfyogc.js} +1 -1
  28. package/dist/assets/{lspLanguageFeatures-CODBdKEZ.js → lspLanguageFeatures-xH9JbZGO.js} +1 -1
  29. package/dist/assets/{mdx-CDAQJZIz.js → mdx-BzrNjNvK.js} +1 -1
  30. package/dist/assets/nodeTypeUtils-BDJsc2Ks.js +1 -0
  31. package/dist/assets/pop-BdKEHslJ.js +1 -0
  32. package/dist/assets/{python-LOIlxJU-.js → python-D4E9jbx7.js} +1 -1
  33. package/dist/assets/{razor-CllPQSHo.js → razor-B3x9cIEM.js} +1 -1
  34. package/dist/assets/{tsMode-CO28ut8t.js → tsMode-dhQYPNMZ.js} +1 -1
  35. package/dist/assets/{typescript-ruU9wKQ_.js → typescript-CITkDxAE.js} +1 -1
  36. package/dist/assets/{workers-Bc1Ywp4-.js → workers-BQvF9bbO.js} +1 -1
  37. package/dist/assets/{xml-DoXP2Dh1.js → xml-DvHlU3A8.js} +1 -1
  38. package/dist/assets/{yaml-D9YPyGiJ.js → yaml-uznsLgs8.js} +1 -1
  39. package/dist/cli/index.js +16227 -12672
  40. package/dist/cli/runtime/io.ts +31 -25
  41. package/dist/contracts/definition.d.ts +21 -2
  42. package/dist/contracts/index.d.ts +2 -0
  43. package/dist/contracts/index.js +2 -0
  44. package/dist/contracts/particles.d.ts +54 -0
  45. package/dist/contracts/particles.js +167 -0
  46. package/dist/contracts/scene.d.ts +129 -0
  47. package/dist/contracts/scene.js +73 -0
  48. package/dist/contracts/schema.js +1 -1
  49. package/dist/contracts/validate.js +4 -2
  50. package/dist/contracts/values.d.ts +3 -2
  51. package/dist/contracts/values.js +9 -0
  52. package/dist/index.html +3 -3
  53. package/dist/io.d.ts +4 -16
  54. package/dist/io.js +26 -8
  55. package/dist/player/app.js +810 -0
  56. package/dist/player/embed.d.ts +4 -0
  57. package/dist/player/embed.js +77 -0
  58. package/dist/player/types.d.ts +35 -0
  59. package/dist/runtime/builtins/core/index.d.ts +3 -2
  60. package/dist/runtime/builtins/core/index.js +6 -2
  61. package/dist/runtime/builtins/core/structural.d.ts +168 -0
  62. package/dist/runtime/builtins/core/structural.js +175 -0
  63. package/dist/runtime/builtins/core/time.d.ts +165 -0
  64. package/dist/runtime/builtins/core/time.js +110 -0
  65. package/dist/runtime/builtins/geo/definitions.d.ts +247 -0
  66. package/dist/runtime/builtins/geo/definitions.js +70 -0
  67. package/dist/runtime/builtins/geo/index.d.ts +2 -1
  68. package/dist/runtime/builtins/geo/index.js +3 -1
  69. package/dist/runtime/builtins/geo/render.d.ts +87 -0
  70. package/dist/runtime/builtins/geo/render.js +87 -0
  71. package/dist/runtime/builtins/geo/svgExport.d.ts +11 -0
  72. package/dist/runtime/builtins/geo/svgExport.js +10 -0
  73. package/dist/runtime/builtins/index.d.ts +5 -2
  74. package/dist/runtime/builtins/index.js +13 -3
  75. package/dist/runtime/builtins/pop/definitions.d.ts +2120 -0
  76. package/dist/runtime/builtins/pop/definitions.js +815 -0
  77. package/dist/runtime/builtins/pop/index.d.ts +2 -0
  78. package/dist/runtime/builtins/pop/index.js +2 -0
  79. package/dist/runtime/builtins/scene/definitions.d.ts +356 -0
  80. package/dist/runtime/builtins/scene/definitions.js +224 -0
  81. package/dist/runtime/builtins/scene/index.d.ts +2 -0
  82. package/dist/runtime/builtins/scene/index.js +2 -0
  83. package/dist/runtime/geometry/render.d.ts +109 -0
  84. package/dist/runtime/geometry/render.js +145 -0
  85. package/dist/runtime/geometry/svg.d.ts +29 -0
  86. package/dist/runtime/geometry/svg.js +11 -5
  87. package/dist/runtime/index.d.ts +1 -0
  88. package/dist/runtime/index.js +1 -0
  89. package/dist/runtime/pop/cache.d.ts +27 -0
  90. package/dist/runtime/pop/cache.js +123 -0
  91. package/dist/runtime/pop/forces.d.ts +202 -0
  92. package/dist/runtime/pop/forces.js +519 -0
  93. package/dist/runtime/pop/index.d.ts +5 -0
  94. package/dist/runtime/pop/index.js +5 -0
  95. package/dist/runtime/pop/state.d.ts +136 -0
  96. package/dist/runtime/pop/state.js +283 -0
  97. package/dist/runtime/pop/trail.d.ts +62 -0
  98. package/dist/runtime/pop/trail.js +142 -0
  99. package/dist/runtime/runtime.js +221 -3
  100. package/dist/runtime/scene/index.d.ts +50 -0
  101. package/dist/runtime/scene/index.js +94 -0
  102. package/dist/studio/panel.d.ts +68 -1
  103. package/doc/HEADLESS_GPU.md +4 -4
  104. package/doc/NODE_REFERENCE.md +372 -1
  105. package/doc/PROJECT_AUTHORING.md +16 -4
  106. package/doc/WEB_PLAYER.md +131 -0
  107. package/package.json +16 -2
  108. package/dist/assets/geo-DhyFdBXX.js +0 -1
  109. package/dist/assets/index-CC6YQw2n.js +0 -212
  110. package/dist/assets/nodeTypeUtils-BBA1JqHx.js +0 -1
@@ -0,0 +1,4 @@
1
+ import type { MountPlayerOptions, PlayerController } from './types.js';
2
+ export type { MountPlayerOptions, PlayerController, PlayerEvent, PlayerListener, PlayerOutputSelection, } from './types.js';
3
+ /** Mount a built player in a realm-isolated same-origin iframe. */
4
+ export declare function mountPlayer(container: HTMLElement, options: MountPlayerOptions): Promise<PlayerController>;
@@ -0,0 +1,77 @@
1
+ // src/player/embed.ts
2
+ async function mountPlayer(container, options) {
3
+ const hostDocument = container.ownerDocument;
4
+ const hostWindow = hostDocument.defaultView;
5
+ if (!hostWindow) throw new Error("Cascade player mount requires a browser window");
6
+ const url = new URL(String(options.url), hostDocument.baseURI);
7
+ if (url.origin !== hostWindow.location.origin) {
8
+ throw new Error(
9
+ "Programmatic Cascade player mounts must be same-origin; use ordinary iframe markup for cross-origin embeds"
10
+ );
11
+ }
12
+ setQueryOption(url, "frame", options.frame ?? 1);
13
+ setQueryOption(url, "fps", options.fps ?? 30);
14
+ url.searchParams.set("autoplay", options.autoplay ? "1" : "0");
15
+ if (options.output) {
16
+ url.searchParams.set("outputNode", options.output.nodeId);
17
+ url.searchParams.set("outputPort", options.output.port);
18
+ } else {
19
+ url.searchParams.delete("outputNode");
20
+ url.searchParams.delete("outputPort");
21
+ }
22
+ const iframe = hostDocument.createElement("iframe");
23
+ iframe.src = url.href;
24
+ iframe.title = "Cascade player";
25
+ iframe.loading = "eager";
26
+ Object.assign(iframe.style, { display: "block", width: "100%", height: "100%", border: "0" });
27
+ iframe.setAttribute("allow", "fullscreen");
28
+ container.append(iframe);
29
+ try {
30
+ await iframeLoaded(iframe);
31
+ const child = iframe.contentWindow;
32
+ const ready = child?.cascadePlayerReady;
33
+ if (!ready) throw new Error("Cascade player did not expose cascadePlayerReady");
34
+ const controller = await ready;
35
+ let disposed = false;
36
+ return {
37
+ play: () => controller.play(),
38
+ pause: () => controller.pause(),
39
+ seek: (frame) => controller.seek(frame),
40
+ setInput: (nodeId, name, value) => controller.setInput(nodeId, name, value),
41
+ setProp: (nodeId, name, value) => controller.setProp(nodeId, name, value),
42
+ getOutput: (nodeId, port) => controller.getOutput(nodeId, port),
43
+ selectOutput: (nodeId, port) => controller.selectOutput(nodeId, port),
44
+ downloadOutput: () => controller.downloadOutput(),
45
+ resize: (width, height) => controller.resize(width, height),
46
+ subscribe: (listener) => controller.subscribe(listener),
47
+ async dispose() {
48
+ if (disposed) return;
49
+ disposed = true;
50
+ try {
51
+ await controller.dispose();
52
+ } finally {
53
+ iframe.remove();
54
+ }
55
+ }
56
+ };
57
+ } catch (error) {
58
+ iframe.remove();
59
+ throw error;
60
+ }
61
+ }
62
+ function setQueryOption(url, name, value) {
63
+ if (value === void 0) return;
64
+ if (!Number.isFinite(value)) throw new TypeError(`${name} must be finite`);
65
+ url.searchParams.set(name, String(value));
66
+ }
67
+ function iframeLoaded(iframe) {
68
+ return new Promise((resolve, reject) => {
69
+ iframe.addEventListener("load", () => resolve(), { once: true });
70
+ iframe.addEventListener("error", () => reject(new Error(`Could not load Cascade player ${iframe.src}`)), {
71
+ once: true
72
+ });
73
+ });
74
+ }
75
+ export {
76
+ mountPlayer
77
+ };
@@ -0,0 +1,35 @@
1
+ export interface PlayerOutputSelection {
2
+ readonly nodeId: string;
3
+ readonly port: string;
4
+ }
5
+ export type PlayerEvent = {
6
+ readonly type: 'frame';
7
+ readonly frame: number;
8
+ } | {
9
+ readonly type: 'playing';
10
+ readonly playing: boolean;
11
+ } | {
12
+ readonly type: 'error';
13
+ readonly error: Error;
14
+ };
15
+ export type PlayerListener = (event: PlayerEvent) => void;
16
+ export interface PlayerController {
17
+ play(): void;
18
+ pause(): void;
19
+ seek(frame: number): Promise<void>;
20
+ setInput(nodeId: string, name: string, value: unknown): Promise<void>;
21
+ setProp(nodeId: string, name: string, value: unknown): Promise<void>;
22
+ getOutput(nodeId: string, port: string): unknown;
23
+ selectOutput(nodeId: string, port: string): Promise<void>;
24
+ downloadOutput(): Promise<Blob>;
25
+ resize(width: number, height: number): void;
26
+ subscribe(listener: PlayerListener): () => void;
27
+ dispose(): Promise<void>;
28
+ }
29
+ export interface MountPlayerOptions {
30
+ readonly url: string | URL;
31
+ readonly frame?: number;
32
+ readonly fps?: number;
33
+ readonly autoplay?: boolean;
34
+ readonly output?: PlayerOutputSelection;
35
+ }
@@ -3,8 +3,9 @@ import { cameraRegistration } from "./camera.js";
3
3
  import { randomRegistration } from "./random.js";
4
4
  import { remapRegistration } from "./remap.js";
5
5
  import { mergeRegistration, nullRegistration, selectRegistration, switchRegistration } from "./routing.js";
6
- import { inputRegistration, outputRegistration, subnetRegistration } from "./structural.js";
7
- export { cameraRegistration, inputRegistration, mergeRegistration, nullRegistration, outputRegistration, randomRegistration, remapRegistration, selectRegistration, subnetRegistration, switchRegistration, };
6
+ import { feedbackRegistration, inputRegistration, outputRegistration, previousRegistration, subnetRegistration } from "./structural.js";
7
+ import { timeRegistration } from "./time.js";
8
+ export { cameraRegistration, feedbackRegistration, previousRegistration, inputRegistration, mergeRegistration, nullRegistration, outputRegistration, randomRegistration, remapRegistration, selectRegistration, subnetRegistration, switchRegistration, timeRegistration, };
8
9
  export declare const coreNodeRegistrations: readonly DefinitionNodeRegistration[];
9
10
  export declare function coreNodeRegistration(moduleId: string): DefinitionNodeRegistration | undefined;
10
11
  //# sourceMappingURL=index.d.ts.map
@@ -2,8 +2,9 @@ import { cameraRegistration } from "./camera.js";
2
2
  import { randomRegistration } from "./random.js";
3
3
  import { remapRegistration } from "./remap.js";
4
4
  import { mergeRegistration, nullRegistration, selectRegistration, switchRegistration } from "./routing.js";
5
- import { inputRegistration, outputRegistration, subnetRegistration } from "./structural.js";
6
- export { cameraRegistration, inputRegistration, mergeRegistration, nullRegistration, outputRegistration, randomRegistration, remapRegistration, selectRegistration, subnetRegistration, switchRegistration, };
5
+ import { feedbackRegistration, inputRegistration, outputRegistration, previousRegistration, subnetRegistration, } from "./structural.js";
6
+ import { timeRegistration } from "./time.js";
7
+ export { cameraRegistration, feedbackRegistration, previousRegistration, inputRegistration, mergeRegistration, nullRegistration, outputRegistration, randomRegistration, remapRegistration, selectRegistration, subnetRegistration, switchRegistration, timeRegistration, };
7
8
  export const coreNodeRegistrations = Object.freeze([
8
9
  subnetRegistration,
9
10
  inputRegistration,
@@ -15,6 +16,9 @@ export const coreNodeRegistrations = Object.freeze([
15
16
  randomRegistration,
16
17
  remapRegistration,
17
18
  cameraRegistration,
19
+ timeRegistration,
20
+ feedbackRegistration,
21
+ previousRegistration,
18
22
  ]);
19
23
  const registrationsById = new Map(coreNodeRegistrations.map((registration) => [registration.moduleId, registration]));
20
24
  export function coreNodeRegistration(moduleId) {
@@ -79,8 +79,176 @@ declare const subnetDefinition: {
79
79
  readonly description: "Contains a nested deterministic graph";
80
80
  readonly icon: "Folder";
81
81
  };
82
+ /**
83
+ * `cascade.core.Feedback` — a container whose contents run once per step, with
84
+ * the previous step's result carried forward.
85
+ *
86
+ * Marcus, 2026-09-09, trying to dive into the particle node: *"how do i jump
87
+ * into the sim node and see its network of forces."* He could not, and the
88
+ * reason was structural rather than a missing handler: **a subnet in Cascade is
89
+ * a grouping, not an evaluation context.** Its children are ordinary nodes in
90
+ * one flat graph, cooked once each. So "the forces are nodes inside, replayed
91
+ * per timestep" had nowhere to live.
92
+ *
93
+ * This is that place, and it is the one construct a pure DAG cannot express. A
94
+ * simulation is `state = f(state)` repeated, which is a cycle — and every
95
+ * dataflow system that supports simulation makes it special somewhere. Houdini's
96
+ * Solver SOP is a subnet with a `Prev_Frame` input; this is the same shape with
97
+ * `cascade.core.Previous` in place of that input.
98
+ *
99
+ * ## The contract
100
+ *
101
+ * - `initial` seeds the first step. Whatever type it carries, the loop carries.
102
+ * - `steps` is how many times the contents run. **An input rather than a prop**,
103
+ * because the whole point is to drive it from `cascade.core.Time` — a frame
104
+ * pinned in a prop is exactly the bug that made the timeline do nothing.
105
+ * - Inside, `cascade.core.Previous` emits the carried value: `initial` on the
106
+ * first step, and the previous step's result after that.
107
+ * - Inside, `cascade.core.Output` at index 0 is the step's result. Without one
108
+ * the loop has no result to carry and the container refuses rather than
109
+ * quietly returning `initial` — a simulation that silently does not simulate
110
+ * is the worst available failure.
111
+ *
112
+ * ## What it does not do, deliberately
113
+ *
114
+ * **`steps` of zero returns `initial` unchanged.** That is not an error: frame
115
+ * zero of a simulation is its initial state, and a graph that throws at the
116
+ * start of the timeline is unusable.
117
+ *
118
+ * **There is no per-step caching here.** The container re-runs from `initial`
119
+ * every cook, which is O(steps) and honest — the same choice `pop.Simulate`
120
+ * already made, for the same reason: re-simulation from the start is the
121
+ * definition of correctness, and a cache is an optimisation layered on top of a
122
+ * thing that is already right.
123
+ */
124
+ declare const feedbackDefinition: {
125
+ readonly apiVersion: 1;
126
+ readonly runsOn: "portable";
127
+ readonly container: "feedback";
128
+ readonly label: "Feedback";
129
+ readonly description: "Runs its contents once per step, carrying the previous result forward.";
130
+ readonly icon: "Repeat";
131
+ readonly inputs: {
132
+ readonly initial: {
133
+ readonly kind: "data";
134
+ readonly type: "any";
135
+ readonly default: null;
136
+ readonly description: "The state the first step starts from.";
137
+ };
138
+ readonly steps: {
139
+ readonly kind: "data";
140
+ readonly type: "int";
141
+ readonly default: 1;
142
+ readonly min: 0;
143
+ readonly description: "How many times the contents run. Drive it from a Time node.";
144
+ };
145
+ };
146
+ readonly outputs: {
147
+ readonly result: {
148
+ readonly kind: "data";
149
+ readonly type: "any";
150
+ readonly description: "The last step's result, or `initial` when steps is zero.";
151
+ };
152
+ /**
153
+ * Every kept step, oldest first.
154
+ *
155
+ * This exists because **a trail needs the history, not the final state**,
156
+ * and nothing outside the loop could reconstruct it: a downstream node sees
157
+ * one frame, which draws nothing. Houdini's Trail SOP has the same
158
+ * requirement and solves it the same way, by being given the frames.
159
+ *
160
+ * Empty unless `history` is non-zero, because the alternative is a
161
+ * simulation that quietly holds 120 copies of its own geometry — and at
162
+ * a few thousand particles that is the difference between a preview and a
163
+ * tab that runs out of memory.
164
+ */
165
+ readonly history: {
166
+ readonly kind: "data";
167
+ readonly type: "array";
168
+ readonly description: "The kept step results, oldest first. Empty unless History is set.";
169
+ };
170
+ };
171
+ readonly props: {
172
+ /**
173
+ * How many step results to keep, newest-biased: `0` keeps none, `N` keeps
174
+ * the last `N`.
175
+ *
176
+ * A prop rather than an input because it is structural — how much memory
177
+ * this loop is allowed — rather than something to animate. It is also a
178
+ * cap rather than a flag on purpose: "keep everything" is not offered,
179
+ * because the honest version of that is a number somebody chose.
180
+ */
181
+ readonly history: {
182
+ readonly type: "int";
183
+ readonly default: 0;
184
+ readonly min: 0;
185
+ readonly max: 100000;
186
+ readonly label: "History";
187
+ readonly description: "Keep the last N step results on the history output. 0 keeps none.";
188
+ };
189
+ };
190
+ };
191
+ /**
192
+ * `cascade.core.Previous` — the carried state, inside a feedback container.
193
+ *
194
+ * The counterpart to `cascade.core.Input`, and it resolves the same way: the
195
+ * runtime special-cases the module id and supplies the value, because a
196
+ * boundary node's input comes from its parent rather than from a wire. What it
197
+ * emits is `initial` on the first step and the previous step's `Output` after
198
+ * that.
199
+ *
200
+ * It also carries the step number, which is what a force needs to evolve noise
201
+ * or ramp a strength over the run — and having it here rather than as a global
202
+ * keeps a step-dependent node a pure function of its inputs.
203
+ *
204
+ * Outside a feedback container it emits its own `initial` input and a step of
205
+ * zero. Not an error: a node dragged out of a container should show something
206
+ * explicable rather than throw, and the alternative is a graph that cannot be
207
+ * edited without first being valid.
208
+ */
209
+ declare const previousDefinition: {
210
+ readonly apiVersion: 1;
211
+ readonly runsOn: "portable";
212
+ readonly label: "Previous";
213
+ readonly description: "The previous step's state, inside a Feedback container. The first step gets the container's initial value.";
214
+ readonly icon: "Rewind";
215
+ readonly inputs: {
216
+ readonly initial: {
217
+ readonly kind: "data";
218
+ readonly type: "any";
219
+ readonly default: null;
220
+ readonly description: "Used only outside a Feedback container.";
221
+ };
222
+ };
223
+ readonly outputs: {
224
+ readonly value: {
225
+ readonly kind: "data";
226
+ readonly type: "any";
227
+ readonly description: "The carried state.";
228
+ };
229
+ readonly step: {
230
+ readonly kind: "data";
231
+ readonly type: "int";
232
+ readonly description: "Zero-based step index.";
233
+ };
234
+ };
235
+ readonly props: {};
236
+ };
82
237
  export declare const inputRegistration: DefinitionNodeRegistration<typeof inputDefinition>;
83
238
  export declare const outputRegistration: DefinitionNodeRegistration<typeof outputDefinition>;
84
239
  export declare const subnetRegistration: DefinitionNodeRegistration<typeof subnetDefinition>;
240
+ /**
241
+ * Both of these have an empty `execute`, and that is the design rather than a
242
+ * stub: the runtime supplies their behaviour because both are *boundaries*, and
243
+ * a boundary's value comes from its parent's evaluation rather than from its own
244
+ * inputs. `cascade.core.Subnet` and `cascade.core.Input` already work this way.
245
+ *
246
+ * Writing the loop in `Feedback.execute` instead would need the node to reach
247
+ * the graph, its children and the scheduler — and `NodeExecutionContext` gives
248
+ * a node its own id and nothing else, deliberately, which is what keeps a
249
+ * definition-v1 node a pure function of its inputs.
250
+ */
251
+ export declare const feedbackRegistration: DefinitionNodeRegistration<typeof feedbackDefinition>;
252
+ export declare const previousRegistration: DefinitionNodeRegistration<typeof previousDefinition>;
85
253
  export {};
86
254
  //# sourceMappingURL=structural.d.ts.map
@@ -42,6 +42,153 @@ const subnetDefinition = {
42
42
  description: "Contains a nested deterministic graph",
43
43
  icon: "Folder",
44
44
  };
45
+ /**
46
+ * `cascade.core.Feedback` — a container whose contents run once per step, with
47
+ * the previous step's result carried forward.
48
+ *
49
+ * Marcus, 2026-09-09, trying to dive into the particle node: *"how do i jump
50
+ * into the sim node and see its network of forces."* He could not, and the
51
+ * reason was structural rather than a missing handler: **a subnet in Cascade is
52
+ * a grouping, not an evaluation context.** Its children are ordinary nodes in
53
+ * one flat graph, cooked once each. So "the forces are nodes inside, replayed
54
+ * per timestep" had nowhere to live.
55
+ *
56
+ * This is that place, and it is the one construct a pure DAG cannot express. A
57
+ * simulation is `state = f(state)` repeated, which is a cycle — and every
58
+ * dataflow system that supports simulation makes it special somewhere. Houdini's
59
+ * Solver SOP is a subnet with a `Prev_Frame` input; this is the same shape with
60
+ * `cascade.core.Previous` in place of that input.
61
+ *
62
+ * ## The contract
63
+ *
64
+ * - `initial` seeds the first step. Whatever type it carries, the loop carries.
65
+ * - `steps` is how many times the contents run. **An input rather than a prop**,
66
+ * because the whole point is to drive it from `cascade.core.Time` — a frame
67
+ * pinned in a prop is exactly the bug that made the timeline do nothing.
68
+ * - Inside, `cascade.core.Previous` emits the carried value: `initial` on the
69
+ * first step, and the previous step's result after that.
70
+ * - Inside, `cascade.core.Output` at index 0 is the step's result. Without one
71
+ * the loop has no result to carry and the container refuses rather than
72
+ * quietly returning `initial` — a simulation that silently does not simulate
73
+ * is the worst available failure.
74
+ *
75
+ * ## What it does not do, deliberately
76
+ *
77
+ * **`steps` of zero returns `initial` unchanged.** That is not an error: frame
78
+ * zero of a simulation is its initial state, and a graph that throws at the
79
+ * start of the timeline is unusable.
80
+ *
81
+ * **There is no per-step caching here.** The container re-runs from `initial`
82
+ * every cook, which is O(steps) and honest — the same choice `pop.Simulate`
83
+ * already made, for the same reason: re-simulation from the start is the
84
+ * definition of correctness, and a cache is an optimisation layered on top of a
85
+ * thing that is already right.
86
+ */
87
+ const feedbackDefinition = {
88
+ apiVersion: 1,
89
+ runsOn: "portable",
90
+ container: "feedback",
91
+ label: "Feedback",
92
+ description: "Runs its contents once per step, carrying the previous result forward.",
93
+ icon: "Repeat",
94
+ inputs: {
95
+ initial: {
96
+ kind: "data",
97
+ type: "any",
98
+ default: null,
99
+ description: "The state the first step starts from.",
100
+ },
101
+ steps: {
102
+ kind: "data",
103
+ type: "int",
104
+ default: 1,
105
+ min: 0,
106
+ description: "How many times the contents run. Drive it from a Time node.",
107
+ },
108
+ },
109
+ outputs: {
110
+ result: {
111
+ kind: "data",
112
+ type: "any",
113
+ description: "The last step's result, or `initial` when steps is zero.",
114
+ },
115
+ /**
116
+ * Every kept step, oldest first.
117
+ *
118
+ * This exists because **a trail needs the history, not the final state**,
119
+ * and nothing outside the loop could reconstruct it: a downstream node sees
120
+ * one frame, which draws nothing. Houdini's Trail SOP has the same
121
+ * requirement and solves it the same way, by being given the frames.
122
+ *
123
+ * Empty unless `history` is non-zero, because the alternative is a
124
+ * simulation that quietly holds 120 copies of its own geometry — and at
125
+ * a few thousand particles that is the difference between a preview and a
126
+ * tab that runs out of memory.
127
+ */
128
+ history: {
129
+ kind: "data",
130
+ type: "array",
131
+ description: "The kept step results, oldest first. Empty unless History is set.",
132
+ },
133
+ },
134
+ props: {
135
+ /**
136
+ * How many step results to keep, newest-biased: `0` keeps none, `N` keeps
137
+ * the last `N`.
138
+ *
139
+ * A prop rather than an input because it is structural — how much memory
140
+ * this loop is allowed — rather than something to animate. It is also a
141
+ * cap rather than a flag on purpose: "keep everything" is not offered,
142
+ * because the honest version of that is a number somebody chose.
143
+ */
144
+ history: {
145
+ type: "int",
146
+ default: 0,
147
+ min: 0,
148
+ max: 100000,
149
+ label: "History",
150
+ description: "Keep the last N step results on the history output. 0 keeps none.",
151
+ },
152
+ },
153
+ };
154
+ /**
155
+ * `cascade.core.Previous` — the carried state, inside a feedback container.
156
+ *
157
+ * The counterpart to `cascade.core.Input`, and it resolves the same way: the
158
+ * runtime special-cases the module id and supplies the value, because a
159
+ * boundary node's input comes from its parent rather than from a wire. What it
160
+ * emits is `initial` on the first step and the previous step's `Output` after
161
+ * that.
162
+ *
163
+ * It also carries the step number, which is what a force needs to evolve noise
164
+ * or ramp a strength over the run — and having it here rather than as a global
165
+ * keeps a step-dependent node a pure function of its inputs.
166
+ *
167
+ * Outside a feedback container it emits its own `initial` input and a step of
168
+ * zero. Not an error: a node dragged out of a container should show something
169
+ * explicable rather than throw, and the alternative is a graph that cannot be
170
+ * edited without first being valid.
171
+ */
172
+ const previousDefinition = {
173
+ apiVersion: 1,
174
+ runsOn: "portable",
175
+ label: "Previous",
176
+ description: "The previous step's state, inside a Feedback container. The first step gets the container's initial value.",
177
+ icon: "Rewind",
178
+ inputs: {
179
+ initial: {
180
+ kind: "data",
181
+ type: "any",
182
+ default: null,
183
+ description: "Used only outside a Feedback container.",
184
+ },
185
+ },
186
+ outputs: {
187
+ value: { kind: "data", type: "any", description: "The carried state." },
188
+ step: { kind: "data", type: "int", description: "Zero-based step index." },
189
+ },
190
+ props: {},
191
+ };
45
192
  export const inputRegistration = {
46
193
  kind: "definition-v1",
47
194
  moduleId: "cascade.core.Input",
@@ -60,4 +207,32 @@ export const subnetRegistration = {
60
207
  definition: subnetDefinition,
61
208
  loadExecute: async () => () => { },
62
209
  };
210
+ /**
211
+ * Both of these have an empty `execute`, and that is the design rather than a
212
+ * stub: the runtime supplies their behaviour because both are *boundaries*, and
213
+ * a boundary's value comes from its parent's evaluation rather than from its own
214
+ * inputs. `cascade.core.Subnet` and `cascade.core.Input` already work this way.
215
+ *
216
+ * Writing the loop in `Feedback.execute` instead would need the node to reach
217
+ * the graph, its children and the scheduler — and `NodeExecutionContext` gives
218
+ * a node its own id and nothing else, deliberately, which is what keeps a
219
+ * definition-v1 node a pure function of its inputs.
220
+ */
221
+ export const feedbackRegistration = {
222
+ kind: "definition-v1",
223
+ moduleId: "cascade.core.Feedback",
224
+ definition: feedbackDefinition,
225
+ loadExecute: async () => () => { },
226
+ };
227
+ export const previousRegistration = {
228
+ kind: "definition-v1",
229
+ moduleId: "cascade.core.Previous",
230
+ definition: previousDefinition,
231
+ loadExecute: async () => ({ inputs, outputs }) => {
232
+ // Outside a container this is what runs. Inside, the runtime overwrites
233
+ // both outputs before any child reads them.
234
+ outputs.value.set(inputs.initial);
235
+ outputs.step.set(0);
236
+ },
237
+ };
63
238
  //# sourceMappingURL=structural.js.map