@field/cascade 0.2.22 → 0.3.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 (233) hide show
  1. package/AGENTS.md +47 -7
  2. package/ARCHITECTURE.md +30 -2
  3. package/CHANGELOG.md +107 -0
  4. package/DESIGN.md +10 -5
  5. package/README.md +103 -7
  6. package/dist/assets/CodeEditor-D22fVi-4.js +623 -0
  7. package/dist/assets/CodePanel-DPNEYfcR.js +2 -0
  8. package/dist/assets/CustomNodeDialog-DeHCAvgb.js +1 -0
  9. package/dist/assets/{ExportDialog-DWN4nhXI.js → ExportDialog-BJu2Bpvv.js} +2 -2
  10. package/dist/assets/ProjectSettings-DWpVtWEx.js +2 -0
  11. package/dist/assets/SettingsDialog-za00IS3L.js +1 -0
  12. package/dist/assets/VersionHistoryDialog-DTMGhI0_.js +3 -0
  13. package/dist/assets/{core-Bji4aZUU.js → core-JsyKMO1k.js} +29 -38
  14. package/dist/assets/{cssMode-B6Z_Q1Kh.js → cssMode-emdRheFK.js} +1 -1
  15. package/dist/assets/{editor.api-jMoW_43X.js → editor.api-xHX-fMSo.js} +1 -1
  16. package/dist/assets/{freemarker2-DvvPV6m5.js → freemarker2-C3M8_Gs3.js} +1 -1
  17. package/dist/assets/geo-BAR6lQp5.js +1 -0
  18. package/dist/assets/{handlebars-B8rfuSIa.js → handlebars-CBMurtFk.js} +1 -1
  19. package/dist/assets/{html-DyHm6JCp.js → html-xbLQwknw.js} +1 -1
  20. package/dist/assets/{htmlMode-E9XLWE5u.js → htmlMode-2wKdDTon.js} +1 -1
  21. package/dist/assets/{image-BUcQC7PG.js → image-i1j6nWKe.js} +26 -40
  22. package/dist/assets/index-BDV5xGBZ.css +1 -0
  23. package/dist/assets/index-CMcH8AgP.js +212 -0
  24. package/dist/assets/index-client-BoJgsN9M.js +1 -0
  25. package/dist/assets/{javascript-C_bM5PCN.js → javascript-DJaOo_2I.js} +1 -1
  26. package/dist/assets/{jsonMode-BdyVS7G4.js → jsonMode-Bheas8_0.js} +1 -1
  27. package/dist/assets/{legacy-DIu3XRwl.js → legacy-CAj2_7QE.js} +2 -2
  28. package/dist/assets/{liquid-CDkn3spA.js → liquid-BCj_BGbM.js} +1 -1
  29. package/dist/assets/{lspLanguageFeatures-AB5DMAsr.js → lspLanguageFeatures-BfIsx_xz.js} +1 -1
  30. package/dist/assets/{mdx-BvH_NjDN.js → mdx-DwiQDiVO.js} +1 -1
  31. package/dist/assets/{nodeTypeUtils-CweA3NHd.js → nodeTypeUtils-BBA1JqHx.js} +1 -1
  32. package/dist/assets/{python-B7uii4fi.js → python-BjrB5nqj.js} +1 -1
  33. package/dist/assets/{razor-Z48fWdJc.js → razor-CfoBmGDs.js} +1 -1
  34. package/dist/assets/{tsMode-BDgacVG4.js → tsMode-BcEoMSxp.js} +1 -1
  35. package/dist/assets/{typescript-Derb6Oqa.js → typescript-DIg7TEmQ.js} +1 -1
  36. package/dist/assets/{workers-BtoMX_pm.js → workers-vXYuwtPw.js} +1 -1
  37. package/dist/assets/{xml-B1CaFSrG.js → xml-DexLQIQy.js} +1 -1
  38. package/dist/assets/{yaml-HmIeA0KH.js → yaml-DUnD_1dE.js} +1 -1
  39. package/dist/cli/index.js +16467 -10200
  40. package/dist/cli/runtime/gpu.ts +76 -0
  41. package/dist/cli/runtime/io.ts +93 -7
  42. package/dist/contracts/animation.d.ts +40 -0
  43. package/dist/contracts/animation.js +14 -0
  44. package/dist/contracts/capabilities.d.ts +69 -6
  45. package/dist/contracts/definition.d.ts +60 -2
  46. package/dist/contracts/document.d.ts +15 -0
  47. package/dist/contracts/index.d.ts +1 -0
  48. package/dist/contracts/index.js +1 -0
  49. package/dist/contracts/schema.js +8 -5
  50. package/dist/contracts/validate.d.ts +10 -1
  51. package/dist/contracts/validate.js +20 -3
  52. package/dist/gpu.d.ts +71 -0
  53. package/dist/gpu.js +53 -0
  54. package/dist/graphs/default.cascade +1 -1
  55. package/dist/index.html +5 -5
  56. package/dist/io.d.ts +196 -0
  57. package/dist/io.js +303 -0
  58. package/dist/net.d.ts +1 -0
  59. package/dist/net.js +28 -0
  60. package/dist/runtime/animation/channel.d.ts +108 -0
  61. package/dist/runtime/animation/channel.js +204 -0
  62. package/dist/runtime/animation/frameRange.d.ts +78 -0
  63. package/dist/runtime/animation/frameRange.js +76 -0
  64. package/dist/runtime/animation/index.d.ts +10 -0
  65. package/dist/runtime/animation/index.js +8 -0
  66. package/dist/runtime/builtins/core/random.d.ts +1 -1
  67. package/dist/runtime/builtins/core/remap.d.ts +1 -1
  68. package/dist/runtime/builtins/geo/copyToPoints.d.ts +1 -1
  69. package/dist/runtime/builtins/geo/copyToPoints.js +1 -1
  70. package/dist/runtime/builtins/geo/generators.d.ts +1 -1
  71. package/dist/runtime/builtins/geo/merge.d.ts +1 -1
  72. package/dist/runtime/builtins/geo/svgExport.d.ts +1 -1
  73. package/dist/runtime/builtins/geo/svgExport.js +1 -1
  74. package/dist/runtime/builtins/geo/transform.d.ts +1 -1
  75. package/dist/runtime/builtins/geo/transform.js +1 -1
  76. package/dist/runtime/definition/architecture.d.ts +1 -1
  77. package/dist/runtime/definition/extract.d.ts +1 -1
  78. package/dist/runtime/definition/extract.js +1 -1
  79. package/dist/runtime/error.d.ts +1 -1
  80. package/dist/runtime/expressions/evaluator.d.ts +53 -0
  81. package/dist/runtime/expressions/evaluator.js +193 -0
  82. package/dist/runtime/expressions/index.d.ts +15 -0
  83. package/dist/runtime/expressions/index.js +12 -0
  84. package/dist/runtime/expressions/math.d.ts +75 -0
  85. package/dist/runtime/expressions/math.js +140 -0
  86. package/dist/runtime/expressions/preprocess.d.ts +33 -0
  87. package/dist/runtime/expressions/preprocess.js +102 -0
  88. package/dist/runtime/expressions/scope.d.ts +27 -0
  89. package/dist/runtime/expressions/scope.js +10 -0
  90. package/dist/runtime/expressions/time.d.ts +54 -0
  91. package/dist/runtime/expressions/time.js +71 -0
  92. package/dist/runtime/expressions/types.d.ts +66 -0
  93. package/dist/runtime/expressions/types.js +9 -0
  94. package/dist/runtime/geometry/attributes.d.ts +2 -2
  95. package/dist/runtime/geometry/attributes.js +1 -1
  96. package/dist/runtime/geometry/builder.d.ts +1 -1
  97. package/dist/runtime/geometry/builder.js +1 -1
  98. package/dist/runtime/geometry/copy.d.ts +1 -1
  99. package/dist/runtime/geometry/copy.js +1 -1
  100. package/dist/runtime/geometry/generate.d.ts +1 -1
  101. package/dist/runtime/geometry/groups.d.ts +1 -1
  102. package/dist/runtime/geometry/groups.js +1 -1
  103. package/dist/runtime/geometry/matrix.d.ts +1 -1
  104. package/dist/runtime/geometry/merge.d.ts +1 -1
  105. package/dist/runtime/geometry/merge.js +1 -1
  106. package/dist/runtime/geometry/primitives.d.ts +1 -1
  107. package/dist/runtime/geometry/primitives.js +1 -1
  108. package/dist/runtime/geometry/promote.d.ts +1 -1
  109. package/dist/runtime/geometry/promote.js +1 -1
  110. package/dist/runtime/geometry/resample.d.ts +1 -1
  111. package/dist/runtime/geometry/resample.js +1 -1
  112. package/dist/runtime/geometry/svg.d.ts +1 -1
  113. package/dist/runtime/geometry/svg.js +1 -1
  114. package/dist/runtime/geometry/transform.d.ts +1 -1
  115. package/dist/runtime/geometry/transform.js +1 -1
  116. package/dist/runtime/index.d.ts +4 -1
  117. package/dist/runtime/index.js +4 -1
  118. package/dist/runtime/params/animator.d.ts +74 -0
  119. package/dist/runtime/params/animator.js +137 -0
  120. package/dist/runtime/params/index.d.ts +11 -0
  121. package/dist/runtime/params/index.js +9 -0
  122. package/dist/runtime/params/resolve.d.ts +73 -0
  123. package/dist/runtime/params/resolve.js +62 -0
  124. package/dist/runtime/runtime.d.ts +42 -0
  125. package/dist/runtime/runtime.js +351 -29
  126. package/dist/runtime/types.d.ts +32 -6
  127. package/dist/shell.d.ts +3 -3
  128. package/doc/NODE_REFERENCE.md +395 -0
  129. package/doc/PROJECT_AUTHORING.md +148 -6
  130. package/package.json +30 -3
  131. package/dist/assets/CodeEditor-DoaNEUaM.js +0 -641
  132. package/dist/assets/CodePanel-B0_2XJ-t.js +0 -2
  133. package/dist/assets/CustomNodeDialog-026lzecD.js +0 -1
  134. package/dist/assets/ProjectSettings-BzT164Yx.js +0 -2
  135. package/dist/assets/SettingsDialog-DmHb1V0r.js +0 -1
  136. package/dist/assets/VersionHistoryDialog-hH6VsXLX.js +0 -3
  137. package/dist/assets/geo-CXmCtzMM.js +0 -2
  138. package/dist/assets/index-BXYXSdgh.js +0 -224
  139. package/dist/assets/index-CrNke7SW.css +0 -1
  140. package/dist/assets/index-client-DN_FvjN8.js +0 -1
  141. package/dist/assets/quill-DjZ69wL_.js +0 -121
  142. package/dist/cli/index.js.map +0 -7
  143. package/dist/contracts/capabilities.d.ts.map +0 -1
  144. package/dist/contracts/capabilities.js.map +0 -1
  145. package/dist/contracts/color.d.ts.map +0 -1
  146. package/dist/contracts/color.js.map +0 -1
  147. package/dist/contracts/definition.d.ts.map +0 -1
  148. package/dist/contracts/definition.js.map +0 -1
  149. package/dist/contracts/document.d.ts.map +0 -1
  150. package/dist/contracts/document.js.map +0 -1
  151. package/dist/contracts/geometry.d.ts.map +0 -1
  152. package/dist/contracts/geometry.js.map +0 -1
  153. package/dist/contracts/index.d.ts.map +0 -1
  154. package/dist/contracts/index.js.map +0 -1
  155. package/dist/contracts/schema.d.ts.map +0 -1
  156. package/dist/contracts/schema.js.map +0 -1
  157. package/dist/contracts/validate.d.ts.map +0 -1
  158. package/dist/contracts/validate.js.map +0 -1
  159. package/dist/contracts/values.d.ts.map +0 -1
  160. package/dist/contracts/values.js.map +0 -1
  161. package/dist/graphs/default-orig.cascade +0 -128
  162. package/dist/runtime/browser.d.ts.map +0 -1
  163. package/dist/runtime/browser.js.map +0 -1
  164. package/dist/runtime/builtins/core/index.d.ts.map +0 -1
  165. package/dist/runtime/builtins/core/index.js.map +0 -1
  166. package/dist/runtime/builtins/core/random.d.ts.map +0 -1
  167. package/dist/runtime/builtins/core/random.js.map +0 -1
  168. package/dist/runtime/builtins/core/remap.d.ts.map +0 -1
  169. package/dist/runtime/builtins/core/remap.js.map +0 -1
  170. package/dist/runtime/builtins/core/routing.d.ts.map +0 -1
  171. package/dist/runtime/builtins/core/routing.js.map +0 -1
  172. package/dist/runtime/builtins/core/structural.d.ts.map +0 -1
  173. package/dist/runtime/builtins/core/structural.js.map +0 -1
  174. package/dist/runtime/builtins/geo/copyToPoints.d.ts.map +0 -1
  175. package/dist/runtime/builtins/geo/copyToPoints.js.map +0 -1
  176. package/dist/runtime/builtins/geo/definitions.d.ts.map +0 -1
  177. package/dist/runtime/builtins/geo/definitions.js.map +0 -1
  178. package/dist/runtime/builtins/geo/generators.d.ts.map +0 -1
  179. package/dist/runtime/builtins/geo/generators.js.map +0 -1
  180. package/dist/runtime/builtins/geo/index.d.ts.map +0 -1
  181. package/dist/runtime/builtins/geo/index.js.map +0 -1
  182. package/dist/runtime/builtins/geo/merge.d.ts.map +0 -1
  183. package/dist/runtime/builtins/geo/merge.js.map +0 -1
  184. package/dist/runtime/builtins/geo/namespace.d.ts.map +0 -1
  185. package/dist/runtime/builtins/geo/namespace.js.map +0 -1
  186. package/dist/runtime/builtins/geo/svgExport.d.ts.map +0 -1
  187. package/dist/runtime/builtins/geo/svgExport.js.map +0 -1
  188. package/dist/runtime/builtins/geo/transform.d.ts.map +0 -1
  189. package/dist/runtime/builtins/geo/transform.js.map +0 -1
  190. package/dist/runtime/builtins/index.d.ts.map +0 -1
  191. package/dist/runtime/builtins/index.js.map +0 -1
  192. package/dist/runtime/definition/architecture.d.ts.map +0 -1
  193. package/dist/runtime/definition/architecture.js.map +0 -1
  194. package/dist/runtime/definition/extract.d.ts.map +0 -1
  195. package/dist/runtime/definition/extract.js.map +0 -1
  196. package/dist/runtime/error.d.ts.map +0 -1
  197. package/dist/runtime/error.js.map +0 -1
  198. package/dist/runtime/geometry/attributes.d.ts.map +0 -1
  199. package/dist/runtime/geometry/attributes.js.map +0 -1
  200. package/dist/runtime/geometry/builder.d.ts.map +0 -1
  201. package/dist/runtime/geometry/builder.js.map +0 -1
  202. package/dist/runtime/geometry/copy.d.ts.map +0 -1
  203. package/dist/runtime/geometry/copy.js.map +0 -1
  204. package/dist/runtime/geometry/generate.d.ts.map +0 -1
  205. package/dist/runtime/geometry/generate.js.map +0 -1
  206. package/dist/runtime/geometry/groups.d.ts.map +0 -1
  207. package/dist/runtime/geometry/groups.js.map +0 -1
  208. package/dist/runtime/geometry/index.d.ts.map +0 -1
  209. package/dist/runtime/geometry/index.js.map +0 -1
  210. package/dist/runtime/geometry/matrix.d.ts.map +0 -1
  211. package/dist/runtime/geometry/matrix.js.map +0 -1
  212. package/dist/runtime/geometry/merge.d.ts.map +0 -1
  213. package/dist/runtime/geometry/merge.js.map +0 -1
  214. package/dist/runtime/geometry/primitives.d.ts.map +0 -1
  215. package/dist/runtime/geometry/primitives.js.map +0 -1
  216. package/dist/runtime/geometry/promote.d.ts.map +0 -1
  217. package/dist/runtime/geometry/promote.js.map +0 -1
  218. package/dist/runtime/geometry/resample.d.ts.map +0 -1
  219. package/dist/runtime/geometry/resample.js.map +0 -1
  220. package/dist/runtime/geometry/svg.d.ts.map +0 -1
  221. package/dist/runtime/geometry/svg.js.map +0 -1
  222. package/dist/runtime/geometry/transform.d.ts.map +0 -1
  223. package/dist/runtime/geometry/transform.js.map +0 -1
  224. package/dist/runtime/index.d.ts.map +0 -1
  225. package/dist/runtime/index.js.map +0 -1
  226. package/dist/runtime/node.d.ts.map +0 -1
  227. package/dist/runtime/node.js.map +0 -1
  228. package/dist/runtime/runtime.d.ts.map +0 -1
  229. package/dist/runtime/runtime.js.map +0 -1
  230. package/dist/runtime/types.d.ts.map +0 -1
  231. package/dist/runtime/types.js.map +0 -1
  232. package/dist/shell.js.map +0 -7
  233. package/dist/stage.js.map +0 -7
