@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,2120 @@
1
+ import { type NodeExecutionContext } from "@field/cascade/contracts";
2
+ import type { DefinitionNodeRegistration } from "../../types.js";
3
+ /**
4
+ * `cascade.pop.*` - the minimum useful particle set.
5
+ *
6
+ * Step 2 of `PLAN particles.md`, and the step that carries the risk: a source,
7
+ * a solver, and forces, on **re-simulation only**. There is no cache. The plan
8
+ * argues for that order - re-simulation from the start frame is the definition
9
+ * of correctness, and a cache is an accelerator holding nothing re-simulation
10
+ * would not have produced, so it is built second and can be cleared at any
11
+ * time without changing a result.
12
+ *
13
+ * The arithmetic lives in `packages/runtime/src/pop`, deliberately: these
14
+ * nodes are thin, the risk is in the stepping, and the stepping is testable
15
+ * without a graph.
16
+ */
17
+ declare const sourceDefinition: {
18
+ readonly apiVersion: 1;
19
+ readonly label: "POP Source";
20
+ readonly description: "Birth particles from a geometry's points.";
21
+ readonly icon: "Sparkles";
22
+ readonly runsOn: "portable";
23
+ readonly inputs: {
24
+ readonly geometry: {
25
+ readonly kind: "data";
26
+ readonly type: "geometry";
27
+ };
28
+ readonly particles: {
29
+ readonly kind: "data";
30
+ readonly type: "geometry";
31
+ };
32
+ };
33
+ readonly outputs: {
34
+ readonly geometry: {
35
+ readonly kind: "data";
36
+ readonly type: "geometry";
37
+ };
38
+ };
39
+ readonly props: {
40
+ readonly impulse: {
41
+ readonly type: "int";
42
+ readonly default: 8;
43
+ readonly min: 0;
44
+ readonly max: 100000;
45
+ };
46
+ readonly life: {
47
+ readonly type: "float";
48
+ readonly default: 3;
49
+ readonly min: 0;
50
+ readonly max: 3600;
51
+ readonly step: 0.1;
52
+ };
53
+ readonly lifevar: {
54
+ readonly type: "float";
55
+ readonly default: 0.3;
56
+ readonly min: 0;
57
+ readonly max: 1;
58
+ readonly step: 0.01;
59
+ };
60
+ readonly velocity: {
61
+ readonly type: "vec2";
62
+ readonly default: readonly [0, 0];
63
+ };
64
+ readonly seed: {
65
+ readonly type: "int";
66
+ readonly default: 7;
67
+ readonly min: 0;
68
+ readonly max: 999999;
69
+ };
70
+ };
71
+ };
72
+ export declare function executeSource(context: NodeExecutionContext<typeof sourceDefinition>): void;
73
+ declare const solverDefinition: {
74
+ readonly apiVersion: 1;
75
+ readonly label: "POP Solver";
76
+ readonly description: "Advance a particle system by one timestep.";
77
+ readonly icon: "Play";
78
+ readonly runsOn: "portable";
79
+ readonly inputs: {
80
+ readonly particles: {
81
+ readonly kind: "data";
82
+ readonly type: "geometry";
83
+ };
84
+ };
85
+ readonly outputs: {
86
+ readonly geometry: {
87
+ readonly kind: "data";
88
+ readonly type: "geometry";
89
+ };
90
+ };
91
+ readonly props: {
92
+ /**
93
+ * Where the timestep comes from. Marcus's ruling, 2026-09-09: *"Let the
94
+ * user choose nodes fixed or variable."*
95
+ *
96
+ * `fixed` is the default on the ordinary grounds that the safer of two
97
+ * behaviours is the one you get without asking - a simulation whose result
98
+ * depends on a playback setting is not reproducible from the document
99
+ * alone. With `fps` the render rate becomes part of what determines the
100
+ * output, which is correct for a mode called variable and must not be
101
+ * silent: it belongs in the cache key, and `cascade check` should say so.
102
+ */
103
+ readonly timestep_mode: {
104
+ readonly type: "string";
105
+ readonly default: "fixed";
106
+ readonly control: "select";
107
+ readonly options: readonly [{
108
+ readonly value: "fixed";
109
+ readonly label: "Fixed - reproducible from the document";
110
+ }, {
111
+ readonly value: "fps";
112
+ readonly label: "Variable - derived from the render rate";
113
+ }];
114
+ };
115
+ readonly timestep: {
116
+ readonly type: "float";
117
+ readonly default: number;
118
+ readonly min: number;
119
+ readonly max: 1;
120
+ readonly step: number;
121
+ };
122
+ readonly maxspeed: {
123
+ readonly type: "float";
124
+ readonly default: 0;
125
+ readonly min: 0;
126
+ readonly max: 10000;
127
+ readonly step: 0.5;
128
+ };
129
+ readonly force: {
130
+ readonly type: "vec2";
131
+ readonly default: readonly [0, 0];
132
+ };
133
+ readonly airresist: {
134
+ readonly type: "float";
135
+ readonly default: 0;
136
+ readonly min: 0;
137
+ readonly max: 20;
138
+ readonly step: 0.05;
139
+ };
140
+ };
141
+ };
142
+ export declare function executeSolver(context: NodeExecutionContext<typeof solverDefinition>): void;
143
+ declare const noiseForceDefinition: {
144
+ readonly apiVersion: 1;
145
+ readonly label: "POP Noise Force";
146
+ readonly description: "Push particles along a heading read from a noise field.";
147
+ readonly icon: "Wind";
148
+ readonly runsOn: "portable";
149
+ readonly inputs: {
150
+ readonly particles: {
151
+ readonly kind: "data";
152
+ readonly type: "geometry";
153
+ };
154
+ };
155
+ readonly outputs: {
156
+ readonly geometry: {
157
+ readonly kind: "data";
158
+ readonly type: "geometry";
159
+ };
160
+ };
161
+ readonly props: {
162
+ readonly amplitude: {
163
+ readonly type: "float";
164
+ readonly default: 6;
165
+ readonly min: 0;
166
+ readonly max: 500;
167
+ readonly step: 0.5;
168
+ };
169
+ readonly frequency: {
170
+ readonly type: "float";
171
+ readonly default: 0.3;
172
+ readonly min: 0.001;
173
+ readonly max: 10;
174
+ readonly step: 0.01;
175
+ };
176
+ readonly evolve: {
177
+ readonly type: "float";
178
+ readonly default: 0;
179
+ readonly min: -1000;
180
+ readonly max: 1000;
181
+ readonly step: 0.01;
182
+ };
183
+ readonly timestep: {
184
+ readonly type: "float";
185
+ readonly default: number;
186
+ readonly min: number;
187
+ readonly max: 1;
188
+ readonly step: number;
189
+ };
190
+ readonly seed: {
191
+ readonly type: "int";
192
+ readonly default: 7;
193
+ readonly min: 0;
194
+ readonly max: 999999;
195
+ };
196
+ };
197
+ };
198
+ /**
199
+ * Accumulates into `force` and moves nothing — Houdini's POP model.
200
+ *
201
+ * Changed 2026-09-09, and it is a correctness fix rather than a refinement.
202
+ * This node used to call `step()`, which integrates. Chain two force nodes
203
+ * inside a `cascade.core.Feedback` and time then advances **twice per step**:
204
+ * the particles travel further than the timestep says, reordering nodes that
205
+ * ought to commute changes the result, and nothing reports anything. That is
206
+ * unreadable from a picture, so the model has to be right rather than tested.
207
+ */
208
+ export declare function executeNoiseForce(context: NodeExecutionContext<typeof noiseForceDefinition>): void;
209
+ declare const separateDefinition: {
210
+ readonly apiVersion: 1;
211
+ readonly label: "POP Separate";
212
+ readonly description: "Push neighbours apart within a radius.";
213
+ readonly icon: "Move3d";
214
+ readonly runsOn: "portable";
215
+ readonly inputs: {
216
+ readonly particles: {
217
+ readonly kind: "data";
218
+ readonly type: "geometry";
219
+ };
220
+ };
221
+ readonly outputs: {
222
+ readonly geometry: {
223
+ readonly kind: "data";
224
+ readonly type: "geometry";
225
+ };
226
+ };
227
+ readonly props: {
228
+ readonly radius: {
229
+ readonly type: "float";
230
+ readonly default: 0.5;
231
+ readonly min: 0;
232
+ readonly max: 100;
233
+ readonly step: 0.01;
234
+ };
235
+ readonly strength: {
236
+ readonly type: "float";
237
+ readonly default: 4;
238
+ readonly min: 0;
239
+ readonly max: 500;
240
+ readonly step: 0.1;
241
+ };
242
+ readonly timestep: {
243
+ readonly type: "float";
244
+ readonly default: number;
245
+ readonly min: number;
246
+ readonly max: 1;
247
+ readonly step: number;
248
+ };
249
+ };
250
+ };
251
+ export declare function executeSeparate(context: NodeExecutionContext<typeof separateDefinition>): void;
252
+ /**
253
+ * `cascade.pop.FieldForce` — the flow field, as a node.
254
+ *
255
+ * Extracted from `pop.Simulate`'s `field_*` props on 2026-09-09 so that the
256
+ * force which actually makes the calligraphy is a node you can see inside the
257
+ * container, reorder, and drive from a ramp. Two components, and they are the
258
+ * whole effect: `normal` pulls along the field's `N` toward the spine of the
259
+ * type, `tangential` pushes perpendicular to it, around the outline. The
260
+ * balance between them is what turns a hairball into a stroke.
261
+ *
262
+ * `level` and `hold` are the reason the lines follow the letters rather than
263
+ * collapsing onto them: instead of climbing to the field's peak, a particle
264
+ * holds a chosen contour. Marcus's own note on wanting this: *"we want the
265
+ * lines to extend and really flow around the letters, creating an elegant
266
+ * calligraphy… not hairballs or knots."*
267
+ */
268
+ declare const fieldForceDefinition: {
269
+ readonly apiVersion: 1;
270
+ readonly label: "POP Field Force";
271
+ readonly description: "Pull particles along a field's gradient and around its contours. The calligraphy force.";
272
+ readonly icon: "Waves";
273
+ readonly runsOn: "portable";
274
+ readonly inputs: {
275
+ readonly particles: {
276
+ readonly kind: "data";
277
+ readonly type: "geometry";
278
+ };
279
+ /** The field: points carrying `N` (the gradient) and optionally `level`. */
280
+ readonly field: {
281
+ readonly kind: "data";
282
+ readonly type: "geometry";
283
+ };
284
+ };
285
+ readonly outputs: {
286
+ readonly geometry: {
287
+ readonly kind: "data";
288
+ readonly type: "geometry";
289
+ };
290
+ };
291
+ readonly props: {
292
+ readonly normal: {
293
+ readonly type: "float";
294
+ readonly default: 0;
295
+ readonly min: -100000;
296
+ readonly max: 100000;
297
+ readonly label: "Normal";
298
+ readonly description: "Along N, toward the spine. Negative pushes away.";
299
+ };
300
+ readonly tangential: {
301
+ readonly type: "float";
302
+ readonly default: 2600;
303
+ readonly min: -100000;
304
+ readonly max: 100000;
305
+ readonly label: "Tangential";
306
+ readonly description: "Perpendicular to N, around the outline. Sign picks the direction.";
307
+ };
308
+ readonly radius: {
309
+ readonly type: "float";
310
+ readonly default: 20;
311
+ readonly min: 0;
312
+ readonly max: 10000;
313
+ readonly label: "Radius";
314
+ readonly description: "How far a field sample reaches.";
315
+ };
316
+ readonly level: {
317
+ readonly type: "float";
318
+ readonly default: 0.28;
319
+ readonly min: 0;
320
+ readonly max: 1;
321
+ readonly step: 0.01;
322
+ readonly label: "Level";
323
+ readonly description: "The contour to hold, rather than climbing to the peak.";
324
+ };
325
+ readonly hold: {
326
+ readonly type: "float";
327
+ readonly default: 5200;
328
+ readonly min: 0;
329
+ readonly max: 100000;
330
+ readonly label: "Hold";
331
+ readonly description: "How hard to hold that contour.";
332
+ };
333
+ };
334
+ };
335
+ export declare function executeFieldForce(context: NodeExecutionContext<typeof fieldForceDefinition>): void;
336
+ /**
337
+ * `cascade.pop.Trail` — trails from a Feedback container's history.
338
+ *
339
+ * The node that makes the network version of the piece possible at all. A trail
340
+ * needs the *frames*, and a node downstream of a simulation sees one — which
341
+ * draws nothing. `cascade.core.Feedback` keeps a bounded history for exactly
342
+ * this, and this node consumes it.
343
+ *
344
+ * The parameter names are Houdini's Trail SOP: `result`, `length`,
345
+ * `increment`, `velocityscale`.
346
+ */
347
+ declare const trailDefinition: {
348
+ readonly apiVersion: 1;
349
+ readonly label: "POP Trail";
350
+ readonly description: "Build trails from a Feedback container's history.";
351
+ readonly icon: "Activity";
352
+ readonly runsOn: "portable";
353
+ readonly inputs: {
354
+ /** The container's `history` output: every kept step, oldest first. */
355
+ readonly history: {
356
+ readonly kind: "data";
357
+ readonly type: "array";
358
+ };
359
+ /** The current state, for `preserve` and for the newest point of each trail. */
360
+ readonly particles: {
361
+ readonly kind: "data";
362
+ readonly type: "geometry";
363
+ };
364
+ };
365
+ readonly outputs: {
366
+ readonly geometry: {
367
+ readonly kind: "data";
368
+ readonly type: "geometry";
369
+ };
370
+ };
371
+ readonly props: {
372
+ readonly result: {
373
+ readonly type: "string";
374
+ readonly default: "polylines";
375
+ readonly control: "select";
376
+ readonly options: readonly [{
377
+ readonly value: "polylines";
378
+ readonly label: "Polylines - one line per particle";
379
+ }, {
380
+ readonly value: "preserve";
381
+ readonly label: "Preserve - pass the points through";
382
+ }];
383
+ readonly label: "Result";
384
+ };
385
+ readonly length: {
386
+ readonly type: "int";
387
+ readonly default: 12;
388
+ readonly min: 1;
389
+ readonly max: 100000;
390
+ readonly label: "Length";
391
+ };
392
+ readonly increment: {
393
+ readonly type: "int";
394
+ readonly default: 1;
395
+ readonly min: 1;
396
+ readonly max: 1000;
397
+ readonly label: "Increment";
398
+ };
399
+ readonly velocityscale: {
400
+ readonly type: "float";
401
+ readonly default: 1;
402
+ readonly min: 0;
403
+ readonly max: 100;
404
+ readonly step: 0.01;
405
+ readonly label: "Velocity Scale";
406
+ };
407
+ };
408
+ };
409
+ export declare function executeTrail(context: NodeExecutionContext<typeof trailDefinition>): void;
410
+ declare const simulateDefinition: {
411
+ readonly apiVersion: 1;
412
+ readonly label: "POP Simulate";
413
+ readonly description: "Re-simulate a particle system from frame zero to this frame.";
414
+ readonly icon: "Rewind";
415
+ readonly runsOn: "portable";
416
+ readonly inputs: {
417
+ /** Where particles are born. Its points are the birth positions. */
418
+ readonly geometry: {
419
+ readonly kind: "data";
420
+ readonly type: "geometry";
421
+ };
422
+ /** The frame to simulate up to. Drive it with `$F`. An input rather than
423
+ * a prop because the whole node is a function of it, and because a node
424
+ * that read the clock itself would be the thing `AGENTS.md` forbids. */
425
+ readonly frame: {
426
+ readonly kind: "data";
427
+ readonly type: "float";
428
+ readonly default: 1;
429
+ };
430
+ /**
431
+ * Points particles are drawn toward. Houdini's POP Attract.
432
+ *
433
+ * **Geometry rather than an image, and that was a correction.** The
434
+ * original piece renders its word to a canvas, blurs it, reads the pixels
435
+ * back and takes central differences — so its target really is a raster
436
+ * field, and an `image` input was the first thing tried here. It was wrong
437
+ * for this node: decoding an image needs a capability, the media
438
+ * capability hands back a host-specific lease rather than a portable
439
+ * raster, and a POP node doing IO stops being the pure arithmetic that
440
+ * makes it cook identically in both hosts.
441
+ *
442
+ * So the rasterising belongs upstream, in whatever node knows about text
443
+ * or photographs, and what reaches the solver is a set of target points.
444
+ * `fieldForce` in `pop/forces.ts` still takes a sampled raster for a node
445
+ * that has one; this input is the composable half.
446
+ */
447
+ readonly attract: {
448
+ readonly kind: "data";
449
+ readonly type: "geometry";
450
+ };
451
+ /**
452
+ * A flow field, as points carrying `N`.
453
+ *
454
+ * Marcus's description of the original: *"a delicate balance between a
455
+ * force pulling the particles towards the centre spine of the type and
456
+ * tangentially around their outlines."* This is that field — the gradient
457
+ * of a BLURRED letterform, whose ridge runs down each stroke's middle, so
458
+ * the gradient points at the spine and its perpendicular runs along the
459
+ * outline. `field_normal` and `field_tangential` are the two halves of the
460
+ * balance.
461
+ */
462
+ readonly field: {
463
+ readonly kind: "data";
464
+ readonly type: "geometry";
465
+ };
466
+ };
467
+ readonly outputs: {
468
+ /** The particles, one point each. */
469
+ readonly geometry: {
470
+ readonly kind: "data";
471
+ readonly type: "geometry";
472
+ };
473
+ /**
474
+ * The trails, one open polyline per particle.
475
+ *
476
+ * Emitted here rather than by a standalone `Trail` node, and that is a
477
+ * consequence of re-simulation rather than a shortcut. Houdini's Trail SOP
478
+ * reads a rolling window the cook already holds, because Houdini's cook is
479
+ * stateful; Cascade's node is a pure function of its inputs, so a
480
+ * standalone `Trail` can only see the frame on its port — one frame, which
481
+ * draws nothing. This node replays every frame from zero, so it has the
482
+ * whole history for free, and a node that draws nothing would be worse than
483
+ * no node.
484
+ *
485
+ * When `CookScheduler` owns the rolling window the plan describes,
486
+ * `cascade.pop.Trail` becomes a real node and this output stays as the
487
+ * cheap path. The parameters are Houdini's either way.
488
+ */
489
+ readonly trails: {
490
+ readonly kind: "data";
491
+ readonly type: "geometry";
492
+ };
493
+ };
494
+ readonly props: {
495
+ /** Frames of history in the trails. Houdini's Trail `length`. */
496
+ readonly trail_length: {
497
+ readonly type: "int";
498
+ readonly default: 14;
499
+ readonly min: 1;
500
+ readonly max: 500;
501
+ };
502
+ /** Use every nth frame. Houdini's `increment`. */
503
+ readonly trail_increment: {
504
+ readonly type: "int";
505
+ readonly default: 1;
506
+ readonly min: 1;
507
+ readonly max: 32;
508
+ };
509
+ /**
510
+ * Re-simulation, and why this is the first implementation rather than a
511
+ * placeholder for a cached one.
512
+ *
513
+ * `PLAN particles.md`: re-simulation from the start frame is the
514
+ * *definition* of correctness, and a cache is an accelerator holding
515
+ * nothing re-simulation would not have produced. So this node is the
516
+ * reference: it reads a frame, replays every step from zero, and depends on
517
+ * no state whatsoever. Scrubbing is O(frame) and that is the honest cost —
518
+ * a checkpoint cache makes it fast later without changing a single output,
519
+ * which is exactly the property a cache should have.
520
+ */
521
+ readonly substeps: {
522
+ readonly type: "int";
523
+ readonly default: 1;
524
+ readonly min: 1;
525
+ readonly max: 32;
526
+ };
527
+ readonly timestep: {
528
+ readonly type: "float";
529
+ readonly default: number;
530
+ readonly min: number;
531
+ readonly max: 1;
532
+ readonly step: number;
533
+ };
534
+ /** Per frame, as on Source. */
535
+ readonly impulse: {
536
+ readonly type: "int";
537
+ readonly default: 6;
538
+ readonly min: 0;
539
+ readonly max: 100000;
540
+ };
541
+ readonly life: {
542
+ readonly type: "float";
543
+ readonly default: 4;
544
+ readonly min: 0;
545
+ readonly max: 3600;
546
+ readonly step: 0.1;
547
+ };
548
+ readonly lifevar: {
549
+ readonly type: "float";
550
+ readonly default: 0.4;
551
+ readonly min: 0;
552
+ readonly max: 1;
553
+ readonly step: 0.01;
554
+ };
555
+ readonly force: {
556
+ readonly type: "vec2";
557
+ readonly default: readonly [0, 0];
558
+ };
559
+ readonly airresist: {
560
+ readonly type: "float";
561
+ readonly default: 0.4;
562
+ readonly min: 0;
563
+ readonly max: 20;
564
+ readonly step: 0.05;
565
+ };
566
+ readonly noise_amplitude: {
567
+ readonly type: "float";
568
+ readonly default: 14;
569
+ readonly min: 0;
570
+ readonly max: 500;
571
+ readonly step: 0.5;
572
+ };
573
+ readonly noise_frequency: {
574
+ readonly type: "float";
575
+ readonly default: 0.02;
576
+ readonly min: 0.001;
577
+ readonly max: 10;
578
+ readonly step: 0.001;
579
+ };
580
+ /** Turns per second of field evolution. */
581
+ readonly noise_evolve: {
582
+ readonly type: "float";
583
+ readonly default: 0.08;
584
+ readonly min: -10;
585
+ readonly max: 10;
586
+ readonly step: 0.01;
587
+ };
588
+ /** Toward the spine, up the field's gradient. */
589
+ readonly field_normal: {
590
+ readonly type: "float";
591
+ readonly default: 0;
592
+ readonly min: -20000;
593
+ readonly max: 20000;
594
+ readonly step: 1;
595
+ };
596
+ /** Around the outline, perpendicular to the gradient. Sign picks which way. */
597
+ readonly field_tangential: {
598
+ readonly type: "float";
599
+ readonly default: 0;
600
+ readonly min: -20000;
601
+ readonly max: 20000;
602
+ readonly step: 1;
603
+ };
604
+ /** Field samples beyond this are ignored, and it is the grid's cell size. */
605
+ readonly field_radius: {
606
+ readonly type: "float";
607
+ readonly default: 24;
608
+ readonly min: 0.5;
609
+ readonly max: 2000;
610
+ readonly step: 0.5;
611
+ };
612
+ /**
613
+ * The contour to hold, as a field level 0..1, with `field_hold` as how
614
+ * firmly.
615
+ *
616
+ * Marcus: *"the lines flow tangentially around the letters in a bundle …
617
+ * loosely describing the type."* A bundle is a contour, so holding a level
618
+ * and letting the tangent carry particles round it is the mechanism —
619
+ * pulling them to the spine instead collapses each letter to a point, and
620
+ * pure tangential flow lets every particle converge onto one level set.
621
+ */
622
+ readonly field_level: {
623
+ readonly type: "float";
624
+ readonly default: 0.35;
625
+ readonly min: 0;
626
+ readonly max: 1;
627
+ readonly step: 0.01;
628
+ };
629
+ readonly field_hold: {
630
+ readonly type: "float";
631
+ readonly default: 0;
632
+ readonly min: 0;
633
+ readonly max: 40000;
634
+ readonly step: 10;
635
+ };
636
+ /**
637
+ * Scatter births across this box instead of on the source geometry's
638
+ * points, `[minX, minY, maxX, maxY]`. All zero uses the points.
639
+ *
640
+ * Houdini's POP Source has the same choice — *Scatter onto Surface* against
641
+ * *Points* — and Marcus's note is that the original spawns *around* the
642
+ * type rather than on it: *"The particles should spawn in the area around
643
+ * the type … and then the forces should drag them around."* Birthing on
644
+ * the letterforms puts every particle where it is already going, so
645
+ * nothing travels and the field has nothing to reveal.
646
+ */
647
+ readonly birth_area: {
648
+ readonly type: "vec4";
649
+ readonly default: readonly [0, 0, 0, 0];
650
+ };
651
+ /**
652
+ * A colour per particle, drawn at birth and fixed for its life.
653
+ *
654
+ * Hashed on the id, so the same particle gets the same colour however many
655
+ * times the frame is re-simulated — a colour drawn from a stream would
656
+ * change on every scrub, which reads as the render being unstable.
657
+ *
658
+ * `[from, to]` in turns around the wheel. Equal values give one hue; a
659
+ * narrow span is usually what a piece wants, because a full-spectrum
660
+ * random set reads as a test pattern rather than as a palette.
661
+ */
662
+ readonly birth_hue: {
663
+ readonly type: "vec2";
664
+ readonly default: readonly [0, 0];
665
+ readonly min: 0;
666
+ readonly max: 1;
667
+ readonly step: 0.01;
668
+ };
669
+ readonly birth_saturation: {
670
+ readonly type: "float";
671
+ readonly default: 0.6;
672
+ readonly min: 0;
673
+ readonly max: 1;
674
+ readonly step: 0.01;
675
+ };
676
+ readonly birth_value: {
677
+ readonly type: "float";
678
+ readonly default: 1;
679
+ readonly min: 0;
680
+ readonly max: 1;
681
+ readonly step: 0.01;
682
+ };
683
+ /** How hard the targets pull. Houdini's POP Attract strength. */
684
+ readonly attract_amplitude: {
685
+ readonly type: "float";
686
+ readonly default: 0;
687
+ readonly min: 0;
688
+ readonly max: 20000;
689
+ readonly step: 1;
690
+ };
691
+ /** Beyond this distance a target is ignored, which is what keeps the pull
692
+ * local and the cost bounded. 0 means every target pulls every particle. */
693
+ readonly attract_radius: {
694
+ readonly type: "float";
695
+ readonly default: 60;
696
+ readonly min: 0;
697
+ readonly max: 5000;
698
+ readonly step: 1;
699
+ };
700
+ readonly separate_radius: {
701
+ readonly type: "float";
702
+ readonly default: 0;
703
+ readonly min: 0;
704
+ readonly max: 500;
705
+ readonly step: 0.5;
706
+ };
707
+ readonly separate_strength: {
708
+ readonly type: "float";
709
+ readonly default: 12;
710
+ readonly min: 0;
711
+ readonly max: 500;
712
+ readonly step: 0.5;
713
+ };
714
+ readonly maxspeed: {
715
+ readonly type: "float";
716
+ readonly default: 60;
717
+ readonly min: 0;
718
+ readonly max: 10000;
719
+ readonly step: 1;
720
+ };
721
+ /** `[minX, minY, maxX, maxY]`, or all zero for open space. The original
722
+ * piece is toroidal. */
723
+ readonly wrap: {
724
+ readonly type: "vec4";
725
+ readonly default: readonly [0, 0, 0, 0];
726
+ };
727
+ readonly seed: {
728
+ readonly type: "int";
729
+ readonly default: 7;
730
+ readonly min: 0;
731
+ readonly max: 999999;
732
+ };
733
+ };
734
+ };
735
+ export declare function executeSimulate(context: NodeExecutionContext<typeof simulateDefinition>): void;
736
+ export declare const simulateRegistration: {
737
+ kind: "definition-v1";
738
+ moduleId: string;
739
+ definition: {
740
+ readonly apiVersion: 1;
741
+ readonly label: "POP Simulate";
742
+ readonly description: "Re-simulate a particle system from frame zero to this frame.";
743
+ readonly icon: "Rewind";
744
+ readonly runsOn: "portable";
745
+ readonly inputs: {
746
+ /** Where particles are born. Its points are the birth positions. */
747
+ readonly geometry: {
748
+ readonly kind: "data";
749
+ readonly type: "geometry";
750
+ };
751
+ /** The frame to simulate up to. Drive it with `$F`. An input rather than
752
+ * a prop because the whole node is a function of it, and because a node
753
+ * that read the clock itself would be the thing `AGENTS.md` forbids. */
754
+ readonly frame: {
755
+ readonly kind: "data";
756
+ readonly type: "float";
757
+ readonly default: 1;
758
+ };
759
+ /**
760
+ * Points particles are drawn toward. Houdini's POP Attract.
761
+ *
762
+ * **Geometry rather than an image, and that was a correction.** The
763
+ * original piece renders its word to a canvas, blurs it, reads the pixels
764
+ * back and takes central differences — so its target really is a raster
765
+ * field, and an `image` input was the first thing tried here. It was wrong
766
+ * for this node: decoding an image needs a capability, the media
767
+ * capability hands back a host-specific lease rather than a portable
768
+ * raster, and a POP node doing IO stops being the pure arithmetic that
769
+ * makes it cook identically in both hosts.
770
+ *
771
+ * So the rasterising belongs upstream, in whatever node knows about text
772
+ * or photographs, and what reaches the solver is a set of target points.
773
+ * `fieldForce` in `pop/forces.ts` still takes a sampled raster for a node
774
+ * that has one; this input is the composable half.
775
+ */
776
+ readonly attract: {
777
+ readonly kind: "data";
778
+ readonly type: "geometry";
779
+ };
780
+ /**
781
+ * A flow field, as points carrying `N`.
782
+ *
783
+ * Marcus's description of the original: *"a delicate balance between a
784
+ * force pulling the particles towards the centre spine of the type and
785
+ * tangentially around their outlines."* This is that field — the gradient
786
+ * of a BLURRED letterform, whose ridge runs down each stroke's middle, so
787
+ * the gradient points at the spine and its perpendicular runs along the
788
+ * outline. `field_normal` and `field_tangential` are the two halves of the
789
+ * balance.
790
+ */
791
+ readonly field: {
792
+ readonly kind: "data";
793
+ readonly type: "geometry";
794
+ };
795
+ };
796
+ readonly outputs: {
797
+ /** The particles, one point each. */
798
+ readonly geometry: {
799
+ readonly kind: "data";
800
+ readonly type: "geometry";
801
+ };
802
+ /**
803
+ * The trails, one open polyline per particle.
804
+ *
805
+ * Emitted here rather than by a standalone `Trail` node, and that is a
806
+ * consequence of re-simulation rather than a shortcut. Houdini's Trail SOP
807
+ * reads a rolling window the cook already holds, because Houdini's cook is
808
+ * stateful; Cascade's node is a pure function of its inputs, so a
809
+ * standalone `Trail` can only see the frame on its port — one frame, which
810
+ * draws nothing. This node replays every frame from zero, so it has the
811
+ * whole history for free, and a node that draws nothing would be worse than
812
+ * no node.
813
+ *
814
+ * When `CookScheduler` owns the rolling window the plan describes,
815
+ * `cascade.pop.Trail` becomes a real node and this output stays as the
816
+ * cheap path. The parameters are Houdini's either way.
817
+ */
818
+ readonly trails: {
819
+ readonly kind: "data";
820
+ readonly type: "geometry";
821
+ };
822
+ };
823
+ readonly props: {
824
+ /** Frames of history in the trails. Houdini's Trail `length`. */
825
+ readonly trail_length: {
826
+ readonly type: "int";
827
+ readonly default: 14;
828
+ readonly min: 1;
829
+ readonly max: 500;
830
+ };
831
+ /** Use every nth frame. Houdini's `increment`. */
832
+ readonly trail_increment: {
833
+ readonly type: "int";
834
+ readonly default: 1;
835
+ readonly min: 1;
836
+ readonly max: 32;
837
+ };
838
+ /**
839
+ * Re-simulation, and why this is the first implementation rather than a
840
+ * placeholder for a cached one.
841
+ *
842
+ * `PLAN particles.md`: re-simulation from the start frame is the
843
+ * *definition* of correctness, and a cache is an accelerator holding
844
+ * nothing re-simulation would not have produced. So this node is the
845
+ * reference: it reads a frame, replays every step from zero, and depends on
846
+ * no state whatsoever. Scrubbing is O(frame) and that is the honest cost —
847
+ * a checkpoint cache makes it fast later without changing a single output,
848
+ * which is exactly the property a cache should have.
849
+ */
850
+ readonly substeps: {
851
+ readonly type: "int";
852
+ readonly default: 1;
853
+ readonly min: 1;
854
+ readonly max: 32;
855
+ };
856
+ readonly timestep: {
857
+ readonly type: "float";
858
+ readonly default: number;
859
+ readonly min: number;
860
+ readonly max: 1;
861
+ readonly step: number;
862
+ };
863
+ /** Per frame, as on Source. */
864
+ readonly impulse: {
865
+ readonly type: "int";
866
+ readonly default: 6;
867
+ readonly min: 0;
868
+ readonly max: 100000;
869
+ };
870
+ readonly life: {
871
+ readonly type: "float";
872
+ readonly default: 4;
873
+ readonly min: 0;
874
+ readonly max: 3600;
875
+ readonly step: 0.1;
876
+ };
877
+ readonly lifevar: {
878
+ readonly type: "float";
879
+ readonly default: 0.4;
880
+ readonly min: 0;
881
+ readonly max: 1;
882
+ readonly step: 0.01;
883
+ };
884
+ readonly force: {
885
+ readonly type: "vec2";
886
+ readonly default: readonly [0, 0];
887
+ };
888
+ readonly airresist: {
889
+ readonly type: "float";
890
+ readonly default: 0.4;
891
+ readonly min: 0;
892
+ readonly max: 20;
893
+ readonly step: 0.05;
894
+ };
895
+ readonly noise_amplitude: {
896
+ readonly type: "float";
897
+ readonly default: 14;
898
+ readonly min: 0;
899
+ readonly max: 500;
900
+ readonly step: 0.5;
901
+ };
902
+ readonly noise_frequency: {
903
+ readonly type: "float";
904
+ readonly default: 0.02;
905
+ readonly min: 0.001;
906
+ readonly max: 10;
907
+ readonly step: 0.001;
908
+ };
909
+ /** Turns per second of field evolution. */
910
+ readonly noise_evolve: {
911
+ readonly type: "float";
912
+ readonly default: 0.08;
913
+ readonly min: -10;
914
+ readonly max: 10;
915
+ readonly step: 0.01;
916
+ };
917
+ /** Toward the spine, up the field's gradient. */
918
+ readonly field_normal: {
919
+ readonly type: "float";
920
+ readonly default: 0;
921
+ readonly min: -20000;
922
+ readonly max: 20000;
923
+ readonly step: 1;
924
+ };
925
+ /** Around the outline, perpendicular to the gradient. Sign picks which way. */
926
+ readonly field_tangential: {
927
+ readonly type: "float";
928
+ readonly default: 0;
929
+ readonly min: -20000;
930
+ readonly max: 20000;
931
+ readonly step: 1;
932
+ };
933
+ /** Field samples beyond this are ignored, and it is the grid's cell size. */
934
+ readonly field_radius: {
935
+ readonly type: "float";
936
+ readonly default: 24;
937
+ readonly min: 0.5;
938
+ readonly max: 2000;
939
+ readonly step: 0.5;
940
+ };
941
+ /**
942
+ * The contour to hold, as a field level 0..1, with `field_hold` as how
943
+ * firmly.
944
+ *
945
+ * Marcus: *"the lines flow tangentially around the letters in a bundle …
946
+ * loosely describing the type."* A bundle is a contour, so holding a level
947
+ * and letting the tangent carry particles round it is the mechanism —
948
+ * pulling them to the spine instead collapses each letter to a point, and
949
+ * pure tangential flow lets every particle converge onto one level set.
950
+ */
951
+ readonly field_level: {
952
+ readonly type: "float";
953
+ readonly default: 0.35;
954
+ readonly min: 0;
955
+ readonly max: 1;
956
+ readonly step: 0.01;
957
+ };
958
+ readonly field_hold: {
959
+ readonly type: "float";
960
+ readonly default: 0;
961
+ readonly min: 0;
962
+ readonly max: 40000;
963
+ readonly step: 10;
964
+ };
965
+ /**
966
+ * Scatter births across this box instead of on the source geometry's
967
+ * points, `[minX, minY, maxX, maxY]`. All zero uses the points.
968
+ *
969
+ * Houdini's POP Source has the same choice — *Scatter onto Surface* against
970
+ * *Points* — and Marcus's note is that the original spawns *around* the
971
+ * type rather than on it: *"The particles should spawn in the area around
972
+ * the type … and then the forces should drag them around."* Birthing on
973
+ * the letterforms puts every particle where it is already going, so
974
+ * nothing travels and the field has nothing to reveal.
975
+ */
976
+ readonly birth_area: {
977
+ readonly type: "vec4";
978
+ readonly default: readonly [0, 0, 0, 0];
979
+ };
980
+ /**
981
+ * A colour per particle, drawn at birth and fixed for its life.
982
+ *
983
+ * Hashed on the id, so the same particle gets the same colour however many
984
+ * times the frame is re-simulated — a colour drawn from a stream would
985
+ * change on every scrub, which reads as the render being unstable.
986
+ *
987
+ * `[from, to]` in turns around the wheel. Equal values give one hue; a
988
+ * narrow span is usually what a piece wants, because a full-spectrum
989
+ * random set reads as a test pattern rather than as a palette.
990
+ */
991
+ readonly birth_hue: {
992
+ readonly type: "vec2";
993
+ readonly default: readonly [0, 0];
994
+ readonly min: 0;
995
+ readonly max: 1;
996
+ readonly step: 0.01;
997
+ };
998
+ readonly birth_saturation: {
999
+ readonly type: "float";
1000
+ readonly default: 0.6;
1001
+ readonly min: 0;
1002
+ readonly max: 1;
1003
+ readonly step: 0.01;
1004
+ };
1005
+ readonly birth_value: {
1006
+ readonly type: "float";
1007
+ readonly default: 1;
1008
+ readonly min: 0;
1009
+ readonly max: 1;
1010
+ readonly step: 0.01;
1011
+ };
1012
+ /** How hard the targets pull. Houdini's POP Attract strength. */
1013
+ readonly attract_amplitude: {
1014
+ readonly type: "float";
1015
+ readonly default: 0;
1016
+ readonly min: 0;
1017
+ readonly max: 20000;
1018
+ readonly step: 1;
1019
+ };
1020
+ /** Beyond this distance a target is ignored, which is what keeps the pull
1021
+ * local and the cost bounded. 0 means every target pulls every particle. */
1022
+ readonly attract_radius: {
1023
+ readonly type: "float";
1024
+ readonly default: 60;
1025
+ readonly min: 0;
1026
+ readonly max: 5000;
1027
+ readonly step: 1;
1028
+ };
1029
+ readonly separate_radius: {
1030
+ readonly type: "float";
1031
+ readonly default: 0;
1032
+ readonly min: 0;
1033
+ readonly max: 500;
1034
+ readonly step: 0.5;
1035
+ };
1036
+ readonly separate_strength: {
1037
+ readonly type: "float";
1038
+ readonly default: 12;
1039
+ readonly min: 0;
1040
+ readonly max: 500;
1041
+ readonly step: 0.5;
1042
+ };
1043
+ readonly maxspeed: {
1044
+ readonly type: "float";
1045
+ readonly default: 60;
1046
+ readonly min: 0;
1047
+ readonly max: 10000;
1048
+ readonly step: 1;
1049
+ };
1050
+ /** `[minX, minY, maxX, maxY]`, or all zero for open space. The original
1051
+ * piece is toroidal. */
1052
+ readonly wrap: {
1053
+ readonly type: "vec4";
1054
+ readonly default: readonly [0, 0, 0, 0];
1055
+ };
1056
+ readonly seed: {
1057
+ readonly type: "int";
1058
+ readonly default: 7;
1059
+ readonly min: 0;
1060
+ readonly max: 999999;
1061
+ };
1062
+ };
1063
+ };
1064
+ loadExecute: () => Promise<typeof executeSimulate>;
1065
+ };
1066
+ export declare const sourceRegistration: {
1067
+ kind: "definition-v1";
1068
+ moduleId: string;
1069
+ definition: {
1070
+ readonly apiVersion: 1;
1071
+ readonly label: "POP Source";
1072
+ readonly description: "Birth particles from a geometry's points.";
1073
+ readonly icon: "Sparkles";
1074
+ readonly runsOn: "portable";
1075
+ readonly inputs: {
1076
+ readonly geometry: {
1077
+ readonly kind: "data";
1078
+ readonly type: "geometry";
1079
+ };
1080
+ readonly particles: {
1081
+ readonly kind: "data";
1082
+ readonly type: "geometry";
1083
+ };
1084
+ };
1085
+ readonly outputs: {
1086
+ readonly geometry: {
1087
+ readonly kind: "data";
1088
+ readonly type: "geometry";
1089
+ };
1090
+ };
1091
+ readonly props: {
1092
+ readonly impulse: {
1093
+ readonly type: "int";
1094
+ readonly default: 8;
1095
+ readonly min: 0;
1096
+ readonly max: 100000;
1097
+ };
1098
+ readonly life: {
1099
+ readonly type: "float";
1100
+ readonly default: 3;
1101
+ readonly min: 0;
1102
+ readonly max: 3600;
1103
+ readonly step: 0.1;
1104
+ };
1105
+ readonly lifevar: {
1106
+ readonly type: "float";
1107
+ readonly default: 0.3;
1108
+ readonly min: 0;
1109
+ readonly max: 1;
1110
+ readonly step: 0.01;
1111
+ };
1112
+ readonly velocity: {
1113
+ readonly type: "vec2";
1114
+ readonly default: readonly [0, 0];
1115
+ };
1116
+ readonly seed: {
1117
+ readonly type: "int";
1118
+ readonly default: 7;
1119
+ readonly min: 0;
1120
+ readonly max: 999999;
1121
+ };
1122
+ };
1123
+ };
1124
+ loadExecute: () => Promise<typeof executeSource>;
1125
+ };
1126
+ export declare const solverRegistration: {
1127
+ kind: "definition-v1";
1128
+ moduleId: string;
1129
+ definition: {
1130
+ readonly apiVersion: 1;
1131
+ readonly label: "POP Solver";
1132
+ readonly description: "Advance a particle system by one timestep.";
1133
+ readonly icon: "Play";
1134
+ readonly runsOn: "portable";
1135
+ readonly inputs: {
1136
+ readonly particles: {
1137
+ readonly kind: "data";
1138
+ readonly type: "geometry";
1139
+ };
1140
+ };
1141
+ readonly outputs: {
1142
+ readonly geometry: {
1143
+ readonly kind: "data";
1144
+ readonly type: "geometry";
1145
+ };
1146
+ };
1147
+ readonly props: {
1148
+ /**
1149
+ * Where the timestep comes from. Marcus's ruling, 2026-09-09: *"Let the
1150
+ * user choose nodes fixed or variable."*
1151
+ *
1152
+ * `fixed` is the default on the ordinary grounds that the safer of two
1153
+ * behaviours is the one you get without asking - a simulation whose result
1154
+ * depends on a playback setting is not reproducible from the document
1155
+ * alone. With `fps` the render rate becomes part of what determines the
1156
+ * output, which is correct for a mode called variable and must not be
1157
+ * silent: it belongs in the cache key, and `cascade check` should say so.
1158
+ */
1159
+ readonly timestep_mode: {
1160
+ readonly type: "string";
1161
+ readonly default: "fixed";
1162
+ readonly control: "select";
1163
+ readonly options: readonly [{
1164
+ readonly value: "fixed";
1165
+ readonly label: "Fixed - reproducible from the document";
1166
+ }, {
1167
+ readonly value: "fps";
1168
+ readonly label: "Variable - derived from the render rate";
1169
+ }];
1170
+ };
1171
+ readonly timestep: {
1172
+ readonly type: "float";
1173
+ readonly default: number;
1174
+ readonly min: number;
1175
+ readonly max: 1;
1176
+ readonly step: number;
1177
+ };
1178
+ readonly maxspeed: {
1179
+ readonly type: "float";
1180
+ readonly default: 0;
1181
+ readonly min: 0;
1182
+ readonly max: 10000;
1183
+ readonly step: 0.5;
1184
+ };
1185
+ readonly force: {
1186
+ readonly type: "vec2";
1187
+ readonly default: readonly [0, 0];
1188
+ };
1189
+ readonly airresist: {
1190
+ readonly type: "float";
1191
+ readonly default: 0;
1192
+ readonly min: 0;
1193
+ readonly max: 20;
1194
+ readonly step: 0.05;
1195
+ };
1196
+ };
1197
+ };
1198
+ loadExecute: () => Promise<typeof executeSolver>;
1199
+ };
1200
+ export declare const noiseForceRegistration: {
1201
+ kind: "definition-v1";
1202
+ moduleId: string;
1203
+ definition: {
1204
+ readonly apiVersion: 1;
1205
+ readonly label: "POP Noise Force";
1206
+ readonly description: "Push particles along a heading read from a noise field.";
1207
+ readonly icon: "Wind";
1208
+ readonly runsOn: "portable";
1209
+ readonly inputs: {
1210
+ readonly particles: {
1211
+ readonly kind: "data";
1212
+ readonly type: "geometry";
1213
+ };
1214
+ };
1215
+ readonly outputs: {
1216
+ readonly geometry: {
1217
+ readonly kind: "data";
1218
+ readonly type: "geometry";
1219
+ };
1220
+ };
1221
+ readonly props: {
1222
+ readonly amplitude: {
1223
+ readonly type: "float";
1224
+ readonly default: 6;
1225
+ readonly min: 0;
1226
+ readonly max: 500;
1227
+ readonly step: 0.5;
1228
+ };
1229
+ readonly frequency: {
1230
+ readonly type: "float";
1231
+ readonly default: 0.3;
1232
+ readonly min: 0.001;
1233
+ readonly max: 10;
1234
+ readonly step: 0.01;
1235
+ };
1236
+ readonly evolve: {
1237
+ readonly type: "float";
1238
+ readonly default: 0;
1239
+ readonly min: -1000;
1240
+ readonly max: 1000;
1241
+ readonly step: 0.01;
1242
+ };
1243
+ readonly timestep: {
1244
+ readonly type: "float";
1245
+ readonly default: number;
1246
+ readonly min: number;
1247
+ readonly max: 1;
1248
+ readonly step: number;
1249
+ };
1250
+ readonly seed: {
1251
+ readonly type: "int";
1252
+ readonly default: 7;
1253
+ readonly min: 0;
1254
+ readonly max: 999999;
1255
+ };
1256
+ };
1257
+ };
1258
+ loadExecute: () => Promise<typeof executeNoiseForce>;
1259
+ };
1260
+ export declare const separateRegistration: {
1261
+ kind: "definition-v1";
1262
+ moduleId: string;
1263
+ definition: {
1264
+ readonly apiVersion: 1;
1265
+ readonly label: "POP Separate";
1266
+ readonly description: "Push neighbours apart within a radius.";
1267
+ readonly icon: "Move3d";
1268
+ readonly runsOn: "portable";
1269
+ readonly inputs: {
1270
+ readonly particles: {
1271
+ readonly kind: "data";
1272
+ readonly type: "geometry";
1273
+ };
1274
+ };
1275
+ readonly outputs: {
1276
+ readonly geometry: {
1277
+ readonly kind: "data";
1278
+ readonly type: "geometry";
1279
+ };
1280
+ };
1281
+ readonly props: {
1282
+ readonly radius: {
1283
+ readonly type: "float";
1284
+ readonly default: 0.5;
1285
+ readonly min: 0;
1286
+ readonly max: 100;
1287
+ readonly step: 0.01;
1288
+ };
1289
+ readonly strength: {
1290
+ readonly type: "float";
1291
+ readonly default: 4;
1292
+ readonly min: 0;
1293
+ readonly max: 500;
1294
+ readonly step: 0.1;
1295
+ };
1296
+ readonly timestep: {
1297
+ readonly type: "float";
1298
+ readonly default: number;
1299
+ readonly min: number;
1300
+ readonly max: 1;
1301
+ readonly step: number;
1302
+ };
1303
+ };
1304
+ };
1305
+ loadExecute: () => Promise<typeof executeSeparate>;
1306
+ };
1307
+ export declare const fieldForceRegistration: {
1308
+ kind: "definition-v1";
1309
+ moduleId: string;
1310
+ definition: {
1311
+ readonly apiVersion: 1;
1312
+ readonly label: "POP Field Force";
1313
+ readonly description: "Pull particles along a field's gradient and around its contours. The calligraphy force.";
1314
+ readonly icon: "Waves";
1315
+ readonly runsOn: "portable";
1316
+ readonly inputs: {
1317
+ readonly particles: {
1318
+ readonly kind: "data";
1319
+ readonly type: "geometry";
1320
+ };
1321
+ /** The field: points carrying `N` (the gradient) and optionally `level`. */
1322
+ readonly field: {
1323
+ readonly kind: "data";
1324
+ readonly type: "geometry";
1325
+ };
1326
+ };
1327
+ readonly outputs: {
1328
+ readonly geometry: {
1329
+ readonly kind: "data";
1330
+ readonly type: "geometry";
1331
+ };
1332
+ };
1333
+ readonly props: {
1334
+ readonly normal: {
1335
+ readonly type: "float";
1336
+ readonly default: 0;
1337
+ readonly min: -100000;
1338
+ readonly max: 100000;
1339
+ readonly label: "Normal";
1340
+ readonly description: "Along N, toward the spine. Negative pushes away.";
1341
+ };
1342
+ readonly tangential: {
1343
+ readonly type: "float";
1344
+ readonly default: 2600;
1345
+ readonly min: -100000;
1346
+ readonly max: 100000;
1347
+ readonly label: "Tangential";
1348
+ readonly description: "Perpendicular to N, around the outline. Sign picks the direction.";
1349
+ };
1350
+ readonly radius: {
1351
+ readonly type: "float";
1352
+ readonly default: 20;
1353
+ readonly min: 0;
1354
+ readonly max: 10000;
1355
+ readonly label: "Radius";
1356
+ readonly description: "How far a field sample reaches.";
1357
+ };
1358
+ readonly level: {
1359
+ readonly type: "float";
1360
+ readonly default: 0.28;
1361
+ readonly min: 0;
1362
+ readonly max: 1;
1363
+ readonly step: 0.01;
1364
+ readonly label: "Level";
1365
+ readonly description: "The contour to hold, rather than climbing to the peak.";
1366
+ };
1367
+ readonly hold: {
1368
+ readonly type: "float";
1369
+ readonly default: 5200;
1370
+ readonly min: 0;
1371
+ readonly max: 100000;
1372
+ readonly label: "Hold";
1373
+ readonly description: "How hard to hold that contour.";
1374
+ };
1375
+ };
1376
+ };
1377
+ loadExecute: () => Promise<typeof executeFieldForce>;
1378
+ };
1379
+ export declare const trailRegistration: {
1380
+ kind: "definition-v1";
1381
+ moduleId: string;
1382
+ definition: {
1383
+ readonly apiVersion: 1;
1384
+ readonly label: "POP Trail";
1385
+ readonly description: "Build trails from a Feedback container's history.";
1386
+ readonly icon: "Activity";
1387
+ readonly runsOn: "portable";
1388
+ readonly inputs: {
1389
+ /** The container's `history` output: every kept step, oldest first. */
1390
+ readonly history: {
1391
+ readonly kind: "data";
1392
+ readonly type: "array";
1393
+ };
1394
+ /** The current state, for `preserve` and for the newest point of each trail. */
1395
+ readonly particles: {
1396
+ readonly kind: "data";
1397
+ readonly type: "geometry";
1398
+ };
1399
+ };
1400
+ readonly outputs: {
1401
+ readonly geometry: {
1402
+ readonly kind: "data";
1403
+ readonly type: "geometry";
1404
+ };
1405
+ };
1406
+ readonly props: {
1407
+ readonly result: {
1408
+ readonly type: "string";
1409
+ readonly default: "polylines";
1410
+ readonly control: "select";
1411
+ readonly options: readonly [{
1412
+ readonly value: "polylines";
1413
+ readonly label: "Polylines - one line per particle";
1414
+ }, {
1415
+ readonly value: "preserve";
1416
+ readonly label: "Preserve - pass the points through";
1417
+ }];
1418
+ readonly label: "Result";
1419
+ };
1420
+ readonly length: {
1421
+ readonly type: "int";
1422
+ readonly default: 12;
1423
+ readonly min: 1;
1424
+ readonly max: 100000;
1425
+ readonly label: "Length";
1426
+ };
1427
+ readonly increment: {
1428
+ readonly type: "int";
1429
+ readonly default: 1;
1430
+ readonly min: 1;
1431
+ readonly max: 1000;
1432
+ readonly label: "Increment";
1433
+ };
1434
+ readonly velocityscale: {
1435
+ readonly type: "float";
1436
+ readonly default: 1;
1437
+ readonly min: 0;
1438
+ readonly max: 100;
1439
+ readonly step: 0.01;
1440
+ readonly label: "Velocity Scale";
1441
+ };
1442
+ };
1443
+ };
1444
+ loadExecute: () => Promise<typeof executeTrail>;
1445
+ };
1446
+ export declare const popNodeRegistrations: readonly DefinitionNodeRegistration[];
1447
+ /** `[moduleId, definition]` pairs, for a host building a node palette. The
1448
+ * same shape `geoNodeDefinitions` has, so Studio's library builder needs no
1449
+ * second branch. */
1450
+ export declare const popNodeDefinitions: readonly [readonly ["cascade.pop.Simulate", {
1451
+ readonly apiVersion: 1;
1452
+ readonly label: "POP Simulate";
1453
+ readonly description: "Re-simulate a particle system from frame zero to this frame.";
1454
+ readonly icon: "Rewind";
1455
+ readonly runsOn: "portable";
1456
+ readonly inputs: {
1457
+ /** Where particles are born. Its points are the birth positions. */
1458
+ readonly geometry: {
1459
+ readonly kind: "data";
1460
+ readonly type: "geometry";
1461
+ };
1462
+ /** The frame to simulate up to. Drive it with `$F`. An input rather than
1463
+ * a prop because the whole node is a function of it, and because a node
1464
+ * that read the clock itself would be the thing `AGENTS.md` forbids. */
1465
+ readonly frame: {
1466
+ readonly kind: "data";
1467
+ readonly type: "float";
1468
+ readonly default: 1;
1469
+ };
1470
+ /**
1471
+ * Points particles are drawn toward. Houdini's POP Attract.
1472
+ *
1473
+ * **Geometry rather than an image, and that was a correction.** The
1474
+ * original piece renders its word to a canvas, blurs it, reads the pixels
1475
+ * back and takes central differences — so its target really is a raster
1476
+ * field, and an `image` input was the first thing tried here. It was wrong
1477
+ * for this node: decoding an image needs a capability, the media
1478
+ * capability hands back a host-specific lease rather than a portable
1479
+ * raster, and a POP node doing IO stops being the pure arithmetic that
1480
+ * makes it cook identically in both hosts.
1481
+ *
1482
+ * So the rasterising belongs upstream, in whatever node knows about text
1483
+ * or photographs, and what reaches the solver is a set of target points.
1484
+ * `fieldForce` in `pop/forces.ts` still takes a sampled raster for a node
1485
+ * that has one; this input is the composable half.
1486
+ */
1487
+ readonly attract: {
1488
+ readonly kind: "data";
1489
+ readonly type: "geometry";
1490
+ };
1491
+ /**
1492
+ * A flow field, as points carrying `N`.
1493
+ *
1494
+ * Marcus's description of the original: *"a delicate balance between a
1495
+ * force pulling the particles towards the centre spine of the type and
1496
+ * tangentially around their outlines."* This is that field — the gradient
1497
+ * of a BLURRED letterform, whose ridge runs down each stroke's middle, so
1498
+ * the gradient points at the spine and its perpendicular runs along the
1499
+ * outline. `field_normal` and `field_tangential` are the two halves of the
1500
+ * balance.
1501
+ */
1502
+ readonly field: {
1503
+ readonly kind: "data";
1504
+ readonly type: "geometry";
1505
+ };
1506
+ };
1507
+ readonly outputs: {
1508
+ /** The particles, one point each. */
1509
+ readonly geometry: {
1510
+ readonly kind: "data";
1511
+ readonly type: "geometry";
1512
+ };
1513
+ /**
1514
+ * The trails, one open polyline per particle.
1515
+ *
1516
+ * Emitted here rather than by a standalone `Trail` node, and that is a
1517
+ * consequence of re-simulation rather than a shortcut. Houdini's Trail SOP
1518
+ * reads a rolling window the cook already holds, because Houdini's cook is
1519
+ * stateful; Cascade's node is a pure function of its inputs, so a
1520
+ * standalone `Trail` can only see the frame on its port — one frame, which
1521
+ * draws nothing. This node replays every frame from zero, so it has the
1522
+ * whole history for free, and a node that draws nothing would be worse than
1523
+ * no node.
1524
+ *
1525
+ * When `CookScheduler` owns the rolling window the plan describes,
1526
+ * `cascade.pop.Trail` becomes a real node and this output stays as the
1527
+ * cheap path. The parameters are Houdini's either way.
1528
+ */
1529
+ readonly trails: {
1530
+ readonly kind: "data";
1531
+ readonly type: "geometry";
1532
+ };
1533
+ };
1534
+ readonly props: {
1535
+ /** Frames of history in the trails. Houdini's Trail `length`. */
1536
+ readonly trail_length: {
1537
+ readonly type: "int";
1538
+ readonly default: 14;
1539
+ readonly min: 1;
1540
+ readonly max: 500;
1541
+ };
1542
+ /** Use every nth frame. Houdini's `increment`. */
1543
+ readonly trail_increment: {
1544
+ readonly type: "int";
1545
+ readonly default: 1;
1546
+ readonly min: 1;
1547
+ readonly max: 32;
1548
+ };
1549
+ /**
1550
+ * Re-simulation, and why this is the first implementation rather than a
1551
+ * placeholder for a cached one.
1552
+ *
1553
+ * `PLAN particles.md`: re-simulation from the start frame is the
1554
+ * *definition* of correctness, and a cache is an accelerator holding
1555
+ * nothing re-simulation would not have produced. So this node is the
1556
+ * reference: it reads a frame, replays every step from zero, and depends on
1557
+ * no state whatsoever. Scrubbing is O(frame) and that is the honest cost —
1558
+ * a checkpoint cache makes it fast later without changing a single output,
1559
+ * which is exactly the property a cache should have.
1560
+ */
1561
+ readonly substeps: {
1562
+ readonly type: "int";
1563
+ readonly default: 1;
1564
+ readonly min: 1;
1565
+ readonly max: 32;
1566
+ };
1567
+ readonly timestep: {
1568
+ readonly type: "float";
1569
+ readonly default: number;
1570
+ readonly min: number;
1571
+ readonly max: 1;
1572
+ readonly step: number;
1573
+ };
1574
+ /** Per frame, as on Source. */
1575
+ readonly impulse: {
1576
+ readonly type: "int";
1577
+ readonly default: 6;
1578
+ readonly min: 0;
1579
+ readonly max: 100000;
1580
+ };
1581
+ readonly life: {
1582
+ readonly type: "float";
1583
+ readonly default: 4;
1584
+ readonly min: 0;
1585
+ readonly max: 3600;
1586
+ readonly step: 0.1;
1587
+ };
1588
+ readonly lifevar: {
1589
+ readonly type: "float";
1590
+ readonly default: 0.4;
1591
+ readonly min: 0;
1592
+ readonly max: 1;
1593
+ readonly step: 0.01;
1594
+ };
1595
+ readonly force: {
1596
+ readonly type: "vec2";
1597
+ readonly default: readonly [0, 0];
1598
+ };
1599
+ readonly airresist: {
1600
+ readonly type: "float";
1601
+ readonly default: 0.4;
1602
+ readonly min: 0;
1603
+ readonly max: 20;
1604
+ readonly step: 0.05;
1605
+ };
1606
+ readonly noise_amplitude: {
1607
+ readonly type: "float";
1608
+ readonly default: 14;
1609
+ readonly min: 0;
1610
+ readonly max: 500;
1611
+ readonly step: 0.5;
1612
+ };
1613
+ readonly noise_frequency: {
1614
+ readonly type: "float";
1615
+ readonly default: 0.02;
1616
+ readonly min: 0.001;
1617
+ readonly max: 10;
1618
+ readonly step: 0.001;
1619
+ };
1620
+ /** Turns per second of field evolution. */
1621
+ readonly noise_evolve: {
1622
+ readonly type: "float";
1623
+ readonly default: 0.08;
1624
+ readonly min: -10;
1625
+ readonly max: 10;
1626
+ readonly step: 0.01;
1627
+ };
1628
+ /** Toward the spine, up the field's gradient. */
1629
+ readonly field_normal: {
1630
+ readonly type: "float";
1631
+ readonly default: 0;
1632
+ readonly min: -20000;
1633
+ readonly max: 20000;
1634
+ readonly step: 1;
1635
+ };
1636
+ /** Around the outline, perpendicular to the gradient. Sign picks which way. */
1637
+ readonly field_tangential: {
1638
+ readonly type: "float";
1639
+ readonly default: 0;
1640
+ readonly min: -20000;
1641
+ readonly max: 20000;
1642
+ readonly step: 1;
1643
+ };
1644
+ /** Field samples beyond this are ignored, and it is the grid's cell size. */
1645
+ readonly field_radius: {
1646
+ readonly type: "float";
1647
+ readonly default: 24;
1648
+ readonly min: 0.5;
1649
+ readonly max: 2000;
1650
+ readonly step: 0.5;
1651
+ };
1652
+ /**
1653
+ * The contour to hold, as a field level 0..1, with `field_hold` as how
1654
+ * firmly.
1655
+ *
1656
+ * Marcus: *"the lines flow tangentially around the letters in a bundle …
1657
+ * loosely describing the type."* A bundle is a contour, so holding a level
1658
+ * and letting the tangent carry particles round it is the mechanism —
1659
+ * pulling them to the spine instead collapses each letter to a point, and
1660
+ * pure tangential flow lets every particle converge onto one level set.
1661
+ */
1662
+ readonly field_level: {
1663
+ readonly type: "float";
1664
+ readonly default: 0.35;
1665
+ readonly min: 0;
1666
+ readonly max: 1;
1667
+ readonly step: 0.01;
1668
+ };
1669
+ readonly field_hold: {
1670
+ readonly type: "float";
1671
+ readonly default: 0;
1672
+ readonly min: 0;
1673
+ readonly max: 40000;
1674
+ readonly step: 10;
1675
+ };
1676
+ /**
1677
+ * Scatter births across this box instead of on the source geometry's
1678
+ * points, `[minX, minY, maxX, maxY]`. All zero uses the points.
1679
+ *
1680
+ * Houdini's POP Source has the same choice — *Scatter onto Surface* against
1681
+ * *Points* — and Marcus's note is that the original spawns *around* the
1682
+ * type rather than on it: *"The particles should spawn in the area around
1683
+ * the type … and then the forces should drag them around."* Birthing on
1684
+ * the letterforms puts every particle where it is already going, so
1685
+ * nothing travels and the field has nothing to reveal.
1686
+ */
1687
+ readonly birth_area: {
1688
+ readonly type: "vec4";
1689
+ readonly default: readonly [0, 0, 0, 0];
1690
+ };
1691
+ /**
1692
+ * A colour per particle, drawn at birth and fixed for its life.
1693
+ *
1694
+ * Hashed on the id, so the same particle gets the same colour however many
1695
+ * times the frame is re-simulated — a colour drawn from a stream would
1696
+ * change on every scrub, which reads as the render being unstable.
1697
+ *
1698
+ * `[from, to]` in turns around the wheel. Equal values give one hue; a
1699
+ * narrow span is usually what a piece wants, because a full-spectrum
1700
+ * random set reads as a test pattern rather than as a palette.
1701
+ */
1702
+ readonly birth_hue: {
1703
+ readonly type: "vec2";
1704
+ readonly default: readonly [0, 0];
1705
+ readonly min: 0;
1706
+ readonly max: 1;
1707
+ readonly step: 0.01;
1708
+ };
1709
+ readonly birth_saturation: {
1710
+ readonly type: "float";
1711
+ readonly default: 0.6;
1712
+ readonly min: 0;
1713
+ readonly max: 1;
1714
+ readonly step: 0.01;
1715
+ };
1716
+ readonly birth_value: {
1717
+ readonly type: "float";
1718
+ readonly default: 1;
1719
+ readonly min: 0;
1720
+ readonly max: 1;
1721
+ readonly step: 0.01;
1722
+ };
1723
+ /** How hard the targets pull. Houdini's POP Attract strength. */
1724
+ readonly attract_amplitude: {
1725
+ readonly type: "float";
1726
+ readonly default: 0;
1727
+ readonly min: 0;
1728
+ readonly max: 20000;
1729
+ readonly step: 1;
1730
+ };
1731
+ /** Beyond this distance a target is ignored, which is what keeps the pull
1732
+ * local and the cost bounded. 0 means every target pulls every particle. */
1733
+ readonly attract_radius: {
1734
+ readonly type: "float";
1735
+ readonly default: 60;
1736
+ readonly min: 0;
1737
+ readonly max: 5000;
1738
+ readonly step: 1;
1739
+ };
1740
+ readonly separate_radius: {
1741
+ readonly type: "float";
1742
+ readonly default: 0;
1743
+ readonly min: 0;
1744
+ readonly max: 500;
1745
+ readonly step: 0.5;
1746
+ };
1747
+ readonly separate_strength: {
1748
+ readonly type: "float";
1749
+ readonly default: 12;
1750
+ readonly min: 0;
1751
+ readonly max: 500;
1752
+ readonly step: 0.5;
1753
+ };
1754
+ readonly maxspeed: {
1755
+ readonly type: "float";
1756
+ readonly default: 60;
1757
+ readonly min: 0;
1758
+ readonly max: 10000;
1759
+ readonly step: 1;
1760
+ };
1761
+ /** `[minX, minY, maxX, maxY]`, or all zero for open space. The original
1762
+ * piece is toroidal. */
1763
+ readonly wrap: {
1764
+ readonly type: "vec4";
1765
+ readonly default: readonly [0, 0, 0, 0];
1766
+ };
1767
+ readonly seed: {
1768
+ readonly type: "int";
1769
+ readonly default: 7;
1770
+ readonly min: 0;
1771
+ readonly max: 999999;
1772
+ };
1773
+ };
1774
+ }], readonly ["cascade.pop.Source", {
1775
+ readonly apiVersion: 1;
1776
+ readonly label: "POP Source";
1777
+ readonly description: "Birth particles from a geometry's points.";
1778
+ readonly icon: "Sparkles";
1779
+ readonly runsOn: "portable";
1780
+ readonly inputs: {
1781
+ readonly geometry: {
1782
+ readonly kind: "data";
1783
+ readonly type: "geometry";
1784
+ };
1785
+ readonly particles: {
1786
+ readonly kind: "data";
1787
+ readonly type: "geometry";
1788
+ };
1789
+ };
1790
+ readonly outputs: {
1791
+ readonly geometry: {
1792
+ readonly kind: "data";
1793
+ readonly type: "geometry";
1794
+ };
1795
+ };
1796
+ readonly props: {
1797
+ readonly impulse: {
1798
+ readonly type: "int";
1799
+ readonly default: 8;
1800
+ readonly min: 0;
1801
+ readonly max: 100000;
1802
+ };
1803
+ readonly life: {
1804
+ readonly type: "float";
1805
+ readonly default: 3;
1806
+ readonly min: 0;
1807
+ readonly max: 3600;
1808
+ readonly step: 0.1;
1809
+ };
1810
+ readonly lifevar: {
1811
+ readonly type: "float";
1812
+ readonly default: 0.3;
1813
+ readonly min: 0;
1814
+ readonly max: 1;
1815
+ readonly step: 0.01;
1816
+ };
1817
+ readonly velocity: {
1818
+ readonly type: "vec2";
1819
+ readonly default: readonly [0, 0];
1820
+ };
1821
+ readonly seed: {
1822
+ readonly type: "int";
1823
+ readonly default: 7;
1824
+ readonly min: 0;
1825
+ readonly max: 999999;
1826
+ };
1827
+ };
1828
+ }], readonly ["cascade.pop.Solver", {
1829
+ readonly apiVersion: 1;
1830
+ readonly label: "POP Solver";
1831
+ readonly description: "Advance a particle system by one timestep.";
1832
+ readonly icon: "Play";
1833
+ readonly runsOn: "portable";
1834
+ readonly inputs: {
1835
+ readonly particles: {
1836
+ readonly kind: "data";
1837
+ readonly type: "geometry";
1838
+ };
1839
+ };
1840
+ readonly outputs: {
1841
+ readonly geometry: {
1842
+ readonly kind: "data";
1843
+ readonly type: "geometry";
1844
+ };
1845
+ };
1846
+ readonly props: {
1847
+ /**
1848
+ * Where the timestep comes from. Marcus's ruling, 2026-09-09: *"Let the
1849
+ * user choose nodes fixed or variable."*
1850
+ *
1851
+ * `fixed` is the default on the ordinary grounds that the safer of two
1852
+ * behaviours is the one you get without asking - a simulation whose result
1853
+ * depends on a playback setting is not reproducible from the document
1854
+ * alone. With `fps` the render rate becomes part of what determines the
1855
+ * output, which is correct for a mode called variable and must not be
1856
+ * silent: it belongs in the cache key, and `cascade check` should say so.
1857
+ */
1858
+ readonly timestep_mode: {
1859
+ readonly type: "string";
1860
+ readonly default: "fixed";
1861
+ readonly control: "select";
1862
+ readonly options: readonly [{
1863
+ readonly value: "fixed";
1864
+ readonly label: "Fixed - reproducible from the document";
1865
+ }, {
1866
+ readonly value: "fps";
1867
+ readonly label: "Variable - derived from the render rate";
1868
+ }];
1869
+ };
1870
+ readonly timestep: {
1871
+ readonly type: "float";
1872
+ readonly default: number;
1873
+ readonly min: number;
1874
+ readonly max: 1;
1875
+ readonly step: number;
1876
+ };
1877
+ readonly maxspeed: {
1878
+ readonly type: "float";
1879
+ readonly default: 0;
1880
+ readonly min: 0;
1881
+ readonly max: 10000;
1882
+ readonly step: 0.5;
1883
+ };
1884
+ readonly force: {
1885
+ readonly type: "vec2";
1886
+ readonly default: readonly [0, 0];
1887
+ };
1888
+ readonly airresist: {
1889
+ readonly type: "float";
1890
+ readonly default: 0;
1891
+ readonly min: 0;
1892
+ readonly max: 20;
1893
+ readonly step: 0.05;
1894
+ };
1895
+ };
1896
+ }], readonly ["cascade.pop.NoiseForce", {
1897
+ readonly apiVersion: 1;
1898
+ readonly label: "POP Noise Force";
1899
+ readonly description: "Push particles along a heading read from a noise field.";
1900
+ readonly icon: "Wind";
1901
+ readonly runsOn: "portable";
1902
+ readonly inputs: {
1903
+ readonly particles: {
1904
+ readonly kind: "data";
1905
+ readonly type: "geometry";
1906
+ };
1907
+ };
1908
+ readonly outputs: {
1909
+ readonly geometry: {
1910
+ readonly kind: "data";
1911
+ readonly type: "geometry";
1912
+ };
1913
+ };
1914
+ readonly props: {
1915
+ readonly amplitude: {
1916
+ readonly type: "float";
1917
+ readonly default: 6;
1918
+ readonly min: 0;
1919
+ readonly max: 500;
1920
+ readonly step: 0.5;
1921
+ };
1922
+ readonly frequency: {
1923
+ readonly type: "float";
1924
+ readonly default: 0.3;
1925
+ readonly min: 0.001;
1926
+ readonly max: 10;
1927
+ readonly step: 0.01;
1928
+ };
1929
+ readonly evolve: {
1930
+ readonly type: "float";
1931
+ readonly default: 0;
1932
+ readonly min: -1000;
1933
+ readonly max: 1000;
1934
+ readonly step: 0.01;
1935
+ };
1936
+ readonly timestep: {
1937
+ readonly type: "float";
1938
+ readonly default: number;
1939
+ readonly min: number;
1940
+ readonly max: 1;
1941
+ readonly step: number;
1942
+ };
1943
+ readonly seed: {
1944
+ readonly type: "int";
1945
+ readonly default: 7;
1946
+ readonly min: 0;
1947
+ readonly max: 999999;
1948
+ };
1949
+ };
1950
+ }], readonly ["cascade.pop.Separate", {
1951
+ readonly apiVersion: 1;
1952
+ readonly label: "POP Separate";
1953
+ readonly description: "Push neighbours apart within a radius.";
1954
+ readonly icon: "Move3d";
1955
+ readonly runsOn: "portable";
1956
+ readonly inputs: {
1957
+ readonly particles: {
1958
+ readonly kind: "data";
1959
+ readonly type: "geometry";
1960
+ };
1961
+ };
1962
+ readonly outputs: {
1963
+ readonly geometry: {
1964
+ readonly kind: "data";
1965
+ readonly type: "geometry";
1966
+ };
1967
+ };
1968
+ readonly props: {
1969
+ readonly radius: {
1970
+ readonly type: "float";
1971
+ readonly default: 0.5;
1972
+ readonly min: 0;
1973
+ readonly max: 100;
1974
+ readonly step: 0.01;
1975
+ };
1976
+ readonly strength: {
1977
+ readonly type: "float";
1978
+ readonly default: 4;
1979
+ readonly min: 0;
1980
+ readonly max: 500;
1981
+ readonly step: 0.1;
1982
+ };
1983
+ readonly timestep: {
1984
+ readonly type: "float";
1985
+ readonly default: number;
1986
+ readonly min: number;
1987
+ readonly max: 1;
1988
+ readonly step: number;
1989
+ };
1990
+ };
1991
+ }], readonly ["cascade.pop.FieldForce", {
1992
+ readonly apiVersion: 1;
1993
+ readonly label: "POP Field Force";
1994
+ readonly description: "Pull particles along a field's gradient and around its contours. The calligraphy force.";
1995
+ readonly icon: "Waves";
1996
+ readonly runsOn: "portable";
1997
+ readonly inputs: {
1998
+ readonly particles: {
1999
+ readonly kind: "data";
2000
+ readonly type: "geometry";
2001
+ };
2002
+ /** The field: points carrying `N` (the gradient) and optionally `level`. */
2003
+ readonly field: {
2004
+ readonly kind: "data";
2005
+ readonly type: "geometry";
2006
+ };
2007
+ };
2008
+ readonly outputs: {
2009
+ readonly geometry: {
2010
+ readonly kind: "data";
2011
+ readonly type: "geometry";
2012
+ };
2013
+ };
2014
+ readonly props: {
2015
+ readonly normal: {
2016
+ readonly type: "float";
2017
+ readonly default: 0;
2018
+ readonly min: -100000;
2019
+ readonly max: 100000;
2020
+ readonly label: "Normal";
2021
+ readonly description: "Along N, toward the spine. Negative pushes away.";
2022
+ };
2023
+ readonly tangential: {
2024
+ readonly type: "float";
2025
+ readonly default: 2600;
2026
+ readonly min: -100000;
2027
+ readonly max: 100000;
2028
+ readonly label: "Tangential";
2029
+ readonly description: "Perpendicular to N, around the outline. Sign picks the direction.";
2030
+ };
2031
+ readonly radius: {
2032
+ readonly type: "float";
2033
+ readonly default: 20;
2034
+ readonly min: 0;
2035
+ readonly max: 10000;
2036
+ readonly label: "Radius";
2037
+ readonly description: "How far a field sample reaches.";
2038
+ };
2039
+ readonly level: {
2040
+ readonly type: "float";
2041
+ readonly default: 0.28;
2042
+ readonly min: 0;
2043
+ readonly max: 1;
2044
+ readonly step: 0.01;
2045
+ readonly label: "Level";
2046
+ readonly description: "The contour to hold, rather than climbing to the peak.";
2047
+ };
2048
+ readonly hold: {
2049
+ readonly type: "float";
2050
+ readonly default: 5200;
2051
+ readonly min: 0;
2052
+ readonly max: 100000;
2053
+ readonly label: "Hold";
2054
+ readonly description: "How hard to hold that contour.";
2055
+ };
2056
+ };
2057
+ }], readonly ["cascade.pop.Trail", {
2058
+ readonly apiVersion: 1;
2059
+ readonly label: "POP Trail";
2060
+ readonly description: "Build trails from a Feedback container's history.";
2061
+ readonly icon: "Activity";
2062
+ readonly runsOn: "portable";
2063
+ readonly inputs: {
2064
+ /** The container's `history` output: every kept step, oldest first. */
2065
+ readonly history: {
2066
+ readonly kind: "data";
2067
+ readonly type: "array";
2068
+ };
2069
+ /** The current state, for `preserve` and for the newest point of each trail. */
2070
+ readonly particles: {
2071
+ readonly kind: "data";
2072
+ readonly type: "geometry";
2073
+ };
2074
+ };
2075
+ readonly outputs: {
2076
+ readonly geometry: {
2077
+ readonly kind: "data";
2078
+ readonly type: "geometry";
2079
+ };
2080
+ };
2081
+ readonly props: {
2082
+ readonly result: {
2083
+ readonly type: "string";
2084
+ readonly default: "polylines";
2085
+ readonly control: "select";
2086
+ readonly options: readonly [{
2087
+ readonly value: "polylines";
2088
+ readonly label: "Polylines - one line per particle";
2089
+ }, {
2090
+ readonly value: "preserve";
2091
+ readonly label: "Preserve - pass the points through";
2092
+ }];
2093
+ readonly label: "Result";
2094
+ };
2095
+ readonly length: {
2096
+ readonly type: "int";
2097
+ readonly default: 12;
2098
+ readonly min: 1;
2099
+ readonly max: 100000;
2100
+ readonly label: "Length";
2101
+ };
2102
+ readonly increment: {
2103
+ readonly type: "int";
2104
+ readonly default: 1;
2105
+ readonly min: 1;
2106
+ readonly max: 1000;
2107
+ readonly label: "Increment";
2108
+ };
2109
+ readonly velocityscale: {
2110
+ readonly type: "float";
2111
+ readonly default: 1;
2112
+ readonly min: 0;
2113
+ readonly max: 100;
2114
+ readonly step: 0.01;
2115
+ readonly label: "Velocity Scale";
2116
+ };
2117
+ };
2118
+ }]];
2119
+ export {};
2120
+ //# sourceMappingURL=definitions.d.ts.map