@field/cascade 0.2.22 → 0.3.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 (228) hide show
  1. package/AGENTS.md +47 -7
  2. package/ARCHITECTURE.md +30 -2
  3. package/CHANGELOG.md +89 -0
  4. package/DESIGN.md +10 -5
  5. package/README.md +103 -7
  6. package/dist/assets/CodeEditor-BADFbTJS.js +623 -0
  7. package/dist/assets/CodePanel-faK6PRfz.js +2 -0
  8. package/dist/assets/CustomNodeDialog-DhlKuviy.js +1 -0
  9. package/dist/assets/{ExportDialog-DWN4nhXI.js → ExportDialog-BJu2Bpvv.js} +2 -2
  10. package/dist/assets/ProjectSettings-D6a6kjqR.js +2 -0
  11. package/dist/assets/SettingsDialog-DJZdIvLC.js +1 -0
  12. package/dist/assets/VersionHistoryDialog-C1X3HOhc.js +3 -0
  13. package/dist/assets/{core-Bji4aZUU.js → core-CiuiuWZi.js} +29 -38
  14. package/dist/assets/{cssMode-B6Z_Q1Kh.js → cssMode-D-NwZSMJ.js} +1 -1
  15. package/dist/assets/{editor.api-jMoW_43X.js → editor.api-DT3WEvBd.js} +1 -1
  16. package/dist/assets/{freemarker2-DvvPV6m5.js → freemarker2-eKNYQaiz.js} +1 -1
  17. package/dist/assets/geo-DUvVbLnx.js +1 -0
  18. package/dist/assets/{handlebars-B8rfuSIa.js → handlebars-C2w1ggK2.js} +1 -1
  19. package/dist/assets/{html-DyHm6JCp.js → html-DQb41jyo.js} +1 -1
  20. package/dist/assets/{htmlMode-E9XLWE5u.js → htmlMode-IhNlh6Lv.js} +1 -1
  21. package/dist/assets/{image-BUcQC7PG.js → image-DMCXBY-_.js} +26 -40
  22. package/dist/assets/index-BDV5xGBZ.css +1 -0
  23. package/dist/assets/index-HTCN44kj.js +212 -0
  24. package/dist/assets/index-client-BoJgsN9M.js +1 -0
  25. package/dist/assets/{javascript-C_bM5PCN.js → javascript-BIT6jMLC.js} +1 -1
  26. package/dist/assets/{jsonMode-BdyVS7G4.js → jsonMode-CNfyB1ro.js} +1 -1
  27. package/dist/assets/{legacy-DIu3XRwl.js → legacy-CAj2_7QE.js} +2 -2
  28. package/dist/assets/{liquid-CDkn3spA.js → liquid-CmSmJmOG.js} +1 -1
  29. package/dist/assets/{lspLanguageFeatures-AB5DMAsr.js → lspLanguageFeatures-BqMm3uMP.js} +1 -1
  30. package/dist/assets/{mdx-BvH_NjDN.js → mdx-6r6YgOFw.js} +1 -1
  31. package/dist/assets/{nodeTypeUtils-CweA3NHd.js → nodeTypeUtils-BBA1JqHx.js} +1 -1
  32. package/dist/assets/{python-B7uii4fi.js → python-x1H0TQuI.js} +1 -1
  33. package/dist/assets/{razor-Z48fWdJc.js → razor-Dzl1vZFm.js} +1 -1
  34. package/dist/assets/{tsMode-BDgacVG4.js → tsMode-n6DMMe3a.js} +1 -1
  35. package/dist/assets/{typescript-Derb6Oqa.js → typescript-ziEz65xN.js} +1 -1
  36. package/dist/assets/{workers-BtoMX_pm.js → workers-D-qUdp-r.js} +1 -1
  37. package/dist/assets/{xml-B1CaFSrG.js → xml-DMZjt5lE.js} +1 -1
  38. package/dist/assets/{yaml-HmIeA0KH.js → yaml-DFe5qZSO.js} +1 -1
  39. package/dist/cli/index.js +16194 -10195
  40. package/dist/cli/runtime/io.ts +93 -7
  41. package/dist/contracts/animation.d.ts +40 -0
  42. package/dist/contracts/animation.js +14 -0
  43. package/dist/contracts/definition.d.ts +39 -1
  44. package/dist/contracts/document.d.ts +15 -0
  45. package/dist/contracts/index.d.ts +1 -0
  46. package/dist/contracts/index.js +1 -0
  47. package/dist/contracts/validate.d.ts +10 -1
  48. package/dist/contracts/validate.js +9 -1
  49. package/dist/graphs/default.cascade +1 -1
  50. package/dist/index.html +5 -5
  51. package/dist/io.d.ts +196 -0
  52. package/dist/io.js +303 -0
  53. package/dist/net.d.ts +1 -0
  54. package/dist/net.js +28 -0
  55. package/dist/runtime/animation/channel.d.ts +108 -0
  56. package/dist/runtime/animation/channel.js +204 -0
  57. package/dist/runtime/animation/frameRange.d.ts +78 -0
  58. package/dist/runtime/animation/frameRange.js +76 -0
  59. package/dist/runtime/animation/index.d.ts +10 -0
  60. package/dist/runtime/animation/index.js +8 -0
  61. package/dist/runtime/builtins/core/random.d.ts +1 -1
  62. package/dist/runtime/builtins/core/remap.d.ts +1 -1
  63. package/dist/runtime/builtins/geo/copyToPoints.d.ts +1 -1
  64. package/dist/runtime/builtins/geo/copyToPoints.js +1 -1
  65. package/dist/runtime/builtins/geo/generators.d.ts +1 -1
  66. package/dist/runtime/builtins/geo/merge.d.ts +1 -1
  67. package/dist/runtime/builtins/geo/svgExport.d.ts +1 -1
  68. package/dist/runtime/builtins/geo/svgExport.js +1 -1
  69. package/dist/runtime/builtins/geo/transform.d.ts +1 -1
  70. package/dist/runtime/builtins/geo/transform.js +1 -1
  71. package/dist/runtime/definition/architecture.d.ts +1 -1
  72. package/dist/runtime/definition/extract.d.ts +1 -1
  73. package/dist/runtime/definition/extract.js +1 -1
  74. package/dist/runtime/error.d.ts +1 -1
  75. package/dist/runtime/expressions/evaluator.d.ts +53 -0
  76. package/dist/runtime/expressions/evaluator.js +193 -0
  77. package/dist/runtime/expressions/index.d.ts +15 -0
  78. package/dist/runtime/expressions/index.js +12 -0
  79. package/dist/runtime/expressions/math.d.ts +75 -0
  80. package/dist/runtime/expressions/math.js +140 -0
  81. package/dist/runtime/expressions/preprocess.d.ts +33 -0
  82. package/dist/runtime/expressions/preprocess.js +102 -0
  83. package/dist/runtime/expressions/scope.d.ts +27 -0
  84. package/dist/runtime/expressions/scope.js +10 -0
  85. package/dist/runtime/expressions/time.d.ts +54 -0
  86. package/dist/runtime/expressions/time.js +71 -0
  87. package/dist/runtime/expressions/types.d.ts +66 -0
  88. package/dist/runtime/expressions/types.js +9 -0
  89. package/dist/runtime/geometry/attributes.d.ts +2 -2
  90. package/dist/runtime/geometry/attributes.js +1 -1
  91. package/dist/runtime/geometry/builder.d.ts +1 -1
  92. package/dist/runtime/geometry/builder.js +1 -1
  93. package/dist/runtime/geometry/copy.d.ts +1 -1
  94. package/dist/runtime/geometry/copy.js +1 -1
  95. package/dist/runtime/geometry/generate.d.ts +1 -1
  96. package/dist/runtime/geometry/groups.d.ts +1 -1
  97. package/dist/runtime/geometry/groups.js +1 -1
  98. package/dist/runtime/geometry/matrix.d.ts +1 -1
  99. package/dist/runtime/geometry/merge.d.ts +1 -1
  100. package/dist/runtime/geometry/merge.js +1 -1
  101. package/dist/runtime/geometry/primitives.d.ts +1 -1
  102. package/dist/runtime/geometry/primitives.js +1 -1
  103. package/dist/runtime/geometry/promote.d.ts +1 -1
  104. package/dist/runtime/geometry/promote.js +1 -1
  105. package/dist/runtime/geometry/resample.d.ts +1 -1
  106. package/dist/runtime/geometry/resample.js +1 -1
  107. package/dist/runtime/geometry/svg.d.ts +1 -1
  108. package/dist/runtime/geometry/svg.js +1 -1
  109. package/dist/runtime/geometry/transform.d.ts +1 -1
  110. package/dist/runtime/geometry/transform.js +1 -1
  111. package/dist/runtime/index.d.ts +4 -1
  112. package/dist/runtime/index.js +4 -1
  113. package/dist/runtime/params/animator.d.ts +74 -0
  114. package/dist/runtime/params/animator.js +137 -0
  115. package/dist/runtime/params/index.d.ts +11 -0
  116. package/dist/runtime/params/index.js +9 -0
  117. package/dist/runtime/params/resolve.d.ts +73 -0
  118. package/dist/runtime/params/resolve.js +62 -0
  119. package/dist/runtime/runtime.d.ts +22 -0
  120. package/dist/runtime/runtime.js +242 -14
  121. package/dist/runtime/types.d.ts +30 -4
  122. package/dist/shell.d.ts +3 -3
  123. package/doc/NODE_REFERENCE.md +395 -0
  124. package/doc/PROJECT_AUTHORING.md +148 -6
  125. package/package.json +26 -3
  126. package/dist/assets/CodeEditor-DoaNEUaM.js +0 -641
  127. package/dist/assets/CodePanel-B0_2XJ-t.js +0 -2
  128. package/dist/assets/CustomNodeDialog-026lzecD.js +0 -1
  129. package/dist/assets/ProjectSettings-BzT164Yx.js +0 -2
  130. package/dist/assets/SettingsDialog-DmHb1V0r.js +0 -1
  131. package/dist/assets/VersionHistoryDialog-hH6VsXLX.js +0 -3
  132. package/dist/assets/geo-CXmCtzMM.js +0 -2
  133. package/dist/assets/index-BXYXSdgh.js +0 -224
  134. package/dist/assets/index-CrNke7SW.css +0 -1
  135. package/dist/assets/index-client-DN_FvjN8.js +0 -1
  136. package/dist/assets/quill-DjZ69wL_.js +0 -121
  137. package/dist/cli/index.js.map +0 -7
  138. package/dist/contracts/capabilities.d.ts.map +0 -1
  139. package/dist/contracts/capabilities.js.map +0 -1
  140. package/dist/contracts/color.d.ts.map +0 -1
  141. package/dist/contracts/color.js.map +0 -1
  142. package/dist/contracts/definition.d.ts.map +0 -1
  143. package/dist/contracts/definition.js.map +0 -1
  144. package/dist/contracts/document.d.ts.map +0 -1
  145. package/dist/contracts/document.js.map +0 -1
  146. package/dist/contracts/geometry.d.ts.map +0 -1
  147. package/dist/contracts/geometry.js.map +0 -1
  148. package/dist/contracts/index.d.ts.map +0 -1
  149. package/dist/contracts/index.js.map +0 -1
  150. package/dist/contracts/schema.d.ts.map +0 -1
  151. package/dist/contracts/schema.js.map +0 -1
  152. package/dist/contracts/validate.d.ts.map +0 -1
  153. package/dist/contracts/validate.js.map +0 -1
  154. package/dist/contracts/values.d.ts.map +0 -1
  155. package/dist/contracts/values.js.map +0 -1
  156. package/dist/graphs/default-orig.cascade +0 -128
  157. package/dist/runtime/browser.d.ts.map +0 -1
  158. package/dist/runtime/browser.js.map +0 -1
  159. package/dist/runtime/builtins/core/index.d.ts.map +0 -1
  160. package/dist/runtime/builtins/core/index.js.map +0 -1
  161. package/dist/runtime/builtins/core/random.d.ts.map +0 -1
  162. package/dist/runtime/builtins/core/random.js.map +0 -1
  163. package/dist/runtime/builtins/core/remap.d.ts.map +0 -1
  164. package/dist/runtime/builtins/core/remap.js.map +0 -1
  165. package/dist/runtime/builtins/core/routing.d.ts.map +0 -1
  166. package/dist/runtime/builtins/core/routing.js.map +0 -1
  167. package/dist/runtime/builtins/core/structural.d.ts.map +0 -1
  168. package/dist/runtime/builtins/core/structural.js.map +0 -1
  169. package/dist/runtime/builtins/geo/copyToPoints.d.ts.map +0 -1
  170. package/dist/runtime/builtins/geo/copyToPoints.js.map +0 -1
  171. package/dist/runtime/builtins/geo/definitions.d.ts.map +0 -1
  172. package/dist/runtime/builtins/geo/definitions.js.map +0 -1
  173. package/dist/runtime/builtins/geo/generators.d.ts.map +0 -1
  174. package/dist/runtime/builtins/geo/generators.js.map +0 -1
  175. package/dist/runtime/builtins/geo/index.d.ts.map +0 -1
  176. package/dist/runtime/builtins/geo/index.js.map +0 -1
  177. package/dist/runtime/builtins/geo/merge.d.ts.map +0 -1
  178. package/dist/runtime/builtins/geo/merge.js.map +0 -1
  179. package/dist/runtime/builtins/geo/namespace.d.ts.map +0 -1
  180. package/dist/runtime/builtins/geo/namespace.js.map +0 -1
  181. package/dist/runtime/builtins/geo/svgExport.d.ts.map +0 -1
  182. package/dist/runtime/builtins/geo/svgExport.js.map +0 -1
  183. package/dist/runtime/builtins/geo/transform.d.ts.map +0 -1
  184. package/dist/runtime/builtins/geo/transform.js.map +0 -1
  185. package/dist/runtime/builtins/index.d.ts.map +0 -1
  186. package/dist/runtime/builtins/index.js.map +0 -1
  187. package/dist/runtime/definition/architecture.d.ts.map +0 -1
  188. package/dist/runtime/definition/architecture.js.map +0 -1
  189. package/dist/runtime/definition/extract.d.ts.map +0 -1
  190. package/dist/runtime/definition/extract.js.map +0 -1
  191. package/dist/runtime/error.d.ts.map +0 -1
  192. package/dist/runtime/error.js.map +0 -1
  193. package/dist/runtime/geometry/attributes.d.ts.map +0 -1
  194. package/dist/runtime/geometry/attributes.js.map +0 -1
  195. package/dist/runtime/geometry/builder.d.ts.map +0 -1
  196. package/dist/runtime/geometry/builder.js.map +0 -1
  197. package/dist/runtime/geometry/copy.d.ts.map +0 -1
  198. package/dist/runtime/geometry/copy.js.map +0 -1
  199. package/dist/runtime/geometry/generate.d.ts.map +0 -1
  200. package/dist/runtime/geometry/generate.js.map +0 -1
  201. package/dist/runtime/geometry/groups.d.ts.map +0 -1
  202. package/dist/runtime/geometry/groups.js.map +0 -1
  203. package/dist/runtime/geometry/index.d.ts.map +0 -1
  204. package/dist/runtime/geometry/index.js.map +0 -1
  205. package/dist/runtime/geometry/matrix.d.ts.map +0 -1
  206. package/dist/runtime/geometry/matrix.js.map +0 -1
  207. package/dist/runtime/geometry/merge.d.ts.map +0 -1
  208. package/dist/runtime/geometry/merge.js.map +0 -1
  209. package/dist/runtime/geometry/primitives.d.ts.map +0 -1
  210. package/dist/runtime/geometry/primitives.js.map +0 -1
  211. package/dist/runtime/geometry/promote.d.ts.map +0 -1
  212. package/dist/runtime/geometry/promote.js.map +0 -1
  213. package/dist/runtime/geometry/resample.d.ts.map +0 -1
  214. package/dist/runtime/geometry/resample.js.map +0 -1
  215. package/dist/runtime/geometry/svg.d.ts.map +0 -1
  216. package/dist/runtime/geometry/svg.js.map +0 -1
  217. package/dist/runtime/geometry/transform.d.ts.map +0 -1
  218. package/dist/runtime/geometry/transform.js.map +0 -1
  219. package/dist/runtime/index.d.ts.map +0 -1
  220. package/dist/runtime/index.js.map +0 -1
  221. package/dist/runtime/node.d.ts.map +0 -1
  222. package/dist/runtime/node.js.map +0 -1
  223. package/dist/runtime/runtime.d.ts.map +0 -1
  224. package/dist/runtime/runtime.js.map +0 -1
  225. package/dist/runtime/types.d.ts.map +0 -1
  226. package/dist/runtime/types.js.map +0 -1
  227. package/dist/shell.js.map +0 -7
  228. package/dist/stage.js.map +0 -7