@@ -0,0 +1,76 @@
1
+ /**
2
+ * `cascade/gpu` — WebGPU's flag namespaces as plain values.
3
+ *
4
+ * Small, and it was load-bearing. TypeScript's DOM library declares the WebGPU
5
+ * *interfaces* but not the constant objects, so the only way to reach
6
+ * `GPUTextureUsage.RENDER_ATTACHMENT` was off `globalThis` — which
7
+ * `architecture/ambient-state` correctly forbids inside a node module, because
8
+ * an ambient read makes a node's inputs invisible to the static reader that
9
+ * lets `cascade check` say anything without cooking.
10
+ *
11
+ * So a sketch had to choose between a rule and a working shader, and it chose
12
+ * a project library file to hide the read in. Both stay honest now: the rule
13
+ * keeps its teeth and a node imports these instead. That is the whole reason
14
+ * this file exists, and it is why it had to land with the capability rather
15
+ * than after it — a rule whose only workaround is removed at the same moment
16
+ * is a rule; one enforced a release earlier is a nuisance.
17
+ *
18
+ * The values are fixed by the WebGPU specification, not by an implementation,
19
+ * which is what makes writing them down safe rather than a guess. They are
20
+ * checked against a real browser's own globals in `tests/gpu-usage-flags.test.ts`
21
+ * — the numbers below were verified against Chrome rather than recalled.
22
+ */
23
+
24
+ /** `GPUBufferUsage`. */
25
+ export const BufferUsage = {
26
+ MAP_READ: 0x0001,
27
+ MAP_WRITE: 0x0002,
28
+ COPY_SRC: 0x0004,
29
+ COPY_DST: 0x0008,
30
+ INDEX: 0x0010,
31
+ VERTEX: 0x0020,
32
+ UNIFORM: 0x0040,
33
+ STORAGE: 0x0080,
34
+ INDIRECT: 0x0100,
35
+ QUERY_RESOLVE: 0x0200,
36
+ } as const;
37
+
38
+ /** `GPUTextureUsage`. */
39
+ export const TextureUsage = {
40
+ COPY_SRC: 0x01,
41
+ COPY_DST: 0x02,
42
+ TEXTURE_BINDING: 0x04,
43
+ STORAGE_BINDING: 0x08,
44
+ RENDER_ATTACHMENT: 0x10,
45
+ /**
46
+ * Chrome 147 has this and it is newer than the rest — verified present
47
+ * 2026-09-08 alongside the others, but do not assume every implementation
48
+ * carries it. Included rather than omitted deliberately: leaving it out
49
+ * would send a node that wants it back to `globalThis`, which is the exact
50
+ * ambient read this file exists to remove, so an honest constant with a
51
+ * caveat beats an omission that forces a rule violation.
52
+ */
53
+ TRANSIENT_ATTACHMENT: 0x20,
54
+ } as const;
55
+
56
+ /** `GPUShaderStage`, for a bind group layout entry's `visibility`. */
57
+ export const ShaderStage = {
58
+ VERTEX: 0x1,
59
+ FRAGMENT: 0x2,
60
+ COMPUTE: 0x4,
61
+ } as const;
62
+
63
+ /** `GPUMapMode`, for `buffer.mapAsync`. */
64
+ export const MapMode = {
65
+ READ: 0x1,
66
+ WRITE: 0x2,
67
+ } as const;
68
+
69
+ /** `GPUColorWrite`, for a colour target's `writeMask`. */
70
+ export const ColorWrite = {
71
+ RED: 0x1,
72
+ GREEN: 0x2,
73
+ BLUE: 0x4,
74
+ ALPHA: 0x8,
75
+ ALL: 0xf,
76
+ } as const;
@@ -43,6 +43,63 @@ export function mediaUrl(path: string, options: MediaOptions = {}): string {
43
43
  return `/api/media/${encoded}${query ? `?${query}` : ''}`;
44
44
  }
