@field/cascade 0.10.0 → 0.12.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.
Files changed (229) hide show
  1. package/AGENTS.md +25 -5
  2. package/ARCHITECTURE.md +5 -4
  3. package/CHANGELOG.md +382 -0
  4. package/README.md +1 -2
  5. package/dist/assets/AgentPanel-1JugFjE4.css +1 -0
  6. package/dist/assets/AgentPanel-BDL5MddA.js +5 -0
  7. package/dist/assets/{CodeEditor-DSZBpzdy.js → CodeEditor-C-xYG-Wn.js} +120 -120
  8. package/dist/assets/CodePanel-CtXd50m6.js +2 -0
  9. package/dist/assets/CookInfoPanel-B3NMhKJF.css +1 -0
  10. package/dist/assets/CookInfoPanel-DG4_TRsX.js +2 -0
  11. package/dist/assets/CustomNodeDialog-CHfZUs-r.js +1 -0
  12. package/dist/assets/DefinitionNode-CaRTXQ_b.js +1 -0
  13. package/dist/assets/DefinitionPanel-C3CxZY9r.js +1 -0
  14. package/dist/assets/DefinitionPanel-VNLnAiQD.css +1 -0
  15. package/dist/assets/OpenGraphDialog-BLdr6k_q.js +1 -0
  16. package/dist/assets/ProjectSettings-ZnvmIAir.js +2 -0
  17. package/dist/assets/{SettingsDialog-p25h3tj0.js → SettingsDialog-CBX3Wa87.js} +1 -1
  18. package/dist/assets/TimelinePanel-CDeR0arY.css +1 -0
  19. package/dist/assets/TimelinePanel-CWgnFH1e.js +1 -0
  20. package/dist/assets/{VersionHistoryDialog-DzFLUotB.js → VersionHistoryDialog-BOSoW1R-.js} +1 -1
  21. package/dist/assets/assert-BO2_erPD.js +1 -0
  22. package/dist/assets/assert-hvykGCnP.js +1 -0
  23. package/dist/assets/builder-BmBKcDix.js +1 -0
  24. package/dist/assets/camera-DFF7YA3F.js +1 -0
  25. package/dist/assets/checks-DAvyO9Gf.js +1 -0
  26. package/dist/assets/contactSheet-N1NcaWxw.js +2 -0
  27. package/dist/assets/{core-BICrEt0y.js → core-DB-MkGjj.js} +3 -4
  28. package/dist/assets/coreTypes-DtnDATZ7.js +1 -0
  29. package/dist/assets/{cssMode-D7TXw57I.js → cssMode-DPVckSOc.js} +1 -1
  30. package/dist/assets/definitions-B227e7f_.js +1 -0
  31. package/dist/assets/definitions-D0WOUJd7.js +1 -0
  32. package/dist/assets/{editor.api-DvkCO3ko.js → editor.api-Dkutg_Z1.js} +1 -1
  33. package/dist/assets/expressions-X-Z-GrgW.js +5 -0
  34. package/dist/assets/file-DAL6IfjV.js +1 -0
  35. package/dist/assets/filters-D3k1LRMa.js +1 -0
  36. package/dist/assets/{freemarker2-BA2lxsF1.js → freemarker2-CLiLKk3B.js} +1 -1
  37. package/dist/assets/generators-BhDxRAFC.js +1 -0
  38. package/dist/assets/geo-BsMImJIJ.js +1 -0
  39. package/dist/assets/geo-C4YngDob.js +2 -0
  40. package/dist/assets/{handlebars-BcDiyPd8.js → handlebars-CcD3aMeL.js} +1 -1
  41. package/dist/assets/{html-C7O4TyhX.js → html-DvZWGfqb.js} +1 -1
  42. package/dist/assets/{htmlMode-WaDGF3Mj.js → htmlMode-DUD3VaoN.js} +1 -1
  43. package/dist/assets/image-0SfH-snh.js +1 -0
  44. package/dist/assets/image-Ck-RdGU4.js +2 -0
  45. package/dist/assets/index-BWnrmIB9.js +137 -0
  46. package/dist/assets/index-DIaxEONI.css +1 -0
  47. package/dist/assets/io-BGmt2g0D.js +1 -0
  48. package/dist/assets/{javascript-CpIEK7tz.js → javascript-Cap2ahgh.js} +1 -1
  49. package/dist/assets/{jsonMode-B2FltW0f.js → jsonMode-B0uLjp9C.js} +1 -1
  50. package/dist/assets/{liquid-CN6FFHZE.js → liquid-CnxwhfLn.js} +1 -1
  51. package/dist/assets/{lspLanguageFeatures-QhV9XtLp.js → lspLanguageFeatures-Dn9hlKX6.js} +1 -1
  52. package/dist/assets/math-GeULS-sk.js +1 -0
  53. package/dist/assets/{mdx-DE89pafV.js → mdx-BV3jbrVa.js} +1 -1
  54. package/dist/assets/pop-KMF_qloi.js +1 -0
  55. package/dist/assets/preload-helper-Czpn1I53.js +1 -0
  56. package/dist/assets/{python-ByTllTB9.js → python-CGyFk4XK.js} +1 -1
  57. package/dist/assets/{razor-USYlRRCE.js → razor-CQfzX3-N.js} +1 -1
  58. package/dist/assets/remap-5SjXQU-q.js +1 -0
  59. package/dist/assets/renderMarkdown-ork0Oir7.js +67 -0
  60. package/dist/assets/studioImages-CAyH-0Pl.js +1 -0
  61. package/dist/assets/text-ZpX7qNkx.js +2 -0
  62. package/dist/assets/time-P_OiBhcf.js +1 -0
  63. package/dist/assets/{tsMode-BXpN8iUz.js → tsMode-BOeXzO9H.js} +1 -1
  64. package/dist/assets/{typescript-DInBjGQQ.js → typescript-J1SnE_Tu.js} +1 -1
  65. package/dist/assets/{workers-C7Ye5B7D.js → workers-CYwwTiVF.js} +1 -1
  66. package/dist/assets/{xml-U0Uocqyo.js → xml-DnPRNzK9.js} +1 -1
  67. package/dist/assets/{yaml-CBw_PVYf.js → yaml-BmmYBNa9.js} +1 -1
  68. package/dist/cli/DefinitionNode-P7AA4TAU.js +15 -0
  69. package/dist/cli/Graph-OB6EWSGL.js +2975 -0
  70. package/dist/cli/assert-GROGQNCQ.js +25 -0
  71. package/dist/cli/checks-3WB44XBL.js +189 -0
  72. package/dist/cli/chunk-2JEU3MGB.js +1125 -0
  73. package/dist/cli/chunk-2KXB47FP.js +115 -0
  74. package/dist/cli/chunk-3RXL5GHB.js +816 -0
  75. package/dist/cli/chunk-53LGFPWX.js +138 -0
  76. package/dist/cli/chunk-5ORUQ6ES.js +267 -0
  77. package/dist/cli/chunk-73S4JZXK.js +336 -0
  78. package/dist/cli/chunk-7QE2ZNSN.js +121 -0
  79. package/dist/cli/chunk-BP7UKF36.js +376 -0
  80. package/dist/cli/chunk-BPODRH52.js +201 -0
  81. package/dist/cli/chunk-BZ63XQ3I.js +1301 -0
  82. package/dist/cli/chunk-CKS2FQWR.js +89 -0
  83. package/dist/cli/chunk-CO22XHNG.js +593 -0
  84. package/dist/cli/chunk-CTPLRSWF.js +2197 -0
  85. package/dist/cli/chunk-DEQSEFFH.js +153 -0
  86. package/dist/cli/chunk-EU4ZFZ7Y.js +122 -0
  87. package/dist/cli/chunk-EZRWJ4G2.js +104 -0
  88. package/dist/cli/chunk-FWMSGKEH.js +71 -0
  89. package/dist/cli/chunk-G23A2BZZ.js +705 -0
  90. package/dist/cli/chunk-GRIN2DDP.js +1465 -0
  91. package/dist/cli/chunk-HFHWYL55.js +139 -0
  92. package/dist/cli/chunk-HSNOBIVT.js +263 -0
  93. package/dist/cli/chunk-ICH7N5PN.js +26 -0
  94. package/dist/cli/chunk-J7VK7MNF.js +418 -0
  95. package/dist/cli/chunk-JVOCK5W4.js +83 -0
  96. package/dist/cli/chunk-KBGUO27Q.js +15 -0
  97. package/dist/cli/chunk-KH4MUBKF.js +1551 -0
  98. package/dist/cli/chunk-L6B6FUQP.js +330 -0
  99. package/dist/cli/chunk-LTXWBXOM.js +438 -0
  100. package/dist/cli/chunk-MI5JLQYA.js +220 -0
  101. package/dist/cli/chunk-MP653A5O.js +3766 -0
  102. package/dist/cli/chunk-NJFMDXHR.js +320 -0
  103. package/dist/cli/chunk-OM6BEME6.js +267 -0
  104. package/dist/cli/chunk-OWWBYLHC.js +136 -0
  105. package/dist/cli/chunk-QQPA2ZHH.js +273 -0
  106. package/dist/cli/chunk-RUU3YWXU.js +99 -0
  107. package/dist/cli/chunk-RXDIMXN5.js +98 -0
  108. package/dist/cli/chunk-ST6LROUG.js +267 -0
  109. package/dist/cli/chunk-TJW67QSI.js +225 -0
  110. package/dist/cli/chunk-WD62JIP6.js +118 -0
  111. package/dist/cli/chunk-WGKPJZDY.js +364 -0
  112. package/dist/cli/chunk-X4WKAOQP.js +634 -0
  113. package/dist/cli/chunk-X566FWBV.js +156 -0
  114. package/dist/cli/chunk-YH637PDL.js +317 -0
  115. package/dist/cli/chunk-ZL4JTG5Y.js +37 -0
  116. package/dist/cli/chunk-ZSCUZE6I.js +112 -0
  117. package/dist/cli/cliCommands-JJN5Z3ZC.js +3470 -0
  118. package/dist/cli/commands-PHYFYA3F.js +1374 -0
  119. package/dist/cli/contactSheet-2PQVRUFW.js +11 -0
  120. package/dist/cli/definitions-BPOPW52L.js +32 -0
  121. package/dist/cli/doctorCommand-ORWQRNYP.js +106 -0
  122. package/dist/cli/edit-S5KEQ5RA.js +28 -0
  123. package/dist/cli/editorCommands-NFR32JOY.js +41 -0
  124. package/dist/cli/encode-ICPEPMH5.js +17 -0
  125. package/dist/cli/file-3HTQHPYQ.js +41 -0
  126. package/dist/cli/filters-6ODFWYK4.js +379 -0
  127. package/dist/cli/frames-LSAUBZ7P.js +16 -0
  128. package/dist/cli/generators-YU7GB4MD.js +210 -0
  129. package/dist/cli/geo-6ZPGKZRU.js +43 -0
  130. package/dist/cli/graphCommands-EOUOBOIJ.js +184 -0
  131. package/dist/cli/graphFile-YTGJB5F5.js +292 -0
  132. package/dist/cli/headlessCanvas-ENRHC7GS.js +16 -0
  133. package/dist/cli/helpCommand-PULH6ZYB.js +157 -0
  134. package/dist/cli/image-TXELZZK5.js +55 -0
  135. package/dist/cli/index.js +100 -30756
  136. package/dist/cli/math-RSYRQ3C6.js +16 -0
  137. package/dist/cli/mcp-JVZFQOEX.js +164 -0
  138. package/dist/cli/parity-X6DEH2DV.js +334 -0
  139. package/dist/cli/pop-B7SIIYKN.js +39 -0
  140. package/dist/cli/project-VYGIA7JX.js +12 -0
  141. package/dist/cli/projectDefinition-F3D7UIHJ.js +27 -0
  142. package/dist/cli/projectRuntime-2Q4JKEH3.js +55 -0
  143. package/dist/cli/registerStandardNodes-BXJCB6E4.js +744 -0
  144. package/dist/cli/render-PT4MWI6U.js +438 -0
  145. package/dist/cli/runCommand-HNPQVZMS.js +198 -0
  146. package/dist/cli/runner-5XPXVW7J.js +43 -0
  147. package/dist/cli/runtime/image.ts +302 -0
  148. package/dist/cli/runtime/io.ts +78 -0
  149. package/dist/cli/runtime/stage.ts +4 -1
  150. package/dist/cli/share-MHF6TNRK.js +166 -0
  151. package/dist/cli/stageCache-TY2BFZJR.js +15 -0
  152. package/dist/cli/stageRunner-KY2EJP7X.js +10 -0
  153. package/dist/cli/text-MB3EKR4H.js +126 -0
  154. package/dist/cli/verbs-PI2JH5FI.js +19 -0
  155. package/dist/cli/webBuild-WPPMGGW7.js +44 -0
  156. package/dist/contracts/capabilities.d.ts +18 -1
  157. package/dist/contracts/color.d.ts +8 -0
  158. package/dist/contracts/color.js +23 -0
  159. package/dist/contracts/definition.d.ts +10 -0
  160. package/dist/contracts/documentFormat.d.ts +18 -0
  161. package/dist/contracts/documentFormat.js +195 -0
  162. package/dist/contracts/editor.d.ts +17 -0
  163. package/dist/contracts/index.d.ts +1 -0
  164. package/dist/contracts/index.js +1 -0
  165. package/dist/contracts/schema.d.ts +1 -1
  166. package/dist/contracts/schema.js +1 -0
  167. package/dist/contracts/validate.js +11 -1
  168. package/dist/contracts/values.d.ts +24 -0
  169. package/dist/image.d.ts +156 -0
  170. package/dist/image.js +161 -0
  171. package/dist/index.html +8 -2
  172. package/dist/io.d.ts +29 -0
  173. package/dist/io.js +40 -0
  174. package/dist/player/app.js +3 -2
  175. package/dist/player/editor.js +35 -9
  176. package/dist/player/editorEngine.js +3 -2
  177. package/dist/runtime/builtins/assert/checks.d.ts +18 -0
  178. package/dist/runtime/builtins/assert/checks.js +207 -0
  179. package/dist/runtime/builtins/assert/definitions.d.ts +130 -0
  180. package/dist/runtime/builtins/assert/definitions.js +54 -0
  181. package/dist/runtime/builtins/assert/index.d.ts +152 -0
  182. package/dist/runtime/builtins/assert/index.js +36 -0
  183. package/dist/runtime/builtins/image/contactSheet.d.ts +64 -0
  184. package/dist/runtime/builtins/image/contactSheet.js +150 -0
  185. package/dist/runtime/builtins/image/definitions.d.ts +950 -0
  186. package/dist/runtime/builtins/image/definitions.js +343 -0
  187. package/dist/runtime/builtins/image/file.d.ts +8 -0
  188. package/dist/runtime/builtins/image/file.js +37 -0
  189. package/dist/runtime/builtins/image/filters.d.ts +8 -0
  190. package/dist/runtime/builtins/image/filters.js +394 -0
  191. package/dist/runtime/builtins/image/generators.d.ts +7 -0
  192. package/dist/runtime/builtins/image/generators.js +144 -0
  193. package/dist/runtime/builtins/image/index.d.ts +952 -0
  194. package/dist/runtime/builtins/image/index.js +44 -0
  195. package/dist/runtime/builtins/image/io.d.ts +100 -0
  196. package/dist/runtime/builtins/image/io.js +142 -0
  197. package/dist/runtime/builtins/image/planar.d.ts +55 -0
  198. package/dist/runtime/builtins/image/planar.js +274 -0
  199. package/dist/runtime/builtins/image/simplex.d.ts +22 -0
  200. package/dist/runtime/builtins/image/simplex.js +92 -0
  201. package/dist/runtime/builtins/image/text.d.ts +4 -0
  202. package/dist/runtime/builtins/image/text.js +141 -0
  203. package/dist/runtime/builtins/index.d.ts +3 -0
  204. package/dist/runtime/builtins/index.js +7 -0
  205. package/dist/runtime/gpu.d.ts +25 -0
  206. package/dist/runtime/gpu.js +40 -0
  207. package/dist/runtime/runtime.d.ts +2 -0
  208. package/dist/runtime/runtime.js +268 -44
  209. package/dist/runtime/types.d.ts +30 -0
  210. package/dist/stage.js +4 -1
  211. package/doc/CLI_REFERENCE.md +91 -2
  212. package/doc/EDITOR_EXPORT.md +37 -2
  213. package/doc/MCP.md +18 -1
  214. package/doc/NODE_REFERENCE.md +424 -10
  215. package/doc/PROJECT_AUTHORING.md +222 -7
  216. package/package.json +19 -4
  217. package/dist/assets/CodePanel-sY6sZa7B.js +0 -2
  218. package/dist/assets/CustomNodeDialog-BRKnqZhp.js +0 -1
  219. package/dist/assets/ExportDialog-C7dVGua4.js +0 -358
  220. package/dist/assets/ExportDialog-DOq0flMV.css +0 -1
  221. package/dist/assets/OpenGraphDialog-ZsgL-iwr.js +0 -1
  222. package/dist/assets/ProjectSettings-Cw17nNe3.js +0 -2
  223. package/dist/assets/definitions-DUww2Rt-.js +0 -1
  224. package/dist/assets/geo-C2RHOVHC.js +0 -1
  225. package/dist/assets/image-DR4oeyJB.js +0 -2062
  226. package/dist/assets/index-7vae6YFC.css +0 -1
  227. package/dist/assets/index-DyvDC6pS.js +0 -219
  228. package/dist/assets/math-DaDEgWKt.js +0 -1
  229. package/dist/assets/pop-pLH7L0PW.js +0 -1
