@field/cascade 0.10.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (133) hide show
  1. package/AGENTS.md +20 -3
  2. package/CHANGELOG.md +187 -0
  3. package/README.md +1 -2
  4. package/dist/assets/{CodeEditor-DSZBpzdy.js → CodeEditor-4O7FQ__R.js} +4 -4
  5. package/dist/assets/{CodePanel-sY6sZa7B.js → CodePanel-BU-autmq.js} +2 -2
  6. package/dist/assets/{CustomNodeDialog-BRKnqZhp.js → CustomNodeDialog-BRpDfOV5.js} +1 -1
  7. package/dist/assets/{OpenGraphDialog-ZsgL-iwr.js → OpenGraphDialog-BwbRlC0X.js} +1 -1
  8. package/dist/assets/{ProjectSettings-Cw17nNe3.js → ProjectSettings-DLOXLxcA.js} +1 -1
  9. package/dist/assets/{SettingsDialog-p25h3tj0.js → SettingsDialog-Bz8JxISH.js} +1 -1
  10. package/dist/assets/{VersionHistoryDialog-DzFLUotB.js → VersionHistoryDialog-D5Z0nJ5-.js} +1 -1
  11. package/dist/assets/{core-BICrEt0y.js → core-Yqi0Ni61.js} +1 -1
  12. package/dist/assets/{cssMode-D7TXw57I.js → cssMode-CxUzc4vp.js} +1 -1
  13. package/dist/assets/{definitions-DUww2Rt-.js → definitions-GreEMXLV.js} +1 -1
  14. package/dist/assets/{editor.api-DvkCO3ko.js → editor.api-BNFb25CX.js} +1 -1
  15. package/dist/assets/{freemarker2-BA2lxsF1.js → freemarker2-0ZDnWxeM.js} +1 -1
  16. package/dist/assets/geo-u0n_jOsD.js +1 -0
  17. package/dist/assets/{handlebars-BcDiyPd8.js → handlebars-Cy18MoaD.js} +1 -1
  18. package/dist/assets/{html-C7O4TyhX.js → html-B-2Dx7nq.js} +1 -1
  19. package/dist/assets/{htmlMode-WaDGF3Mj.js → htmlMode-DUAc9B_d.js} +1 -1
  20. package/dist/assets/{image-DR4oeyJB.js → image-cAa-Nu9O.js} +1 -1
  21. package/dist/assets/{index-DyvDC6pS.js → index-5ZtDAuy2.js} +42 -41
  22. package/dist/assets/{javascript-CpIEK7tz.js → javascript-DeQsDqP5.js} +1 -1
  23. package/dist/assets/{jsonMode-B2FltW0f.js → jsonMode-BiFwJVSk.js} +1 -1
  24. package/dist/assets/{liquid-CN6FFHZE.js → liquid-BLoHH68a.js} +1 -1
  25. package/dist/assets/{lspLanguageFeatures-QhV9XtLp.js → lspLanguageFeatures-CNFgm1Kv.js} +1 -1
  26. package/dist/assets/math-DioIQOPJ.js +1 -0
  27. package/dist/assets/{mdx-DE89pafV.js → mdx-CFXQVNfs.js} +1 -1
  28. package/dist/assets/pop-DC_TQG9t.js +1 -0
  29. package/dist/assets/{python-ByTllTB9.js → python-Cf7S7p2E.js} +1 -1
  30. package/dist/assets/{razor-USYlRRCE.js → razor-CMHBcG-g.js} +1 -1
  31. package/dist/assets/{tsMode-BXpN8iUz.js → tsMode-BEih0GDj.js} +1 -1
  32. package/dist/assets/{typescript-DInBjGQQ.js → typescript-COEKYD_a.js} +1 -1
  33. package/dist/assets/{workers-C7Ye5B7D.js → workers-BNhAhMNn.js} +1 -1
  34. package/dist/assets/{xml-U0Uocqyo.js → xml-CPiMpnPl.js} +1 -1
  35. package/dist/assets/{yaml-CBw_PVYf.js → yaml-Bgp1ITLR.js} +1 -1
  36. package/dist/cli/Graph-ZJJTRQTM.js +2975 -0
  37. package/dist/cli/chunk-2KXB47FP.js +115 -0
  38. package/dist/cli/chunk-35D343DC.js +1447 -0
  39. package/dist/cli/chunk-36P5K3SD.js +631 -0
  40. package/dist/cli/chunk-3JTJHF7V.js +267 -0
  41. package/dist/cli/chunk-4UDFYF5G.js +132 -0
  42. package/dist/cli/chunk-53NMBJAO.js +35 -0
  43. package/dist/cli/chunk-A4OAMOZH.js +310 -0
  44. package/dist/cli/chunk-BIJO5OFS.js +71 -0
  45. package/dist/cli/chunk-BP7UKF36.js +376 -0
  46. package/dist/cli/chunk-CKS2FQWR.js +89 -0
  47. package/dist/cli/chunk-CO22XHNG.js +593 -0
  48. package/dist/cli/chunk-D6BUOHUG.js +634 -0
  49. package/dist/cli/chunk-DEQSEFFH.js +153 -0
  50. package/dist/cli/chunk-DUIUFVU2.js +241 -0
  51. package/dist/cli/chunk-EUIAC3U3.js +3006 -0
  52. package/dist/cli/chunk-FCDXJ6EK.js +2208 -0
  53. package/dist/cli/chunk-GFS6W3C5.js +308 -0
  54. package/dist/cli/chunk-HFHWYL55.js +139 -0
  55. package/dist/cli/chunk-HSNOBIVT.js +263 -0
  56. package/dist/cli/chunk-ICH7N5PN.js +26 -0
  57. package/dist/cli/chunk-J7VK7MNF.js +418 -0
  58. package/dist/cli/chunk-KH4MUBKF.js +1551 -0
  59. package/dist/cli/chunk-MIIEFUGG.js +623 -0
  60. package/dist/cli/chunk-OWWBYLHC.js +136 -0
  61. package/dist/cli/chunk-POMMNKZZ.js +672 -0
  62. package/dist/cli/chunk-QBBKRNGY.js +90 -0
  63. package/dist/cli/chunk-QQPA2ZHH.js +273 -0
  64. package/dist/cli/chunk-RLCAJBVR.js +224 -0
  65. package/dist/cli/chunk-RQZ4SX5R.js +1125 -0
  66. package/dist/cli/chunk-RUU3YWXU.js +99 -0
  67. package/dist/cli/chunk-RXDIMXN5.js +98 -0
  68. package/dist/cli/chunk-SGJAET2L.js +197 -0
  69. package/dist/cli/chunk-VVGSLVSF.js +151 -0
  70. package/dist/cli/chunk-WD62JIP6.js +118 -0
  71. package/dist/cli/chunk-WIH4TEIS.js +104 -0
  72. package/dist/cli/chunk-WKURDVYP.js +1119 -0
  73. package/dist/cli/chunk-WPUDGEFY.js +20 -0
  74. package/dist/cli/chunk-XWDSKG7N.js +1301 -0
  75. package/dist/cli/chunk-YAT5Y25U.js +121 -0
  76. package/dist/cli/chunk-ZL4JTG5Y.js +37 -0
  77. package/dist/cli/cliCommands-UQSFGEHF.js +3464 -0
  78. package/dist/cli/commands-Q23M26QM.js +1292 -0
  79. package/dist/cli/definitions-HF2FTHYU.js +32 -0
  80. package/dist/cli/doctorCommand-B7BPQMN6.js +76 -0
  81. package/dist/cli/edit-JHCPTQFT.js +28 -0
  82. package/dist/cli/editorCommands-UIF5YLUZ.js +33 -0
  83. package/dist/cli/encode-HAUZTYPT.js +17 -0
  84. package/dist/cli/frames-LSAUBZ7P.js +16 -0
  85. package/dist/cli/geo-4RNK3QVN.js +43 -0
  86. package/dist/cli/graphCommands-6BMAKGQE.js +184 -0
  87. package/dist/cli/graphFile-VPSQ6OF7.js +286 -0
  88. package/dist/cli/headlessCanvas-5XLDZT7M.js +15 -0
  89. package/dist/cli/helpCommand-3VRE3KVO.js +155 -0
  90. package/dist/cli/index.js +100 -30756
  91. package/dist/cli/math-ZRMLKU2D.js +16 -0
  92. package/dist/cli/mcp-ZLEEMYGF.js +164 -0
  93. package/dist/cli/parity-KINARCP2.js +328 -0
  94. package/dist/cli/pop-USDFXB7H.js +39 -0
  95. package/dist/cli/project-K3QIPGBB.js +12 -0
  96. package/dist/cli/projectDefinition-DMTZN2NE.js +27 -0
  97. package/dist/cli/projectRuntime-Z5PJOH47.js +48 -0
  98. package/dist/cli/registerStandardNodes-S2VLMF6J.js +3081 -0
  99. package/dist/cli/render-YURAPPUI.js +436 -0
  100. package/dist/cli/runCommand-PI764LEG.js +198 -0
  101. package/dist/cli/runner-RA6UKEPD.js +37 -0
  102. package/dist/cli/runtime/image.ts +277 -0
  103. package/dist/cli/runtime/stage.ts +4 -1
  104. package/dist/cli/stageRunner-KUNIMQKJ.js +10 -0
  105. package/dist/cli/verbs-RCE3K4VQ.js +19 -0
  106. package/dist/cli/webBuild-GT4PZT5D.js +37 -0
  107. package/dist/contracts/documentFormat.d.ts +18 -0
  108. package/dist/contracts/documentFormat.js +195 -0
  109. package/dist/contracts/editor.d.ts +17 -0
  110. package/dist/contracts/index.d.ts +1 -0
  111. package/dist/contracts/index.js +1 -0
  112. package/dist/image.d.ts +141 -0
  113. package/dist/image.js +127 -0
  114. package/dist/index.html +1 -1
  115. package/dist/player/editor.js +32 -7
  116. package/dist/runtime/builtins/headless.d.ts +19 -0
  117. package/dist/runtime/builtins/headless.js +25 -0
  118. package/dist/runtime/builtins/image/contactSheet.d.ts +226 -0
  119. package/dist/runtime/builtins/image/contactSheet.js +199 -0
  120. package/dist/runtime/builtins/image/index.d.ts +9 -0
  121. package/dist/runtime/builtins/image/index.js +9 -0
  122. package/dist/stage.js +4 -1
  123. package/doc/CLI_REFERENCE.md +73 -2
  124. package/doc/EDITOR_EXPORT.md +11 -2
  125. package/doc/MCP.md +17 -1
  126. package/doc/NODE_REFERENCE.md +142 -0
  127. package/doc/PROJECT_AUTHORING.md +127 -7
  128. package/package.json +16 -4
  129. package/dist/assets/ExportDialog-C7dVGua4.js +0 -358
  130. package/dist/assets/ExportDialog-DOq0flMV.css +0 -1
  131. package/dist/assets/geo-C2RHOVHC.js +0 -1
  132. package/dist/assets/math-DaDEgWKt.js +0 -1
  133. package/dist/assets/pop-pLH7L0PW.js +0 -1