45
45
 
46
+ /**
47
+ * The transport a host installs so `cascade/io` can reach project files.
48
+ *
49
+ * Same shape of decision as `cascade/stage`: the page posts to the server,
50
+ * because that is the only door a browser has, and a headless host installs a
51
+ * local transport before it runs the graph. Without one, every read here is a
52
+ * `fetch('/api/media/...')` against a relative URL with no origin to resolve it
53
+ * — which is exactly how a Canvas 2D node came to render in Studio and fail
54
+ * under `cascade run`.
55
+ *
56
+ * Paths stay project-relative on both sides. The bridge, not the node, decides
57
+ * what that resolves to.
58
+ */
59
+ export interface IoBridge {
60
+ /** Read a project file's bytes. */
61
+ read(path: string, options?: MediaOptions): Promise<Uint8Array>;
62
+ /** Write bytes into the project cache. Returns the stored project-relative path. */
63
+ write(path: string, data: Uint8Array): Promise<string>;
64
+ }
65
+
66
+ const IO_BRIDGE_KEY = '__cascadeIoBridge';
67
+
68
+ /** Install the local transport. Returns a disposer, so a test can take it down. */
69
+ export function installIoBridge(bridge: IoBridge | null): () => void {
70
+ const globals = globalThis as Record<string, unknown>;
71
+ const previous = globals[IO_BRIDGE_KEY];
72
+ globals[IO_BRIDGE_KEY] = bridge ?? undefined;
73
+ return () => {
74
+ globals[IO_BRIDGE_KEY] = previous;
75
+ };
76
+ }
77
+
78
+ function localIo(): IoBridge | null {
79
+ const bridge = (globalThis as Record<string, unknown>)[IO_BRIDGE_KEY] as IoBridge | undefined;
80
+ return bridge && typeof bridge.read === 'function' && typeof bridge.write === 'function' ? bridge : null;
81
+ }
82
+
83
+ /**
84
+ * A project file's bytes, over whichever transport this host has. Every read in
85
+ * this module goes through here so the two hosts differ in one place only.
86
+ */
87
+ export async function loadBytes(path: string, options: MediaOptions = { raw: true }): Promise<ArrayBuffer> {
88
+ const bridge = localIo();
89
+ if (bridge) {
90
+ const bytes = await bridge.read(path, options);
91
+ // A Buffer is usually a view into a larger pool, and `loadFloats` builds a
92
+ // typed-array VIEW onto whatever comes back — so hand on the exact bytes.
93
+ const exact = bytes.byteOffset === 0 && bytes.byteLength === bytes.buffer.byteLength
94
+ ? bytes.buffer
95
+ : bytes.slice().buffer;
96
+ return exact as ArrayBuffer;
97
+ }
98
+ const response = await fetch(mediaUrl(path, options));
99
+ if (!response.ok) throw new Error(`cascade/io: cannot load ${path} (${response.status})`);
100
+ return await response.arrayBuffer();
101
+ }
102
+
46
103
  /**
47
104
  * Load a project image as an <img>. Defaults to `raw` — a node is processing
48
105
  * these pixels, not showing them, so a re-encoded copy would be the wrong thing
@@ -50,14 +107,21 @@ export function mediaUrl(path: string, options: MediaOptions = {}): string {
50
107
  */