@@ -61,6 +61,40 @@ difference. Use a deliberate local package override
61
61
  for framework development rather than committing a machine-specific
62
62
  `file:../../Dev/cascade` dependency to a shared project.
63
63
 
64
+ ### Pin the core
65
+
66
+ A sketch pins one published Cascade, exactly, rather than linking the live
67
+ checkout, so a change on Cascade main cannot move its pixels until it opts in:
68
+
69
+ ```json
70
+ "devDependencies": {
71
+ "cascade": "npm:@field/cascade@0.11.0",
72
+ "@field/cascade-kit": "file:../../Dev/cascade/packages/kit"
73
+ }
74
+ ```
75
+
76
+ Keep the dependency name `cascade`: imports stay `cascade/io`,
77
+ `cascade/stage` and so on, which is what Studio's compiler resolves. Only the
78
+ core is pinned; the kit is not on npm and stays a `file:` dependency. Commit
79
+ the lockfile, since it holds the core's own dependencies (a newer
80
+ `@napi-rs/canvas` shifts antialiasing by a few levels). To move from a link:
81
+
82
+ ```bash
83
+ npm install --save-dev --save-exact cascade@npm:@field/cascade@0.11.0
84
+ ```
85
+
86
+ If `node_modules/cascade` is still a link afterwards, or npm fails with
87
+ `Cannot destructure property 'package' of 'node.target'`, the kit link is the
88
+ cause: its target sits inside the checkout, whose own package is
89
+ `@field/cascade`, so npm reuses the checkout. Move `node_modules` and
90
+ `package-lock.json` aside, install with the kit removed from `package.json`,
91
+ then add it back with `npm install --save-dev ../../Dev/cascade/packages/kit`.
92
+
93
+ To bump: change the version, `npm install`, `npx cascade check <graph>`, and
94
+ render a frame to compare. A sketch that needs something only on main (for
95
+ example `cascade share` before it ships) keeps the `file:` link and says why in
96
+ its README until the next release.
97
+
64
98
  `index.cascade` is the default graph name. A project may contain several `.cascade` files; pass one explicitly when there is no unambiguous default:
