@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.
Files changed (129) hide show
  1. package/AGENTS.md +32 -9
  2. package/ARCHITECTURE.md +75 -13
  3. package/CHANGELOG.md +98 -1
  4. package/CONTRIBUTING.md +104 -0
  5. package/DESIGN.md +24 -15
  6. package/README.md +121 -14
  7. package/dist/assets/{CodeEditor-D22fVi-4.js → CodeEditor-dOrU3aQg.js} +7 -7
  8. package/dist/assets/{CodePanel-DPNEYfcR.js → CodePanel-Bqy85qen.js} +2 -2
  9. package/dist/assets/{CustomNodeDialog-DeHCAvgb.js → CustomNodeDialog-DkZlOgjs.js} +1 -1
  10. package/dist/assets/{ProjectSettings-DWpVtWEx.js → ProjectSettings-D192mr6g.js} +2 -2
  11. package/dist/assets/{SettingsDialog-za00IS3L.js → SettingsDialog-BTeeIE3T.js} +1 -1
  12. package/dist/assets/{VersionHistoryDialog-DTMGhI0_.js → VersionHistoryDialog-TZQKkn3Y.js} +1 -1
  13. package/dist/assets/{core-JsyKMO1k.js → core-DwbcGL0o.js} +2 -2
  14. package/dist/assets/{cssMode-emdRheFK.js → cssMode-Dcjn356x.js} +1 -1
  15. package/dist/assets/definitions-C_SY6Bhg.js +1 -0
  16. package/dist/assets/{editor.api-xHX-fMSo.js → editor.api-D7Q83606.js} +1 -1
  17. package/dist/assets/{freemarker2-C3M8_Gs3.js → freemarker2-DCCiHlV6.js} +1 -1
  18. package/dist/assets/geo-CT-wtvaE.js +1 -0
  19. package/dist/assets/{handlebars-CBMurtFk.js → handlebars-BCxC8xu1.js} +1 -1
  20. package/dist/assets/{html-xbLQwknw.js → html-COwepzSd.js} +1 -1
  21. package/dist/assets/{htmlMode-2wKdDTon.js → htmlMode-Dpg7H0rx.js} +1 -1
  22. package/dist/assets/{image-i1j6nWKe.js → image-CMfMMxlC.js} +3 -3
  23. package/dist/assets/index-De1cIZ9N.js +212 -0
  24. package/dist/assets/index-ilgbOu6G.css +1 -0
  25. package/dist/assets/{javascript-DJaOo_2I.js → javascript-CSwWxfLr.js} +1 -1
  26. package/dist/assets/{jsonMode-Bheas8_0.js → jsonMode-BXRriTpj.js} +1 -1
  27. package/dist/assets/{liquid-BCj_BGbM.js → liquid-Cmkfyogc.js} +1 -1
  28. package/dist/assets/{lspLanguageFeatures-BfIsx_xz.js → lspLanguageFeatures-xH9JbZGO.js} +1 -1
  29. package/dist/assets/{mdx-DwiQDiVO.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-BjrB5nqj.js → python-D4E9jbx7.js} +1 -1
  33. package/dist/assets/{razor-CfoBmGDs.js → razor-B3x9cIEM.js} +1 -1
  34. package/dist/assets/{tsMode-BcEoMSxp.js → tsMode-dhQYPNMZ.js} +1 -1
  35. package/dist/assets/{typescript-DIg7TEmQ.js → typescript-CITkDxAE.js} +1 -1
  36. package/dist/assets/{workers-vXYuwtPw.js → workers-BQvF9bbO.js} +1 -1
  37. package/dist/assets/{xml-DexLQIQy.js → xml-DvHlU3A8.js} +1 -1
  38. package/dist/assets/{yaml-DUnD_1dE.js → yaml-uznsLgs8.js} +1 -1
  39. package/dist/cli/index.js +16465 -12096
  40. package/dist/cli/runtime/gpu.ts +71 -0
  41. package/dist/cli/runtime/io.ts +31 -25
  42. package/dist/contracts/camera.d.ts +70 -0
  43. package/dist/contracts/camera.js +27 -0
  44. package/dist/contracts/capabilities.d.ts +6 -6
  45. package/dist/contracts/definition.d.ts +62 -4
  46. package/dist/contracts/index.d.ts +4 -0
  47. package/dist/contracts/index.js +4 -0
  48. package/dist/contracts/particles.d.ts +54 -0
  49. package/dist/contracts/particles.js +167 -0
  50. package/dist/contracts/scene.d.ts +129 -0
  51. package/dist/contracts/scene.js +73 -0
  52. package/dist/contracts/schema.js +26 -3
  53. package/dist/contracts/series.d.ts +75 -0
  54. package/dist/contracts/series.js +2 -0
  55. package/dist/contracts/validate.js +23 -5
  56. package/dist/contracts/values.d.ts +4 -2
  57. package/dist/contracts/values.js +10 -0
  58. package/dist/gpu.d.ts +17 -0
  59. package/dist/gpu.js +61 -1
  60. package/dist/index.html +3 -3
  61. package/dist/io.d.ts +4 -16
  62. package/dist/io.js +26 -8
  63. package/dist/player/app.js +810 -0
  64. package/dist/player/embed.d.ts +4 -0
  65. package/dist/player/embed.js +77 -0
  66. package/dist/player/types.d.ts +35 -0
  67. package/dist/runtime/builtins/core/camera.d.ts +252 -0
  68. package/dist/runtime/builtins/core/camera.js +149 -0
  69. package/dist/runtime/builtins/core/index.d.ts +4 -2
  70. package/dist/runtime/builtins/core/index.js +8 -2
  71. package/dist/runtime/builtins/core/structural.d.ts +168 -0
  72. package/dist/runtime/builtins/core/structural.js +175 -0
  73. package/dist/runtime/builtins/core/time.d.ts +165 -0
  74. package/dist/runtime/builtins/core/time.js +110 -0
  75. package/dist/runtime/builtins/geo/definitions.d.ts +247 -0
  76. package/dist/runtime/builtins/geo/definitions.js +70 -0
  77. package/dist/runtime/builtins/geo/index.d.ts +2 -1
  78. package/dist/runtime/builtins/geo/index.js +3 -1
  79. package/dist/runtime/builtins/geo/render.d.ts +87 -0
  80. package/dist/runtime/builtins/geo/render.js +87 -0
  81. package/dist/runtime/builtins/geo/svgExport.d.ts +11 -0
  82. package/dist/runtime/builtins/geo/svgExport.js +10 -0
  83. package/dist/runtime/builtins/index.d.ts +5 -2
  84. package/dist/runtime/builtins/index.js +13 -3
  85. package/dist/runtime/builtins/pop/definitions.d.ts +2120 -0
  86. package/dist/runtime/builtins/pop/definitions.js +815 -0
  87. package/dist/runtime/builtins/pop/index.d.ts +2 -0
  88. package/dist/runtime/builtins/pop/index.js +2 -0
  89. package/dist/runtime/builtins/scene/definitions.d.ts +356 -0
  90. package/dist/runtime/builtins/scene/definitions.js +224 -0
  91. package/dist/runtime/builtins/scene/index.d.ts +2 -0
  92. package/dist/runtime/builtins/scene/index.js +2 -0
  93. package/dist/runtime/camera/index.d.ts +60 -0
  94. package/dist/runtime/camera/index.js +234 -0
  95. package/dist/runtime/definition/architecture.js +36 -1
  96. package/dist/runtime/geometry/render.d.ts +109 -0
  97. package/dist/runtime/geometry/render.js +145 -0
  98. package/dist/runtime/geometry/svg.d.ts +29 -0
  99. package/dist/runtime/geometry/svg.js +11 -5
  100. package/dist/runtime/gpu.d.ts +33 -0
  101. package/dist/runtime/gpu.js +218 -0
  102. package/dist/runtime/index.d.ts +4 -0
  103. package/dist/runtime/index.js +4 -0
  104. package/dist/runtime/pop/cache.d.ts +27 -0
  105. package/dist/runtime/pop/cache.js +123 -0
  106. package/dist/runtime/pop/forces.d.ts +202 -0
  107. package/dist/runtime/pop/forces.js +519 -0
  108. package/dist/runtime/pop/index.d.ts +5 -0
  109. package/dist/runtime/pop/index.js +5 -0
  110. package/dist/runtime/pop/state.d.ts +136 -0
  111. package/dist/runtime/pop/state.js +283 -0
  112. package/dist/runtime/pop/trail.d.ts +62 -0
  113. package/dist/runtime/pop/trail.js +142 -0
  114. package/dist/runtime/runtime.js +284 -9
  115. package/dist/runtime/scene/index.d.ts +50 -0
  116. package/dist/runtime/scene/index.js +94 -0
  117. package/dist/runtime/series/index.d.ts +41 -0
  118. package/dist/runtime/series/index.js +167 -0
  119. package/dist/runtime/types.d.ts +1 -1
  120. package/dist/studio/panel.d.ts +68 -1
  121. package/doc/HEADLESS_GPU.md +160 -0
  122. package/doc/NODE_REFERENCE.md +408 -1
  123. package/doc/PROJECT_AUTHORING.md +118 -20
  124. package/doc/WEB_PLAYER.md +131 -0
  125. package/package.json +25 -3
  126. package/dist/assets/geo-BAR6lQp5.js +0 -1
  127. package/dist/assets/index-BDV5xGBZ.css +0 -1
  128. package/dist/assets/index-CMcH8AgP.js +0 -212
  129. package/dist/assets/nodeTypeUtils-BBA1JqHx.js +0 -1
