@field/cascade 0.8.0 → 0.11.0
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 +36 -3
- package/CHANGELOG.md +401 -0
- package/DESIGN.md +14 -0
- package/README.md +41 -26
- package/dist/assets/CodeEditor-4O7FQ__R.js +623 -0
- package/dist/assets/CodePanel-BU-autmq.js +2 -0
- package/dist/assets/{CustomNodeDialog-BQbEhIwL.js → CustomNodeDialog-BRpDfOV5.js} +1 -1
- package/dist/assets/OpenGraphDialog-BwbRlC0X.js +1 -0
- package/dist/assets/ProjectSettings-DLOXLxcA.js +2 -0
- package/dist/assets/SettingsDialog-Bz8JxISH.js +1 -0
- package/dist/assets/{VersionHistoryDialog-D6VBzSQ0.js → VersionHistoryDialog-D5Z0nJ5-.js} +2 -2
- package/dist/assets/{core-BInLG4gw.js → core-Yqi0Ni61.js} +8 -8
- package/dist/assets/{cssMode-DfR2qmYG.js → cssMode-CxUzc4vp.js} +1 -1
- package/dist/assets/definitions-GreEMXLV.js +1 -0
- package/dist/assets/{editor.api-Ck8_vgQV.js → editor.api-BNFb25CX.js} +1 -1
- package/dist/assets/{freemarker2-fVn10Wn3.js → freemarker2-0ZDnWxeM.js} +1 -1
- package/dist/assets/geo-u0n_jOsD.js +1 -0
- package/dist/assets/{handlebars-BCSchLT2.js → handlebars-Cy18MoaD.js} +1 -1
- package/dist/assets/{html-CVpSv_k9.js → html-B-2Dx7nq.js} +1 -1
- package/dist/assets/{htmlMode-Da4ESTv0.js → htmlMode-DUAc9B_d.js} +1 -1
- package/dist/assets/{image-BLn--oSF.js → image-cAa-Nu9O.js} +3 -3
- package/dist/assets/index-5ZtDAuy2.js +220 -0
- package/dist/assets/index-7vae6YFC.css +1 -0
- package/dist/assets/index-client-DqYmKstU.js +1 -0
- package/dist/assets/{javascript-BpX0Gvcf.js → javascript-DeQsDqP5.js} +1 -1
- package/dist/assets/{jsonMode-CsLqaou7.js → jsonMode-BiFwJVSk.js} +1 -1
- package/dist/assets/legacy-CTEeXeqF.js +2 -0
- package/dist/assets/{liquid-efUz51Wf.js → liquid-BLoHH68a.js} +1 -1
- package/dist/assets/{lspLanguageFeatures-DCi5-K0n.js → lspLanguageFeatures-CNFgm1Kv.js} +1 -1
- package/dist/assets/math-DioIQOPJ.js +1 -0
- package/dist/assets/{mdx-DP_sjGVy.js → mdx-CFXQVNfs.js} +1 -1
- package/dist/assets/pop-DC_TQG9t.js +1 -0
- package/dist/assets/{python-aAuP3LUi.js → python-Cf7S7p2E.js} +1 -1
- package/dist/assets/{razor-7M6sMmC5.js → razor-CMHBcG-g.js} +1 -1
- package/dist/assets/studioScenes-8sq9FfFg.js +1 -0
- package/dist/assets/{tsMode-Bf4CnoYK.js → tsMode-BEih0GDj.js} +1 -1
- package/dist/assets/{typescript-ddhYBGyT.js → typescript-COEKYD_a.js} +1 -1
- package/dist/assets/{workers-X5la8DDT.js → workers-BNhAhMNn.js} +1 -1
- package/dist/assets/{xml-BzbXa3Ps.js → xml-CPiMpnPl.js} +1 -1
- package/dist/assets/{yaml-CXs-VzxL.js → yaml-Bgp1ITLR.js} +1 -1
- package/dist/cli/Graph-ZJJTRQTM.js +2975 -0
- package/dist/cli/chunk-2KXB47FP.js +115 -0
- package/dist/cli/chunk-35D343DC.js +1447 -0
- package/dist/cli/chunk-36P5K3SD.js +631 -0
- package/dist/cli/chunk-3JTJHF7V.js +267 -0
- package/dist/cli/chunk-4UDFYF5G.js +132 -0
- package/dist/cli/chunk-53NMBJAO.js +35 -0
- package/dist/cli/chunk-A4OAMOZH.js +310 -0
- package/dist/cli/chunk-BIJO5OFS.js +71 -0
- package/dist/cli/chunk-BP7UKF36.js +376 -0
- package/dist/cli/chunk-CKS2FQWR.js +89 -0
- package/dist/cli/chunk-CO22XHNG.js +593 -0
- package/dist/cli/chunk-D6BUOHUG.js +634 -0
- package/dist/cli/chunk-DEQSEFFH.js +153 -0
- package/dist/cli/chunk-DUIUFVU2.js +241 -0
- package/dist/cli/chunk-EUIAC3U3.js +3006 -0
- package/dist/cli/chunk-FCDXJ6EK.js +2208 -0
- package/dist/cli/chunk-GFS6W3C5.js +308 -0
- package/dist/cli/chunk-HFHWYL55.js +139 -0
- package/dist/cli/chunk-HSNOBIVT.js +263 -0
- package/dist/cli/chunk-ICH7N5PN.js +26 -0
- package/dist/cli/chunk-J7VK7MNF.js +418 -0
- package/dist/cli/chunk-KH4MUBKF.js +1551 -0
- package/dist/cli/chunk-MIIEFUGG.js +623 -0
- package/dist/cli/chunk-OWWBYLHC.js +136 -0
- package/dist/cli/chunk-POMMNKZZ.js +672 -0
- package/dist/cli/chunk-QBBKRNGY.js +90 -0
- package/dist/cli/chunk-QQPA2ZHH.js +273 -0
- package/dist/cli/chunk-RLCAJBVR.js +224 -0
- package/dist/cli/chunk-RQZ4SX5R.js +1125 -0
- package/dist/cli/chunk-RUU3YWXU.js +99 -0
- package/dist/cli/chunk-RXDIMXN5.js +98 -0
- package/dist/cli/chunk-SGJAET2L.js +197 -0
- package/dist/cli/chunk-VVGSLVSF.js +151 -0
- package/dist/cli/chunk-WD62JIP6.js +118 -0
- package/dist/cli/chunk-WIH4TEIS.js +104 -0
- package/dist/cli/chunk-WKURDVYP.js +1119 -0
- package/dist/cli/chunk-WPUDGEFY.js +20 -0
- package/dist/cli/chunk-XWDSKG7N.js +1301 -0
- package/dist/cli/chunk-YAT5Y25U.js +121 -0
- package/dist/cli/chunk-ZL4JTG5Y.js +37 -0
- package/dist/cli/cliCommands-UQSFGEHF.js +3464 -0
- package/dist/cli/commands-Q23M26QM.js +1292 -0
- package/dist/cli/definitions-HF2FTHYU.js +32 -0
- package/dist/cli/doctorCommand-B7BPQMN6.js +76 -0
- package/dist/cli/edit-JHCPTQFT.js +28 -0
- package/dist/cli/editorCommands-UIF5YLUZ.js +33 -0
- package/dist/cli/encode-HAUZTYPT.js +17 -0
- package/dist/cli/frames-LSAUBZ7P.js +16 -0
- package/dist/cli/geo-4RNK3QVN.js +43 -0
- package/dist/cli/graphCommands-6BMAKGQE.js +184 -0
- package/dist/cli/graphFile-VPSQ6OF7.js +286 -0
- package/dist/cli/headlessCanvas-5XLDZT7M.js +15 -0
- package/dist/cli/helpCommand-3VRE3KVO.js +155 -0
- package/dist/cli/index.js +106 -14207
- package/dist/cli/math-ZRMLKU2D.js +16 -0
- package/dist/cli/mcp-ZLEEMYGF.js +164 -0
- package/dist/cli/parity-KINARCP2.js +328 -0
- package/dist/cli/pop-USDFXB7H.js +39 -0
- package/dist/cli/project-K3QIPGBB.js +12 -0
- package/dist/cli/projectDefinition-DMTZN2NE.js +27 -0
- package/dist/cli/projectRuntime-Z5PJOH47.js +48 -0
- package/dist/cli/registerStandardNodes-S2VLMF6J.js +3081 -0
- package/dist/cli/render-YURAPPUI.js +436 -0
- package/dist/cli/runCommand-PI764LEG.js +198 -0
- package/dist/cli/runner-RA6UKEPD.js +37 -0
- package/dist/cli/runtime/image.ts +277 -0
- package/dist/cli/runtime/stage.ts +74 -11
- package/dist/cli/stageRunner-KUNIMQKJ.js +10 -0
- package/dist/cli/verbs-RCE3K4VQ.js +19 -0
- package/dist/cli/webBuild-GT4PZT5D.js +37 -0
- package/dist/contracts/bundle.d.ts +75 -0
- package/dist/contracts/bundle.js +112 -0
- package/dist/contracts/definition.d.ts +67 -4
- package/dist/contracts/document.d.ts +29 -0
- package/dist/contracts/documentFormat.d.ts +18 -0
- package/dist/contracts/documentFormat.js +195 -0
- package/dist/contracts/editor.d.ts +189 -0
- package/dist/contracts/editor.js +2 -0
- package/dist/contracts/index.d.ts +3 -0
- package/dist/contracts/index.js +3 -0
- package/dist/contracts/validate.js +59 -3
- package/dist/contracts/values.d.ts +3 -3
- package/dist/contracts/values.js +1 -0
- package/dist/image.d.ts +141 -0
- package/dist/image.js +127 -0
- package/dist/index.html +4 -4
- package/dist/player/app.js +17 -5
- package/dist/player/editor.js +1919 -0
- package/dist/player/editorEngine.js +477 -0
- package/dist/runtime/builtins/core/bundle.d.ts +70 -0
- package/dist/runtime/builtins/core/bundle.js +81 -0
- package/dist/runtime/builtins/core/index.d.ts +3 -2
- package/dist/runtime/builtins/core/index.js +6 -2
- package/dist/runtime/builtins/core/routing.d.ts +27 -0
- package/dist/runtime/builtins/core/routing.js +28 -0
- package/dist/runtime/builtins/headless.d.ts +19 -0
- package/dist/runtime/builtins/headless.js +25 -0
- package/dist/runtime/builtins/image/contactSheet.d.ts +226 -0
- package/dist/runtime/builtins/image/contactSheet.js +199 -0
- package/dist/runtime/builtins/image/index.d.ts +9 -0
- package/dist/runtime/builtins/image/index.js +9 -0
- package/dist/runtime/edit/document.d.ts +94 -0
- package/dist/runtime/edit/document.js +194 -0
- package/dist/runtime/edit/graphEditor.d.ts +223 -0
- package/dist/runtime/edit/graphEditor.js +893 -0
- package/dist/runtime/edit/index.d.ts +12 -0
- package/dist/runtime/edit/index.js +6 -0
- package/dist/runtime/edit/layout.d.ts +50 -0
- package/dist/runtime/edit/layout.js +126 -0
- package/dist/runtime/edit/operations.d.ts +65 -0
- package/dist/runtime/edit/operations.js +57 -0
- package/dist/runtime/edit/parameters.d.ts +75 -0
- package/dist/runtime/edit/parameters.js +284 -0
- package/dist/runtime/edit/read.d.ts +35 -0
- package/dist/runtime/edit/read.js +65 -0
- package/dist/runtime/editor/index.d.ts +104 -0
- package/dist/runtime/editor/index.js +439 -0
- package/dist/runtime/expressions/bundlePaths.d.ts +37 -0
- package/dist/runtime/expressions/bundlePaths.js +55 -0
- package/dist/runtime/expressions/index.d.ts +2 -0
- package/dist/runtime/expressions/index.js +1 -0
- package/dist/runtime/index.d.ts +2 -0
- package/dist/runtime/index.js +2 -0
- package/dist/runtime/params/animator.d.ts +5 -0
- package/dist/runtime/params/animator.js +16 -0
- package/dist/runtime/runtime.js +238 -12
- package/dist/runtime/seed.d.ts +29 -0
- package/dist/runtime/seed.js +52 -0
- package/dist/runtime/types.d.ts +12 -0
- package/dist/stage.js +43 -9
- package/dist/studio/panel.d.ts +2 -0
- package/doc/CLI_REFERENCE.md +171 -0
- package/doc/EDITOR_EXPORT.md +235 -0
- package/doc/HEADLESS_GPU.md +27 -0
- package/doc/MCP.md +69 -0
- package/doc/NODE_REFERENCE.md +208 -0
- package/doc/PROJECT_AUTHORING.md +258 -1
- package/package.json +29 -8
- package/dist/assets/CodeEditor-BEk-kIBd.js +0 -623
- package/dist/assets/CodePanel-CeSdjXkg.js +0 -2
- package/dist/assets/ExportDialog-CjTbbTZJ.js +0 -358
- package/dist/assets/ExportDialog-DOq0flMV.css +0 -1
- package/dist/assets/OpenGraphDialog-DCwH9itM.js +0 -1
- package/dist/assets/ProjectSettings-Dg7M5xt9.js +0 -2
- package/dist/assets/SettingsDialog-e7GgsjB9.js +0 -1
- package/dist/assets/definitions-DZpOjWKR.js +0 -1
- package/dist/assets/geo-D2VPjAdt.js +0 -1
- package/dist/assets/index-CMiBdEuT.js +0 -212
- package/dist/assets/index-DMIuml9a.css +0 -1
- package/dist/assets/index-client-BI2H82FJ.js +0 -1
- package/dist/assets/legacy-ZA7textG.js +0 -2
- package/dist/assets/math-B6hpxPJO.js +0 -1
- package/dist/assets/pop-CYUIjT6x.js +0 -1
- package/dist/assets/projectSettingsApi-C1WvIwfP.js +0 -1
package/doc/MCP.md
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# MCP server
|
|
2
|
+
|
|
3
|
+
`cascade mcp [project-directory]` serves the CLI as MCP tools over stdio, so an agent calls `set` instead of shelling out and parsing text. The tools are the CLI's commands under the same names, and every result is the same `--json` envelope (`{ ok, data, warnings }`, or `{ ok: false, error, warnings }` with `isError` set). Graph paths are relative to the project directory and may not leave it.
|
|
4
|
+
|
|
5
|
+
## Set it up in Claude Code
|
|
6
|
+
|
|
7
|
+
From the project directory:
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
claude mcp add cascade -- npx --no-install cascade mcp .
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Or in the project's `.mcp.json`, so every session in the project has it:
|
|
14
|
+
|
|
15
|
+
```json
|
|
16
|
+
{
|
|
17
|
+
"mcpServers": {
|
|
18
|
+
"cascade": { "command": "npx", "args": ["--no-install", "cascade", "mcp", "."] }
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
With a global install (`npm i -g @field/cascade`), use `"command": "cascade", "args": ["mcp", "."]`. The tools then appear as `mcp__cascade__set`, `mcp__cascade__render` and so on.
|
|
24
|
+
|
|
25
|
+
## Tools
|
|
26
|
+
|
|
27
|
+
| Tool | CLI equivalent |
|
|
28
|
+
| --- | --- |
|
|
29
|
+
| `graph_list`, `graph_read`, `graph_create` | `cascade graph list \| read \| create` |
|
|
30
|
+
| `node_add`, `node_remove` | `cascade node add \| rm` |
|
|
31
|
+
| `connect`, `disconnect` | `cascade connect \| disconnect` |
|
|
32
|
+
| `set` | `cascade set` (value, `expression`, `keys`, `removeKeys`, `interpolation`, `unset`) |
|
|
33
|
+
| `subnet_create`, `subnet_move`, `subnet_dissolve` | `cascade subnet create \| move \| dissolve` |
|
|
34
|
+
| `edit` | `cascade edit` (a batch of operations, all or nothing) |
|
|
35
|
+
| `check`, `inspect` | `cascade check \| inspect` |
|
|
36
|
+
| `cook` | `cascade run <graph> [--node <id>]` |
|
|
37
|
+
| `render` | `cascade run <graph> --frames … [--fps] [--out] [--node]… [--scene]` |
|
|
38
|
+
| `export` | `cascade export <graph> --plan \| --out <dir>` |
|
|
39
|
+
| `lock`, `replay`, `locks` | `cascade lock <graph> <node> --name`, `cascade replay <id> [--live]`, `cascade locks` |
|
|
40
|
+
| `ladder`, `pick` | `cascade ladder <graph> <node> --name --vary node.prop=a,b,c \| --seeds a,b,c` (`vary` is `{ "node.prop": [a, b, c] }`), `cascade pick <ladder> <n>` |
|
|
41
|
+
| `node_help` | `cascade help node [<type>]` (a node type's declaration; every type without one) |
|
|
42
|
+
| `doctor` | `cascade doctor [--fix]` (Python environment, stage interpreter, ffmpeg, canvas; `fix` builds the environment) |
|
|
43
|
+
|
|
44
|
+
The editing tools take `dryRun`. Reads and edits run inside the server through the same functions the CLI commands call. Cooking, rendering, checking, exporting and the lock tools run `cascade <command> --json` as one child process each, so a long render leaves nothing behind in the server and returns exactly what the CLI would print. Calls run one at a time, so two edits to one file never interleave.
|
|
45
|
+
|
|
46
|
+
Each tool is an `mcp` entry on its command's row in `src/cli/verbs.ts`, with its description and input schema, so a tool cannot name a command this build does not have (`tests/cli-verbs.test.ts` checks every tool against the table and this page).
|
|
47
|
+
|
|
48
|
+
## A session
|
|
49
|
+
|
|
50
|
+
```text
|
|
51
|
+
graph_read { "graph": "index.cascade" }
|
|
52
|
+
node_add { "graph": "index.cascade", "type": "Transform", "set": { "rotate": 0.4 } }
|
|
53
|
+
connect { "graph": "index.cascade", "from": "motif.output_0", "to": "transform.geometry" }
|
|
54
|
+
set { "graph": "index.cascade", "target": "render.opacity", "keys": [{ "frame": 1, "value": 0.25 }, { "frame": 48, "value": 1 }] }
|
|
55
|
+
render { "graph": "index.cascade", "frames": "1-48", "out": "renders/check" }
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
See `doc/CLI_REFERENCE.md` for what each operation guarantees about diffs, placement and subnet boundaries.
|
|
59
|
+
|
|
60
|
+
## Another project, per call
|
|
61
|
+
|
|
62
|
+
Every tool takes an optional `project`: the project directory for that call, absolute or relative to the directory the server was started in. Without it nothing changes. With it, the call's paths (`graph`, `out`, `scene`, `directory`) resolve against that project and may not leave it, and `cook`, `render`, `check` and the others run their CLI child there. So one server started in `commons` can edit and render a sketch next to it:
|
|
63
|
+
|
|
64
|
+
```text
|
|
65
|
+
graph_read { "project": "/Users/me/Documents/Cascade/cloud-volumes", "graph": "index.cascade" }
|
|
66
|
+
cook { "project": "../cloud-volumes", "graph": "index.cascade", "out": "renders/check" }
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
A `project` that is not a directory is refused.
|
package/doc/NODE_REFERENCE.md
CHANGED
|
@@ -79,6 +79,24 @@ Runs on `portable`.
|
|
|
79
79
|
| `result` | `any` | | |
|
|
80
80
|
| `history` | `array` | | |
|
|
81
81
|
|
|
82
|
+
### `cascade.core.Frozen`
|
|
83
|
+
|
|
84
|
+
Frozen — Emits a frozen snapshot of a data source's chosen values
|
|
85
|
+
|
|
86
|
+
Runs on `portable`.
|
|
87
|
+
|
|
88
|
+
**Props**
|
|
89
|
+
|
|
90
|
+
| name | type | default | range |
|
|
91
|
+
| --- | --- | --- | --- |
|
|
92
|
+
| `value` | `any` | `null` | |
|
|
93
|
+
|
|
94
|
+
**Outputs**
|
|
95
|
+
|
|
96
|
+
| name | type | default | range |
|
|
97
|
+
| --- | --- | --- | --- |
|
|
98
|
+
| `value` | `any` | | |
|
|
99
|
+
|
|
82
100
|
### `cascade.core.Input`
|
|
83
101
|
|
|
84
102
|
Input — Defines a public graph or subnet input
|
|
@@ -173,6 +191,54 @@ Runs on `portable`.
|
|
|
173
191
|
| --- | --- | --- | --- |
|
|
174
192
|
| `output` | `any` | | |
|
|
175
193
|
|
|
194
|
+
### `cascade.core.Pack`
|
|
195
|
+
|
|
196
|
+
Pack — Collects inputs into a bundle, one named field each
|
|
197
|
+
|
|
198
|
+
Runs on `portable`.
|
|
199
|
+
|
|
200
|
+
**Inputs**
|
|
201
|
+
|
|
202
|
+
| name | type | default | range |
|
|
203
|
+
| --- | --- | --- | --- |
|
|
204
|
+
| `inputs` | `any` | | |
|
|
205
|
+
|
|
206
|
+
**Props**
|
|
207
|
+
|
|
208
|
+
| name | type | default | range |
|
|
209
|
+
| --- | --- | --- | --- |
|
|
210
|
+
| `names` | `string` | `""` | |
|
|
211
|
+
|
|
212
|
+
**Outputs**
|
|
213
|
+
|
|
214
|
+
| name | type | default | range |
|
|
215
|
+
| --- | --- | --- | --- |
|
|
216
|
+
| `bundle` | `bundle` | | |
|
|
217
|
+
|
|
218
|
+
### `cascade.core.Pick`
|
|
219
|
+
|
|
220
|
+
Pick — Reads one field of a bundle
|
|
221
|
+
|
|
222
|
+
Runs on `portable`.
|
|
223
|
+
|
|
224
|
+
**Inputs**
|
|
225
|
+
|
|
226
|
+
| name | type | default | range |
|
|
227
|
+
| --- | --- | --- | --- |
|
|
228
|
+
| `bundle` | `bundle` | | |
|
|
229
|
+
|
|
230
|
+
**Props**
|
|
231
|
+
|
|
232
|
+
| name | type | default | range |
|
|
233
|
+
| --- | --- | --- | --- |
|
|
234
|
+
| `field` | `string` | `""` | |
|
|
235
|
+
|
|
236
|
+
**Outputs**
|
|
237
|
+
|
|
238
|
+
| name | type | default | range |
|
|
239
|
+
| --- | --- | --- | --- |
|
|
240
|
+
| `value` | `any` | | |
|
|
241
|
+
|
|
176
242
|
### `cascade.core.Previous`
|
|
177
243
|
|
|
178
244
|
Previous — The previous step's state, inside a Feedback container. The first step gets the container's initial value.
|
|
@@ -523,6 +589,39 @@ Runs on `portable`.
|
|
|
523
589
|
| --- | --- | --- | --- |
|
|
524
590
|
| `geometry` | `geometry` | | |
|
|
525
591
|
|
|
592
|
+
### `cascade.image.ContactSheet`
|
|
593
|
+
|
|
594
|
+
Contact Sheet — Lay images out as one labelled grid.
|
|
595
|
+
|
|
596
|
+
Runs on `portable`.
|
|
597
|
+
|
|
598
|
+
**Inputs**
|
|
599
|
+
|
|
600
|
+
| name | type | default | range |
|
|
601
|
+
| --- | --- | --- | --- |
|
|
602
|
+
| `images` | `image` | | |
|
|
603
|
+
| `list` | `array` | | |
|
|
604
|
+
|
|
605
|
+
**Props**
|
|
606
|
+
|
|
607
|
+
| name | type | default | range |
|
|
608
|
+
| --- | --- | --- | --- |
|
|
609
|
+
| `columns` | `int` | `0` | 0…64 |
|
|
610
|
+
| `gap` | `int` | `16` | 0…512 |
|
|
611
|
+
| `background` | `color` | `[1,1,1,1]` | |
|
|
612
|
+
| `cellWidth` | `int` | `0` | 0…8192 |
|
|
613
|
+
| `labels` | `string` | `""` | |
|
|
614
|
+
| `labelSize` | `int` | `14` | 6…128 |
|
|
615
|
+
| `labelColor` | `color` | `[0.1,0.1,0.1,1]` | |
|
|
616
|
+
| `filename` | `string` | `"contact-sheet.png"` | |
|
|
617
|
+
|
|
618
|
+
**Outputs**
|
|
619
|
+
|
|
620
|
+
| name | type | default | range |
|
|
621
|
+
| --- | --- | --- | --- |
|
|
622
|
+
| `image` | `image` | | |
|
|
623
|
+
| `asset` | `asset` | | |
|
|
624
|
+
|
|
526
625
|
### `cascade.math.Binary`
|
|
527
626
|
|
|
528
627
|
Binary Math — Two values, one operation. Component-wise, and a scalar broadcasts against a vector.
|
|
@@ -907,6 +1006,115 @@ Runs on `portable`.
|
|
|
907
1006
|
| --- | --- | --- | --- |
|
|
908
1007
|
| `scene` | `scene` | | |
|
|
909
1008
|
|
|
1009
|
+
## Assertions, headless only
|
|
1010
|
+
|
|
1011
|
+
`cascade.assert.*` fail a `cascade run` (non-zero exit, the message in
|
|
1012
|
+
`run --json`'s `error.message`) when their check does not hold, and pass
|
|
1013
|
+
their input through. The CLI registers them; Studio does not yet, and shows
|
|
1014
|
+
them as unresolved. An image wired only into assertions still counts as the
|
|
1015
|
+
graph's final image for `run --out`.
|
|
1016
|
+
|
|
1017
|
+
### `cascade.assert.Equals`
|
|
1018
|
+
|
|
1019
|
+
Assert Equals — Fails the run unless value equals expected (JSON-equal; numbers within tolerance); passes value through
|
|
1020
|
+
|
|
1021
|
+
Runs on `portable`.
|
|
1022
|
+
|
|
1023
|
+
**Inputs**
|
|
1024
|
+
|
|
1025
|
+
| name | type | default | range |
|
|
1026
|
+
| --- | --- | --- | --- |
|
|
1027
|
+
| `value` | `any` | `null` | |
|
|
1028
|
+
|
|
1029
|
+
**Props**
|
|
1030
|
+
|
|
1031
|
+
| name | type | default | range |
|
|
1032
|
+
| --- | --- | --- | --- |
|
|
1033
|
+
| `expected` | `any` | `null` | |
|
|
1034
|
+
| `tolerance` | `float` | `0` | |
|
|
1035
|
+
| `message` | `string` | `""` | |
|
|
1036
|
+
|
|
1037
|
+
**Outputs**
|
|
1038
|
+
|
|
1039
|
+
| name | type | default | range |
|
|
1040
|
+
| --- | --- | --- | --- |
|
|
1041
|
+
| `value` | `any` | | |
|
|
1042
|
+
|
|
1043
|
+
### `cascade.assert.NotBlank`
|
|
1044
|
+
|
|
1045
|
+
Assert Not Blank — Fails the run when an image (PNG or PPM) is one flat colour or fully transparent; passes the image through
|
|
1046
|
+
|
|
1047
|
+
Runs on `portable`.
|
|
1048
|
+
|
|
1049
|
+
**Inputs**
|
|
1050
|
+
|
|
1051
|
+
| name | type | default | range |
|
|
1052
|
+
| --- | --- | --- | --- |
|
|
1053
|
+
| `image` | `image` | | |
|
|
1054
|
+
|
|
1055
|
+
**Props**
|
|
1056
|
+
|
|
1057
|
+
| name | type | default | range |
|
|
1058
|
+
| --- | --- | --- | --- |
|
|
1059
|
+
| `tolerance` | `int` | `0` | 0…255 |
|
|
1060
|
+
| `message` | `string` | `""` | |
|
|
1061
|
+
|
|
1062
|
+
**Outputs**
|
|
1063
|
+
|
|
1064
|
+
| name | type | default | range |
|
|
1065
|
+
| --- | --- | --- | --- |
|
|
1066
|
+
| `image` | `image` | | |
|
|
1067
|
+
|
|
1068
|
+
### `cascade.assert.Range`
|
|
1069
|
+
|
|
1070
|
+
Assert Range — Fails the run unless value is a finite number in [min, max]; passes value through
|
|
1071
|
+
|
|
1072
|
+
Runs on `portable`.
|
|
1073
|
+
|
|
1074
|
+
**Inputs**
|
|
1075
|
+
|
|
1076
|
+
| name | type | default | range |
|
|
1077
|
+
| --- | --- | --- | --- |
|
|
1078
|
+
| `value` | `float` | `0` | |
|
|
1079
|
+
|
|
1080
|
+
**Props**
|
|
1081
|
+
|
|
1082
|
+
| name | type | default | range |
|
|
1083
|
+
| --- | --- | --- | --- |
|
|
1084
|
+
| `min` | `float` | `0` | |
|
|
1085
|
+
| `max` | `float` | `1` | |
|
|
1086
|
+
| `message` | `string` | `""` | |
|
|
1087
|
+
|
|
1088
|
+
**Outputs**
|
|
1089
|
+
|
|
1090
|
+
| name | type | default | range |
|
|
1091
|
+
| --- | --- | --- | --- |
|
|
1092
|
+
| `value` | `float` | | |
|
|
1093
|
+
|
|
1094
|
+
### `cascade.assert.True`
|
|
1095
|
+
|
|
1096
|
+
Assert True — Fails the run unless value is true (wire a condition, or give the prop an expression); passes value through
|
|
1097
|
+
|
|
1098
|
+
Runs on `portable`.
|
|
1099
|
+
|
|
1100
|
+
**Inputs**
|
|
1101
|
+
|
|
1102
|
+
| name | type | default | range |
|
|
1103
|
+
| --- | --- | --- | --- |
|
|
1104
|
+
| `value` | `bool` | `false` | |
|
|
1105
|
+
|
|
1106
|
+
**Props**
|
|
1107
|
+
|
|
1108
|
+
| name | type | default | range |
|
|
1109
|
+
| --- | --- | --- | --- |
|
|
1110
|
+
| `message` | `string` | `""` | |
|
|
1111
|
+
|
|
1112
|
+
**Outputs**
|
|
1113
|
+
|
|
1114
|
+
| name | type | default | range |
|
|
1115
|
+
| --- | --- | --- | --- |
|
|
1116
|
+
| `value` | `bool` | | |
|
|
1117
|
+
|
|
910
1118
|
## Studio node libraries
|
|
911
1119
|
|
|
912
1120
|
Class-based nodes, available in Studio. Their parameters are declared in the
|
package/doc/PROJECT_AUTHORING.md
CHANGED
|
@@ -83,6 +83,13 @@ my-artwork/
|
|
|
83
83
|
|
|
84
84
|
Keep generated output in a project-specific directory such as `renders/`. Keep reusable algorithms in `nodes/` or a normal TypeScript package rather than in Studio components.
|
|
85
85
|
|
|
86
|
+
Before writing OKLCH conversions, a seeded generator, noise, paper, a pen plot,
|
|
87
|
+
an ffmpeg call, a Cairn or Observatory loader or a disk cache, use the sketch
|
|
88
|
+
kit, `@field/cascade-kit` (`packages/kit`, a `file:` dependency). It also
|
|
89
|
+
generates the stubs for using another project's nodes without a symlink
|
|
90
|
+
(`npx cascade-kit nodes <provider>`). Read [KIT.md](KIT.md). The kit is
|
|
91
|
+
outside the core: Cascade never imports it.
|
|
92
|
+
|
|
86
93
|
## Add a project panel
|
|
87
94
|
|
|
88
95
|
A project can provide an optional dockable Studio panel without adding UI code to Cascade. Create `panels/asset-browser/index.ts`:
|
|
@@ -191,6 +198,19 @@ image-api:
|
|
|
191
198
|
|
|
192
199
|
The proxy accepts HTTPS only, refuses undeclared hosts, strips caller-supplied authorization headers, and never returns the credential value to the page. Studio defers its initial cook when a required credential is missing and links to File → Project Settings.
|
|
193
200
|
|
|
201
|
+
## Design the graph, not one big node
|
|
202
|
+
|
|
203
|
+
A piece is several nodes, each doing one job: load the data, lay it out, colour it, draw it, export it. The usage survey of November 2026 found the opposite in most projects (1 to 5 nodes of 300+ lines each over `lib/`), which leaves the graph as a parameter panel around opaque code. The graph is the record Marcus inspects, so each node boundary should be a place worth looking at, with the parameters that matter on it.
|
|
204
|
+
|
|
205
|
+
Clean graph design is clean code design:
|
|
206
|
+
|
|
207
|
+
- **One responsibility per node**, named for what it does (`layout_rings`, not `main`).
|
|
208
|
+
- **Data flows through typed ports**, never through module-level state or files one node writes and another reads behind the graph's back.
|
|
209
|
+
- **What varies is a parameter**; seeds, counts and colours are on the node, not constants in the code.
|
|
210
|
+
- **Shared helpers go in `lib/` or the kit** (`KIT.md`); the node is the unit you compose and reuse.
|
|
211
|
+
|
|
212
|
+
Split a node when it does two of load, generate, style, render and export, or when its `execute` grows past about 150 lines.
|
|
213
|
+
|
|
194
214
|
## Add a custom node
|
|
195
215
|
|
|
196
216
|
Before writing one, check whether you need one. A parameter can hold an expression, so anything time-based is usually a parameter rather than a node. See [Animate a parameter](#animate-a-parameter). A node earns its place when it produces geometry or pixels, or when several parameters share a computation. The built-in library is listed in `NODE_REFERENCE.md`, generated from the definitions themselves; read that before searching for what a node does.
|
|
@@ -269,6 +289,58 @@ The reason is not tidiness. Two floats cannot connect to a `vec2` output, take t
|
|
|
269
289
|
|
|
270
290
|
Keep them separate only when the components differ in kind or in range — a `width` and a `depth` that mean different things are two props, not a `vec2`.
|
|
271
291
|
|
|
292
|
+
#### Bundle ports
|
|
293
|
+
|
|
294
|
+
When a node would fan out more than three or four outputs that travel together, export one `bundle` instead: a single port carrying named fields. Each field is any Cascade type; an image or cache entry travels as its reference (`ImageRef`, `AssetRef`), never its pixels.
|
|
295
|
+
|
|
296
|
+
```ts
|
|
297
|
+
// Producer: declares the fields it carries, and optionally a schema name.
|
|
298
|
+
outputs: {
|
|
299
|
+
format: {
|
|
300
|
+
kind: 'data', type: 'bundle', schema: 'format',
|
|
301
|
+
fields: {
|
|
302
|
+
size: 'vec2i',
|
|
303
|
+
size_mm: 'vec2',
|
|
304
|
+
dpi: { type: 'float', description: 'Resolution of the working canvas' },
|
|
305
|
+
paper: 'string',
|
|
306
|
+
},
|
|
307
|
+
},
|
|
308
|
+
},
|
|
309
|
+
// in execute:
|
|
310
|
+
context.outputs.format.set({ size, size_mm, dpi, paper });
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
```ts
|
|
314
|
+
// Consumer: declares the fields it reads.
|
|
315
|
+
inputs: {
|
|
316
|
+
format: { kind: 'data', type: 'bundle', schema: 'format', fields: ['size', 'paper'] },
|
|
317
|
+
},
|
|
318
|
+
// in execute:
|
|
319
|
+
const { size, paper } = context.inputs.format ?? {};
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
- **`fields` on an output** maps each field to its type. A producer may set only declared fields; a declared field may be left unset. Without `fields` the bundle is untyped and nothing can check it.
|
|
323
|
+
- **`fields` on an input** lists the fields the node reads. The node then sees only those, and Studio re-cooks it only when one of them changed: a change to `dpi` does not re-cook a consumer that reads `size` and `paper`. Without `fields` a consumer depends on the whole bundle. Field access is not tracked at run time, so declare the selection when the node is expensive.
|
|
324
|
+
- **`schema`** names the bundle's shape. Two named schemas must match to connect; a port without one takes any bundle.
|
|
325
|
+
- **Expressions** read a field of a wired bundle input with a dotted path: `ch("format.dpi")`, `chv("format.size_mm")`, `chs("format.paper")`. The path names an input on the same node, not another node's output, so the wire is the dependency and the producer always cooks first. `ch("/spread-format/format.dpi")` is refused by `cascade check`, which says which wire to add. A field read in an expression counts as a declared field for caching.
|
|
326
|
+
- **`cascade.core.Pick`** reads one field (`field` prop) for a node that wants a single value, and **`cascade.core.Pack`** builds a bundle from loose values, one name per input in its comma-separated `names` prop. Pick is cheap and its output only changes when the picked field does, so it is also how a node that cannot declare fields depends on one.
|
|
327
|
+
- **`cascade check`** reports a field that the producer does not declare, in a consumer's `fields`, in an expression, or on a Pick, with the nearest declared name: `dot-layer.radius reads unknown bundle field "dpu" — did you mean "dpi"?`.
|
|
328
|
+
- **Studio** draws a bundle as one port in the neutral wire colour. Hovering it lists every field with its type and current value, and the Inspector shows each field with its own control.
|
|
329
|
+
|
|
330
|
+
A bundle passed to a Python stage is an ordinary JSON object, so the worker receives a `dict`; reference fields arrive as their small JSON form, e.g. `args["format"]["preview"]["path"]`. A stage that returns a dict can be set straight onto a bundle output:
|
|
331
|
+
|
|
332
|
+
```ts
|
|
333
|
+
const result = await runStage('signals', { format: context.inputs.format });
|
|
334
|
+
context.outputs.signals.set(result as Record<string, unknown>);
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
```python
|
|
338
|
+
def signals(args):
|
|
339
|
+
fmt = args["format"] # a dict
|
|
340
|
+
w, h = fmt["size"]
|
|
341
|
+
return {"brightness": {"path": cache_png(...), "size": [w, h], ...}, "edge_count": 412}
|
|
342
|
+
```
|
|
343
|
+
|
|
272
344
|
### dynamic
|
|
273
345
|
|
|
274
346
|
A dynamic module declares its interface imperatively, inside `execute`, which runs once to discover the interface and again on every cook. There is no `definition` export; `runsOn` and `icon` are plain exports.
|
|
@@ -293,7 +365,7 @@ export async function execute(node: any) {
|
|
|
293
365
|
|
|
294
366
|
`param()` is a method on the node the engine hands to `execute`, not an import. It is re-declared on every cook, so it never resets a value, an expression, or a keyframe someone set: reading `.value` gives the *resolved* value for the current frame, whichever of the three bindings is in force. The loader imports the module as real JavaScript, so top-level code does run at import time. Keep dynamic modules free of top-level effects and put interface declaration and cooking inside `execute`.
|
|
295
367
|
|
|
296
|
-
Prefer `new OffscreenCanvas(width, height)` to `document.createElement('canvas')` when both hosts can run the node. A portable declaration still requires output parity checks; installing Skia does not override a browser-only declaration. Use Python for libraries or tools that require a server. Measure transport, serialization, and cold-start costs; persistent workers can avoid repeated process/model startup.
|
|
368
|
+
Prefer `new OffscreenCanvas(width, height)` to `document.createElement('canvas')` when both hosts can run the node; a definition-v1 node uses `cascade/image` (see "Draw an image in any host"). A portable declaration still requires output parity checks; installing Skia does not override a browser-only declaration. Use Python for libraries or tools that require a server. Measure transport, serialization, and cold-start costs; persistent workers can avoid repeated process/model startup.
|
|
297
369
|
|
|
298
370
|
Choose the narrowest execution locus:
|
|
299
371
|
|
|
@@ -315,6 +387,64 @@ export const definition = {
|
|
|
315
387
|
|
|
316
388
|
The runtime refuses unsupported host/capability combinations before executing the graph.
|
|
317
389
|
|
|
390
|
+
### Draw an image in any host
|
|
391
|
+
|
|
392
|
+
A node that draws, reads or writes an image uses `cascade/image`, and then one
|
|
393
|
+
body cooks in Studio, under `cascade run`/`render`/MCP, and in an exported
|
|
394
|
+
editor's worker. Declare `runsOn: 'portable'` and `capabilities: ['assets']`,
|
|
395
|
+
and import nothing host-specific: no `node:*`, no `@napi-rs/canvas`, no
|
|
396
|
+
`document`.
|
|
397
|
+
|
|
398
|
+
```ts
|
|
399
|
+
import type { NodeDefinition, NodeExecutionContext } from 'cascade/contracts';
|
|
400
|
+
import { readBytes, readImage, registerFont, surface, writeImage } from 'cascade/image';
|
|
401
|
+
|
|
402
|
+
export const definition = {
|
|
403
|
+
apiVersion: 1,
|
|
404
|
+
label: 'Poster',
|
|
405
|
+
runsOn: 'portable',
|
|
406
|
+
capabilities: ['assets'],
|
|
407
|
+
inputs: { photo: { kind: 'data', type: 'image' } },
|
|
408
|
+
outputs: { image: { kind: 'data', type: 'image' } },
|
|
409
|
+
props: { title: { type: 'string', default: 'Hello' }, font: { type: 'string', default: 'assets/fonts/Display.otf' } },
|
|
410
|
+
} as const satisfies NodeDefinition;
|
|
411
|
+
|
|
412
|
+
export async function execute(context: NodeExecutionContext<typeof definition>) {
|
|
413
|
+
const photo = await readImage(context, context.inputs.photo); // an image input, asset or project path
|
|
414
|
+
await registerFont('Display', await readBytes(context, context.props.font));
|
|
415
|
+
const { canvas, context: g } = surface(photo.width, photo.height); // OffscreenCanvas, CPU-backed 2D
|
|
416
|
+
g.drawImage(photo, 0, 0);
|
|
417
|
+
g.font = '96px Display';
|
|
418
|
+
g.fillText(context.props.title, 40, 140);
|
|
419
|
+
context.outputs.image.set(await writeImage(context, canvas, `${context.nodeId}.png`));
|
|
420
|
+
}
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
| | Browser (Studio, exported editor) | Headless (CLI, MCP) |
|
|
424
|
+
| --- | --- | --- |
|
|
425
|
+
| `createCanvas`, `surface` | the realm's `OffscreenCanvas` | Skia's, from `@napi-rs/canvas` |
|
|
426
|
+
| `decodeImage`, `readImage` | `createImageBitmap` | Skia's decoder |
|
|
427
|
+
| `writeImage` | an in-memory generated asset | `.cascade-cache/<name>.<hash>.png` |
|
|
428
|
+
| `registerFont` | `FontFace` added to the page's or worker's font set | Skia's `GlobalFonts` |
|
|
429
|
+
| `readBytes` | shipped files only (`editor.assets`, literal asset props, frozen files) | any project file |
|
|
430
|
+
|
|
431
|
+
- The host chooses where a written image lands; never name a path or read an
|
|
432
|
+
output file back by a path you built. `cachePath` and `saveImage` from
|
|
433
|
+
`cascade/io` remain for dynamic nodes.
|
|
434
|
+
- `surface(w, h)` asks for a `willReadFrequently` context, which keeps Chrome
|
|
435
|
+
on the same software Skia rasteriser the headless host uses. Expect the same
|
|
436
|
+
image, not the same bytes: on commons `plotter-social` the paper, foil and
|
|
437
|
+
wordmark match exactly and scanline marks differ at stroke edges (mean
|
|
438
|
+
channel delta under 0.5 of 255 with the same fonts).
|
|
439
|
+
- A font only matches across hosts when both register the same bytes. A file
|
|
440
|
+
that is not there (a licensed face the export never ships) should fall back
|
|
441
|
+
to a named system face, not fail; read it with `readBytes` in a `try`.
|
|
442
|
+
- What has no browser form is refused, not imitated: no canvas in the realm,
|
|
443
|
+
or `assets` not declared, throws a sentence saying which. A node that needs
|
|
444
|
+
the filesystem, a network fetch or ffmpeg stays `runsOn: 'server'`; to put
|
|
445
|
+
its output in an editor, freeze it (`cascade freeze <graph> <node> --fields
|
|
446
|
+
path,size,channels,depth,space` ships the image file with the snapshot).
|
|
447
|
+
|
|
318
448
|
### GPU nodes today
|
|
319
449
|
|
|
320
450
|
A definition-v1 node may declare `capabilities: ['gpu']`. Studio and the optional
|
|
@@ -512,6 +642,102 @@ It may also accept inline JSON after `--args` for manual compatibility. Projects
|
|
|
512
642
|
with expensive imports may additionally declare a newline-delimited JSON
|
|
513
643
|
`worker`; Cascade keeps that process warm and correlates requests by `id`.
|
|
514
644
|
|
|
645
|
+
Cascade sends the worker one request at a time. When a newer edit supersedes a
|
|
646
|
+
running stage, Cascade withdraws it: a queued request is never sent, and the
|
|
647
|
+
one in flight gets `SIGINT`. A worker that catches `KeyboardInterrupt` around a
|
|
648
|
+
request should answer `{"id": <id>, "ok": false, "cancelled": true}` and keep
|
|
649
|
+
serving, so warm models survive a scrub; it should also ignore an interrupt
|
|
650
|
+
that arrives between requests. A worker that does not handle it exits and is
|
|
651
|
+
respawned on the next call, and one that has not answered within three seconds
|
|
652
|
+
is killed. Whatever a cancelled request returns is discarded. Write cache
|
|
653
|
+
entries atomically (temp file plus rename), since a stage can now be
|
|
654
|
+
interrupted part-way. Nodes can pass `{ signal }` as `runStage`'s third
|
|
655
|
+
argument; without it the signal of the cook in flight is used.
|
|
656
|
+
|
|
657
|
+
#### Stage code, cache keys and the run trace
|
|
658
|
+
|
|
659
|
+
Cascade keys every stage call on four things: the stage code (a hash of every
|
|
660
|
+
`.py` file under `exec.stages.sources`), the arguments with each input file
|
|
661
|
+
replaced by a hash of its bytes, those input hashes, and the Cascade version.
|
|
662
|
+
The key and the cache state are reported on every call, hit or miss: in the
|
|
663
|
+
server log (`[stage] ink: miss (no entry) key 3f2a… code 99dad29d… 1204 ms`),
|
|
664
|
+
in `cascade run --json` under `stages`, and in the run trace.
|
|
665
|
+
|
|
666
|
+
```json
|
|
667
|
+
{
|
|
668
|
+
"exec": {
|
|
669
|
+
"stages": {
|
|
670
|
+
"entrypoint": "runtime/node_cli.py",
|
|
671
|
+
"worker": "runtime/worker.py",
|
|
672
|
+
"sources": ["runtime", "src", "shared/bridge"],
|
|
673
|
+
"cache": { "exclude": ["plot-status"], "outputs": ["out", "svg_out"] }
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
```
|
|
678
|
+
|
|
679
|
+
- `sources` lists the files and folders whose `.py` files are the stage code.
|
|
680
|
+
Without it: the folders of the entrypoint and the worker, plus `src/`. A
|
|
681
|
+
symlinked folder (such as `shared/`) is followed only when it is listed.
|
|
682
|
+
- The warm worker fingerprints those sources when it starts and again before
|
|
683
|
+
every request, and restarts first when they changed, so an edited stage never
|
|
684
|
+
keeps running its old code in a live Studio. Its bytecode cache is keyed by
|
|
685
|
+
the code hash, because Python accepts a `.pyc` whose source kept its size
|
|
686
|
+
within the same second.
|
|
687
|
+
- `cache` controls Cascade's own result cache. A stored result is served only
|
|
688
|
+
while every file it names still has the bytes it had, so a stage that writes
|
|
689
|
+
a fixed path and was re-run with other inputs misses rather than serving the
|
|
690
|
+
overwritten file. See "The stage cache default" below.
|
|
691
|
+
|
|
692
|
+
#### The stage cache default
|
|
693
|
+
|
|
694
|
+
Since 6 Nov 2026 the cache is **on by default for the calls a node's cook
|
|
695
|
+
makes**, and off for everything else. A node is a pure function of its inputs,
|
|
696
|
+
so a stage it calls through `runStage` from `cascade/stage` is treated as pure:
|
|
697
|
+
same stage code, same arguments, same input file bytes, same answer. A second
|
|
698
|
+
identical cook is served from `.cascade/cache/stages/` without starting Python
|
|
699
|
+
at all; on `cloud-plots/relief.cascade` that takes a re-cook from about 4.2 s
|
|
700
|
+
to 0.45 s. A call from a panel (`api.runStage`) or a hand-rolled
|
|
701
|
+
`fetch('/api/exec')` is an action — `plot-start`, `plot-status` — and is not
|
|
702
|
+
cached unless you say so.
|
|
703
|
+
|
|
704
|
+
What is not pure, and must be opted out:
|
|
705
|
+
|
|
706
|
+
- a stage whose answer changes under an identical call: it reads the clock,
|
|
707
|
+
draws fresh randomness, queries a device or a job's progress, or calls a
|
|
708
|
+
network service whose answer you want fresh each time;
|
|
709
|
+
- a stage that reads a file its arguments do not name (a config file, a folder
|
|
710
|
+
it scans, a model downloaded on the side). Only files named in the arguments
|
|
711
|
+
are hashed into the key, so an edit to an unnamed file is invisible;
|
|
712
|
+
- a stage whose purpose is a side effect (copying to a named place, posting
|
|
713
|
+
somewhere). A result whose named files still have their bytes is served, so
|
|
714
|
+
a copy that already happened is not repeated; when the effect must happen
|
|
715
|
+
every time, opt the stage out.
|
|
716
|
+
|
|
717
|
+
How to opt out:
|
|
718
|
+
|
|
719
|
+
```json
|
|
720
|
+
"cache": { "exclude": ["plot-status", "camera-grab"], "outputs": ["out", "svg_out"] }
|
|
721
|
+
```
|
|
722
|
+
|
|
723
|
+
| Declaration | What is cached |
|
|
724
|
+
|---|---|
|
|
725
|
+
| absent | calls from node cooks, except `exclude` (none) |
|
|
726
|
+
| `{ "exclude": [...], "outputs": [...] }` | calls from node cooks, except the named stages |
|
|
727
|
+
| `{ "scope": "all", ... }` or `true` | every call, panel actions included, except `exclude` |
|
|
728
|
+
| `false` or `{ "enabled": false }` | nothing |
|
|
729
|
+
|
|
730
|
+
`CASCADE_STAGE_CACHE=off` in the environment still turns it off for one
|
|
731
|
+
process (`cascade parity` sets it). Every call's trace says what happened and
|
|
732
|
+
why: `hit`, `miss (no entry)`, `off (excluded)`, `off (not a cook call)`,
|
|
733
|
+
`off (not enabled)`. A project that keeps its own Python-side cache can keep
|
|
734
|
+
it; Cascade's sits in front, and on a hit the project's code is not called.
|
|
735
|
+
- A request carries `"cascade": {"key", "code", "cache"}` beside `args` (the
|
|
736
|
+
worker), or `CASCADE_STAGE_ENVELOPE` in the environment (one-shot). A
|
|
737
|
+
project that keeps its own cache should stand aside when `cache` is `"off"`,
|
|
738
|
+
which `cascade parity` sets, and may answer `"cache": "hit" | "miss"` beside
|
|
739
|
+
`result` so the trace shows its state too.
|
|
740
|
+
|
|
515
741
|
## Work with `.cascade` files
|
|
516
742
|
|
|
517
743
|
A graph document stores authored elements separately from runtime state. Nodes and annotations remain flat even inside subnets:
|
|
@@ -550,6 +776,11 @@ cascade run index.cascade --frames 1-100x2 --fps 25 --out frames
|
|
|
550
776
|
cascade run index.cascade --frames 42 --entry-node logo-1024
|
|
551
777
|
```
|
|
552
778
|
|
|
779
|
+
Every run writes a trace to `.cascade/runs/<time>-<graph>.json` (the last 200
|
|
780
|
+
are kept): each node that cooked, its time, and every stage call it made with
|
|
781
|
+
the key and cache state. `--verbose` prints the stage lines as they happen, and
|
|
782
|
+
`--json` prints the status, the seed, the trace path and the stage calls.
|
|
783
|
+
|
|
553
784
|
`--frames` takes a range, a stepped range, or a single frame, and refuses a range it cannot parse rather than repairing it. Files are written as `<out>/<node id>.<frame>.<ext>`, zero-padded to at least four digits, into `renders/` unless `--out` says otherwise. One rule whether the graph has one output or three. Every image output with nothing connected downstream is saved, or the outputs of `--entry-node` when one is named. Cascade stops at the image sequence; a directory of numbered frames is what an encoder takes.
|
|
554
785
|
|
|
555
786
|
Offline, drawing goes through Skia: the optional `@napi-rs/canvas` supplies `OffscreenCanvas`, `Image`, `Path2D` and the rest as globals in the Node process, deliberately not `document`, because that is how a node tells which host it is in. This is why a node that draws on an `OffscreenCanvas` needs no second implementation for headless runs, and why the built-in `cascade.image.*` nodes render under `cascade run` as well as in Studio. A graph that draws nothing runs without the renderer installed.
|
|
@@ -583,6 +814,32 @@ testing the exported artifact for the particular graph. Prefer `cascade build`
|
|
|
583
814
|
for definition-v1 browser graphs, or embed `cascade/runtime` directly when an
|
|
584
815
|
application needs custom capabilities and presentation.
|
|
585
816
|
|
|
817
|
+
## Seeds and parity
|
|
818
|
+
|
|
819
|
+
A graph has one `seed` (an unsigned 32-bit integer, 0 when absent) and each
|
|
820
|
+
node gets `context.seed`, derived from it and the node id, plus
|
|
821
|
+
`context.random()`, a stream seeded with it that restarts at every cook. Both
|
|
822
|
+
hosts derive them the same way, so a node that draws with `context.random`
|
|
823
|
+
draws the same thing headless and in Studio. `Math.random` cannot be
|
|
824
|
+
reproduced by any host; pass `context.seed` on to a Python stage instead of
|
|
825
|
+
seeding there from the clock. `cascade run --seed N` cooks with another seed
|
|
826
|
+
without editing the file.
|
|
827
|
+
|
|
828
|
+
```bash
|
|
829
|
+
cascade parity index.cascade # every node
|
|
830
|
+
cascade parity index.cascade --node ink # one node and what feeds it
|
|
831
|
+
cascade parity index.cascade --tolerance 2 --json
|
|
832
|
+
```
|
|
833
|
+
|
|
834
|
+
`parity` cooks three times with every cache off: headless, headless again, and
|
|
835
|
+
through Studio's own executor (the compatibility graph and its definition
|
|
836
|
+
adapter) run headlessly with the same Dawn GPU and project capabilities. It
|
|
837
|
+
prints one row per node output with both comparisons side by side (`same`,
|
|
838
|
+
`within tolerance`, or the difference: pixels and the largest channel delta
|
|
839
|
+
for an image, both values for a number) and exits 1 when anything differs.
|
|
840
|
+
See [Headless GPU](HEADLESS_GPU.md#determinism) for what makes a GPU stage
|
|
841
|
+
repeatable.
|
|
842
|
+
|
|
586
843
|
## Verify a project
|
|
587
844
|
|
|
588
845
|
Before committing a custom node or graph:
|