@field/cascade 0.4.0 → 0.5.2

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 (110) hide show
  1. package/AGENTS.md +18 -0
  2. package/ARCHITECTURE.md +53 -7
  3. package/CHANGELOG.md +68 -0
  4. package/CONTRIBUTING.md +33 -0
  5. package/DESIGN.md +9 -6
  6. package/README.md +80 -4
  7. package/dist/assets/{CodeEditor-BNOJIDzL.js → CodeEditor-BHej8kM9.js} +7 -7
  8. package/dist/assets/{CodePanel-2wp2G0Fj.js → CodePanel-CT3eirK8.js} +2 -2
  9. package/dist/assets/{CustomNodeDialog-DNqaVBqA.js → CustomNodeDialog-BG0m7_Rv.js} +1 -1
  10. package/dist/assets/{ProjectSettings-DcdYF2E1.js → ProjectSettings-BxoLWJG6.js} +2 -2
  11. package/dist/assets/{SettingsDialog-D2xVvyiR.js → SettingsDialog-r9PzZ22z.js} +1 -1
  12. package/dist/assets/{VersionHistoryDialog-DW288ykF.js → VersionHistoryDialog-B6zUV6Dy.js} +1 -1
  13. package/dist/assets/{core-B1y38i95.js → core-wwx5881P.js} +2 -2
  14. package/dist/assets/{cssMode-BLucnaln.js → cssMode-DY5T2Or0.js} +1 -1
  15. package/dist/assets/definitions-BgCvRHyR.js +1 -0
  16. package/dist/assets/{editor.api-B3Et3Pph.js → editor.api-mkORHEMm.js} +1 -1
  17. package/dist/assets/{freemarker2-DVlPoano.js → freemarker2-CIWlk0sw.js} +1 -1
  18. package/dist/assets/geo-CIxgs0oE.js +1 -0
  19. package/dist/assets/{handlebars-B5FZBAMR.js → handlebars-t4jbj3Br.js} +1 -1
  20. package/dist/assets/{html-FodD8KbR.js → html-Ctx7UOlY.js} +1 -1
  21. package/dist/assets/{htmlMode-CsG57VCD.js → htmlMode-BKNZxVw7.js} +1 -1
  22. package/dist/assets/{image-B3F-nq25.js → image-BCBFz0gD.js} +3 -3
  23. package/dist/assets/index-CeqTfMbr.js +212 -0
  24. package/dist/assets/{index-Y2ock1cw.css → index-ilgbOu6G.css} +1 -1
  25. package/dist/assets/{javascript-B3gonF_H.js → javascript-Da_TJBzH.js} +1 -1
  26. package/dist/assets/{jsonMode-Y3w1ahZg.js → jsonMode-Ftt5d1nV.js} +1 -1
  27. package/dist/assets/{liquid-CTpWXIYO.js → liquid-D5kWYG57.js} +1 -1
  28. package/dist/assets/{lspLanguageFeatures-CODBdKEZ.js → lspLanguageFeatures-B-93LC_L.js} +1 -1
  29. package/dist/assets/{mdx-CDAQJZIz.js → mdx-DGT3C_qs.js} +1 -1
  30. package/dist/assets/nodeTypeUtils-BDJsc2Ks.js +1 -0
  31. package/dist/assets/pop-BJGN5Ac4.js +1 -0
  32. package/dist/assets/{python-LOIlxJU-.js → python-Bz3Eew5S.js} +1 -1
  33. package/dist/assets/{razor-CllPQSHo.js → razor-CFDdKpKI.js} +1 -1
  34. package/dist/assets/{tsMode-CO28ut8t.js → tsMode-BaCINWBI.js} +1 -1
  35. package/dist/assets/{typescript-ruU9wKQ_.js → typescript-BoOWspVs.js} +1 -1
  36. package/dist/assets/{workers-Bc1Ywp4-.js → workers-dSWyDxsQ.js} +1 -1
  37. package/dist/assets/{xml-DoXP2Dh1.js → xml-eFi1_vYt.js} +1 -1
  38. package/dist/assets/{yaml-D9YPyGiJ.js → yaml-a7xrJHxm.js} +1 -1
  39. package/dist/cli/index.js +16266 -12653
  40. package/dist/cli/runtime/io.ts +31 -25
  41. package/dist/contracts/definition.d.ts +21 -2
  42. package/dist/contracts/index.d.ts +2 -0
  43. package/dist/contracts/index.js +2 -0
  44. package/dist/contracts/particles.d.ts +54 -0
  45. package/dist/contracts/particles.js +167 -0
  46. package/dist/contracts/scene.d.ts +129 -0
  47. package/dist/contracts/scene.js +73 -0
  48. package/dist/contracts/schema.js +1 -1
  49. package/dist/contracts/validate.js +4 -2
  50. package/dist/contracts/values.d.ts +3 -2
  51. package/dist/contracts/values.js +9 -0
  52. package/dist/index.html +3 -3
  53. package/dist/io.d.ts +4 -16
  54. package/dist/io.js +26 -8
  55. package/dist/player/app.js +810 -0
  56. package/dist/player/embed.d.ts +4 -0
  57. package/dist/player/embed.js +77 -0
  58. package/dist/player/types.d.ts +35 -0
  59. package/dist/runtime/builtins/core/index.d.ts +3 -2
  60. package/dist/runtime/builtins/core/index.js +6 -2
  61. package/dist/runtime/builtins/core/structural.d.ts +168 -0
  62. package/dist/runtime/builtins/core/structural.js +175 -0
  63. package/dist/runtime/builtins/core/time.d.ts +165 -0
  64. package/dist/runtime/builtins/core/time.js +110 -0
  65. package/dist/runtime/builtins/geo/definitions.d.ts +247 -0
  66. package/dist/runtime/builtins/geo/definitions.js +70 -0
  67. package/dist/runtime/builtins/geo/index.d.ts +2 -1
  68. package/dist/runtime/builtins/geo/index.js +3 -1
  69. package/dist/runtime/builtins/geo/render.d.ts +87 -0
  70. package/dist/runtime/builtins/geo/render.js +87 -0
  71. package/dist/runtime/builtins/geo/svgExport.d.ts +11 -0
  72. package/dist/runtime/builtins/geo/svgExport.js +10 -0
  73. package/dist/runtime/builtins/index.d.ts +5 -2
  74. package/dist/runtime/builtins/index.js +13 -3
  75. package/dist/runtime/builtins/pop/definitions.d.ts +2120 -0
  76. package/dist/runtime/builtins/pop/definitions.js +815 -0
  77. package/dist/runtime/builtins/pop/index.d.ts +2 -0
  78. package/dist/runtime/builtins/pop/index.js +2 -0
  79. package/dist/runtime/builtins/scene/definitions.d.ts +356 -0
  80. package/dist/runtime/builtins/scene/definitions.js +224 -0
  81. package/dist/runtime/builtins/scene/index.d.ts +2 -0
  82. package/dist/runtime/builtins/scene/index.js +2 -0
  83. package/dist/runtime/geometry/render.d.ts +109 -0
  84. package/dist/runtime/geometry/render.js +145 -0
  85. package/dist/runtime/geometry/svg.d.ts +29 -0
  86. package/dist/runtime/geometry/svg.js +11 -5
  87. package/dist/runtime/index.d.ts +1 -0
  88. package/dist/runtime/index.js +1 -0
  89. package/dist/runtime/pop/cache.d.ts +27 -0
  90. package/dist/runtime/pop/cache.js +123 -0
  91. package/dist/runtime/pop/forces.d.ts +202 -0
  92. package/dist/runtime/pop/forces.js +519 -0
  93. package/dist/runtime/pop/index.d.ts +5 -0
  94. package/dist/runtime/pop/index.js +5 -0
  95. package/dist/runtime/pop/state.d.ts +136 -0
  96. package/dist/runtime/pop/state.js +283 -0
  97. package/dist/runtime/pop/trail.d.ts +62 -0
  98. package/dist/runtime/pop/trail.js +142 -0
  99. package/dist/runtime/runtime.js +221 -3
  100. package/dist/runtime/scene/index.d.ts +50 -0
  101. package/dist/runtime/scene/index.js +94 -0
  102. package/dist/studio/panel.d.ts +68 -1
  103. package/doc/HEADLESS_GPU.md +4 -4
  104. package/doc/NODE_REFERENCE.md +372 -1
  105. package/doc/PROJECT_AUTHORING.md +16 -4
  106. package/doc/WEB_PLAYER.md +131 -0
  107. package/package.json +16 -2
  108. package/dist/assets/geo-DhyFdBXX.js +0 -1
  109. package/dist/assets/index-CC6YQw2n.js +0 -212
  110. package/dist/assets/nodeTypeUtils-BBA1JqHx.js +0 -1