@@ -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
@@ -0,0 +1,165 @@
1
+ import type { NodeExecutionContext } from "@field/cascade/contracts";
2
+ /**
3
+ * `cascade.core.Time` — the clock, as a value the graph can carry.
4
+ *
5
+ * Built 2026-09-09, from Marcus scrubbing the timeline on `particle-type` and
6
+ * seeing nothing move: *"when i jump to a different time on the timeline i dont
7
+ * actually see the pop simulation animating in the viewer."*
8
+ *
9
+ * The cause was not the viewer. `cascade.pop.Simulate` takes the frame on an
10
+ * **input**, its own comment says *"drive it with `$F`"*, and **that was not
11
+ * possible**: expressions attach to props, and an input port has no expression
12
+ * field. So the sketch had a constant in there and no graph in the system could
13
+ * read the clock through a port at all.
14
+ *
15
+ * ## Why a node rather than an expression on the input
16
+ *
17
+ * Making inputs expression-bearing is the other fix, and it is the wrong one.
18
+ * An input is a *wire*, and a wire that is sometimes a formula means every
19
+ * consumer of every input has two ways to get a value — which is the shape that
20
+ * made props themselves complicated (a value, an expression, a channel, and a
21
+ * resolution order between them). Doing it again on the input side doubles that
22
+ * surface for one use.
23
+ *
24
+ * The node is also the more honest picture. Time is an input to the graph in
25
+ * the same way a camera is, and a graph that shows where the clock enters can
26
+ * be read; one where any of two hundred ports might secretly be `$F` cannot.
27
+ *
28
+ * ## Why the props carry the expressions
29
+ *
30
+ * `frame` and `time` are props with declared default expressions — `$F` and
31
+ * `$T` — which is the one mechanism that already reads the clock and updates a
32
+ * node's time dependency. Nothing here calls a clock: the node is a pure
33
+ * function of its props, and the props are where the binding lives. That keeps
34
+ * `execute` deterministic and testable, which a node reading a global clock
35
+ * would not be.
36
+ *
37
+ * They are props rather than fixed values so you can override one: pinning
38
+ * `frame` to a constant is exactly what a still render wants, and it is a
39
+ * one-field edit rather than a rewire.
40
+ */
41
+ export declare const timeDefinition: {
42
+ readonly apiVersion: 1;
43
+ readonly label: "Time";
44
+ readonly description: "The current frame and time, as values the graph can read.";
45
+ readonly icon: "Clock";
46
+ readonly runsOn: "portable";
47
+ readonly inputs: {};
48
+ readonly outputs: {
49
+ /**
50
+ * The whole frame, as an integer — Houdini's `$F`.
51
+ *
52
+ * An `int` rather than a `float`, and that is the point of having two
53
+ * outputs. Anything that **counts** frames needs an integer: a feedback
54
+ * container's step count, a sequence index, a modulo. Narrowing is never
55
+ * implicit in Cascade, so a float frame simply could not be wired to a step
56
+ * count, and the alternative — truncating silently somewhere downstream —
57
+ * is a simulation running 3 steps when the graph said 3.7.
58
+ */
59
+ readonly frame: {
60
+ readonly kind: "data";
61
+ readonly type: "int";
62
+ readonly description: "The whole frame. Houdini's $F.";
63
+ };
64
+ /** The fractional frame — Houdini's `$FF`. For anything that moves. */
65
+ readonly fframe: {
66
+ readonly kind: "data";
67
+ readonly type: "float";
68
+ readonly description: "The fractional frame, for sub-frame motion. Houdini's $FF.";
69
+ };
70
+ readonly time: {
71
+ readonly kind: "data";
72
+ readonly type: "float";
73
+ readonly description: "The playhead in seconds.";
74
+ };
75
+ };
76
+ readonly props: {
77
+ readonly frame: {
78
+ readonly type: "int";
79
+ readonly default: 1;
80
+ readonly expression: "$F";
81
+ readonly label: "Frame";
82
+ readonly description: "Bound to $F. Replace with a number to pin a still, or with any expression.";
83
+ };
84
+ readonly fframe: {
85
+ readonly type: "float";
86
+ readonly default: 1;
87
+ readonly expression: "$FF";
88
+ readonly label: "Fractional Frame";
89
+ readonly description: "Bound to $FF.";
90
+ };
91
+ readonly time: {
92
+ readonly type: "float";
93
+ readonly default: 0;
94
+ readonly expression: "$T";
95
+ readonly label: "Time";
96
+ readonly description: "Bound to $T, in seconds.";
97
+ };
98
+ };
99
+ };
100
+ export declare function executeTime(context: NodeExecutionContext<typeof timeDefinition>): void;
101
+ export declare const timeRegistration: {
102
+ kind: "definition-v1";
103
+ moduleId: string;
104
+ definition: {
105
+ readonly apiVersion: 1;
106
+ readonly label: "Time";
107
+ readonly description: "The current frame and time, as values the graph can read.";
108
+ readonly icon: "Clock";
109
+ readonly runsOn: "portable";
110
+ readonly inputs: {};
111
+ readonly outputs: {
112
+ /**
113
+ * The whole frame, as an integer — Houdini's `$F`.
114
+ *
115
+ * An `int` rather than a `float`, and that is the point of having two
116
+ * outputs. Anything that **counts** frames needs an integer: a feedback
117
+ * container's step count, a sequence index, a modulo. Narrowing is never
118
+ * implicit in Cascade, so a float frame simply could not be wired to a step
119
+ * count, and the alternative — truncating silently somewhere downstream —
120
+ * is a simulation running 3 steps when the graph said 3.7.
121
+ */
122
+ readonly frame: {
123
+ readonly kind: "data";
124
+ readonly type: "int";
125
+ readonly description: "The whole frame. Houdini's $F.";
126
+ };
127
+ /** The fractional frame — Houdini's `$FF`. For anything that moves. */
128
+ readonly fframe: {
129
+ readonly kind: "data";
130
+ readonly type: "float";
131
+ readonly description: "The fractional frame, for sub-frame motion. Houdini's $FF.";
132
+ };
133
+ readonly time: {
134
+ readonly kind: "data";
135
+ readonly type: "float";
136
+ readonly description: "The playhead in seconds.";
137
+ };
138
+ };
139
+ readonly props: {
140
+ readonly frame: {
141
+ readonly type: "int";
142
+ readonly default: 1;
143
+ readonly expression: "$F";
144
+ readonly label: "Frame";
145
+ readonly description: "Bound to $F. Replace with a number to pin a still, or with any expression.";
146
+ };
147
+ readonly fframe: {
148
+ readonly type: "float";
149
+ readonly default: 1;
150
+ readonly expression: "$FF";
151
+ readonly label: "Fractional Frame";
152
+ readonly description: "Bound to $FF.";
153
+ };
154
+ readonly time: {
155
+ readonly type: "float";
156
+ readonly default: 0;
157
+ readonly expression: "$T";
158
+ readonly label: "Time";
159
+ readonly description: "Bound to $T, in seconds.";
160
+ };
161
+ };
162
+ };
163
+ loadExecute: () => Promise<typeof executeTime>;
164
+ };
165
+ //# sourceMappingURL=time.d.ts.map
@@ -0,0 +1,110 @@
1
+ /**
2
+ * `cascade.core.Time` — the clock, as a value the graph can carry.
3
+ *
4
+ * Built 2026-09-09, from Marcus scrubbing the timeline on `particle-type` and
5
+ * seeing nothing move: *"when i jump to a different time on the timeline i dont
6
+ * actually see the pop simulation animating in the viewer."*
7
+ *
8
+ * The cause was not the viewer. `cascade.pop.Simulate` takes the frame on an
9
+ * **input**, its own comment says *"drive it with `$F`"*, and **that was not
10
+ * possible**: expressions attach to props, and an input port has no expression
11
+ * field. So the sketch had a constant in there and no graph in the system could
12
+ * read the clock through a port at all.
13
+ *
14
+ * ## Why a node rather than an expression on the input
15
+ *
16
+ * Making inputs expression-bearing is the other fix, and it is the wrong one.
17
+ * An input is a *wire*, and a wire that is sometimes a formula means every
18
+ * consumer of every input has two ways to get a value — which is the shape that
19
+ * made props themselves complicated (a value, an expression, a channel, and a
20
+ * resolution order between them). Doing it again on the input side doubles that
21
+ * surface for one use.
22
+ *
23
+ * The node is also the more honest picture. Time is an input to the graph in
24
+ * the same way a camera is, and a graph that shows where the clock enters can
25
+ * be read; one where any of two hundred ports might secretly be `$F` cannot.
26
+ *
27
+ * ## Why the props carry the expressions
28
+ *
29
+ * `frame` and `time` are props with declared default expressions — `$F` and
30
+ * `$T` — which is the one mechanism that already reads the clock and updates a
31
+ * node's time dependency. Nothing here calls a clock: the node is a pure
32
+ * function of its props, and the props are where the binding lives. That keeps
33
+ * `execute` deterministic and testable, which a node reading a global clock
34
+ * would not be.
35
+ *
36
+ * They are props rather than fixed values so you can override one: pinning
37
+ * `frame` to a constant is exactly what a still render wants, and it is a
38
+ * one-field edit rather than a rewire.
39
+ */
40
+ export const timeDefinition = {
41
+ apiVersion: 1,
42
+ label: "Time",
43
+ description: "The current frame and time, as values the graph can read.",
44
+ icon: "Clock",
45
+ runsOn: "portable",
46
+ inputs: {},
47
+ outputs: {
48
+ /**
49
+ * The whole frame, as an integer — Houdini's `$F`.
50
+ *
51
+ * An `int` rather than a `float`, and that is the point of having two
52
+ * outputs. Anything that **counts** frames needs an integer: a feedback
53
+ * container's step count, a sequence index, a modulo. Narrowing is never
54
+ * implicit in Cascade, so a float frame simply could not be wired to a step
55
+ * count, and the alternative — truncating silently somewhere downstream —
56
+ * is a simulation running 3 steps when the graph said 3.7.
57
+ */
58
+ frame: {
59
+ kind: "data",
60
+ type: "int",
61
+ description: "The whole frame. Houdini's $F.",
62
+ },
63
+ /** The fractional frame — Houdini's `$FF`. For anything that moves. */
64
+ fframe: {
65
+ kind: "data",
66
+ type: "float",
67
+ description: "The fractional frame, for sub-frame motion. Houdini's $FF.",
68
+ },
69
+ time: {
70
+ kind: "data",
71
+ type: "float",
72
+ description: "The playhead in seconds.",
73
+ },
74
+ },
75
+ props: {
76
+ frame: {
77
+ type: "int",
78
+ default: 1,
79
+ expression: "$F",
80
+ label: "Frame",
81
+ description: "Bound to $F. Replace with a number to pin a still, or with any expression.",
82
+ },
83
+ fframe: {
84
+ type: "float",
85
+ default: 1,
86
+ expression: "$FF",
87
+ label: "Fractional Frame",
88
+ description: "Bound to $FF.",
89
+ },
90
+ time: {
91
+ type: "float",
92
+ default: 0,
93
+ expression: "$T",
94
+ label: "Time",
95
+ description: "Bound to $T, in seconds.",
96
+ },
97
+ },
98
+ };
99
+ export function executeTime(context) {
100
+ context.outputs.frame.set(context.props.frame);
101
+ context.outputs.fframe.set(context.props.fframe);
102
+ context.outputs.time.set(context.props.time);
103
+ }
104
+ export const timeRegistration = {
105
+ kind: "definition-v1",
106
+ moduleId: "cascade.core.Time",
107
+ definition: timeDefinition,
108
+ loadExecute: async () => executeTime,
109
+ };
110
+ //# sourceMappingURL=time.js.map