@@ -0,0 +1,277 @@
1
+ /**
2
+ * `cascade/image` — the one image API a node body uses, in every host.
3
+ *
4
+ * A node that draws used to have two honest choices and one dishonest one.
5
+ * `new OffscreenCanvas` worked in Studio and under `cascade run` (the CLI
6
+ * installs Skia's canvas as that global), but the bytes in and out went
7
+ * through `cascade/io`'s project paths, and fonts had no portable door at all.
8
+ * So the nodes that needed fonts or files reached for `@napi-rs/canvas` and
9
+ * `node:fs` directly, declared `runsOn: 'server'`, and could never be exported
10
+ * as a browser editor — the commons plotter-social job was blocked on exactly
11
+ * that ("Cascade has no browser image contract").
12
+ *
13
+ * The contract, in full:
14
+ *
15
+ * - **Surface.** `createCanvas(w, h)` is an `OffscreenCanvas`: the browser's
16
+ * own in a page or a worker, Skia's (`@napi-rs/canvas`, installed by the
17
+ * headless host) under `cascade run`/`render`/MCP. `context2d(canvas)` asks
18
+ * for a CPU-backed 2D context (`willReadFrequently`), which is what keeps
19
+ * Chrome on the same software Skia rasteriser the headless host uses, and
20
+ * is the right hint anyway for nodes that read pixels back.
21
+ * - **Pixels in.** `readImage(context, ref)` reads an `ImageRef`, `AssetRef`
22
+ * or project path through the node's declared `assets` capability and
23
+ * decodes it with `decodeImage` (`createImageBitmap` in both hosts).
24
+ * - **Pixels out.** `writeImage(context, canvas, name)` encodes PNG through
25
+ * `encodeImage` and stores it with `assets.write`, returning the `ImageRef`
26
+ * to set on an `image` output. The host decides where it lands
27
+ * (`.cascade-cache/<name>.<hash>.png` headless, an in-memory generated
28
+ * asset in an exported editor), so a node never names a path.
29
+ * - **Fonts.** `registerFont(family, bytes)` makes a face drawable by
30
+ * `ctx.font` in this host: `FontFace` added to the page's or worker's font
31
+ * set in a browser, Skia's `GlobalFonts` headless. Bytes come from the same
32
+ * `assets` capability, so a font is a project file like any other.
33
+ * - **Refusal, not fallback.** No canvas in this host, or no `assets`
34
+ * capability declared, is an error with one sentence saying which, never a
35
+ * silent blank image.
36
+ *
37
+ * A node using it declares `runsOn: 'portable'` and `capabilities: ['assets']`
38
+ * and imports nothing host-specific (`node:*`, `@napi-rs/canvas`, `document`).
39
+ * Then the same body cooks in Studio, in `cascade run`, and in an exported
40
+ * editor's worker.
41
+ *
42
+ * The headless host installs its font door under a global key, like
43
+ * `cascade/io`'s bridge, because the node's copy of this module and the CLI's
44
+ * copy are not always the same module instance.
45
+ */
46
+
47
+ /** An image port value. Mirrors Cascade's core `ImageRef`. */
48
+ export interface ImageRef {
49
+ path: string;
50
+ size: [number, number];
51
+ channels: 'r' | 'a' | 'rg' | 'rgb' | 'rgba';
52
+ depth: 'u8' | 'u16' | 'f16' | 'f32';
53
+ space: 'srgb' | 'linear';
54
+ }
55
+
56
+ /** Anything naming stored bytes: an `ImageRef`, an `AssetRef`, or a project path. */
57
+ export type ImageSourceRef = string | { readonly path: string };
58
+
59
+ /** The part of the `assets` capability these helpers use. */
60
+ export interface ImageAssets {
61
+ read(ref: { path: string }, options: { signal: unknown }): Promise<Uint8Array>;
62
+ write(
63
+ data: Uint8Array,
64
+ metadata: { mediaType?: string; suggestedName?: string },
65
+ options: { signal: unknown },
66
+ ): Promise<{ path: string; mediaType?: string }>;
67
+ }
68
+
69
+ /**
70
+ * The slice of a definition-v1 `NodeExecutionContext` these helpers need, so
71
+ * a node passes its own `context`. Typed loosely on purpose: a node that did
72
+ * not declare `assets` still type-checks here and gets a sentence at run time
73
+ * naming the missing declaration rather than a type error about generics.
74
+ */
75
+ export interface ImageNodeContext {
76
+ readonly capabilities: object;
77
+ readonly signal?: unknown;
78
+ }
79
+
80
+ export type ImageType = 'image/png' | 'image/webp' | 'image/jpeg';
81
+
82
+ /** What a host gives `registerFont`. A browser realm needs none. */
83
+ export interface ImageHost {
84
+ /** For a `--verbose` line and for errors. */
85
+ readonly renderer: string;
86
+ registerFont(family: string, bytes: Uint8Array, descriptors: FontDescriptors): void | Promise<void>;
87
+ }
88
+
89
+ export interface FontDescriptors {
90
+ /** CSS weight, `'400'`, `'bold'`. Headless reads the weight from the file. */
91
+ weight?: string;
92
+ /** `'normal'` or `'italic'`. Headless reads the style from the file. */
93
+ style?: string;
94
+ }
95
+
96
+ const HOST_KEY = '__cascadeImageHost';
97
+
98
+ const NO_CANVAS =
99
+ 'cascade/image: this host has no canvas. In a browser this needs OffscreenCanvas; headless, Cascade installs ' +
100
+ '@napi-rs/canvas before a run (install it with "npm install @napi-rs/canvas" if it is missing).';
101
+
102
+ /** Install the headless host's font door. Returns the disposer. */
103
+ export function installImageHost(host: ImageHost | null): () => void {
104
+ const globals = globalThis as Record<string, unknown>;
105
+ const previous = globals[HOST_KEY];
106
+ globals[HOST_KEY] = host ?? undefined;
107
+ return () => {
108
+ globals[HOST_KEY] = previous;
109
+ };
110
+ }
111
+
112
+ function installedHost(): ImageHost | null {
113
+ const host = (globalThis as Record<string, unknown>)[HOST_KEY] as ImageHost | undefined;
114
+ return host && typeof host.registerFont === 'function' ? host : null;
115
+ }
116
+
117
+ type CanvasConstructor = new (width: number, height: number) => OffscreenCanvas;
118
+
119
+ function canvasConstructor(): CanvasConstructor | undefined {
120
+ return (globalThis as { OffscreenCanvas?: CanvasConstructor }).OffscreenCanvas;
121
+ }
122
+
123
+ /** Whether this host can draw. Never throws. */
124
+ export function canvasAvailable(): boolean {
125
+ return typeof canvasConstructor() === 'function';
126
+ }
127
+
128
+ /** A drawing surface of whole pixels, at least 1x1. */
129
+ export function createCanvas(width: number, height: number): OffscreenCanvas {
130
+ const Canvas = canvasConstructor();
131
+ if (!Canvas) throw new Error(NO_CANVAS);
132
+ return new Canvas(Math.max(1, Math.round(width)), Math.max(1, Math.round(height)));
133
+ }
134
+
135
+ /**
136
+ * The canvas's 2D context, CPU-backed. `willReadFrequently` keeps a browser
137
+ * off the GPU rasteriser, so a node draws with the same software Skia in Chrome
138
+ * as headless, and `getImageData` does not stall on a readback.
139
+ */
140
+ export function context2d(canvas: OffscreenCanvas): OffscreenCanvasRenderingContext2D {
141
+ const context = canvas.getContext('2d', { willReadFrequently: true }) as OffscreenCanvasRenderingContext2D | null;
142
+ if (!context) throw new Error('cascade/image: the canvas has no 2D context');
143
+ return context;
144
+ }
145
+
146
+ /** `createCanvas` and `context2d` in one: `const { canvas, context } = surface(w, h)`. */
147
+ export function surface(width: number, height: number): {
148
+ canvas: OffscreenCanvas;
149
+ context: OffscreenCanvasRenderingContext2D;
150
+ } {
151
+ const canvas = createCanvas(width, height);
152
+ return { canvas, context: context2d(canvas) };
153
+ }
154
+
155
+ /** Encoded bytes (PNG, JPEG, WebP) to something `drawImage` accepts. */
156
+ export async function decodeImage(bytes: Uint8Array | ArrayBuffer | Blob): Promise<ImageBitmap> {
157
+ const create = (globalThis as { createImageBitmap?: (source: Blob) => Promise<ImageBitmap> }).createImageBitmap;
158
+ if (typeof create !== 'function') throw new Error(NO_CANVAS);
159
+ const blob = bytes instanceof Blob ? bytes : new Blob([bytes as BlobPart]);
160
+ const image = await create(blob);
161
+ if (!image?.width || !image?.height) throw new Error('cascade/image: could not decode the image data');
162
+ return image;
163
+ }
164
+
165
+ /** A canvas's pixels as encoded bytes, PNG unless asked otherwise. */
166
+ export async function encodeImage(
167
+ canvas: OffscreenCanvas,
168
+ options: { type?: ImageType; quality?: number } = {},
169
+ ): Promise<Uint8Array> {
170
+ const type = options.type ?? 'image/png';
171
+ if (typeof canvas.convertToBlob !== 'function') throw new Error('cascade/image: this canvas cannot encode (no convertToBlob)');
172
+ const blob = await canvas.convertToBlob({ type, ...(options.quality === undefined ? {} : { quality: options.quality }) });
173
+ return new Uint8Array(await blob.arrayBuffer());
174
+ }
175
+
176
+ // One record per realm, so a font registered once is not added again on every
177
+ // cook. Keyed by family and content: new bytes under a family name register.
178
+ // The headless realm is keyed by its renderer name rather than the host
179
+ // object, because a long-lived process (MCP) installs a fresh host per run
180
+ // while Skia's font registry lives for the whole process.
181
+ const registered = new Map<unknown, Set<string>>();
182
+
183
+ function fingerprint(bytes: Uint8Array): string {
184
+ let hash = 0x811c9dc5;
185
+ for (let index = 0; index < bytes.length; index += 1) {
186
+ hash ^= bytes[index]!;
187
+ hash = Math.imul(hash, 0x01000193);
188
+ }
189
+ return `${bytes.length}:${(hash >>> 0).toString(16)}`;
190
+ }
191
+
192
+ interface BrowserFontSet { add(face: unknown): unknown }
193
+ type FontFaceConstructor = new (family: string, source: ArrayBuffer | Uint8Array, descriptors?: FontDescriptors) => {
194
+ load(): Promise<unknown>;
195
+ };
196
+
197
+ /**
198
+ * Make `family` drawable by `ctx.font` here. Idempotent per family and bytes.
199
+ * A browser realm (page or worker) uses `FontFace`; the headless host
200
+ * registers with Skia. A host with neither refuses.
201
+ */
202
+ export async function registerFont(
203
+ family: string,
204
+ bytes: Uint8Array | ArrayBuffer,
205
+ descriptors: FontDescriptors = {},
206
+ ): Promise<void> {
207
+ if (!family.trim()) throw new Error('cascade/image: registerFont needs a family name');
208
+ const data = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
209
+ if (!data.byteLength) throw new Error(`cascade/image: the font for ${family} is empty`);
210
+ const host = installedHost();
211
+ const globals = globalThis as { FontFace?: FontFaceConstructor; fonts?: BrowserFontSet; document?: { fonts?: BrowserFontSet } };
212
+ const fonts = host ? null : globals.fonts ?? globals.document?.fonts;
213
+ const realm: unknown = host ? `headless:${host.renderer}` : fonts ?? globalThis;
214
+ const key = `${family}\u0000${descriptors.weight ?? ''}\u0000${descriptors.style ?? ''}\u0000${fingerprint(data)}`;
215
+ const seen = registered.get(realm) ?? new Set<string>();
216
+ if (seen.has(key)) return;
217
+ if (host) {
218
+ await host.registerFont(family, data, descriptors);
219
+ } else if (typeof globals.FontFace === 'function' && fonts) {
220
+ // A copy: FontFace may detach or keep the buffer it is given.
221
+ const face = new globals.FontFace(family, data.slice().buffer, descriptors);
222
+ fonts.add(await face.load());
223
+ } else {
224
+ throw new Error('cascade/image: this host cannot register fonts (no FontFace and no headless canvas host)');
225
+ }
226
+ seen.add(key);
227
+ registered.set(realm, seen);
228
+ }
229
+
230
+ function assetsOf(context: ImageNodeContext): ImageAssets {
231
+ const assets = (context.capabilities as { assets?: ImageAssets }).assets;
232
+ if (!assets || typeof assets.read !== 'function' || typeof assets.write !== 'function') {
233
+ throw new Error("cascade/image: this node reads or writes images, so its definition must declare capabilities: ['assets']");
234
+ }
235
+ return assets;
236
+ }
237
+
238
+ function refOf(ref: ImageSourceRef): { path: string } {
239
+ const path = typeof ref === 'string' ? ref : ref?.path;
240
+ if (typeof path !== 'string' || !path) throw new Error('cascade/image: no image to read (an empty path or an unset image input)');
241
+ return { path };
242
+ }
243
+
244
+ const NEVER = { aborted: false, addEventListener() {}, removeEventListener() {} };
245
+
246
+ /** A stored file's bytes through the node's `assets` capability: an image, a font, anything. */
247
+ export async function readBytes(context: ImageNodeContext, ref: ImageSourceRef): Promise<Uint8Array> {
248
+ return assetsOf(context).read(refOf(ref), { signal: context.signal ?? NEVER });
249
+ }
250
+
251
+ /** Read and decode an image input, an asset, or a project file. */
252
+ export async function readImage(context: ImageNodeContext, ref: ImageSourceRef): Promise<ImageBitmap> {
253
+ return decodeImage(await readBytes(context, ref));
254
+ }
255
+
256
+ /**
257
+ * Encode a canvas and store it; set the result on an `image` output. `name`
258
+ * is a suggestion (`context.nodeId + '.png'` keeps instances apart); the host
259
+ * picks the stored path.
260
+ */
261
+ export async function writeImage(
262
+ context: ImageNodeContext,
263
+ canvas: OffscreenCanvas,
264
+ name: string,
265
+ options: { type?: ImageType; quality?: number; space?: 'srgb' | 'linear' } = {},
266
+ ): Promise<ImageRef> {
267
+ const type = options.type ?? 'image/png';
268
+ const bytes = await encodeImage(canvas, { type, ...(options.quality === undefined ? {} : { quality: options.quality }) });
269
+ const stored = await assetsOf(context).write(bytes, { mediaType: type, suggestedName: name }, { signal: context.signal ?? NEVER });
270
+ return {
271
+ path: stored.path,
272
+ size: [canvas.width, canvas.height],
273
+ channels: 'rgba',
274
+ depth: 'u8',
275
+ space: options.space ?? 'srgb',
276
+ };
277
+ }
@@ -115,7 +115,10 @@ async function overHttp<T>(stage: string, args: Record<string, unknown>, signal?
115
115
  const postStage = async () => fetch('/api/exec', {
116
116
  method: 'POST',
117
117
  headers: { 'Content-Type': 'application/json', 'X-Cascade-Exec-Capability': await capability() },
118
- body: JSON.stringify({ stage, args }),
118
+ // `cook`: a node's cook is a pure function of its inputs, so the server's
119
+ // default stage cache may serve it (see StageCachePolicy). A panel posts
120
+ // through its own api.runStage, without this, and is never cached unasked.
121
+ body: JSON.stringify({ stage, args, caller: 'cook' }),
119
122
  signal,
120
123
  });
121
124
 
@@ -0,0 +1,10 @@
1
+ import {
2
+ readStageConfig,
3
+ runProjectStage
4
+ } from "./chunk-VVGSLVSF.js";
5
+ import "./chunk-WD62JIP6.js";
6
+ export {
7
+ readStageConfig,
8
+ runProjectStage
9
+ };
10
+ //# sourceMappingURL=stageRunner-KUNIMQKJ.js.map
@@ -0,0 +1,19 @@
1
+ import {
2
+ VERBS,
3
+ graphCommandsHelp,
4
+ graphOptions,
5
+ isGraphCommand,
6
+ mainHelp,
7
+ mcpTools,
8
+ verbFor
9
+ } from "./chunk-POMMNKZZ.js";
10
+ export {
11
+ VERBS,
12
+ graphCommandsHelp,
13
+ graphOptions,
14
+ isGraphCommand,
15
+ mainHelp,
16
+ mcpTools,
17
+ verbFor
18
+ };
19
+ //# sourceMappingURL=verbs-RCE3K4VQ.js.map
@@ -0,0 +1,37 @@
1
+ import {
2
+ buildPlayer,
3
+ canonicalFuturePath,
4
+ exists,
5
+ inside,
6
+ installRoot,
7
+ playerFile,
8
+ staticRuntimePlugin
9
+ } from "./chunk-3JTJHF7V.js";
10
+ import "./chunk-EUIAC3U3.js";
11
+ import "./chunk-36P5K3SD.js";
12
+ import "./chunk-D6BUOHUG.js";
13
+ import "./chunk-RXDIMXN5.js";
14
+ import "./chunk-QQPA2ZHH.js";
15
+ import "./chunk-KH4MUBKF.js";
16
+ import "./chunk-35D343DC.js";
17
+ import "./chunk-DEQSEFFH.js";
18
+ import "./chunk-HSNOBIVT.js";
19
+ import "./chunk-ICH7N5PN.js";
20
+ import "./chunk-GFS6W3C5.js";
21
+ import "./chunk-HFHWYL55.js";
22
+ import "./chunk-CO22XHNG.js";
23
+ import "./chunk-RQZ4SX5R.js";
24
+ import "./chunk-A4OAMOZH.js";
25
+ import "./chunk-J7VK7MNF.js";
26
+ import "./chunk-XWDSKG7N.js";
27
+ import "./chunk-BIJO5OFS.js";
28
+ export {
29
+ buildPlayer,
30
+ canonicalFuturePath,
31
+ exists,
32
+ inside,
33
+ installRoot,
34
+ playerFile,
35
+ staticRuntimePlugin
36
+ };
37
+ //# sourceMappingURL=webBuild-GT4PZT5D.js.map
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Compact-array JSON formatting for .cascade files — ported unchanged
3
+ * from the original routes/projects.ts (real design work: keeps small
4
+ * arrays like [x, y] coordinates and [from, to] connections on one line
5
+ * instead of exploding them across many, so a saved graph stays readable
6
+ * and diffable in git — which matters now that every save is a commit.
7
+ *
8
+ * The one copy: Studio's save and download (src/utils/fileSystem.ts), the
9
+ * server's graph route and the CLI's graph edits all write through it, so a
10
+ * file saved by any of them diffs cleanly against the others.
11
+ */
12
+ /**
13
+ * Format JSON string with compact arrays (keeps small arrays on single lines)
14
+ * This function finds multi-line arrays with 2-4 elements and compacts them
15
+ * Handles both numeric arrays and arrays containing small arrays (like connections)
16
+ */
17
+ export declare function formatCompactJSON(jsonString: string, indent?: number): string;
18
+ //# sourceMappingURL=documentFormat.d.ts.map
@@ -0,0 +1,195 @@
1
+ /**
2
+ * Compact-array JSON formatting for .cascade files — ported unchanged
3
+ * from the original routes/projects.ts (real design work: keeps small
4
+ * arrays like [x, y] coordinates and [from, to] connections on one line
5
+ * instead of exploding them across many, so a saved graph stays readable
6
+ * and diffable in git — which matters now that every save is a commit.
7
+ *
8
+ * The one copy: Studio's save and download (src/utils/fileSystem.ts), the
9
+ * server's graph route and the CLI's graph edits all write through it, so a
10
+ * file saved by any of them diffs cleanly against the others.
11
+ */
12
+ /**
13
+ * Format JSON string with compact arrays (keeps small arrays on single lines)
14
+ * This function finds multi-line arrays with 2-4 elements and compacts them
15
+ * Handles both numeric arrays and arrays containing small arrays (like connections)
16
+ */
17
+ export function formatCompactJSON(jsonString, indent = 2) {
18
+ // Process multiple passes to handle nested arrays
19
+ let result = jsonString;
20
+ let previousResult = '';
21
+ // Keep processing until no more changes (handles nested arrays)
22
+ while (result !== previousResult) {
23
+ previousResult = result;
24
+ result = formatCompactJSONPass(result, indent);
25
+ }
26
+ return result;
27
+ }
28
+ /**
29
+ * Single pass of compact formatting
30
+ */
31
+ function formatCompactJSONPass(jsonString, indent = 2) {
32
+ const lines = jsonString.split('\n');
33
+ const result = [];
34
+ let i = 0;
35
+ while (i < lines.length) {
36
+ const line = lines[i];
37
+ const trimmed = line.trim();
38
+ // Check if this line starts an array (pattern: "key": [ or just [)
39
+ if (trimmed.endsWith('[') && (trimmed.includes(':') || trimmed === '[')) {
40
+ // Collect lines until we find the closing bracket
41
+ let arrayLines = [line];
42
+ let bracketDepth = 1;
43
+ let j = i + 1;
44
+ while (j < lines.length && bracketDepth > 0) {
45
+ const currentLine = lines[j];
46
+ arrayLines.push(currentLine);
47
+ // Count bracket depth
48
+ for (const char of currentLine) {
49
+ if (char === '[')
50
+ bracketDepth++;
51
+ if (char === ']')
52
+ bracketDepth--;
53
+ }
54
+ j++;
55
+ }
56
+ // Extract array content (lines between opening and closing brackets)
57
+ // Handle nested arrays by extracting complete array elements
58
+ const arrayContent = [];
59
+ let k = 1;
60
+ while (k < arrayLines.length - 1) {
61
+ const currentLine = arrayLines[k].trim();
62
+ // Check if this line starts a nested array
63
+ if (currentLine.endsWith('[') || (currentLine.startsWith('[') && !currentLine.endsWith(']'))) {
64
+ // Extract the complete nested array
65
+ let nestedArrayLines = [arrayLines[k]];
66
+ let nestedDepth = 1;
67
+ let m = k + 1;
68
+ while (m < arrayLines.length - 1 && nestedDepth > 0) {
69
+ nestedArrayLines.push(arrayLines[m]);
70
+ for (const char of arrayLines[m]) {
71
+ if (char === '[')
72
+ nestedDepth++;
73
+ if (char === ']')
74
+ nestedDepth--;
75
+ }
76
+ m++;
77
+ }
78
+ // Join nested array lines into a single compact representation
79
+ const nestedContent = [];
80
+ for (let n = 1; n < nestedArrayLines.length - 1; n++) {
81
+ const nestedLine = nestedArrayLines[n].trim().replace(/,$/, '').trim();
82
+ if (nestedLine) {
83
+ nestedContent.push(nestedLine);
84
+ }
85
+ }
86
+ // Create compact nested array string
87
+ const nestedArray = '[' + nestedContent.map(c => c.trim()).join(', ') + ']';
88
+ arrayContent.push(nestedArray);
89
+ k = m;
90
+ }
91
+ else {
92
+ // Simple element
93
+ const content = currentLine.replace(/,$/, '').trim();
94
+ if (content) {
95
+ arrayContent.push(content);
96
+ }
97
+ k++;
98
+ }
99
+ }
100
+ // Check if it's a small numeric array (2-4 elements)
101
+ const isSmallNumericArray = arrayContent.length >= 2 &&
102
+ arrayContent.length <= 4 &&
103
+ arrayContent.every(item => {
104
+ const trimmed = item.trim();
105
+ return /^-?\d+(\.\d+)?([eE][+-]?\d+)?$/.test(trimmed);
106
+ });
107
+ // Check if it's an array containing small arrays (like connections: [[a, b], [c, d]])
108
+ // Look for patterns where each element is a small array (2-4 elements)
109
+ // This handles both already-compacted arrays and multi-line nested arrays
110
+ let isArrayOfSmallArrays = false;
111
+ if (arrayContent.length >= 2 && arrayContent.length <= 20) { // Reasonable limit for connections
112
+ // Check if elements are arrays (either compact or multi-line)
113
+ const allAreArrays = arrayContent.every(item => {
114
+ const trimmed = item.trim();
115
+ // Already compact array like [a, b] or ["a", 0]
116
+ if (trimmed.startsWith('[') && trimmed.endsWith(']')) {
117
+ const inner = trimmed.slice(1, -1).trim();
118
+ const parts = inner.split(',').map(p => p.trim()).filter(p => p);
119
+ return parts.length >= 2 && parts.length <= 4;
120
+ }
121
+ // Multi-line array - check if this line starts an array
122
+ if (trimmed.endsWith('[')) {
123
+ return true; // This will be handled in a later pass
124
+ }
125
+ return false;
126
+ });
127
+ if (allAreArrays) {
128
+ // Check if inner arrays are small (2-4 elements each)
129
+ // For multi-line arrays, we'll need to extract their content
130
+ let allSmall = true;
131
+ for (const item of arrayContent) {
132
+ const trimmed = item.trim();
133
+ if (trimmed.startsWith('[') && trimmed.endsWith(']')) {
134
+ // Already compact - check size
135
+ const inner = trimmed.slice(1, -1).trim();
136
+ const parts = inner.split(',').map(p => p.trim()).filter(p => p);
137
+ if (parts.length < 2 || parts.length > 4) {
138
+ allSmall = false;
139
+ break;
140
+ }
141
+ }
142
+ else if (trimmed.endsWith('[')) {
143
+ // Multi-line array - will be compacted in next pass, assume it's small for now
144
+ // We'll handle this in the multi-pass approach
145
+ }
146
+ else {
147
+ allSmall = false;
148
+ break;
149
+ }
150
+ }
151
+ isArrayOfSmallArrays = allSmall;
152
+ }
153
+ }
154
+ if (isSmallNumericArray || isArrayOfSmallArrays) {
155
+ // Compact format: put entire array on one line
156
+ const indentLevel = (line.match(/^(\s*)/)?.[1] || '').length;
157
+ // Extract the key part (everything before the opening bracket)
158
+ const keyMatch = trimmed.match(/^(.+?)\s*\[\s*$/);
159
+ const keyPart = keyMatch ? keyMatch[1] : '';
160
+ // Check if the closing bracket line has a comma (last line in arrayLines)
161
+ const closingLine = arrayLines[arrayLines.length - 1];
162
+ const hasComma = closingLine.trim().endsWith(',');
163
+ // Format array content
164
+ let formattedContent;
165
+ if (isArrayOfSmallArrays) {
166
+ // For arrays of arrays, each element should be compact
167
+ // If an element is a multi-line array (ends with [), we can't compact it yet
168
+ // Otherwise, use the already-compact form
169
+ formattedContent = arrayContent.map(c => {
170
+ const trimmed = c.trim();
171
+ // If it's already compact, use it as-is
172
+ if (trimmed.startsWith('[') && trimmed.endsWith(']')) {
173
+ return trimmed;
174
+ }
175
+ // If it's a multi-line array start, we'll need another pass
176
+ // For now, return as-is (will be handled in next pass)
177
+ return trimmed;
178
+ }).join(', ');
179
+ }
180
+ else {
181
+ // Simple numeric array
182
+ formattedContent = arrayContent.map(c => c.trim()).join(', ');
183
+ }
184
+ const compactArray = keyPart + ' [' + formattedContent + ']' + (hasComma ? ',' : '');
185
+ result.push(' '.repeat(indentLevel) + compactArray);
186
+ i = j; // Skip the processed lines
187
+ continue;
188
+ }
189
+ }
190
+ result.push(line);
191
+ i++;
192
+ }
193
+ return result.join('\n');
194
+ }
195
+ //# sourceMappingURL=documentFormat.js.map
@@ -42,6 +42,13 @@ export interface EditorDeclaration {
42
42
  readonly panel?: EditorPanelDeclaration;
43
43
  /** The read-only stage view. On unless the author turns it off. */
44
44
  readonly inspect?: boolean;
45
+ /**
46
+ * Fonts the exported editor ships and loads, in the page and in the engine
47
+ * worker, so a panel or a canvas that names the family draws with it rather
48
+ * than the system fallback. Open-licensed only: each one states its licence
49
+ * (`OFL-1.1`), and a file under `licensed/` is refused like any other asset.
50
+ */
51
+ readonly fonts?: readonly EditorFont[];
45
52
  /**
46
53
  * Parameter ids a phone keeps in reach at the bottom sheet's lowest snap
47
54
  * point (the text, a Reroll). Defaults to the first text and seed parameters.
@@ -54,6 +61,16 @@ export interface EditorDeclaration {
54
61
  */
55
62
  readonly draft?: Readonly<Record<string, JsonValue>>;
56
63
  }
64
+ export interface EditorFont {
65
+ /** The CSS family name the project's code uses (`DM Mono`). */
66
+ readonly family: string;
67
+ /** Project-relative font file: `.woff2`, `.woff`, `.ttf` or `.otf`. */
68
+ readonly file: string;
69
+ /** SIL Open Font License. Anything else is refused at export. */
70
+ readonly license: "OFL-1.1";
71
+ readonly weight?: string | number;
72
+ readonly style?: "normal" | "italic";
73
+ }
57
74
  export type PublishedControl = "number" | "slider" | "int" | "bool" | "select" | "text" | "textarea" | "color" | "file" | "seed" | "vec2";
58
75
  export interface PublishedParameter {
59
76
  /** Scene key. Stable across renames of the node or the prop it targets. */
@@ -5,6 +5,7 @@ export * from "./capabilities.js";
5
5
  export * from "./color.js";
6
6
  export * from "./definition.js";
7
7
  export * from "./document.js";
8
+ export * from "./documentFormat.js";
8
9
  export * from "./editor.js";
9
10
  export * from "./geometry.js";
10
11
  export * from "./particles.js";
@@ -5,6 +5,7 @@ export * from "./capabilities.js";
5
5
  export * from "./color.js";
6
6
  export * from "./definition.js";
7
7
  export * from "./document.js";
8
+ export * from "./documentFormat.js";
8
9
  export * from "./editor.js";
9
10
  export * from "./geometry.js";
10
11
  export * from "./particles.js";