65
99
 
66
100
  ```bash
@@ -83,6 +117,13 @@ my-artwork/
83
117
 
84
118
  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
119
 
120
+ Before writing OKLCH conversions, a seeded generator, noise, paper, a pen plot,
121
+ an ffmpeg call, a Cairn or Observatory loader or a disk cache, use the sketch
122
+ kit, `@field/cascade-kit` (`packages/kit`, a `file:` dependency). It also
123
+ generates the stubs for using another project's nodes without a symlink
124
+ (`npx cascade-kit nodes <provider>`). Read [KIT.md](KIT.md). The kit is
125
+ outside the core: Cascade never imports it.
126
+
86
127
  ## Add a project panel
87
128
 
88
129
  A project can provide an optional dockable Studio panel without adding UI code to Cascade. Create `panels/asset-browser/index.ts`:
@@ -191,6 +232,19 @@ image-api:
191
232
 
192
233
  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
234
 
235
+ ## Design the graph, not one big node
236
+
237
+ 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.
238
+
239
+ Clean graph design is clean code design:
240
+
241
+ - **One responsibility per node**, named for what it does (`layout_rings`, not `main`).
242
+ - **Data flows through typed ports**, never through module-level state or files one node writes and another reads behind the graph's back.
243
+ - **What varies is a parameter**; seeds, counts and colours are on the node, not constants in the code.
244
+ - **Shared helpers go in `lib/` or the kit** (`KIT.md`); the node is the unit you compose and reuse.
245
+
246
+ Split a node when it does two of load, generate, style, render and export, or when its `execute` grows past about 150 lines. `cascade check` warns on both (`shape/long-execute`, `shape/mixed-jobs`); the warnings are advice and never fail the check. Drawing with noise and colour is one job's worth of calls, so it only flags a module doing four of the five, or loading and exporting in the same node.
247
+
194
248
  ## Add a custom node
195
249
 
196
250
  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.
@@ -345,7 +399,7 @@ export async function execute(node: any) {
345
399
 
346
400
  `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`.
347
401
 
348
- 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.
402
+ 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.
349
403
 
350
404
  Choose the narrowest execution locus:
351
405
 
@@ -367,6 +421,96 @@ export const definition = {
367
421
 
368
422
  The runtime refuses unsupported host/capability combinations before executing the graph.
369
423
 
424
+ ### Draw an image in any host
425
+
426
+ A node that draws, reads or writes an image uses `cascade/image`, and then one
427
+ body cooks in Studio, under `cascade run`/`render`/MCP, and in an exported
428
+ editor's worker. Declare `runsOn: 'portable'` and `capabilities: ['assets']`,
429
+ and import nothing host-specific: no `node:*`, no `@napi-rs/canvas`, no
430
+ `document`.
431
+
432
+ ```ts
433
+ import type { NodeDefinition, NodeExecutionContext } from 'cascade/contracts';
434
+ import { readBytes, readImage, registerFont, surface, writeImage } from 'cascade/image';
435
+
436
+ export const definition = {
437
+ apiVersion: 1,
438
+ label: 'Poster',
439
+ runsOn: 'portable',
440
+ capabilities: ['assets'],
441
+ inputs: { photo: { kind: 'data', type: 'image' } },
442
+ outputs: { image: { kind: 'data', type: 'image' } },
443
+ props: { title: { type: 'string', default: 'Hello' }, font: { type: 'string', default: 'assets/fonts/Display.otf' } },
444
+ } as const satisfies NodeDefinition;
445
+
446
+ export async function execute(context: NodeExecutionContext<typeof definition>) {
447
+ const photo = await readImage(context, context.inputs.photo); // an image input, asset or project path
448
+ await registerFont('Display', await readBytes(context, context.props.font));
449
+ const { canvas, context: g } = surface(photo.width, photo.height); // OffscreenCanvas, CPU-backed 2D
450
+ g.drawImage(photo, 0, 0);
451
+ g.font = '96px Display';
452
+ g.fillText(context.props.title, 40, 140);
453
+ context.outputs.image.set(await writeImage(context, canvas, `${context.nodeId}.png`));
454
+ }
455
+ ```
456
+
457
+ | | Browser (Studio, exported editor) | Headless (CLI, MCP) |
458
+ | --- | --- | --- |
459
+ | `createCanvas`, `surface` | the realm's `OffscreenCanvas` | Skia's, from `@napi-rs/canvas` |
460
+ | `decodeImage`, `readImage` | `createImageBitmap` | the pixels from memory while the run holds them, else Skia's decoder |
461
+ | `writeImage` | an in-memory generated asset | `.cascade-cache/<name>.<hash>.png`, pixels held in memory and the file written behind |
462
+ | `registerFont` | `FontFace` added to the page's or worker's font set | Skia's `GlobalFonts` |
463
+ | `readBytes` | shipped files only (`editor.assets`, literal asset props, frozen files) | any project file |
464
+
465
+ - The host chooses where a written image lands; never name a path or read an
466
+ output file back by a path you built. `cachePath` and `saveImage` from
467
+ `cascade/io` remain for dynamic nodes.
468
+ - `surface(w, h)` asks for a `willReadFrequently` context, which keeps Chrome
469
+ on the same software Skia rasteriser the headless host uses. Expect the same
470
+ image, not the same bytes: on commons `plotter-social` the paper, foil and
471
+ wordmark match exactly and scanline marks differ at stroke edges (mean
472
+ channel delta under 0.5 of 255 with the same fonts).
473
+ - A font only matches across hosts when both register the same bytes. A file
474
+ that is not there (a licensed face the export never ships) should fall back
475
+ to a named system face, not fail; read it with `readBytes` in a `try`.
476
+ - What has no browser form is refused, not imitated: no canvas in the realm,
477
+ or `assets` not declared, throws a sentence saying which. A node that needs
478
+ the filesystem, a network fetch or ffmpeg stays `runsOn: 'server'`; to put
479
+ its output in an editor, freeze it (`cascade freeze <graph> <node> --fields
480
+ path,size,channels,depth,space` ships the image file with the snapshot).
481
+
482
+ #### How an image travels between nodes
483
+
484
+ An `image` port carries an `ImageRef` (a path plus size, channels, depth and
485
+ space), never the pixels, so a value stays cheap to snapshot, hash, trace and
486
+ send to Python. What sits behind the path is the host's business, and the
487
+ headless host keeps it in memory:
488
+
489
+ - `writeImage` (and `saveImage` in `cascade/io`) hands the canvas to the host
490
+ as `ImagePixels` (8-bit RGBA, sRGB, unpremultiplied, top row first: what
491
+ `getImageData` returns and what a PNG decodes to) through the optional
492
+ `assets.keepImage`. Under `cascade run`/`render` the host holds those pixels,
493
+ names the file by their hash, and writes a lossless PNG at zlib level 1
494
+ behind the run, off the main thread.
495
+ - `readImage` (and `loadBitmap`/`loadImage`) asks `assets.imagePixels` first
496
+ and gets a canvas holding the pixels: no file read and no decode. The next
497
+ node sees the same numbers a PNG round trip would give it.
498
+ - Anything that reads the path as a file gets a whole file: a Python stage
499
+ call, `assets.read`, a run's end, `freeze`, `lock` and `ladder` all wait for
500
+ the write behind, and so does every `runsOn: 'server'` node before it
501
+ starts, since it may read an input with `node:fs`. A run's saved outputs
502
+ (`--out`, `run --frames`, `render`) are encoded from the pixels at full
503
+ compression, never copied from the level-1 intermediate.
504
+ - Hosts without `keepImage` (an exported editor, the web player, and Studio
505
+ for project nodes) encode and store as before, so a node written against
506
+ `readImage`/`writeImage` works unchanged in each. Studio gives `keepImage`
507
+ to the built-in image library only: its images stay in the page under
508
+ `.cascade-studio/…` paths, which `readImage`, `assets.read` and the Viewer
509
+ all resolve, but which name no file a server stage could open.
510
+
511
+ On field-logo (12 canvas nodes at 1024²) this took a frame from 1,791 ms to
512
+ about 860 ms; most of what remains is the nodes' own pixel loops.
513
+
370
514
  ### GPU nodes today
371
515
 
372
516
  A definition-v1 node may declare `capabilities: ['gpu']`. Studio and the optional
@@ -375,6 +519,26 @@ a cache scoped to each node instance for pipelines, samplers, and other
375
519
  resources that outlive one cook. The host clears cached resources on host
376
520
  disposal or device loss and requests a fresh device on a later cook.
377
521
 
522
+ The device is asked for with every limit the adapter allows, not WebGPU's
523
+ defaults: on an M3 Max that is 4 GiB per storage binding (default 128 MiB),
524
+ 1,024 invocations per workgroup (256), 32 KiB of workgroup storage (16 KiB) and
525
+ 10 storage buffers per stage (8). Read what you got from
526
+ `context.capabilities.gpu.limits` and size buffers and workgroups from it. A
527
+ node that cannot run below some limit declares it, and a smaller GPU then fails
528
+ before the cook with a sentence naming the limit:
529
+
530
+ ```ts
531
+ export const definition = {
532
+ apiVersion: 1,
533
+ runsOn: 'portable',
534
+ capabilities: ['gpu'],
535
+ gpuLimits: { maxStorageBufferBindingSize: 512 * 2 ** 20 },
536
+ // ...
537
+ } as const;
538
+ ```
539
+
540
+ `max*` limits are floors; `min*Alignment` limits are ceilings.
541
+
378
542
  Use `runsOn: 'portable'` for shared WebGPU code without browser APIs. Render into
379
543
  a texture and use `readTexture` from `cascade/gpu` for explicit RGBA8 pixels,
380
544
  then encode through the image/IO host. See [headless GPU rendering](HEADLESS_GPU.md)
@@ -606,12 +770,63 @@ in `cascade run --json` under `stages`, and in the run trace.
606
770
  keeps running its old code in a live Studio. Its bytecode cache is keyed by
607
771
  the code hash, because Python accepts a `.pyc` whose source kept its size
608
772
  within the same second.
609
- - `cache` turns on Cascade's own result cache: `true`, or an object with
610
- `exclude` (stages whose answer changes under an identical call) and
611
- `outputs` (arguments naming files a stage writes, which are not inputs). Off
612
- by default. A stored result is served only while every file it names still
613
- has the bytes it had, so a stage that writes a fixed path and was re-run with
614
- other inputs misses rather than serving the overwritten file.
773
+ - `workers` (headless runs only) is how many warm workers `cascade run` keeps
774
+ for the project, and so how many independent nodes it cooks at once: sibling
775
+ stage calls in one frame (the morph's four renders) run side by side. Default
776
+ 2–4, one per four cores and per 8 GB of memory; `CASCADE_STAGE_WORKERS`
777
+ overrides it, and `cascade render` workers keep one each. A second worker
778
+ starts only when a call finds the first busy. Nodes still start in the
779
+ graph's order, so the run trace lists them the same way every run; a stage
780
+ that writes a fixed path shared with another node is the one thing this can
781
+ break, and `"workers": 1` turns it off.
782
+ - `cache` controls Cascade's own result cache. A stored result is served only
783
+ while every file it names still has the bytes it had, so a stage that writes
784
+ a fixed path and was re-run with other inputs misses rather than serving the
785
+ overwritten file. See "The stage cache default" below.
786
+
787
+ #### The stage cache default
788
+
789
+ Since 6 Nov 2026 the cache is **on by default for the calls a node's cook
790
+ makes**, and off for everything else. A node is a pure function of its inputs,
791
+ so a stage it calls through `runStage` from `cascade/stage` is treated as pure:
792
+ same stage code, same arguments, same input file bytes, same answer. A second
793
+ identical cook is served from `.cascade/cache/stages/` without starting Python
794
+ at all; on `cloud-plots/relief.cascade` that takes a re-cook from about 4.2 s
795
+ to 0.45 s. A call from a panel (`api.runStage`) or a hand-rolled
796
+ `fetch('/api/exec')` is an action — `plot-start`, `plot-status` — and is not
797
+ cached unless you say so.
798
+
799
+ What is not pure, and must be opted out:
800
+
801
+ - a stage whose answer changes under an identical call: it reads the clock,
802
+ draws fresh randomness, queries a device or a job's progress, or calls a
803
+ network service whose answer you want fresh each time;
804
+ - a stage that reads a file its arguments do not name (a config file, a folder
805
+ it scans, a model downloaded on the side). Only files named in the arguments
806
+ are hashed into the key, so an edit to an unnamed file is invisible;
807
+ - a stage whose purpose is a side effect (copying to a named place, posting
808
+ somewhere). A result whose named files still have their bytes is served, so
809
+ a copy that already happened is not repeated; when the effect must happen
810
+ every time, opt the stage out.
811
+
812
+ How to opt out:
813
+
814
+ ```json
815
+ "cache": { "exclude": ["plot-status", "camera-grab"], "outputs": ["out", "svg_out"] }
816
+ ```
817
+
818
+ | Declaration | What is cached |
819
+ |---|---|
820
+ | absent | calls from node cooks, except `exclude` (none) |
821
+ | `{ "exclude": [...], "outputs": [...] }` | calls from node cooks, except the named stages |
822
+ | `{ "scope": "all", ... }` or `true` | every call, panel actions included, except `exclude` |
823
+ | `false` or `{ "enabled": false }` | nothing |
824
+
825
+ `CASCADE_STAGE_CACHE=off` in the environment still turns it off for one
826
+ process (`cascade parity` sets it). Every call's trace says what happened and
827
+ why: `hit`, `miss (no entry)`, `off (excluded)`, `off (not a cook call)`,
828
+ `off (not enabled)`. A project that keeps its own Python-side cache can keep
829
+ it; Cascade's sits in front, and on a hit the project's code is not called.
615
830
  - A request carries `"cascade": {"key", "code", "cache"}` beside `args` (the
616
831
  worker), or `CASCADE_STAGE_ENVELOPE` in the environment (one-shot). A
617
832
  project that keeps its own cache should stand aside when `cache` is `"off"`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@field/cascade",
3
- "version": "0.10.0",
3
+ "version": "0.12.0",
4
4
  "description": "Visual programming for generative art",
5
5
  "license": "MIT",
6
6
  "author": "FIELD.IO <marcus@field.io>",
@@ -43,7 +43,8 @@
43
43
  ],
44
44
  "workspaces": [
45
45
  "packages/contracts",
46
- "packages/runtime"
46
+ "packages/runtime",
47
+ "packages/kit"
47
48
  ],
48
49
  "bin": {
49
50
  "cascade": "./dist/cli/index.js"
@@ -105,6 +106,10 @@
105
106
  "types": "./dist/io.d.ts",
106
107
  "default": "./dist/io.js"
107
108
  },
109
+ "./image": {
110
+ "types": "./dist/image.d.ts",
111
+ "default": "./dist/image.js"
112
+ },
108
113
  "./gpu": {
109
114
  "types": "./dist/gpu.d.ts",
110
115
  "default": "./dist/gpu.js"
@@ -129,15 +134,24 @@
129
134
  "dev": "vite",
130
135
  "dev:client": "vite",
131
136
  "build": "node scripts/build.mjs",
132
- "build:workspaces": "npm run build --workspace @cascade/contracts && npm run build --workspace @cascade/runtime",
137
+ "build:workspaces": "npm run build --workspace @cascade/contracts && npm run build --workspace @cascade/runtime && npm run build --workspace @field/cascade-kit",
138
+ "build:kit": "npm run build --workspace @field/cascade-kit",
139
+ "test:kit": "npm run test --workspace @field/cascade-kit",
133
140
  "build:packages": "npm run build:workspaces && node scripts/copy-package-builds.mjs",
134
141
  "check:packages": "npm run check --workspace @cascade/contracts && npm run build --workspace @cascade/contracts && npm run check --workspace @cascade/runtime",
135
- "test:packages": "npm run test --workspace @cascade/contracts && npm run test --workspace @cascade/runtime",
142
+ "test:packages": "npm run test --workspace @cascade/contracts && npm run test --workspace @cascade/runtime && npm run test --workspace @field/cascade-kit",
136
143
  "build:cli": "node scripts/build.mjs --cli",
137
144
  "build:server": "npm --prefix server run build",
138
145
  "prepack": "npm run build",
139
146
  "preview": "vite preview",
140
147
  "check": "svelte-check --tsconfig ./tsconfig.json",
148
+ "verify": "node scripts/verify.mjs",
149
+ "verify:fast": "node scripts/verify.mjs --fast",
150
+ "verify:push": "node scripts/verify.mjs --push",
151
+ "prepare": "node scripts/install-hooks.mjs",
152
+ "check:architecture": "vitest run tests/architecture-",
153
+ "check:bundle": "node scripts/bundle-budget.mjs",
154
+ "check:node-reference": "tsx scripts/build-node-reference.mjs --check",
141
155
  "test": "vitest",
142
156
  "test:run": "vitest run",
143
157
  "test:coverage": "vitest run --coverage",
@@ -171,6 +185,7 @@
171
185
  "svelte": "^5.0.0",
172
186
  "svelte-check": "^4.7.6",
173
187
  "tsx": "^4.23.12",
188
+ "typescript7": "npm:typescript@^7.0.2",
174
189
  "vite": "^8.2.2",
175
190
  "vitest": "^4.1.11"
176
191
  },
@@ -1,2 +0,0 @@
1
- const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/CodeEditor-DSZBpzdy.js","assets/legacy-CTEeXeqF.js","assets/nodeTypeUtils-BDJsc2Ks.js","assets/index-DyvDC6pS.js","assets/index-client-DqYmKstU.js","assets/index-7vae6YFC.css","assets/editor.api-DvkCO3ko.js","assets/editor-1FQrmyyI.css","assets/CodeEditor-DR7u0q7p.css"])))=>i.map(i=>d[i]);
2
- import{Dt as e,G as t,M as n,Ot as r,Q as i,U as a,V as o,W as s,Z as c,ct as l,dt as u,et as d,ht as f,i as p,it as m,jt as h,l as g,nt as _,ot as v,pt as y,r as b,rt as x,st as S,vt as C,yt as w,z as T}from"./legacy-CTEeXeqF.js";import{P as E,et as D}from"./index-DyvDC6pS.js";var O=t(`<div class="loading svelte-1pkj8m">Loading editor...</div>`),k=t(`<div class="error svelte-1pkj8m">Loading...</div>`),A=t(`<div class="error svelte-1pkj8m"> </div>`),j=t(`<div class="panel-wrapper svelte-1pkj8m"><!></div>`);function M(t,M){r(M,!1);let N=()=>w(D,`$sharedContextStore`,P),[P,F]=C(),I=u(null);b(async()=>{let e=await E(()=>import(`./CodeEditor-DSZBpzdy.js`).then(e=>e.t),__vite__mapDeps([0,1,2,3,4,5,6,7,8]));y(I,e.default)});let L=p(M,`panelParams`,8),R=p(M,`panelApi`,8),z=p(M,`graph`,12,void 0),B=p(M,`onRecordHistory`,12,void 0),V=u(null),H=L()?.nodeId;function U(){R()?.close&&R().close()}_(()=>N(),()=>{N()&&(z(N().graph),B(N().onRecordHistory))}),_(()=>c(z()),()=>{z()&&H?y(V,z().getNode(H)||null):y(V,null)}),_(()=>(c(R()),i(V)),()=>{R()?.setTitle&&i(V)&&R().setTitle(`Code: ${i(V).id}`)}),x(),g();var W=j(),G=v(W),K=e=>{var t=s(),r=S(t);{let e=f(()=>(c(z()),d(()=>z().packageManager||null)));n(r,()=>i(I),(t,n)=>{n(t,{get node(){return i(V)},get graph(){return z()},get packageManager(){return i(e)},onClose:U,showCloseButton:!1,get onRecordHistory(){return B()}})})}a(e,t)},q=e=>{var t=O();a(e,t)},J=e=>{var t=k();a(e,t)},Y=e=>{var t=A(),n=l(t);m(()=>o(n,`Node not found: ${H??``}`)),a(e,t)};T(G,e=>{i(V)&&z()&&i(I)?e(K):i(I)?z()?e(Y,-1):e(J,2):e(q,1)}),h(W),a(t,W),e(),F()}export{M as default};
@@ -1 +0,0 @@
1
- import{At as e,Dt as t,G as n,I as r,L as i,O as ee,Ot as a,Q as o,U as s,V as c,W as l,X as u,Z as d,_ as te,at as f,b as p,ct as m,d as h,dt as g,et as _,f as ne,i as v,it as y,jt as b,l as x,lt as S,nt as C,ot as w,p as re,pt as T,rt as E,s as ie,st as D,t as O,u as ae,v as k,z as A}from"./legacy-CTEeXeqF.js";import{H as j,U as M}from"./index-DyvDC6pS.js";var N=n(`<span class="auto-badge svelte-1c1mvak">auto</span>`),P=n(`<span class="radio-path svelte-1c1mvak"> </span>`),F=n(`<input type="text" class="custom-base-input svelte-1c1mvak" placeholder="e.g., cascade.mylib.MyBaseNode" autocomplete="off"/>`),I=n(`<label><input type="radio" name="baseClass" class="svelte-1c1mvak"/> <div class="radio-content svelte-1c1mvak"><div class="radio-header svelte-1c1mvak"><span class="radio-label svelte-1c1mvak"> </span> <!></div> <!></div></label>`),oe=n(`<div class="dialog-overlay svelte-1c1mvak"><div class="dialog svelte-1c1mvak" role="dialog" aria-modal="true" aria-labelledby="custom-node-title"><div class="dialog-header svelte-1c1mvak"><h2 id="custom-node-title" class="svelte-1c1mvak">Create Custom Node</h2> <button class="close-button svelte-1c1mvak" aria-label="Close"><!></button></div> <form class="svelte-1c1mvak"><div class="form-group svelte-1c1mvak"><label for="node-name" class="svelte-1c1mvak">Node Name</label> <input id="node-name" type="text" placeholder="e.g., ParticleField" autocomplete="off" class="svelte-1c1mvak"/> <span class="hint svelte-1c1mvak">The display name for your node</span></div> <div class="form-group svelte-1c1mvak"><label for="module-path" class="svelte-1c1mvak">Module Path</label> <input id="module-path" type="text" placeholder="e.g., project.particle-field" autocomplete="off" class="svelte-1c1mvak"/> <span class="hint svelte-1c1mvak">Unique identifier for the node module <!></span></div> <div class="form-group svelte-1c1mvak"><span class="field-label svelte-1c1mvak">Base Class</span> <div class="base-class-options svelte-1c1mvak"></div></div> <div class="dialog-footer svelte-1c1mvak"><button type="button" class="cancel-button svelte-1c1mvak">Cancel</button> <button type="submit" class="create-button svelte-1c1mvak"><!> Create Node</button></div></form></div></div>`);function L(n,L){a(L,!1);let R=g(),se=[],z=v(L,`open`,12,!1),B=O(),V=g(``),H=g(``),U=g(`cascade.Node`),W=g(``),G=g(!0),K=[{id:`cascade.Node`,label:`Node`,description:`Base class for all nodes - use for general purpose computation`,template:`node`},{id:`cascade.image.ImageNodeBase`,label:`ImageNodeBase`,description:`Image processing base class with canvas/image utilities`,template:`image`},{id:`custom`,label:`Other`,description:`Specify a custom base class path`,template:`node`}];function ce(){T(G,!1)}function le(){let e=`local.${o(V).toLowerCase().replace(/[^a-z0-9]/g,``)}`;(o(H)===e||o(H)===``)&&T(G,!0)}function q(){if(!o(V).trim()||o(U)===`custom`&&!o(W).trim())return;let e=K.find(e=>e.id===o(U));B(`create`,{name:o(V).trim(),modulePath:o(H)||`local.${o(V).toLowerCase().replace(/[^a-z0-9]/g,``)}`,baseClass:o(R),template:e?.template||`node`}),Y()}function J(){Y(),B(`close`)}function Y(){T(V,``),T(H,``),T(U,`cascade.Node`),T(W,``),T(G,!0),z(!1)}function X(e){e.key===`Escape`?J():e.key===`Enter`&&!e.shiftKey&&(e.preventDefault(),q())}let Z=g();C(()=>(o(U),o(W)),()=>{T(R,o(U)===`custom`?o(W):o(U))}),C(()=>(o(G),o(V)),()=>{if(o(G)&&o(V)){let e=o(V).toLowerCase().replace(/[^a-z0-9]/g,``);T(H,`local.${e}`)}}),C(()=>(d(z()),o(Z)),()=>{z()&&o(Z)&&setTimeout(()=>o(Z)?.focus(),50)}),E(),x();var Q=l();u(`keydown`,f,X);var $=D(Q),ue=t=>{var n=oe(),a=w(n),l=w(a),d=S(w(l),2),f=w(d);M(f,{size:18}),b(d),b(l);var g=S(l,2),v=w(g),x=S(w(v),2);p(x),re(x,e=>T(Z,e),()=>o(Z)),e(2),b(v);var C=S(v,2),E=S(w(C),2);p(E);var D=S(E,2),O=S(w(D)),R=e=>{var t=N();s(e,t)};A(O,e=>{o(G)&&e(R)}),b(D),b(C);var z=S(C,2),B=S(w(z),2);r(B,5,()=>K,i,(e,t)=>{var n=I();let r;var i=w(n);p(i);var a,l=S(i,2),d=w(l),f=w(d),h=m(f,!0),g=S(f,2),v=e=>{var n=P(),r=m(n,!0);y(()=>c(r,(o(t),_(()=>o(t).id)))),s(e,n)};A(g,e=>{o(t),_(()=>o(t).id!==`custom`)&&e(v)}),b(d);var x=S(d,2),C=e=>{var t=F();p(t),k(t,()=>o(W),e=>T(W,e)),u(`click`,t,ne(function(e){ie.call(this,L,e)})),s(e,t)};A(x,e=>{o(t),o(U),_(()=>o(t).id===`custom`&&o(U)===`custom`)&&e(C)}),b(l),b(n),y(()=>{r=ee(n,1,`radio-option svelte-1c1mvak`,null,r,{selected:o(U)===o(t).id}),a!==(a=(o(t),_(()=>o(t).id)))&&(i.value=(i.__value=a)??``),c(h,(o(t),_(()=>o(t).label)))}),te(se,[],i,()=>(o(t),_(()=>o(t).id),o(U)),e=>T(U,e)),s(e,n)}),b(B),b(z);var Y=S(z,2),X=w(Y),Q=S(X,2),$=w(Q);j($,{name:`Zap`,size:14}),e(),b(Q),b(Y),b(g),b(a),b(n),y(e=>Q.disabled=e,[()=>(o(V),o(U),o(W),_(()=>!o(V).trim()||o(U)===`custom`&&!o(W).trim()))]),u(`click`,d,J),k(x,()=>o(V),e=>T(V,e)),u(`input`,x,le),k(E,()=>o(H),e=>T(H,e)),u(`input`,E,ce),u(`click`,X,J),u(`submit`,g,ae(q)),u(`click`,n,h(J)),s(t,n)};A($,e=>{z()&&e(ue)}),s(n,Q),t()}export{L as default};