51
108
  export async function loadImage(path: string, options: MediaOptions = { raw: true }): Promise<HTMLImageElement> {
52
109
  const image = new Image();
53
- image.crossOrigin = 'anonymous';
54
- image.src = mediaUrl(path, options);
110
+ const bridge = localIo();
111
+ if (bridge) {
112
+ // No origin to resolve a URL against; hand the decoder the bytes.
113
+ (image as unknown as { src: unknown }).src = new Uint8Array(await loadBytes(path, options));
114
+ } else {
115
+ image.crossOrigin = 'anonymous';
116
+ image.src = mediaUrl(path, options);
117
+ }
55
118
  await image.decode();
56
119
  return image;
57
120
  }
58
121
 
59
122
  /** Same, as an ImageBitmap — the right input for `texImage2D`. */
60
123
  export async function loadBitmap(path: string, options: MediaOptions = { raw: true }): Promise<ImageBitmap> {
124
+ if (localIo()) return createImageBitmap(new Blob([await loadBytes(path, options)]));
61
125
  const response = await fetch(mediaUrl(path, options));
62
126
  if (!response.ok) throw new Error(`cascade/io: cannot load ${path} (${response.status})`);
63
127
  return createImageBitmap(await response.blob());
@@ -80,6 +144,17 @@ export interface SaveOptions {
80
144
  quality?: number;
81
145
  }
82
146
 
147
+ /**
148
+ * Anything that can produce encoded pixels. The two browser canvases are the
149
+ * usual case; a headless host's canvas offers Skia's own encoder instead, and
150
+ * `saveImage` takes whichever it is handed rather than making a node care.
151
+ */
152
+ export interface ImageSource {
153
+ convertToBlob?(options?: { type?: string; quality?: number }): Promise<Blob>;
154
+ encode?(format: string, quality?: number): Promise<Uint8Array>;
155
+ toBuffer?(mime: string): Uint8Array;
156
+ }
157
+
83
158
  /**
84
159
  * Write pixels into the project and get back the path to hand downstream.
85
160
  * Confined to the cache directory, like every other node output.
@@ -88,7 +163,7 @@ export interface SaveOptions {
88
163
  * node.out('path').setValue(out);
89
164
  */
90
165
  export async function saveImage(
91
- source: HTMLCanvasElement | OffscreenCanvas | Blob,
166
+ source: HTMLCanvasElement | OffscreenCanvas | Blob | ImageSource,
92
167
  path: string,
93
168
  options: SaveOptions = {}
94
169
  ): Promise<string> {
@@ -98,8 +173,14 @@ export async function saveImage(
98
173
 
99
174
  if (source instanceof Blob) {
100
175
  blob = source;
101
- } else if ('convertToBlob' in source) {
176
+ } else if (typeof (source as OffscreenCanvas).convertToBlob === 'function') {
102
177
  blob = await (source as OffscreenCanvas).convertToBlob({ type, quality: options.quality });
178
+ } else if (typeof (source as ImageSource).encode === 'function') {
179
+ // A headless canvas that was handed over unwrapped — Skia's own encoder,
180
+ // rather than a browser API it does not have.
181
+ blob = new Blob([(await (source as ImageSource).encode!(format, options.quality)) as BlobPart], { type });
182
+ } else if (typeof (source as ImageSource).toBuffer === 'function') {
183
+ blob = new Blob([(source as ImageSource).toBuffer!(type) as BlobPart], { type });
103
184
  } else {
104
185
  blob = await new Promise<Blob>((resolve, reject) => {
105
186
  (source as HTMLCanvasElement).toBlob(
@@ -116,6 +197,13 @@ export async function saveImage(
116
197
  /** Write any bytes into the project cache and return the path. */
117
198
  export async function saveBytes(data: Blob | ArrayBuffer | Uint8Array, path: string): Promise<string> {
118
199
  const clean = path.replace(/^\.?\//, '');
200
+ const bridge = localIo();
201
+ if (bridge) {
202
+ const bytes = data instanceof Uint8Array
203
+ ? data
204
+ : new Uint8Array(data instanceof Blob ? await data.arrayBuffer() : data);
205
+ return await bridge.write(clean, bytes);
206
+ }
119
207
  const response = await fetch(`/api/media/${clean.split('/').map(encodeURIComponent).join('/')}`, {
120
208
  method: 'PUT',
121
209
  headers: { 'Content-Type': 'application/octet-stream' },
@@ -204,9 +292,7 @@ function parseNpyHeader(buffer: ArrayBuffer): NpyHeader {
204
292
  * still loads — it just costs a pass.
205
293
  */
206
294
  export async function loadFloats(path: string): Promise<FloatImage> {
207
- const response = await fetch(mediaUrl(path, { raw: true }));
208
- if (!response.ok) throw new Error(`cascade/io: cannot load ${path} (${response.status})`);
209
- const buffer = await response.arrayBuffer();
295
+ const buffer = await loadBytes(path, { raw: true });
210
296
  const header = parseNpyHeader(buffer);
211
297
 
212
298
  if (header.fortran) throw new Error('cascade/io: Fortran-ordered .npy is not supported');
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Animation as it appears in a document.
3
+ *
4
+ * A parameter can be a literal, an expression, or a keyframe channel. All
5
+ * three serialise in one place — the node's `props` map — because a channel
6
+ * that lived in a timeline object would need a registry of targets, and that
7
+ * registry goes stale the moment a node is renamed.
8
+ *
9
+ * A prop with neither an expression nor a channel is still written as a bare
10
+ * value, which is what every existing file on disk contains. The object form
11
+ * appears only when there is something to say beyond the value.
12
+ */
13
+ export type CascadeInterpolation = "constant" | "linear" | "smooth";
14
+ /** One key. `frame` may be fractional. Interpolation governs the segment that
15
+ * LEAVES this key, and is omitted when it is the default (`smooth`). */
16
+ export interface CascadeSerializedKeyframe {
17
+ readonly frame: number;
18
+ readonly value: number;
19
+ readonly interpolation?: CascadeInterpolation;
20
+ }
21
+ /** Keys are written in frame order and hold at most one key per frame. A
22
+ * reader must tolerate neither being true and normalise. */
23
+ export interface CascadeSerializedChannel {
24
+ readonly keys: readonly CascadeSerializedKeyframe[];
25
+ }
26
+ /**
27
+ * The object form of a prop. Written when the parameter carries an expression,
28
+ * a channel, or both.
29
+ *
30
+ * Both is legal, and the resolution order is fixed: **the channel wins and the
31
+ * expression is kept but inert.** Discarding an author's expression because
32
+ * they keyed the parameter is the worse failure of the two — the expression is
33
+ * still there to go back to, and deleting the channel restores it.
34
+ */
35
+ export interface CascadeSerializedProp {
36
+ readonly value: unknown;
37
+ readonly expression?: string;
38
+ readonly channel?: CascadeSerializedChannel;
39
+ }
40
+ //# sourceMappingURL=animation.d.ts.map
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Animation as it appears in a document.
3
+ *
4
+ * A parameter can be a literal, an expression, or a keyframe channel. All
5
+ * three serialise in one place — the node's `props` map — because a channel
6
+ * that lived in a timeline object would need a registry of targets, and that
7
+ * registry goes stale the moment a node is renamed.
8
+ *
9
+ * A prop with neither an expression nor a channel is still written as a bare
10
+ * value, which is what every existing file on disk contains. The object form
11
+ * appears only when there is something to say beyond the value.
12
+ */
13
+ export {};
14
+ //# sourceMappingURL=animation.js.map
@@ -1,4 +1,4 @@
1
- import type { AssetRef, ImageRef, JsonValue, TextureHandle } from "./values.js";
1
+ import type { AssetRef, ImageRef, JsonValue } from "./values.js";
2
2
  export interface CascadeAbortSignal {
3
3
  readonly aborted: boolean;
4
4
  readonly reason?: unknown;
@@ -66,10 +66,73 @@ export interface PythonCapability {
66
66
  progress: ProgressReporter;
67
67
  }): Promise<JsonValue | AssetRef | ImageRef>;
68
68
  }
69
- export interface WebGLCapability {
70
- createTexture(descriptor: JsonValue, options: {
71
- signal: CascadeAbortSignal;
72
- }): Promise<ResourceLease<TextureHandle>>;
69
+ /**
70
+ * A GPU a node can compute on, and the state that has to outlive one cook.
71
+ *
72
+ * This replaces `WebGLCapability`, which nothing implemented, nothing declared
73
+ * and no host installed — its one method was WebGL2-shaped, and keeping both
74
+ * names would have meant two devices in one page. Two devices cannot exchange
75
+ * a texture: an allocation made on one is not bindable, sampleable or copyable
76
+ * on the other, and the failure is a validation error at bind time rather than
77
+ * anything a graph author would recognise. So a shared device is not a
78
+ * preference, it is the precondition for a chain of GPU nodes at all.
79
+ *
80
+ * **Stage 0 deliberately carries three members and no textures.** Ports still
81
+ * pass `image`, so every picture is byte-identical to before; what this buys is
82
+ * one device instead of one per node module, an owner for `device.lost`, and
83
+ * somewhere legitimate to keep a pipeline — which is what lets the
84
+ * `architecture/ambient-state` rule stay strict. The texture store, the
85
+ * generation-stamped handles and the readback are Stage 1, and the shape of
86
+ * this interface is meant to be lived with for a few days first.
87
+ *
88
+ * The `GPUDevice` type comes from `@webgpu/types`, added to this package's
89
+ * `tsconfig.json` as its only `types` entry. Contracts compiles with
90
+ * `lib: ["ES2022"]` and no DOM on purpose — it is the vocabulary both hosts
91
+ * share, so pulling in the whole browser would let a browser-only type reach a
92
+ * portable definition by accident. A hand-written stub was the alternative and
93
+ * would have been worse than a dependency: it would shadow the genuine
94
+ * `GPUDevice` at the node's own call site, so every real method call would
95
+ * stop typechecking.
96
+ *
97
+ * The device is raw on purpose. A wrapped pipeline API would be a second thing
98
+ * to learn, would lag the spec, and would fight the reason to reach for WebGPU
99
+ * at all, which is that you write WGSL and encode passes. The capability wraps
100
+ * only what carries lifetime or identity, because those are the parts a host
101
+ * has to be able to account for.
102
+ */
103
+ export interface GpuCapability {
104
+ /**
105
+ * The one device for this page. Shared by every node declaring `gpu`, which
106
+ * is the whole point — see the note above on why two would be useless.
107
+ */
108
+ readonly device: GPUDevice;
109
+ /**
110
+ * Who is actually rendering, recorded so a difference between two runs of
111
+ * the same graph is attributable rather than a mystery. WebGPU output can
112
+ * differ across adapters, and today there is no way to answer "which one".
113
+ */
114
+ readonly adapterInfo: Readonly<{
115
+ vendor: string;
116
+ architecture: string;
117
+ device: string;
118
+ description: string;
119
+ }>;
120
+ /** What this adapter will actually allow — `maxTextureDimension3D` and the
121
+ * rest. Read at runtime rather than assumed; the 3D default is far below
122
+ * the 2D one and a hard-coded guess would be wrong on some machines. */
123
+ readonly limits: Readonly<Record<string, number>>;
124
+ /**
125
+ * A resource that outlives one cook — a compiled pipeline, a sampler, a
126
+ * bind group layout. Keyed per node by the host, so two instances of one
127
+ * module do not share a cache entry and a module-level `Map` is never the
128
+ * right answer again.
129
+ *
130
+ * `create` runs once per key. `destroy` is called when the entry is dropped:
131
+ * when the device is lost, or when the graph disposes. This is the member
132
+ * that makes `architecture/module-state` keepable — it was previously
133
+ * forbidding the only available way to hold a pipeline.
134
+ */
135
+ cache<T>(key: string, create: (device: GPUDevice) => T, destroy?: (value: T) => void): T;
73
136
  }
74
137
  export interface ShellRunResult {
75
138
  readonly stdout: string;
@@ -102,7 +165,7 @@ export interface CapabilityMap {
102
165
  readonly assets: AssetCapability;
103
166
  readonly media: MediaCapability;
104
167
  readonly python: PythonCapability;
105
- readonly webgl: WebGLCapability;
168
+ readonly gpu: GpuCapability;
106
169
  readonly shell: ShellCapability;
107
170
  }
108
171
  export type NodeCapabilityName = keyof CapabilityMap;
@@ -1,10 +1,33 @@
1
1
  import type { CapabilityMap, CascadeAbortSignal, NodeCapabilityName, ProgressReporter } from "./capabilities.js";
2
2
  import type { CascadeType, CoreType, JsonValue, NamespacedType, SerializableValueForType, ValueForType } from "./values.js";
3
3
  export type RuntimeEnvironment = "portable" | "browser" | "server";
4
- export type CapabilityForEnvironment<E extends RuntimeEnvironment> = E extends "portable" ? "assets" | "media" : E extends "browser" ? "assets" | "media" | "webgl" : "files" | "assets" | "media" | "python" | "shell";
4
+ export type CapabilityForEnvironment<E extends RuntimeEnvironment> = E extends "portable" ? "assets" | "media" : E extends "browser" ? "assets" | "media" | "gpu" : "files" | "assets" | "media" | "python" | "shell";
5
5
  export type DataTypeForEnvironment<E extends RuntimeEnvironment> = E extends "browser" ? CascadeType : Exclude<CascadeType, "texture">;
6
6
  type ControlFor<T extends CascadeType> = T extends "float" ? "number" | "slider" | "range" : T extends "int" ? "int" | "slider" | "range" : T extends "bool" ? "boolean" : T extends "string" ? "text" | "textarea" | "select" : T extends "vec2" | "vec3" | "vec4" | "vec2i" | "vec3i" | "vec4i" ? "vector" : T extends "mat2" | "mat3" | "mat4" ? "matrix" : T extends "color" ? "color" : T extends "image" ? "image" : T extends "asset" ? "asset" | "select" : T extends "texture" ? never : "select";
7
- type NumericMetadata<T extends CascadeType> = T extends "float" | "int" ? {
7
+ /**
8
+ * `min`, `max` and `step`, for the types whose components are numbers.
9
+ *
10
+ * The vectors are included, and were not: they typed all three as `never`, so
11
+ * the six vector types could not carry a range at all. Nothing exercised it
12
+ * because no builtin vec prop has one — but Marcus's standing rule is that
13
+ * anything with an x and a y is one `vec2` rather than two floats, and two
14
+ * floats *can* carry a range while the `vec2` they became could not. So
15
+ * following the convention cost the Inspector its clamp and its drag
16
+ * granularity every time: `field-io-gradient-logo.offset` lost −1..1 step
17
+ * 0.005, `offset.offset` lost −2..2, `ripple.centre` lost −1..2, and every
18
+ * remaining vector candidate across the sketches carries a range too.
19
+ *
20
+ * One range for all components, not one per component. A per-component tuple
21
+ * is a real design question — an `x` and a `y` with different limits is a
22
+ * legitimate thing to want — but it is a second shape for this field and every
23
+ * reader would have to handle both, so it waits for a case that needs it
24
+ * rather than being guessed at now.
25
+ *
26
+ * Matrices stay excluded. Their control is a grid of components with an
27
+ * identity button, and a range across a transform's cells is not a thing
28
+ * anybody has asked for.
29
+ */
30
+ type NumericMetadata<T extends CascadeType> = T extends "float" | "int" | "vec2" | "vec3" | "vec4" | "vec2i" | "vec3i" | "vec4i" ? {
8
31
  readonly min?: number;
9
32
  readonly max?: number;
10
33
  readonly step?: number;
@@ -57,6 +80,26 @@ export type PropDefinition<T extends PropDataType> = {
57
80
  readonly default: SerializableValueForType<T>;
58
81
  readonly label?: string;
59
82
  readonly description?: string;
83
+ /**
84
+ * An expression the prop starts with, so a node can come out of the box
85
+ * already moving — `sine-oscillator` and `ripple` baked in `$T` as dynamic
86
+ * nodes and shipped a static 0 once converted, which is what this restores.
87
+ * Marcus's decision, 2026-09-08, asked as a taste question about his own
88
+ * tool: a node like that should self-animate when dropped.
89
+ *
90
+ * **It is a default, so it loses to a stored value like any other.** A
91
+ * document that saved a plain number for this prop keeps that number and
92
+ * does not silently start animating on load. That is the mirror of the
93
+ * dropped-`params` fault: whatever the author set has to win, and the test
94
+ * for this asserts the losing case rather than the winning one.
95
+ *
96
+ * Applied only when the document stored nothing at all for the prop. It is a
97
+ * real expression once applied, not a hidden fallback — visible in the
98
+ * Inspector, editable, and deletable, which is how a stored expression
99
+ * already reads and the only version that does not lie about where the
100
+ * number came from.
101
+ */
102
+ readonly expression?: string;
60
103
  } & NumericMetadata<T> & AcceptMetadata<T> & SelectMetadata<T>;
61
104
  export type AnyPropDefinition = {
62
105
  [T in PropDataType]: PropDefinition<T>;
@@ -100,6 +143,21 @@ type OutputValue<O> = O extends TriggerOutputDefinition ? {
100
143
  } : never;
101
144
  export type DeclaredCapabilities<D extends NodeDefinition> = Readonly<Pick<CapabilityMap, D["capabilities"] extends readonly NodeCapabilityName[] ? D["capabilities"][number] : never>>;
102
145
  export interface NodeExecutionContext<D extends NodeDefinition> {
146
+ /**
147
+ * This node instance's id, stable for the life of the graph.
148
+ *
149
+ * It exists for one reason: naming a scratch file so that two instances of
150
+ * the same module do not overwrite each other. `field-logo` runs one module
151
+ * three times at three sizes, and `cloud-plots` runs `height-field` twice
152
+ * with opposite mode toggles — a literal path silently loses one of each
153
+ * pair. Pass it to `cachePath(context.nodeId, '.png')` and nothing else.
154
+ *
155
+ * An id is deliberately all a node gets. It names a namespace, not a
156
+ * position: there is no way to reach a parent, a sibling or the graph
157
+ * through it, which is what keeps a definition-v1 node a pure function of
158
+ * its own inputs. Do not parse it.
159
+ */
160
+ readonly nodeId: string;
103
161
  readonly inputs: Readonly<{
104
162
  [K in keyof Entries<D, "inputs">]: InputValue<Entries<D, "inputs">[K]>;
105
163
  }>;
@@ -10,6 +10,21 @@ export interface CascadeDocumentNode {
10
10
  value?: unknown;
11
11
  }>[] | Readonly<Record<string, unknown>>;
12
12
  readonly props?: Readonly<Record<string, unknown>>;
13
+ /**
14
+ * The dynamic engine's stored parameter values, and dead weight here.
15
+ *
16
+ * The deterministic runtime reads `props` and nothing else, so a document
17
+ * that still carries `params` loses every value in it and reports nothing —
18
+ * a converted sketch silently reverts to its defaults and draws something
19
+ * different. Declared so that the runtime can recognise the key and warn
20
+ * about it, which is the only reason it appears in a v1 type.
21
+ *
22
+ * @deprecated Migrate to `props`.
23
+ */
24
+ readonly params?: readonly Readonly<{
25
+ name: string;
26
+ value?: unknown;
27
+ }>[] | Readonly<Record<string, unknown>>;
13
28
  }
14
29
  export interface CascadeDocumentAnnotation {
15
30
  readonly id: string;
@@ -1,3 +1,4 @@
1
+ export * from "./animation.js";
1
2
  export * from "./capabilities.js";
2
3
  export * from "./color.js";
3
4
  export * from "./definition.js";
@@ -1,3 +1,4 @@
1
+ export * from "./animation.js";
1
2
  export * from "./capabilities.js";
2
3
  export * from "./color.js";
3
4
  export * from "./definition.js";
@@ -41,10 +41,13 @@ const dataProperties = {
41
41
  const data = object(dataProperties, ["kind", "type"]);
42
42
  const output = object({ kind: dataProperties.kind, type: dataProperties.type, description }, ["kind", "type"]);
43
43
  const { kind: _kind, variadic: _variadic, ...propProperties } = dataProperties;
44
- const prop = object({ ...propProperties, label: { type: "string" } }, [
45
- "type",
46
- "default",
47
- ]);
44
+ const prop = object({
45
+ ...propProperties,
46
+ label: { type: "string" },
47
+ // Props only, not inputs: an input's value comes from whatever is wired to
48
+ // it, so a default expression there would be describing the wrong thing.
49
+ expression: { type: "string", minLength: 1 },
50
+ }, ["type", "default"]);
48
51
  function environmentCapabilities(runsOn, capabilities) {
49
52
  return {
50
53
  if: { properties: { runsOn: { const: runsOn } } },
@@ -72,7 +75,7 @@ export const nodeDefinitionSchema = {
72
75
  }, ["apiVersion", "runsOn"]),
73
76
  allOf: [
74
77
  environmentCapabilities("portable", ["assets", "media"]),
75
- environmentCapabilities("browser", ["assets", "media", "webgl"]),
78
+ environmentCapabilities("browser", ["assets", "media", "gpu"]),
76
79
  environmentCapabilities("server", [
77
80
  "files",
78
81
  "assets",
@@ -1,4 +1,5 @@
1
- import type { NodeDefinition } from "./definition.js";
1
+ import type { NodeCapabilityName } from "./capabilities.js";
2
+ import type { NodeDefinition, RuntimeEnvironment } from "./definition.js";
2
3
  export interface Diagnostic {
3
4
  readonly phase: string;
4
5
  readonly code: string;
@@ -8,6 +9,14 @@ export interface Diagnostic {
8
9
  readonly line?: number;
9
10
  readonly column?: number;
10
11
  }
12
+ /**
13
+ * The capabilities a definition may *declare* for each environment. Declaring
14
+ * one is not the same as a host installing it — `MediaCapability` is declared
15
+ * here and implemented by nobody — so anything deciding whether a graph can
16
+ * actually run must ask the host, and use this only to classify which host a
17
+ * capability belongs to.
18
+ */
19
+ export declare const CAPABILITIES_BY_ENVIRONMENT: Record<RuntimeEnvironment, readonly NodeCapabilityName[]>;
11
20
  export declare function validateNodeDefinition(value: unknown): readonly Diagnostic[];
12
21
  export declare function assertNodeDefinition(value: unknown): asserts value is NodeDefinition;
13
22
  //# sourceMappingURL=validate.d.ts.map
@@ -5,16 +5,24 @@ const NAMESPACED = /^[a-z][a-z0-9-]*(?:\.[A-Za-z][A-Za-z0-9_]*)+$/;
5
5
  const RESERVED = new Set(["__proto__", "prototype", "constructor"]);
6
6
  const CORE = new Set(CORE_TYPES);
7
7
  const CONTROLS = new Set(["number", "slider", "range", "int", "boolean", "text", "textarea", "select", "vector", "matrix", "color", "image", "asset"]);
8
- const ALLOWED = {
8
+ /**
9
+ * The capabilities a definition may *declare* for each environment. Declaring
10
+ * one is not the same as a host installing it — `MediaCapability` is declared
11
+ * here and implemented by nobody — so anything deciding whether a graph can
12
+ * actually run must ask the host, and use this only to classify which host a
13
+ * capability belongs to.
14
+ */
15
+ export const CAPABILITIES_BY_ENVIRONMENT = {
9
16
  portable: ["assets", "media"],
10
- browser: ["assets", "media", "webgl"],
17
+ browser: ["assets", "media", "gpu"],
11
18
  server: ["files", "assets", "media", "python", "shell"],
12
19
  };
20
+ const ALLOWED = CAPABILITIES_BY_ENVIRONMENT;
13
21
  const fields = (names) => new Set(names.split(" "));
14
22
  const TOP_FIELDS = fields("apiVersion runsOn container capabilities label description icon inputs outputs props");
15
23
  const DATA_FIELDS = fields("kind type default description variadic min max step accept control options");
16
24
  const OUTPUT_FIELDS = fields("kind type description");
17
- const PROP_FIELDS = fields("type default label description min max step accept control options");
25
+ const PROP_FIELDS = fields("type default label description min max step accept control options expression");
18
26
  function diagnostic(code, message, path) {
19
27
  return { phase: "definition", code: `definition/${code}`, message, path };
20
28
  }
@@ -186,6 +194,15 @@ export function validateNodeDefinition(value) {
186
194
  !Number.isFinite(raw.step) ||
187
195
  raw.step <= 0))
188
196
  add("invalid-step", "step must be greater than zero", `${path}.step`);
197
+ if (section === "props" &&
198
+ "expression" in raw &&
199
+ (typeof raw.expression !== "string" || raw.expression.length === 0))
200
+ add("invalid-expression", "expression must be a non-empty string", `${path}.expression`);
201
+ for (const field of ["min", "max"]) {
202
+ if (field in raw &&
203
+ (typeof raw[field] !== "number" || !Number.isFinite(raw[field])))
204
+ add(`invalid-${field}`, `${field} must be a finite number`, `${path}.${field}`);
205
+ }
189
206
  if (typeof raw.min === "number" &&
190
207
  typeof raw.max === "number" &&
191
208
  raw.min > raw.max)