@@ -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
@@ -4,7 +4,30 @@ export type RuntimeEnvironment = "portable" | "browser" | "server";
4
4
  export type CapabilityForEnvironment<E extends RuntimeEnvironment> = E extends "portable" ? "assets" | "media" : E extends "browser" ? "assets" | "media" | "webgl" : "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;
@@ -100,6 +123,21 @@ type OutputValue<O> = O extends TriggerOutputDefinition ? {
100
123
  } : never;
101
124
  export type DeclaredCapabilities<D extends NodeDefinition> = Readonly<Pick<CapabilityMap, D["capabilities"] extends readonly NodeCapabilityName[] ? D["capabilities"][number] : never>>;
102
125
  export interface NodeExecutionContext<D extends NodeDefinition> {
126
+ /**
127
+ * This node instance's id, stable for the life of the graph.
128
+ *
129
+ * It exists for one reason: naming a scratch file so that two instances of
130
+ * the same module do not overwrite each other. `field-logo` runs one module
131
+ * three times at three sizes, and `cloud-plots` runs `height-field` twice
132
+ * with opposite mode toggles — a literal path silently loses one of each
133
+ * pair. Pass it to `cachePath(context.nodeId, '.png')` and nothing else.
134
+ *
135
+ * An id is deliberately all a node gets. It names a namespace, not a
136
+ * position: there is no way to reach a parent, a sibling or the graph
137
+ * through it, which is what keeps a definition-v1 node a pure function of
138
+ * its own inputs. Do not parse it.
139
+ */
140
+ readonly nodeId: string;
103
141
  readonly inputs: Readonly<{
104
142
  [K in keyof Entries<D, "inputs">]: InputValue<Entries<D, "inputs">[K]>;
105
143
  }>;
@@ -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";
@@ -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,11 +5,19 @@ 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
17
  browser: ["assets", "media", "webgl"],
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");
@@ -9,7 +9,7 @@
9
9
  "nodes": [
10
10
  {
11
11
  "id": "Color1",
12
- "module": "cascade.lens.Color",
12
+ "module": "cascade.image.Color",
13
13
  "position": [0, 0],
14
14
  "source": "stdlib",
15
15
  "props": {
package/dist/index.html CHANGED
@@ -4,11 +4,11 @@
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
6
  <title>Cascade</title>
7
- <script type="module" crossorigin src="/assets/index-BXYXSdgh.js"></script>
8
- <link rel="modulepreload" crossorigin href="/assets/legacy-DIu3XRwl.js">
9
- <link rel="modulepreload" crossorigin href="/assets/index-client-DN_FvjN8.js">
10
- <link rel="modulepreload" crossorigin href="/assets/nodeTypeUtils-CweA3NHd.js">
11
- <link rel="stylesheet" crossorigin href="/assets/index-CrNke7SW.css">
7
+ <script type="module" crossorigin src="/assets/index-HTCN44kj.js"></script>
8
+ <link rel="modulepreload" crossorigin href="/assets/legacy-CAj2_7QE.js">
9
+ <link rel="modulepreload" crossorigin href="/assets/index-client-BoJgsN9M.js">
10
+ <link rel="modulepreload" crossorigin href="/assets/nodeTypeUtils-BBA1JqHx.js">
11
+ <link rel="stylesheet" crossorigin href="/assets/index-BDV5xGBZ.css">
12
12
  </head>
13
13
  <body>
14
14
  <div id="app"></div>
package/dist/io.d.ts ADDED
@@ -0,0 +1,196 @@
1
+ /**
2
+ * `cascade/io` — the runtime a BROWSER-side node imports.
3
+ *
4
+ * The interop rests on one rule: an image port carries a project-relative PATH,
5
+ * never pixels. A Python node writes a file and emits its path; a WebGL node
6
+ * reads that path as a texture, renders, writes its result back, and emits a
7
+ * path of the same kind. Neither knows or cares which side produced its input,
8
+ * which is what makes the two interchangeable — and it lets the heavy stages
9
+ * stay in Python while the interactive ones run in the page.
10
+ *
11
+ * The counterpart on the server side is the project's own exec bridge
12
+ * (`runStage`), which is per-project because the stages are.
13
+ *
14
+ * This file is compiled into any node module that imports 'cascade/io'; it is
15
+ * not published to npm and has no dependencies.
16
+ */
17
+ export interface MediaOptions {
18
+ /** Ask for a resized copy. Never upscales past the file's own resolution. */
19
+ width?: number;
20
+ /** 'webp' | 'png' | 'jpeg' | 'avif'. Defaults to WebP, which is what you want
21
+ * for anything being looked at rather than processed. */
22
+ format?: string;
23
+ /** The original bytes, unresized and unconverted. Use this when the pixels
24
+ * matter — reading a render back for further processing, not previewing it. */
25
+ raw?: boolean;
26
+ quality?: number;
27
+ }
28
+ /** Build the URL that serves a project file to the page. */
29
+ export declare function mediaUrl(path: string, options?: MediaOptions): string;
30
+ /**
31
+ * The transport a host installs so `cascade/io` can reach project files.
32
+ *
33
+ * Same shape of decision as `cascade/stage`: the page posts to the server,
34
+ * because that is the only door a browser has, and a headless host installs a
35
+ * local transport before it runs the graph. Without one, every read here is a
36
+ * `fetch('/api/media/...')` against a relative URL with no origin to resolve it
37
+ * — which is exactly how a Canvas 2D node came to render in Studio and fail
38
+ * under `cascade run`.
39
+ *
40
+ * Paths stay project-relative on both sides. The bridge, not the node, decides
41
+ * what that resolves to.
42
+ */
43
+ export interface IoBridge {
44
+ /** Read a project file's bytes. */
45
+ read(path: string, options?: MediaOptions): Promise<Uint8Array>;
46
+ /** Write bytes into the project cache. Returns the stored project-relative path. */
47
+ write(path: string, data: Uint8Array): Promise<string>;
48
+ }
49
+ /** Install the local transport. Returns a disposer, so a test can take it down. */
50
+ export declare function installIoBridge(bridge: IoBridge | null): () => void;
51
+ /**
52
+ * A project file's bytes, over whichever transport this host has. Every read in
53
+ * this module goes through here so the two hosts differ in one place only.
54
+ */
55
+ export declare function loadBytes(path: string, options?: MediaOptions): Promise<ArrayBuffer>;
56
+ /**
57
+ * Load a project image as an <img>. Defaults to `raw` — a node is processing
58
+ * these pixels, not showing them, so a re-encoded copy would be the wrong thing
59
+ * to hand it. Pass a width explicitly to work at a smaller size on purpose.
60
+ */
61
+ export declare function loadImage(path: string, options?: MediaOptions): Promise<HTMLImageElement>;
62
+ /** Same, as an ImageBitmap — the right input for `texImage2D`. */
63
+ export declare function loadBitmap(path: string, options?: MediaOptions): Promise<ImageBitmap>;
64
+ /**
65
+ * A .npy field (a mask, a signal, a height or density field) rendered to
66
+ * greyscale, ready to upload as a texture. The numeric array itself stays on
67
+ * the server side for now; this is the visual form of it.
68
+ */
69
+ export declare function loadFieldBitmap(path: string, options?: MediaOptions): Promise<ImageBitmap>;
70
+ export interface SaveOptions {
71
+ /** 'png' (default), 'webp' or 'jpeg'. PNG is the default because the usual
72
+ * consumer of a node's output is another node, and a lossy intermediate
73
+ * compounds down a pipeline. Choose webp when the output is a preview. */
74
+ format?: 'png' | 'webp' | 'jpeg';
75
+ quality?: number;
76
+ }
77
+ /**
78
+ * Anything that can produce encoded pixels. The two browser canvases are the
79
+ * usual case; a headless host's canvas offers Skia's own encoder instead, and
80
+ * `saveImage` takes whichever it is handed rather than making a node care.
81
+ */
82
+ export interface ImageSource {
83
+ convertToBlob?(options?: {
84
+ type?: string;
85
+ quality?: number;
86
+ }): Promise<Blob>;
87
+ encode?(format: string, quality?: number): Promise<Uint8Array>;
88
+ toBuffer?(mime: string): Uint8Array;
89
+ }
90
+ /**
91
+ * Write pixels into the project and get back the path to hand downstream.
92
+ * Confined to the cache directory, like every other node output.
93
+ *
94
+ * const out = await saveImage(canvas, cachePath(node.id, '.png'));
95
+ * node.out('path').setValue(out);
96
+ */
97
+ export declare function saveImage(source: HTMLCanvasElement | OffscreenCanvas | Blob | ImageSource, path: string, options?: SaveOptions): Promise<string>;
98
+ /** Write any bytes into the project cache and return the path. */
99
+ export declare function saveBytes(data: Blob | ArrayBuffer | Uint8Array, path: string): Promise<string>;
100
+ /**
101
+ * The output path convention, shared with the Python side: scoped by node id so
102
+ * one node's scratch file never lands on a sibling's. The server puts the cache
103
+ * key into the filename when it writes, so different inputs keep separate files.
104
+ */
105
+ export declare function cachePath(nodeId: string, suffix: string): string;
106
+ /**
107
+ * Float pixel data, shared between Python and JavaScript with no conversion.
108
+ *
109
+ * This works because a `.npy` file holding little-endian float32 in C order IS
110
+ * a Float32Array with a header in front of it. numpy pads that header so the
111
+ * data begins on a 64-byte boundary — measured on this project's own files, it
112
+ * starts at byte 128 every time — which means a typed array can be constructed
113
+ * as a VIEW onto the fetched buffer rather than a copy of it. A 10 MB density
114
+ * field costs one fetch and no marshalling.
115
+ *
116
+ * From there it goes straight where you need it: into `texImage2D` as an R32F
117
+ * or RGBA32F texture, into a compute pass, or through `toImageData` for
118
+ * Canvas2D. And `saveFloats` writes the same layout back, so `np.load` on the
119
+ * Python side reads it as an ordinary array.
120
+ */
121
+ export interface FloatImage {
122
+ data: Float32Array;
123
+ /** [width, height] */
124
+ size: [number, number];
125
+ /** 1 for a mask or signal, 3 for RGB, 4 for RGBA. */
126
+ channels: number;
127
+ }
128
+ /**
129
+ * Read a float image as a zero-copy Float32Array view. Non-float and
130
+ * non-native-endian arrays are converted rather than refused, so a uint8 mask
131
+ * still loads — it just costs a pass.
132
+ */
133
+ export declare function loadFloats(path: string): Promise<FloatImage>;
134
+ /** Write float pixels back into the project. Python reads the result with a
135
+ * plain `np.load`. Returns the stored path. */
136
+ export declare function saveFloats(image: FloatImage, path: string): Promise<string>;
137
+ /**
138
+ * Float pixels -> ImageData for Canvas2D. Values are assumed to be 0..1 unless
139
+ * a range is given; pass the field's actual range to see it the way the viewer
140
+ * does rather than clipped.
141
+ */
142
+ export declare function toImageData(image: FloatImage, range?: [number, number]): ImageData;
143
+ /**
144
+ * Upload float pixels as a GPU texture, no conversion on the way. Needs the
145
+ * EXT_color_buffer_float extension to render INTO one; sampling works without.
146
+ */
147
+ export declare function uploadFloatTexture(gl: WebGL2RenderingContext, image: FloatImage): WebGLTexture;
148
+ /**
149
+ * A live GPU texture, passed between browser nodes.
150
+ *
151
+ * Separate from `image` on purpose, and the distinction is about cost rather
152
+ * than taste. An `image` is file-backed: durable, portable, readable by Python,
153
+ * and it costs a write and a read every time it is handed on. A `texture` never
154
+ * leaves the page — a chain of WebGL nodes passes one along with no round trip
155
+ * at all, which is what makes an interactive shader chain feel immediate.
156
+ *
157
+ * Because it is session-bound it cannot be cached, cannot be inspected after a
158
+ * reload, and cannot be read by a Python stage. So the conversions are explicit:
159
+ * `toImage` is a GPU readback and a file write, and it should be visible in the
160
+ * graph rather than happening silently on a connection.
161
+ */
162
+ export interface TextureRef {
163
+ gl: WebGL2RenderingContext;
164
+ texture: WebGLTexture;
165
+ /** [width, height] */
166
+ size: [number, number];
167
+ channels: number;
168
+ /** True when the texture holds float data rather than 8-bit. */
169
+ float: boolean;
170
+ }
171
+ /** Upload an image file to the GPU. The integer path for `uploadFloatTexture`. */
172
+ export declare function textureFromImage(gl: WebGL2RenderingContext, image: string | {
173
+ path: string;
174
+ }, options?: MediaOptions): Promise<TextureRef>;
175
+ /** Allocate an empty texture to render into. */
176
+ export declare function createTexture(gl: WebGL2RenderingContext, size: [number, number], options?: {
177
+ float?: boolean;
178
+ channels?: number;
179
+ }): TextureRef;
180
+ /**
181
+ * Read a texture back off the GPU and write it into the project as an image.
182
+ * The one place a texture becomes durable — deliberately a named step, because
183
+ * a readback stalls the pipeline and a write costs a file.
184
+ */
185
+ export declare function textureToImage(ref: TextureRef, path: string, options?: {
186
+ format?: 'png' | 'webp';
187
+ space?: 'srgb' | 'linear';
188
+ }): Promise<ImageRef>;
189
+ /** The value on an `image` port. Mirrors Cascade's core ImageRef. */
190
+ export interface ImageRef {
191
+ path: string;
192
+ size: [number, number];
193
+ channels: 'r' | 'a' | 'rg' | 'rgb' | 'rgba';
194
+ depth: 'u8' | 'u16' | 'f16' | 'f32';
195
+ space: 'srgb' | 'linear';
196
+ }