@@ -0,0 +1,810 @@
1
+ // src/player/app.ts
2
+ import {
3
+ CAMERA_DEFAULTS,
4
+ isScene,
5
+ sceneFromGeometry
6
+ } from "@field/cascade/contracts";
7
+ import { createBrowserRuntimeHost } from "@field/cascade/runtime/browser";
8
+ import { renderScene } from "@field/cascade/runtime/render";
9
+ import { createRuntime, PREFLIGHT_WARNING_CODES } from "@field/cascade/runtime";
10
+ import { sceneBounds } from "@field/cascade/runtime/scene";
11
+ import { installIoBridge } from "@field/cascade/io";
12
+
13
+ // src/browser/gpu.ts
14
+ import {
15
+ createGpuHost
16
+ } from "@field/cascade/runtime";
17
+ function createBrowserGpuHost() {
18
+ const gpu = globalThis.navigator?.gpu;
19
+ if (!gpu) return void 0;
20
+ return createGpuHost({
21
+ async acquire() {
22
+ const adapter = await gpu.requestAdapter();
23
+ if (!adapter) {
24
+ throw new Error(
25
+ "No WebGPU adapter is available \u2014 the browser reports WebGPU but the system offers no GPU to render on."
26
+ );
27
+ }
28
+ const device = await adapter.requestDevice();
29
+ const info = adapter.info;
30
+ return {
31
+ device,
32
+ info: {
33
+ vendor: info?.vendor ?? "",
34
+ architecture: info?.architecture ?? "",
35
+ device: info?.device ?? "",
36
+ description: info?.description ?? ""
37
+ },
38
+ limits: device.limits ?? adapter.limits
39
+ };
40
+ }
41
+ });
42
+ }
43
+
44
+ // src/player/assets.ts
45
+ var DEFAULT_MAX_BYTES = 256 * 1024 * 1024;
46
+ var DEFAULT_MAX_ENTRIES = 4096;
47
+ var GENERATED_ROOT = ".cascade-player/generated";
48
+ var PlayerAssetStore = class {
49
+ constructor(assets, options = {}) {
50
+ this.authored = /* @__PURE__ */ new Map();
51
+ this.generated = /* @__PURE__ */ new Map();
52
+ this.generatedByUrl = /* @__PURE__ */ new Map();
53
+ this.generatedBytes = 0;
54
+ this.sequence = 0;
55
+ this.disposed = false;
56
+ this.maxGeneratedBytes = finiteLimit(
57
+ options.maxGeneratedBytes,
58
+ DEFAULT_MAX_BYTES,
59
+ "maxGeneratedBytes"
60
+ );
61
+ this.maxGeneratedEntries = finiteLimit(
62
+ options.maxGeneratedEntries,
63
+ DEFAULT_MAX_ENTRIES,
64
+ "maxGeneratedEntries"
65
+ );
66
+ for (const [path, url] of Object.entries(assets)) {
67
+ const clean = cleanPath(path);
68
+ if (typeof url !== "string" || url.length === 0)
69
+ throw new TypeError(`Player asset ${clean} has no deployment URL`);
70
+ this.authored.set(clean, url);
71
+ }
72
+ }
73
+ async read(ref, options = {}) {
74
+ this.assertActive();
75
+ const path = cleanPath(typeof ref === "string" ? ref : ref.path);
76
+ if (typeof ref === "string") assertIdentityMediaOptions(path, options);
77
+ const signal = "signal" in options ? options.signal : void 0;
78
+ if (signal?.aborted) throw abortError(signal.reason);
79
+ const generated = this.generated.get(path);
80
+ if (generated) return generated.bytes.slice();
81
+ const url = this.authored.get(path);
82
+ if (!url) throw new Error(`Player asset not found: ${path}`);
83
+ const relay = signal ? relayAbort(signal) : void 0;
84
+ try {
85
+ const response = await fetch(url, {
86
+ ...relay ? { signal: relay.signal } : {}
87
+ });
88
+ if (!response.ok)
89
+ throw new Error(`Could not read player asset ${path} (${response.status})`);
90
+ return new Uint8Array(await response.arrayBuffer());
91
+ } finally {
92
+ relay?.dispose();
93
+ }
94
+ }
95
+ async write(first, second, third) {
96
+ this.assertActive();
97
+ const fromIo = typeof first === "string";
98
+ const data = fromIo ? second : first;
99
+ if (!(data instanceof Uint8Array))
100
+ throw new TypeError("Player asset writes require Uint8Array bytes");
101
+ const metadata = fromIo ? { suggestedName: first, mediaType: mediaTypeForName(first) } : second;
102
+ if (third?.signal.aborted) throw abortError(third.signal.reason);
103
+ const name = safeName(metadata.suggestedName);
104
+ const path = `${GENERATED_ROOT}/${String(++this.sequence).padStart(8, "0")}-${name}`;
105
+ const nextBytes = this.generatedBytes + data.byteLength;
106
+ const nextEntries = this.generated.size + 1;
107
+ if (nextBytes > this.maxGeneratedBytes) {
108
+ throw new Error(
109
+ `Player asset ${path} would use ${nextBytes} bytes; limit is ${this.maxGeneratedBytes} bytes`
110
+ );
111
+ }
112
+ if (nextEntries > this.maxGeneratedEntries) {
113
+ throw new Error(
114
+ `Player asset ${path} would use ${nextEntries} entries; limit is ${this.maxGeneratedEntries} entries`
115
+ );
116
+ }
117
+ const stored = data.slice();
118
+ this.generated.set(path, {
119
+ bytes: stored,
120
+ ...metadata.mediaType ? { mediaType: metadata.mediaType } : {},
121
+ leases: 0
122
+ });
123
+ this.generatedBytes = nextBytes;
124
+ return fromIo ? path : { path, ...metadata.mediaType ? { mediaType: metadata.mediaType } : {} };
125
+ }
126
+ url(path, options = {}) {
127
+ this.assertActive();
128
+ assertIdentityMediaOptions(path, options);
129
+ const clean = cleanPath(path);
130
+ const authored = this.authored.get(clean);
131
+ if (authored) return authored;
132
+ const generated = this.generated.get(clean);
133
+ if (!generated) throw new Error(`Player asset not found: ${clean}`);
134
+ if (!generated.objectUrl) {
135
+ generated.objectUrl = URL.createObjectURL(blobFor(generated));
136
+ this.generatedByUrl.set(generated.objectUrl, clean);
137
+ }
138
+ return generated.objectUrl;
139
+ }
140
+ async resolveUrl(ref) {
141
+ this.assertActive();
142
+ const path = cleanPath(ref.path);
143
+ const generated = this.generated.get(path);
144
+ if (!generated) {
145
+ const authored = this.authored.get(path);
146
+ if (!authored) throw new Error(`Player asset not found: ${path}`);
147
+ return { value: authored, release: () => {
148
+ } };
149
+ }
150
+ generated.leases += 1;
151
+ let value;
152
+ try {
153
+ value = this.url(path, { raw: true });
154
+ } catch (error) {
155
+ generated.leases -= 1;
156
+ throw error;
157
+ }
158
+ let released = false;
159
+ return {
160
+ value,
161
+ release: () => {
162
+ if (released) return;
163
+ released = true;
164
+ generated.leases = Math.max(0, generated.leases - 1);
165
+ }
166
+ };
167
+ }
168
+ /** Remove generated entries that no settled graph value or display lease owns. */
169
+ collect(roots) {
170
+ if (this.disposed) return;
171
+ const reachable = /* @__PURE__ */ new Set();
172
+ const seen = /* @__PURE__ */ new Set();
173
+ for (const root of roots)
174
+ visitPaths(root, reachable, seen, this.generated, this.generatedByUrl);
175
+ const errors = [];
176
+ for (const [path, entry] of this.generated) {
177
+ if (reachable.has(path) || entry.leases > 0) continue;
178
+ try {
179
+ this.drop(path, entry);
180
+ } catch (error) {
181
+ errors.push(error);
182
+ }
183
+ }
184
+ throwCollectedErrors(errors, "Player asset collection failed");
185
+ }
186
+ stats() {
187
+ return {
188
+ generatedBytes: this.generatedBytes,
189
+ generatedEntries: this.generated.size,
190
+ objectUrls: this.generatedByUrl.size
191
+ };
192
+ }
193
+ dispose() {
194
+ if (this.disposed) return;
195
+ this.disposed = true;
196
+ const errors = [];
197
+ for (const [path, entry] of this.generated) {
198
+ try {
199
+ this.drop(path, entry);
200
+ } catch (error) {
201
+ errors.push(error);
202
+ }
203
+ }
204
+ throwCollectedErrors(errors, "Player asset disposal failed");
205
+ }
206
+ drop(path, entry) {
207
+ try {
208
+ if (entry.objectUrl) URL.revokeObjectURL(entry.objectUrl);
209
+ } finally {
210
+ if (entry.objectUrl) this.generatedByUrl.delete(entry.objectUrl);
211
+ this.generated.delete(path);
212
+ this.generatedBytes -= entry.bytes.byteLength;
213
+ }
214
+ }
215
+ assertActive() {
216
+ if (this.disposed) throw new Error("Player asset store is disposed");
217
+ }
218
+ };
219
+ function finiteLimit(value, fallback, name) {
220
+ const resolved = value ?? fallback;
221
+ if (!Number.isSafeInteger(resolved) || resolved < 0)
222
+ throw new TypeError(`${name} must be a non-negative safe integer`);
223
+ return resolved;
224
+ }
225
+ function cleanPath(path) {
226
+ const clean = path.replace(/^\.\//, "").replace(/\\/g, "/");
227
+ if (!clean || clean.startsWith("/") || clean.split("/").some((part) => part === ".."))
228
+ throw new Error(`Invalid player asset path: ${path}`);
229
+ return clean;
230
+ }
231
+ function safeName(value) {
232
+ const source = (value?.trim() || "asset.bin").replace(/\\/g, "/").split("/").pop();
233
+ const safe = source.replace(/[^A-Za-z0-9._-]/g, "-").replace(/^\.+/, "");
234
+ return safe || "asset.bin";
235
+ }
236
+ function assertIdentityMediaOptions(path, options) {
237
+ if (options.width !== void 0 || options.format !== void 0 || options.quality !== void 0)
238
+ throw new Error(`Player asset transforms are unsupported for ${path}; request the raw asset`);
239
+ }
240
+ function blobFor(entry) {
241
+ return new Blob([entry.bytes.slice().buffer], {
242
+ type: entry.mediaType ?? "application/octet-stream"
243
+ });
244
+ }
245
+ function visitPaths(value, reachable, seen, generated, generatedByUrl) {
246
+ if (typeof value === "string") {
247
+ const path = value.replace(/^\.\//, "");
248
+ if (generated.has(path)) reachable.add(path);
249
+ else {
250
+ const generatedPath = generatedByUrl.get(value);
251
+ if (generatedPath) reachable.add(generatedPath);
252
+ }
253
+ return;
254
+ }
255
+ if (typeof value !== "object" || value === null) return;
256
+ if (ArrayBuffer.isView(value) || value instanceof ArrayBuffer) return;
257
+ if (seen.has(value)) return;
258
+ seen.add(value);
259
+ if ("path" in value && typeof value.path === "string") {
260
+ const path = value.path.replace(/^\.\//, "");
261
+ if (generated.has(path)) reachable.add(path);
262
+ }
263
+ if (Array.isArray(value)) {
264
+ for (const item of value)
265
+ visitPaths(item, reachable, seen, generated, generatedByUrl);
266
+ return;
267
+ }
268
+ const prototype = Object.getPrototypeOf(value);
269
+ if (prototype !== Object.prototype && prototype !== null) return;
270
+ for (const item of Object.values(value))
271
+ visitPaths(item, reachable, seen, generated, generatedByUrl);
272
+ }
273
+ function relayAbort(signal) {
274
+ const controller = new AbortController();
275
+ const abort = () => controller.abort(signal.reason);
276
+ signal.addEventListener("abort", abort, { once: true });
277
+ if (signal.aborted) abort();
278
+ return {
279
+ signal: controller.signal,
280
+ dispose: () => signal.removeEventListener("abort", abort)
281
+ };
282
+ }
283
+ function abortError(reason) {
284
+ return reason instanceof Error ? reason : new Error(reason === void 0 ? "Aborted" : String(reason));
285
+ }
286
+ function mediaTypeForName(name) {
287
+ if (/\.png$/i.test(name)) return "image/png";
288
+ if (/\.svg$/i.test(name)) return "image/svg+xml";
289
+ if (/\.jpe?g$/i.test(name)) return "image/jpeg";
290
+ if (/\.webp$/i.test(name)) return "image/webp";
291
+ return void 0;
292
+ }
293
+ function throwCollectedErrors(errors, message) {
294
+ if (errors.length === 1) throw errors[0];
295
+ if (errors.length > 1) {
296
+ const combined = new Error(message);
297
+ Object.assign(combined, { errors });
298
+ throw combined;
299
+ }
300
+ }
301
+
302
+ // src/player/app.ts
303
+ async function startPlayer(options) {
304
+ const cascadeDocument = options.document;
305
+ const hostDocument = options.root?.ownerDocument ?? globalThis.document;
306
+ const root = options.root ?? hostDocument.body;
307
+ if (!root) throw new Error("Cascade player needs a document body or explicit root");
308
+ const view = hostDocument.defaultView;
309
+ const initial = readInitialState(view?.location.href);
310
+ const ui = createUi(hostDocument, root);
311
+ let assets;
312
+ let disposeIo;
313
+ let gpu;
314
+ let runtime;
315
+ let graph;
316
+ try {
317
+ assets = new PlayerAssetStore(options.assets);
318
+ disposeIo = installIoBridge(assets);
319
+ gpu = createBrowserGpuHost();
320
+ const registrations = new Map(options.registrations.map((item) => [item.moduleId, item]));
321
+ runtime = createRuntime({
322
+ host: createBrowserRuntimeHost({
323
+ modules: { resolve: async (moduleId) => registrations.get(moduleId) ?? null },
324
+ assets,
325
+ ...gpu ? { gpu } : {},
326
+ report: (diagnostic) => {
327
+ if (diagnostic.phase === "run") ui.error.textContent = diagnostic.message;
328
+ }
329
+ }),
330
+ nodes: options.registrations
331
+ });
332
+ graph = await runtime.load(cascadeDocument);
333
+ const preflight = graph.preflight();
334
+ const blocking = preflight.find((diagnostic) => !PREFLIGHT_WARNING_CODES.has(diagnostic.code));
335
+ if (blocking) throw new Error(blocking.message);
336
+ const controller = new BrowserPlayer({
337
+ graph,
338
+ runtime,
339
+ gpu,
340
+ assets,
341
+ disposeIo,
342
+ root,
343
+ ui,
344
+ document: cascadeDocument,
345
+ initial
346
+ });
347
+ await controller.start();
348
+ return controller;
349
+ } catch (cause) {
350
+ const error = asError(cause);
351
+ ui.error.textContent = error.message;
352
+ ui.error.hidden = false;
353
+ try {
354
+ await cleanupAll([
355
+ () => graph?.dispose(),
356
+ () => runtime?.dispose(),
357
+ () => gpu?.dispose(),
358
+ () => disposeIo?.(),
359
+ () => assets?.dispose()
360
+ ]);
361
+ } catch (cleanupError) {
362
+ Object.assign(error, { cleanupError });
363
+ }
364
+ throw error;
365
+ }
366
+ }
367
+ var BrowserPlayer = class {
368
+ constructor(context) {
369
+ this.context = context;
370
+ this.listeners = /* @__PURE__ */ new Set();
371
+ this.controllerRoots = /* @__PURE__ */ new Map();
372
+ this.settledRoots = [];
373
+ this.commandTail = Promise.resolve();
374
+ this.playing = false;
375
+ this.playbackAnchor = { frame: 1, time: 0 };
376
+ this.disposed = false;
377
+ this.togglePlayback = () => {
378
+ if (this.playing) this.pause();
379
+ else this.play();
380
+ };
381
+ this.commitFrame = () => {
382
+ const frame = Number(this.context.ui.frame.value);
383
+ if (Number.isFinite(frame)) void this.seek(frame).catch((error) => this.fail(asError(error)));
384
+ };
385
+ this.selected = context.initial.output;
386
+ this.width = Math.max(1, context.root.clientWidth || 1280);
387
+ this.height = Math.max(1, context.root.clientHeight || 720);
388
+ this.frameWindow = context.root.ownerDocument.defaultView;
389
+ context.graph.setFrame(context.initial.frame);
390
+ context.graph.setFps(context.initial.fps);
391
+ context.ui.play.addEventListener("click", this.togglePlayback);
392
+ context.ui.frame.addEventListener("change", this.commitFrame);
393
+ }
394
+ async start() {
395
+ await this.cook(this.context.initial.frame);
396
+ if (this.context.initial.autoplay) this.play();
397
+ }
398
+ play() {
399
+ this.assertActive();
400
+ if (this.playing) return;
401
+ this.playing = true;
402
+ this.context.ui.play.textContent = "Pause";
403
+ this.emit({ type: "playing", playing: true });
404
+ this.playbackAnchor = { frame: this.context.graph.getFrame(), time: this.now() };
405
+ const tick = (time) => {
406
+ if (!this.playing || this.disposed) return;
407
+ const elapsed = Math.max(0, time - this.playbackAnchor.time);
408
+ const wanted = this.playbackAnchor.frame + elapsed * this.context.graph.getFps() / 1e3;
409
+ this.requestPlaybackFrame(wanted);
410
+ this.animationFrame = this.requestAnimationFrame(tick);
411
+ };
412
+ this.animationFrame = this.requestAnimationFrame(tick);
413
+ }
414
+ pause() {
415
+ if (!this.playing) return;
416
+ this.playing = false;
417
+ this.pendingPlaybackFrame = void 0;
418
+ if (this.animationFrame !== void 0) this.cancelAnimationFrame(this.animationFrame);
419
+ this.animationFrame = void 0;
420
+ this.context.ui.play.textContent = "Play";
421
+ this.emit({ type: "playing", playing: false });
422
+ }
423
+ seek(frame) {
424
+ requireFinite(frame, "frame");
425
+ return this.enqueue(async () => {
426
+ await this.cook(frame);
427
+ this.pendingPlaybackFrame = void 0;
428
+ this.playbackAnchor = { frame: this.context.graph.getFrame(), time: this.now() };
429
+ });
430
+ }
431
+ setInput(nodeId, name, value) {
432
+ return this.enqueue(async () => {
433
+ await this.context.graph.setInput(nodeId, name, value);
434
+ this.controllerRoots.set(`input\0${nodeId}\0${name}`, value);
435
+ await this.cook(this.context.graph.getFrame());
436
+ });
437
+ }
438
+ setProp(nodeId, name, value) {
439
+ return this.enqueue(async () => {
440
+ await this.context.graph.setProp(nodeId, name, value);
441
+ this.controllerRoots.set(`prop\0${nodeId}\0${name}`, value);
442
+ await this.cook(this.context.graph.getFrame());
443
+ });
444
+ }
445
+ getOutput(nodeId, port) {
446
+ this.assertActive();
447
+ return this.context.graph.getOutput(nodeId, port);
448
+ }
449
+ selectOutput(nodeId, port) {
450
+ return this.enqueue(async () => {
451
+ if (!this.context.graph.getOutputs(nodeId).has(port))
452
+ throw new Error(`Player output not found: ${nodeId}.${port}`);
453
+ const previous = this.selected;
454
+ this.selected = { nodeId, port };
455
+ try {
456
+ await this.display(this.context.graph.getOutput(nodeId, port));
457
+ } catch (cause) {
458
+ this.selected = previous;
459
+ const error = asError(cause);
460
+ this.fail(error);
461
+ throw error;
462
+ }
463
+ this.collect();
464
+ });
465
+ }
466
+ async downloadOutput() {
467
+ this.assertActive();
468
+ const value = this.currentOutput();
469
+ if (value instanceof Blob) return value;
470
+ if (isAssetRef(value)) {
471
+ const bytes = await this.context.assets.read(value, { signal: new AbortController().signal });
472
+ const mediaType = "mediaType" in value ? value.mediaType : void 0;
473
+ return new Blob([bytes.slice().buffer], { type: mediaType ?? mediaTypeForName(value.path) ?? "application/octet-stream" });
474
+ }
475
+ if (typeof value === "string") {
476
+ return new Blob([value], {
477
+ type: value.trimStart().startsWith("<svg") ? "image/svg+xml" : "text/plain;charset=utf-8"
478
+ });
479
+ }
480
+ if ((isGeometry(value) || isScene(value)) && this.displayedElement instanceof HTMLCanvasElement)
481
+ return canvasBlob(this.displayedElement);
482
+ if (value instanceof HTMLCanvasElement) return canvasBlob(value);
483
+ return new Blob([JSON.stringify(value, jsonReplacer, 2)], { type: "application/json" });
484
+ }
485
+ resize(width, height) {
486
+ requireFinite(width, "width");
487
+ requireFinite(height, "height");
488
+ this.width = Math.max(1, Math.round(width));
489
+ this.height = Math.max(1, Math.round(height));
490
+ void this.enqueue(async () => this.display(this.currentOutput())).catch((error) => this.fail(asError(error)));
491
+ }
492
+ subscribe(listener) {
493
+ this.assertActive();
494
+ this.listeners.add(listener);
495
+ return () => this.listeners.delete(listener);
496
+ }
497
+ async dispose() {
498
+ if (this.disposed) return;
499
+ this.disposed = true;
500
+ this.pause();
501
+ this.context.graph.cancel("Player disposed");
502
+ await this.commandTail.catch(() => {
503
+ });
504
+ await this.playbackCook?.catch(() => {
505
+ });
506
+ this.context.ui.play.removeEventListener("click", this.togglePlayback);
507
+ this.context.ui.frame.removeEventListener("change", this.commitFrame);
508
+ this.listeners.clear();
509
+ try {
510
+ await cleanupAll([
511
+ () => this.releaseDisplay(),
512
+ () => this.context.graph.dispose(),
513
+ () => this.context.runtime.dispose(),
514
+ () => this.context.gpu?.dispose(),
515
+ () => this.context.disposeIo(),
516
+ () => this.context.assets.dispose()
517
+ ]);
518
+ } finally {
519
+ this.context.root.replaceChildren();
520
+ }
521
+ }
522
+ enqueue(operation) {
523
+ this.assertActive();
524
+ const scheduled = this.commandTail.then(operation);
525
+ this.commandTail = scheduled.catch(() => {
526
+ });
527
+ return scheduled;
528
+ }
529
+ requestPlaybackFrame(frame) {
530
+ this.pendingPlaybackFrame = frame;
531
+ if (this.playbackCook) return;
532
+ this.playbackCook = this.enqueue(async () => {
533
+ const next = this.pendingPlaybackFrame;
534
+ this.pendingPlaybackFrame = void 0;
535
+ if (!this.disposed && this.playing && next !== void 0) await this.cook(next);
536
+ }).catch(() => {
537
+ }).finally(() => {
538
+ this.playbackCook = void 0;
539
+ if (this.playing && this.pendingPlaybackFrame !== void 0)
540
+ this.requestPlaybackFrame(this.pendingPlaybackFrame);
541
+ });
542
+ }
543
+ async cook(frame) {
544
+ this.assertActive();
545
+ let result;
546
+ try {
547
+ result = await this.context.graph.run({ frame, fps: this.context.graph.getFps() });
548
+ this.settledRoots = outputValues(result);
549
+ if (result.status !== "completed") throw resultError(result);
550
+ if (!this.selected) this.selected = preferredOutput(result);
551
+ if (this.selected && !result.outputs.get(this.selected.nodeId)?.has(this.selected.port))
552
+ throw new Error(`Player output not found: ${this.selected.nodeId}.${this.selected.port}`);
553
+ await this.display(this.currentOutput());
554
+ const effectiveFrame = this.context.graph.getFrame();
555
+ this.context.ui.frame.value = String(effectiveFrame);
556
+ this.context.ui.error.hidden = true;
557
+ this.context.ui.error.textContent = "";
558
+ this.emit({ type: "frame", frame: effectiveFrame });
559
+ } catch (cause) {
560
+ const error = asError(cause);
561
+ this.fail(error);
562
+ this.collect();
563
+ throw error;
564
+ }
565
+ this.collect();
566
+ }
567
+ currentOutput() {
568
+ return this.selected ? this.context.graph.getOutput(this.selected.nodeId, this.selected.port) : void 0;
569
+ }
570
+ async display(value) {
571
+ const output = this.context.ui.output;
572
+ let next;
573
+ let nextRelease;
574
+ const asset = isAssetRef(value) ? value : void 0;
575
+ if (asset || typeof value === "string" && value.trimStart().startsWith("<svg")) {
576
+ let url;
577
+ if (asset) {
578
+ const lease = await this.context.assets.resolveUrl(asset);
579
+ url = lease.value;
580
+ nextRelease = lease.release;
581
+ } else {
582
+ url = URL.createObjectURL(new Blob([value], { type: "image/svg+xml" }));
583
+ nextRelease = () => URL.revokeObjectURL(url);
584
+ }
585
+ try {
586
+ if (!asset || isImageAsset(asset)) {
587
+ const image = output.ownerDocument.createElement("img");
588
+ image.src = url;
589
+ image.alt = this.selected ? `${this.selected.nodeId}.${this.selected.port}` : asset ? "Cascade output" : "Cascade SVG output";
590
+ await image.decode();
591
+ next = image;
592
+ } else {
593
+ const link = output.ownerDocument.createElement("a");
594
+ link.href = url;
595
+ link.download = asset.path.split("/").pop() || "asset";
596
+ link.textContent = asset.path;
597
+ next = link;
598
+ }
599
+ } catch (error) {
600
+ await nextRelease();
601
+ throw error;
602
+ }
603
+ } else if (isScene(value) || isGeometry(value)) {
604
+ const scene = isScene(value) ? value : sceneFromGeometry(value);
605
+ next = renderSceneCanvas(output.ownerDocument, scene, this.width, this.height);
606
+ } else {
607
+ const pre = output.ownerDocument.createElement("pre");
608
+ pre.textContent = value === void 0 ? "No output" : JSON.stringify(value, jsonReplacer, 2);
609
+ next = pre;
610
+ }
611
+ try {
612
+ await this.releaseDisplay();
613
+ } catch (error) {
614
+ await nextRelease?.();
615
+ throw error;
616
+ }
617
+ if (next.tagName === "IMG" || next.tagName === "CANVAS") {
618
+ Object.assign(next.style, { display: "block", maxWidth: "100%", maxHeight: "100%", objectFit: "contain", minWidth: "0", minHeight: "0" });
619
+ }
620
+ output.replaceChildren(next);
621
+ this.displayedElement = next;
622
+ this.displayRelease = nextRelease;
623
+ }
624
+ async releaseDisplay() {
625
+ const release = this.displayRelease;
626
+ this.displayRelease = void 0;
627
+ this.displayedElement = void 0;
628
+ await release?.();
629
+ }
630
+ collect() {
631
+ this.context.assets.collect([
632
+ this.context.document,
633
+ ...this.controllerRoots.values(),
634
+ ...this.settledRoots
635
+ ]);
636
+ }
637
+ fail(error) {
638
+ this.pause();
639
+ this.context.ui.error.hidden = false;
640
+ this.context.ui.error.textContent = error.message;
641
+ this.emit({ type: "error", error });
642
+ }
643
+ emit(event) {
644
+ for (const listener of this.listeners) {
645
+ try {
646
+ listener(event);
647
+ } catch (error) {
648
+ console.error("Cascade player listener failed", error);
649
+ }
650
+ }
651
+ }
652
+ now() {
653
+ return this.frameWindow?.performance.now() ?? performance.now();
654
+ }
655
+ requestAnimationFrame(callback) {
656
+ if (this.frameWindow?.requestAnimationFrame)
657
+ return this.frameWindow.requestAnimationFrame(callback);
658
+ return globalThis.setTimeout(() => callback(this.now()), 16);
659
+ }
660
+ cancelAnimationFrame(handle) {
661
+ if (this.frameWindow?.cancelAnimationFrame) this.frameWindow.cancelAnimationFrame(handle);
662
+ else globalThis.clearTimeout(handle);
663
+ }
664
+ assertActive() {
665
+ if (this.disposed) throw new Error("Cascade player is disposed");
666
+ }
667
+ };
668
+ function createUi(hostDocument, root) {
669
+ root.replaceChildren();
670
+ root.dataset.cascadePlayer = "";
671
+ Object.assign(root.style, { display: "flex", flexDirection: "column", height: "100%", margin: "0", background: "#141414", color: "#eee", overflow: "hidden" });
672
+ const toolbar = hostDocument.createElement("div");
673
+ toolbar.dataset.cascadeControls = "";
674
+ Object.assign(toolbar.style, { display: "flex", gap: "8px", padding: "8px", flex: "none", alignItems: "center" });
675
+ const play = hostDocument.createElement("button");
676
+ play.type = "button";
677
+ play.textContent = "Play";
678
+ const frame = hostDocument.createElement("input");
679
+ frame.type = "number";
680
+ frame.step = "any";
681
+ frame.setAttribute("aria-label", "Frame");
682
+ frame.style.width = "100px";
683
+ const error = hostDocument.createElement("div");
684
+ error.dataset.cascadeError = "";
685
+ error.setAttribute("role", "alert");
686
+ error.hidden = true;
687
+ Object.assign(error.style, { padding: "8px", whiteSpace: "pre-wrap", overflow: "auto" });
688
+ const output = hostDocument.createElement("div");
689
+ output.dataset.cascadeOutput = "";
690
+ Object.assign(output.style, { display: "grid", placeItems: "center", flex: "1", minHeight: "0", minWidth: "0", overflow: "auto" });
691
+ toolbar.append(play, frame);
692
+ root.append(toolbar, error, output);
693
+ return { play, frame, error, output };
694
+ }
695
+ function renderSceneCanvas(hostDocument, scene, width, height) {
696
+ let canvas;
697
+ const camera = scene.camera ?? fitCamera(scene, width, height);
698
+ renderScene(scene.geometry, {
699
+ camera,
700
+ size: [width, height],
701
+ stroke: [1, 1, 1, 1],
702
+ strokeWidth: 1,
703
+ opacity: 1,
704
+ pointRadius: 1,
705
+ drawPoints: true,
706
+ surface: (surfaceWidth, surfaceHeight) => {
707
+ canvas = hostDocument.createElement("canvas");
708
+ canvas.width = surfaceWidth;
709
+ canvas.height = surfaceHeight;
710
+ return canvas;
711
+ }
712
+ });
713
+ return canvas;
714
+ }
715
+ function fitCamera(scene, width, height) {
716
+ const bounds = sceneBounds(scene);
717
+ if (!bounds) return { ...CAMERA_DEFAULTS, projection: "orthographic", resolution: [width, height] };
718
+ const centerX = ((bounds.min[0] ?? 0) + (bounds.max[0] ?? 0)) / 2;
719
+ const centerY = ((bounds.min[1] ?? 0) + (bounds.max[1] ?? 0)) / 2;
720
+ const maxZ = bounds.max[2] ?? 0;
721
+ const spanX = (bounds.max[0] ?? 0) - (bounds.min[0] ?? 0);
722
+ const spanY = (bounds.max[1] ?? 0) - (bounds.min[1] ?? 0);
723
+ const frameAspect = width / height;
724
+ return {
725
+ ...CAMERA_DEFAULTS,
726
+ translate: [centerX, centerY, maxZ + Math.max(spanX, spanY, 1)],
727
+ projection: "orthographic",
728
+ orthowidth: Math.max(spanX, spanY * frameAspect, 1) * 1.1,
729
+ resolution: [width, height]
730
+ };
731
+ }
732
+ function readInitialState(href) {
733
+ const query = new URL(href ?? "http://localhost/").searchParams;
734
+ const frame = finiteQuery(query.get("frame"), 1);
735
+ const fps = finiteQuery(query.get("fps"), 30);
736
+ const nodeId = query.get("outputNode");
737
+ const port = query.get("outputPort");
738
+ return {
739
+ frame,
740
+ fps,
741
+ autoplay: query.get("autoplay") === "1",
742
+ ...nodeId && port ? { output: { nodeId, port } } : {}
743
+ };
744
+ }
745
+ function finiteQuery(value, fallback) {
746
+ if (value === null || value.trim() === "") return fallback;
747
+ const parsed = Number(value);
748
+ return Number.isFinite(parsed) ? parsed : fallback;
749
+ }
750
+ function preferredOutput(result) {
751
+ let fallback;
752
+ let preferred;
753
+ for (const [nodeId, outputs] of result.outputs) {
754
+ for (const [port, value] of outputs) {
755
+ fallback = { nodeId, port };
756
+ if (isAssetRef(value) || isScene(value) || isGeometry(value) || typeof value === "string" && value.trimStart().startsWith("<svg"))
757
+ preferred = { nodeId, port };
758
+ }
759
+ }
760
+ return preferred ?? fallback;
761
+ }
762
+ function outputValues(result) {
763
+ const values = [];
764
+ for (const outputs of result.outputs.values())
765
+ for (const value of outputs.values()) values.push(value);
766
+ return values;
767
+ }
768
+ function resultError(result) {
769
+ const details = result.diagnostics.map((diagnostic) => diagnostic.message).filter(Boolean);
770
+ return new Error(details.join("\n") || `Player cook ${result.status}`);
771
+ }
772
+ function isGeometry(value) {
773
+ return typeof value === "object" && value !== null && value.kind === "geometry";
774
+ }
775
+ function isAssetRef(value) {
776
+ return typeof value === "object" && value !== null && typeof value.path === "string";
777
+ }
778
+ function isImageAsset(value) {
779
+ if ("size" in value && Array.isArray(value.size)) return true;
780
+ if ("mediaType" in value && value.mediaType?.startsWith("image/")) return true;
781
+ return /\.(png|svg|jpe?g|webp|gif|avif)$/i.test(value.path);
782
+ }
783
+ function jsonReplacer(_key, value) {
784
+ return ArrayBuffer.isView(value) ? Array.from(value) : value;
785
+ }
786
+ function canvasBlob(canvas) {
787
+ return new Promise((resolve, reject) => {
788
+ canvas.toBlob((blob) => blob ? resolve(blob) : reject(new Error("Canvas produced no image")), "image/png");
789
+ });
790
+ }
791
+ async function cleanupAll(actions) {
792
+ const errors = [];
793
+ for (const action of actions) {
794
+ try {
795
+ await action();
796
+ } catch (error) {
797
+ errors.push(error);
798
+ }
799
+ }
800
+ throwCollectedErrors(errors, "Cascade player cleanup failed");
801
+ }
802
+ function requireFinite(value, label) {
803
+ if (!Number.isFinite(value)) throw new TypeError(`${label} must be finite`);
804
+ }
805
+ function asError(value) {
806
+ return value instanceof Error ? value : new Error(String(value));
807
+ }
808
+ export {
809
+ startPlayer
810
+ };