@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
|
@@ -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
|